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
- Add Toggle Switch Widget to Django Forms by OgliariNatan 1 month, 4 weeks ago
 - get_object_or_none by azwdevops 5 months, 2 weeks ago
 - Mask sensitive data from logger by agusmakmun 7 months, 2 weeks ago
 - Template tag - list punctuation for a list of items by shapiromatron 1 year, 9 months ago
 - JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 1 year, 9 months ago
 
Comments
Please login first before commenting.