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 datetime 

23import logging 

24 

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 

31 

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 

36 

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 

44 

45monitoring_logger = logging.getLogger('monitoring.recruitment') 

46 

47 

48class ParticipationRequestMixin(StudyMixin, SubjectMixin): 

49 """Use this on every view that represents a participation request. 

50 

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 

55 

56 Requires PermissionRequiredMixin. 

57 """ 

58 

59 participationrequest_status = [] 

60 

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

67 

68 def get_context_data(self, **kwargs): 

69 context = super().get_context_data(**kwargs) 

70 context['participationrequest'] = self.participationrequest 

71 return context 

72 

73 

74class BaseAttributeSetUpdateView(PermissionRequiredMixin, UpdateView): 

75 model = AttributeSet 

76 permission_required = 'recruitment.change_attributeset' 

77 

78 def get_form_class(self): 

79 return AttributeSetForm.factory(self.request.user) 

80 

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 

88 

89 def get_context_data(self, **kwargs): 

90 context = super().get_context_data(**kwargs) 

91 form = context['form'] 

92 

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 

100 

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 } 

107 

108 return context 

109 

110 

111class BaseCalendarView(DetailView): 

112 template_name = 'recruitment/calendar.html' 

113 

114 def get_appointments(self): 

115 return Appointment.objects.filter(participant__status=ParticipationRequest.INVITED) 

116 

117 def render_appointment(self, appointment): 

118 return { 

119 'start': appointment.start, 

120 'end': appointment.end, 

121 } 

122 

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) 

130 

131 

132class BaseFollowUpFeed(BaseICalFeed): 

133 def item_title(self, item): 

134 return '{} - Follow-Up'.format(item.study.name) 

135 

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 

141 

142 def item_updateddate(self, item): 

143 return item.updated_at 

144 

145 def item_link(self, item): 

146 return reverse('recruitment:contact', args=[item.study.pk, item.pk]) 

147 

148 def item_description(self, item): 

149 return self.request.build_absolute_uri(self.item_link(item)) 

150 

151 

152class BaseAppointmentFeed(BaseICalFeed): 

153 def items(self, obj): 

154 return Appointment.objects.filter(participant__status=ParticipationRequest.INVITED) 

155 

156 def item_start_datetime(self, item): 

157 return item.start 

158 

159 def item_end_datetime(self, item): 

160 return item.end