-
Notifications
You must be signed in to change notification settings - Fork 5
/
fields.py
186 lines (162 loc) · 7.43 KB
/
fields.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
from datetime import date
from django import forms
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _
from tbxforms.widgets import DateInputWidget
class DateInputField(forms.MultiValueField):
"""
.. _Date input: https://design-system.service.gov.uk/components/date-input/
DateInputField is a MultiValueField for dates with separate fields for
the day, month and year. It is used to implement the `Date input`_
component.
The attribute ``require_all_fields`` determines where blank fields are
handled. If ``True`` and if all of the day, month or year fields are
blank a ``required`` error is raised. If any of the fields are blank
then an ``incomplete`` error is raised, These errors are reported
at the MultiValueField level - collectively - and so are not tied to
any specific field. Generally, for the Design System, you will want to
tie every error to a specific field. You can do that by setting
``require_all_fields`` to ``False`` when you create a Form. Then an
error will be reported for each blank field. Which strategy to choose
will probably depend in the application, some cases a general "one of
the fields is blank" is sufficient and for other applications a detailed
list of exactly what error occurred in what field will be more instructive.
The error messages on the MultiValueField and in the validators on each
sub-field are utterly generic and do not provide the clear guidance that
is generally needed for public-facing applications. You should almost
always override these with message tailored to your application so the
user can receive clear and specific instruction on what the problem is
and how it can be corrected.
"""
widget = DateInputWidget
def __init__(self, **kwargs):
# Define one message for all fields.
error_messages = {
"required": _("Enter the day, month, and year."),
"incomplete": _("Enter the day, month, and year."),
}
# Or define a different message for each field.
fields = (
forms.CharField(
label=_("Day"),
error_messages={"incomplete": _("Enter the day of the month")},
validators=[
RegexValidator(r"^[0-9]+$", _("Enter a valid date"))
],
),
forms.CharField(
label=_("Month"),
error_messages={"incomplete": _("Enter the month")},
validators=[
RegexValidator(r"^[0-9]+$", _("Enter a valid month"))
],
),
forms.CharField(
label=_("Year"),
error_messages={"incomplete": _("Enter the year")},
validators=[
RegexValidator(r"^[0-9]+$", _("Enter a valid year"))
],
),
)
if "help_text" not in kwargs:
kwargs["help_text"] = "For example, 24 12 2020"
super().__init__(
error_messages=error_messages, fields=fields, **kwargs
)
def clean(self, value):
"""
Validate the values entered into the day, month and year fields.
Validate every value in the given list. A value is validated against
the corresponding Field in self.fields.
Normally, all errors are reported at the level of the MultiValueField.
However the Design System requires that the Error Summary has links to
tie an error to a specific field. To make that work the
ValidationErrors for each field (day, month and year) are added to a
list on the respective widgets as well as the error list on the (bound)
field. This was the easiest way to get access to the errors for a
specific field when the DateInputWidget on the MultiValueField is
rendered.
Args:
value (list, tuple): the values entered into each field. The values
are in the order the fields are added to the ``fields``
attribute.
Raises:
ValidationError: if any of the values fails the validation checks
performed at the widget level or in this method.
Returns:
the value converted to a ``date``.
"""
clean_data = []
errors = []
if self.disabled and not isinstance(value, list):
value = self.widget.decompress(value)
if not value or isinstance(value, (list, tuple)):
if not value or not [
v for v in value if v not in self.empty_values
]:
if self.required:
raise ValidationError(
self.error_messages["required"], code="required"
)
else:
return self.compress([])
else:
raise ValidationError(
self.error_messages["invalid"], code="invalid"
)
for i, field in enumerate(self.fields):
field.widget.errors = []
try:
field_value = value[i]
except IndexError:
field_value = None
if field_value in self.empty_values:
if self.require_all_fields:
# Raise a 'required' error if the MultiValueField is
# required and any field is empty.
if self.required:
raise ValidationError(
self.error_messages["required"], code="required"
)
elif field.required:
# Otherwise, add an 'incomplete' error to the list of
# collected errors and skip field cleaning, if a required
# field is empty.
if field.error_messages["incomplete"] not in errors:
errors.append(field.error_messages["incomplete"])
field.widget.errors.append(
field.error_messages["incomplete"]
)
continue
try:
clean_data.append(field.clean(field_value))
except ValidationError as e:
# Collect all validation errors in a single list, which we'll
# raise at the end of clean(), rather than raising a single
# exception for the first error we encounter. Skip duplicates.
errors.extend(m for m in e.error_list if m not in errors)
field.widget.errors.extend(
m for m in e.messages if m not in field.widget.errors
)
if errors:
raise ValidationError(errors)
out = self.compress(clean_data)
self.validate(out)
self.run_validators(out)
return out
def compress(self, data_list):
"""
Convert the values entered into the fields as a ``date``.
Args:
data_list (tuple): a 3-tuple the of values entered into the fields.
Returns:
the ``date`` for the values entered in the day, month and year
fields. If any of the field are blank then None is returned.
"""
day, month, year = data_list
if day and month and year:
return date(day=int(day), month=int(month), year=int(year))
else:
return None