Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #5780 -- Adjusted the ModelAdmin API to allow the created/updat…

…ed objects

to be passed to the formsets prior to validation.

This is a backward incompatible change for anyone overridding save_add or
save_change. They have been removed in favor of more granular methods
introduced in [8266] and the new response_add and response_change nethods.
save_model has been renamed to save_form due to its slightly changed behavior.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@8273 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 65be56816fc173f823566728ab78b72d061bb466 1 parent 50e6928
Brian Rosner authored August 09, 2008
198  django/contrib/admin/options.py
@@ -452,37 +452,53 @@ def message_user(self, request, message):
452 452
         """
453 453
         request.user.message_set.create(message=message)
454 454
 
455  
-    def save_model(self, request, form, change):
  455
+    def save_form(self, request, form, change):
456 456
         """
457  
-        Save and return a model given a ModelForm. ``change`` is True if the
458  
-        object is being changed, and False if it's being added.
  457
+        Given a ModelForm return an unsaved instance. ``change`` is True if
  458
+        the object is being changed, and False if it's being added.
459 459
         """
460  
-        return form.save(commit=True)
  460
+        return form.save(commit=False)
461 461
 
462 462
     def save_formset(self, request, form, formset, change):
463 463
         """
464  
-        Save an inline formset attached to the object.
  464
+        Given an inline formset return unsaved instances.
465 465
         """
466  
-        formset.save()
  466
+        return formset.save(commit=False)
467 467
 
468  
-    def save_add(self, request, form, formsets, post_url_continue):
  468
+    def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
  469
+        opts = self.model._meta
  470
+        app_label = opts.app_label
  471
+        ordered_objects = opts.get_ordered_objects()
  472
+        context.update({
  473
+            'add': add,
  474
+            'change': change,
  475
+            'has_add_permission': self.has_add_permission(request),
  476
+            'has_change_permission': self.has_change_permission(request, obj),
  477
+            'has_delete_permission': self.has_delete_permission(request, obj),
  478
+            'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
  479
+            'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
  480
+            'ordered_objects': ordered_objects,
  481
+            'form_url': mark_safe(form_url),
  482
+            'opts': opts,
  483
+            'content_type_id': ContentType.objects.get_for_model(self.model).id,
  484
+            'save_as': self.save_as,
  485
+            'save_on_top': self.save_on_top,
  486
+            'root_path': self.admin_site.root_path,
  487
+        })
  488
+        return render_to_response(self.change_form_template or [
  489
+            "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
  490
+            "admin/%s/change_form.html" % app_label,
  491
+            "admin/change_form.html"
  492
+        ], context, context_instance=template.RequestContext(request))
  493
+    
  494
+    def response_add(self, request, obj, post_url_continue='../%s/'):
469 495
         """
470  
-        Saves the object in the "add" stage and returns an HttpResponseRedirect.
471  
-
472  
-        `form` is a bound Form instance that's verified to be valid.
  496
+        Determines the HttpResponse for the add_view stage.
473 497
         """
474  
-        opts = self.model._meta
  498
+        opts = obj._meta
  499
+        pk_value = obj._get_pk_val()
475 500
         
476  
-        new_object = self.save_model(request, form, change=False)
477  
-        if formsets:
478  
-            for formset in formsets:
479  
-                formset.instance = new_object
480  
-                self.save_formset(request, form, formset, change=False)
481  
-
482  
-        pk_value = new_object._get_pk_val()
483  
-        self.log_addition(request, new_object)
484  
-                
485  
-        msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)}
  501
+        msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)}
486 502
         # Here, we distinguish between different save types by checking for
487 503
         # the presence of keys in request.POST.
488 504
         if request.POST.has_key("_continue"):
@@ -490,11 +506,11 @@ def save_add(self, request, form, formsets, post_url_continue):
490 506
             if request.POST.has_key("_popup"):
491 507
                 post_url_continue += "?_popup=1"
492 508
             return HttpResponseRedirect(post_url_continue % pk_value)
493  
-
  509
+        
494 510
         if request.POST.has_key("_popup"):
495 511
             return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
496 512
                 # escape() calls force_unicode.
497  
-                (escape(pk_value), escape(new_object)))
  513
+                (escape(pk_value), escape(obj)))
498 514
         elif request.POST.has_key("_addanother"):
499 515
             self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
500 516
             return HttpResponseRedirect(request.path)
@@ -509,28 +525,15 @@ def save_add(self, request, form, formsets, post_url_continue):
509 525
             else:
510 526
                 post_url = '../../../'
511 527
             return HttpResponseRedirect(post_url)
512  
-    save_add = transaction.commit_on_success(save_add)
513  
-
514  
-    def save_change(self, request, form, formsets=None):
  528
+    
  529
+    def response_change(self, request, obj):
515 530
         """
516  
-        Saves the object in the "change" stage and returns an HttpResponseRedirect.
517  
-
518  
-        `form` is a bound Form instance that's verified to be valid.
519  
-
520  
-        `formsets` is a sequence of InlineFormSet instances that are verified to be valid.
  531
+        Determines the HttpResponse for the change_view stage.
521 532
         """
522  
-        opts = self.model._meta
523  
-        new_object = self.save_model(request, form, change=True)
524  
-        pk_value = new_object._get_pk_val()
525  
-
526  
-        if formsets:
527  
-            for formset in formsets:
528  
-                self.save_formset(request, form, formset, change=True)
  533
+        opts = obj._meta
  534
+        pk_value = obj._get_pk_val()
529 535
         
530  
-        change_message = self.construct_change_message(request, form, formsets)
531  
-        self.log_change(request, new_object, change_message)        
532  
-
533  
-        msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)}
  536
+        msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)}
534 537
         if request.POST.has_key("_continue"):
535 538
             self.message_user(request, msg + ' ' + _("You may edit it again below."))
536 539
             if request.REQUEST.has_key('_popup'):
@@ -538,7 +541,7 @@ def save_change(self, request, form, formsets=None):
538 541
             else:
539 542
                 return HttpResponseRedirect(request.path)
540 543
         elif request.POST.has_key("_saveasnew"):
541  
-            msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(opts.verbose_name), 'obj': new_object}
  544
+            msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(opts.verbose_name), 'obj': obj}
542 545
             self.message_user(request, msg)
543 546
             return HttpResponseRedirect("../%s/" % pk_value)
544 547
         elif request.POST.has_key("_addanother"):
@@ -547,33 +550,6 @@ def save_change(self, request, form, formsets=None):
547 550
         else:
548 551
             self.message_user(request, msg)
549 552
             return HttpResponseRedirect("../")
550  
-    save_change = transaction.commit_on_success(save_change)
551  
-
552  
-    def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
553  
-        opts = self.model._meta
554  
-        app_label = opts.app_label
555  
-        ordered_objects = opts.get_ordered_objects()
556  
-        context.update({
557  
-            'add': add,
558  
-            'change': change,
559  
-            'has_add_permission': self.has_add_permission(request),
560  
-            'has_change_permission': self.has_change_permission(request, obj),
561  
-            'has_delete_permission': self.has_delete_permission(request, obj),
562  
-            'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
563  
-            'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
564  
-            'ordered_objects': ordered_objects,
565  
-            'form_url': mark_safe(form_url),
566  
-            'opts': opts,
567  
-            'content_type_id': ContentType.objects.get_for_model(self.model).id,
568  
-            'save_as': self.save_as,
569  
-            'save_on_top': self.save_on_top,
570  
-            'root_path': self.admin_site.root_path,
571  
-        })
572  
-        return render_to_response(self.change_form_template or [
573  
-            "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
574  
-            "admin/%s/change_form.html" % app_label,
575  
-            "admin/change_form.html"
576  
-        ], context, context_instance=template.RequestContext(request))
577 553
 
578 554
     def add_view(self, request, form_url='', extra_context=None):
579 555
         "The 'add' admin view for this model."
@@ -592,29 +568,44 @@ def add_view(self, request, form_url='', extra_context=None):
592 568
             post_url = '../../../'
593 569
 
594 570
         ModelForm = self.get_form(request)
595  
-        inline_formsets = []
596  
-        obj = self.model()
  571
+        formsets = []
597 572
         if request.method == 'POST':
598 573
             form = ModelForm(request.POST, request.FILES)
  574
+            if form.is_valid():
  575
+                form_validated = True
  576
+                new_object = self.save_form(request, form, change=False)
  577
+            else:
  578
+                form_validated = False
  579
+                new_object = self.model()
599 580
             for FormSet in self.get_formsets(request):
600  
-                inline_formset = FormSet(data=request.POST, files=request.FILES,
601  
-                    instance=obj, save_as_new=request.POST.has_key("_saveasnew"))
602  
-                inline_formsets.append(inline_formset)
603  
-            if all_valid(inline_formsets) and form.is_valid():
604  
-                return self.save_add(request, form, inline_formsets, '../%s/')
  581
+                formset = FormSet(data=request.POST, files=request.FILES,
  582
+                                  instance=new_object,
  583
+                                  save_as_new=request.POST.has_key("_saveasnew"))
  584
+                formsets.append(formset)
  585
+            if all_valid(formsets) and form_validated:
  586
+                new_object.save()
  587
+                form.save_m2m()
  588
+                for formset in formsets:
  589
+                    instances = self.save_formset(request, form, formset, change=False)
  590
+                    for instance in instances:
  591
+                        instance.save()
  592
+                    formset.save_m2m()
  593
+                
  594
+                self.log_addition(request, new_object)
  595
+                return self.response_add(request, new_object)
605 596
         else:
606 597
             form = ModelForm(initial=dict(request.GET.items()))
607 598
             for FormSet in self.get_formsets(request):
608  
-                inline_formset = FormSet(instance=obj)
609  
-                inline_formsets.append(inline_formset)
  599
+                formset = FormSet(instance=self.model())
  600
+                formsets.append(formset)
610 601
 
611 602
         adminForm = AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
612 603
         media = self.media + adminForm.media
613  
-        for fs in inline_formsets:
614  
-            media = media + fs.media
  604
+        for formset in formsets:
  605
+            media = media + formset.media
615 606
 
616 607
         inline_admin_formsets = []
617  
-        for inline, formset in zip(self.inline_instances, inline_formsets):
  608
+        for inline, formset in zip(self.inline_instances, formsets):
618 609
             fieldsets = list(inline.get_fieldsets(request))
619 610
             inline_admin_formset = InlineAdminFormSet(inline, formset, fieldsets)
620 611
             inline_admin_formsets.append(inline_admin_formset)
@@ -626,11 +617,12 @@ def add_view(self, request, form_url='', extra_context=None):
626 617
             'show_delete': False,
627 618
             'media': mark_safe(media),
628 619
             'inline_admin_formsets': inline_admin_formsets,
629  
-            'errors': AdminErrorList(form, inline_formsets),
  620
+            'errors': AdminErrorList(form, formsets),
630 621
             'root_path': self.admin_site.root_path,
631 622
         }
632 623
         context.update(extra_context or {})
633 624
         return self.render_change_form(request, context, add=True)
  625
+    add_view = transaction.commit_on_success(add_view)
634 626
 
635 627
     def change_view(self, request, object_id, extra_context=None):
636 628
         "The 'change' admin view for this model."
@@ -656,26 +648,43 @@ def change_view(self, request, object_id, extra_context=None):
656 648
             return self.add_view(request, form_url='../../add/')
657 649
 
658 650
         ModelForm = self.get_form(request, obj)
659  
-        inline_formsets = []
  651
+        formsets = []
660 652
         if request.method == 'POST':
661 653
             form = ModelForm(request.POST, request.FILES, instance=obj)
662  
-            for FormSet in self.get_formsets(request, obj):
663  
-                inline_formset = FormSet(request.POST, request.FILES, instance=obj)
664  
-                inline_formsets.append(inline_formset)
665  
-
666  
-            if all_valid(inline_formsets) and form.is_valid():
667  
-                return self.save_change(request, form, inline_formsets)
  654
+            if form.is_valid():
  655
+                form_validated = True
  656
+                new_object = self.save_form(request, form, change=True)
  657
+            else:
  658
+                form_validated = False
  659
+                new_object = obj
  660
+            for FormSet in self.get_formsets(request, new_object):
  661
+                formset = FormSet(request.POST, request.FILES,
  662
+                                  instance=new_object)
  663
+                formsets.append(formset)
  664
+
  665
+            if all_valid(formsets) and form_validated:
  666
+                new_object.save()
  667
+                form.save_m2m()
  668
+                for formset in formsets:
  669
+                    instances = self.save_formset(request, form, formset, change=True)
  670
+                    for instance in instances:
  671
+                        instance.save()
  672
+                    formset.save_m2m()
  673
+                
  674
+                change_message = self.construct_change_message(request, form, formsets)
  675
+                self.log_change(request, new_object, change_message)
  676
+                return self.response_change(request, new_object)
668 677
         else:
669 678
             form = ModelForm(instance=obj)
670 679
             for FormSet in self.get_formsets(request, obj):
671  
-                inline_formset = FormSet(instance=obj)
672  
-                inline_formsets.append(inline_formset)
  680
+                formset = FormSet(instance=obj)
  681
+                formsets.append(formset)
673 682
 
674 683
         adminForm = AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
675 684
         media = self.media + adminForm.media
676 685
 
677 686
         inline_admin_formsets = []
678  
-        for inline, formset in zip(self.inline_instances, inline_formsets):
  687
+        for inline, formset in zip(self.inline_instances, formsets):
679 688
             fieldsets = list(inline.get_fieldsets(request, obj))
680 689
             inline_admin_formset = InlineAdminFormSet(inline, formset, fieldsets)
681 690
             inline_admin_formsets.append(inline_admin_formset)
@@ -689,11 +698,12 @@ def change_view(self, request, object_id, extra_context=None):
689 698
             'is_popup': request.REQUEST.has_key('_popup'),
690 699
             'media': mark_safe(media),
691 700
             'inline_admin_formsets': inline_admin_formsets,
692  
-            'errors': AdminErrorList(form, inline_formsets),
  701
+            'errors': AdminErrorList(form, formsets),
693 702
             'root_path': self.admin_site.root_path,
694 703
         }
695 704
         context.update(extra_context or {})
696 705
         return self.render_change_form(request, context, change=True, obj=obj)
  706
+    change_view = transaction.commit_on_success(change_view)
697 707
 
698 708
     def changelist_view(self, request, extra_context=None):
699 709
         "The 'change list' admin view for this model."
38  docs/admin.txt
@@ -521,6 +521,44 @@ with an operator:
521 521
     Performs a full-text match. This is like the default search method but uses
522 522
     an index. Currently this is only available for MySQL.
523 523
 
  524
+``ModelAdmin`` methods
  525
+----------------------
  526
+
  527
+``save_form(self, request, form, change)``
  528
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  529
+
  530
+The ``save_form`` method is given the ``HttpRequest``, a ``ModelForm``
  531
+instance and a boolean value based on whether it is adding or changing the
  532
+object.
  533
+
  534
+This method should return an unsaved instance. For example to attach
  535
+``request.user`` to the object prior to saving::
  536
+
  537
+    class ArticleAdmin(admin.ModelAdmin):
  538
+        def save_form(self, request, form, change):
  539
+            instance = form.save(commit=False)
  540
+            instance.user = request.user
  541
+            return instance
  542
+
  543
+``save_formset(self, request, form, formset, change)``
  544
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  545
+
  546
+The ``save_formset`` method is given the ``HttpRequest``, the parent
  547
+``ModelForm`` instance and a boolean value baesed on whether it is adding or
  548
+changing the parent object.
  549
+
  550
+This method should return unsaved instances. These instances will later be
  551
+saved to the database. By default the formset will only return instances that
  552
+have changed. For example to attach ``request.user`` to each changed formset
  553
+model instance::
  554
+
  555
+    class ArticleAdmin(admin.ModelAdmin):
  556
+        def save_formset(self, request, form, formset, change):
  557
+            instances = formset.save(commit=False)
  558
+            for instance in instances:
  559
+                instance.user = request.user
  560
+            return instances
  561
+
524 562
 ``ModelAdmin`` media definitions
525 563
 --------------------------------
526 564
 
5  tests/regressiontests/admin_views/models.py
@@ -20,6 +20,9 @@ class Article(models.Model):
20 20
     def __unicode__(self):
21 21
         return self.title
22 22
 
  23
+class ArticleInline(admin.TabularInline):
  24
+    model = Article
  25
+
23 26
 class ArticleAdmin(admin.ModelAdmin):
24 27
     list_display = ('content', 'date')
25 28
     list_filter = ('date',)
@@ -61,5 +64,5 @@ def __unicode__(self):
61 64
 
62 65
 admin.site.register(Article, ArticleAdmin)
63 66
 admin.site.register(CustomArticle, CustomArticleAdmin)
64  
-admin.site.register(Section)
  67
+admin.site.register(Section, inlines=[ArticleInline])
65 68
 admin.site.register(ModelWithStringPrimaryKey)
95  tests/regressiontests/admin_views/tests.py
@@ -11,10 +11,90 @@
11 11
 # local test models
12 12
 from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey
13 13
 
  14
+class AdminViewBasicTest(TestCase):
  15
+    fixtures = ['admin-views-users.xml']
  16
+    
  17
+    def setUp(self):
  18
+        self.client.login(username='super', password='secret')
  19
+    
  20
+    def tearDown(self):
  21
+        self.client.logout()
  22
+    
  23
+    def testTrailingSlashRequired(self):
  24
+        """
  25
+        If you leave off the trailing slash, app should redirect and add it.
  26
+        """
  27
+        request = self.client.get('/test_admin/admin/admin_views/article/add')
  28
+        self.assertRedirects(request,
  29
+            '/test_admin/admin/admin_views/article/add/'
  30
+        )
  31
+    
  32
+    def testBasicAddGet(self):
  33
+        """
  34
+        A smoke test to ensure GET on the add_view works.
  35
+        """
  36
+        response = self.client.get('/test_admin/admin/admin_views/section/add/')
  37
+        self.failUnlessEqual(response.status_code, 200)
  38
+    
  39
+    def testBasicEditGet(self):
  40
+        """
  41
+        A smoke test to ensureGET on the change_view works.
  42
+        """
  43
+        response = self.client.get('/test_admin/admin/admin_views/section/1/')
  44
+        self.failUnlessEqual(response.status_code, 200)
  45
+    
  46
+    def testBasicAddPost(self):
  47
+        """
  48
+        A smoke test to ensure POST on add_view works.
  49
+        """
  50
+        post_data = {
  51
+            "name": u"Another Section",
  52
+            # inline data
  53
+            "article_set-TOTAL_FORMS": u"3",
  54
+            "article_set-INITIAL_FORMS": u"0",
  55
+        }
  56
+        response = self.client.post('/test_admin/admin/admin_views/section/add/', post_data)
  57
+        self.failUnlessEqual(response.status_code, 302) # redirect somewhere
  58
+    
  59
+    def testBasicEditPost(self):
  60
+        """
  61
+        A smoke test to ensure POST on edit_view works.
  62
+        """
  63
+        post_data = {
  64
+            "name": u"Test section",
  65
+            # inline data
  66
+            "article_set-TOTAL_FORMS": u"4",
  67
+            "article_set-INITIAL_FORMS": u"1",
  68
+            "article_set-0-id": u"1",
  69
+            # there is no title in database, give one here or formset
  70
+            # will fail.
  71
+            "article_set-0-title": u"Need a title.",
  72
+            "article_set-0-content": u"&lt;p&gt;test content&lt;/p&gt;",
  73
+            "article_set-0-date_0": u"2008-03-18",
  74
+            "article_set-0-date_1": u"11:54:58",
  75
+            "article_set-1-id": u"",
  76
+            "article_set-1-title": u"",
  77
+            "article_set-1-content": u"",
  78
+            "article_set-1-date_0": u"",
  79
+            "article_set-1-date_1": u"",
  80
+            "article_set-2-id": u"",
  81
+            "article_set-2-title": u"",
  82
+            "article_set-2-content": u"",
  83
+            "article_set-2-date_0": u"",
  84
+            "article_set-2-date_1": u"",
  85
+            "article_set-3-id": u"",
  86
+            "article_set-3-title": u"",
  87
+            "article_set-3-content": u"",
  88
+            "article_set-3-date_0": u"",
  89
+            "article_set-3-date_1": u"",
  90
+        }
  91
+        response = self.client.post('/test_admin/admin/admin_views/section/1/', post_data)
  92
+        self.failUnlessEqual(response.status_code, 302) # redirect somewhere
  93
+
14 94
 def get_perm(Model, perm):
15 95
     """Return the permission object, for the Model"""
16 96
     ct = ContentType.objects.get_for_model(Model)
17  
-    return Permission.objects.get(content_type=ct,codename=perm)
  97
+    return Permission.objects.get(content_type=ct, codename=perm)
18 98
 
19 99
 class AdminViewPermissionsTest(TestCase):
20 100
     """Tests for Admin Views Permissions."""
@@ -77,19 +157,6 @@ def setUp(self):
77 157
                      'username': 'joepublic',
78 158
                      'password': 'secret'}
79 159
 
80  
-    def testTrailingSlashRequired(self):
81  
-        """
82  
-        If you leave off the trailing slash, app should redirect and add it.
83  
-        """
84  
-        self.client.post('/test_admin/admin/', self.super_login)
85  
-
86  
-        request = self.client.get(
87  
-            '/test_admin/admin/admin_views/article/add'
88  
-        )
89  
-        self.assertRedirects(request,
90  
-            '/test_admin/admin/admin_views/article/add/'
91  
-        )
92  
-
93 160
     def testLogin(self):
94 161
         """
95 162
         Make sure only staff members can log in.

0 notes on commit 65be568

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