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/>.
22from django.conf import settings
23from django.contrib import messages
24from django.forms import modelformset_factory
25from django.http import HttpResponseBadRequest
26from django.shortcuts import get_object_or_404
27from django.shortcuts import redirect
28from django.template.response import TemplateResponse
29from django.urls import reverse
30from django.utils.translation import gettext_lazy as _
31from django.views.generic import DeleteView
32from django.views.generic import ListView
33from django.views.generic import UpdateView
34from django.views.generic import View
36from castellum.castellum_auth.mixins import PermissionRequiredMixin
37from castellum.recruitment import filter_queries
38from castellum.recruitment.forms import SubjectFilterAddForm
39from castellum.recruitment.forms import SubjectFilterForm
40from castellum.recruitment.forms import SubjectFilterFormSet
41from castellum.recruitment.models import AttributeDescription
42from castellum.recruitment.models import SubjectFilter
43from castellum.recruitment.models import SubjectFilterGroup
44from castellum.subjects.models import Subject
45from castellum.utils.views import ReadonlyMixin
47from ..mixins import StudyMixin
48from ..models import Study
51class CustomFilterMixin:
52 """Should be called *after* PermissionRequiredMixin."""
54 def dispatch(self, request, *args, **kwargs):
55 if self.study.custom_filter: 55 ↛ 56line 55 didn't jump to line 56, because the condition on line 55 was never true
56 return TemplateResponse(
57 request=self.request,
58 template='studies/filtergroup_blocked.html',
59 context={
60 'study': self.study,
61 'total_count': Subject.objects.count(),
62 'count': (
63 Subject.objects.filter(
64 filter_queries.study(self.object),
65 filter_queries.has_consent(),
66 ).count()
67 ),
68 },
69 )
70 return super().dispatch(request, *args, **kwargs)
73class FilterMixin(StudyMixin, PermissionRequiredMixin, CustomFilterMixin):
74 tab = 'recruitmentsettings'
75 subtab = 'filters'
77 def get_permission_required(self):
78 permission_required = set(['studies.change_study'])
79 permission_required.update(super().get_permission_required())
81 max_privacy_level = self.study.get_filter_max_privacy_level()
82 if max_privacy_level:
83 permission_required.add('castellum_auth.privacy_level_{}'.format(max_privacy_level))
85 return permission_required
88class FilterGroupListView(FilterMixin, ListView):
89 model = SubjectFilterGroup
90 template_name = 'studies/filtergroup_advanced.html'
91 permission_required = 'recruitment.view_subjectfilter'
93 def get_queryset(self):
94 qs = super().get_queryset()
95 return qs.filter(study=self.study)
97 def get_context_data(self, **kwargs):
98 context = super().get_context_data(**kwargs)
99 context['total_count'] = Subject.objects.count()
100 context['count'] = (
101 Subject.objects.filter(
102 filter_queries.study(self.study),
103 filter_queries.has_consent()
104 ).count()
105 )
106 context['expected_subject_factor'] = settings.CASTELLUM_EXPECTED_SUBJECT_FACTOR
107 context['expected_subject_count'] = (
108 self.study.min_subject_count * settings.CASTELLUM_EXPECTED_SUBJECT_FACTOR
109 )
110 return context
112 def get(self, request, *args, **kwargs):
113 if self.study.advanced_filtering:
114 self.object_list = self.get_queryset()
115 context = self.get_context_data()
116 return self.render_to_response(context)
117 else:
118 group, __ = SubjectFilterGroup.objects.get_or_create(study=self.study)
119 return redirect(group.get_absolute_url())
122class FilterGroupCreateView(FilterMixin, View):
123 permission_required = 'recruitment.change_subjectfilter'
124 study_status = [Study.EDIT]
126 def post(self, *args, **kwargs):
127 group = SubjectFilterGroup.objects.create(study=self.study)
128 return redirect(group.get_absolute_url())
131class FilterGroupUpdateView(FilterMixin, ReadonlyMixin, UpdateView):
132 model = SubjectFilterGroup
133 fields = []
134 template_name = "studies/filtergroup.html"
135 permission_required = 'recruitment.change_subjectfilter'
137 def get_object(self):
138 return get_object_or_404(SubjectFilterGroup, study=self.study, pk=self.kwargs['pk'])
140 def post(self, request, *args, **kwargs):
141 self.object = self.get_object()
142 if self.get_readonly(): 142 ↛ 143line 142 didn't jump to line 143, because the condition on line 142 was never true
143 return HttpResponseBadRequest()
144 form = self.get_form()
145 formset = self.get_formset()
146 if form.is_valid() and formset.is_valid():
147 return self.form_valid(form, formset)
148 else:
149 return self.form_invalid(form, formset)
151 def form_invalid(self, form, formset):
152 return self.render_to_response(self.get_context_data(form=form, formset=formset))
154 def form_valid(self, form, formset):
155 messages.success(self.request, _('Filter update successfull!'))
156 formset.save()
157 return super().form_valid(form)
159 def get_formset(self):
160 formset_class = modelformset_factory(
161 SubjectFilter,
162 form=SubjectFilterForm,
163 formset=SubjectFilterFormSet,
164 extra=0,
165 can_delete=True,
166 )
167 return formset_class(
168 queryset=self.object.subjectfilter_set.all(),
169 data=self.get_form_kwargs().get('data'),
170 )
172 def get_readonly(self):
173 return self.study.status != Study.EDIT
175 def get_inaccessible_attributedescriptions(self):
176 min_privacy_level = 2
177 for member in self.study.members.all():
178 if member.has_perm('recruitment.view_subjectfilter', obj=self.study): 178 ↛ 177line 178 didn't jump to line 177, because the condition on line 178 was never false
179 privacy_level = member.get_privacy_level(obj=self.study)
180 if privacy_level < min_privacy_level: 180 ↛ 177line 180 didn't jump to line 177, because the condition on line 180 was never false
181 min_privacy_level = privacy_level
183 return AttributeDescription.objects.filter(
184 subjectfilter__group__study=self.study,
185 privacy_level_read__gt=min_privacy_level,
186 ).distinct()
188 def get_context_data(self, **kwargs):
189 context = super().get_context_data(**kwargs)
191 context['count'] = self.object.get_matches().count()
193 context['total_count'] = Subject.objects.count()
194 context['expected_subject_factor'] = settings.CASTELLUM_EXPECTED_SUBJECT_FACTOR
195 context['expected_subject_count'] = (
196 self.study.min_subject_count * settings.CASTELLUM_EXPECTED_SUBJECT_FACTOR
197 )
199 context['inaccessible'] = list(self.get_inaccessible_attributedescriptions())
201 context['add_form'] = SubjectFilterAddForm(user=self.request.user)
202 context['templates'] = {}
203 for description in AttributeDescription.objects.all():
204 context['templates'][description.pk] = SubjectFilterForm(
205 initial={'description': description}, prefix='{prefix}'
206 )
208 if 'formset' not in context:
209 context['formset'] = self.get_formset()
211 return context
214class FilterGroupDeleteView(FilterMixin, DeleteView):
215 model = SubjectFilterGroup
216 template_name = 'studies/filtergroup_confirm_delete.html'
217 permission_required = 'recruitment.change_subjectfilter'
218 study_status = [Study.EDIT]
220 def get_object(self):
221 return get_object_or_404(SubjectFilterGroup, study=self.study, pk=self.kwargs['pk'])
223 def get_success_url(self):
224 return reverse('studies:filtergroup-index', args=[self.study.pk])
227class FilterGroupDuplicateView(FilterMixin, View):
228 permission_required = 'recruitment.change_subjectfilter'
229 study_status = [Study.EDIT]
231 def post(self, request, study_pk, pk):
232 original = get_object_or_404(SubjectFilterGroup, study=self.study, pk=pk)
233 SubjectFilterGroup.objects.clone(original)
234 messages.success(request, _(
235 'The filters have been duplicated. You can now edit the duplicate.'))
236 return redirect('studies:filtergroup-index', self.study.pk)