UPDATE: This doesn’t work in in Django >= 1.0 anymore. Please refer to this post.

I have a django model called ‘category’. And I wanted to be able to change the order of these categories from within the admin interface. The same way for example phpBB does it with its forums (move up, move down). So I wrote a custom Field, called OrderingField, which looks like this in the admin interface:

orderingfield

And here is the code:

from django import oldforms
from django.utils.functional import curry
from django.db import models
from django.db.models import signals
from django.dispatch import dispatcher
from django.utils.translation import ugettext as _

class OrderingField(models.Field):
    MOVE_CHOICES = (
        (models.BLANK_CHOICE_DASH[0]),
        ('FIRST', 'First'),
        ('UP', 'Up'),
        ('DOWN', 'Down'),
        ('LAST', 'Last'),
    )
    move = ''

    def __init__(self, *args, **kwargs):
        kwargs['blank'] = True
        kwargs['verbose_name'] = _("move")
        models.Field.__init__(self, *args, **kwargs)

    def get_internal_type(self):
        return "IntegerField"

    def to_python(self, value):
        if value is None:
            return value
        try:
            return int(value)
        except (TypeError, ValueError):
            raise validators.ValidationError, _("This value must be an integer.")

    def contribute_to_class(self, cls, name):
        super(OrderingField, self).contribute_to_class(cls, name)
        dispatcher.connect(self.post_save, signals.post_save, sender=cls)

    def get_manipulator_new_data(self, new_data, rel=False):
        self.move = new_data[self.name + '_move']
        return super(OrderingField, self).get_manipulator_new_data(new_data, rel)

    def post_save(self, instance=None):
        if hasattr(instance, 'move') and self.move:
            move = self.move
            self.move = None
            instance.move(move)

    def get_manipulator_field_objs(self):
        return [oldforms.HiddenField, curry(oldforms.SelectField, choices=self.MOVE_CHOICES)]

    def get_manipulator_field_names(self, name_prefix):
        return [name_prefix + self.name, name_prefix + self.name + '_move']

Internally the order is saved as an integer. The module that uses this field needs to implement a method called ‘move’ with one arguement. The arguement can be UP, DOWN, FIRST or LAST. The method is called after the object has been saved, and can now reorder the categories respectively to the arguement.

For example:

class Category(models.Model):
    #...
    order = OrderingField()

    def move(self, move):
        if move == 'UP':
            cat = Category.objects.get(order=self.order-1)
            cat.order += 1
            cat.save()
            self.order -= 1
            self.save()
        #...