Hide keyboard shortcuts

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/>. 

21 

22import logging 

23 

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 

44 

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 

51 

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 

58 

59monitoring_logger = logging.getLogger('monitoring.studies') 

60 

61 

62class StudyIndexView(LoginRequiredMixin, ListView): 

63 model = Study 

64 ordering = 'name' 

65 paginate_by = 15 

66 

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) 

73 

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() 

94 

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 } 

102 

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 

108 

109 

110class StudyDetailView(PermissionRequiredMixin, DetailView): 

111 model = Study 

112 permission_required = 'studies.view_study' 

113 tab = 'detail' 

114 

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() 

119 

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 

130 

131 

132class StudyCreateView(PermissionRequiredMixin, CreateView): 

133 model = Study 

134 form_class = StudyForm 

135 permission_required = 'studies.add_study' 

136 

137 def form_valid(self, form): 

138 response = super().form_valid(form) 

139 

140 StudyMembership.objects.get_or_create(user=self.request.user, study=self.object) 

141 

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() 

155 

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()) 

163 

164 return response 

165 

166 def get_success_url(self): 

167 return reverse('studies:index') 

168 

169 def post(self, request, *args, **kwargs): 

170 self.object = None 

171 return super().post(request, *args, **kwargs) 

172 

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 

183 

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 

189 

190 @cached_property 

191 def duplicate(self): 

192 if 'duplicate_pk' not in self.request.GET: 

193 return None 

194 

195 duplicate_pk = self.request.GET['duplicate_pk'] 

196 study = get_object_or_404(Study, pk=duplicate_pk) 

197 

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() 

201 

202 return study 

203 

204 

205class StudyUpdateView( 

206 StudyMixin, PermissionRequiredMixin, ReadonlyMixin, UpdateView 

207): 

208 model = Study 

209 form_class = StudyForm 

210 permission_required = 'studies.change_study' 

211 tab = 'update' 

212 

213 def get_readonly(self): 

214 return self.object.status == Study.FINISHED 

215 

216 def get_success_url(self): 

217 return reverse('studies:update', args=[self.object.pk]) 

218 

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 

223 

224 def form_valid(self, form): 

225 messages.success(self.request, _('Data has been saved.')) 

226 return super().form_valid(form) 

227 

228 

229class StudyDeleteView(StudyMixin, PermissionRequiredMixin, DeleteView): 

230 model = Study 

231 permission_required = 'studies.delete_study' 

232 tab = 'detail' 

233 

234 def get_success_url(self): 

235 return reverse('studies:index') 

236 

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) 

243 

244 

245class StudyStartRecruitmentView(StudyMixin, PermissionRequiredMixin, View): 

246 permission_required = 'studies.change_study' 

247 

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() 

257 

258 

259class StudyFinishRecruitmentView(StudyMixin, PermissionRequiredMixin, View): 

260 permission_required = 'studies.change_study' 

261 

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]))) 

271 

272 

273class StudyImportView(PermissionRequiredMixin, FormView): 

274 permission_required = 'studies.add_study' 

275 template_name = 'studies/study_import.html' 

276 form_class = ImportForm 

277 

278 def render_person(self, person): 

279 return person.get('name', person.get('email')) 

280 

281 def render_persons(self, persons): 

282 return ', '.join([s for s in [self.render_person(p) for p in persons] if s]) 

283 

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 """ 

292 

293 json = form.cleaned_data['json'] 

294 

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 }) 

308 

309 create_view = StudyCreateView.as_view() 

310 return create_view(self.request) 

311 

312 

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 } 

320 

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 } 

333 

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