→ Nederlandstalig? E: kevin@roam.beT: +32475435169

DRY KISS: django-stdfields

Because Django doesn't really have a model field to track a duration and I always end up inventing something new to make working with choices on a field easier, I set out to finally build a solution that works for me: django-stdfields.

Duration in Django

When you need to record a duration in Django, you're out of luck using a standard Django model field. The DateField, DateTimeField and TimeField do exactly what they are supposed to do: record a date, date and time or only the time.

Instead, you could use the model field fields.MinutesField from django-stdfields. It's actually a simple extension of Django's PositiveIntegerField accompanied by a forms.MinutesField that will allow input in a format most users can understand. Instead of other existing third-party solutions, the MinutesField does exactly what its name suggests: track minutes, nothing more.

Users can input 8:30, resulting in 510 minutes. They can input 8.5 for the same result. Or even 8,5. Any integer value is interpreted as hours spent, any decimal value is interpreted as hours and part of an hour spent and anything of the form hh:mm is interpreted as the number of hours and exact number of minutes spent. It's basically the way you enter time spent on a task in Basecamp.

The tricky part is displaying those minutes back to your users. It's far easier to interpret 8:30 than 510. Simple solution: use the included minutes filter:

{% load stdfieldstags %}
...
{{ task.time_spent|minutes }}
...

When task.time_spent equals 510, this template will print 8:30.

Choices in Django

The choices argument for fields is great when you're dealing with a fixed number of possible values. The thing is... I always need to start messing with the constants further down the line. And then I need to get the label of the constant. I hate it because it gets gradually harder to find out what the hell you were doing two weeks later. The solution: enumerations.

Django-stdfields contains an easy way to create enums:

from django.db import models
from stdfields.models import Enum, EnumValue, EnumCharField

class CardinalDirection(Enum):
    NORTH = EnumValue('N', 'North')
    EAST = EnumValue('E', 'East')
    SOUTH = EnumValue('S', 'South')
    WEST = EnumValue('W', 'West')


class Wind(models.Model):
    direction = EnumCharField(enum=CardinalDirection,
                            max_length=CardinalDirection.max_length())
    force = models.IntegerField()

You could still use a regular models.CharField and the choices argument. It's basically the same thing the EnumCharField and EnumIntegerField do behind the scenes.

But thanks to the Enumeration base class, things get easier to manage when you have to change stuff around (note: all fields can be used with South).

from django.db import models
from stdfields.models import Enum, EnumValue, EnumCharField

class CardinalDirection(Enum):
    NORTH = EnumValue('N', 'North')
    NORTHEAST = EnumValue('NE', 'Northeast')
    EAST = EnumValue('E', 'East')
    SOUTHEAST = EnumValue('SE', 'Southeast')
    SOUTH = EnumValue('S', 'South')
    SOUTHWEST = EnumValue('SW', 'Southwest')
    WEST = EnumValue('W', 'West')
    NORTHWEST = EnumValue('NW', 'Northwest')


class Wind(models.Model):
    direction = EnumCharField(enum=CardinalDirection,
                            max_length=CardinalDirection.max_length())
    force = models.IntegerField()

We just added northeast, southeast, southwest and northwest to the possible values. Did you notice that this also changes the maximum length for the direction field of Wind?

Well, it doesn't matter: we're using the max_length method provided by Enumeration which will automatically determine the maximum possible length of the values -- in this case 2. Just don't forget to create and run a schema migration with South.

But Enum might not work entirely as expected: when it's constructed, all contained EnumValue fields will be turned into regular fields:

>>> CardinalDirection.NORTH == 'N'
True
>>> CardinalDirection.NORTH_display == 'North'
True
>>> CardinalDirection.all()
[('N', 'North'), ('NE', 'Northeast'), ('E', 'East'), ('SE', 'Southeast'),
('S', 'South'), ('SW', 'Southwest'), ('W', 'West'), ('NW', 'Northwest')]
>>> CardinalDirection.max_length()
2
>>> CardinalDirection.as_display(CardinalDirection.NORTH)
'North'

That's the gist of django-stdfields. It will save me a lot of time and I hope it does the same for you. Get it from Pypi or Bitbucket.

Are you a Java developer getting started with Django? Then you'll surely love my upcoming Django for Java Developers ebook! Find out how to manage dependencies, start, build and deploy a Django project from the perspective of a Java web developer.

§