Update 23 Jun 2011: I have renamed the django-model-filters
package django-model-blocks
. It now lets you easily override the template used for a given model. Check out the changes on Github or PyPI.
Tonight I'm writing my first Django custom filter. The problem I'm trying to solve is that I want generic templates. For a given model I want to be able to set up browseable index and detail pages with minimal effort. As it stands now, say I have the following model:
...
class PepulatorModel (models.Model):
serial_number = IntegerField()
height = IntegerField()
width = IntegerField()
manufacture_date = DateTimeField()
color = CharField(max_length=32)
def __unicode__(self):
return u'Pepulator #%s' % self.serial_number
...
Now say I want my users to be able to browse my pepulators in a simple way, with the following caveats:
- They cannot edit pepulators, only view (rules out
admin
app)
- I want to define the URL structure (rules out
databrowse
) to be something like:
http://www.mysite.com/pepulators/ http://www.mysite.com/pepulators/?color=red http://www.mysite.com/pepulators/012345/
- I want to specify the base template so that it integrates well with the rest of my project (also rules out
databrowse
)
Currently, I can use the generic views ListView
and DetailView
, but I still have to write templates that go something like this:
{% extends base.html %}
{% block content %}
<header>
<h1>Pepulator #{{ pepulator.serial_number }}</h1>
</header>
<div>
<span>Serial Number</span>
<p>{{ pepulator.serial_number }}</p>
</div>
<div>
<span>Height</span>
<p>{{ pepulator.height }}</p>
</div>
<div>
<span>Width</span>
<p>{{ pepulator.width }}</p>
</div>
<div>
<span>Manufacturer</span>
<p>{{ pepulator.manufacturer }}</p>
</div>
<div>
<span>Color</span>
<p>{{ pepulator.color }}</p>
</div>
{% endblock %}
Okay, a bit verbose, but it's not going to kill me. However, now say I want to change some of the fields on my model. Well, then I have to remember to change the fields in my template as well (error-prone — this is why you don't violate DRY without good reason).
All I wanted was a simple view of my model!
So, I considered making an app that was leaner than databrowser and just provided generic templates to go with generic views. I found myself having to extend the generic views anyway, though, because there's no way to access a model instance's fields and field names without explicitly feeding them to the template's context. Then, I gleaned some inspiration from uni_forms: I'll make filters!
Now my plan is to be able to say, using the example of the Pepulator detail view above:
{% extends base.html %}
{% block content %}
{{ pepulator|as_detail_html }}
{% endblock %}
Sublime. (This must exist somewhere; but for now, I can't find it.)
So, I start off by creating my app
$ python manage.py startapp generic_templates
Now, from the documentation on creating custom tags and filters, I see I should create a templatetags
direcotry in my app. In here I'll put an __init__.py
file and a module called generic_filters
. This way, when I'm done, to use the filters, I'll put near the top of my template file:
{% load generic_filters %}
I decided to start with the detail filter (as_detail_html
), and to write a test first. I know generally what I want this to do, so I write the following test:
"""
Test the generic filters
"""
import datetime
from django.test import TestCase
from mock import Mock
from django.db.models import Model, IntegerField, DateTimeField, CharField
from django.template import Context, Template
from generic_templates.templatetags import generic_filters as gf
class DetailHtmlFilterTest (TestCase):
def setUp(self):
# Create a sample model
class PepulatorModel (Model):
serial_number = IntegerField(primary_key=True)
height = IntegerField()
width = IntegerField()
manufacture_date = DateTimeField()
color = CharField(max_length=32)
def __unicode__(self):
return u'Pepulator #%s' % self.serial_number
# Create a model instance
now = datetime.datetime.now()
self.m = PepulatorModel(
serial_number = 123456,
height = 25,
width = 16,
manufacture_date = now,
color = 'chartreuse',
)
# Mock Django's get_template so that it doesn't load a real file;
# instead just return a template that allows us to verify the context
gf.get_template = Mock(
return_value=Template('{{ instance|safe }}:{{ fields|safe }}'))
def test_model_format(self):
"""Tests that a given model is formatted as expected."""
expected_detail = (u"Pepulator #123456:[('serial number', 123456),"
" ('height', 25), ('width', 16), ('manufacture date', %r),"
" ('color', 'chartreuse')]") % self.m.manufacture_date
detail = gf.as_detail_html(self.m)
gf.get_template.assert_called_with('object_detail.html')
self.assertEqual(detail, expected_detail)
In short, set up a model and an easy template, and check that the template is filled in correctly. Of course, since I haven't yet written my filter, this fails.
This (as_detail_html
) was a straightforward method to write, but I did get tripped up because of the poor documentation available on Model
s' Meta
classes. Here's the first go at the filter:
from django.template import Context, Template
from django.template.loader import get_template
def as_detail_html(instance):
"""
Template filter that returns the given instance as a template-formatted
block. Inserts two objects into the context:
``instance`` - The model instance
``fields`` - A list of (name, value)-pairs representing the instance's
fields
"""
template = get_template('object_detail.html')
fields = [(field.verbose_name, getattr(instance, field.name))
for field in instance._meta.fields]
context = Context({'instance':instance, 'fields':fields})
return template.render(context)
One other thing: I actually want to be able to use the filter in my templates, not call it directly in my code. I'm new here, so I write another test to make sure I understand what's going on:
def test_filter_is_registered(self):
"""Test that the filter can be used from within a template"""
template = Template(('{% load generic_filters %}'
'{{ pepulator|as_detail_html }}'))
context = Context({'pepulator':self.m})
expected_detail = (u"Pepulator #123456:[('serial number', 123456),"
" ('height', 25), ('width', 16), ('manufacture date', %r),"
" ('color', 'chartreuse')]") % self.m.manufacture_date
detail = template.render(context)
gf.get_template.assert_called_with('object_detail.html')
self.assertEqual(detail, expected_detail)
And it turns out all I have to do to satisfy it is change my module in the following way:
from django.template import Context, Template, Library
from django.template.loader import get_template
register = Library()
@register.filter
def as_detail_html(instance):
...
Now I have a working object detail template. Yay! I figure I'll do the list the same way.
More on Github: https://github.com/mjumbewu/django-model-filters