Writing reusable ModelForm templates with Django
Introduction
One of the things that drew me to Django was how easy Django makes it to interact with items in a database. Once you've defined your models, Django's model forms can handle the heavy lifting of allowing your users to create, update, and delete entries in your project's database.
The purpose of this post is to show a strategy for writing form templates that can be shared among all models in your project without modification. In doing so, we're employing at least three of Django's design philosophies:
- Don't repeat yourself (DRY)
- Less code
- Discourage redundancy
ModelForm and its template
Let's get our feet wet with Django's ModelForms and their templates before we DRY them off.
Here's a quick example from the Django docs of class-based model editing views to create, update, and delete an Author (check out the Django ModelForm class docs to brush up on the class that provides forms to these views):
# author/models.py
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from myapp.models import Author
class AuthorCreate(CreateView):
model = Author
fields = ['name']
template_name = 'author/author_form.html'
class AuthorUpdate(UpdateView):
model = Author
fields = ['name']
template_name = 'author/author_form.html'
class AuthorDelete(DeleteView):
model = Author
success_url = reverse_lazy('author-list')
template_name = 'author/author_confirm_delete.html'
I've added an explicit template_name
to each of the classes, and we can see that Django practices what it preaches: we have three different ModelForm
s but we only need two templates—for this model. Before we look into making a single template we can share across all models in a project, let's look to see how we can reuse a single template for both a CreateView
and an UpdateView
.
A single Create/Update template
Create and update forms aren't all that different. We can see above that AuthorCreate
and AuthorUpdate
specify the same model and the same fields. Although the forms do different things once they're submitted (one creates an entry in the database, the other updates an entry), the forms are the same before they're submitted. They're just a bunch of HTML form inputs.
Let's take a look at author_form.html
:
{% extends 'base.html.jinja' %}
{# author_form.html #}
{% block content %}
<h2>{% if object %}Update{% else %}Create{% endif %} Author</h2>
<form action="" method="post">
{% csrf_token %}
{{ form }}
<button>Submit</button>
</form>
{% endblock %}
This form pretty DRY. The action="" method="post"
attributes mean that the form will perform an HTTP POST to the current URL, so we don't have to specify diffent form actions if the template is being used in our CreateView
or UpdateView
. The {% if object %}
at the top of the form shows "Update Form" if the template is used in a view that has an object
variable in its context, which an UpdateView
does. A CreateView
doesn't have an object because that view is responsible for creating an object in the database.
The only thing preventing us from using this form for every model in our project is the sneaky "Author" in the template's header (<h2>
). To make this template reusable, we'll need to replace that hard-coded model name with something more dynamic.
Call me by my verbose_name
Django models have a user-friendly representation used in places like the Django admin. "CoverPhoto", the name of a model class, is less legible than "cover photo" for most users—camels being a special case. This user-friendly string is the model's verbose_name
.
By default, Django derives a model's verbose_name
from the name of the model class, splitting the string by how it's capitalized. "CoverPhoto" becomes "cover photo", for instance. There's also verbose_name_plural
, which by default just tacks an 's' to the end of verbose_name
. This works well quite a lot of the time, but there are at least three cases where the default verbose_name
and verbose_name_plural
don't pass muster:
- Irregular pluralizations
- Initialisms and acronyms
- Internationalized applications
Given this model for an information technology person:
from django.db import models
class ITPerson(models.Model):
pass
here are Django's default verbose names:
>>> from .models import ITPerson
>>> str(ITPerson._meta.verbose_name)
it person
>>> str(ITPerson._meta.verbose_name_plural)
it persons
To use verbose_name
in our user-facing templates, we'd expect to see 'IT person' and 'IT people' instead. Specifying verbose names for our model fixes this up:
from django.db import models
from django.utils.translation import ugettext_lazy as _
class ITPerson(models.Model):
Meta:
verbose_name = _('IT person')
verbose_name_plural = _('IT people')
(See the Django translation docs for information about ugettext_lazy
.)
With these changes, our model's verbose_name
and verbose_name_plural
are user-ready:
>>> from .models import ITPerson
>>> str(ITPerson._meta.verbose_name)
IT person
>>> str(ITPerson._meta.verbose_name_plural)
IT people
So, now we know how to specify user-friendly names for our models and how to reference them (Model._meta.verbose_name
). Let's use this in our form template!
Thou shall not pass _meta attributes to a template variable
Not so fast. If we try to use the model's verbose names directly in our template like this:
<h2>{% if object %}Update{% else %}Create{% endif %} {{ view.model._meta.verbose_name }}</h2>
Django will raise a TemplateSyntaxError
:
TemplateSyntaxError at /author/create/
Variables and attributes may not begin with underscores: 'view.model._meta.verbose_name'
Attributes that begin with an underscore are considered private in Python. And while that isn't enforced at the language-level, Django's template syntax does enforce that convention. (See the Python docs for a discussion on private variables.)
Although Python says that private attributes could change at any time, Django models' verbose_name
and verbose_name_plural
aren't going to change on us any time soon (they're used in the Django admin). So, we can still use this information in our ModelForm
template, just not directly. What we really want is a function that accepts a model form instance as an argument and returns the model's verbose_name
.
Template filters
Django does offer the ability to pass a variable through a function and output the result into a template using a filter. A filter modifies how a variable is displayed in a template. For example, Django's built-in capfirst
filter capitalizes the first letter of a string:
{% with curved_fruit='banana' %}
<p>{{ curved_fruit|capfirst }}</p>
{% endwith %}
<!-- The above renders: -->
<p>Banana</p>
While Django has a pile of filters, we'll need to write our own to return a model's verbose name given a form instance.
Custom filters
Filters live alongside template tags, and Django recognizes custom template tags and filters in the templatetags
directory in each of your installed apps.
Let's make a template filter accepts a ModelForm
instance and spits out the appropriate model's verbose_name
:
# project/templatetags/modelform_title.py
"""Template tag to get a model's name from a ModelForm"""
from django import template
register = template.Library()
@register.filter
def modelform_title(modelform):
"""Returns a modelform's titlized Model.verbose_name"""
return modelform._meta.model._meta.verbose_name.title()
There's a bit of _meta
madness going on here. Under the hood of a CreateView
or UpdateView
is a ModelForm
class that itself takes a model argument in its Meta
class:
class AuthorForm(ModelForm):
class Meta:
model = Author
So if we have an AuthorForm
instance, we can reference the Author
model with AuthorForm._meta.model
. We already know that a model's verbose_name
property is in its Meta class, so given an AuthorForm
instance named form
, form._meta.model._meta.verbose_name
gives us the Author model's verbose name: "author".
By appending title()
, we capitalize the first letter of every word in the model's verbose name. Given an ITPerson
ModelForm
instance, that filter will output "IT Person".
Using it
To use custom template tags and filters in a template, they need to be loaded at the top of any template that uses them. Django knows where to look for files containing template tags and filters (the templatetags
directory in any installed app), and we use those filenames to load template tags and filters so we can use them in a template (see line 2).
Here's our new template, model_form.html
:
{% extends 'base.html.jinja' %}
{# model_form.html #}
{% load modelform_title %}
{% block content %}
<h2>{% if object %}Update{% else %}Create{% endif %} {{ form|modelform_title }}</h2>
<form action="" method="post">
{% csrf_token %}
{{ form }}
<button>Submit</button>
</form>
{% endblock %}
You may have to squint to see how our new universal create/update template differs from the old one. There are two changes:
- We're loading our new template filter & tag library at the top of the template (named after our
modelform_title.py
file):{% load modelform_title %}
- We're using our fancy new template filter:
{{ form|modelform_title }}
If we move model_form.html
beside our base.html.jinja
template file, we can update our Author views to use this new template:
# author/models.py
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from myapp.models import Author
class AuthorCreate(CreateView):
model = Author
fields = ['name']
template_name = 'model_form.html'
class AuthorUpdate(UpdateView):
model = Author
fields = ['name']
template_name = 'model_form.html'
class AuthorDelete(DeleteView):
model = Author
success_url = reverse_lazy('author-list')
template_name = 'confirm_delete.html'
What about a delete form template?
I didn't forget! It's easy to create a reusable delete form template without having to use a custom template tag. Because every DeleteView
is guaranteed to have an object, we can use that to differentiate the forms across models:
{% extends 'base.html.jinja' %}
{# confirm_delete.html #}
{% block content %}
<h2>Confirm Delete</h2>
<form action="" method="post">
{% csrf_token %}
<p>
Are you sure you want to delete {{ object }}?
</p>
<button>Delete</button>
</form>
{% endblock %}
Just make sure that your model has a user-friendly string representation (what your model returns in its __str__()
method).
Conclusion
After a bit of a walkabout, we've reached our destination and covered a fair bit of ground. By leveraging a Django model's verbose_name
, we can glean user-friendly names for every model in our project. With a simple custom template filter, we're able to write one template (model_form.html
) and reuse it for every CreateView
and UpdateView
in a project, regardless of which model those views use. In a project with just 5 models, model_form.html
and confirm_delete.html
do the work of 10 templates.
You don't need a hygrometer to see that that's pretty darn DRY.