1 # -*- coding: utf-8 -*- |
|
2 from django.db import models, router |
|
3 from django.db.models import signals |
|
4 from django.db.models.fields.related import (create_many_related_manager, |
|
5 ManyToManyField, ReverseManyRelatedObjectsDescriptor) |
|
6 from hdabo.forms import SortedMultipleChoiceField |
|
7 from hdabo.utils import OrderedSet |
|
8 |
|
9 |
|
10 SORT_VALUE_FIELD_NAME = 'sort_value' |
|
11 |
|
12 |
|
13 def create_sorted_many_related_manager(superclass, rel): |
|
14 RelatedManager = create_many_related_manager(superclass, rel) |
|
15 |
|
16 class SortedRelatedManager(RelatedManager): |
|
17 def get_query_set(self): |
|
18 # We use ``extra`` method here because we have no other access to |
|
19 # the extra sorting field of the intermediary model. The fields |
|
20 # are hidden for joins because we set ``auto_created`` on the |
|
21 # intermediary's meta options. |
|
22 return super(SortedRelatedManager, self).\ |
|
23 get_query_set().\ |
|
24 order_by('%s.%s' % ( |
|
25 rel.through._meta.db_table, |
|
26 rel.through._sort_field_name,)) |
|
27 |
|
28 def _add_items(self, source_field_name, target_field_name, *objs): |
|
29 # join_table: name of the m2m link table |
|
30 # source_field_name: the PK fieldname in join_table for the source object |
|
31 # target_field_name: the PK fieldname in join_table for the target object |
|
32 # *objs - objects to add. Either object instances, or primary keys of object instances. |
|
33 |
|
34 # If there aren't any objects, there is nothing to do. |
|
35 from django.db.models import Model |
|
36 if objs: |
|
37 count = self.through._default_manager.count |
|
38 new_ids = OrderedSet() |
|
39 for obj in objs: |
|
40 if isinstance(obj, self.model): |
|
41 if not router.allow_relation(obj, self.instance): |
|
42 raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % |
|
43 (obj, self.instance._state.db, obj._state.db)) |
|
44 new_ids.add(obj.pk) |
|
45 elif isinstance(obj, Model): |
|
46 raise TypeError("'%s' instance expected" % self.model._meta.object_name) |
|
47 else: |
|
48 new_ids.add(obj) |
|
49 db = router.db_for_write(self.through, instance=self.instance) |
|
50 vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True) |
|
51 vals = vals.filter(**{ |
|
52 source_field_name: self._pk_val, |
|
53 '%s__in' % target_field_name: new_ids, |
|
54 }) |
|
55 new_ids = new_ids - OrderedSet(vals) |
|
56 |
|
57 if self.reverse or source_field_name == self.source_field_name: |
|
58 # Don't send the signal when we are inserting the |
|
59 # duplicate data row for symmetrical reverse entries. |
|
60 signals.m2m_changed.send(sender=rel.through, action='pre_add', |
|
61 instance=self.instance, reverse=self.reverse, |
|
62 model=self.model, pk_set=new_ids, using=db) |
|
63 # Add the ones that aren't there already |
|
64 count = self.through._default_manager.count |
|
65 for obj_id in new_ids: |
|
66 self.through._default_manager.using(db).create(**{ |
|
67 '%s_id' % source_field_name: self._pk_val, |
|
68 '%s_id' % target_field_name: obj_id, |
|
69 self.through._sort_field_name: count(), |
|
70 }) |
|
71 if self.reverse or source_field_name == self.source_field_name: |
|
72 # Don't send the signal when we are inserting the |
|
73 # duplicate data row for symmetrical reverse entries. |
|
74 signals.m2m_changed.send(sender=rel.through, action='post_add', |
|
75 instance=self.instance, reverse=self.reverse, |
|
76 model=self.model, pk_set=new_ids, using=db) |
|
77 |
|
78 |
|
79 return SortedRelatedManager |
|
80 |
|
81 |
|
82 class ReverseSortedManyRelatedObjectsDescriptor(ReverseManyRelatedObjectsDescriptor): |
|
83 def __get__(self, instance, instance_type=None): |
|
84 if instance is None: |
|
85 return self |
|
86 |
|
87 # Dynamically create a class that subclasses the related |
|
88 # model's default manager. |
|
89 rel_model = self.field.rel.to |
|
90 superclass = rel_model._default_manager.__class__ |
|
91 RelatedManager = create_sorted_many_related_manager(superclass, self.field.rel) |
|
92 |
|
93 manager = RelatedManager( |
|
94 model=rel_model, |
|
95 core_filters={'%s__pk' % self.field.related_query_name(): instance._get_pk_val()}, |
|
96 instance=instance, |
|
97 symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)), |
|
98 source_field_name=self.field.m2m_field_name(), |
|
99 target_field_name=self.field.m2m_reverse_field_name(), |
|
100 reverse=False |
|
101 ) |
|
102 |
|
103 return manager |
|
104 |
|
105 def __set__(self, instance, value): |
|
106 if instance is None: |
|
107 raise AttributeError, "Manager must be accessed via instance" |
|
108 |
|
109 manager = self.__get__(instance) |
|
110 manager.clear() |
|
111 manager.add(*value) |
|
112 |
|
113 |
|
114 class SortedManyToManyField(ManyToManyField): |
|
115 ''' |
|
116 Providing a many to many relation that remembers the order of related |
|
117 objects. |
|
118 |
|
119 Accept a boolean ``sorted`` attribute which specifies if relation is |
|
120 ordered or not. Default is set to ``True``. If ``sorted`` is set to |
|
121 ``False`` the field will behave exactly like django's ``ManyToManyField``. |
|
122 ''' |
|
123 def __init__(self, to, sorted=True, **kwargs): |
|
124 self.sorted = sorted |
|
125 if self.sorted: |
|
126 # This is very hacky and should be removed if a better solution is |
|
127 # found. |
|
128 kwargs.setdefault('through', True) |
|
129 super(SortedManyToManyField, self).__init__(to, **kwargs) |
|
130 self.help_text = kwargs.get('help_text', None) |
|
131 |
|
132 def create_intermediary_model(self, cls, field_name): |
|
133 ''' |
|
134 Create intermediary model that stores the relation's data. |
|
135 ''' |
|
136 module = '' |
|
137 |
|
138 # make sure rel.to is a model class and not a string |
|
139 if isinstance(self.rel.to, basestring): |
|
140 bits = self.rel.to.split('.') |
|
141 if len(bits) == 1: |
|
142 bits = cls._meta.app_label.lower(), bits[0] |
|
143 self.rel.to = models.get_model(*bits) |
|
144 |
|
145 model_name = '%s_%s_%s' % ( |
|
146 cls._meta.app_label, |
|
147 cls._meta.object_name, |
|
148 field_name) |
|
149 from_ = '%s.%s' % ( |
|
150 cls._meta.app_label, |
|
151 cls._meta.object_name) |
|
152 |
|
153 def default_sort_value(): |
|
154 model = models.get_model(cls._meta.app_label, model_name) |
|
155 return model._default_manager.count() |
|
156 |
|
157 # Using from and to model's name as field names for relations. This is |
|
158 # also django default behaviour for m2m intermediary tables. |
|
159 fields = { |
|
160 cls._meta.object_name.lower(): |
|
161 models.ForeignKey(from_), |
|
162 # using to model's name as field name for the other relation |
|
163 self.rel.to._meta.object_name.lower(): |
|
164 models.ForeignKey(self.rel.to), |
|
165 SORT_VALUE_FIELD_NAME: |
|
166 models.IntegerField(default=default_sort_value), |
|
167 } |
|
168 |
|
169 class Meta: |
|
170 db_table = '%s_%s_%s' % ( |
|
171 cls._meta.app_label.lower(), |
|
172 cls._meta.object_name.lower(), |
|
173 field_name.lower()) |
|
174 app_label = cls._meta.app_label |
|
175 ordering = (SORT_VALUE_FIELD_NAME,) |
|
176 auto_created = cls |
|
177 |
|
178 attrs = { |
|
179 '__module__': module, |
|
180 'Meta': Meta, |
|
181 '_sort_field_name': SORT_VALUE_FIELD_NAME, |
|
182 '__unicode__': lambda s: 'pk=%d' % s.pk, |
|
183 } |
|
184 |
|
185 # Add in any fields that were provided |
|
186 if fields: |
|
187 attrs.update(fields) |
|
188 |
|
189 # Create the class, which automatically triggers ModelBase processing |
|
190 model = type(model_name, (models.Model,), attrs) |
|
191 |
|
192 return model |
|
193 |
|
194 def contribute_to_class(self, cls, name): |
|
195 if self.sorted: |
|
196 self.rel.through = self.create_intermediary_model(cls, name) |
|
197 super(SortedManyToManyField, self).contribute_to_class(cls, name) |
|
198 # overwrite default descriptor with reverse and sorted one |
|
199 setattr(cls, self.name, ReverseSortedManyRelatedObjectsDescriptor(self)) |
|
200 else: |
|
201 super(SortedManyToManyField, self).contribute_to_class(cls, name) |
|
202 |
|
203 def formfield(self, **kwargs): |
|
204 defaults = {} |
|
205 if self.sorted: |
|
206 defaults['form_class'] = SortedMultipleChoiceField |
|
207 defaults.update(kwargs) |
|
208 return super(SortedManyToManyField, self).formfield(**defaults) |
|