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