Tag Archive: newforms.

Dynamic ModelForms in Django

For an ongoing project I am implementing basic advertising functionality, where I define a number of positions in the page and advertisers can self-serve to create an advertisement to fit those positions.

Each 'Position' can have different attributes turned on or off. For example, a 'sidebar' ad may permit an image and link text, however a 'footer' ad may only contain link text. When the user creates their own ad, I wanted a single form that morphed itself based on the Position being used so that fields were enabled or disabled, and made mandatory as required.

To get started, here are excerpts from the two models I'll use to demonstrate how I achieved this:

# models.py
from django.db import models

class Position(models.Model):
    title = models.CharField(max_length=100)
    description = models.TextField()
    has_title = models.BooleanField(blank=True, null=True)
    has_summary = models.BooleanField(blank=True, null=True)
    has_link = models.BooleanField(blank=True, null=True)
    has_image = models.BooleanField(blank=True, null=True)

class Advertisement(models.Model):
    position = models.ForeignKey(Position)
    internal_name = models.CharField(max_length=150)
    image = models.ImageField(upload_to=ad_image_path, blank=True, null=True)
    link_url = models.URLField(blank=True, null=True)
    link_text = models.CharField(max_length=100, blank=True, null=True)
    ad_text = models.CharField(max_length=100, blank=True, null=True)

The trick here is to ensure that when creating an Advertisement, the image, link_url, link_text and ad_text fields are turned on and off based on the related Position.

First things first: I created a ModelForm instance for my Advertisement model:

class AdvertisementForm(forms.ModelForm):
    class Meta:
        model = Advertisement

This basic form shows all fields from the Advertisement model to the user, which wasn't exactly what I wanted. My final AdvertisementForm class made use of the __init__() method to remove irrelevant fields, and make any remaining fields required.

# forms.py
class AdvertisementForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):

        self.position = kwargs['position']
        del kwargs['position']
        super(AdvertisementForm, self).__init__(*args, **kwargs)

        if not self.position.has_title:
            del self.fields['link_text']
            self.fields['link_text'].required = True

        if not self.position.has_summary:
            del self.fields['ad_text']
            self.fields['ad_text'].required = True

        if not self.position.has_link:
            del self.fields['link_url']
            self.fields['link_url'].required = True

        if not self.position.has_image:
            del self.fields['image']
            self.fields['image'].required = True

    class Meta:
        model = Advertisement
        exclude = ('position',)

You'll notice that I am using the position keyword argument to the form to determine which fields to show and make required. Obviously this turns the usage of this form into a two-step process: Firstly the user must select a position, then the relevant form is displayed to them based on their selection. This means that the in-view usage is slightly different to how we usually use forms in Django:

# views.py
def create_ad(request):
    if request.REQUEST.get('position', None):
        # This matches both request.GET and request.POST
        position = Position.objects.get(slug=request.REQUEST.get('position'))

        if request.method == 'POST':
            post_data = request.POST.copy()
            if position.has_image:
                form = AdvertisementForm(post_data, request.FILES, position=position)
                form = AdvertisementForm(post_data, position=position)

            if form.is_valid():
                ad = form.save(commit=False)
                ad.position = position

                return HttpResponseRedirect(reverse('ad_listing'))
            form = AdvertisementForm(position=position)

        return render_to_response("create_ad.html", RequestContext(request, {
            'position': position,
            'form': form,
        positions = Position.objects.filter()
        return render_to_response("select_position.html", RequestContext(request, {
            'positions': positions,

This view shows the two-step process that's now involved for the user:

  1. They are shown select_position.html, a very simple template:

    <ul>{% for position in positions %}
        <li><a href='./?position={{ position.slug }}'>{{ position.title }}</a></li>{% endfor %}
  2. The user selects a position, and the slug is passed back into the view as a GET parameter.

  3. The view grabs the Position object from the database based on the slug

  4. The form is initialised, passing the position keyword paramater

  5. The form sets self.position to save the position within the AdvertisementForm instance, then deletes the parameter from the keyword options.

  6. Normal form initialisation then occurs (via the Super call)

  7. The position is then checked to see if the has_title paramater is set. If it is, then the link_text field is made mandatory. If it isn't, then the link_text field is deleted from the form.

  8. This happens again for the ad_text, link_url and image fields.

  9. The form is rendered to the user with a neat little loop within the create_ad.html template:

    <form method='post' action='./'{% if form.is_multipart %} enctype='multipart/form-data'{% endif %}>
        {% for field in form %}
            {% if field.is_hidden %}
                {{ field }}
            {% else %}
                <dt>{{ field.label_tag }}</dt>
                <dd>{{ field }}</dd>
                {% if field.help_text %}<dd class='help_text'>{{ field.help_text }}</dd>{% endif %}
                {% if field.errors %}<dd class='errors'>{{ field.errors }}</dd>{% endif %}
            {% endif %}
        {% endfor %}
        <dd><input type="submit" value="Add Item" /></dd>
    <input type="hidden" name="position" value="{{ position.slug }}" />
  10. When the user clicks 'Submit', the form initialisation is again run before the form is validated, using the POSTed 'position' variable.. This means that validation will be run against the modified form, not against the base form.

This is all pretty straightforward, but it seems to be missed by many people trying to build custom forms. It's very valuable to remember that if you modify a form in it's __init__() method, those modifications will be done to both the unbound form and then to the bound form, so any modifications will impact upon your form validation.


How I Moved My Commercial Projects to Newforms-Admin

My projects were all running on an SVN checkout from late April 2007, after the Queryset-refactor branch was merged into trunk. This meant that I had to make a number of changes, on a public server, to incorporate modifications to file uploads, generic create/update views, and more.

In this post I'm only going to cover how I did the change to Newforms Admin, as the other changes were relatively simple for my projects.

I made these changes on a live server, whilst my projects were running. For the volume of changes I had to make, this was very straightforward. I use FastCGI, and because the FastCGI processes were already running I was able to modify the Python code without it taking effect until I restarted FastCGI.

As always, make sure you've got a backup that's easy to roll back to.

The first thing I did was create my admin.py files in each application. For example, for DjangoSites I removed this admin definition from my 'Website' model:

    class Admin:
        list_filter = ('verified', 'screenshot',)
        list_display = ('url', 'title', 'owner', 'created', 'verified', )
        search_fields = ('title',)

and moved it into websites/admin.py:

from django.contrib import admin
from djangosites.websites.models import Website

class WebsiteAdmin(admin.ModelAdmin):
    list_filter = ('verified', 'screenshot',)
    list_display = ('url', 'title', 'owner', 'created', 'verified', )
    search_fields = ('title',)

admin.site.register(Website, WebsiteAdmin)

It's useful to note that at this stage, if I restarted my Django FastCGI instances, nothing would be broken - but the Admin wouldn't work.

Next, my urls.py file had to be updated. Out with the old:

(r'^admin/', include('django.contrib.admin.urls')),

and in with the new:

from django.contrib import admin
urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),

The last step to do before restarting FastCGI was to update Django. All i did was CD into my Django trunk folder and run svn up.

Finally, I restarted FastCGI and browsed to /admin/.

This is very straightforward, as most of my models have basic Admin requirements. For applications where you don't need any fancy Admin functionality at all, your admin.py can be even simpler:

from django.contrib import admin
from mysite.models import FirstModel, SecondModel, ThirdModel

for model in [FirstModel, SecondModel, ThirdModel]:

The only real gotcha with doing an svn up is that there have been a heap of backwards-incompatible changes made recently, so you really need to make sure you work through the list and make the required changes. The main one that tripped me up was on WhisperGifts where I still used oldforms in a few places. After moving these to newforms to match the rest of the site, it all worked a charm.

I recommend updating Django incrementally - eg update to 7476, make changes required for queryset-refactor, update to 7814, make changes required for file uploads, etc. This makes it easier to manage than one huge update.

Luckily as we approach Django 1.0 backwards-incompatible changes shouldn't be as regular, and most people should be able to stick to the official 1.0 release. This will be a godsend for those distributing applications, as they will be able to recommend a particular Django release and know it is stable and it works correctly.

Congratulations to everybody on the Django team who contributed to newforms-admin going live. I've only covered a raw conversion from the old admin to the new here, and haven't even looked at the new features that are available like application-specific admin screens and admin-level managers (to filter data at the admin level). This is a fantastic change to Django and everybody involved has put in a mammoth effort. Kudos to you all.


Want to see more? Check out the yearly archives below.