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)
|
Comments