#!/usr/bin/env python # -*- coding: utf-8 -*- from django.db import models class SortingManager(models.Manager): """An extended manager that extends the default manager by `get_sorted()` which returns the objects in the database sorted by their position""" def get_sorted(self): """Returns a sorted list""" return self.model.objects.all().order_by('position') class InjectingModelBase(models.base.ModelBase): """This helper metaclass is used by PositionalSortMixIn. This metaclass injects two attributes: - objects: this replaces the default manager - position: this field holds the information about the position of the item in the list""" def __new__(cls, name, bases, attrs): """Metaclass constructor calling Django and then modifying the resulting class""" # get the class which was alreeady built by Django child = models.base.ModelBase.__new__(cls, name, bases, attrs) # try to add some more or less neccessary fields try: # add the sorting manager child.add_to_class('objects', SortingManager()) # add the IntegerField child.add_to_class('position', models.IntegerField(editable=False)) except AttributeError: # add_to_class was not yet added to the class. # No problem, this is called twice by Django, add_to-class # will appear later pass # we're done - output the class, it's ready for use return child class PositionalSortMixIn(object): """This mix-in class implements a user defined order in the database. To apply this mix-in you need to inherit from it before you inherit from `models.Model`. It adds a custom manager and a IntegerField called `position` to your model. be careful, it overwrites any existing fiels that you might have defined.""" # get a metaclass which injects the neccessary fields __metaclass__ = InjectingModelBase def get_object_at_offset(self, offset): """Get the object whose position is `offset` positions away from it's own.""" # get the class in which this was mixed in model_class = self.__class__ try: return model_class.objects.get(position=self.position+offset) except model_class.DoesNotExist: # no such model? no deal, just return None return None # some shortcuts, convenience methods get_next = lambda self: self.get_object_at_offset(1) get_previous = lambda self: self.get_object_at_offset(-1) def move_down(self): """Moves element one position down""" # get the element after this one one_after = self.get_next() if not one_after: # already the last element return # flip the positions one_after.position, self.position = self.position, one_after.position for obj in (one_after, self): obj.save() def move_up(self): """Moves element one position up""" # get the element before this one one_before = self.get_previous() if not one_before: # already the first return # flip the positions one_before.position, self.position = self.position, one_before.position for obj in (one_before, self): obj.save() def save(self): """Saves the model to the database. It populates the `position` field of the model automatically if there is no such field set. In this case, the element will be appended at the end of the list.""" model_class = self.__class__ # is there a position saved? if not self.position: # no, it was ampty. Find one try: # get the last object last = model_class.objects.all().order_by('-position')[0] # the position of the last element self.position = last.position +1 except IndexError: # IndexError happened: the quary did not return any objects # so this has to be the first self.position = 0 # save the now properly set-up model return models.Model.save(self) def delete(self): """Deletes the item from the list.""" model_class = self.__class__ # get all objects with a position greater than this objects position objects_after = model_class.objects.filter(position__gt=self.position) # iterate through all objects which were found for element in objects_after: # decrease the position in the list (means: move forward) element.position -= 1 element.save() # now we can safely remove this model instance return models.Model.delete(self)