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 codecs
23import json
25from django import forms
26from django.forms import ValidationError
27from django.forms.widgets import TextInput
28from django.utils.deconstruct import deconstructible
29from django.utils.translation import gettext_lazy as _
31import jsonschema
32import magic
33from phonenumber_field.formfields import PhoneNumberField as _PhoneNumberFormField
34from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget
37class ReadonlyFormMixin:
38 def __init__(self, *args, **kwargs):
39 self.readonly = kwargs.pop('readonly', False)
40 super().__init__(*args, **kwargs)
42 if callable(self.readonly): 42 ↛ 43line 42 didn't jump to line 43, because the condition on line 42 was never true
43 self.readonly = self.readonly(self)
45 if self.readonly:
46 for field in self.fields.values():
47 if hasattr(field, 'choices'): 47 ↛ 48line 47 didn't jump to line 48, because the condition on line 47 was never true
48 field.widget.attrs['disabled'] = True
49 elif getattr(field.widget, 'input_type', 'text') in ['checkbox', 'radio', 'file']:
50 field.widget.attrs['disabled'] = True
51 else:
52 field.widget.attrs['readonly'] = True
55class ReadonlyModelForm(ReadonlyFormMixin, forms.ModelForm):
56 pass
59class DateInput(forms.DateInput):
60 def __init__(self, attrs=None):
61 defaults = {
62 'placeholder': _('yyyy-mm-dd, e.g. 2018-07-21'),
63 'autocomplete': 'off',
64 'type': 'date',
65 }
66 if attrs: 66 ↛ 67line 66 didn't jump to line 67, because the condition on line 66 was never true
67 defaults.update(attrs)
68 super().__init__(format='%Y-%m-%d', attrs=defaults)
71class DateTimeInput(forms.SplitDateTimeWidget):
72 template_name = 'utils/widgets/splitdatetime.html'
74 def __init__(self, date_attrs=None, time_attrs=None):
75 date_defaults = {
76 'placeholder': _('yyyy-mm-dd, e.g. 2018-07-21'),
77 'autocomplete': 'off',
78 'type': 'date',
79 }
80 if date_attrs: 80 ↛ 81line 80 didn't jump to line 81, because the condition on line 80 was never true
81 date_defaults.update(date_attrs)
83 time_defaults = {
84 'placeholder': _('HH:MM, e.g. 13:00'),
85 }
86 if time_attrs: 86 ↛ 87line 86 didn't jump to line 87, because the condition on line 86 was never true
87 time_defaults.update(time_attrs)
89 super().__init__(date_format='%Y-%m-%d', date_attrs=date_defaults, time_attrs=time_defaults)
92class DateField(forms.DateField):
93 widget = DateInput
96class DateTimeField(forms.SplitDateTimeField):
97 widget = DateTimeInput
100class PhoneNumberWidget(PhoneNumberInternationalFallbackWidget):
101 def __init__(self, attrs=None):
102 defaults = {
103 'placeholder': _('e.g. 030 123456'),
104 }
105 if attrs: 105 ↛ 106line 105 didn't jump to line 106, because the condition on line 105 was never true
106 defaults.update(defaults)
107 super().__init__(attrs=defaults)
110class PhoneNumberField(_PhoneNumberFormField):
111 widget = PhoneNumberWidget
114class DisabledSelect(forms.Select):
115 def __init__(self, **kwargs):
116 self.disabled_choices = kwargs.pop('disabled_choices', [])
117 super().__init__(**kwargs)
119 def create_option(self, name, value, *args, **kwargs):
120 option = super().create_option(name, value, *args, **kwargs)
121 if value in self.disabled_choices: 121 ↛ 122line 121 didn't jump to line 122, because the condition on line 121 was never true
122 option['attrs']['disabled'] = True
123 return option
126class DisabledChoiceField(forms.TypedChoiceField):
127 widget = DisabledSelect
129 def validate(self, value):
130 if value in self.widget.disabled_choices: 130 ↛ 131line 130 didn't jump to line 131, because the condition on line 130 was never true
131 raise forms.ValidationError(
132 self.error_messages['invalid_choice'],
133 code='invalid_choice',
134 params={'value': value},
135 )
136 return super().validate(value)
139class DisabledModelChoiceField(forms.ModelChoiceField):
140 widget = DisabledSelect
142 def validate(self, value):
143 # value is the instance, so we have to check value.pk
144 if value.pk in self.widget.disabled_choices:
145 raise forms.ValidationError(
146 self.error_messages['invalid_choice'],
147 code='invalid_choice',
148 params={'value': value},
149 )
150 return super().validate(value)
153class IntegerChoiceField(forms.TypedChoiceField):
154 def __init__(self, *args, **kwargs):
155 kwargs.setdefault('coerce', int)
156 super().__init__(*args, **kwargs)
159class IntegerMultipleChoiceField(forms.TypedMultipleChoiceField):
160 def __init__(self, *args, **kwargs):
161 kwargs.setdefault('coerce', int)
162 super().__init__(*args, **kwargs)
165class DatalistWidget(TextInput):
166 template_name = 'utils/widgets/datalist.html'
168 def __init__(self, *args, datalist=[], **kwargs):
169 self.datalist = datalist
170 super().__init__(*args, **kwargs)
172 def get_context(self, name, value, attrs):
173 context = super().get_context(name, value, attrs)
174 context['widget']['attrs']['list'] = context['widget']['attrs']['id'] + '_list'
175 context['datalist'] = self.datalist
176 return context
179class RestrictedFileField(forms.FileField):
180 def __init__(self, *args, **kwargs):
181 self.content_types = kwargs.pop('content_types', [])
182 self.max_upload_size = kwargs.pop('max_upload_size', 0)
183 super().__init__(*args, **kwargs)
185 def to_python(self, data):
186 f = super().to_python(data)
187 if f is None:
188 return None
190 if self.max_upload_size and f.size > self.max_upload_size: 190 ↛ 191line 190 didn't jump to line 191, because the condition on line 190 was never true
191 raise forms.ValidationError(_('File is too big.'), code='size')
193 content_type = magic.detect_from_content(f.read(1024)).mime_type
194 if content_type not in self.content_types: 194 ↛ 195line 194 didn't jump to line 195, because the condition on line 194 was never true
195 raise forms.ValidationError(_('Filetype not supported.'), code='content_type')
197 f.seek(0)
199 return f
201 def widget_attrs(self, widget):
202 attrs = super().widget_attrs(widget)
203 if self.content_types: 203 ↛ 205line 203 didn't jump to line 205, because the condition on line 203 was never false
204 attrs.setdefault('accept', ','.join(self.content_types))
205 return attrs
208@deconstructible
209class JsonFileValidator:
210 def __init__(self, schema, schema_ref):
211 self.schema = {'$ref': schema_ref}
212 self.schema.update(schema)
214 def format_error(self, error):
215 path = '.'.join(str(x) for x in error.path)
216 return '%s %s' % (path, error.message)
218 def __eq__(self, other):
219 return self.schema == other.schema and self.schema_ref == other.schema_ref
221 def __call__(self, fh):
222 try:
223 reader = codecs.getreader('utf-8')
224 data = json.load(reader(fh))
225 except json.JSONDecodeError: 225 ↛ 226line 225 didn't jump to line 226, because the exception caught by line 225 didn't happen
226 raise ValidationError(_('Uploaded file does not contain valid JSON.'), code='encoding')
227 except UnicodeDecodeError:
228 raise ValidationError(_('Uploaded file must be UTF-8 encoded.'), code='json')
230 validator = jsonschema.Draft7Validator(self.schema)
231 errors = list(validator.iter_errors(data))
232 if errors: 232 ↛ 233line 232 didn't jump to line 233, because the condition on line 232 was never true
233 raise ValidationError([
234 ValidationError(self.format_error(error), code='schema') for error in errors
235 ])
237 return data
240class BaseImportForm(forms.Form):
241 # magic seems to detect 'text/plain' for json files
242 file = RestrictedFileField(
243 label=_('File'), content_types=['application/json', 'text/plain'], required=True
244 )
246 def clean(self):
247 cleaned_data = super().clean()
248 if 'file' in cleaned_data: 248 ↛ 251line 248 didn't jump to line 251, because the condition on line 248 was never false
249 validator = JsonFileValidator(self.schema, self.schema_ref)
250 cleaned_data['json'] = validator(cleaned_data['file'])
251 return cleaned_data