Forms
HTML form handling and validation.
The Form
and Field
classes help output, parse, and validate form data from an HTTP request. Unlike other frameworks, the HTML inputs are not rendered automatically, though there are some helpers for you to do your own rendering.
With forms, you will typically use one of the built-in view classes to tie everything together.
from plain import forms
from plain.views import FormView
class ContactForm(forms.Form):
email = forms.EmailField()
message = forms.CharField()
class ContactView(FormView):
form_class = ContactForm
template_name = "contact.html"
Then in your template, you can render the form fields.
{% extends "base.html" %}
{% block content %}
<form method="post">
{{ csrf_input }}
<!-- Render general form errors -->
{% for error in form.non_field_errors %}
<div>{{ error }}</div>
{% endfor %}
<div>
<label for="{{ form.email.html_id }}">Email</label>
<input
required
type="email"
name="{{ form.email.html_name }}"
id="{{ form.email.html_id }}"
value="{{ form.email.value }}">
{% if form.email.errors %}
<div>{{ form.email.errors|join(', ') }}</div>
{% endif %}
</div>
<div>
<label for="{{ form.message.html_id }}">Message</label>
<textarea
required
rows="10"
name="{{ form.message.html_name }}"
id="{{ form.message.html_id }}">{{ form.message.value }}</textarea>
{% if form.message.errors %}
<div>{{ form.message.errors|join(', ') }}</div>
{% endif %}
</div>
<button type="submit">Submit</button>
</form>
{% endblock %}
With manual form rendering, you have full control over the HTML classes, attributes, and JS behavior. But in large applications the form rendering can become repetitive. You will often end up re-using certain patterns in your HTML which can be abstracted away using Jinja includes, macros, or plain.elements.
1"""
2Form classes
3"""
4
5import copy
6
7from plain.exceptions import NON_FIELD_ERRORS
8from plain.utils.datastructures import MultiValueDict
9from plain.utils.functional import cached_property
10
11from .exceptions import ValidationError
12from .fields import Field, FileField
13
14__all__ = ("BaseForm", "Form")
15
16
17class DeclarativeFieldsMetaclass(type):
18 """Collect Fields declared on the base classes."""
19
20 def __new__(mcs, name, bases, attrs):
21 # Collect fields from current class and remove them from attrs.
22 attrs["declared_fields"] = {
23 key: attrs.pop(key)
24 for key, value in list(attrs.items())
25 if isinstance(value, Field)
26 }
27
28 new_class = super().__new__(mcs, name, bases, attrs)
29
30 # Walk through the MRO.
31 declared_fields = {}
32 for base in reversed(new_class.__mro__):
33 # Collect fields from base class.
34 if hasattr(base, "declared_fields"):
35 declared_fields.update(base.declared_fields)
36
37 # Field shadowing.
38 for attr, value in base.__dict__.items():
39 if value is None and attr in declared_fields:
40 declared_fields.pop(attr)
41
42 new_class.base_fields = declared_fields
43 new_class.declared_fields = declared_fields
44
45 return new_class
46
47
48class BaseForm:
49 """
50 The main implementation of all the Form logic. Note that this class is
51 different than Form. See the comments by the Form class for more info. Any
52 improvements to the form API should be made to this class, not to the Form
53 class.
54 """
55
56 prefix = None
57
58 def __init__(
59 self,
60 data=None,
61 files=None,
62 auto_id="id_%s",
63 prefix=None,
64 initial=None,
65 ):
66 self.is_bound = data is not None or files is not None
67 self.data = MultiValueDict() if data is None else data
68 self.files = MultiValueDict() if files is None else files
69 self._auto_id = auto_id
70 if prefix is not None:
71 self.prefix = prefix
72 self.initial = initial or {}
73 self._errors = None # Stores the errors after clean() has been called.
74
75 # The base_fields class attribute is the *class-wide* definition of
76 # fields. Because a particular *instance* of the class might want to
77 # alter self.fields, we create self.fields here by copying base_fields.
78 # Instances should always modify self.fields; they should not modify
79 # self.base_fields.
80 self.fields = copy.deepcopy(self.base_fields)
81 self._bound_fields_cache = {}
82
83 def __repr__(self):
84 if self._errors is None:
85 is_valid = "Unknown"
86 else:
87 is_valid = self.is_bound and not self._errors
88 return "<{cls} bound={bound}, valid={valid}, fields=({fields})>".format(
89 cls=self.__class__.__name__,
90 bound=self.is_bound,
91 valid=is_valid,
92 fields=";".join(self.fields),
93 )
94
95 def _bound_items(self):
96 """Yield (name, bf) pairs, where bf is a BoundField object."""
97 for name in self.fields:
98 yield name, self[name]
99
100 def __iter__(self):
101 """Yield the form's fields as BoundField objects."""
102 for name in self.fields:
103 yield self[name]
104
105 def __getitem__(self, name):
106 """Return a BoundField with the given name."""
107 try:
108 field = self.fields[name]
109 except KeyError:
110 raise KeyError(
111 "Key '{}' not found in '{}'. Choices are: {}.".format(
112 name,
113 self.__class__.__name__,
114 ", ".join(sorted(self.fields)),
115 )
116 )
117 if name not in self._bound_fields_cache:
118 self._bound_fields_cache[name] = field.get_bound_field(self, name)
119 return self._bound_fields_cache[name]
120
121 @property
122 def errors(self):
123 """Return an error dict for the data provided for the form."""
124 if self._errors is None:
125 self.full_clean()
126 return self._errors
127
128 def is_valid(self):
129 """Return True if the form has no errors, or False otherwise."""
130 return self.is_bound and not self.errors
131
132 def add_prefix(self, field_name):
133 """
134 Return the field name with a prefix appended, if this Form has a
135 prefix set.
136
137 Subclasses may wish to override.
138 """
139 return f"{self.prefix}-{field_name}" if self.prefix else field_name
140
141 @property
142 def non_field_errors(self):
143 """
144 Return a list of errors that aren't associated with a particular
145 field -- i.e., from Form.clean(). Return an empty list if there
146 are none.
147 """
148 return self.errors.get(
149 NON_FIELD_ERRORS,
150 [],
151 )
152
153 def add_error(self, field, error):
154 """
155 Update the content of `self._errors`.
156
157 The `field` argument is the name of the field to which the errors
158 should be added. If it's None, treat the errors as NON_FIELD_ERRORS.
159
160 The `error` argument can be a single error, a list of errors, or a
161 dictionary that maps field names to lists of errors. An "error" can be
162 either a simple string or an instance of ValidationError with its
163 message attribute set and a "list or dictionary" can be an actual
164 `list` or `dict` or an instance of ValidationError with its
165 `error_list` or `error_dict` attribute set.
166
167 If `error` is a dictionary, the `field` argument *must* be None and
168 errors will be added to the fields that correspond to the keys of the
169 dictionary.
170 """
171 if not isinstance(error, ValidationError):
172 raise TypeError(
173 "The argument `error` must be an instance of "
174 f"`ValidationError`, not `{type(error).__name__}`."
175 )
176
177 if hasattr(error, "error_dict"):
178 if field is not None:
179 raise TypeError(
180 "The argument `field` must be `None` when the `error` "
181 "argument contains errors for multiple fields."
182 )
183 else:
184 error = error.error_dict
185 else:
186 error = {field or NON_FIELD_ERRORS: error.error_list}
187
188 class ValidationErrors(list):
189 def __iter__(self):
190 for err in super().__iter__():
191 # TODO make sure this works...
192 yield next(iter(err))
193
194 for field, error_list in error.items():
195 if field not in self.errors:
196 if field != NON_FIELD_ERRORS and field not in self.fields:
197 raise ValueError(
198 f"'{self.__class__.__name__}' has no field named '{field}'."
199 )
200 self._errors[field] = ValidationErrors()
201
202 self._errors[field].extend(error_list)
203
204 # The field had an error, so removed it from the final data
205 # (we use getattr here so errors can be added to uncleaned forms)
206 if field in getattr(self, "cleaned_data", {}):
207 del self.cleaned_data[field]
208
209 def full_clean(self):
210 """
211 Clean all of self.data and populate self._errors and self.cleaned_data.
212 """
213 self._errors = {}
214 if not self.is_bound: # Stop further processing.
215 return
216 self.cleaned_data = {}
217
218 self._clean_fields()
219 self._clean_form()
220 self._post_clean()
221
222 def _field_data_value(self, field, html_name):
223 if hasattr(self, f"parse_{html_name}"):
224 # Allow custom parsing from form data/files at the form level
225 return getattr(self, f"parse_{html_name}")()
226
227 return field.value_from_form_data(self.data, self.files, html_name)
228
229 def _clean_fields(self):
230 for name, bf in self._bound_items():
231 field = bf.field
232
233 if field.disabled:
234 value = bf.initial
235 else:
236 value = self._field_data_value(bf.field, bf.html_name)
237
238 try:
239 if isinstance(field, FileField):
240 value = field.clean(value, bf.initial)
241 else:
242 value = field.clean(value)
243 self.cleaned_data[name] = value
244 if hasattr(self, f"clean_{name}"):
245 value = getattr(self, f"clean_{name}")()
246 self.cleaned_data[name] = value
247 except ValidationError as e:
248 self.add_error(name, e)
249
250 def _clean_form(self):
251 try:
252 cleaned_data = self.clean()
253 except ValidationError as e:
254 self.add_error(None, e)
255 else:
256 if cleaned_data is not None:
257 self.cleaned_data = cleaned_data
258
259 def _post_clean(self):
260 """
261 An internal hook for performing additional cleaning after form cleaning
262 is complete. Used for model validation in model forms.
263 """
264 pass
265
266 def clean(self):
267 """
268 Hook for doing any extra form-wide cleaning after Field.clean() has been
269 called on every field. Any ValidationError raised by this method will
270 not be associated with a particular field; it will have a special-case
271 association with the field named '__all__'.
272 """
273 return self.cleaned_data
274
275 @cached_property
276 def changed_data(self):
277 return [name for name, bf in self._bound_items() if bf._has_changed()]
278
279 def get_initial_for_field(self, field, field_name):
280 """
281 Return initial data for field on form. Use initial data from the form
282 or the field, in that order. Evaluate callable values.
283 """
284 value = self.initial.get(field_name, field.initial)
285 if callable(value):
286 value = value()
287 return value
288
289
290class Form(BaseForm, metaclass=DeclarativeFieldsMetaclass):
291 "A collection of Fields, plus their associated data."
292
293 # This is a separate class from BaseForm in order to abstract the way
294 # self.fields is specified. This class (Form) is the one that does the
295 # fancy metaclass stuff purely for the semantic sugar -- it allows one
296 # to define a form using declarative syntax.
297 # BaseForm itself has no way of designating self.fields.