Ordered items in the database

  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. Ordered items in the database - alternative by Leonidas 6 years, 10 months ago
  2. DRY with common model fields by miracle2k 6 years, 9 months ago
  3. Improved Pickled Object Field by taavi223 4 years, 8 months ago
  4. Extended i18n base model by alcinnz 1 year ago
  5. Complex Form Preview by smagala 5 years ago

Comments

(Forgotten your password?)