I love building web applications with Django and I love its ORM. Defining models is fairly simple and rendering forms and list is (more or less) straight forward.
However, if you want to access the properties of the model’s fields in templates, then you might find a dead end really fast. This is especially hard when you’re trying to access the verbose names of the model or its fields.
Example
The model
As mentioned before, defining models is quite simple. Here’s an example of a model:
from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext_lazy as _ class Host(models.Model): ''' Example model of a host. ''' hostname = models.CharField( max_length=64, verbose_name=_('hostname'), ) os = models.CharField( max_length=32, verbose_name=_('operating system'), ) class Meta: ''' Meta class for the model. ''' verbose_name = _('Host') verbose_name_plural = _('Hosts')
As you can see, a Host has a hostname and an os field.
The list view
Now let’s say we want to display a list of hosts.
First of all we need our ListView:
from django.views.generic.list import ListView class HostListView(ListView): ''' View which displays a list of all hosts. ''' model = Host
And of course our host_list.html template, which is something like:
{% extends 'project/layout/base.html' %} {% load i18n %} {% block content %} <h1>{% trans 'Hosts' %}</h1> <table> <thead> <tr> <th>{% trans 'hostame' %}</th> <th>{% trans 'operating system' %}</th> </tr> </thead> <tbody> {% for host in host_list %} <tr> <td> <a href="{{ host.get_absolute_url }}"> {{ host.hostname }} </a> </td> <td> {{ host.os }} </td> </tr> {% endfor %} </tbody> </table> {% endblock %}
The problem
This is how I’ve defined my ListViews in the past. Of course this works and there’s no problem with it, except for one little and probably important thing:
We’ve already defined verbose name of the model and its field in the model definition, for example the os field:
os = models.CharField( max_length=32, verbose_name=_('operating system'), )
In our template we had to define and translate the exactly same strings again, for example:
<th>{% trans 'operating system'</th>
Sure this would work fine, but this isn’t very nice and doesn’t meet my DRY expectations. This problem occurs only when we’re using ListViews or DetailViews. As soon as we’re using Forms, we no longer have these problems, as forms already access the verbose names out of the box.
I had to change a lot of verbose names lately. Unfortunately I’ve defined all the table headers manually as described above, so eventually I had to search and replace all the occurences in the templates by hand. To be honest, I’m not a big fan of repetetive tasks and I was going nuts.
The solution
I came up with a solution, because I thought there must be a better way to deal with this kind of problem.
Adding the model to the context data
First of all I’ve added the model to the context data to all of my ListView instances. I didn’t want to repeat myself, so I created a new ListView class inherited from Django’s ListView:
from django.views.generic.list import ListView as DjangoListView class ListView(DjangoListView): ''' Enhanced ListView which also includes the ``model`` in the context data, so that the template has access to its model class. ''' def get_context_data(self): ''' Adds the model to the context data. ''' context = super(ListView, self).get_context_data() context['model'] = self.model return context
Then I’ve updated all my existing view modules by importing my ListView instead of the origin Django ListView:
#from django.views.generic.list import ListView from myproject.lib.views import ListView
Of course you can also go for method decorators, but in this case you always have to define get_context_data() in your views just to decorate them.
Adding template tags
Now as I had access to the models in all my list templates, I was going to create some template tags:
from django import template register = template.Library() @register.simple_tag def model_name(value): ''' Django template filter which returns the verbose name of a model. ''' if hasattr(value, 'model'): value = value.model return value._meta.verbose_name.title() @register.simple_tag def model_name_plural(value): ''' Django template filter which returns the plural verbose name of a model. ''' if hasattr(value, 'model'): value = value.model return value._meta.verbose_name_plural.title() @register.simple_tag def field_name(value, field): ''' Django template filter which returns the verbose name of an object's, model's or related manager's field. ''' if hasattr(value, 'model'): value = value.model return value._meta.get_field(field).verbose_name.title()
I saved them as model_name.py.
Accessing the verbose names in templates
Now I finally used the magic sauce I’ve put together:
{% extends 'project/layout/base.html' %} {% load model_name %} {% block content %} <h1>{% model_name_plural model %}</h1> <table> <thead> <tr> <th>{% field_name model 'hostname' %}</th> <th>{% field_name model 'os' %}</th> </tr> </thead> <tbody> {% for host in host_list %} <tr> <td> <a href="{{ host.get_absolute_url }}"> {{ host.hostname }} </a> </td> <td> {{ host.os }} </td> </tr> {% endfor %} </tbody> </table> {% endblock %}
As you can see with the load of model_name and the model itself, you can now get the model and field names by using:
{% model_name model %} {% model_name_plural model %} {% field_name model field_name %}
Conclusion
Adding the model to the ListView context is a clean way to access the model’s and fields verbose names in list views. By accessing the existing model and field properties, you can define the verbose names in one single place – the model definition.
You don’t necessary need the template tags, as you can now access the model’s attributes directly in the template itself. However, using template tags is more convenient and you can now add some additional logic to the tags (e.g. fallback to the name when no verbose name is defined).
16 Comments