From 4bbb65179a3128a523a5181fd8cf617e2570e1ee Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Tue, 3 Nov 2020 12:20:33 +0100 Subject: [PATCH 1/3] add InvitationApiView --- scheduler/main/views.py | 30 ++++++++++++++++++++++++++++++ scheduler/settings/development.py | 2 ++ scheduler/urls.py | 6 ++++++ 3 files changed, 38 insertions(+) diff --git a/scheduler/main/views.py b/scheduler/main/views.py index f1a1fa6..b33a5f6 100644 --- a/scheduler/main/views.py +++ b/scheduler/main/views.py @@ -18,15 +18,22 @@ from collections import OrderedDict +from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib import messages +from django.core.exceptions import PermissionDenied +from django.http import HttpResponse +from django.http import JsonResponse from django.shortcuts import get_object_or_404 from django.urls import reverse +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ +from django.views.decorators.csrf import csrf_exempt from django.views.generic import CreateView from django.views.generic import DeleteView from django.views.generic import ListView from django.views.generic import UpdateView +from django.views.generic import View from .forms import ScheduleForm from .models import Invitation @@ -109,3 +116,26 @@ class InvitationUpdateView(UpdateView): def form_valid(self, form, *args): messages.success(self.request, _('Your reservation has been saved.')) return super().form_valid(form, *args) + + +@method_decorator(csrf_exempt, 'dispatch') +class InvitationApiView(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 get(self, request, *args, **kwargs): + invitation = get_object_or_404(Invitation, **kwargs) + return JsonResponse({ + 'datetime': invitation.timeslot.datetime if invitation.timeslot else None, + }) + + def put(self, request, *args, **kwargs): + invitation, _ = Invitation.objects.get_or_create(**kwargs) + return HttpResponse(status=204) + + def delete(self, request, *args, **kwargs): + invitation = get_object_or_404(Invitation, **kwargs) + invitation.delete() + return HttpResponse(status=204) diff --git a/scheduler/settings/development.py b/scheduler/settings/development.py index 33447fa..04d8b2a 100644 --- a/scheduler/settings/development.py +++ b/scheduler/settings/development.py @@ -18,3 +18,5 @@ DATABASES = { 'NAME': BASE_DIR / 'db.sqlite3', } } + +API_TOKEN = 'CHANGEME' diff --git a/scheduler/urls.py b/scheduler/urls.py index 24c0a4d..da2f6bd 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 .main.views import InvitationApiView from .main.views import InvitationUpdateView from .main.views import ScheduleCreateView from .main.views import ScheduleDeleteView @@ -41,6 +42,11 @@ urlpatterns = [ InvitationUpdateView.as_view(), name='invitation', ), + path( + 'api///', + InvitationApiView.as_view(), + name='api-invitation', + ), path('login/', LoginView.as_view(), name='login'), path('logout/', LogoutView.as_view(), name='logout'), path('admin/', admin.site.urls), -- GitLab From a597440f45faaa4161eae3864955d8742d6bc14e Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Tue, 3 Nov 2020 12:21:33 +0100 Subject: [PATCH 2/3] ping external service on invitation update --- requirements.txt | 1 + scheduler/main/views.py | 19 +++++++++++++++++-- scheduler/settings/default.py | 2 ++ scheduler/settings/development.py | 1 + 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4f0ec58..3d83d3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django==3.1.2 django-bootstrap4==2.3.1 django-npm==1.0.0 +requests==2.24.0 diff --git a/scheduler/main/views.py b/scheduler/main/views.py index b33a5f6..58d05b4 100644 --- a/scheduler/main/views.py +++ b/scheduler/main/views.py @@ -35,6 +35,8 @@ from django.views.generic import ListView from django.views.generic import UpdateView from django.views.generic import View +import requests + from .forms import ScheduleForm from .models import Invitation from .models import Schedule @@ -114,8 +116,21 @@ class InvitationUpdateView(UpdateView): return context def form_valid(self, form, *args): - messages.success(self.request, _('Your reservation has been saved.')) - return super().form_valid(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 + if ok: + messages.success(self.request, _('Your reservation has been saved.')) + else: + self.object.timeslot = None + self.object.save() + messages.error(self.request, _('An error occured.')) + return response @method_decorator(csrf_exempt, 'dispatch') diff --git a/scheduler/settings/default.py b/scheduler/settings/default.py index b490a5b..47c27d0 100644 --- a/scheduler/settings/default.py +++ b/scheduler/settings/default.py @@ -119,3 +119,5 @@ NAV = [ ('/imprint/', 'Imprint'), ('/data-protection/', 'Data protection'), ] + +PING_URL = '' diff --git a/scheduler/settings/development.py b/scheduler/settings/development.py index 04d8b2a..442081d 100644 --- a/scheduler/settings/development.py +++ b/scheduler/settings/development.py @@ -20,3 +20,4 @@ DATABASES = { } API_TOKEN = 'CHANGEME' +PING_URL = 'http://localhost:8000/recruitment/ping/{schedule_id}/{token}/' -- GitLab From 5c39e71ac24499d4eedfb9428923d1f6bb80abe8 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Tue, 3 Nov 2020 12:49:01 +0100 Subject: [PATCH 3/3] describe API in README --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 3e79bb9..f5292cb 100644 --- a/README.md +++ b/README.md @@ -12,4 +12,29 @@ other tools just as well. For development, a single `make` will install all dependencies and start the server. You can log in as "admin" with password "password". +# API + +All API requests must send an `Authorization` header with the secret +token defined in `settings.API_TOKEN`. + +You can use PUT/DELETE requests to create/delete invitations for a +schedule. PUT will always respond with 204. DELETE will respond with 404 +if no matching invitation existed. + +You can use a GET request to get the currently selected timeslot for an +invitation. + +When an invitation is changed, a POST request is sent to the URL defined +in `settings.PING_URL`. This request is not authenticated and should not +be trusted, so it does not itself contain the new data. Instead, the +other service is expected to make an authenticated GET request as +described above. + +Example: + + $ curl -X PUT -H 'Authorization: token CHANGEME' http://localhost:8001/api/1/foo/ + $ curl -X GET -H 'Authorization: token CHANGEME' http://localhost:8001/api/1/foo/ + {"datetime": "2020-11-03T07:00:00"} + $ curl -X DELETE -H 'Authorization: token CHANGEME' http://localhost:8001/api/1/foo/ + [1]: https://www.mpib-berlin.mpg.de/research-data/castellum -- GitLab