From 0ea15f5650bc83483fc4ac18b379ae4add373573 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 31 Oct 2008 22:07:05 +0000 Subject: [PATCH] Fixed #8882 -- When a foreign key is among the unique_together fields in an inline formset properly handle it. git-svn-id: http://code.djangoproject.com/svn/django/trunk@9297 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/admin/helpers.py | 20 +++++ .../templates/admin/edit_inline/stacked.html | 1 + .../templates/admin/edit_inline/tabular.html | 2 +- django/forms/models.py | 48 +++++++++-- tests/modeltests/model_formsets/models.py | 84 +++++++++++++++---- 5 files changed, 129 insertions(+), 26 deletions(-) diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index a0d3237806..e969a41390 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -109,6 +109,8 @@ class InlineAdminFormSet(object): def fields(self): for field_name in flatten_fieldsets(self.fieldsets): + if self.formset.fk.name == field_name: + continue yield self.formset.form.base_fields[field_name] def _media(self): @@ -130,6 +132,10 @@ class InlineAdminForm(AdminForm): self.show_url = original and hasattr(original, 'get_absolute_url') super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields) + def __iter__(self): + for name, options in self.fieldsets: + yield InlineFieldset(self.formset, self.form, name, **options) + def field_count(self): # tabular.html uses this function for colspan value. num_of_fields = 1 # always has at least one field @@ -142,6 +148,9 @@ class InlineAdminForm(AdminForm): def pk_field(self): return AdminField(self.form, self.formset._pk_field.name, False) + + def fk_field(self): + return AdminField(self.form, self.formset.fk.name, False) def deletion_field(self): from django.forms.formsets import DELETION_FIELD_NAME @@ -151,6 +160,17 @@ class InlineAdminForm(AdminForm): from django.forms.formsets import ORDERING_FIELD_NAME return AdminField(self.form, ORDERING_FIELD_NAME, False) +class InlineFieldset(Fieldset): + def __init__(self, formset, *args, **kwargs): + self.formset = formset + super(InlineFieldset, self).__init__(*args, **kwargs) + + def __iter__(self): + for field in self.fields: + if self.formset.fk.name == field: + continue + yield Fieldline(self.form, field) + class AdminErrorList(forms.util.ErrorList): """ Stores all errors for the form/formsets in an add/change stage view. diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html index 840c1293e1..9d9f59896c 100644 --- a/django/contrib/admin/templates/admin/edit_inline/stacked.html +++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html @@ -18,6 +18,7 @@ {% include "admin/includes/fieldset.html" %} {% endfor %} {{ inline_admin_form.pk_field.field }} + {{ inline_admin_form.fk_field.field }} {% endfor %} diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html index 25dbe763f8..820928a143 100644 --- a/django/contrib/admin/templates/admin/edit_inline/tabular.html +++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html @@ -26,7 +26,7 @@ {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %} {% if inline_admin_form.show_url %}{% trans "View on site" %}{% endif %}

{% endif %} - {{ inline_admin_form.pk_field.field }} + {{ inline_admin_form.pk_field.field }} {{ inline_admin_form.fk_field.field }} {% spaceless %} {% for fieldset in inline_admin_form %} {% for line in fieldset %} diff --git a/django/forms/models.py b/django/forms/models.py index 0c98f52660..7f49324ff7 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -3,7 +3,7 @@ Helper functions for creating Form classes from Django models and database field objects. """ -from django.utils.encoding import smart_unicode +from django.utils.encoding import smart_unicode, force_unicode from django.utils.datastructures import SortedDict from django.utils.text import get_text_list, capfirst from django.utils.translation import ugettext_lazy as _ @@ -468,7 +468,7 @@ class BaseInlineFormSet(BaseModelFormSet): # creating new instances form.data[form.add_prefix(self._pk_field.name)] = None return form - + def get_queryset(self): """ Returns this FormSet's queryset, but restricted to children of @@ -485,7 +485,9 @@ class BaseInlineFormSet(BaseModelFormSet): def add_fields(self, form, index): super(BaseInlineFormSet, self).add_fields(form, index) if self._pk_field == self.fk: - form.fields[self._pk_field.name] = IntegerField(required=False, widget=HiddenInput) + form.fields[self._pk_field.name] = InlineForeignKeyField(self.instance, pk_field=True) + else: + form.fields[self.fk.name] = InlineForeignKeyField(self.instance, label=form.fields[self.fk.name].label) def _get_foreign_key(parent_model, model, fk_name=None): """ @@ -537,11 +539,6 @@ def inlineformset_factory(parent_model, model, form=ModelForm, # enforce a max_num=1 when the foreign key to the parent model is unique. if fk.unique: max_num = 1 - if exclude is not None: - exclude = list(exclude) - exclude.append(fk.name) - else: - exclude = [fk.name] kwargs = { 'form': form, 'formfield_callback': formfield_callback, @@ -560,6 +557,41 @@ def inlineformset_factory(parent_model, model, form=ModelForm, # Fields ##################################################################### +class InlineForeignKeyHiddenInput(HiddenInput): + def _has_changed(self, initial, data): + return False + +class InlineForeignKeyField(Field): + """ + A basic integer field that deals with validating the given value to a + given parent instance in an inline. + """ + default_error_messages = { + 'invalid_choice': _(u'The inline foreign key did not match the parent instance primary key.'), + } + + def __init__(self, parent_instance, *args, **kwargs): + self.parent_instance = parent_instance + self.pk_field = kwargs.pop("pk_field", False) + if self.parent_instance is not None: + kwargs["initial"] = self.parent_instance.pk + kwargs["required"] = False + kwargs["widget"] = InlineForeignKeyHiddenInput + super(InlineForeignKeyField, self).__init__(*args, **kwargs) + + def clean(self, value): + if value in EMPTY_VALUES: + if self.pk_field: + return None + # if there is no value act as we did before. + return self.parent_instance + # 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): def __init__(self, field): self.field = field diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py index 215f35b59a..4bf7b40eae 100644 --- a/tests/modeltests/model_formsets/models.py +++ b/tests/modeltests/model_formsets/models.py @@ -96,6 +96,24 @@ class Price(models.Model): class MexicanRestaurant(Restaurant): serves_tacos = models.BooleanField() +# models for testing unique_together validation when a fk is involved and +# 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)) + # models for testing callable defaults (see bug #7975). If you define a model # with a callable default value, you cannot rely on the initial value in a # form. @@ -375,9 +393,9 @@ admin system's edit inline functionality works. >>> formset = AuthorBooksFormSet(instance=author) >>> for form in formset.forms: ... print form.as_p() -

-

-

+

+

+

>>> data = { ... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered @@ -409,9 +427,9 @@ book. >>> formset = AuthorBooksFormSet(instance=author) >>> for form in formset.forms: ... print form.as_p() -

-

-

+

+

+

>>> data = { ... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered @@ -454,7 +472,7 @@ This is used in the admin for save_as functionality. True >>> new_author = Author.objects.create(name='Charles Baudelaire') ->>> formset.instance = new_author +>>> formset = AuthorBooksFormSet(data, instance=new_author, save_as_new=True) >>> [book for book in formset.save() if book.author.pk == new_author.pk] [, ] @@ -463,8 +481,8 @@ Test using a custom prefix on an inline formset. >>> formset = AuthorBooksFormSet(prefix="test") >>> for form in formset.forms: ... print form.as_p() -

-

+

+

# Test a custom primary key ################################################### @@ -486,8 +504,8 @@ We need to ensure that it is displayed >>> formset = FormSet(instance=place) >>> for form in formset.forms: ... print form.as_p() -

-

+

+

>>> data = { ... 'owner_set-TOTAL_FORMS': '2', @@ -506,9 +524,9 @@ True >>> formset = FormSet(instance=place) >>> for form in formset.forms: ... print form.as_p() -

-

-

+

+

+

>>> data = { ... 'owner_set-TOTAL_FORMS': '3', @@ -545,7 +563,7 @@ True >>> formset = FormSet(instance=owner) >>> for form in formset.forms: ... print form.as_p() -

+

>>> data = { ... 'ownerprofile-TOTAL_FORMS': '1', @@ -583,7 +601,7 @@ True >>> for form in formset.forms: ... print form.as_p()

-

+

# Foreign keys in parents ######################################## @@ -646,6 +664,38 @@ False >>> formset.errors [{'__all__': [u'Price with this Price and Quantity already exists.']}] +# unique_together with inlineformset_factory +# Also see bug #8882. + +>>> repository = Repository.objects.create(name=u'Test Repo') +>>> FormSet = inlineformset_factory(Repository, Revision, extra=1) +>>> data = { +... 'revision_set-TOTAL_FORMS': '1', +... 'revision_set-INITIAL_FORMS': '0', +... 'revision_set-0-repository': repository.pk, +... 'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76', +... 'revision_set-0-DELETE': '', +... } +>>> formset = FormSet(data, instance=repository) +>>> formset.is_valid() +True +>>> formset.save() +[] + +# attempt to save the same revision against against the same repo. +>>> data = { +... 'revision_set-TOTAL_FORMS': '1', +... 'revision_set-INITIAL_FORMS': '0', +... 'revision_set-0-repository': repository.pk, +... 'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76', +... 'revision_set-0-DELETE': '', +... } +>>> formset = FormSet(data, instance=repository) +>>> formset.is_valid() +False +>>> formset.errors +[{'__all__': [u'Revision with this Repository and Revision already exists.']}] + # Use of callable defaults (see bug #7975). >>> person = Person.objects.create(name='Ringo') @@ -660,7 +710,7 @@ False >>> now = form.fields['date_joined'].initial >>> print form.as_p()

-

+

# test for validation with callable defaults. Validations rely on hidden fields