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 datetime
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 _
30from castellum.subjects.models import Subject
31from castellum.utils.forms import DatalistWidget
33from .models import Address
34from .models import Contact
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
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 }
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)
66 class Meta:
67 model = Contact
68 exclude = ['guardians']
70 def __init__(self, *args, **kwargs):
71 user = kwargs.pop('user')
72 is_guardian = kwargs.pop('is_guardian', False)
73 super().__init__(*args, **kwargs)
75 if self.instance.is_guardian:
76 is_guardian = True
78 self.address = self.get_address_form(**kwargs)
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'
85 self.fields['guardians_remove'].choices = self.get_guardians_rm_choices(user)
87 self.fields['date_of_birth'].required = not is_guardian
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
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
107 def clean(self):
108 cleaned_data = super().clean()
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', '')
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'] = ''
123 has_address = any(self.data.get(name) for name in self.address.fields)
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')
134 # validate address
135 if has_address:
136 # populate self.address.cleaned_data
137 self.address.is_valid()
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)
146 raise ValidationError(_("Provide more details for the address."), code='invalid')
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."))
159 return cleaned_data
161 def save(self):
162 contact = super().save()
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)
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 ])
178 contact.guardians.add(*[
179 subject.contact for subject in self.cleaned_data.get('guardians_add', [])
180 ])
181 else:
182 contact.guardians.set([])
184 return contact
187class SearchForm(forms.Form):
188 search = forms.CharField(label=_('Search for name or email address'), required=True)
190 def clean(self):
191 cleaned_data = super().clean()
193 search = cleaned_data.get('search', '')
195 names = [part for part in search.split() if '@' not in part]
196 emails = [part for part in search.split() if '@' in part]
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 ''
202 return cleaned_data