1 | """
|
---|
2 | Helper functions for creating Form classes from Django models
|
---|
3 | and database field objects.
|
---|
4 | """
|
---|
5 |
|
---|
6 | from django.utils.encoding import smart_unicode
|
---|
7 | from django.utils.datastructures import SortedDict
|
---|
8 | from django.utils.text import get_text_list, capfirst
|
---|
9 | from django.utils.translation import ugettext_lazy as _
|
---|
10 |
|
---|
11 | from util import ValidationError, ErrorList
|
---|
12 | from forms import BaseForm, get_declared_fields
|
---|
13 | from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
|
---|
14 | from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
|
---|
15 | from widgets import media_property
|
---|
16 | from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME
|
---|
17 |
|
---|
18 | __all__ = (
|
---|
19 | 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
|
---|
20 | 'save_instance', 'form_for_fields', 'ModelChoiceField',
|
---|
21 | 'ModelMultipleChoiceField',
|
---|
22 | )
|
---|
23 |
|
---|
24 |
|
---|
25 | def save_instance(form, instance, fields=None, fail_message='saved',
|
---|
26 | commit=True, exclude=None):
|
---|
27 | """
|
---|
28 | Saves bound Form ``form``'s cleaned_data into model instance ``instance``.
|
---|
29 |
|
---|
30 | If commit=True, then the changes to ``instance`` will be saved to the
|
---|
31 | database. Returns ``instance``.
|
---|
32 | """
|
---|
33 | from django.db import models
|
---|
34 | opts = instance._meta
|
---|
35 | if form.errors:
|
---|
36 | raise ValueError("The %s could not be %s because the data didn't"
|
---|
37 | " validate." % (opts.object_name, fail_message))
|
---|
38 | cleaned_data = form.cleaned_data
|
---|
39 | for f in opts.fields:
|
---|
40 | if not f.editable or isinstance(f, models.AutoField) \
|
---|
41 | or not f.name in cleaned_data:
|
---|
42 | continue
|
---|
43 | if fields and f.name not in fields:
|
---|
44 | continue
|
---|
45 | if exclude and f.name in exclude:
|
---|
46 | continue
|
---|
47 | f.save_form_data(instance, cleaned_data[f.name])
|
---|
48 | # Wrap up the saving of m2m data as a function.
|
---|
49 | def save_m2m():
|
---|
50 | opts = instance._meta
|
---|
51 | cleaned_data = form.cleaned_data
|
---|
52 | for f in opts.many_to_many:
|
---|
53 | if fields and f.name not in fields:
|
---|
54 | continue
|
---|
55 | if f.name in cleaned_data:
|
---|
56 | f.save_form_data(instance, cleaned_data[f.name])
|
---|
57 | if commit:
|
---|
58 | # If we are committing, save the instance and the m2m data immediately.
|
---|
59 | instance.save()
|
---|
60 | save_m2m()
|
---|
61 | else:
|
---|
62 | # We're not committing. Add a method to the form to allow deferred
|
---|
63 | # saving of m2m data.
|
---|
64 | form.save_m2m = save_m2m
|
---|
65 | return instance
|
---|
66 |
|
---|
67 | def make_model_save(model, fields, fail_message):
|
---|
68 | """Returns the save() method for a Form."""
|
---|
69 | def save(self, commit=True):
|
---|
70 | return save_instance(self, model(), fields, fail_message, commit)
|
---|
71 | return save
|
---|
72 |
|
---|
73 | def make_instance_save(instance, fields, fail_message):
|
---|
74 | """Returns the save() method for a Form."""
|
---|
75 | def save(self, commit=True):
|
---|
76 | return save_instance(self, instance, fields, fail_message, commit)
|
---|
77 | return save
|
---|
78 |
|
---|
79 | def form_for_fields(field_list):
|
---|
80 | """
|
---|
81 | Returns a Form class for the given list of Django database field instances.
|
---|
82 | """
|
---|
83 | fields = SortedDict([(f.name, f.formfield())
|
---|
84 | for f in field_list if f.editable])
|
---|
85 | return type('FormForFields', (BaseForm,), {'base_fields': fields})
|
---|
86 |
|
---|
87 |
|
---|
88 | # ModelForms #################################################################
|
---|
89 |
|
---|
90 | def model_to_dict(instance, fields=None, exclude=None):
|
---|
91 | """
|
---|
92 | Returns a dict containing the data in ``instance`` suitable for passing as
|
---|
93 | a Form's ``initial`` keyword argument.
|
---|
94 |
|
---|
95 | ``fields`` is an optional list of field names. If provided, only the named
|
---|
96 | fields will be included in the returned dict.
|
---|
97 |
|
---|
98 | ``exclude`` is an optional list of field names. If provided, the named
|
---|
99 | fields will be excluded from the returned dict, even if they are listed in
|
---|
100 | the ``fields`` argument.
|
---|
101 | """
|
---|
102 | # avoid a circular import
|
---|
103 | from django.db.models.fields.related import ManyToManyField, OneToOneField
|
---|
104 | opts = instance._meta
|
---|
105 | data = {}
|
---|
106 | for f in opts.fields + opts.many_to_many:
|
---|
107 | if not f.editable:
|
---|
108 | continue
|
---|
109 | if fields and not f.name in fields:
|
---|
110 | continue
|
---|
111 | if exclude and f.name in exclude:
|
---|
112 | continue
|
---|
113 | if isinstance(f, ManyToManyField):
|
---|
114 | # If the object doesn't have a primry key yet, just use an empty
|
---|
115 | # list for its m2m fields. Calling f.value_from_object will raise
|
---|
116 | # an exception.
|
---|
117 | if instance.pk is None:
|
---|
118 | data[f.name] = []
|
---|
119 | else:
|
---|
120 | # MultipleChoiceWidget needs a list of pks, not object instances.
|
---|
121 | data[f.name] = [obj.pk for obj in f.value_from_object(instance)]
|
---|
122 | else:
|
---|
123 | data[f.name] = f.value_from_object(instance)
|
---|
124 | return data
|
---|
125 |
|
---|
126 | def fields_for_model(model, fields=None, exclude=None, formfield_callback=lambda f: f.formfield()):
|
---|
127 | """
|
---|
128 | Returns a ``SortedDict`` containing form fields for the given model.
|
---|
129 |
|
---|
130 | ``fields`` is an optional list of field names. If provided, only the named
|
---|
131 | fields will be included in the returned fields.
|
---|
132 |
|
---|
133 | ``exclude`` is an optional list of field names. If provided, the named
|
---|
134 | fields will be excluded from the returned fields, even if they are listed
|
---|
135 | in the ``fields`` argument.
|
---|
136 | """
|
---|
137 | # TODO: if fields is provided, it would be nice to return fields in that order
|
---|
138 | field_list = []
|
---|
139 | opts = model._meta
|
---|
140 | for f in opts.fields + opts.many_to_many:
|
---|
141 | if not f.editable:
|
---|
142 | continue
|
---|
143 | if fields and not f.name in fields:
|
---|
144 | continue
|
---|
145 | if exclude and f.name in exclude:
|
---|
146 | continue
|
---|
147 | formfield = formfield_callback(f)
|
---|
148 | if formfield:
|
---|
149 | field_list.append((f.name, formfield))
|
---|
150 | return SortedDict(field_list)
|
---|
151 |
|
---|
152 | class ModelFormOptions(object):
|
---|
153 | def __init__(self, options=None):
|
---|
154 | self.model = getattr(options, 'model', None)
|
---|
155 | self.fields = getattr(options, 'fields', None)
|
---|
156 | self.exclude = getattr(options, 'exclude', None)
|
---|
157 |
|
---|
158 |
|
---|
159 | class ModelFormMetaclass(type):
|
---|
160 | def __new__(cls, name, bases, attrs):
|
---|
161 | formfield_callback = attrs.pop('formfield_callback',
|
---|
162 | lambda f: f.formfield())
|
---|
163 | try:
|
---|
164 | parents = [b for b in bases if issubclass(b, ModelForm)]
|
---|
165 | except NameError:
|
---|
166 | # We are defining ModelForm itself.
|
---|
167 | parents = None
|
---|
168 | declared_fields = get_declared_fields(bases, attrs, False)
|
---|
169 | new_class = super(ModelFormMetaclass, cls).__new__(cls, name, bases,
|
---|
170 | attrs)
|
---|
171 | if not parents:
|
---|
172 | return new_class
|
---|
173 |
|
---|
174 | if 'media' not in attrs:
|
---|
175 | new_class.media = media_property(new_class)
|
---|
176 | opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
|
---|
177 | if opts.model:
|
---|
178 | # If a model is defined, extract form fields from it.
|
---|
179 | fields = fields_for_model(opts.model, opts.fields,
|
---|
180 | opts.exclude, formfield_callback)
|
---|
181 | # Override default model fields with any custom declared ones
|
---|
182 | # (plus, include all the other declared fields).
|
---|
183 | fields.update(declared_fields)
|
---|
184 | else:
|
---|
185 | fields = declared_fields
|
---|
186 | new_class.declared_fields = declared_fields
|
---|
187 | new_class.base_fields = fields
|
---|
188 | return new_class
|
---|
189 |
|
---|
190 | class BaseModelForm(BaseForm):
|
---|
191 | def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
|
---|
192 | initial=None, error_class=ErrorList, label_suffix=':',
|
---|
193 | empty_permitted=False, instance=None):
|
---|
194 | opts = self._meta
|
---|
195 | if instance is None:
|
---|
196 | # if we didn't get an instance, instantiate a new one
|
---|
197 | self.instance = opts.model()
|
---|
198 | object_data = {}
|
---|
199 | else:
|
---|
200 | self.instance = instance
|
---|
201 | object_data = model_to_dict(instance, opts.fields, opts.exclude)
|
---|
202 | # if initial was provided, it should override the values from instance
|
---|
203 | if initial is not None:
|
---|
204 | object_data.update(initial)
|
---|
205 | super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
|
---|
206 | error_class, label_suffix, empty_permitted)
|
---|
207 | def clean(self):
|
---|
208 | self.validate_unique()
|
---|
209 | return self.cleaned_data
|
---|
210 |
|
---|
211 | def validate_unique(self):
|
---|
212 | from django.db.models.fields import FieldDoesNotExist
|
---|
213 |
|
---|
214 | # Gather a list of checks to perform. Since this is a ModelForm, some
|
---|
215 | # fields may have been excluded; we can't perform a unique check on a
|
---|
216 | # form that is missing fields involved in that check.
|
---|
217 | unique_checks = []
|
---|
218 | for check in self.instance._meta.unique_together[:]:
|
---|
219 | fields_on_form = [field for field in check if field in self.fields]
|
---|
220 | if len(fields_on_form) == len(check):
|
---|
221 | unique_checks.append(check)
|
---|
222 |
|
---|
223 | form_errors = []
|
---|
224 |
|
---|
225 | # Gather a list of checks for fields declared as unique and add them to
|
---|
226 | # the list of checks. Again, skip fields not on the form.
|
---|
227 | for name, field in self.fields.items():
|
---|
228 | try:
|
---|
229 | f = self.instance._meta.get_field_by_name(name)[0]
|
---|
230 | except FieldDoesNotExist:
|
---|
231 | # This is an extra field that's not on the ModelForm, ignore it
|
---|
232 | continue
|
---|
233 | # MySQL can't handle ... WHERE pk IS NULL, so make sure we
|
---|
234 | # don't generate queries of that form.
|
---|
235 | is_null_pk = f.primary_key and self.cleaned_data[name] is None
|
---|
236 | if name in self.cleaned_data and f.unique and not is_null_pk:
|
---|
237 | unique_checks.append((name,))
|
---|
238 |
|
---|
239 | # Don't run unique checks on fields that already have an error.
|
---|
240 | unique_checks = [check for check in unique_checks if not [x in self._errors for x in check if x in self._errors]]
|
---|
241 |
|
---|
242 | for unique_check in unique_checks:
|
---|
243 | # Try to look up an existing object with the same values as this
|
---|
244 | # object's values for all the unique field.
|
---|
245 |
|
---|
246 | lookup_kwargs = {}
|
---|
247 | print '------------------------------------'
|
---|
248 | print unique_check
|
---|
249 | for field_name in unique_check:
|
---|
250 | print '========================================================'
|
---|
251 | print self.cleaned_data
|
---|
252 | lookup_kwargs[field_name] = self.cleaned_data[field_name]
|
---|
253 |
|
---|
254 | qs = self.instance.__class__._default_manager.filter(**lookup_kwargs)
|
---|
255 |
|
---|
256 | # Exclude the current object from the query if we are editing an
|
---|
257 | # instance (as opposed to creating a new one)
|
---|
258 | if self.instance.pk is not None:
|
---|
259 | qs = qs.exclude(pk=self.instance.pk)
|
---|
260 |
|
---|
261 | # This cute trick with extra/values is the most efficient way to
|
---|
262 | # tell if a particular query returns any results.
|
---|
263 | if qs.extra(select={'a': 1}).values('a').order_by():
|
---|
264 | model_name = capfirst(self.instance._meta.verbose_name)
|
---|
265 |
|
---|
266 | # A unique field
|
---|
267 | if len(unique_check) == 1:
|
---|
268 | field_name = unique_check[0]
|
---|
269 | field_label = self.fields[field_name].label
|
---|
270 | # Insert the error into the error dict, very sneaky
|
---|
271 | self._errors[field_name] = ErrorList([
|
---|
272 | _(u"%(model_name)s with this %(field_label)s already exists.") % \
|
---|
273 | {'model_name': unicode(model_name),
|
---|
274 | 'field_label': unicode(field_label)}
|
---|
275 | ])
|
---|
276 | # unique_together
|
---|
277 | else:
|
---|
278 | field_labels = [self.fields[field_name].label for field_name in unique_check]
|
---|
279 | field_labels = get_text_list(field_labels, _('and'))
|
---|
280 | form_errors.append(
|
---|
281 | _(u"%(model_name)s with this %(field_label)s already exists.") % \
|
---|
282 | {'model_name': unicode(model_name),
|
---|
283 | 'field_label': unicode(field_labels)}
|
---|
284 | )
|
---|
285 |
|
---|
286 | # Remove the data from the cleaned_data dict since it was invalid
|
---|
287 | #for field_name in unique_check:
|
---|
288 | # del self.cleaned_data[field_name]
|
---|
289 |
|
---|
290 | if form_errors:
|
---|
291 | # Raise the unique together errors since they are considered form-wide.
|
---|
292 | raise ValidationError(form_errors)
|
---|
293 |
|
---|
294 | def save(self, commit=True):
|
---|
295 | """
|
---|
296 | Saves this ``form``'s cleaned_data into model instance
|
---|
297 | ``self.instance``.
|
---|
298 |
|
---|
299 | If commit=True, then the changes to ``instance`` will be saved to the
|
---|
300 | database. Returns ``instance``.
|
---|
301 | """
|
---|
302 | if self.instance.pk is None:
|
---|
303 | fail_message = 'created'
|
---|
304 | else:
|
---|
305 | fail_message = 'changed'
|
---|
306 | return save_instance(self, self.instance, self._meta.fields, fail_message, commit)
|
---|
307 |
|
---|
308 | class ModelForm(BaseModelForm):
|
---|
309 | __metaclass__ = ModelFormMetaclass
|
---|
310 |
|
---|
311 | def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
|
---|
312 | formfield_callback=lambda f: f.formfield()):
|
---|
313 | # HACK: we should be able to construct a ModelForm without creating
|
---|
314 | # and passing in a temporary inner class
|
---|
315 | class Meta:
|
---|
316 | pass
|
---|
317 | setattr(Meta, 'model', model)
|
---|
318 | setattr(Meta, 'fields', fields)
|
---|
319 | setattr(Meta, 'exclude', exclude)
|
---|
320 | class_name = model.__name__ + 'Form'
|
---|
321 | return ModelFormMetaclass(class_name, (form,), {'Meta': Meta,
|
---|
322 | 'formfield_callback': formfield_callback})
|
---|
323 |
|
---|
324 |
|
---|
325 | # ModelFormSets ##############################################################
|
---|
326 |
|
---|
327 | class BaseModelFormSet(BaseFormSet):
|
---|
328 | """
|
---|
329 | A ``FormSet`` for editing a queryset and/or adding new objects to it.
|
---|
330 | """
|
---|
331 | model = None
|
---|
332 |
|
---|
333 | def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
|
---|
334 | queryset=None, **kwargs):
|
---|
335 | self.queryset = queryset
|
---|
336 | defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
|
---|
337 | defaults['initial'] = [model_to_dict(obj) for obj in self.get_queryset()]
|
---|
338 | defaults.update(kwargs)
|
---|
339 | super(BaseModelFormSet, self).__init__(**defaults)
|
---|
340 |
|
---|
341 | def _construct_form(self, i, **kwargs):
|
---|
342 | if i < self._initial_form_count:
|
---|
343 | kwargs['instance'] = self.get_queryset()[i]
|
---|
344 | return super(BaseModelFormSet, self)._construct_form(i, **kwargs)
|
---|
345 |
|
---|
346 | def get_queryset(self):
|
---|
347 | if not hasattr(self, '_queryset'):
|
---|
348 | if self.queryset is not None:
|
---|
349 | qs = self.queryset
|
---|
350 | else:
|
---|
351 | qs = self.model._default_manager.get_query_set()
|
---|
352 | if self.max_num > 0:
|
---|
353 | self._queryset = qs[:self.max_num]
|
---|
354 | else:
|
---|
355 | self._queryset = qs
|
---|
356 | return self._queryset
|
---|
357 |
|
---|
358 | def save_new(self, form, commit=True):
|
---|
359 | """Saves and returns a new model instance for the given form."""
|
---|
360 | return save_instance(form, self.model(), exclude=[self._pk_field.name], commit=commit)
|
---|
361 |
|
---|
362 | def save_existing(self, form, instance, commit=True):
|
---|
363 | """Saves and returns an existing model instance for the given form."""
|
---|
364 | return save_instance(form, instance, exclude=[self._pk_field.name], commit=commit)
|
---|
365 |
|
---|
366 | def save(self, commit=True):
|
---|
367 | """Saves model instances for every form, adding and changing instances
|
---|
368 | as necessary, and returns the list of instances.
|
---|
369 | """
|
---|
370 | if not commit:
|
---|
371 | self.saved_forms = []
|
---|
372 | def save_m2m():
|
---|
373 | for form in self.saved_forms:
|
---|
374 | form.save_m2m()
|
---|
375 | self.save_m2m = save_m2m
|
---|
376 | return self.save_existing_objects(commit) + self.save_new_objects(commit)
|
---|
377 |
|
---|
378 | def save_existing_objects(self, commit=True):
|
---|
379 | self.changed_objects = []
|
---|
380 | self.deleted_objects = []
|
---|
381 | if not self.get_queryset():
|
---|
382 | return []
|
---|
383 |
|
---|
384 | # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
|
---|
385 | existing_objects = {}
|
---|
386 | for obj in self.get_queryset():
|
---|
387 | existing_objects[obj.pk] = obj
|
---|
388 | saved_instances = []
|
---|
389 | for form in self.initial_forms:
|
---|
390 | obj = existing_objects[form.cleaned_data[self._pk_field.name]]
|
---|
391 | if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
|
---|
392 | self.deleted_objects.append(obj)
|
---|
393 | obj.delete()
|
---|
394 | else:
|
---|
395 | if form.changed_data:
|
---|
396 | self.changed_objects.append((obj, form.changed_data))
|
---|
397 | saved_instances.append(self.save_existing(form, obj, commit=commit))
|
---|
398 | if not commit:
|
---|
399 | self.saved_forms.append(form)
|
---|
400 | return saved_instances
|
---|
401 |
|
---|
402 | def save_new_objects(self, commit=True):
|
---|
403 | self.new_objects = []
|
---|
404 | for form in self.extra_forms:
|
---|
405 | if not form.has_changed():
|
---|
406 | continue
|
---|
407 | # If someone has marked an add form for deletion, don't save the
|
---|
408 | # object.
|
---|
409 | if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
|
---|
410 | continue
|
---|
411 | self.new_objects.append(self.save_new(form, commit=commit))
|
---|
412 | if not commit:
|
---|
413 | self.saved_forms.append(form)
|
---|
414 | return self.new_objects
|
---|
415 |
|
---|
416 | def add_fields(self, form, index):
|
---|
417 | """Add a hidden field for the object's primary key."""
|
---|
418 | from django.db.models import AutoField
|
---|
419 | self._pk_field = pk = self.model._meta.pk
|
---|
420 | if pk.auto_created or isinstance(pk, AutoField):
|
---|
421 | form.fields[self._pk_field.name] = IntegerField(required=False, widget=HiddenInput)
|
---|
422 | super(BaseModelFormSet, self).add_fields(form, index)
|
---|
423 |
|
---|
424 | def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
|
---|
425 | formset=BaseModelFormSet,
|
---|
426 | extra=1, can_delete=False, can_order=False,
|
---|
427 | max_num=0, fields=None, exclude=None):
|
---|
428 | """
|
---|
429 | Returns a FormSet class for the given Django model class.
|
---|
430 | """
|
---|
431 | form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
|
---|
432 | formfield_callback=formfield_callback)
|
---|
433 | FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
|
---|
434 | can_order=can_order, can_delete=can_delete)
|
---|
435 | FormSet.model = model
|
---|
436 | return FormSet
|
---|
437 |
|
---|
438 |
|
---|
439 | # InlineFormSets #############################################################
|
---|
440 |
|
---|
441 | class BaseInlineFormSet(BaseModelFormSet):
|
---|
442 | """A formset for child objects related to a parent."""
|
---|
443 | def __init__(self, data=None, files=None, instance=None,
|
---|
444 | save_as_new=False, prefix=None):
|
---|
445 | from django.db.models.fields.related import RelatedObject
|
---|
446 | self.instance = instance
|
---|
447 | self.save_as_new = save_as_new
|
---|
448 | # is there a better way to get the object descriptor?
|
---|
449 | self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
|
---|
450 | super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix or self.rel_name)
|
---|
451 |
|
---|
452 | def _construct_forms(self):
|
---|
453 | if self.save_as_new:
|
---|
454 | self._total_form_count = self._initial_form_count
|
---|
455 | self._initial_form_count = 0
|
---|
456 | super(BaseInlineFormSet, self)._construct_forms()
|
---|
457 |
|
---|
458 | def _construct_form(self, i, **kwargs):
|
---|
459 | form = super(BaseInlineFormSet, self)._construct_form(i, **kwargs)
|
---|
460 | if self.save_as_new:
|
---|
461 | # Remove the primary key from the form's data, we are only
|
---|
462 | # creating new instances
|
---|
463 | form.data[form.add_prefix(self._pk_field.name)] = None
|
---|
464 | return form
|
---|
465 |
|
---|
466 | def get_queryset(self):
|
---|
467 | """
|
---|
468 | Returns this FormSet's queryset, but restricted to children of
|
---|
469 | self.instance
|
---|
470 | """
|
---|
471 | kwargs = {self.fk.name: self.instance}
|
---|
472 | return self.model._default_manager.filter(**kwargs)
|
---|
473 |
|
---|
474 | def save_new(self, form, commit=True):
|
---|
475 | kwargs = {self.fk.get_attname(): self.instance.pk}
|
---|
476 | new_obj = self.model(**kwargs)
|
---|
477 | return save_instance(form, new_obj, exclude=[self._pk_field.name], commit=commit)
|
---|
478 |
|
---|
479 | def add_fields(self, form, index):
|
---|
480 | super(BaseInlineFormSet, self).add_fields(form, index)
|
---|
481 | if self._pk_field == self.fk:
|
---|
482 | form.fields[self._pk_field.name] = IntegerField(required=False, widget=HiddenInput)
|
---|
483 |
|
---|
484 | def _get_foreign_key(parent_model, model, fk_name=None):
|
---|
485 | """
|
---|
486 | Finds and returns the ForeignKey from model to parent if there is one.
|
---|
487 | If fk_name is provided, assume it is the name of the ForeignKey field.
|
---|
488 | """
|
---|
489 | # avoid circular import
|
---|
490 | from django.db.models import ForeignKey
|
---|
491 | opts = model._meta
|
---|
492 | if fk_name:
|
---|
493 | fks_to_parent = [f for f in opts.fields if f.name == fk_name]
|
---|
494 | if len(fks_to_parent) == 1:
|
---|
495 | fk = fks_to_parent[0]
|
---|
496 | if not isinstance(fk, ForeignKey) or \
|
---|
497 | (fk.rel.to != parent_model and
|
---|
498 | fk.rel.to not in parent_model._meta.get_parent_list()):
|
---|
499 | raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
|
---|
500 | elif len(fks_to_parent) == 0:
|
---|
501 | raise Exception("%s has no field named '%s'" % (model, fk_name))
|
---|
502 | else:
|
---|
503 | # Try to discover what the ForeignKey from model to parent_model is
|
---|
504 | fks_to_parent = [
|
---|
505 | f for f in opts.fields
|
---|
506 | if isinstance(f, ForeignKey)
|
---|
507 | and (f.rel.to == parent_model
|
---|
508 | or f.rel.to in parent_model._meta.get_parent_list())
|
---|
509 | ]
|
---|
510 | if len(fks_to_parent) == 1:
|
---|
511 | fk = fks_to_parent[0]
|
---|
512 | elif len(fks_to_parent) == 0:
|
---|
513 | raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
|
---|
514 | else:
|
---|
515 | raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
|
---|
516 | return fk
|
---|
517 |
|
---|
518 |
|
---|
519 | def inlineformset_factory(parent_model, model, form=ModelForm,
|
---|
520 | formset=BaseInlineFormSet, fk_name=None,
|
---|
521 | fields=None, exclude=None,
|
---|
522 | extra=3, can_order=False, can_delete=True, max_num=0,
|
---|
523 | formfield_callback=lambda f: f.formfield()):
|
---|
524 | """
|
---|
525 | Returns an ``InlineFormSet`` for the given kwargs.
|
---|
526 |
|
---|
527 | You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
|
---|
528 | to ``parent_model``.
|
---|
529 | """
|
---|
530 | fk = _get_foreign_key(parent_model, model, fk_name=fk_name)
|
---|
531 | # enforce a max_num=1 when the foreign key to the parent model is unique.
|
---|
532 | if fk.unique:
|
---|
533 | max_num = 1
|
---|
534 | if exclude is not None:
|
---|
535 | exclude.append(fk.name)
|
---|
536 | else:
|
---|
537 | exclude = [fk.name]
|
---|
538 | kwargs = {
|
---|
539 | 'form': form,
|
---|
540 | 'formfield_callback': formfield_callback,
|
---|
541 | 'formset': formset,
|
---|
542 | 'extra': extra,
|
---|
543 | 'can_delete': can_delete,
|
---|
544 | 'can_order': can_order,
|
---|
545 | 'fields': fields,
|
---|
546 | 'exclude': exclude,
|
---|
547 | 'max_num': max_num,
|
---|
548 | }
|
---|
549 | FormSet = modelformset_factory(model, **kwargs)
|
---|
550 | FormSet.fk = fk
|
---|
551 | return FormSet
|
---|
552 |
|
---|
553 |
|
---|
554 | # Fields #####################################################################
|
---|
555 |
|
---|
556 | class ModelChoiceIterator(object):
|
---|
557 | def __init__(self, field):
|
---|
558 | self.field = field
|
---|
559 | self.queryset = field.queryset
|
---|
560 |
|
---|
561 | def __iter__(self):
|
---|
562 | if self.field.empty_label is not None:
|
---|
563 | yield (u"", self.field.empty_label)
|
---|
564 | if self.field.cache_choices:
|
---|
565 | if self.field.choice_cache is None:
|
---|
566 | self.field.choice_cache = [
|
---|
567 | self.choice(obj) for obj in self.queryset.all()
|
---|
568 | ]
|
---|
569 | for choice in self.field.choice_cache:
|
---|
570 | yield choice
|
---|
571 | else:
|
---|
572 | for obj in self.queryset.all():
|
---|
573 | yield self.choice(obj)
|
---|
574 |
|
---|
575 | def choice(self, obj):
|
---|
576 | if self.field.to_field_name:
|
---|
577 | # FIXME: The try..except shouldn't be necessary here. But this is
|
---|
578 | # going in just before 1.0, so I want to be careful. Will check it
|
---|
579 | # out later.
|
---|
580 | try:
|
---|
581 | key = getattr(obj, self.field.to_field_name).pk
|
---|
582 | except AttributeError:
|
---|
583 | key = getattr(obj, self.field.to_field_name)
|
---|
584 | else:
|
---|
585 | key = obj.pk
|
---|
586 | return (key, self.field.label_from_instance(obj))
|
---|
587 |
|
---|
588 |
|
---|
589 | class ModelChoiceField(ChoiceField):
|
---|
590 | """A ChoiceField whose choices are a model QuerySet."""
|
---|
591 | # This class is a subclass of ChoiceField for purity, but it doesn't
|
---|
592 | # actually use any of ChoiceField's implementation.
|
---|
593 | default_error_messages = {
|
---|
594 | 'invalid_choice': _(u'Select a valid choice. That choice is not one of'
|
---|
595 | u' the available choices.'),
|
---|
596 | }
|
---|
597 |
|
---|
598 | def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
|
---|
599 | required=True, widget=None, label=None, initial=None,
|
---|
600 | help_text=None, to_field_name=None, *args, **kwargs):
|
---|
601 | self.empty_label = empty_label
|
---|
602 | self.cache_choices = cache_choices
|
---|
603 |
|
---|
604 | # Call Field instead of ChoiceField __init__() because we don't need
|
---|
605 | # ChoiceField.__init__().
|
---|
606 | Field.__init__(self, required, widget, label, initial, help_text,
|
---|
607 | *args, **kwargs)
|
---|
608 | self.queryset = queryset
|
---|
609 | self.choice_cache = None
|
---|
610 | self.to_field_name = to_field_name
|
---|
611 |
|
---|
612 | def _get_queryset(self):
|
---|
613 | return self._queryset
|
---|
614 |
|
---|
615 | def _set_queryset(self, queryset):
|
---|
616 | self._queryset = queryset
|
---|
617 | self.widget.choices = self.choices
|
---|
618 |
|
---|
619 | queryset = property(_get_queryset, _set_queryset)
|
---|
620 |
|
---|
621 | # this method will be used to create object labels by the QuerySetIterator.
|
---|
622 | # Override it to customize the label.
|
---|
623 | def label_from_instance(self, obj):
|
---|
624 | """
|
---|
625 | This method is used to convert objects into strings; it's used to
|
---|
626 | generate the labels for the choices presented by this object. Subclasses
|
---|
627 | can override this method to customize the display of the choices.
|
---|
628 | """
|
---|
629 | return smart_unicode(obj)
|
---|
630 |
|
---|
631 | def _get_choices(self):
|
---|
632 | # If self._choices is set, then somebody must have manually set
|
---|
633 | # the property self.choices. In this case, just return self._choices.
|
---|
634 | if hasattr(self, '_choices'):
|
---|
635 | return self._choices
|
---|
636 |
|
---|
637 | # Otherwise, execute the QuerySet in self.queryset to determine the
|
---|
638 | # choices dynamically. Return a fresh QuerySetIterator that has not been
|
---|
639 | # consumed. Note that we're instantiating a new QuerySetIterator *each*
|
---|
640 | # time _get_choices() is called (and, thus, each time self.choices is
|
---|
641 | # accessed) so that we can ensure the QuerySet has not been consumed. This
|
---|
642 | # construct might look complicated but it allows for lazy evaluation of
|
---|
643 | # the queryset.
|
---|
644 | return ModelChoiceIterator(self)
|
---|
645 |
|
---|
646 | choices = property(_get_choices, ChoiceField._set_choices)
|
---|
647 |
|
---|
648 | def clean(self, value):
|
---|
649 | Field.clean(self, value)
|
---|
650 | if value in EMPTY_VALUES:
|
---|
651 | return None
|
---|
652 | try:
|
---|
653 | key = self.to_field_name or 'pk'
|
---|
654 | value = self.queryset.get(**{key: value})
|
---|
655 | except self.queryset.model.DoesNotExist:
|
---|
656 | raise ValidationError(self.error_messages['invalid_choice'])
|
---|
657 | return value
|
---|
658 |
|
---|
659 | class ModelMultipleChoiceField(ModelChoiceField):
|
---|
660 | """A MultipleChoiceField whose choices are a model QuerySet."""
|
---|
661 | widget = SelectMultiple
|
---|
662 | hidden_widget = MultipleHiddenInput
|
---|
663 | default_error_messages = {
|
---|
664 | 'list': _(u'Enter a list of values.'),
|
---|
665 | 'invalid_choice': _(u'Select a valid choice. %s is not one of the'
|
---|
666 | u' available choices.'),
|
---|
667 | }
|
---|
668 |
|
---|
669 | def __init__(self, queryset, cache_choices=False, required=True,
|
---|
670 | widget=None, label=None, initial=None,
|
---|
671 | help_text=None, *args, **kwargs):
|
---|
672 | super(ModelMultipleChoiceField, self).__init__(queryset, None,
|
---|
673 | cache_choices, required, widget, label, initial, help_text,
|
---|
674 | *args, **kwargs)
|
---|
675 |
|
---|
676 | def clean(self, value):
|
---|
677 | if self.required and not value:
|
---|
678 | raise ValidationError(self.error_messages['required'])
|
---|
679 | elif not self.required and not value:
|
---|
680 | return []
|
---|
681 | if not isinstance(value, (list, tuple)):
|
---|
682 | raise ValidationError(self.error_messages['list'])
|
---|
683 | final_values = []
|
---|
684 | for val in value:
|
---|
685 | try:
|
---|
686 | obj = self.queryset.get(pk=val)
|
---|
687 | except self.queryset.model.DoesNotExist:
|
---|
688 | raise ValidationError(self.error_messages['invalid_choice'] % val)
|
---|
689 | else:
|
---|
690 | final_values.append(obj)
|
---|
691 | return final_values
|
---|