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 

23 

24from django import forms 

25from django.conf import settings 

26from django.forms import ValidationError 

27from django.utils.functional import lazy 

28from django.utils.translation import gettext_lazy as _ 

29 

30from castellum.subjects.models import Subject 

31from castellum.utils.forms import DatalistWidget 

32 

33from .models import Address 

34from .models import Contact 

35 

36 

37class SubjectMultipleChoiceField(forms.ModelMultipleChoiceField): 

38 def label_from_instance(self, obj): 

39 # Getting subject.contact is expensive (separate database). 

40 # Since we only render the selected choices it makes sense to calculate it lazily. 

41 return lazy(lambda: obj.contact.full_name, str) 41 ↛ exitline 41 didn't run the lambda on line 41

42 

43 

44class AddressForm(forms.ModelForm): 

45 class Meta: 

46 model = Address 

47 exclude = ['contact'] 

48 widgets = { 

49 'country': DatalistWidget(datalist=( 

50 Address.objects.order_by('country').values_list('country', flat=True).distinct 

51 )), 

52 'city': DatalistWidget(datalist=( 

53 Address.objects.order_by('city').values_list('city', flat=True).distinct 

54 )), 

55 } 

56 

57 

58class ContactForm(forms.ModelForm): 

59 guardians_pane = forms.ChoiceField(required=False, choices=[ 

60 ('self', _('Full of age')), 

61 ('guardians', _('Has legal guardian')), 

62 ], widget=forms.RadioSelect) 

63 guardians_remove = forms.ModelMultipleChoiceField(Subject.objects, required=False) 

64 guardians_add = SubjectMultipleChoiceField(Subject.objects, required=False) 

65 

66 class Meta: 

67 model = Contact 

68 exclude = ['guardians'] 

69 

70 def __init__(self, *args, **kwargs): 

71 user = kwargs.pop('user') 

72 is_guardian = kwargs.pop('is_guardian', False) 

73 super().__init__(*args, **kwargs) 

74 

75 if self.instance.is_guardian: 

76 is_guardian = True 

77 

78 self.address = self.get_address_form(**kwargs) 

79 

80 if self.instance.has_guardian: 

81 self.fields['guardians_pane'].initial = 'guardians' 

82 elif any([self.instance.get_address(), self.instance.phone_number, self.instance.email]): 

83 self.fields['guardians_pane'].initial = 'self' 

84 

85 self.fields['guardians_remove'].choices = self.get_guardians_rm_choices(user) 

86 

87 self.fields['date_of_birth'].required = not is_guardian 

88 

89 def get_address_form(self, **kwargs): 

90 address_kwargs = kwargs.copy() 

91 if kwargs.get('instance'): 

92 address_kwargs['instance'] = kwargs['instance'].get_address() 

93 form = AddressForm(**address_kwargs) 

94 for field in form.fields.values(): 

95 field.required = False 

96 return form 

97 

98 def get_guardians_rm_choices(self, user): 

99 self.guardians_blocked = 0 

100 if self.instance.id: 

101 for contact in self.instance.guardians.all(): 

102 if user.has_privacy_level(contact.subject.privacy_level): 102 ↛ 105line 102 didn't jump to line 105, because the condition on line 102 was never false

103 yield (contact.subject.pk, contact.full_name) 

104 else: 

105 self.guardians_blocked += 1 

106 

107 def clean(self): 

108 cleaned_data = super().clean() 

109 

110 is_guardians_pane = cleaned_data.get('guardians_pane') == 'guardians' 

111 date_of_birth = cleaned_data.get('date_of_birth') 

112 phone_number = cleaned_data.get('phone_number', '') 

113 email = cleaned_data.get('email', '') 

114 preferred_contact_method = cleaned_data.get('preferred_contact_method', '') 

115 

116 # clear unselected pane 

117 if is_guardians_pane: 

118 cleaned_data['phone_number'] = '' 

119 cleaned_data['phone_number_alternative'] = '' 

120 cleaned_data['email'] = '' 

121 cleaned_data['preferred_contact_method'] = '' 

122 

123 has_address = any(self.data.get(name) for name in self.address.fields) 

124 

125 # validate guardian required for underage subjects 

126 if date_of_birth: 126 ↛ 135line 126 didn't jump to line 135, because the condition on line 126 was never false

127 age = datetime.date.today() - date_of_birth 

128 if not is_guardians_pane and age.days // 365 < settings.CASTELLUM_FULL_AGE: 

129 msg = _( 

130 'Subjects under the age of %i need a legal guardian.' 

131 ) % settings.CASTELLUM_FULL_AGE 

132 raise ValidationError(msg, code='invalid') 

133 

134 # validate address 

135 if has_address: 

136 # populate self.address.cleaned_data 

137 self.address.is_valid() 

138 

139 # do strict validation (with required) 

140 address_form = AddressForm(data=self.data) 

141 if not address_form.is_valid(): 

142 if address_form.errors: 142 ↛ 146line 142 didn't jump to line 146, because the condition on line 142 was never false

143 for field, errors in address_form.errors.items(): 

144 self.address.add_error(field, errors) 

145 

146 raise ValidationError(_("Provide more details for the address."), code='invalid') 

147 

148 # validate preferred_contact_method is set 

149 if not has_address and preferred_contact_method == 'postal': 

150 self.add_error('preferred_contact_method', _("Chosen contact method can not be saved.")) 

151 self.add_error(None, _("Please provide a complete address.")) 

152 if not phone_number and preferred_contact_method == 'phone': 

153 self.add_error('preferred_contact_method', _("Chosen contact method can not be saved.")) 

154 self.add_error(None, _("Please provide a phone number.")) 

155 if not email and preferred_contact_method == 'email': 

156 self.add_error('preferred_contact_method', _("Chosen contact method can not be saved.")) 

157 self.add_error(None, _("Please provide an email.")) 

158 

159 return cleaned_data 

160 

161 def save(self): 

162 contact = super().save() 

163 

164 has_address = any(self.data.get(name) for name in self.address.fields) 

165 if has_address and self.cleaned_data.get('guardians_pane') != 'guardians': 

166 self.address.instance.contact = contact 

167 self.address.save() 

168 elif contact.get_address(): 

169 contact.address.delete() 

170 # refresh from db 

171 contact = Contact.objects.get(pk=contact.pk) 

172 

173 if self.cleaned_data.get('guardians_pane') == 'guardians': 

174 contact.guardians.remove(*[ 

175 subject.contact for subject in self.cleaned_data.get('guardians_remove', []) 

176 ]) 

177 

178 contact.guardians.add(*[ 

179 subject.contact for subject in self.cleaned_data.get('guardians_add', []) 

180 ]) 

181 else: 

182 contact.guardians.set([]) 

183 

184 return contact 

185 

186 

187class SearchForm(forms.Form): 

188 search = forms.CharField(label=_('Search for name or email address'), required=True) 

189 

190 def clean(self): 

191 cleaned_data = super().clean() 

192 

193 search = cleaned_data.get('search', '') 

194 

195 names = [part for part in search.split() if '@' not in part] 

196 emails = [part for part in search.split() if '@' in part] 

197 

198 cleaned_data['first_name'] = names[0] if names else '' 

199 cleaned_data['last_name'] = ' '.join(names[1:]) 

200 cleaned_data['email'] = emails[0] if emails else '' 

201 

202 return cleaned_data