diff --git a/scheduler/jobs/helpers.py b/scheduler/jobs/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..7323cb6d3a3e11bf074c404827b306597960a180 --- /dev/null +++ b/scheduler/jobs/helpers.py @@ -0,0 +1,70 @@ +# (c) 2020 MPIB , +# +# This file is part of castellum-scheduler. +# +# castellum-scheduler is free software; you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# Castellum is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with Castellum. If not, see +# . + +from time import sleep + +from django.db import DatabaseError + +from .models import Job + + +def request(data, timeout=10, interval=1): + job = Job.objects.create(data=data) + t = 0 + while True: + sleep(interval) + t += interval + job = Job.objects.get(pk=job.pk) + + if job.state == Job.FINISHED: + job.delete() + return True + if job.state == Job.ERROR: + job.delete() + return False + if t > timeout: + job.delete() + raise TimeoutError + + +def listen(interval=1): + while True: + for job in Job.objects.filter(state=Job.NEW): + try: + job.state = Job.STARTED + job.save(force_update=True) + except DatabaseError: + # Job has already timed out + continue + yield job.pk, job.data + + sleep(interval) + + +def response(pk, success): + try: + job = Job.objects.get(pk=pk, state=Job.STARTED) + except Job.DoesNotExist: + raise KeyError + + job.state = Job.FINISHED if success else Job.ERROR + try: + job.save(force_update=True) + except DatabaseError: + # Job has already timed out + pass diff --git a/scheduler/jobs/migrations/0001_initial.py b/scheduler/jobs/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..411fc2ba69d374116f8c3a91047ea0bf95c45324 --- /dev/null +++ b/scheduler/jobs/migrations/0001_initial.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.6 on 2021-08-24 15:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Job', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('data', models.TextField(blank=True)), + ('state', models.SmallIntegerField(choices=[(0, 'new'), (1, 'started'), (2, 'finished'), (3, 'errored')], default=0)), + ], + ), + ] diff --git a/scheduler/jobs/migrations/__init__.py b/scheduler/jobs/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/scheduler/jobs/models.py b/scheduler/jobs/models.py new file mode 100644 index 0000000000000000000000000000000000000000..95a6a780b6d1518b600d58116f1d36b1dd062bea --- /dev/null +++ b/scheduler/jobs/models.py @@ -0,0 +1,34 @@ +# (c) 2020 MPIB , +# +# This file is part of castellum-scheduler. +# +# castellum-scheduler is free software; you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# Castellum is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with Castellum. If not, see +# . + +from django.db import models + + +class Job(models.Model): + NEW = 0 + STARTED = 1 + FINISHED = 2 + ERROR = 3 + + data = models.TextField(blank=True) + state = models.SmallIntegerField(choices=[ + (NEW, 'new'), + (STARTED, 'started'), + (FINISHED, 'finished'), + (ERROR, 'errored'), + ], default=NEW) diff --git a/scheduler/jobs/views.py b/scheduler/jobs/views.py new file mode 100644 index 0000000000000000000000000000000000000000..2b7e20dbae877409079732a739ac18fc621da011 --- /dev/null +++ b/scheduler/jobs/views.py @@ -0,0 +1,50 @@ +# (c) 2020 MPIB , +# +# This file is part of castellum-scheduler. +# +# castellum-scheduler is free software; you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# Castellum is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with Castellum. If not, see +# . + +from django.conf import settings +from django.core.exceptions import PermissionDenied +from django.http import Http404 +from django.http import HttpResponse +from django.http import StreamingHttpResponse +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import View + +from . import helpers + + +@method_decorator(csrf_exempt, 'dispatch') +class JobView(View): + def dispatch(self, request, *args, **kwargs): + if request.headers.get('Authorization') != 'token ' + settings.API_TOKEN: + raise PermissionDenied + return super().dispatch(request, *args, **kwargs) + + def stream(self): + for id, data in helpers.listen(): + yield 'id: {}\ndata: {}\n\n'.format(id, data) + + def get(self, request, *args, **kwargs): + return StreamingHttpResponse(self.stream(), content_type='text/event-stream') + + def post(self, request, *args, **kwargs): + try: + helpers.response(request.GET['id'], 'success' in request.GET) + except KeyError: + raise Http404 + return HttpResponse(status=204) diff --git a/scheduler/main/views.py b/scheduler/main/views.py index be429cac7d0d42f8e457b205a151a90a76b1661c..87253f81c806a798abc97ec0c6e7d962e62de752 100644 --- a/scheduler/main/views.py +++ b/scheduler/main/views.py @@ -16,6 +16,7 @@ # License along with Castellum. If not, see # . +import json from collections import OrderedDict from django.conf import settings @@ -36,7 +37,7 @@ from django.views.generic import ListView from django.views.generic import UpdateView from django.views.generic import View -import requests +from scheduler.jobs import helpers as jobs from .forms import ScheduleForm from .models import Invitation @@ -124,12 +125,13 @@ class InvitationUpdateView(UpdateView): def form_valid(self, form, *args): ok = True response = super().form_valid(form, *args) - if settings.PING_URL: - r = requests.post(settings.PING_URL.format( - schedule_id=self.object.schedule.id, - token=self.object.token, - )) - ok = r.ok + try: + ok = jobs.request(json.dumps({ + 'schedule': self.object.schedule.id, + 'token': self.object.token, + })) + except TimeoutError: + ok = False if ok: messages.success(self.request, _('Your booking has been saved.')) else: diff --git a/scheduler/settings/default.py b/scheduler/settings/default.py index 8a263da9c2069c75859fa6d3771a1d1d8aa227a2..0d7d28fc9892fc46e66ec3cb1ce19132f262aee5 100644 --- a/scheduler/settings/default.py +++ b/scheduler/settings/default.py @@ -16,6 +16,7 @@ INSTALLED_APPS = [ 'django.contrib.flatpages', 'bootstrap4', 'scheduler.main', + 'scheduler.jobs', ] SITE_ID = 1 diff --git a/scheduler/urls.py b/scheduler/urls.py index 01e3418a3dd0c7cf78d5616765a0229b8981f709..2862516dc5fffdd1e5e3ddfe07419af1331936fc 100644 --- a/scheduler/urls.py +++ b/scheduler/urls.py @@ -24,6 +24,7 @@ from django.urls import path from django.urls import re_path from django.views.i18n import JavaScriptCatalog +from .jobs.views import JobView from .main.views import InvitationApiView from .main.views import InvitationUpdateView from .main.views import ScheduleCreateView @@ -47,6 +48,7 @@ urlpatterns = [ InvitationApiView.as_view(), name='api-invitation', ), + path('api/jobs/', JobView.as_view(), name='jobs'), path('login/', LoginView.as_view(), name='login'), path('logout/', LogoutView.as_view(), name='logout'), path('admin/', admin.site.urls), diff --git a/setup.cfg b/setup.cfg index 426fb32d7601381f5b7a14c8be868f7214b4450f..c291ebcd12529c5f8cee33428cf1eb3c9e385c8e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,6 @@ install_requires = Django == 3.2.6 django-bootstrap4 == 3.0.1 django-npm == 1.0.0 - requests == 2.26.0 [options.extras_require] test =