Django

Code

root/django/trunk/django/forms/models.py

Revision 9678, 29.7 kB (checked in by gwilson, 2 weeks ago)

Fixed #9882 -- Added alters_data = True to BaseModelForm.save method, thanks dc.

  • Property svn:eol-style set to native
Line 
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, force_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 try:
19     set
20 except NameError:
21     from sets import Set as set     # Python 2.3 fallback
22
23 __all__ = (
24     'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
25     'save_instance', 'form_for_fields', 'ModelChoiceField',
26     'ModelMultipleChoiceField',
27 )
28
29
30 def save_instance(form, instance, fields=None, fail_message='saved',
31                   commit=True, exclude=None):
32     """
33     Saves bound Form ``form``'s cleaned_data into model instance ``instance``.
34
35     If commit=True, then the changes to ``instance`` will be saved to the
36     database. Returns ``instance``.
37     """
38     from django.db import models
39     opts = instance._meta
40     if form.errors:
41         raise ValueError("The %s could not be %s because the data didn't"
42                          " validate." % (opts.object_name, fail_message))
43     cleaned_data = form.cleaned_data
44     file_field_list = []
45     for f in opts.fields:
46         if not f.editable or isinstance(f, models.AutoField) \
47                 or not f.name in cleaned_data:
48             continue
49         if fields and f.name not in fields:
50             continue
51         if exclude and f.name in exclude:
52             continue
53         # Defer saving file-type fields until after the other fields, so a
54         # callable upload_to can use the values from other fields.
55         if isinstance(f, models.FileField):
56             file_field_list.append(f)
57         else:
58             f.save_form_data(instance, cleaned_data[f.name])
59
60     for f in file_field_list:
61         f.save_form_data(instance, cleaned_data[f.name])
62
63     # Wrap up the saving of m2m data as a function.
64     def save_m2m():
65         opts = instance._meta
66         cleaned_data = form.cleaned_data
67         for f in opts.many_to_many:
68             if fields and f.name not in fields:
69                 continue
70             if f.name in cleaned_data:
71                 f.save_form_data(instance, cleaned_data[f.name])
72     if commit:
73         # If we are committing, save the instance and the m2m data immediately.
74         instance.save()
75         save_m2m()
76     else:
77         # We're not committing. Add a method to the form to allow deferred
78         # saving of m2m data.
79         form.save_m2m = save_m2m
80     return instance
81
82 def make_model_save(model, fields, fail_message):
83     """Returns the save() method for a Form."""
84     def save(self, commit=True):
85         return save_instance(self, model(), fields, fail_message, commit)
86     return save
87
88 def make_instance_save(instance, fields, fail_message):
89     """Returns the save() method for a Form."""
90     def save(self, commit=True):
91         return save_instance(self, instance, fields, fail_message, commit)
92     return save
93
94 def form_for_fields(field_list):
95     """
96     Returns a Form class for the given list of Django database field instances.
97     """
98     fields = SortedDict([(f.name, f.formfield())
99                          for f in field_list if f.editable])
100     return type('FormForFields', (BaseForm,), {'base_fields': fields})
101
102
103 # ModelForms #################################################################
104
105 def model_to_dict(instance, fields=None, exclude=None):
106     """
107     Returns a dict containing the data in ``instance`` suitable for passing as
108     a Form's ``initial`` keyword argument.
109
110     ``fields`` is an optional list of field names. If provided, only the named
111     fields will be included in the returned dict.
112
113     ``exclude`` is an optional list of field names. If provided, the named
114     fields will be excluded from the returned dict, even if they are listed in
115     the ``fields`` argument.
116     """
117     # avoid a circular import
118     from django.db.models.fields.related import ManyToManyField, OneToOneField
119     opts = instance._meta
120     data = {}
121     for f in opts.fields + opts.many_to_many:
122         if not f.editable:
123             continue
124         if fields and not f.name in fields:
125             continue
126         if exclude and f.name in exclude:
127             continue
128         if isinstance(f, ManyToManyField):
129             # If the object doesn't have a primry key yet, just use an empty
130             # list for its m2m fields. Calling f.value_from_object will raise
131             # an exception.
132             if instance.pk is None:
133                 data[f.name] = []
134             else:
135                 # MultipleChoiceWidget needs a list of pks, not object instances.
136                 data[f.name] = [obj.pk for obj in f.value_from_object(instance)]
137         else:
138             data[f.name] = f.value_from_object(instance)
139     return data
140
141 def fields_for_model(model, fields=None, exclude=None, formfield_callback=lambda f: f.formfield()):
142     """
143     Returns a ``SortedDict`` containing form fields for the given model.
144
145     ``fields`` is an optional list of field names. If provided, only the named
146     fields will be included in the returned fields.
147
148     ``exclude`` is an optional list of field names. If provided, the named
149     fields will be excluded from the returned fields, even if they are listed
150     in the ``fields`` argument.
151     """
152     # TODO: if fields is provided, it would be nice to return fields in that order
153     field_list = []
154     opts = model._meta
155     for f in opts.fields + opts.many_to_many:
156         if not f.editable:
157             continue
158         if fields and not f.name in fields:
159             continue
160         if exclude and f.name in exclude:
161             continue
162         formfield = formfield_callback(f)
163         if formfield:
164             field_list.append((f.name, formfield))
165     return SortedDict(field_list)
166
167 class ModelFormOptions(object):
168     def __init__(self, options=None):
169         self.model = getattr(options, 'model', None)
170         self.fields = getattr(options, 'fields', None)
171         self.exclude = getattr(options, 'exclude', None)
172
173
174 class ModelFormMetaclass(type):
175     def __new__(cls, name, bases, attrs):
176         formfield_callback = attrs.pop('formfield_callback',
177                 lambda f: f.formfield())
178         try:
179             parents = [b for b in bases if issubclass(b, ModelForm)]
180         except NameError:
181             # We are defining ModelForm itself.
182             parents = None
183         declared_fields = get_declared_fields(bases, attrs, False)
184         new_class = super(ModelFormMetaclass, cls).__new__(cls, name, bases,
185                 attrs)
186         if not parents:
187             return new_class
188
189         if 'media' not in attrs:
190             new_class.media = media_property(new_class)
191         opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
192         if opts.model:
193             # If a model is defined, extract form fields from it.
194             fields = fields_for_model(opts.model, opts.fields,
195                                       opts.exclude, formfield_callback)
196             # Override default model fields with any custom declared ones
197             # (plus, include all the other declared fields).
198             fields.update(declared_fields)
199         else:
200             fields = declared_fields
201         new_class.declared_fields = declared_fields
202         new_class.base_fields = fields
203         return new_class
204
205 class BaseModelForm(BaseForm):
206     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
207                  initial=None, error_class=ErrorList, label_suffix=':',
208                  empty_permitted=False, instance=None):
209         opts = self._meta
210         if instance is None:
211             # if we didn't get an instance, instantiate a new one
212             self.instance = opts.model()
213             object_data = {}
214         else:
215             self.instance = instance
216             object_data = model_to_dict(instance, opts.fields, opts.exclude)
217         # if initial was provided, it should override the values from instance
218         if initial is not None:
219             object_data.update(initial)
220         super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
221                                             error_class, label_suffix, empty_permitted)
222     def clean(self):
223         self.validate_unique()
224         return self.cleaned_data
225
226     def validate_unique(self):
227         from django.db.models.fields import FieldDoesNotExist
228
229         # Gather a list of checks to perform. We only perform unique checks
230         # for fields present and not None in cleaned_data.  Since this is a
231         # ModelForm, some fields may have been excluded; we can't perform a unique
232         # check on a form that is missing fields involved in that check.  It also does
233         # not make sense to check data that didn't validate, and since NULL does not
234         # equal NULL in SQL we should not do any unique checking for NULL values.
235         unique_checks = []
236         for check in self.instance._meta.unique_together[:]:
237             fields_on_form = [field for field in check if self.cleaned_data.get(field) is not None]
238             if len(fields_on_form) == len(check):
239                 unique_checks.append(check)
240
241         form_errors = []
242
243         # Gather a list of checks for fields declared as unique and add them to
244         # the list of checks. Again, skip empty fields and any that did not validate.
245         for name, field in self.fields.items():
246             try:
247                 f = self.instance._meta.get_field_by_name(name)[0]
248             except FieldDoesNotExist:
249                 # This is an extra field that's not on the ModelForm, ignore it
250                 continue
251             if f.unique and self.cleaned_data.get(name) is not None:
252                 unique_checks.append((name,))
253
254         bad_fields = set()
255         for unique_check in unique_checks:
256             # Try to look up an existing object with the same values as this
257             # object's values for all the unique field.
258
259             lookup_kwargs = {}
260             for field_name in unique_check:
261                 lookup_kwargs[field_name] = self.cleaned_data[field_name]
262
263             qs = self.instance.__class__._default_manager.filter(**lookup_kwargs)
264
265             # Exclude the current object from the query if we are editing an
266             # instance (as opposed to creating a new one)
267             if self.instance.pk is not None:
268                 qs = qs.exclude(pk=self.instance.pk)
269
270             # This cute trick with extra/values is the most efficient way to
271             # tell if a particular query returns any results.
272             if qs.extra(select={'a': 1}).values('a').order_by():
273                 model_name = capfirst(self.instance._meta.verbose_name)
274
275                 # A unique field
276                 if len(unique_check) == 1:
277                     field_name = unique_check[0]
278                     field_label = self.fields[field_name].label
279                     # Insert the error into the error dict, very sneaky
280                     self._errors[field_name] = ErrorList([
281                         _(u"%(model_name)s with this %(field_label)s already exists.") % \
282                         {'model_name': unicode(model_name),
283                          'field_label': unicode(field_label)}
284                     ])
285                 # unique_together
286                 else:
287                     field_labels = [self.fields[field_name].label for field_name in unique_check]
288                     field_labels = get_text_list(field_labels, _('and'))
289                     form_errors.append(
290                         _(u"%(model_name)s with this %(field_label)s already exists.") % \
291                         {'model_name': unicode(model_name),
292                          'field_label': unicode(field_labels)}
293                     )
294
295                 # Mark these fields as needing to be removed from cleaned data
296                 # later.
297                 for field_name in unique_check:
298                     bad_fields.add(field_name)
299
300         for field_name in bad_fields:
301             del self.cleaned_data[field_name]
302         if form_errors:
303             # Raise the unique together errors since they are considered
304             # form-wide.
305             raise ValidationError(form_errors)
306
307     def save(self, commit=True):
308         """
309         Saves this ``form``'s cleaned_data into model instance
310         ``self.instance``.
311
312         If commit=True, then the changes to ``instance`` will be saved to the
313         database. Returns ``instance``.
314         """
315         if self.instance.pk is None:
316             fail_message = 'created'
317         else:
318             fail_message = 'changed'
319         return save_instance(self, self.instance, self._meta.fields, fail_message, commit)
320
321     save.alters_data = True
322
323 class ModelForm(BaseModelForm):
324     __metaclass__ = ModelFormMetaclass
325
326 def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
327                        formfield_callback=lambda f: f.formfield()):
328     # HACK: we should be able to construct a ModelForm without creating
329     # and passing in a temporary inner class
330     class Meta:
331         pass
332     setattr(Meta, 'model', model)
333     setattr(Meta, 'fields', fields)
334     setattr(Meta, 'exclude', exclude)
335     class_name = model.__name__ + 'Form'
336     return ModelFormMetaclass(class_name, (form,), {'Meta': Meta,
337                               'formfield_callback': formfield_callback})
338
339
340 # ModelFormSets ##############################################################
341
342 class BaseModelFormSet(BaseFormSet):
343     """
344     A ``FormSet`` for editing a queryset and/or adding new objects to it.
345     """
346     model = None
347
348     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
349                  queryset=None, **kwargs):
350         self.queryset = queryset
351         defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
352         defaults['initial'] = [model_to_dict(obj) for obj in self.get_queryset()]
353         defaults.update(kwargs)
354         super(BaseModelFormSet, self).__init__(**defaults)
355
356     def _construct_form(self, i, **kwargs):
357         if i < self._initial_form_count:
358             kwargs['instance'] = self.get_queryset()[i]
359         return super(BaseModelFormSet, self)._construct_form(i, **kwargs)
360
361     def get_queryset(self):
362         if not hasattr(self, '_queryset'):
363             if self.queryset is not None:
364                 qs = self.queryset
365             else:
366                 qs = self.model._default_manager.get_query_set()
367             if self.max_num > 0:
368                 self._queryset = qs[:self.max_num]
369             else:
370                 self._queryset = qs
371         return self._queryset
372
373     def save_new(self, form, commit=True):
374         """Saves and returns a new model instance for the given form."""
375         return save_instance(form, self.model(), exclude=[self._pk_field.name], commit=commit)
376
377     def save_existing(self, form, instance, commit=True):
378         """Saves and returns an existing model instance for the given form."""
379         return save_instance(form, instance, exclude=[self._pk_field.name], commit=commit)
380
381     def save(self, commit=True):
382         """Saves model instances for every form, adding and changing instances
383         as necessary, and returns the list of instances.
384         """
385         if not commit:
386             self.saved_forms = []
387             def save_m2m():
388                 for form in self.saved_forms:
389                     form.save_m2m()
390             self.save_m2m = save_m2m
391         return self.save_existing_objects(commit) + self.save_new_objects(commit)
392
393     def save_existing_objects(self, commit=True):
394         self.changed_objects = []
395         self.deleted_objects = []
396         if not self.get_queryset():
397             return []
398
399         # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
400         existing_objects = {}
401         for obj in self.get_queryset():
402             existing_objects[obj.pk] = obj
403         saved_instances = []
404         for form in self.initial_forms:
405             obj = existing_objects[form.cleaned_data[self._pk_field.name]]
406             if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
407                 self.deleted_objects.append(obj)
408                 obj.delete()
409             else:
410                 if form.changed_data:
411                     self.changed_objects.append((obj, form.changed_data))
412                     saved_instances.append(self.save_existing(form, obj, commit=commit))
413                     if not commit:
414                         self.saved_forms.append(form)
415         return saved_instances
416
417     def save_new_objects(self, commit=True):
418         self.new_objects = []
419         for form in self.extra_forms:
420             if not form.has_changed():
421                 continue
422             # If someone has marked an add form for deletion, don't save the
423             # object.
424             if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
425                 continue
426             self.new_objects.append(self.save_new(form, commit=commit))
427             if not commit:
428                 self.saved_forms.append(form)
429         return self.new_objects
430
431     def add_fields(self, form, index):
432         """Add a hidden field for the object's primary key."""
433         from django.db.models import AutoField
434         self._pk_field = pk = self.model._meta.pk
435         if pk.auto_created or isinstance(pk, AutoField):
436             form.fields[self._pk_field.name] = IntegerField(required=False, widget=HiddenInput)
437         super(BaseModelFormSet, self).add_fields(form, index)
438
439 def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
440                          formset=BaseModelFormSet,
441                          extra=1, can_delete=False, can_order=False,
442                          max_num=0, fields=None, exclude=None):
443     """
444     Returns a FormSet class for the given Django model class.
445     """
446     form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
447                              formfield_callback=formfield_callback)
448     FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
449                               can_order=can_order, can_delete=can_delete)
450     FormSet.model = model
451     return FormSet
452
453
454 # InlineFormSets #############################################################
455
456 class BaseInlineFormSet(BaseModelFormSet):
457     """A formset for child objects related to a parent."""
458     def __init__(self, data=None, files=None, instance=None,
459                  save_as_new=False, prefix=None):
460         from django.db.models.fields.related import RelatedObject
461         if instance is None:
462             self.instance = self.model()
463         else:
464             self.instance = instance
465         self.save_as_new = save_as_new
466         # is there a better way to get the object descriptor?
467         self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
468         qs = self.model._default_manager.filter(**{self.fk.name: self.instance})
469         super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix or self.rel_name,
470                                                 queryset=qs)
471
472     def _construct_forms(self):
473         if self.save_as_new:
474             self._total_form_count = self._initial_form_count
475             self._initial_form_count = 0
476         super(BaseInlineFormSet, self)._construct_forms()
477
478     def _construct_form(self, i, **kwargs):
479         form = super(BaseInlineFormSet, self)._construct_form(i, **kwargs)
480         if self.save_as_new:
481             # Remove the primary key from the form's data, we are only
482             # creating new instances
483             form.data[form.add_prefix(self._pk_field.name)] = None
484         return form
485
486     def save_new(self, form, commit=True):
487         fk_attname = self.fk.get_attname()
488         kwargs = {fk_attname: self.instance.pk}
489         new_obj = self.model(**kwargs)
490         if fk_attname == self._pk_field.attname:
491             exclude =  [self._pk_field.name]
492         else:
493             exclude = []