Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# (c) 2018-2020
2# MPIB <https://www.mpib-berlin.mpg.de/>,
3# MPI-CBS <https://www.cbs.mpg.de/>,
4# MPIP <http://www.psych.mpg.de/>
5#
6# This file is part of Castellum.
7#
8# Castellum is free software; you can redistribute it and/or modify it
9# under the terms of the GNU Affero General Public License as published
10# by the Free Software Foundation; either version 3 of the License, or
11# (at your option) any later version.
12#
13# Castellum is distributed in the hope that it will be useful, but
14# WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16# Affero General Public License for more details.
17#
18# You should have received a copy of the GNU Affero General Public
19# License along with Castellum. If not, see
20# <http://www.gnu.org/licenses/>.
22import datetime
23import logging
25from django.http import JsonResponse
26from django.shortcuts import get_object_or_404
27from django.urls import reverse
28from django.utils.functional import cached_property
29from django.views.generic import DetailView
30from django.views.generic import UpdateView
32from castellum.castellum_auth.mixins import PermissionRequiredMixin
33from castellum.studies.mixins import StudyMixin
34from castellum.subjects.mixins import SubjectMixin
35from castellum.utils.feeds import BaseICalFeed
37from .forms import AttributeSetForm
38from .models import Appointment
39from .models import AttributeDescription
40from .models import AttributeSet
41from .models import ParticipationRequest
42from .models.attributesets import ANSWER_DECLINED
43from .models.attributesets import UNCATEGORIZED
45monitoring_logger = logging.getLogger('monitoring.recruitment')
48class ParticipationRequestMixin(StudyMixin, SubjectMixin):
49 """Use this on every view that represents a participation request.
51 - pull in StudyMixin and SubjectMixin
52 - set ``self.participation_request``
53 - check that ``pk`` and ``study_pk`` refer to the same study
54 - check that status is in ``participationrequest_status``, otherwise return 404
56 Requires PermissionRequiredMixin.
57 """
59 participationrequest_status = []
61 @cached_property
62 def participationrequest(self):
63 qs = ParticipationRequest.objects.all()
64 if self.participationrequest_status:
65 qs = qs.filter(status__in=self.participationrequest_status)
66 return get_object_or_404(qs, pk=self.kwargs['pk'], study_id=self.kwargs['study_pk'])
68 def get_context_data(self, **kwargs):
69 context = super().get_context_data(**kwargs)
70 context['participationrequest'] = self.participationrequest
71 return context
74class BaseAttributeSetUpdateView(PermissionRequiredMixin, UpdateView):
75 model = AttributeSet
76 permission_required = 'recruitment.change_attributeset'
78 def get_form_class(self):
79 return AttributeSetForm.factory(self.request.user)
81 def form_valid(self, form):
82 result = super().form_valid(form)
83 monitoring_logger.info('AttributeSet update: {} by {}'.format(
84 self.object.subject.pk,
85 self.request.user.pk,
86 ))
87 return result
89 def get_context_data(self, **kwargs):
90 context = super().get_context_data(**kwargs)
91 form = context['form']
93 def get_bound_fields(descriptions):
94 for description in descriptions:
95 answer_declined = self.object.data.get(description.json_key) == ANSWER_DECLINED
96 try:
97 yield form['d%i' % description.pk], answer_declined
98 except KeyError:
99 pass
101 categories = AttributeDescription.objects.by_category()
102 context['uncategorized'] = get_bound_fields(categories.pop(UNCATEGORIZED))
103 context['categories'] = {
104 category: get_bound_fields(descriptions)
105 for category, descriptions in categories.items()
106 }
108 return context
111class BaseCalendarView(DetailView):
112 template_name = 'recruitment/calendar.html'
114 def get_appointments(self):
115 return Appointment.objects.filter(participant__status=ParticipationRequest.INVITED)
117 def render_appointment(self, appointment):
118 return {
119 'start': appointment.start,
120 'end': appointment.end,
121 }
123 def get(self, request, *args, **kwargs):
124 if 'events' in request.GET:
125 self.object = self.get_object()
126 events = [self.render_appointment(a) for a in self.get_appointments()]
127 return JsonResponse({'events': events})
128 else:
129 return super().get(request, *args, **kwargs)
132class BaseFollowUpFeed(BaseICalFeed):
133 def item_title(self, item):
134 return '{} - Follow-Up'.format(item.study.name)
136 def item_start_datetime(self, item):
137 if item.followup_time: 137 ↛ 138line 137 didn't jump to line 138, because the condition on line 137 was never true
138 return datetime.combine(item.followup_date, item.followup_time)
139 else:
140 return item.followup_date
142 def item_updateddate(self, item):
143 return item.updated_at
145 def item_link(self, item):
146 return reverse('recruitment:contact', args=[item.study.pk, item.pk])
148 def item_description(self, item):
149 return self.request.build_absolute_uri(self.item_link(item))
152class BaseAppointmentFeed(BaseICalFeed):
153 def items(self, obj):
154 return Appointment.objects.filter(participant__status=ParticipationRequest.INVITED)
156 def item_start_datetime(self, item):
157 return item.start
159 def item_end_datetime(self, item):
160 return item.end