/
forms.py
427 lines (330 loc) · 14.8 KB
/
forms.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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
"""Mixins for integrating a web API resource with a form."""
from django.core.exceptions import ValidationError
from django.forms.forms import NON_FIELD_ERRORS
from django.forms.models import model_to_dict
from django.utils.encoding import force_str
from djblets.webapi.errors import INVALID_FORM_DATA
class UpdateFormMixin(object):
"""A mixin for providing the ability to create and update using a form.
A WebAPIResource class using this mixin must set the :py:attr:`form_class`
attribute to a :py:class:`~django.forms.ModelForm` instance that
corresponds to the model being updated.
Classes using this mixin can provide methods of the form
:samp:`parse_{field_name}_field` to do parsing of form data before it is
passed to the form. Parser methods should be of the form:
.. code-block:: python
def parse_some_field(self, value, request, **kwargs):
return some_value
These methods may return either a single value or a list of values (in
the case where the corresponding field expects a list of values, such as a
:py:class:`~django.forms.ModelMultipleChoiceField`). They may also raise a
:py:class:`~django.core.exceptions.ValidationError`, though it's up to the
caller of :py:meth:`create_form` to catch this and return any suitable
errors.
Most implementations will want to call :py:meth:`handle_form_request` in
their POST/PUT handlers, and override behavior with the parsing methods.
Some may also want to override :py:meth:`save_form`,
:py:meth:`build_form_success_response`, or
:py:meth:`build_form_error_response` to customize behavior.
"""
#: The form class for updating models.
#:
#: This should be a subclass of :py:class:`django.forms.ModelForm`.
form_class = None
@property
def add_form_class(self):
"""The form class for creating new models.
This should be a subclass of :py:class:`django.forms.ModelForm`. It
defaults to :py:attr:`form_class`.
"""
return self.form_class
def handle_form_request(self, request, data=None, files=None,
instance=None, form_kwargs=None,
save_kwargs=None, **kwargs):
"""Handle an HTTP request for creating or updating through a form.
This can be called directly from a resource's
:py:meth:`~djblets.webapi.resources.base.WebAPIResource.create` or
:py:meth:`~djblets.webapi.resources.base.WebAPIResource.update` method
to parse request data, create the form, and handle errors or the
saving of the form.
Version Added:
1.0.9
Args:
request (django.http.HttpRequest):
The HTTP request from the client.
data (dict, optional):
The data to pass to :py:meth:`create_form`.
files (dict, optional):
Files to pass to the form.
instance (django.db.models.Model, optional):
An existing instance to update, if performing an HTTP PUT
request.
form_kwargs (dict, optional):
Keyword arguments to pass to the form's constructor.
save_kwargs (dict, optional):
Keyword arguments to pass to the form's
:py:meth:`ModelForm.save() <django.forms.ModelForm.save>`
method.
**kwargs (dict):
Keyword arguments to pass to
:py:meth:`build_form_error_response`,
:py:meth:`build_form_success_response`,
:py:meth:`create_form`,
:py:meth:`save_form`, and any field parsing methods.
Returns:
tuple or django.http.HttpResponse:
The response to send back to the client.
"""
is_created = instance is None
try:
form = self.create_form(instance=instance,
data=data,
files=files,
request=request,
form_kwargs=form_kwargs,
**kwargs)
except ValidationError as e:
return self.build_form_error_response(errors=e,
instance=instance,
**kwargs)
if not form.is_valid():
return self.build_form_error_response(form=form,
instance=instance,
**kwargs)
instance = self.save_form(form=form,
save_kwargs=save_kwargs,
**kwargs)
return self.build_form_success_response(form=form,
instance=instance,
is_created=is_created,
**kwargs)
def create_form(self, data, request, files=None, instance=None,
form_kwargs=None, **kwargs):
"""Create a new form and pre-fill it with data.
Version Changed:
1.0.9:
The initial values for form fields are now automatically provided
in the form data, if not otherwise overridden, making it easier to
construct forms.
Along with this, a :py:class:`~django.forms.ModelForm`'s ``fields``
and ``exclude`` lists are now factored in when populating the formw
with an instance's data.
Args:
data (dict):
The request data to pass to the form.
request (django.http.HttpRequest):
The HTTP request.
files (dict):
Files to pass to the form.
instance (django.db.models.Model, optional):
The instance model, if it exists. If this is not ``None``,
fields that appear in the form class's ``fields`` attribute
that do not appear in the ``data`` dict as keys will be copied
from the instance.
form_kwargs (dict, optional):
Additional keyword arguments to provide to the form's
constructor.
**kwargs (dict):
Additional arguments. These will be passed to the resource's
parser methods.
This contains anything passed as keyword arguments to
:py:meth:`handle_form_request`.
Returns:
django.forms.ModelForm:
The form with data filled.
Raises:
django.core.exceptions.ValidationError:
A field failed validation. This is allowed to be raised by
any ``parse_*`` methods defined on the resource.
"""
assert self.form_class, ('%s must define a form_class attribute.'
% self.__class__.__name__)
if instance is not None:
form_cls = self.form_class
else:
form_cls = self.add_form_class
form_data = self._get_initial_form_data(form_cls)
if instance is not None:
meta = form_cls._meta
form_data.update(model_to_dict(instance=instance,
fields=meta.fields,
exclude=meta.exclude))
form_data.update(self._parse_form_data(data, request, **kwargs))
# Dynamically provide the arguments we want to the form, so that
# any form classes lacking an argument (files, for instance) won't
# result in a crash so long as the equivalent argument is not provided.
if form_kwargs is None:
form_kwargs = {}
else:
form_kwargs = form_kwargs.copy()
if form_data is not None:
form_kwargs['data'] = form_data
if files is not None:
form_kwargs['files'] = files
if instance is not None:
form_kwargs['instance'] = instance
return form_cls(**form_kwargs)
def save_form(self, form, save_kwargs=None, **kwargs):
"""Save and return the results of the form.
This is a simple wrapper around calling :py:meth:`ModelForm.save()
<django.forms.ModelForm.save>`. It can be overridden by subclasses
that need to perform additional operations on the instance or form.
Version Added:
1.0.9
Args:
form (django.forms.Form):
The form to save.
save_kwargs (dict):
Any keyword arguments to pass when saving the form.
**kwargs (dict):
Additional keyword arguments passed by the caller. This
contains anything passed as keyword arguments to
:py:meth:`handle_form_request`.
Returns:
django.db.models.Model:
The saved model instance.
"""
return form.save(**(save_kwargs or {}))
def build_form_errors(self, form=None, errors=None, **kwargs):
"""Return a dictionary of field errors for use in a response payload.
This will convert each error to a string, resulting in a dictionary
mapping field names to lists of errors. This can be safely returned in
any API payload.
Version Added:
1.0.9
Args:
form (django.forms.Form, optional):
The form containing errors. This may be ``None`` if handling
a :py:class:`~django.core.exceptions.ValidationError` from
field parsing.
errors (django.core.exceptions.ValidationError, optional):
An explicit validation error to use for the payload. This will
be used if field parsing fails.
**kwargs (dict):
Additional keyword arguments passed by the caller. This
contains anything passed as keyword arguments to
:py:meth:`handle_form_request`.
Returns:
dict:
The dictionary of errors. Each key is a field name and each
value is a list of error strings.
"""
assert (form is None) != (errors is None), \
'Only one of form or errors can be provided.'
if form is not None:
return {
field_name: [force_str(e) for e in field_errors]
for field_name, field_errors in form.errors.items()
}
else:
if hasattr(errors, 'error_dict'):
return errors.message_dict
else:
return {
NON_FIELD_ERRORS: errors.messages,
}
def build_form_success_response(self, form, instance, is_created,
**kwargs):
"""Return a success response for a saved instance.
Version Added:
1.0.9
Args:
form (django.forms.Form):
The form that saved the instance.
instance (django.db.models.Model):
The saved instance.
is_created (bool):
Whether the instance was created.
**kwargs (dict):
Additional keyword arguments passed by the caller. This
contains anything passed as keyword arguments to
:py:meth:`handle_form_request`.
Returns:
tuple or django.http.HttpResponse:
The success response to return from the API handler.
"""
if is_created:
code = 201
else:
code = 200
return code, {
self.item_result_key: instance,
}
def build_form_error_response(self, form=None, errors=None, instance=None,
**kwargs):
"""Return an error response containing errors for form fields.
Version Added:
1.0.9
Args:
form (django.forms.Form, optional):
The form containing errors. This may be ``None`` if handling
a :py:class:`~django.core.exceptions.ValidationError` from
field parsing.
errors (django.core.exceptions.ValidationError, optional):
An explicit validation error to use for the payload. This will
be used if field parsing fails.
instance (django.db.models.Model, optional):
The existing instance, if any.
**kwargs (dict):
Additional keyword arguments passed by the caller. This
contains anything passed as ``response_kwargs`` to
:py:meth:`handle_form_request`.
Returns:
tuple or django.http.HttpResponse:
The error response to return from the API handler.
"""
return INVALID_FORM_DATA, {
'fields': self.build_form_errors(form=form,
errors=errors),
}
def _get_initial_form_data(self, form_cls):
"""Return the initial data for a form.
This will return the initial data from all fields in a form,
converting each into a format suitable for passing as bound data
for a form.
Args:
form_cls (type):
The form's class.
Returns:
dict:
The initial data for the fields in the form.
"""
initial = {}
for field_name, field in form_cls.base_fields.items():
data = field.initial
if callable(data):
data = data()
initial[field_name] = data
return initial
def _parse_form_data(self, form_data, request, **kwargs):
"""Parse the form data.
Args:
form_data (dict):
The request data.
request (django.http.HttpRequest):
The HTTP request.
**kwargs (dict):
Additional arguments to pass to parser methods.
Returns:
dict:
A mapping of field names to parsed values.
Raises:
django.core.exceptions.ValidationError:
A field failed validation containing errors for any fields
that failed to parse.
"""
parsed_data = form_data.copy()
errors = {}
for field, value in form_data.items():
parser = getattr(self, 'parse_%s_field' % field, None)
if parser is not None:
try:
parsed_data[field] = parser(value, request=request,
**kwargs)
except ValidationError as e:
errors[field] = e.messages
else:
parsed_data[field] = value
if errors:
raise ValidationError(errors)
return parsed_data