Author: jkocherhans Date: 2009-03-30 10:58:52 -0500 (Mon, 30 Mar 2009) New Revision: 10190
Modified: django/trunk/django/forms/formsets.py django/trunk/django/forms/models.py django/trunk/docs/topics/forms/formsets.txt django/trunk/tests/modeltests/model_formsets/models.py Log: Fixed #9284. Fixed #8813. BaseModelFormSet now calls ModelForm.save(). This is backwards-incompatible if you were doing things to 'initial' in BaseModelFormSet.__init__, or if you relied on the internal _total_form_count or _initial_form_count attributes of BaseFormSet. Those attributes are now public methods. Modified: django/trunk/django/forms/formsets.py =================================================================== --- django/trunk/django/forms/formsets.py 2009-03-30 03:36:35 UTC (rev 10189) +++ django/trunk/django/forms/formsets.py 2009-03-30 15:58:52 UTC (rev 10190) @@ -40,39 +40,51 @@ self.error_class = error_class self._errors = None self._non_form_errors = None - # initialization is different depending on whether we recieved data, initial, or nothing - if data or files: - self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix) - if self.management_form.is_valid(): - self._total_form_count = self.management_form.cleaned_data[TOTAL_FORM_COUNT] - self._initial_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT] - else: - raise ValidationError('ManagementForm data is missing or has been tampered with') - else: - if initial: - self._initial_form_count = len(initial) - if self._initial_form_count > self.max_num and self.max_num > 0: - self._initial_form_count = self.max_num - self._total_form_count = self._initial_form_count + self.extra - else: - self._initial_form_count = 0 - self._total_form_count = self.extra - if self._total_form_count > self.max_num and self.max_num > 0: - self._total_form_count = self.max_num - initial = {TOTAL_FORM_COUNT: self._total_form_count, - INITIAL_FORM_COUNT: self._initial_form_count} - self.management_form = ManagementForm(initial=initial, auto_id=self.auto_id, prefix=self.prefix) - # construct the forms in the formset self._construct_forms() def __unicode__(self): return self.as_table() + def _management_form(self): + """Returns the ManagementForm instance for this FormSet.""" + if self.data or self.files: + form = ManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix) + if not form.is_valid(): + raise ValidationError('ManagementForm data is missing or has been tampered with') + else: + form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial={ + TOTAL_FORM_COUNT: self.total_form_count(), + INITIAL_FORM_COUNT: self.initial_form_count() + }) + return form + management_form = property(_management_form) + + def total_form_count(self): + """Returns the total number of forms in this FormSet.""" + if self.data or self.files: + return self.management_form.cleaned_data[TOTAL_FORM_COUNT] + else: + total_forms = self.initial_form_count() + self.extra + if total_forms > self.max_num > 0: + total_forms = self.max_num + return total_forms + + def initial_form_count(self): + """Returns the number of forms that are required in this FormSet.""" + if self.data or self.files: + return self.management_form.cleaned_data[INITIAL_FORM_COUNT] + else: + # Use the length of the inital data if it's there, 0 otherwise. + initial_forms = self.initial and len(self.initial) or 0 + if initial_forms > self.max_num > 0: + initial_forms = self.max_num + return initial_forms + def _construct_forms(self): # instantiate all the forms and put them in self.forms self.forms = [] - for i in xrange(self._total_form_count): + for i in xrange(self.total_form_count()): self.forms.append(self._construct_form(i)) def _construct_form(self, i, **kwargs): @@ -89,7 +101,7 @@ except IndexError: pass # Allow extra forms to be empty. - if i >= self._initial_form_count: + if i >= self.initial_form_count(): defaults['empty_permitted'] = True defaults.update(kwargs) form = self.form(**defaults) @@ -97,13 +109,13 @@ return form def _get_initial_forms(self): - """Return a list of all the intial forms in this formset.""" - return self.forms[:self._initial_form_count] + """Return a list of all the initial forms in this formset.""" + return self.forms[:self.initial_form_count()] initial_forms = property(_get_initial_forms) def _get_extra_forms(self): """Return a list of all the extra forms in this formset.""" - return self.forms[self._initial_form_count:] + return self.forms[self.initial_form_count():] extra_forms = property(_get_extra_forms) # Maybe this should just go away? @@ -127,10 +139,10 @@ # that have had their deletion widget set to True if not hasattr(self, '_deleted_form_indexes'): self._deleted_form_indexes = [] - for i in range(0, self._total_form_count): + for i in range(0, self.total_form_count()): form = self.forms[i] # if this is an extra form and hasn't changed, don't consider it - if i >= self._initial_form_count and not form.has_changed(): + if i >= self.initial_form_count() and not form.has_changed(): continue if form.cleaned_data[DELETION_FIELD_NAME]: self._deleted_form_indexes.append(i) @@ -150,10 +162,10 @@ # by the form data. if not hasattr(self, '_ordering'): self._ordering = [] - for i in range(0, self._total_form_count): + for i in range(0, self.total_form_count()): form = self.forms[i] # if this is an extra form and hasn't changed, don't consider it - if i >= self._initial_form_count and not form.has_changed(): + if i >= self.initial_form_count() and not form.has_changed(): continue # don't add data marked for deletion to self.ordered_data if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]: @@ -221,7 +233,7 @@ self._errors = [] if not self.is_bound: # Stop further processing. return - for i in range(0, self._total_form_count): + for i in range(0, self.total_form_count()): form = self.forms[i] self._errors.append(form.errors) # Give self.clean() a chance to do cross-form validation. @@ -243,7 +255,7 @@ """A hook for adding extra fields on to each form instance.""" if self.can_order: # Only pre-fill the ordering field for initial forms. - if index < self._initial_form_count: + if index < self.initial_form_count(): form.fields[ORDERING_FIELD_NAME] = IntegerField(label=_(u'Order'), initial=index+1, required=False) else: form.fields[ORDERING_FIELD_NAME] = IntegerField(label=_(u'Order'), required=False) Modified: django/trunk/django/forms/models.py =================================================================== --- django/trunk/django/forms/models.py 2009-03-30 03:36:35 UTC (rev 10189) +++ django/trunk/django/forms/models.py 2009-03-30 15:58:52 UTC (rev 10190) @@ -54,6 +54,10 @@ # callable upload_to can use the values from other fields. if isinstance(f, models.FileField): file_field_list.append(f) + # OneToOneField doesn't allow assignment of None. Guard against that + # instead of allowing it and throwing an error. + if isinstance(f, models.OneToOneField) and cleaned_data[f.name] is None: + pass else: f.save_form_data(instance, cleaned_data[f.name]) @@ -266,7 +270,13 @@ lookup_kwargs = {} for field_name in unique_check: - lookup_kwargs[field_name] = self.cleaned_data[field_name] + lookup_value = self.cleaned_data[field_name] + # ModelChoiceField will return an object instance rather than + # a raw primary key value, so convert it to a pk value before + # using it in a lookup. + if isinstance(self.fields[field_name], ModelChoiceField): + lookup_value = lookup_value.pk + lookup_kwargs[field_name] = lookup_value qs = self.instance.__class__._default_manager.filter(**lookup_kwargs) @@ -357,12 +367,17 @@ queryset=None, **kwargs): self.queryset = queryset defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix} - defaults['initial'] = [model_to_dict(obj) for obj in self.get_queryset()] defaults.update(kwargs) super(BaseModelFormSet, self).__init__(**defaults) + def initial_form_count(self): + """Returns the number of forms that are required in this FormSet.""" + if not (self.data or self.files): + return len(self.get_queryset()) + return super(BaseModelFormSet, self).initial_form_count() + def _construct_form(self, i, **kwargs): - if i < self._initial_form_count: + if i < self.initial_form_count(): kwargs['instance'] = self.get_queryset()[i] return super(BaseModelFormSet, self)._construct_form(i, **kwargs) @@ -380,11 +395,11 @@ def save_new(self, form, commit=True): """Saves and returns a new model instance for the given form.""" - return save_instance(form, self.model(), exclude=[self._pk_field.name], commit=commit) + return form.save(commit=commit) def save_existing(self, form, instance, commit=True): """Saves and returns an existing model instance for the given form.""" - return save_instance(form, instance, exclude=[self._pk_field.name], commit=commit) + return form.save(commit=commit) def save(self, commit=True): """Saves model instances for every form, adding and changing instances @@ -410,7 +425,7 @@ existing_objects[obj.pk] = obj saved_instances = [] for form in self.initial_forms: - obj = existing_objects[form.cleaned_data[self._pk_field.name]] + obj = existing_objects[form.cleaned_data[self._pk_field.name].pk] if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]: self.deleted_objects.append(obj) obj.delete() @@ -438,10 +453,23 @@ def add_fields(self, form, index): """Add a hidden field for the object's primary key.""" - from django.db.models import AutoField + from django.db.models import AutoField, OneToOneField, ForeignKey self._pk_field = pk = self.model._meta.pk - if pk.auto_created or isinstance(pk, AutoField): - form.fields[self._pk_field.name] = IntegerField(required=False, widget=HiddenInput) + # If a pk isn't editable, then it won't be on the form, so we need to + # add it here so we can tell which object is which when we get the + # data back. Generally, pk.editable should be false, but for some + # reason, auto_created pk fields and AutoField's editable attribute is + # True, so check for that as well. + if (not pk.editable) or (pk.auto_created or isinstance(pk, AutoField)): + try: + pk_value = self.get_queryset()[index].pk + except IndexError: + pk_value = None + if isinstance(pk, OneToOneField) or isinstance(pk, ForeignKey): + qs = pk.rel.to._default_manager.get_query_set() + else: + qs = self.model._default_manager.get_query_set() + form.fields[self._pk_field.name] = ModelChoiceField(qs, initial=pk_value, required=False, widget=HiddenInput) super(BaseModelFormSet, self).add_fields(form, index) def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(), @@ -477,12 +505,16 @@ super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix, queryset=qs) - def _construct_forms(self): + def initial_form_count(self): if self.save_as_new: - self._total_form_count = self._initial_form_count - self._initial_form_count = 0 - super(BaseInlineFormSet, self)._construct_forms() + return 0 + return super(BaseInlineFormSet, self).initial_form_count() + def total_form_count(self): + if self.save_as_new: + return super(BaseInlineFormSet, self).initial_form_count() + return super(BaseInlineFormSet, self).total_form_count() + def _construct_form(self, i, **kwargs): form = super(BaseInlineFormSet, self)._construct_form(i, **kwargs) if self.save_as_new: @@ -498,14 +530,15 @@ get_default_prefix = classmethod(get_default_prefix) def save_new(self, form, commit=True): - fk_attname = self.fk.get_attname() - kwargs = {fk_attname: self.instance.pk} - new_obj = self.model(**kwargs) - if fk_attname == self._pk_field.attname or self._pk_field.auto_created: - exclude = [self._pk_field.name] - else: - exclude = [] - return save_instance(form, new_obj, exclude=exclude, commit=commit) + # Use commit=False so we can assign the parent key afterwards, then + # save the object. + obj = form.save(commit=False) + setattr(obj, self.fk.get_attname(), self.instance.pk) + obj.save() + # form.save_m2m() can be called via the formset later on if commit=False + if commit and hasattr(form, 'save_m2m'): + form.save_m2m() + return obj def add_fields(self, form, index): super(BaseInlineFormSet, self).add_fields(form, index) @@ -620,8 +653,6 @@ # ensure the we compare the values as equal types. if force_unicode(value) != force_unicode(self.parent_instance.pk): raise ValidationError(self.error_messages['invalid_choice']) - if self.pk_field: - return self.parent_instance.pk return self.parent_instance class ModelChoiceIterator(object): Modified: django/trunk/docs/topics/forms/formsets.txt =================================================================== --- django/trunk/docs/topics/forms/formsets.txt 2009-03-30 03:36:35 UTC (rev 10189) +++ django/trunk/docs/topics/forms/formsets.txt 2009-03-30 15:58:52 UTC (rev 10190) @@ -133,6 +133,20 @@ you are adding new forms via JavaScript, you should increment the count fields in this form as well. +.. versionadded:: 1.1 + +``total_form_count`` and ``initial_form_count`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``BaseModelFormSet`` has a couple of methods that are closely related to the +``ManagementForm``, ``total_form_count`` and ``initial_form_count``. + +``total_form_count`` returns the total number of forms in this formset. +``initial_form_count`` returns the number of forms in the formset that were +pre-filled, and is also used to determine how many forms are required. You +will probably never need to override either of these methods, so please be +sure you understand what they do before doing so. + Custom formset validation ~~~~~~~~~~~~~~~~~~~~~~~~~ Modified: django/trunk/tests/modeltests/model_formsets/models.py =================================================================== --- django/trunk/tests/modeltests/model_formsets/models.py 2009-03-30 03:36:35 UTC (rev 10189) +++ django/trunk/tests/modeltests/model_formsets/models.py 2009-03-30 15:58:52 UTC (rev 10190) @@ -1,6 +1,4 @@ - import datetime - from django import forms from django.db import models @@ -27,7 +25,7 @@ def __unicode__(self): return self.title - + class BookWithCustomPK(models.Model): my_pk = models.DecimalField(max_digits=5, decimal_places=0, primary_key=True) author = models.ForeignKey(Author) @@ -35,13 +33,13 @@ def __unicode__(self): return u'%s: %s' % (self.my_pk, self.title) - + class AlternateBook(Book): notes = models.CharField(max_length=100) - + def __unicode__(self): return u'%s - %s' % (self.title, self.notes) - + class AuthorMeeting(models.Model): name = models.CharField(max_length=100) authors = models.ManyToManyField(Author) @@ -68,7 +66,7 @@ auto_id = models.AutoField(primary_key=True) name = models.CharField(max_length=100) place = models.ForeignKey(Place) - + def __unicode__(self): return "%s at %s" % (self.name, self.place) @@ -81,7 +79,7 @@ class OwnerProfile(models.Model): owner = models.OneToOneField(Owner, primary_key=True) age = models.PositiveIntegerField() - + def __unicode__(self): return "%s is %d" % (self.owner.name, self.age) @@ -114,17 +112,17 @@ # using inlineformset_factory. class Repository(models.Model): name = models.CharField(max_length=25) - + def __unicode__(self): return self.name class Revision(models.Model): repository = models.ForeignKey(Repository) revision = models.CharField(max_length=40) - + class Meta: unique_together = (("repository", "revision"),) - + def __unicode__(self): return u"%s (%s)" % (self.revision, unicode(self.repository)) @@ -146,10 +144,24 @@ class Player(models.Model): team = models.ForeignKey(Team, null=True) name = models.CharField(max_length=100) - + def __unicode__(self): return self.name +# Models for testing custom ModelForm save methods in formsets and inline formsets +class Poet(models.Model): + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +class Poem(models.Model): + poet = models.ForeignKey(Poet) + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + __test__ = {'API_TESTS': """ >>> from datetime import date @@ -337,14 +349,45 @@ >>> AuthorFormSet = modelformset_factory(Author, max_num=2) >>> formset = AuthorFormSet(queryset=qs) ->>> [sorted(x.items()) for x in formset.initial] -[[('id', 1), ('name', u'Charles Baudelaire')], [('id', 3), ('name', u'Paul Verlaine')]] +>>> [x.name for x in formset.get_queryset()] +[u'Charles Baudelaire', u'Paul Verlaine'] >>> AuthorFormSet = modelformset_factory(Author, max_num=3) >>> formset = AuthorFormSet(queryset=qs) ->>> [sorted(x.items()) for x in formset.initial] -[[('id', 1), ('name', u'Charles Baudelaire')], [('id', 3), ('name', u'Paul Verlaine')], [('id', 2), ('name', u'Walt Whitman')]] +>>> [x.name for x in formset.get_queryset()] +[u'Charles Baudelaire', u'Paul Verlaine', u'Walt Whitman'] + +# ModelForm with a custom save method in a formset ########################### + +>>> class PoetForm(forms.ModelForm): +... def save(self, commit=True): +... # change the name to "Vladimir Mayakovsky" just to be a jerk. +... author = super(PoetForm, self).save(commit=False) +... author.name = u"Vladimir Mayakovsky" +... if commit: +... author.save() +... return author + +>>> PoetFormSet = modelformset_factory(Poet, form=PoetForm) + +>>> data = { +... 'form-TOTAL_FORMS': '3', # the number of forms rendered +... 'form-INITIAL_FORMS': '0', # the number of forms with initial data +... 'form-0-name': 'Walt Whitman', +... 'form-1-name': 'Charles Baudelaire', +... 'form-2-name': '', +... } + +>>> qs = Poet.objects.all() +>>> formset = PoetFormSet(data=data, queryset=qs) +>>> formset.is_valid() +True + +>>> formset.save() +[<Poet: Vladimir Mayakovsky>, <Poet: Vladimir Mayakovsky>] + + # Model inheritance in model formsets ######################################## >>> BetterAuthorFormSet = modelformset_factory(BetterAuthor) @@ -553,6 +596,36 @@ [<AlternateBook: Flowers of Evil - English translation of Les Fleurs du Mal>] +# ModelForm with a custom save method in an inline formset ################### + +>>> class PoemForm(forms.ModelForm): +... def save(self, commit=True): +... # change the name to "Brooklyn Bridge" just to be a jerk. +... poem = super(PoemForm, self).save(commit=False) +... poem.name = u"Brooklyn Bridge" +... if commit: +... poem.save() +... return poem + +>>> PoemFormSet = inlineformset_factory(Poet, Poem, form=PoemForm) + +>>> data = { +... 'poem_set-TOTAL_FORMS': '3', # the number of forms rendered +... 'poem_set-INITIAL_FORMS': '0', # the number of forms with initial data +... 'poem_set-0-name': 'The Cloud in Trousers', +... 'poem_set-1-name': 'I', +... 'poem_set-2-name': '', +... } + +>>> poet = Poet.objects.create(name='Vladimir Mayakovsky') +>>> formset = PoemFormSet(data=data, instance=poet) +>>> formset.is_valid() +True + +>>> formset.save() +[<Poem: Brooklyn Bridge>, <Poem: Brooklyn Bridge>] + + # Test a custom primary key ################################################### We need to ensure that it is displayed --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Django updates" group. To post to this group, send email to django-updates@googlegroups.com To unsubscribe from this group, send email to django-updates+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/django-updates?hl=en -~----------~----~----~----~------~----~------~--~---