Yawd website

may26

Admin widget for generic relations in django

The contenttypes framework adds wonderful functionality for dynamic foreign keys in Django. However, it lacks of a widget to allow the user select the content object from the administrator website. As you can see in Figure 1, the user has to manually type the object id in a plain textbox.

django's default generic foreign key widgetFigure 1. Screenshot demonstrating the default generic foreign key widgets.

We use a very simple rating system as an example. A rating model is used to rate books, articles and papers. The resulting models.py is shown below:

models.py
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

class Book(models.Model):
    title = models.CharField(max_length=100)
    
    def __unicode__(self):
        return u'%s' % self.title
    
class Article(models.Model):
    title = models.CharField(max_length=100)
    
    def __unicode__(self):
        return u'%s' % self.title

class Paper(models.Model):
    title = models.CharField(max_length=100) 
    
    def __unicode__(self):
        return u'%s' % self.title

class Rating(models.Model):    
    RATING_CHOICES = (
        ('ex', 'Excelent'),
        ('go', 'Good'),
        ('db', 'Don\'t bother'),
        ('df', 'Dog food')
    )
    
    #Generic foreign key
    content_type = models.ForeignKey(ContentType, 
        limit_choices_to = {"model__in": ("Book", "Article", "Paper")}, 
        default=ContentType.objects.get(app_label="rating", model="Article").pk)
    object_id = models.PositiveIntegerField()
    parent = generic.GenericForeignKey()

    rate = models.CharField(max_length=2, choices=RATING_CHOICES)

    def __unicode__(self):
        return u'%s' % self.rate

To override the custom widgets, we need to create a new admin form for the 'Rating' model

forms.py
from django.forms import ModelForm
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
from django.db.models.fields.related import ManyToOneRel
from widgets import ContentTypeSelect
from models import Rating, Book

class AdminRatingForm(ModelForm):

    def __init__(self, *args, **kwargs):
            super(AdminRatingForm, self).__init__(*args, **kwargs)
            try:
                model = self.instance.content_type.model_class()
                model_key = model._meta.pk.name
            except:
                model = Book
                model_key = 'id'
            self.fields['object_id'].widget = ForeignKeyRawIdWidget(rel=ManyToOneRel(model, model_key))
            self.fields['content_type'].widget.widget = ContentTypeSelect('lookup_id_object_id', 
                            self.fields['content_type'].widget.widget.attrs, 
                            self.fields['content_type'].widget.widget.choices)

    class Meta:
        model = Rating

The 'ForeignKeyRawIdWidget' is by default used by django for fields declared in the ModelAdmin's 'rawid_fields' list. So what we really need is a widget that changes the url that the 'ForeignKeyRawIdWidget' targets when a user changes Content type model. That is done by the 'ConentTypeSelect' widget that is implemented below.

widgets.py
from itertools import chain
from django.utils.safestring import mark_safe
from django import forms
from django.contrib.contenttypes.models import ContentType

class ContentTypeSelect(forms.Select):
    def __init__(self, lookup_id,  attrs=None, choices=()):
        self.lookup_id = lookup_id
        super(ContentTypeSelect, self).__init__(attrs, choices)
        
    def render(self, name, value, attrs=None, choices=()):
        output = super(ContentTypeSelect, self).render(name, value, attrs, choices)
        
        choices = chain(self.choices, choices)
        choiceoutput = ' var %s_choice_urls = {' % (attrs['id'],)
        for choice in choices:
            try:
                ctype = ContentType.objects.get(pk=int(choice[0]))
                choiceoutput += '    \'%s\' : \'../../../%s/%s?t=%s\','  % ( str(choice[0]), 
                    ctype.app_label, ctype.model, ctype.model_class()._meta.pk.name)
            except:
                pass
        choiceoutput += '};'
        
        output += ('<script type="text/javascript">'
                   '(function($) {'
                   '  $(document).ready( function() {'
                   '%(choiceoutput)s'
                   '    $(\'#%(id)s\').change(function (){'
                   '        $(\'#%(fk_id)s\').attr(\'href\',%(id)s_choice_urls[$(this).val()]);'
                   '    });'
                   '  });'
                   '})(django.jQuery);'
                   '</script>' % { 'choiceoutput' : choiceoutput, 
                                    'id' : attrs['id'],
                                    'fk_id' : self.lookup_id
                                  })
        return mark_safe(u''.join(output))

Finally, we only need to tie our form to the admin model class in the application's admin.py module.

admin.py
from django.contrib import admin
from models import Book, Article, Paper, Rating
from forms import AdminRatingForm

class RatingAdmin(admin.ModelAdmin):
    form = AdminRatingForm

admin.site.register(Book)
admin.site.register(Article)
admin.site.register(Paper)
admin.site.register(Rating, RatingAdmin)

The resulting widget is shown in Figure 2.

the object id selection widgetFigure 2. Screenshot of the 'Add Rating model' page demonstrating the object id selection widget.

Comments powered by Disqus