Django

Code

Changeset 8273

Show
Ignore:
Timestamp:
08/09/08 15:52:40 (1 year ago)
Author:
brosner
Message:

Fixed #5780 -- Adjusted the ModelAdmin? API to allow the created/updated 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.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/contrib/admin/options.py

    r8266 r8273  
    453453        request.user.message_set.create(message=message) 
    454454 
    455     def save_model(self, request, form, change): 
    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. 
    459         """ 
    460         return form.save(commit=True) 
     455    def save_form(self, request, form, change): 
     456        """ 
     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        """ 
     460        return form.save(commit=False) 
    461461 
    462462    def save_formset(self, request, form, formset, change): 
    463463        """ 
    464         Save an inline formset attached to the object. 
    465         """ 
    466         formset.save() 
    467  
    468     def save_add(self, request, form, formsets, post_url_continue): 
    469         """ 
    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. 
    473         """ 
    474         opts = self.model._meta 
    475          
    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)} 
    486         # Here, we distinguish between different save types by checking for 
    487         # the presence of keys in request.POST. 
    488         if request.POST.has_key("_continue"): 
    489             self.message_user(request, msg + ' ' + _("You may edit it again below.")) 
    490             if request.POST.has_key("_popup"): 
    491                 post_url_continue += "?_popup=1" 
    492             return HttpResponseRedirect(post_url_continue % pk_value) 
    493  
    494         if request.POST.has_key("_popup"): 
    495             return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \ 
    496                 # escape() calls force_unicode. 
    497                 (escape(pk_value), escape(new_object))) 
    498         elif request.POST.has_key("_addanother"): 
    499             self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name))) 
    500             return HttpResponseRedirect(request.path) 
    501         else: 
    502             self.message_user(request, msg) 
    503  
    504             # Figure out where to redirect. If the user has change permission, 
    505             # redirect to the change-list page for this object. Otherwise, 
    506             # redirect to the admin index. 
    507             if self.has_change_permission(request, None): 
    508                 post_url = '../' 
    509             else: 
    510                 post_url = '../../../' 
    511             return HttpResponseRedirect(post_url) 
    512     save_add = transaction.commit_on_success(save_add) 
    513  
    514     def save_change(self, request, form, formsets=None): 
    515         """ 
    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. 
    521         """ 
    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) 
    529          
    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)} 
    534         if request.POST.has_key("_continue"): 
    535             self.message_user(request, msg + ' ' + _("You may edit it again below.")) 
    536             if request.REQUEST.has_key('_popup'): 
    537                 return HttpResponseRedirect(request.path + "?_popup=1") 
    538             else: 
    539                 return HttpResponseRedirect(request.path) 
    540         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} 
    542             self.message_user(request, msg) 
    543             return HttpResponseRedirect("../%s/" % pk_value) 
    544         elif request.POST.has_key("_addanother"): 
    545             self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name))) 
    546             return HttpResponseRedirect("../add/") 
    547         else: 
    548             self.message_user(request, msg) 
    549             return HttpResponseRedirect("../") 
    550     save_change = transaction.commit_on_success(save_change) 
     464        Given an inline formset return unsaved instances. 
     465        """ 
     466        return formset.save(commit=False) 
    551467 
    552468    def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): 
     
    575491            "admin/change_form.html" 
    576492        ], context, context_instance=template.RequestContext(request)) 
     493     
     494    def response_add(self, request, obj, post_url_continue='../%s/'): 
     495        """ 
     496        Determines the HttpResponse for the add_view stage. 
     497        """ 
     498        opts = obj._meta 
     499        pk_value = obj._get_pk_val() 
     500         
     501        msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)} 
     502        # Here, we distinguish between different save types by checking for 
     503        # the presence of keys in request.POST. 
     504        if request.POST.has_key("_continue"): 
     505            self.message_user(request, msg + ' ' + _("You may edit it again below.")) 
     506            if request.POST.has_key("_popup"): 
     507                post_url_continue += "?_popup=1" 
     508            return HttpResponseRedirect(post_url_continue % pk_value) 
     509         
     510        if request.POST.has_key("_popup"): 
     511            return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \ 
     512                # escape() calls force_unicode. 
     513                (escape(pk_value), escape(obj))) 
     514        elif request.POST.has_key("_addanother"): 
     515            self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name))) 
     516            return HttpResponseRedirect(request.path) 
     517        else: 
     518            self.message_user(request, msg) 
     519 
     520            # Figure out where to redirect. If the user has change permission, 
     521            # redirect to the change-list page for this object. Otherwise, 
     522            # redirect to the admin index. 
     523            if self.has_change_permission(request, None): 
     524                post_url = '../' 
     525            else: 
     526                post_url = '../../../' 
     527            return HttpResponseRedirect(post_url) 
     528     
     529    def response_change(self, request, obj): 
     530        """ 
     531        Determines the HttpResponse for the change_view stage. 
     532        """ 
     533        opts = obj._meta 
     534        pk_value = obj._get_pk_val() 
     535         
     536        msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)} 
     537        if request.POST.has_key("_continue"): 
     538            self.message_user(request, msg + ' ' + _("You may edit it again below.")) 
     539            if request.REQUEST.has_key('_popup'): 
     540                return HttpResponseRedirect(request.path + "?_popup=1") 
     541            else: 
     542                return HttpResponseRedirect(request.path) 
     543        elif request.POST.has_key("_saveasnew"): 
     544            msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(opts.verbose_name), 'obj': obj} 
     545            self.message_user(request, msg) 
     546            return HttpResponseRedirect("../%s/" % pk_value) 
     547        elif request.POST.has_key("_addanother"): 
     548            self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name))) 
     549            return HttpResponseRedirect("../add/") 
     550        else: 
     551            self.message_user(request, msg) 
     552            return HttpResponseRedirect("../") 
    577553 
    578554    def add_view(self, request, form_url='', extra_context=None): 
     
    593569 
    594570        ModelForm = self.get_form(request) 
    595         inline_formsets = [] 
    596         obj = self.model() 
     571        formsets = [] 
    597572        if request.method == 'POST': 
    598573            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() 
    599580            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) 
    605596        else: 
    606597            form = ModelForm(initial=dict(request.GET.items())) 
    607598            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) 
    610601 
    611602        adminForm = AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields) 
    612603        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 
    615606 
    616607        inline_admin_formsets = [] 
    617         for inline, formset in zip(self.inline_instances, inline_formsets): 
     608        for inline, formset in zip(self.inline_instances, formsets): 
    618609            fieldsets = list(inline.get_fieldsets(request)) 
    619610            inline_admin_formset = InlineAdminFormSet(inline, formset, fieldsets) 
     
    627618            'media': mark_safe(media), 
    628619            'inline_admin_formsets': inline_admin_formsets, 
    629             'errors': AdminErrorList(form, inline_formsets), 
     620            'errors': AdminErrorList(form, formsets), 
    630621            'root_path': self.admin_site.root_path, 
    631622        } 
    632623        context.update(extra_context or {}) 
    633624        return self.render_change_form(request, context, add=True) 
     625    add_view = transaction.commit_on_success(add_view) 
    634626 
    635627    def change_view(self, request, object_id, extra_context=None): 
     
    657649 
    658650        ModelForm = self.get_form(request, obj) 
    659         inline_formsets = [] 
     651        formsets = [] 
    660652        if request.method == 'POST': 
    661653            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) 
    668677        else: 
    669678            form = ModelForm(instance=obj) 
    670679            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) 
    673682 
    674683        adminForm = AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields) 
     
    676685 
    677686        inline_admin_formsets = [] 
    678         for inline, formset in zip(self.inline_instances, inline_formsets): 
     687        for inline, formset in zip(self.inline_instances, formsets): 
    679688            fieldsets = list(inline.get_fieldsets(request, obj)) 
    680689            inline_admin_formset = InlineAdminFormSet(inline, formset, fieldsets) 
     
    690699            'media': mark_safe(media), 
    691700            'inline_admin_formsets': inline_admin_formsets, 
    692             'errors': AdminErrorList(form, inline_formsets), 
     701            'errors': AdminErrorList(form, formsets), 
    693702            'root_path': self.admin_site.root_path, 
    694703        } 
    695704        context.update(extra_context or {}) 
    696705        return self.render_change_form(request, context, change=True, obj=obj) 
     706    change_view = transaction.commit_on_success(change_view) 
    697707 
    698708    def changelist_view(self, request, extra_context=None): 
  • django/trunk/docs/admin.txt

    r8243 r8273  
    522522    an index. Currently this is only available for MySQL. 
    523523 
     524``ModelAdmin`` methods 
     525---------------------- 
     526 
     527``save_form(self, request, form, change)`` 
     528~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
     529 
     530The ``save_form`` method is given the ``HttpRequest``, a ``ModelForm`` 
     531instance and a boolean value based on whether it is adding or changing the 
     532object. 
     533 
     534This 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 
     546The ``save_formset`` method is given the ``HttpRequest``, the parent 
     547``ModelForm`` instance and a boolean value baesed on whether it is adding or 
     548changing the parent object. 
     549 
     550This method should return unsaved instances. These instances will later be 
     551saved to the database. By default the formset will only return instances that 
     552have changed. For example to attach ``request.user`` to each changed formset 
     553model 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 
    524562``ModelAdmin`` media definitions 
    525563-------------------------------- 
  • django/trunk/tests/regressiontests/admin_views/models.py

    r8236 r8273  
    2020    def __unicode__(self): 
    2121        return self.title 
     22 
     23class ArticleInline(admin.TabularInline): 
     24    model = Article 
    2225 
    2326class ArticleAdmin(admin.ModelAdmin): 
     
    6265admin.site.register(Article, ArticleAdmin) 
    6366admin.site.register(CustomArticle, CustomArticleAdmin) 
    64 admin.site.register(Section
     67admin.site.register(Section, inlines=[ArticleInline]
    6568admin.site.register(ModelWithStringPrimaryKey) 
  • django/trunk/tests/regressiontests/admin_views/tests.py

    r8271 r8273  
    1212from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey 
    1313 
     14class 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 
    1494def get_perm(Model, perm): 
    1595    """Return the permission object, for the Model""" 
    1696    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) 
    1898 
    1999class AdminViewPermissionsTest(TestCase): 
     
    77157                     'username': 'joepublic', 
    78158                     'password': 'secret'} 
    79  
    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         ) 
    92159 
    93160    def testLogin(self):