L

django 1.0 enhanced reverse generic admin view

Sept. 14th 2008 17:05:47

Django 1.0 has some nice new features and some better ways of doing things we used to do manually. The admin app getting decoupled from the ORM has really paved the way for a lot of customization to go into the admin app; so much so that the last 10% it used to be missing for most CMS needs is now probably easily realized via small decoupled customizations.

This short article is about a customization I've made to the way reverse generic relationships are presented in the admin application. Generic relationships are at the center of the contenttypes contrib application. What contenttypes allows you to do is create "generic" foreign key that can relate an object to an object of any other table. It does this by storing a "contenttype" (object type/table name), a foreign object ID, and an object field in the model. The object field becomes the instantiated result of the foreign object ID from the contenttype table.

Django provides a way you can hook this into the admin easily, with two interfaces: tabular and stacked. You idntify the reverse generic relation via a GenericRelation field in the model that is being pointed to, and include either the tabular or stacked inline class provided by the contenttypes framework in your ModelAdmin class' inlines. So far so good.

The stacked or tabular interfaces will provide you with 3 (this is configurable) interface elements from which to select your generically related objects. This is a great start, but what if you want 4? What if you want "n"? The tabular and stacked interfaces will always provide you with N number of empty interfaces, but entering in say 7 in the default situation requires you to save twice to get the new empty rows. A little exploration in the template (admin/edit_inline/tabular.html) revealed that either an "Add Rows" button was planned, or the vestiges of one remained. So I saw about hooking that back up!

These changes were done as part of a simple tagging application. The first thing I did was copy the tabular.html template over to tagging/templates/tagging/edit_inline/tabular.html, and uncomment & change the small unordered list at the bottom:

    <!-- XXX: We inline this background style here because we can't access admin_media_prefix from the css file -->
    <ul class="tools add-row" style="background: white url({% admin_media_prefix %}img/admin/nav-bg.gif) repeat-x scroll 0 100%;">
        <li>
           <a href="javascript:inline_add_row('
                   {% stringformat inline_admin_formset.formset.auto_id 
                   inline_admin_formset.formset.prefix %}-TOTAL_FORMS')
                   "><img src="{% admin_media_prefix %}img/admin/icon_addlink.gif" alt="+" />
                   Add {{ inline_admin_formset.opts.verbose_name|title }}</a>
        </li>
    </ul>

The crux of this code is the inline_add_row function, which I implemented w/ a little assistance from jquery in tagging/media/js/inline.js. There are a few modifications to the existing row that need to be done:

  • change the row class from 'row1' to 'row2' or vice versa, to preserve alternating bgcolors
  • alter the value of a hidden input dictating how many rows there are
  • alter the ids & names of form & anchor elements within the row to have the new row ordinal

This function takes the ID of the hidden input that tracks how many rows will be submitted. The ID, unfortunately, is created by applying the prefix to a python string format (stored in auto_id), so I wrote a quick template tag to apply expressions to a format string. Now that we can find this input (and the form elements and table elements that are it's parents and sisters) we can pass it to javascript and do the rest there. To get this integrated into the admin application, I subclassed generic.GenericTabularInline to include the media I needed and use the proper template:

    class TaggedObjectInline(generic.GenericTabularInline):
        """An inline object to add with objects you want to tag from the admin."""
        def _media(self):
            """Add some media to the admin page."""
            js = [settings.TAGGING_MEDIA_PREFIX + "js/inline.js",
                  settings.TAGGING_MEDIA_PREFIX + "js/jquery-1.2.6.pack.js"]
            css = {'screen' : [settings.TAGGING_MEDIA_PREFIX + "css/inline.css"]}
            return forms.Media(js=js, css=css)
        media = property(_media)
        template = 'tagging/edit_inline/tabular.html'
        model = TaggedObject

Although this code is specific to a model, you could easily pull all of this out to a third party application like EnhancedReverseGeneric and provide the models via subclass. Nothing else in this solution is tailored to my tagging application at all. In the admin.py of the application that will be using this, just include this class as an Inline and you will be able to add N generic relations to the object you are editing without saving and reloading.

I am quite excited with the ease of customization of the newforms_admin, especially considering how often the admin was almost enough for me in past projects. Perhaps there will be a rich set of admin enhancing apps distributed either in an ad-hoc fashion or via djangosnippets. The potential to control down to a high degree of detail how CRUD works without actually having to write any CRUD code is definite a killer feature of django and one that I hope sees lots of activity in the coming months.

comments

+ leave a comment on "django 1.0 enhanced reverse generic admin view"