Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

[1.0.X] Fixed #8882 -- When a foreign key is among the unique_togethe…

…r fields in an inline formset properly handle it.

Backport of r9297 from trunk.

git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.0.X@9298 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 7b80af625bb20b7bbafa2224afdc8ee431428059 1 parent 4931612
Brian Rosner authored October 31, 2008
20  django/contrib/admin/helpers.py
@@ -109,6 +109,8 @@ def __iter__(self):
109 109
 
110 110
     def fields(self):
111 111
         for field_name in flatten_fieldsets(self.fieldsets):
  112
+            if self.formset.fk.name == field_name:
  113
+                continue
112 114
             yield self.formset.form.base_fields[field_name]
113 115
 
114 116
     def _media(self):
@@ -130,6 +132,10 @@ def __init__(self, formset, form, fieldsets, prepopulated_fields, original):
130 132
         self.show_url = original and hasattr(original, 'get_absolute_url')
131 133
         super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields)
132 134
     
  135
+    def __iter__(self):
  136
+        for name, options in self.fieldsets:
  137
+            yield InlineFieldset(self.formset, self.form, name, **options)
  138
+    
133 139
     def field_count(self):
134 140
         # tabular.html uses this function for colspan value.
135 141
         num_of_fields = 1 # always has at least one field
@@ -142,6 +148,9 @@ def field_count(self):
142 148
 
143 149
     def pk_field(self):
144 150
         return AdminField(self.form, self.formset._pk_field.name, False)
  151
+    
  152
+    def fk_field(self):
  153
+        return AdminField(self.form, self.formset.fk.name, False)
145 154
 
146 155
     def deletion_field(self):
147 156
         from django.forms.formsets import DELETION_FIELD_NAME
@@ -151,6 +160,17 @@ def ordering_field(self):
151 160
         from django.forms.formsets import ORDERING_FIELD_NAME
152 161
         return AdminField(self.form, ORDERING_FIELD_NAME, False)
153 162
 
  163
+class InlineFieldset(Fieldset):
  164
+    def __init__(self, formset, *args, **kwargs):
  165
+        self.formset = formset
  166
+        super(InlineFieldset, self).__init__(*args, **kwargs)
  167
+        
  168
+    def __iter__(self):
  169
+        for field in self.fields:
  170
+            if self.formset.fk.name == field:
  171
+                continue
  172
+            yield Fieldline(self.form, field)
  173
+            
154 174
 class AdminErrorList(forms.util.ErrorList):
155 175
     """
156 176
     Stores all errors for the form/formsets in an add/change stage view.
1  django/contrib/admin/templates/admin/edit_inline/stacked.html
@@ -18,6 +18,7 @@
18 18
     {% include "admin/includes/fieldset.html" %}
19 19
   {% endfor %}
20 20
   {{ inline_admin_form.pk_field.field }}
  21
+  {{ inline_admin_form.fk_field.field }}
21 22
 </div>
22 23
 {% endfor %}
23 24
 
2  django/contrib/admin/templates/admin/edit_inline/tabular.html
@@ -26,7 +26,7 @@
26 26
           {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %}
27 27
           {% if inline_admin_form.show_url %}<a href="../../../r/{{ inline_admin_form.original.content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a>{% endif %}
28 28
             </p>{% endif %}
29  
-          {{ inline_admin_form.pk_field.field }}
  29
+          {{ inline_admin_form.pk_field.field }} {{ inline_admin_form.fk_field.field }}
30 30
           {% spaceless %}
31 31
           {% for fieldset in inline_admin_form %}
32 32
             {% for line in fieldset %}
48  django/forms/models.py
@@ -3,7 +3,7 @@
3 3
 and database field objects.
4 4
 """
5 5
 
6  
-from django.utils.encoding import smart_unicode
  6
+from django.utils.encoding import smart_unicode, force_unicode
7 7
 from django.utils.datastructures import SortedDict
8 8
 from django.utils.text import get_text_list, capfirst
9 9
 from django.utils.translation import ugettext_lazy as _
@@ -468,7 +468,7 @@ def _construct_form(self, i, **kwargs):
468 468
             # creating new instances
469 469
             form.data[form.add_prefix(self._pk_field.name)] = None
470 470
         return form
471  
-
  471
+    
472 472
     def get_queryset(self):
473 473
         """
474 474
         Returns this FormSet's queryset, but restricted to children of
@@ -485,7 +485,9 @@ def save_new(self, form, commit=True):
485 485
     def add_fields(self, form, index):
486 486
         super(BaseInlineFormSet, self).add_fields(form, index)
487 487
         if self._pk_field == self.fk:
488  
-            form.fields[self._pk_field.name] = IntegerField(required=False, widget=HiddenInput)
  488
+            form.fields[self._pk_field.name] = InlineForeignKeyField(self.instance, pk_field=True)
  489
+        else:
  490
+            form.fields[self.fk.name] = InlineForeignKeyField(self.instance, label=form.fields[self.fk.name].label)
489 491
 
490 492
 def _get_foreign_key(parent_model, model, fk_name=None):
491 493
     """
@@ -537,11 +539,6 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
537 539
     # enforce a max_num=1 when the foreign key to the parent model is unique.
538 540
     if fk.unique:
539 541
         max_num = 1
540  
-    if exclude is not None:
541  
-        exclude = list(exclude)
542  
-        exclude.append(fk.name)
543  
-    else:
544  
-        exclude = [fk.name]
545 542
     kwargs = {
546 543
         'form': form,
547 544
         'formfield_callback': formfield_callback,
@@ -560,6 +557,41 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
560 557
 
561 558
 # Fields #####################################################################
562 559
 
  560
+class InlineForeignKeyHiddenInput(HiddenInput):
  561
+    def _has_changed(self, initial, data):
  562
+        return False
  563
+
  564
+class InlineForeignKeyField(Field):
  565
+    """
  566
+    A basic integer field that deals with validating the given value to a
  567
+    given parent instance in an inline.
  568
+    """
  569
+    default_error_messages = {
  570
+        'invalid_choice': _(u'The inline foreign key did not match the parent instance primary key.'),
  571
+    }
  572
+    
  573
+    def __init__(self, parent_instance, *args, **kwargs):
  574
+        self.parent_instance = parent_instance
  575
+        self.pk_field = kwargs.pop("pk_field", False)
  576
+        if self.parent_instance is not None:
  577
+            kwargs["initial"] = self.parent_instance.pk
  578
+        kwargs["required"] = False
  579
+        kwargs["widget"] = InlineForeignKeyHiddenInput
  580
+        super(InlineForeignKeyField, self).__init__(*args, **kwargs)
  581
+    
  582
+    def clean(self, value):
  583
+        if value in EMPTY_VALUES:
  584
+            if self.pk_field:
  585
+                return None
  586
+            # if there is no value act as we did before.
  587
+            return self.parent_instance
  588
+        # ensure the we compare the values as equal types.
  589
+        if force_unicode(value) != force_unicode(self.parent_instance.pk):
  590
+            raise ValidationError(self.error_messages['invalid_choice'])
  591
+        if self.pk_field:
  592
+            return self.parent_instance.pk
  593
+        return self.parent_instance
  594
+
563 595
 class ModelChoiceIterator(object):
564 596
     def __init__(self, field):
565 597
         self.field = field
84  tests/modeltests/model_formsets/models.py
@@ -96,6 +96,24 @@ class Meta:
96 96
 class MexicanRestaurant(Restaurant):
97 97
     serves_tacos = models.BooleanField()
98 98
 
  99
+# models for testing unique_together validation when a fk is involved and
  100
+# using inlineformset_factory.
  101
+class Repository(models.Model):
  102
+    name = models.CharField(max_length=25)
  103
+    
  104
+    def __unicode__(self):
  105
+        return self.name
  106
+
  107
+class Revision(models.Model):
  108
+    repository = models.ForeignKey(Repository)
  109
+    revision = models.CharField(max_length=40)
  110
+    
  111
+    class Meta:
  112
+        unique_together = (("repository", "revision"),)
  113
+    
  114
+    def __unicode__(self):
  115
+        return u"%s (%s)" % (self.revision, unicode(self.repository))
  116
+
99 117
 # models for testing callable defaults (see bug #7975). If you define a model
100 118
 # with a callable default value, you cannot rely on the initial value in a
101 119
 # form.
@@ -375,9 +393,9 @@ def __unicode__(self):
375 393
 >>> formset = AuthorBooksFormSet(instance=author)
376 394
 >>> for form in formset.forms:
377 395
 ...     print form.as_p()
378  
-<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p>
379  
-<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
380  
-<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
  396
+<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-author" value="1" id="id_book_set-0-author" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p>
  397
+<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-author" value="1" id="id_book_set-1-author" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
  398
+<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-author" value="1" id="id_book_set-2-author" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
381 399
 
382 400
 >>> data = {
383 401
 ...     'book_set-TOTAL_FORMS': '3', # the number of forms rendered
@@ -409,9 +427,9 @@ def __unicode__(self):
409 427
 >>> formset = AuthorBooksFormSet(instance=author)
410 428
 >>> for form in formset.forms:
411 429
 ...     print form.as_p()
412  
-<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p>
413  
-<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
414  
-<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
  430
+<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-author" value="1" id="id_book_set-0-author" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p>
  431
+<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-author" value="1" id="id_book_set-1-author" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
  432
+<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-author" value="1" id="id_book_set-2-author" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
415 433
 
416 434
 >>> data = {
417 435
 ...     'book_set-TOTAL_FORMS': '3', # the number of forms rendered
@@ -454,7 +472,7 @@ def __unicode__(self):
454 472
 True
455 473
 
456 474
 >>> new_author = Author.objects.create(name='Charles Baudelaire')
457  
->>> formset.instance = new_author
  475
+>>> formset = AuthorBooksFormSet(data, instance=new_author, save_as_new=True)
458 476
 >>> [book for book in formset.save() if book.author.pk == new_author.pk]
459 477
 [<Book: Les Fleurs du Mal>, <Book: Le Spleen de Paris>]
460 478
 
@@ -463,8 +481,8 @@ def __unicode__(self):
463 481
 >>> formset = AuthorBooksFormSet(prefix="test")
464 482
 >>> for form in formset.forms:
465 483
 ...     print form.as_p()
466  
-<p><label for="id_test-0-title">Title:</label> <input id="id_test-0-title" type="text" name="test-0-title" maxlength="100" /><input type="hidden" name="test-0-id" id="id_test-0-id" /></p>
467  
-<p><label for="id_test-1-title">Title:</label> <input id="id_test-1-title" type="text" name="test-1-title" maxlength="100" /><input type="hidden" name="test-1-id" id="id_test-1-id" /></p>
  484
+<p><label for="id_test-0-title">Title:</label> <input id="id_test-0-title" type="text" name="test-0-title" maxlength="100" /><input type="hidden" name="test-0-author" id="id_test-0-author" /><input type="hidden" name="test-0-id" id="id_test-0-id" /></p>
  485
+<p><label for="id_test-1-title">Title:</label> <input id="id_test-1-title" type="text" name="test-1-title" maxlength="100" /><input type="hidden" name="test-1-author" id="id_test-1-author" /><input type="hidden" name="test-1-id" id="id_test-1-id" /></p>
468 486
 
469 487
 # Test a custom primary key ###################################################
470 488
 
@@ -486,8 +504,8 @@ def __unicode__(self):
486 504
 >>> formset = FormSet(instance=place)
487 505
 >>> for form in formset.forms:
488 506
 ...     print form.as_p()
489  
-<p><label for="id_owner_set-0-name">Name:</label> <input id="id_owner_set-0-name" type="text" name="owner_set-0-name" maxlength="100" /><input type="hidden" name="owner_set-0-auto_id" id="id_owner_set-0-auto_id" /></p>
490  
-<p><label for="id_owner_set-1-name">Name:</label> <input id="id_owner_set-1-name" type="text" name="owner_set-1-name" maxlength="100" /><input type="hidden" name="owner_set-1-auto_id" id="id_owner_set-1-auto_id" /></p>
  507
+<p><label for="id_owner_set-0-name">Name:</label> <input id="id_owner_set-0-name" type="text" name="owner_set-0-name" maxlength="100" /><input type="hidden" name="owner_set-0-place" value="1" id="id_owner_set-0-place" /><input type="hidden" name="owner_set-0-auto_id" id="id_owner_set-0-auto_id" /></p>
  508
+<p><label for="id_owner_set-1-name">Name:</label> <input id="id_owner_set-1-name" type="text" name="owner_set-1-name" maxlength="100" /><input type="hidden" name="owner_set-1-place" value="1" id="id_owner_set-1-place" /><input type="hidden" name="owner_set-1-auto_id" id="id_owner_set-1-auto_id" /></p>
491 509
 
492 510
 >>> data = {
493 511
 ...     'owner_set-TOTAL_FORMS': '2',
@@ -506,9 +524,9 @@ def __unicode__(self):
506 524
 >>> formset = FormSet(instance=place)
507 525
 >>> for form in formset.forms:
508 526
 ...     print form.as_p()
509  
-<p><label for="id_owner_set-0-name">Name:</label> <input id="id_owner_set-0-name" type="text" name="owner_set-0-name" value="Joe Perry" maxlength="100" /><input type="hidden" name="owner_set-0-auto_id" value="1" id="id_owner_set-0-auto_id" /></p>
510  
-<p><label for="id_owner_set-1-name">Name:</label> <input id="id_owner_set-1-name" type="text" name="owner_set-1-name" maxlength="100" /><input type="hidden" name="owner_set-1-auto_id" id="id_owner_set-1-auto_id" /></p>
511  
-<p><label for="id_owner_set-2-name">Name:</label> <input id="id_owner_set-2-name" type="text" name="owner_set-2-name" maxlength="100" /><input type="hidden" name="owner_set-2-auto_id" id="id_owner_set-2-auto_id" /></p>
  527
+<p><label for="id_owner_set-0-name">Name:</label> <input id="id_owner_set-0-name" type="text" name="owner_set-0-name" value="Joe Perry" maxlength="100" /><input type="hidden" name="owner_set-0-place" value="1" id="id_owner_set-0-place" /><input type="hidden" name="owner_set-0-auto_id" value="1" id="id_owner_set-0-auto_id" /></p>
  528
+<p><label for="id_owner_set-1-name">Name:</label> <input id="id_owner_set-1-name" type="text" name="owner_set-1-name" maxlength="100" /><input type="hidden" name="owner_set-1-place" value="1" id="id_owner_set-1-place" /><input type="hidden" name="owner_set-1-auto_id" id="id_owner_set-1-auto_id" /></p>
  529
+<p><label for="id_owner_set-2-name">Name:</label> <input id="id_owner_set-2-name" type="text" name="owner_set-2-name" maxlength="100" /><input type="hidden" name="owner_set-2-place" value="1" id="id_owner_set-2-place" /><input type="hidden" name="owner_set-2-auto_id" id="id_owner_set-2-auto_id" /></p>
512 530
 
513 531
 >>> data = {
514 532
 ...     'owner_set-TOTAL_FORMS': '3',
@@ -545,7 +563,7 @@ def __unicode__(self):
545 563
 >>> formset = FormSet(instance=owner)
546 564
 >>> for form in formset.forms:
547 565
 ...     print form.as_p()
548  
-<p><label for="id_ownerprofile-0-age">Age:</label> <input type="text" name="ownerprofile-0-age" id="id_ownerprofile-0-age" /><input type="hidden" name="ownerprofile-0-owner" id="id_ownerprofile-0-owner" /></p>
  566
+<p><label for="id_ownerprofile-0-age">Age:</label> <input type="text" name="ownerprofile-0-age" id="id_ownerprofile-0-age" /><input type="hidden" name="ownerprofile-0-owner" value="1" id="id_ownerprofile-0-owner" /></p>
549 567
 
550 568
 >>> data = {
551 569
 ...     'ownerprofile-TOTAL_FORMS': '1',
@@ -583,7 +601,7 @@ def __unicode__(self):
583 601
 >>> for form in formset.forms:
584 602
 ...     print form.as_p()
585 603
 <p><label for="id_location_set-0-lat">Lat:</label> <input id="id_location_set-0-lat" type="text" name="location_set-0-lat" maxlength="100" /></p>
586  
-<p><label for="id_location_set-0-lon">Lon:</label> <input id="id_location_set-0-lon" type="text" name="location_set-0-lon" maxlength="100" /><input type="hidden" name="location_set-0-id" id="id_location_set-0-id" /></p>
  604
+<p><label for="id_location_set-0-lon">Lon:</label> <input id="id_location_set-0-lon" type="text" name="location_set-0-lon" maxlength="100" /><input type="hidden" name="location_set-0-place" value="1" id="id_location_set-0-place" /><input type="hidden" name="location_set-0-id" id="id_location_set-0-id" /></p>
587 605
 
588 606
 # Foreign keys in parents ########################################
589 607
 
@@ -646,6 +664,38 @@ def __unicode__(self):
646 664
 >>> formset.errors
647 665
 [{'__all__': [u'Price with this Price and Quantity already exists.']}]
648 666
 
  667
+# unique_together with inlineformset_factory
  668
+# Also see bug #8882.
  669
+
  670
+>>> repository = Repository.objects.create(name=u'Test Repo')
  671
+>>> FormSet = inlineformset_factory(Repository, Revision, extra=1)
  672
+>>> data = {
  673
+...     'revision_set-TOTAL_FORMS': '1',
  674
+...     'revision_set-INITIAL_FORMS': '0',
  675
+...     'revision_set-0-repository': repository.pk,
  676
+...     'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76',
  677
+...     'revision_set-0-DELETE': '',
  678
+... }
  679
+>>> formset = FormSet(data, instance=repository)
  680
+>>> formset.is_valid()
  681
+True
  682
+>>> formset.save()
  683
+[<Revision: 146239817507f148d448db38840db7c3cbf47c76 (Test Repo)>]
  684
+
  685
+# attempt to save the same revision against against the same repo.
  686
+>>> data = {
  687
+...     'revision_set-TOTAL_FORMS': '1',
  688
+...     'revision_set-INITIAL_FORMS': '0',
  689
+...     'revision_set-0-repository': repository.pk,
  690
+...     'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76',
  691
+...     'revision_set-0-DELETE': '',
  692
+... }
  693
+>>> formset = FormSet(data, instance=repository)
  694
+>>> formset.is_valid()
  695
+False
  696
+>>> formset.errors
  697
+[{'__all__': [u'Revision with this Repository and Revision already exists.']}]
  698
+
649 699
 # Use of callable defaults (see bug #7975).
650 700
 
651 701
 >>> person = Person.objects.create(name='Ringo')
@@ -660,7 +710,7 @@ def __unicode__(self):
660 710
 >>> now = form.fields['date_joined'].initial
661 711
 >>> print form.as_p()
662 712
 <p><label for="id_membership_set-0-date_joined">Date joined:</label> <input type="text" name="membership_set-0-date_joined" value="..." id="id_membership_set-0-date_joined" /><input type="hidden" name="initial-membership_set-0-date_joined" value="..." id="id_membership_set-0-date_joined" /></p>
663  
-<p><label for="id_membership_set-0-karma">Karma:</label> <input type="text" name="membership_set-0-karma" id="id_membership_set-0-karma" /><input type="hidden" name="membership_set-0-id" id="id_membership_set-0-id" /></p>
  713
+<p><label for="id_membership_set-0-karma">Karma:</label> <input type="text" name="membership_set-0-karma" id="id_membership_set-0-karma" /><input type="hidden" name="membership_set-0-person" value="1" id="id_membership_set-0-person" /><input type="hidden" name="membership_set-0-id" id="id_membership_set-0-id" /></p>
664 714
 
665 715
 # test for validation with callable defaults. Validations rely on hidden fields
666 716
 

0 notes on commit 7b80af6

Please sign in to comment.
Something went wrong with that request. Please try again.