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 logging
24from django.contrib import messages
25from django.contrib.auth.mixins import LoginRequiredMixin
26from django.core.exceptions import PermissionDenied
27from django.db import models
28from django.http import HttpResponseBadRequest
29from django.http import JsonResponse
30from django.http import QueryDict
31from django.shortcuts import get_object_or_404
32from django.shortcuts import redirect
33from django.urls import reverse
34from django.utils.functional import cached_property
35from django.utils.text import slugify
36from django.utils.translation import gettext_lazy as _
37from django.views import View
38from django.views.generic import CreateView
39from django.views.generic import DeleteView
40from django.views.generic import DetailView
41from django.views.generic import FormView
42from django.views.generic import ListView
43from django.views.generic import UpdateView
45from castellum.castellum_auth.mixins import PermissionRequiredMixin
46from castellum.recruitment import filter_queries
47from castellum.recruitment.models import ParticipationRequest
48from castellum.subjects.models import Subject
49from castellum.utils.views import ReadonlyMixin
50from castellum.utils.views import get_next_url
52from ..forms import ImportForm
53from ..forms import StudyForm
54from ..mixins import StudyMixin
55from ..models import Study
56from ..models import StudyMembership
57from .recruitment import get_related_studies
59monitoring_logger = logging.getLogger('monitoring.studies')
62class StudyIndexView(LoginRequiredMixin, ListView):
63 model = Study
64 ordering = 'name'
65 paginate_by = 15
67 def get(self, request, *args, **kwargs):
68 if 'events' in request.GET: 68 ↛ 69line 68 didn't jump to line 69, because the condition on line 68 was never true
69 events = [self.render_event(study) for study in self.get_queryset()]
70 return JsonResponse({'events': events})
71 else:
72 return super().get(request, *args, **kwargs)
74 def get_queryset(self):
75 qs = super().get_queryset()
76 q = self.request.GET.get('q')
77 if q: 77 ↛ 78line 77 didn't jump to line 78, because the condition on line 77 was never true
78 for part in q.split():
79 qs = qs.filter(
80 models.Q(name__icontains=part) |
81 models.Q(contact_person__icontains=part) |
82 models.Q(description__icontains=part) |
83 models.Q(keywords__icontains=part) |
84 models.Q(studysession__type__translations__label__iexact=part)
85 )
86 tab = self.request.GET.get('tab', 'my')
87 if tab == 'my':
88 qs = qs.filter(studymembership__user=self.request.user)
89 if 'start' in self.request.GET: 89 ↛ 90line 89 didn't jump to line 90, because the condition on line 89 was never true
90 qs = qs.filter(sessions_end__gte=self.request.GET['start'][:10])
91 if 'end' in self.request.GET: 91 ↛ 92line 91 didn't jump to line 92, because the condition on line 91 was never true
92 qs = qs.filter(sessions_start__lte=self.request.GET['end'][:10])
93 return qs.distinct()
95 def render_event(self, study):
96 return {
97 'start': study.sessions_start,
98 'end': study.sessions_end,
99 'title': study.name,
100 'url': reverse('studies:detail', args=[study.pk]),
101 }
103 def get_context_data(self, **kwargs):
104 context = super().get_context_data(**kwargs)
105 context['q'] = self.request.GET.get('q', '')
106 context['tab'] = self.request.GET.get('tab', 'my')
107 return context
110class StudyDetailView(PermissionRequiredMixin, DetailView):
111 model = Study
112 permission_required = 'studies.view_study'
113 tab = 'detail'
115 def get_permission_object(self):
116 # In other views this is handled by StudyMixin. We cannot use
117 # that here because this view should not require access_study.
118 return self.get_object()
120 def get_context_data(self, **kwargs):
121 context = super().get_context_data(**kwargs)
122 context['count'] = (
123 Subject.objects.filter(
124 filter_queries.study(self.object),
125 filter_queries.has_consent()
126 ).count()
127 )
128 context['related_studies'] = list(get_related_studies(self.object))
129 return context
132class StudyCreateView(PermissionRequiredMixin, CreateView):
133 model = Study
134 form_class = StudyForm
135 permission_required = 'studies.add_study'
137 def form_valid(self, form):
138 response = super().form_valid(form)
140 StudyMembership.objects.get_or_create(user=self.request.user, study=self.object)
142 if self.duplicate:
143 self.object.exclusion_criteria = self.duplicate.exclusion_criteria
144 self.object.recruitment_text = self.duplicate.recruitment_text
145 self.object.advanced_filtering = self.duplicate.advanced_filtering
146 self.object.custom_filter = self.duplicate.custom_filter
147 self.object.is_exclusive = self.duplicate.is_exclusive
148 self.object.complete_matches_only = self.duplicate.complete_matches_only
149 self.object.excluded_studies.set(self.duplicate.excluded_studies.all())
150 self.object.consent = self.duplicate.consent
151 self.object.mail_subject = self.duplicate.mail_subject
152 self.object.mail_body = self.duplicate.mail_body
153 self.object.mail_reply_address = self.duplicate.mail_reply_address
154 self.object.save()
156 for session in self.duplicate.studysession_set.all(): 156 ↛ 157line 156 didn't jump to line 157, because the loop on line 156 never started
157 s = self.object.studysession_set.create(
158 name=session.name,
159 duration=session.duration,
160 resource_id=session.resource_id,
161 )
162 s.type.set(session.type.all())
164 return response
166 def get_success_url(self):
167 return reverse('studies:index')
169 def post(self, request, *args, **kwargs):
170 self.object = None
171 return super().post(request, *args, **kwargs)
173 def get_initial(self):
174 initial = super().get_initial()
175 if self.duplicate:
176 initial['name'] = "Copy " + self.duplicate.name
177 initial['contact_person'] = self.duplicate.contact_person
178 initial['principal_investigator'] = self.duplicate.principal_investigator
179 initial['affiliated_scientists'] = self.duplicate.affiliated_scientists
180 initial['affiliated_research_assistants'] = self.duplicate.affiliated_research_assistants # noqa
181 initial['description'] = self.duplicate.description
182 return initial
184 def get_context_data(self, **kwargs):
185 context = super().get_context_data(**kwargs)
186 if self.duplicate:
187 context['duplicate'] = self.duplicate
188 return context
190 @cached_property
191 def duplicate(self):
192 if 'duplicate_pk' not in self.request.GET:
193 return None
195 duplicate_pk = self.request.GET['duplicate_pk']
196 study = get_object_or_404(Study, pk=duplicate_pk)
198 perms = ('studies.access_study', 'studies.change_study')
199 if not self.request.user.has_perms(perms, study): 199 ↛ 200line 199 didn't jump to line 200, because the condition on line 199 was never true
200 raise PermissionDenied()
202 return study
205class StudyUpdateView(
206 StudyMixin, PermissionRequiredMixin, ReadonlyMixin, UpdateView
207):
208 model = Study
209 form_class = StudyForm
210 permission_required = 'studies.change_study'
211 tab = 'update'
213 def get_readonly(self):
214 return self.object.status == Study.FINISHED
216 def get_success_url(self):
217 return reverse('studies:update', args=[self.object.pk])
219 def get_context_data(self, **kwargs):
220 context = super().get_context_data(**kwargs)
221 context['base_template'] = 'studies/study_base.html'
222 return context
224 def form_valid(self, form):
225 messages.success(self.request, _('Data has been saved.'))
226 return super().form_valid(form)
229class StudyDeleteView(StudyMixin, PermissionRequiredMixin, DeleteView):
230 model = Study
231 permission_required = 'studies.delete_study'
232 tab = 'detail'
234 def get_success_url(self):
235 return reverse('studies:index')
237 def delete(self, request, *args, **kwargs):
238 monitoring_logger.info(
239 'Study {} deleted by {}'.format(self.study.name, self.request.user.pk)
240 )
241 messages.success(self.request, _('Study has been deleted.'))
242 return super().delete(self, request, *args, **kwargs)
245class StudyStartRecruitmentView(StudyMixin, PermissionRequiredMixin, View):
246 permission_required = 'studies.change_study'
248 def post(self, request, study_pk):
249 if self.study.status != self.study.FINISHED: 249 ↛ 256line 249 didn't jump to line 256, because the condition on line 249 was never false
250 if self.study.status == self.study.EXECUTION:
251 self.study.set_status(self.study.EDIT)
252 else:
253 self.study.set_status(self.study.EXECUTION)
254 return redirect(get_next_url(request, reverse('studies:detail', args=[self.study.pk])))
255 else:
256 return HttpResponseBadRequest()
259class StudyFinishRecruitmentView(StudyMixin, PermissionRequiredMixin, View):
260 permission_required = 'studies.change_study'
262 def post(self, request, study_pk):
263 if self.study.status == self.study.FINISHED:
264 self.study.restore_previous_status()
265 else:
266 self.study.set_status(self.study.FINISHED)
267 self.study.participationrequest_set\
268 .exclude(status=ParticipationRequest.INVITED)\
269 .delete()
270 return redirect(get_next_url(request, reverse('studies:detail', args=[self.study.pk])))
273class StudyImportView(PermissionRequiredMixin, FormView):
274 permission_required = 'studies.add_study'
275 template_name = 'studies/study_import.html'
276 form_class = ImportForm
278 def render_person(self, person):
279 return person.get('name', person.get('email'))
281 def render_persons(self, persons):
282 return ', '.join([s for s in [self.render_person(p) for p in persons] if s])
284 def form_valid(self, form):
285 """
286 Not every JSON input that is valid according to the schema is
287 also valid according to the model. So we send the input to
288 ``StudyCreateView`` to also check the model restrictions. If
289 there are any issues, that view will provide helpful error
290 messages to users.
291 """
293 json = form.cleaned_data['json']
295 self.request.POST = QueryDict(mutable=True)
296 self.request.POST.update({
297 'name': json['name'],
298 'contact_person': self.render_person(json.get('contact_person', {})),
299 'principal_investigator': self.render_person(json.get('principal_investigator', {})),
300 'affiliated_scientists': self.render_persons(json.get('affiliated_scientists', [])),
301 'affiliated_research_assistants': self.render_persons(
302 json.get('affiliated_research_assistants', [])
303 ),
304 'description': json.get('description'),
305 'keywords': ', '.join(json.get('keywords', [])),
306 'min_subject_count': json.get('number_participants_expected'),
307 })
309 create_view = StudyCreateView.as_view()
310 return create_view(self.request)
313class StudyExportView(StudyMixin, PermissionRequiredMixin, View):
314 permission_required = 'studies.view_study'
315 STATUS_MAP = {
316 Study.EDIT: 'not_started',
317 Study.EXECUTION: 'started',
318 Study.FINISHED: 'finished',
319 }
321 def get(self, request, *args, **kwargs):
322 data = {
323 'name': self.study.name,
324 'contact_person': {'name': self.study.contact_person},
325 'principal_investigator': {'name': self.study.principal_investigator},
326 'description': self.study.description,
327 'status': self.STATUS_MAP[self.study.status],
328 'sessions_start': self.study.sessions_start,
329 'sessions_end': self.study.sessions_end,
330 'number_participants_expected': self.study.min_subject_count,
331 'type': [t for t in self.study.study_type.values_list('export_key', flat=True) if t],
332 }
334 response = JsonResponse({k: v for k, v in data.items() if v})
335 filename = 'castellum_%s.json' % slugify(self.study.name)
336 response['Content-Disposition'] = 'attachment; filename="%s"' % filename
337 return response