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