Using dynamic choices with Django newforms and custom widgets
Note: This post is more than 10 years old. The solutions proposed below most certainly don't work with current Django versions.
Most of the examples I have found on the web about replacing the default Django newforms
widgets use hard coded values for the list of choices the widget displays. But those hard coded examples fall short when the widgets are bounded to many-to-many fields or to foreign keys in the model class. That is, when the list of choices is generated dynamically at run time, read from a database table.
Maybe the explanation is "over-verbose"... suggestions accepted.
Don't hit the database twice
Consider the following model class:
# models.py Models for django blog application
from django.db import models
class Tag(models.Model):
tag = models.CharField(maxlength=50)
class Author(models.Model):
name = models.CharField(maxlength=100)
email = models.EmailField()
class Article(models.Model):
(...)
author = models.ForeignKey(Author)
tags = models.ManyToManyField(Tag)
The standard method for displaying a form for the model class in a webpage is to subclass forms.Form, create an instance of the subclass and return the rendered the template. Examples of this can be found on the Django website and on the web.
# ArticleForm is a subclass of form.Form
ArticleForm = forms.models.form_for_model(Article)
The next step is to create an instance of ArticleForm
, and return the rendered template:
form = ArticleForm()
return render_to_response('post.html', { 'form': form })
I don't like lists for multiple option selection, at least not in webpages. I prefer checkboxes. It takes some CSS tweaking to render them correctly and evenly spaced on the webpage, but it greatly enhances the user experience. The class generated by form_for_model (and by form_for_instance) stores the widgets in a variable named base_fields
(a dictionary). This makes it simple to replace any of the default widgets with any other widget, provided it makes sense to do so. For example, the following code replaces the author
and tags
widgets with a RadioSelect widget and a CheckboxSelectMultiple widget:
ArticleForm.base_fields['tags'].widget = CheckboxSelectMultiple(choices= ... )
ArticleForm.base_fields['author'].widget = RadioSelect(choices= ... )
The only thing left is to provide the list of choices that both CheckboxSelectMultiple
and RadioSelect
constructors expect as one of their parameters. One way to build such lists is to query the database and fetch the entries for the Tag and Author classes. Something like tagChoices = Tag.objects.all()
and authorChoices = Author.objects.all()
. Convert the resulting queryset to a dictionary and use it as a parameter to the RadioSelect widget constructor.
ArticleForm = forms.models.form_for_model(Article)
ArticleForm.base_fields['tags'].widget = CheckboxSelectMultiple(
choices=ArticleForm.base_fields['tags'].choices)
ArticleForm.base_fields['author'].widget = RadioSelect(
choices=ArticleForm.base_fields['author'].choices)
form = ArticleForm()
return render_to_response('post.html', { 'form': form })
That's it. The Author SelectField gets replaced by radio buttons, and the Tag MultipleSelectField by some nice checkboxes. The database gets hit only once. Nothing to be ashamed of.
A complete example
Updated 2007-5-5: added editpost.html, which was not included by mistake. The following is a test blog application I wrote. It uses form_for_instance
and form_for_model
to define the proper subclass of form, then replaces the standard widgets with custom ones. The widgets load the Author and Tags choices dynamically. In addition, creation and edition of the blog posts are handled by the same method. A regular expression in urls.py
converts web page names to slugs as registered in the database, so permalinks are effectively implemented.
Some screenshots of the Django mini-blog
This is pretty spartan. Throw in some CSS if you like.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd>
<html xmlns="https://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{% block pagetitle %}Page title{% endblock %}</title>
<link rel="stylesheet" href="" type="text/css" media="screen" />
{% block head %}
{% endblock %}
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>
{% extends "base.html" %}
{% block title %}
A minimal blog, powered by Django
{% endblock %}
{% block body %}
<h2>Hello, world!</h2>
<ul>
<li><a href="./add">Write new post</a></li>
</ul>
{% for post in posts %}
<div class="post">
<h2><a href="./{{ post.slug }}.html" title="{{ post.title }}">
{{ post.title }}</a></h2>
<div class="postmeta">
<div class="postdate">{{ post.pub_date }}</div>
<div class="posttags">
Tags:
{% for tag in post.tags.all %}
{{ tag.tag }},
{% endfor %}
</div>
<div class="postauthor">
By {{ post.author }}.
</div>
</div><!-- end postmeta -->
<a href="./edit/{{ post.id }}">Edit post</a>
<div class="postcontent">
{{ post.content }}
</div>
</div>
{% endfor %}
{% endblock %}
{% extends "base.html" %}
{% block title %}
Add post
{% endblock %}
{% block body %}
<h2>Write a post</h2>
<form method="POST" action=".">
{{ form.as_p }}
<input type="submit" value="submit" />
</form>
{% endblock %}
models.py
# Models for django blog application
from django.db import models
class Tag(models.Model):
tag = models.CharField(maxlength=50) # tagname
def __str__(self):
return self.tag
class Admin:
pass
class Author(models.Model):
name = models.CharField(maxlength=100)
email = models.EmailField()
def __str__(self):
return self.name
class Admin:
pass
class Article(models.Model):
title = models.CharField(maxlength=250)
slug = models.CharField(maxlength=250)
content = models.TextField()
author = models.ForeignKey(Author)
pub_date = models.DateTimeField('date published')
mod_date = models.DateTimeField('date modified')
tags = models.ManyToManyField(Tag)
def __str__(self):
return self.title
class Admin:
pass
url.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
# Example:
# (r'^djangotest/', include('djangotest.foo.urls')),
(r'^blog/
views.py
# views.py
from django.http import HttpResponse, HttpResponseRedirect
from django.template import Context, loader
from django.shortcuts import render_to_response, get_list_or_404
from django import newforms as forms
from django.newforms.widgets import *
from djangotest.blog.models import *
def index(request):
""" Display last 5 articles """
last_five = Article.objects.all().order_by('-pub_date')[:5]
t = loader.get_template('blog/index.html')
c = Context({
'posts' : last_five,
})
return HttpResponse(t.render(c))
def single(request, slug):
# handle page not found
articles = get_list_or_404(Article, slug__exact=slug)
return render_to_response('blog/index.html', { 'posts':articles })
def edit(request, post_id=None):
if post_id is None:
# no id, user is creating new post
ArticleForm = forms.models.form_for_model(Article)
else:
# editing or updating post
try:
article = Article.objects.get(id=post_id)
except Article.DoesNotExist:
HttpResponseRedirect('blog/postdoesnotexist.html')
ArticleForm = forms.models.form_for_instance(article)
# we need to feed the new widget with the choices from the
ArticleForm.base_fields['tags'].widget = CheckboxSelectMultiple(
choices=ArticleForm.base_fields['tags'].choices)
ArticleForm.base_fields['author'].widget = RadioSelect(
choices=ArticleForm.base_fields['author'].choices)
if request.method == 'POST':
# form has been submited (i.e., new post or old post update)
form = ArticleForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect('/blog')
else:
# empty form
form = ArticleForm()
return render_to_response('blog/editpost.html', { 'form': form })
References
-
The following pages were of great help in understanding the newforms library better:
-
The Django documentation for newforms. (Still work in progress.)
-
How to use the model specified defaults as the default values for fields, and how to make the help_text entries available in the fields for rendering.
, 'djangotest.blog.views.index'), # regex for slugs. Note that the regex matches even (r'^blog/(?P<slug>((\w+|-)*))(\.html)\/?
views.py
References
-
The following pages were of great help in understanding the newforms library better:
-
The Django documentation for newforms. (Still work in progress.)
-
How to use the model specified defaults as the default values for fields, and how to make the help_text entries available in the fields for rendering.
, 'djangotest.blog.views.single'), # edit post (r'^blog/edit/(?P<post_id>\d+)/
views.py
References
-
The following pages were of great help in understanding the newforms library better:
-
The Django documentation for newforms. (Still work in progress.)
-
How to use the model specified defaults as the default values for fields, and how to make the help_text entries available in the fields for rendering.
, 'djangotest.blog.views.edit'), # add new post (r'^blog/add/
views.py
References
-
The following pages were of great help in understanding the newforms library better:
-
The Django documentation for newforms. (Still work in progress.)
-
How to use the model specified defaults as the default values for fields, and how to make the help_text entries available in the fields for rendering.
, 'djangotest.blog.views.edit'), # Uncomment this for admin: (r'^admin/', include('django.contrib.admin.urls')), )
views.py
References
-
The following pages were of great help in understanding the newforms library better:
-
The Django documentation for newforms. (Still work in progress.)
-
How to use the model specified defaults as the default values for fields, and how to make the help_text entries available in the fields for rendering.