Login

Ordered items in the database

Author:
Leonidas
Posted:
May 21, 2007
Language:
Python
Version:
.96
Score:
2 (after 2 ratings)

First off: This snippet is more or less obsoleted by snippet #259. The way that snippet #259 uses feels cleaner and more usable. It is basically the same code but the magic works a little bit different. In case you prefer this way, I've left this snippet as-is.

Maybe you know this problem: you have some objects in your database and would like to display them in some way. The problem: the Django ORM does not provide any way to sort model instances by a user-defined criterion. There was already a plug-in in SQLAlchemy called orderinglist, so implementing this in Django was basically not a big deal.

Usage is (due to use of meta-classes) quite simple. It is recommended to save this snippet into a separate file called positional.py. To use it, you only have to import PositionalSortMixIn from the positional module and inherit from it in your own, custom model (but before you inherit from models.Model, the order counts).

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#!/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)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 9 months, 3 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 4 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 5 months ago
  5. Help text hyperlinks by sa2812 1 year, 6 months ago

Comments

Please login first before commenting.