Dynamic Django ModelForms

T

he forms module in Django is pretty amazing and it allows you to do some rather complex things with not a lot of code. Django forms ships with a class called, ModelForm. ModelForm, if you are not familiar, when given a model class, on an instance of a model, will generate a form instance for you complete with validation. You can, of course, specify which fields to exclude from the form and which widgets to use for each field.

Django forms ships with a class called, ModelForm. ModelForms, if you are not familiar, when given a model class, or an instance of a model, will generate a form instance for you complete with validation. You can, of course, specify which fields to exclude from the form and which widgets to use for each field. As you can imagine, as your application grows in size and complexity, the number of forms as well as the variations on each form also grows.

For example, I may have a model that I want to show a certain set of fields to admins and a certain set of fields for everyone else. Or, I may want to show different fields of a model during creation versus editing. The most common case I find myself in is, I really don't want to define a new ModelForm class every time I create a new model. Luckily, understanding python's lexical scoping, closures and a little help from Django's ContentTypes framework, we can write a factory function that generate a form class for model in your project.

Standard Approach

Let's look a simple django models.py:

from django.db import modelsclass Post( models.Model ):
    title   = modles.CharField("Title", max_length = 255 )
    author  = models.CharField("Author", max_length = 255 )
    content = models.TextField("Content")
    secret_field = models.BooleanField()
    objects = models.Manager()

Making a ModelForm is pretty straight forward:

from django import forms
from mypackage.models import Post
class PostForm( models.ModelForm ):
    class Meta:
        model = Post

If we want a form that shows all fields for site staff, but restricts certain fields for non staff, we would need to define a new form that explicitly excludes the fields we don't want to show:

from django import forms
from mypackage.models import Post
class AdminPostForm( models.ModelForm ):
    class Meta:
        model = Post
        class UserPostForm( models.ModelForm ):
    class Meta:
        model = Post
        excludes = ('secret_field','admin_field')

You can see here that going down this path will take you down a dark and dirty path. As the size of your django project grows, you will quickly end up more forms that you can keep track of. Trying to code each situation in which certain forms are used will become a mess to maintain.

Lexical [lek]-si-kuhl  -a adjective ( scope )

if a variable name's scope is a certain function, then its scope is the program text of the function definition: within that text, the variable name exists, and is bound to its variable, but outside that text, the variable name does not exist

What we really need to do is create a form class on the fly. Python implements Lexical scoping, and more importantly, closures to a degree, that makes doing something like this fairly easy. For starters, we can define a factory function that returns a new ModelForm class for our blog post example.

from django import forms
def get_post_form(  ):
class _PostForm( forms.ModelForm ):
    class Meta:
        model = Post
    return _PostForm

This in it self is much more useful, however it illustrates the fact that we can define on the fly and return it for use somewhere else in our application. It would only take a small amount of work from here to modify this function to generate any number of forms based on some other criteria.

Generic Approach

Because a class is a Python object, we can pass them to and return them from functions. Now we can take our factory function one step further be eliminating the need to hard code the Model class with the help of Django's ContentTypes framework.

from django.contrib.contenttypes.models import ContentType
from django import forms
def get_object_form( type_id,  excludes=None ):
    ctype = ContentType.objects.get( pk=type_id )
    model_class = ctype.model_class( )
    class _ObjectForm( forms.ModelForm ):
        class Meta:
            model = model_class
            excludes = excludes
    return _ObjectForm

Given the id of the ContentType we want to add/edit we can generate a form class suitable for use in our code. The first step is to retrieve the model class based on the content type we are dealing with via the model_class method on ContentType instances. After we have that, we can define a basic ModelForm passing it our model class and a simple configuration to exclude certain fields if so desired. This simple function allows you to do something like this in a django view:

from mypackage.forms import get_object_form
from django.shortcuts import render_to_response
from django.template.context import RequestContext
from django.http import HttpResponseRedirect

def create_object(request, type_id):
    form_class = get_object_form( type_id )
    form = form_class( 
        request.POST or None,
        request.FILES or None
    )
    if form.is_valid():
        item = form.save()
        return HttpResponseRedirect('/')
    return render_to_response(
        "mytemplate.html", 
        {"form":form},
        context_instance=RequestContext( request )
    )

Although these are overly simple examples, using a function to create a class on the fly lays the foundation for doing some pretty interesting things. One could take this example one step further and use a quick for loop to filter out field names based on a simple set of criteria and set the included fields dynamically. The possibilities may not be limitless, but they are vast.

python django closure modelform