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 #10271, #10281 -- Fixed the handling multiple inline mo…

…dels that share a common base class and have the link to the inline parent on the base class. Includes modifications that allow the equivalent handling for GenericFields. Thanks to Idan Gazit, Antti Kaihola (akaihola), and Alex Gaynor for their work on this patch.

Backport of r10017 from trunk.


git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.0.X@10019 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit ee0320f63400de67cb5a3363d95899bc21354172 1 parent ea35018
Russell Keith-Magee authored
1  AUTHORS
@@ -156,6 +156,7 @@ answer newbie questions, and generally made Django that much better:
156 156
     Marc Garcia <marc.garcia@accopensys.com>
157 157
     Alex Gaynor <alex.gaynor@gmail.com>
158 158
     Andy Gayton <andy-django@thecablelounge.com>
  159
+    Idan Gazit
159 160
     Baishampayan Ghose
160 161
     Dimitris Glezos <dimitris@glezos.com>
161 162
     glin@seznam.cz
29  django/contrib/admin/options.py
@@ -485,10 +485,16 @@ def add_view(self, request, form_url='', extra_context=None):
485 485
             else:
486 486
                 form_validated = False
487 487
                 new_object = self.model()
  488
+            prefixes = {}
488 489
             for FormSet in self.get_formsets(request):
  490
+                prefix = FormSet.get_default_prefix()
  491
+                prefixes[prefix] = prefixes.get(prefix, 0) + 1
  492
+                if prefixes[prefix] != 1:
  493
+                    prefix = "%s-%s" % (prefix, prefixes[prefix])
489 494
                 formset = FormSet(data=request.POST, files=request.FILES,
490 495
                                   instance=new_object,
491  
-                                  save_as_new=request.POST.has_key("_saveasnew"))
  496
+                                  save_as_new=request.POST.has_key("_saveasnew"),
  497
+                                  prefix=prefix)
492 498
                 formsets.append(formset)
493 499
             if all_valid(formsets) and form_validated:
494 500
                 self.save_model(request, new_object, form, change=False)
@@ -510,8 +516,13 @@ def add_view(self, request, form_url='', extra_context=None):
510 516
                 if isinstance(f, models.ManyToManyField):
511 517
                     initial[k] = initial[k].split(",")
512 518
             form = ModelForm(initial=initial)
  519
+            prefixes = {}
513 520
             for FormSet in self.get_formsets(request):
514  
-                formset = FormSet(instance=self.model())
  521
+                prefix = FormSet.get_default_prefix()
  522
+                prefixes[prefix] = prefixes.get(prefix, 0) + 1
  523
+                if prefixes[prefix] != 1:
  524
+                    prefix = "%s-%s" % (prefix, prefixes[prefix])
  525
+                formset = FormSet(instance=self.model(), prefix=prefix)
515 526
                 formsets.append(formset)
516 527
 
517 528
         adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
@@ -571,9 +582,14 @@ def change_view(self, request, object_id, extra_context=None):
571 582
             else:
572 583
                 form_validated = False
573 584
                 new_object = obj
  585
+            prefixes = {}
574 586
             for FormSet in self.get_formsets(request, new_object):
  587
+                prefix = FormSet.get_default_prefix()
  588
+                prefixes[prefix] = prefixes.get(prefix, 0) + 1
  589
+                if prefixes[prefix] != 1:
  590
+                    prefix = "%s-%s" % (prefix, prefixes[prefix])
575 591
                 formset = FormSet(request.POST, request.FILES,
576  
-                                  instance=new_object)
  592
+                                  instance=new_object, prefix=prefix)
577 593
                 formsets.append(formset)
578 594
 
579 595
             if all_valid(formsets) and form_validated:
@@ -588,8 +604,13 @@ def change_view(self, request, object_id, extra_context=None):
588 604
                 
589 605
         else:
590 606
             form = ModelForm(instance=obj)
  607
+            prefixes = {}
591 608
             for FormSet in self.get_formsets(request, obj):
592  
-                formset = FormSet(instance=obj)
  609
+                prefix = FormSet.get_default_prefix()
  610
+                prefixes[prefix] = prefixes.get(prefix, 0) + 1
  611
+                if prefixes[prefix] != 1:
  612
+                    prefix = "%s-%s" % (prefix, prefixes[prefix])
  613
+                formset = FormSet(instance=obj, prefix=prefix)
593 614
                 formsets.append(formset)
594 615
 
595 616
         adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
12  django/contrib/contenttypes/generic.py
@@ -291,7 +291,7 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
291 291
     ct_field_name = "content_type"
292 292
     ct_fk_field_name = "object_id"
293 293
 
294  
-    def __init__(self, data=None, files=None, instance=None, save_as_new=None):
  294
+    def __init__(self, data=None, files=None, instance=None, save_as_new=None, prefix=None):
295 295
         opts = self.model._meta
296 296
         self.instance = instance
297 297
         self.rel_name = '-'.join((
@@ -300,9 +300,17 @@ def __init__(self, data=None, files=None, instance=None, save_as_new=None):
300 300
         ))
301 301
         super(BaseGenericInlineFormSet, self).__init__(
302 302
             queryset=self.get_queryset(), data=data, files=files,
303  
-            prefix=self.rel_name
  303
+            prefix=prefix
304 304
         )
305 305
 
  306
+    #@classmethod
  307
+    def get_default_prefix(cls):
  308
+        opts = cls.model._meta
  309
+        return '-'.join((opts.app_label, opts.object_name.lower(),
  310
+                        cls.ct_field.name, cls.ct_fk_field.name,
  311
+        ))
  312
+    get_default_prefix = classmethod(get_default_prefix)
  313
+
306 314
     def get_queryset(self):
307 315
         # Avoid a circular import.
308 316
         from django.contrib.contenttypes.models import ContentType
13  django/forms/formsets.py
@@ -32,7 +32,7 @@ class BaseFormSet(StrAndUnicode):
32 32
     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
33 33
                  initial=None, error_class=ErrorList):
34 34
         self.is_bound = data is not None or files is not None
35  
-        self.prefix = prefix or 'form'
  35
+        self.prefix = prefix or self.get_default_prefix()
36 36
         self.auto_id = auto_id
37 37
         self.data = data
38 38
         self.files = files
@@ -62,7 +62,7 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
62 62
             initial = {TOTAL_FORM_COUNT: self._total_form_count,
63 63
                        INITIAL_FORM_COUNT: self._initial_form_count}
64 64
             self.management_form = ManagementForm(initial=initial, auto_id=self.auto_id, prefix=self.prefix)
65  
-        
  65
+
66 66
         # construct the forms in the formset
67 67
         self._construct_forms()
68 68
 
@@ -74,7 +74,7 @@ def _construct_forms(self):
74 74
         self.forms = []
75 75
         for i in xrange(self._total_form_count):
76 76
             self.forms.append(self._construct_form(i))
77  
-    
  77
+
78 78
     def _construct_form(self, i, **kwargs):
79 79
         """
80 80
         Instantiates and returns the i-th form instance in a formset.
@@ -118,7 +118,7 @@ def _get_cleaned_data(self):
118 118
 
119 119
     def _get_deleted_forms(self):
120 120
         """
121  
-        Returns a list of forms that have been marked for deletion. Raises an 
  121
+        Returns a list of forms that have been marked for deletion. Raises an
122 122
         AttributeError if deletion is not allowed.
123 123
         """
124 124
         if not self.is_valid() or not self.can_delete:
@@ -176,6 +176,11 @@ def compare_ordering_values(x, y):
176 176
         return [self.forms[i[0]] for i in self._ordering]
177 177
     ordered_forms = property(_get_ordered_forms)
178 178
 
  179
+    #@classmethod
  180
+    def get_default_prefix(cls):
  181
+        return 'form'
  182
+    get_default_prefix = classmethod(get_default_prefix)
  183
+
179 184
     def non_form_errors(self):
180 185
         """
181 186
         Returns an ErrorList of errors that aren't associated with a particular
12  django/forms/models.py
@@ -249,8 +249,8 @@ def validate_unique(self):
249 249
                 # This is an extra field that's not on the ModelForm, ignore it
250 250
                 continue
251 251
             if not isinstance(f, ModelField):
252  
-                # This is an extra field that happens to have a name that matches, 
253  
-                # for example, a related object accessor for this model.  So 
  252
+                # This is an extra field that happens to have a name that matches,
  253
+                # for example, a related object accessor for this model.  So
254 254
                 # get_field_by_name found it, but it is not a Field so do not proceed
255 255
                 # to use it as if it were.
256 256
                 continue
@@ -472,7 +472,7 @@ def __init__(self, data=None, files=None, instance=None,
472 472
         # is there a better way to get the object descriptor?
473 473
         self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
474 474
         qs = self.model._default_manager.filter(**{self.fk.name: self.instance})
475  
-        super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix or self.rel_name,
  475
+        super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix,
476 476
                                                 queryset=qs)
477 477
 
478 478
     def _construct_forms(self):
@@ -489,6 +489,12 @@ def _construct_form(self, i, **kwargs):
489 489
             form.data[form.add_prefix(self._pk_field.name)] = None
490 490
         return form
491 491
 
  492
+    #@classmethod
  493
+    def get_default_prefix(cls):
  494
+        from django.db.models.fields.related import RelatedObject
  495
+        return RelatedObject(cls.fk.rel.to, cls.model, cls.fk).get_accessor_name()
  496
+    get_default_prefix = classmethod(get_default_prefix)
  497
+
492 498
     def save_new(self, form, commit=True):
493 499
         fk_attname = self.fk.get_attname()
494 500
         kwargs = {fk_attname: self.instance.pk}
5  tests/modeltests/generic_relations/models.py
@@ -238,4 +238,9 @@ def __unicode__(self):
238 238
 <p><label for="id_generic_relations-taggeditem-content_type-object_id-1-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-1-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-1-tag" maxlength="50" /></p>
239 239
 <p><label for="id_generic_relations-taggeditem-content_type-object_id-1-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-1-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-1-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-1-id" id="id_generic_relations-taggeditem-content_type-object_id-1-id" /></p>
240 240
 
  241
+>>> formset = GenericFormSet(instance=lion, prefix='x')
  242
+>>> for form in formset.forms:
  243
+...     print form.as_p()
  244
+<p><label for="id_x-0-tag">Tag:</label> <input id="id_x-0-tag" type="text" name="x-0-tag" maxlength="50" /></p>
  245
+<p><label for="id_x-0-DELETE">Delete:</label> <input type="checkbox" name="x-0-DELETE" id="id_x-0-DELETE" /><input type="hidden" name="x-0-id" id="id_x-0-id" /></p>
241 246
 """}
56  tests/regressiontests/admin_views/models.py
@@ -20,7 +20,7 @@ class Article(models.Model):
20 20
 
21 21
     def __unicode__(self):
22 22
         return self.title
23  
-    
  23
+
24 24
     def model_year(self):
25 25
         return self.date.year
26 26
     model_year.admin_order_field = 'date'
@@ -54,14 +54,14 @@ class Meta:
54 54
 
55 55
 class ChapterXtra1(models.Model):
56 56
     chap = models.OneToOneField(Chapter, verbose_name=u'¿Chap?')
57  
-    xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?') 
  57
+    xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?')
58 58
 
59 59
     def __unicode__(self):
60 60
         return u'¿Xtra1: %s' % self.xtra
61 61
 
62 62
 class ChapterXtra2(models.Model):
63 63
     chap = models.OneToOneField(Chapter, verbose_name=u'¿Chap?')
64  
-    xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?') 
  64
+    xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?')
65 65
 
66 66
     def __unicode__(self):
67 67
         return u'¿Xtra2: %s' % self.xtra
@@ -87,7 +87,7 @@ def changelist_view(self, request):
87 87
                 'extra_var': 'Hello!'
88 88
             }
89 89
         )
90  
-        
  90
+
91 91
     def modeladmin_year(self, obj):
92 92
         return obj.date.year
93 93
     modeladmin_year.admin_order_field = 'date'
@@ -121,7 +121,7 @@ def __unicode__(self):
121 121
 
122 122
 class Color(models.Model):
123 123
     value = models.CharField(max_length=10)
124  
-    warm = models.BooleanField()   
  124
+    warm = models.BooleanField()
125 125
     def __unicode__(self):
126 126
         return self.value
127 127
 
@@ -134,12 +134,56 @@ def __unicode__(self):
134 134
 class ThingAdmin(admin.ModelAdmin):
135 135
     list_filter = ('color',)
136 136
 
  137
+class Persona(models.Model):
  138
+    """
  139
+    A simple persona associated with accounts, to test inlining of related
  140
+    accounts which inherit from a common accounts class.
  141
+    """
  142
+    name = models.CharField(blank=False,  max_length=80)
  143
+    def __unicode__(self):
  144
+        return self.name
  145
+
  146
+class Account(models.Model):
  147
+    """
  148
+    A simple, generic account encapsulating the information shared by all
  149
+    types of accounts.
  150
+    """
  151
+    username = models.CharField(blank=False,  max_length=80)
  152
+    persona = models.ForeignKey(Persona, related_name="accounts")
  153
+    servicename = u'generic service'
  154
+
  155
+    def __unicode__(self):
  156
+        return "%s: %s" % (self.servicename, self.username)
  157
+
  158
+class FooAccount(Account):
  159
+    """A service-specific account of type Foo."""
  160
+    servicename = u'foo'
  161
+
  162
+class BarAccount(Account):
  163
+    """A service-specific account of type Bar."""
  164
+    servicename = u'bar'
  165
+
  166
+class FooAccountAdmin(admin.StackedInline):
  167
+    model = FooAccount
  168
+    extra = 1
  169
+
  170
+class BarAccountAdmin(admin.StackedInline):
  171
+    model = BarAccount
  172
+    extra = 1
  173
+
  174
+class PersonaAdmin(admin.ModelAdmin):
  175
+    inlines = (
  176
+        FooAccountAdmin,
  177
+        BarAccountAdmin
  178
+    )
  179
+
137 180
 admin.site.register(Article, ArticleAdmin)
138 181
 admin.site.register(CustomArticle, CustomArticleAdmin)
139 182
 admin.site.register(Section, inlines=[ArticleInline])
140 183
 admin.site.register(ModelWithStringPrimaryKey)
141 184
 admin.site.register(Color)
142 185
 admin.site.register(Thing, ThingAdmin)
  186
+admin.site.register(Persona, PersonaAdmin)
143 187
 
144 188
 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
145 189
 # That way we cover all four cases:
@@ -153,3 +197,5 @@ class ThingAdmin(admin.ModelAdmin):
153 197
 admin.site.register(Book, inlines=[ChapterInline])
154 198
 admin.site.register(Promo)
155 199
 admin.site.register(ChapterXtra1)
  200
+
  201
+
148  tests/regressiontests/admin_views/tests.py
... ...
@@ -1,5 +1,7 @@
1 1
 # coding: utf-8
2 2
 
  3
+import re
  4
+
3 5
 from django.test import TestCase
4 6
 from django.contrib.auth.models import User, Permission
5 7
 from django.contrib.contenttypes.models import ContentType
@@ -9,17 +11,22 @@
9 11
 from django.utils.html import escape
10 12
 
11 13
 # local test models
12  
-from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey
  14
+from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, Persona, FooAccount, BarAccount
  15
+
  16
+try:
  17
+    set
  18
+except NameError:
  19
+    from sets import Set as set
13 20
 
14 21
 class AdminViewBasicTest(TestCase):
15 22
     fixtures = ['admin-views-users.xml', 'admin-views-colors.xml']
16  
-    
  23
+
17 24
     def setUp(self):
18 25
         self.client.login(username='super', password='secret')
19  
-    
  26
+
20 27
     def tearDown(self):
21 28
         self.client.logout()
22  
-    
  29
+
23 30
     def testTrailingSlashRequired(self):
24 31
         """
25 32
         If you leave off the trailing slash, app should redirect and add it.
@@ -28,29 +35,29 @@ def testTrailingSlashRequired(self):
28 35
         self.assertRedirects(request,
29 36
             '/test_admin/admin/admin_views/article/add/'
30 37
         )
31  
-    
  38
+
32 39
     def testBasicAddGet(self):
33 40
         """
34 41
         A smoke test to ensure GET on the add_view works.
35 42
         """
36 43
         response = self.client.get('/test_admin/admin/admin_views/section/add/')
37 44
         self.failUnlessEqual(response.status_code, 200)
38  
-    
  45
+
39 46
     def testAddWithGETArgs(self):
40 47
         response = self.client.get('/test_admin/admin/admin_views/section/add/', {'name': 'My Section'})
41 48
         self.failUnlessEqual(response.status_code, 200)
42 49
         self.failUnless(
43  
-            'value="My Section"' in response.content, 
  50
+            'value="My Section"' in response.content,
44 51
             "Couldn't find an input with the right value in the response."
45 52
         )
46  
-    
  53
+
47 54
     def testBasicEditGet(self):
48 55
         """
49 56
         A smoke test to ensureGET on the change_view works.
50 57
         """
51 58
         response = self.client.get('/test_admin/admin/admin_views/section/1/')
52 59
         self.failUnlessEqual(response.status_code, 200)
53  
-    
  60
+
54 61
     def testBasicAddPost(self):
55 62
         """
56 63
         A smoke test to ensure POST on add_view works.
@@ -63,7 +70,7 @@ def testBasicAddPost(self):
63 70
         }
64 71
         response = self.client.post('/test_admin/admin/admin_views/section/add/', post_data)
65 72
         self.failUnlessEqual(response.status_code, 302) # redirect somewhere
66  
-    
  73
+
67 74
     def testBasicEditPost(self):
68 75
         """
69 76
         A smoke test to ensure POST on edit_view works.
@@ -111,7 +118,7 @@ def testBasicEditPost(self):
111 118
 
112 119
     def testChangeListSortingCallable(self):
113 120
         """
114  
-        Ensure we can sort on a list_display field that is a callable 
  121
+        Ensure we can sort on a list_display field that is a callable
115 122
         (column 2 is callable_year in ArticleAdmin)
116 123
         """
117 124
         response = self.client.get('/test_admin/admin/admin_views/article/', {'ot': 'asc', 'o': 2})
@@ -121,10 +128,10 @@ def testChangeListSortingCallable(self):
121 128
             response.content.index('Middle content') < response.content.index('Newest content'),
122 129
             "Results of sorting on callable are out of order."
123 130
         )
124  
-    
  131
+
125 132
     def testChangeListSortingModel(self):
126 133
         """
127  
-        Ensure we can sort on a list_display field that is a Model method 
  134
+        Ensure we can sort on a list_display field that is a Model method
128 135
         (colunn 3 is 'model_year' in ArticleAdmin)
129 136
         """
130 137
         response = self.client.get('/test_admin/admin/admin_views/article/', {'ot': 'dsc', 'o': 3})
@@ -134,40 +141,40 @@ def testChangeListSortingModel(self):
134 141
             response.content.index('Middle content') < response.content.index('Oldest content'),
135 142
             "Results of sorting on Model method are out of order."
136 143
         )
137  
-    
  144
+
138 145
     def testChangeListSortingModelAdmin(self):
139 146
         """
140  
-        Ensure we can sort on a list_display field that is a ModelAdmin method 
  147
+        Ensure we can sort on a list_display field that is a ModelAdmin method
141 148
         (colunn 4 is 'modeladmin_year' in ArticleAdmin)
142 149
         """
143 150
         response = self.client.get('/test_admin/admin/admin_views/article/', {'ot': 'asc', 'o': 4})
144 151
         self.failUnlessEqual(response.status_code, 200)
145 152
         self.failUnless(
146  
-            response.content.index('Oldest content') < response.content.index('Middle content') and 
  153
+            response.content.index('Oldest content') < response.content.index('Middle content') and
147 154
             response.content.index('Middle content') < response.content.index('Newest content'),
148 155
             "Results of sorting on ModelAdmin method are out of order."
149 156
         )
150  
-        
  157
+
151 158
     def testLimitedFilter(self):
152 159
         """Ensure admin changelist filters do not contain objects excluded via limit_choices_to."""
153 160
         response = self.client.get('/test_admin/admin/admin_views/thing/')
154 161
         self.failUnlessEqual(response.status_code, 200)
155 162
         self.failUnless(
156  
-            '<div id="changelist-filter">' in response.content, 
  163
+            '<div id="changelist-filter">' in response.content,
157 164
             "Expected filter not found in changelist view."
158 165
         )
159 166
         self.failIf(
160 167
             '<a href="?color__id__exact=3">Blue</a>' in response.content,
161 168
             "Changelist filter not correctly limited by limit_choices_to."
162 169
         )
163  
-        
  170
+
164 171
     def testIncorrectLookupParameters(self):
165 172
         """Ensure incorrect lookup parameters are handled gracefully."""
166 173
         response = self.client.get('/test_admin/admin/admin_views/thing/', {'notarealfield': '5'})
167  
-        self.assertRedirects(response, '/test_admin/admin/admin_views/thing/?e=1')        
  174
+        self.assertRedirects(response, '/test_admin/admin/admin_views/thing/?e=1')
168 175
         response = self.client.get('/test_admin/admin/admin_views/thing/', {'color__id__exact': 'StringNotInteger!'})
169 176
         self.assertRedirects(response, '/test_admin/admin/admin_views/thing/?e=1')
170  
-            
  177
+
171 178
 def get_perm(Model, perm):
172 179
     """Return the permission object, for the Model"""
173 180
     ct = ContentType.objects.get_for_model(Model)
@@ -387,7 +394,7 @@ def testChangeView(self):
387 394
         post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
388 395
         self.assertRedirects(post, '/test_admin/admin/admin_views/article/')
389 396
         self.failUnlessEqual(Article.objects.get(pk=1).content, '<p>edited article</p>')
390  
-        
  397
+
391 398
         # one error in form should produce singular error message, multiple errors plural
392 399
         change_dict['title'] = ''
393 400
         post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
@@ -398,7 +405,7 @@ def testChangeView(self):
398 405
         post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
399 406
         self.failUnlessEqual(request.status_code, 200)
400 407
         self.failUnless('Please correct the errors below.' in post.content,
401  
-                        'Plural error message not found in response to post with multiple errors.')        
  408
+                        'Plural error message not found in response to post with multiple errors.')
402 409
         self.client.get('/test_admin/admin/logout/')
403 410
 
404 411
     def testCustomModelAdminTemplates(self):
@@ -537,7 +544,7 @@ def test_deleteconfirmation_link(self):
537 544
         response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/delete/' % quote(self.pk))
538 545
         should_contain = """<a href="../../%s/">%s</a>""" % (quote(self.pk), escape(self.pk))
539 546
         self.assertContains(response, should_contain)
540  
-    
  547
+
541 548
     def test_url_conflicts_with_add(self):
542 549
         "A model with a primary key that ends with add should be visible"
543 550
         add_model = ModelWithStringPrimaryKey(id="i have something to add")
@@ -545,7 +552,7 @@ def test_url_conflicts_with_add(self):
545 552
         response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(add_model.pk))
546 553
         should_contain = """<h1>Change model with string primary key</h1>"""
547 554
         self.assertContains(response, should_contain)
548  
-    
  555
+
549 556
     def test_url_conflicts_with_delete(self):
550 557
         "A model with a primary key that ends with delete should be visible"
551 558
         delete_model = ModelWithStringPrimaryKey(id="delete")
@@ -553,7 +560,7 @@ def test_url_conflicts_with_delete(self):
553 560
         response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(delete_model.pk))
554 561
         should_contain = """<h1>Change model with string primary key</h1>"""
555 562
         self.assertContains(response, should_contain)
556  
-    
  563
+
557 564
     def test_url_conflicts_with_history(self):
558 565
         "A model with a primary key that ends with history should be visible"
559 566
         history_model = ModelWithStringPrimaryKey(id="history")
@@ -561,7 +568,7 @@ def test_url_conflicts_with_history(self):
561 568
         response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(history_model.pk))
562 569
         should_contain = """<h1>Change model with string primary key</h1>"""
563 570
         self.assertContains(response, should_contain)
564  
-        
  571
+
565 572
 
566 573
 class SecureViewTest(TestCase):
567 574
     fixtures = ['admin-views-users.xml']
@@ -596,28 +603,28 @@ def setUp(self):
596 603
                      LOGIN_FORM_KEY: 1,
597 604
                      'username': 'joepublic',
598 605
                      'password': 'secret'}
599  
-    
  606
+
600 607
     def tearDown(self):
601 608
         self.client.logout()
602  
-    
  609
+
603 610
     def test_secure_view_shows_login_if_not_logged_in(self):
604 611
         "Ensure that we see the login form"
605 612
         response = self.client.get('/test_admin/admin/secure-view/' )
606 613
         self.assertTemplateUsed(response, 'admin/login.html')
607  
-    
  614
+
608 615
     def test_secure_view_login_successfully_redirects_to_original_url(self):
609 616
         request = self.client.get('/test_admin/admin/secure-view/')
610 617
         self.failUnlessEqual(request.status_code, 200)
611 618
         query_string = "the-answer=42"
612 619
         login = self.client.post('/test_admin/admin/secure-view/', self.super_login, QUERY_STRING = query_string )
613 620
         self.assertRedirects(login, '/test_admin/admin/secure-view/?%s' % query_string)
614  
-    
  621
+
615 622
     def test_staff_member_required_decorator_works_as_per_admin_login(self):
616 623
         """
617 624
         Make sure only staff members can log in.
618 625
 
619 626
         Successful posts to the login page will redirect to the orignal url.
620  
-        Unsuccessfull attempts will continue to render the login page with 
  627
+        Unsuccessfull attempts will continue to render the login page with
621 628
         a 200 status code.
622 629
         """
623 630
         # Super User
@@ -735,3 +742,80 @@ def testUnicodeDelete(self):
735 742
         self.failUnlessEqual(response.status_code, 200)
736 743
         response = self.client.post('/test_admin/admin/admin_views/book/1/delete/', delete_dict)
737 744
         self.assertRedirects(response, '/test_admin/admin/admin_views/book/')
  745
+
  746
+class AdminInheritedInlinesTest(TestCase):
  747
+    fixtures = ['admin-views-users.xml',]
  748
+
  749
+    def setUp(self):
  750
+        self.client.login(username='super', password='secret')
  751
+
  752
+    def tearDown(self):
  753
+        self.client.logout()
  754
+
  755
+    def testInline(self):
  756
+        "Ensure that inline models which inherit from a common parent are correctly handled by admin."
  757
+
  758
+        foo_user = u"foo username"
  759
+        bar_user = u"bar username"
  760
+
  761
+        name_re = re.compile('name="(.*?)"')
  762
+
  763
+        # test the add case
  764
+        response = self.client.get('/test_admin/admin/admin_views/persona/add/')
  765
+        names = name_re.findall(response.content)
  766
+        # make sure we have no duplicate HTML names
  767
+        self.failUnlessEqual(len(names), len(set(names)))
  768
+
  769
+        # test the add case
  770
+        post_data = {
  771
+            "name": u"Test Name",
  772
+            # inline data
  773
+            "accounts-TOTAL_FORMS": u"1",
  774
+            "accounts-INITIAL_FORMS": u"0",
  775
+            "accounts-0-username": foo_user,
  776
+            "accounts-2-TOTAL_FORMS": u"1",
  777
+            "accounts-2-INITIAL_FORMS": u"0",
  778
+            "accounts-2-0-username": bar_user,
  779
+        }
  780
+
  781
+        response = self.client.post('/test_admin/admin/admin_views/persona/add/', post_data)
  782
+        self.failUnlessEqual(response.status_code, 302) # redirect somewhere
  783
+        self.failUnlessEqual(Persona.objects.count(), 1)
  784
+        self.failUnlessEqual(FooAccount.objects.count(), 1)
  785
+        self.failUnlessEqual(BarAccount.objects.count(), 1)
  786
+        self.failUnlessEqual(FooAccount.objects.all()[0].username, foo_user)
  787
+        self.failUnlessEqual(BarAccount.objects.all()[0].username, bar_user)
  788
+        self.failUnlessEqual(Persona.objects.all()[0].accounts.count(), 2)
  789
+
  790
+        # test the edit case
  791
+
  792
+        response = self.client.get('/test_admin/admin/admin_views/persona/1/')
  793
+        names = name_re.findall(response.content)
  794
+        # make sure we have no duplicate HTML names
  795
+        self.failUnlessEqual(len(names), len(set(names)))
  796
+
  797
+        post_data = {
  798
+            "name": u"Test Name",
  799
+
  800
+            "accounts-TOTAL_FORMS": "2",
  801
+            "accounts-INITIAL_FORMS": u"1",
  802
+
  803
+            "accounts-0-username": "%s-1" % foo_user,
  804
+            "accounts-0-account_ptr": "1",
  805
+            "accounts-0-persona": "1",
  806
+
  807
+            "accounts-2-TOTAL_FORMS": u"2",
  808
+            "accounts-2-INITIAL_FORMS": u"1",
  809
+
  810
+            "accounts-2-0-username": "%s-1" % bar_user,
  811
+            "accounts-2-0-account_ptr": "2",
  812
+            "accounts-2-0-persona": "1",
  813
+        }
  814
+        response = self.client.post('/test_admin/admin/admin_views/persona/1/', post_data)
  815
+        self.failUnlessEqual(response.status_code, 302)
  816
+        self.failUnlessEqual(Persona.objects.count(), 1)
  817
+        self.failUnlessEqual(FooAccount.objects.count(), 1)
  818
+        self.failUnlessEqual(BarAccount.objects.count(), 1)
  819
+        self.failUnlessEqual(FooAccount.objects.all()[0].username, "%s-1" % foo_user)
  820
+        self.failUnlessEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user)
  821
+        self.failUnlessEqual(Persona.objects.all()[0].accounts.count(), 2)

0 notes on commit ee0320f

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