diff --git a/castellum/appointments/mixins.py b/castellum/appointments/mixins.py
index 1145ae937d959bb6b25175895e3c3c819cd7f5ff..54bee5d13d8500d7768cdfb2d494a9e56e64fab1 100644
--- a/castellum/appointments/mixins.py
+++ b/castellum/appointments/mixins.py
@@ -33,7 +33,7 @@ from castellum.castellum_auth.mixins import PermissionRequiredMixin
from castellum.recruitment.mixins import ParticipationMixin
from castellum.recruitment.models import Participation
from castellum.utils import add_working_days
-from castellum.utils.feeds import BaseICalFeed
+from castellum.utils.feeds import BaseCalendarFeed
from castellum.utils.mail import MailContext
from .forms import AppointmentsForm
@@ -137,12 +137,12 @@ class BaseCalendarView(DetailView):
return super().get(request, *args, **kwargs)
-class BaseAppointmentFeed(BaseICalFeed):
- def items(self, obj):
+class BaseAppointmentFeed(BaseCalendarFeed):
+ def items(self):
return Appointment.objects.filter(participation__status=Participation.INVITED)
- def item_start_datetime(self, item):
+ def item_start(self, item):
return item.start
- def item_end_datetime(self, item):
+ def item_end(self, item):
return item.end
diff --git a/castellum/castellum_auth/mixins.py b/castellum/castellum_auth/mixins.py
index 13ef0ec86712b265670b5802cb8afecb78945d88..74c955f3d0db031a2123e1d8c4fdaaf4186d8b25 100644
--- a/castellum/castellum_auth/mixins.py
+++ b/castellum/castellum_auth/mixins.py
@@ -39,37 +39,25 @@ class PermissionRequiredMixin(auth_mixins.PermissionRequiredMixin):
class APIAuthMixin(StrongholdPublicMixin):
- """Similar to PermissionRequiredMixin.
+ """Authenticate with token header instead of login.
- Differences:
- - Does not redirect to login on error
- - Gets request.user from the HTTP_AUTHORIZATION header
+ This should be the first mixin so that authentication is checked
+ before anything else.
"""
- permission_required = None
- def handle_no_permission(self):
- raise PermissionDenied
+ raise_exception = True
- def get_permission_required(self):
- return self.permission_required
-
- def get_permission_object(self):
- return None
-
- def has_permission(self):
- perms = self.get_permission_required()
- permission_object = self.get_permission_object()
- return self.request.user.has_perms(perms, obj=permission_object)
-
- def authenticate(self):
+ def get_token(self):
auth = self.request.headers.get('Authorization', '').split()
if len(auth) != 2 or auth[0].lower() != 'token':
- self.handle_no_permission()
+ raise PermissionDenied
+ return auth[1]
+ def authenticate(self):
try:
- user = User.objects.get(token=auth[1])
+ user = User.objects.get(token=self.get_token())
except User.DoesNotExist:
- self.handle_no_permission()
+ raise PermissionDenied
if not user.expiration_date:
raise PermissionDenied('Account is not activated.')
@@ -80,6 +68,11 @@ class APIAuthMixin(StrongholdPublicMixin):
def dispatch(self, request, *args, **kwargs):
self.request.user = self.authenticate()
- if not self.has_permission():
- return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
+
+
+class ParamAuthMixin(APIAuthMixin):
+ def get_token(self):
+ if 'token' not in self.request.GET:
+ raise PermissionDenied
+ return self.request.GET['token']
diff --git a/castellum/execution/api.py b/castellum/execution/api.py
index 617fda9570719a3658bd9dc6b19daf1913961960..f7983b3e057a7086bb8a8dd739b249b164f086e2 100644
--- a/castellum/execution/api.py
+++ b/castellum/execution/api.py
@@ -30,6 +30,7 @@ from django.shortcuts import get_object_or_404
from django.views.generic import View
from castellum.castellum_auth.mixins import APIAuthMixin
+from castellum.castellum_auth.mixins import PermissionRequiredMixin
from castellum.pseudonyms.helpers import get_pseudonym
from castellum.pseudonyms.helpers import get_subject
from castellum.pseudonyms.models import Domain
@@ -42,7 +43,7 @@ from castellum.subjects.models import Subject
monitoring_logger = logging.getLogger('monitoring.execution')
-class BaseAPIView(StudyMixin, APIAuthMixin, View):
+class BaseAPIView(APIAuthMixin, StudyMixin, PermissionRequiredMixin, View):
permission_required = ['recruitment.conduct_study']
study_status = [Study.EXECUTION]
diff --git a/castellum/execution/feeds.py b/castellum/execution/feeds.py
index ce34798188981f8e3cf10e814bc3008bd19cf786..fbf34e07b4fb9148a952012c3a53031e6c491941 100644
--- a/castellum/execution/feeds.py
+++ b/castellum/execution/feeds.py
@@ -19,12 +19,13 @@
# License along with Castellum. If not, see
# .
-from django.core.exceptions import PermissionDenied
-from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from castellum.appointments.mixins import BaseAppointmentFeed
+from castellum.castellum_auth.mixins import ParamAuthMixin
+from castellum.castellum_auth.mixins import PermissionRequiredMixin
+from castellum.studies.mixins import StudyMixin
from castellum.studies.models import Study
@@ -41,34 +42,26 @@ class BaseExecutionAppointmentFeed(BaseAppointmentFeed):
item.session.study.pk, item.participation.pk
])
- def item_description(self, item):
- return self.request.build_absolute_uri(self.item_link(item))
+class AppointmentFeedForStudy(
+ ParamAuthMixin, StudyMixin, PermissionRequiredMixin, BaseExecutionAppointmentFeed
+):
+ permission_required = 'recruitment.conduct_study'
+ study_status = [Study.EXECUTION]
-class AppointmentFeedForStudy(BaseExecutionAppointmentFeed):
- def get_object(self, request, *args, **kwargs):
- study = get_object_or_404(Study, pk=kwargs['pk'], status=Study.EXECUTION)
- perms = ['recruitment.conduct_study', 'studies.access_study']
- if not self.request.user.has_perms(perms, obj=study):
- raise PermissionDenied
- return study
-
- def items(self, study):
- return super().items(study)\
- .filter(session__study=study)\
+ def items(self):
+ return super().items()\
+ .filter(session__study=self.study)\
.select_related('participation__subject', 'session__study')
-class AppointmentFeedForUser(BaseExecutionAppointmentFeed):
- def get_object(self, request, *args, **kwargs):
- return self.request.user
-
- def items(self, user):
- items = super().items(user)\
+class AppointmentFeedForUser(ParamAuthMixin, BaseExecutionAppointmentFeed):
+ def items(self):
+ items = super().items()\
.filter(session__study__status=Study.EXECUTION)\
.select_related('participation__subject', 'session__study')
perms = ['recruitment.conduct_study', 'studies.access_study']
for item in items:
- if user.has_perms(perms, obj=item.session.study):
+ if self.request.user.has_perms(perms, obj=item.session.study):
yield item
diff --git a/castellum/execution/urls.py b/castellum/execution/urls.py
index 51e98ff495404765df29f3e8921cbd26f38fc92e..af1e31c6df7475006a136cceba98bed0304610c6 100644
--- a/castellum/execution/urls.py
+++ b/castellum/execution/urls.py
@@ -52,12 +52,12 @@ from .views import TagView
app_name = 'execution'
urlpatterns = [
- path('calendar/feed/', AppointmentFeedForUser(), name='calendar-user-feed'),
+ path('calendar/feed/', AppointmentFeedForUser.as_view(), name='calendar-user-feed'),
path('/', StudyDetailView.as_view(), name='study-detail'),
path('/resolve/', ResolveView.as_view(), name='resolve'),
path('/news/', NewsMailView.as_view(), name='news-mail'),
path('/calendar/', CalendarView.as_view(), name='calendar'),
- path('/calendar/feed/', AppointmentFeedForStudy(), name='calendar-feed'),
+ path('/calendar/feed/', AppointmentFeedForStudy.as_view(), name='calendar-feed'),
path('/criteria/', ExclusionCriteriaView.as_view(), name='criteria'),
path('/export/', ExportView.as_view(), name='export'),
path('/progress/', ProgressView.as_view(), name='progress'),
diff --git a/castellum/recruitment/feeds.py b/castellum/recruitment/feeds.py
index 6b9d52a0ed96d0e1db55f2dcec9ac07ab132ca46..ba9cd5a1491b63c40472de6bb4c2933fe50f87bb 100644
--- a/castellum/recruitment/feeds.py
+++ b/castellum/recruitment/feeds.py
@@ -19,10 +19,12 @@
# License along with Castellum. If not, see
# .
-from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404
from castellum.appointments.mixins import BaseAppointmentFeed
+from castellum.castellum_auth.mixins import ParamAuthMixin
+from castellum.castellum_auth.mixins import PermissionRequiredMixin
+from castellum.studies.mixins import StudyMixin
from castellum.studies.models import Resource
from castellum.studies.models import Study
@@ -30,28 +32,21 @@ from .mixins import BaseFollowUpFeed
from .models import Participation
-class FollowUpFeedForStudy(BaseFollowUpFeed):
- def get_object(self, request, *args, **kwargs):
- study = get_object_or_404(Study, pk=kwargs.get('study_pk'), status=Study.EXECUTION)
- perms = ['recruitment.recruit', 'studies.access_study']
- if not self.request.user.has_perms(perms, obj=study):
- raise PermissionDenied
- return study
+class FollowUpFeedForStudy(ParamAuthMixin, StudyMixin, PermissionRequiredMixin, BaseFollowUpFeed):
+ permission_required = 'recruitment.recruit'
+ study_status = [Study.EXECUTION]
- def items(self, study):
+ def items(self):
return Participation.objects\
- .filter(study=study, followup_date__isnull=False)\
+ .filter(study=self.study, followup_date__isnull=False)\
.order_by('-followup_date', '-followup_time')
-class FollowUpFeedForUser(BaseFollowUpFeed):
- def get_object(self, request, *args, **kwargs):
- return self.request.user
-
- def items(self, user):
+class FollowUpFeedForUser(ParamAuthMixin, BaseFollowUpFeed):
+ def items(self):
perms = ['recruitment.recruit', 'studies.access_study']
for study in Study.objects.filter(status=Study.EXECUTION):
- if not user.has_perms(perms, obj=study):
+ if not self.request.user.has_perms(perms, obj=study):
continue
qs = Participation.objects\
.filter(study=study, followup_date__isnull=False)\
@@ -60,29 +55,26 @@ class FollowUpFeedForUser(BaseFollowUpFeed):
yield participation
-class AppointmentFeed(BaseAppointmentFeed):
- def get_object(self, request, *args, **kwargs):
- study = get_object_or_404(Study, pk=kwargs['pk'], status=Study.EXECUTION)
- perms = ['appointments.change_appointment', 'studies.access_study']
- if not self.request.user.has_perms(perms, obj=study):
- raise PermissionDenied
- return study
+class AppointmentFeed(ParamAuthMixin, StudyMixin, PermissionRequiredMixin, BaseAppointmentFeed):
+ permission_required = 'appointments.change_appointment'
+ study_status = [Study.EXECUTION]
- def items(self, study):
- return super().items(study).filter(session__study=study)
+ def items(self):
+ return super().items().filter(session__study=self.study)
- def item_link(self, item):
+ def item_title(self, item):
+ return ''
+
+ def item_uid(self, item):
# This is not really a usable link, but it is unique
return '/appointments/{}/'.format(item.pk)
-class ResourceFeed(BaseAppointmentFeed):
- def get_object(self, request, *args, **kwargs):
- return get_object_or_404(Resource, pk=kwargs['pk'])
-
- def items(self, resource):
+class ResourceFeed(ParamAuthMixin, BaseAppointmentFeed):
+ def items(self):
+ resource = get_object_or_404(Resource, pk=self.kwargs['pk'])
return (
- super().items(resource)
+ super().items()
.filter(session__resource=resource)
.exclude(session__study__status=Study.FINISHED)
)
@@ -90,6 +82,6 @@ class ResourceFeed(BaseAppointmentFeed):
def item_title(self, item):
return item.session.study.name
- def item_link(self, item):
+ def item_uid(self, item):
# This is not really a usable link, but it is unique
return '/appointments/{}/'.format(item.pk)
diff --git a/castellum/recruitment/mixins.py b/castellum/recruitment/mixins.py
index d3e446942366b4e484e0a91ca5a10aa7db46e196..4b3639f18eebe33e6e68174bdce7045ee6370ac1 100644
--- a/castellum/recruitment/mixins.py
+++ b/castellum/recruitment/mixins.py
@@ -40,7 +40,7 @@ from castellum.castellum_auth.mixins import PermissionRequiredMixin
from castellum.studies.mixins import StudyMixin
from castellum.subjects.mixins import SubjectMixin
from castellum.subjects.models import Subject
-from castellum.utils.feeds import BaseICalFeed
+from castellum.utils.feeds import BaseCalendarFeed
from castellum.utils.mail import MailContext
from . import filter_queries
@@ -146,25 +146,22 @@ class BaseAttributesUpdateView(PermissionRequiredMixin, UpdateView):
return context
-class BaseFollowUpFeed(BaseICalFeed):
+class BaseFollowUpFeed(BaseCalendarFeed):
def item_title(self, item):
return '{} - Follow-Up'.format(item.study.name)
- def item_start_datetime(self, item):
+ def item_start(self, item):
if item.followup_time:
return datetime.datetime.combine(item.followup_date, item.followup_time)
else:
return item.followup_date
- def item_updateddate(self, item):
+ def item_updated_at(self, item):
return item.updated_at
def item_link(self, item):
return reverse('recruitment:contact', args=[item.study.pk, item.pk])
- def item_description(self, item):
- return self.request.build_absolute_uri(self.item_link(item))
-
class BaseMailRecruitmentView(StudyMixin, PermissionRequiredMixin, FormView):
template_name = 'recruitment/mail.html'
diff --git a/castellum/recruitment/urls.py b/castellum/recruitment/urls.py
index 261385b1884fe8c673f78cc8e57a4b36ed8234cb..877ef500c94bc004bad9a8b0306aa38c017f087c 100644
--- a/castellum/recruitment/urls.py
+++ b/castellum/recruitment/urls.py
@@ -44,8 +44,8 @@ from .views import SchedulerPingView
app_name = 'recruitment'
urlpatterns = [
- path('followups/', FollowUpFeedForUser(), name='followups-user-feed'),
- path('resources//', ResourceFeed(), name='resource-feed'),
+ path('followups/', FollowUpFeedForUser.as_view(), name='followups-user-feed'),
+ path('resources//', ResourceFeed.as_view(), name='resource-feed'),
path('scheduler-ping///', SchedulerPingView.as_view()),
path('/', RecruitmentViewOpen.as_view(), name='recruitment-open'),
path('/invited/', RecruitmentViewInvited.as_view(), name='recruitment-invited'),
@@ -67,8 +67,8 @@ urlpatterns = [
),
path('/cleanup/', RecruitmentCleanupView.as_view(), name='cleanup'),
path('/calendar/', CalendarView.as_view(), name='calendar'),
- path('/calendar/feed/', AppointmentFeed(), name='calendar-feed'),
- path('/followups/', FollowUpFeedForStudy(), name='followups-feed'),
+ path('/calendar/feed/', AppointmentFeed.as_view(), name='calendar-feed'),
+ path('/followups/', FollowUpFeedForStudy.as_view(), name='followups-feed'),
path('//', ContactView.as_view(), name='contact'),
path(
'//appointments/',
diff --git a/castellum/utils/feeds.py b/castellum/utils/feeds.py
index 4c32e79212e7e755805bfaa1b2c22b5fb16bb962..078c4c0e95d1bbe7cec225197c1e0e6b8bb49ad4 100644
--- a/castellum/utils/feeds.py
+++ b/castellum/utils/feeds.py
@@ -19,26 +19,45 @@
# License along with Castellum. If not, see
# .
-from django.core.exceptions import PermissionDenied
+from django.http import HttpResponse
+from django.utils import timezone
+from django.views.generic import View
-from django_ical.views import ICalFeed
-from stronghold.decorators import public
+from icalendar import Calendar
+from icalendar import Event
-from castellum.castellum_auth.models import User
+class BaseCalendarFeed(View):
+ def get(self, request, **kwargs):
+ cal = Calendar()
+ cal.add('version', '2.0')
+ cal.add('prodid', 'castellum')
-@public
-class BaseICalFeed(ICalFeed):
- def __call__(self, request, *args, **kwargs):
- self.request = request
- try:
- self.request.user = User.objects.get(token=request.GET.get('token'))
- except User.DoesNotExist:
- raise PermissionDenied
- return super().__call__(request, *args, **kwargs)
+ for item in self.items():
+ event = Event()
+ event.add('uid', request.build_absolute_uri(self.item_uid(item)))
+ event.add('dtstamp', self.item_updated_at(item))
+ event.add('summary', self.item_title(item))
+ event.add('dtstart', self.item_start(item))
+ if hasattr(self, 'item_end'):
+ event.add('dtend', self.item_end(item))
+ if hasattr(self, 'item_link'):
+ event.add('url', self._item_link(item))
+ if hasattr(self, 'item_description'):
+ event.add('description', self.item_description(item))
+ elif hasattr(self, 'item_link'):
+ event.add('description', self._item_link(item))
+ cal.add_component(event)
- def item_title(self, item):
- return None
+ response = HttpResponse(content_type='text/calendar')
+ response.write(cal.to_ical())
+ return response
- def item_description(self, item):
- return None
+ def item_updated_at(self, item):
+ return timezone.now()
+
+ def item_uid(self, item):
+ return self.item_link(item)
+
+ def _item_link(self, item):
+ return self.request.build_absolute_uri(self.item_link(item))
diff --git a/setup.cfg b/setup.cfg
index f117bf6d5603f1925d16f90153549135fda3fe0b..8732600db003cf7f04ed9046ff1ef80f3e2f1624 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -14,7 +14,6 @@ install_requires =
cologne-phonetics == 1.3.0
Django == 3.2.8
django-bootstrap4 == 3.0.1
- django-ical == 1.8.0
django-mfa3 == 0.3.0
django-npm == 1.0.0
django-parler == 2.2.0