diff --git a/activities/views.py b/activities/views.py index acf2a5f..a2b4a0e 100644 --- a/activities/views.py +++ b/activities/views.py @@ -8,6 +8,7 @@ from django.views.generic import DetailView, ListView, View from django.views.generic.detail import SingleObjectMixin from people import models as people_models +from people import permissions from . import models @@ -54,20 +55,23 @@ class ActivityDetailView(DetailView): return context -class ActivityAttendanceView(SingleObjectMixin, View): +class ActivityAttendanceView(permissions.UserIsLinkedPersonMixin, SingleObjectMixin, View): """ View to add or delete attendance of an activity. """ model = models.Activity + def get_test_person(self) -> people_models.Person: + data = json.loads(self.request.body) + + self.person = people_models.Person.objects.get(pk=data['pk']) + return self.person + def post(self, request, *args, **kwargs): self.object = self.get_object() if request.is_ajax(): - data = json.loads(request.body) - - person = people_models.Person.objects.get(pk=data['pk']) - self.object.attendance_list.add(person) + self.object.attendance_list.add(self.person) return HttpResponse(status=204) @@ -77,10 +81,7 @@ class ActivityAttendanceView(SingleObjectMixin, View): self.object = self.get_object() if request.is_ajax(): - data = json.loads(request.body) - - person = people_models.Person.objects.get(pk=data['pk']) - self.object.attendance_list.remove(person) + self.object.attendance_list.remove(self.person) return HttpResponse(status=204) diff --git a/breccia_mapper/templates/403.html b/breccia_mapper/templates/403.html new file mode 100644 index 0000000..f142b39 --- /dev/null +++ b/breccia_mapper/templates/403.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} + +{% block content %} +

Error 403 at {{ request.path }}

+ +

{{ exception }}

+{% endblock content %} \ No newline at end of file diff --git a/people/permissions.py b/people/permissions.py new file mode 100644 index 0000000..a9edb85 --- /dev/null +++ b/people/permissions.py @@ -0,0 +1,33 @@ +""" +Permission mixins for views relating to :class:`Person`s. +""" + +from django.contrib.auth.mixins import UserPassesTestMixin + +from . import models + + +class UserIsLinkedPersonMixin(UserPassesTestMixin): + """ + Grant access if the user is staff or has a :class:`Person` record and + this is the one referred to in the view. + """ + related_person_field = None + permission_denied_message = 'You do not have permission to view this page.' + + def get_test_person(self) -> models.Person: + """ + Get the :class:`Person` to test the user against. + """ + if self.related_person_field is None: + test_person = self.get_object() + + if not isinstance(test_person, models.Person): + raise AttributeError('View incorrectly configured: \'related_person_field\' must be defined.') + + return test_person + + return getattr(self.get_object(), self.related_person_field) + + def test_func(self) -> bool: + return self.request.user.is_staff or self.get_test_person() == self.request.user.person diff --git a/people/views.py b/people/views.py index 3a674e2..581b0e6 100644 --- a/people/views.py +++ b/people/views.py @@ -5,7 +5,7 @@ Views for displaying or manipulating models in the 'people' app. from django.http import HttpResponseRedirect from django.views.generic import CreateView, DetailView, ListView, UpdateView -from . import forms, models +from . import forms, models, permissions class PersonCreateView(CreateView): @@ -33,7 +33,7 @@ class PersonListView(ListView): template_name = 'people/person/list.html' -class ProfileView(DetailView): +class ProfileView(permissions.UserIsLinkedPersonMixin, DetailView): """ View displaying the profile of a :class:`Person` - who may be a user. """ @@ -50,10 +50,11 @@ class ProfileView(DetailView): return super().get_object(queryset) except AttributeError: + # pk was not provided in URL return self.request.user.person -class PersonUpdateView(UpdateView): +class PersonUpdateView(permissions.UserIsLinkedPersonMixin, UpdateView): """ View for updating a :class:`Person` record. """ @@ -62,15 +63,16 @@ class PersonUpdateView(UpdateView): form_class = forms.PersonForm -class RelationshipDetailView(DetailView): +class RelationshipDetailView(permissions.UserIsLinkedPersonMixin, DetailView): """ View displaying details of a :class:`Relationship`. """ model = models.Relationship template_name = 'people/relationship/detail.html' + related_person_field = 'source' -class RelationshipCreateView(CreateView): +class RelationshipCreateView(permissions.UserIsLinkedPersonMixin, CreateView): """ View for creating a :class:`Relationship`. @@ -80,6 +82,15 @@ class RelationshipCreateView(CreateView): template_name = 'people/relationship/create.html' form_class = forms.RelationshipForm + def get_test_person(self) -> models.Person: + """ + + """ + if self.request.method == 'POST': + return models.Person.objects.get(pk=self.request.POST.get('source')) + + return models.Person.objects.get(pk=self.kwargs.get('person_pk')) + def get(self, request, *args, **kwargs): self.person = models.Person.objects.get(pk=self.kwargs.get('person_pk'))