Daniel Roy Greenfeld

Daniel Roy Greenfeld

About | Articles | Books | Jobs | News | Tags

Core Concepts of Django ModelForms

2013-6-13

In my opinion, the concepts behind Django's model forms can be listed in just six (6) bullets. The bullets I've marked in bold at the top are the topic of this blog post, while the two of these that were covered in a previous blog post on Django forms are at bottom of my list.

  • ModelForms render Model fields as HTML.
  • ModelForms select validators based off of Model field definitions.
  • ModelForms don't have to display/change all available fields.
  • ModelForms save dictionaries to SQL tables.
  • Forms are "just" Python constructs. (covered previous)
  • Forms validate Python dictionaries. (covered previous)

ModelForms render Model fields as HTML.

If I create a Django model:

# myapp/models.py
from django.db import models

class MyModel(models.Model):

    title = models.CharField(max_length=100)

Then attach it to a ModelForm:

# myapp/forms.py
from django import forms

from .models import MyModel

class MyModelForm(forms.ModelForm):

    class Meta:

        model = MyModel

I can render it in a template, or for better clarity in this post, the Python REPL:

>>> from myapp.forms import MyModelForm
>>> mf = MyModelForm()
>>> mf
<__main__.MyForm object at 0x1023c8bd0>
>>> print(mf)
<tr><th><label for="id_title">Title:</label></th>
<td><input id="id_title" name="title" maxlength="100" type="text" /></td></tr>

ModelForms select validators based off of Model field definitions.

One of the nice things about Django is that its forms library protects your models. It does this by assigning one or more of Django's many built-in validators to the form fields it generates, and using them to check incoming data. Let's dive in:

>>> from myapp.forms import MyModelForm
>>> mf = MyModelForm()
>>> mf
<__main__.MyForm object at 0x1023c8bd0>
>>> mf.fields
{'title': <django.forms.fields.CharField object at 0x102474bd0>}
>>> field = mf.fields['title']
>>> field.max_length
100
>>> field.validators
[<django.core.validators.MaxLengthValidator object at 0x102403b10>]

Each individual field contains a list of validators (in this case, just one validator) supplied by Django and any ModelForm customizations that might have been done.

If you want to add more validators to a ModelForm (perhaps we want our title field to require at least 20 characters) one way to do it is by overriding the field definition in the ModelForm class's __init__ method. That's a mouthful, so I'll just demonstrate in code:

# myapp/forms.py
from django import forms
from django.core.validators import MinLengthValidator

from .models import MyModel

class MyModelForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super(MyModelForm, self).__init__(*args, **kwargs)
        self.fields["title"].min_length = 20
        self.fields["title"].validators.append(MinLengthValidator)

    class Meta:

        model = MyModel

If we stop/start the shell, we now see some new elements added to the form object:

>>> # Don't forget to stop/start the Django shell!
>>> from myapp.forms import MyModelForm
>>> mf = MyModelForm()
>>> mf
<__main__.MyForm object at 0x1023c8bd0>
>>> mf.fields
{'title': <django.forms.fields.CharField object at 0x1023ee810>}
>>> field = mf.fields['title']
>>> field.max_length
100
>>> field.min_length
20
>>> field.validators
[<django.core.validators.MaxLengthValidator object at 0x10240c7d0>, <django.core.validators.MinLengthValidator object at 0x1023eef90>]

Now we have two validators for the field!

There are other ways to override the title field validators. The easiest but not necessarily the best way is to replicate the ModelForm definition of the field in the form like so:

# myapp/forms.py
from django import forms

from .models import MyModel

class MyModelForm(forms.ModelForm):

    title = forms.CharField(max_length=100, min_length=20)

    class Meta:

        model = MyModel

I don't like this technique. This makes it so we are defining the title field in two places, once in the model and once in the form. I go into more of the details and problems of this approach in my previous blog post at Overloading Django Form Fields.

ModelForms don't have to display/change all available fields.

Before we dive into this section, let's increase our model to have two fields as shown below:

# myapp/models.py
from django.db import models

class MyModel(models.Model):

    title = models.CharField(max_length=100)
    slug = models.SlugField()

Let's say that we don't want to allow users the ability to change slugs on existing content, otherwise URLs will be broken. In this case, we rely on the fields attribute of ModelForm.Meta to make it so we only display what we want to display:

# myapp/forms.py
from django import forms

from .models import MyModel

class MyModelForm(forms.ModelForm):

    class Meta:

        model = MyModel
        fields = ('title', )

Easy!

But what about ModelForm.Meta.excludes?

We advocate strongly against using ModelForm.Meta.excludes.

In fact, when we were writing Two Scoops of Django the majority of our technical reviewers as well as our security reviewer fervently insisted that we advocate against use of ModelForm.Meta.excludes. We provide numerous warnings about it's usage, and go in-depth as to why in section 21.12. For reference, Django's own documentation is now including a rather mild warning (no warning box) on the subject at selecting the fields to use. I might try and get that addressed in the next few days...

In any case, the problem with ModelForm.Meta.excludes is similar to but worse than duplicating field functionality. It means that changes to models (new fields for example) will display in associated forms unless you remember to modify the associated forms. Since a single model can have multiple forms, and we developers forget or leave projects, you can understanding what sort of security nightmare this can cause.

Do yourself a favor and stay away from ModelForm.Meta.excludes.

ModelForms save dictionaries to SQL tables

In my previous post of Django forms I covered forms validate dictionaries. Well, ModelForms do the same thing AND give us the power to save that validated dictionary to SQL tables. We don't even need to involve web pages!

This is really useful because it means that we can take data from any source, be it user input from the web, JSON data fetched from an API, and even CSVs generated from Excel reports and transform that into data that resides in SQL.

Let's go over using our ModelForm with title/slug fields used with all those methods. In our samples (web page, json, csv), we'll use generating a timestamp to demonstrate how we can modify the model data before it's saved, and we'll base all three examples off the model and ModelForm combination listed below.

# myapp/models.py
from django.db import models


class MyModel(models.Model):

    title = models.CharField(max_length=100)
    timestamp = models.DateTimeField()
# myapp/forms.py
from django import Forms

from .models import MyModel


class MyModel(forms.ModelForm):

    class Meta:

        model = MyModel
        fields = ('title', )

And now to our three examples!

Example #1 Web Page

This should look pretty familiar to many Django developers. it's the traditional Django view pattern of processing simple model forms.

# myapp/views.py
from django.core.shortcuts import render, redirect
from django import forms
from django.utils import timezone

from .forms import MyModelForm


def add_model(request):

    if request.method == "POST":
        form = MyModelForm(request.POST)
        if form.is_valid():

            # commit=False means the form doesn't save at this time.
            # commit defaults to True which means it normally saves.
            model_instance = form.save(commit=False)
            model_instance.timestamp = timezone.now()
            model_instance.save()
            return redirect('victory')
    else:
        form = MyModelForm()

    return render(request, "my_template.html", {'form': form})

Example #2 API/JSON

In this example, we're validating the output of a RESTful API before letting it touch our database. It's critical that such APIs are not allowed to touch our systems without proper validation - don't make my mistakes! Also, even internally within a project it's really important to validate all data coming from different databases. And Django makes it easy!

# myapp/api/reitz.py
from django.utils import timezone

import requests  # You are using requests-python, right?

from .forms import MyModelForm


class ReitzApiException(Exception):
    pass


def fetch_reitz_data(target_url):
    response = requests.get(target_url)
    if response.status_code == 200:

        # generate the form from the response
        form = MyModelForm(response.json())
        if form.is_valid():
            model_instance = form.save(commit=False)
            model_instance.timestamp = timezone.now()
            model_instance.save()
            return model_instance

        # Simplistic exception handling
        raise ReitzApiException(form.errors)

    # Simplistic exception handling
    raise ReitzApiException(response.status_code)

Example #3 CSV Import

I'll admit my mistake again: I've written my own validation tools to handle data coming from CSVs and Excel documents into Django projects. My validation scripts always seem fragile, and they are. What I'm doing going forward is I'm leaning on form libraries to do the hard work of validating data and saving it to models.

import csv

from django.utils import timezone

from .forms import MyModelForm


def import_csv(filename):
    rows = open(filename)
    records_added = 0
    errors = []
    # Generate a dict per row, with the first CSV row being the keys.
    for row in csv.DictReader(rows, delimiter=","):

        # Bind the row data to the MyModelForm
        form = MyModelForm(row)
        if form.is_valid():
            model_instance = form.save(commit=False)
            model_instance.timestamp = timezone.now()
            model_instance.save()
            records_added += 1
        else:
            errors.append(form.errors)

    return records_added, errors

Closing Thoughts

I can think of three things:

Forget the HTML: For nearly every sort of occasion where new data is coming into your system, form libraries save you from doing extra work and cover your behind. While my experience is with Django forms, there are many form libraries out there. The patterns explored in this post are certainly available in other web frameworks (see https://flask.pocoo.org/snippets/category/forms/ for proof).

HTML Rendering Issues: No form library is going to do everything, and because of evolving standards, decent HTML rendering is a pain for form library authors. For example, Django's default form HTML rendering remains stuck in 2005 because if they had kept up with modern trends of HTML form layout we would have many different flavors of forms in Django core (a testing nightmare). Which means, as a developer, it's important when using a new form library to learn how to override the default form HTML rendering.

What about AJAX?: Whoops! Does this mean I have to write another blog post? Not at all. In Django, AJAX is just another view, either function- or class-based. The secret is to validate the incoming data the same way as you would any other view request by using forms.


Tags: python django forms
← Back to home