diff --git a/README.md b/README.md index 3e79bb9b5db6a294c4e08e7744697f98a369f476..f5292cb2f67c9e1e629708890998c75a6f09d007 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 diff --git a/requirements.txt b/requirements.txt index 4f0ec58561ed19f333f524d6987dfba4640ce089..3d83d3c58f59666cf3868237259a5c1f8ab733b6 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 f1a1fa6d755b42e740a6bd5e9122586d3c1de17c..58d05b46a22129d48216d3b1f36043766523b2f6 100644 --- a/scheduler/main/views.py +++ b/scheduler/main/views.py @@ -18,15 +18,24 @@ 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 + +import requests from .forms import ScheduleForm from .models import Invitation @@ -107,5 +116,41 @@ 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') +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/default.py b/scheduler/settings/default.py index b490a5b1b3585de5ad41a28513f5e629667122fd..47c27d0e03ad9d727d361f1fdd1fa4d79688f6bb 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 33447fad586558dbe4956b7da1dee0cf99b2cc8e..442081da10cde16d3f71d5c9d78defd9857c9c85 100644 --- a/scheduler/settings/development.py +++ b/scheduler/settings/development.py @@ -18,3 +18,6 @@ DATABASES = { 'NAME': BASE_DIR / 'db.sqlite3', } } + +API_TOKEN = 'CHANGEME' +PING_URL = 'http://localhost:8000/recruitment/ping/{schedule_id}/{token}/' diff --git a/scheduler/urls.py b/scheduler/urls.py index 24c0a4d635693f85ab71ea43083d100441edeec1..da2f6bdb4af9f72fab241df03377f817fb5e3984 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),