|
1 """ |
|
2 Classes allowing "generic" relations through ContentType and object-id fields. |
|
3 """ |
|
4 |
|
5 from django import oldforms |
|
6 from django.core.exceptions import ObjectDoesNotExist |
|
7 from django.db import connection |
|
8 from django.db.models import signals |
|
9 from django.db.models.fields.related import RelatedField, Field, ManyToManyRel |
|
10 from django.db.models.loading import get_model |
|
11 from django.dispatch import dispatcher |
|
12 from django.utils.functional import curry |
|
13 |
|
14 class GenericForeignKey(object): |
|
15 """ |
|
16 Provides a generic relation to any object through content-type/object-id |
|
17 fields. |
|
18 """ |
|
19 |
|
20 def __init__(self, ct_field="content_type", fk_field="object_id"): |
|
21 self.ct_field = ct_field |
|
22 self.fk_field = fk_field |
|
23 |
|
24 def contribute_to_class(self, cls, name): |
|
25 # Make sure the fields exist (these raise FieldDoesNotExist, |
|
26 # which is a fine error to raise here) |
|
27 self.name = name |
|
28 self.model = cls |
|
29 self.cache_attr = "_%s_cache" % name |
|
30 |
|
31 # For some reason I don't totally understand, using weakrefs here doesn't work. |
|
32 dispatcher.connect(self.instance_pre_init, signal=signals.pre_init, sender=cls, weak=False) |
|
33 |
|
34 # Connect myself as the descriptor for this field |
|
35 setattr(cls, name, self) |
|
36 |
|
37 def instance_pre_init(self, signal, sender, args, kwargs): |
|
38 """ |
|
39 Handles initializing an object with the generic FK instaed of |
|
40 content-type/object-id fields. |
|
41 """ |
|
42 if self.name in kwargs: |
|
43 value = kwargs.pop(self.name) |
|
44 kwargs[self.ct_field] = self.get_content_type(obj=value) |
|
45 kwargs[self.fk_field] = value._get_pk_val() |
|
46 |
|
47 def get_content_type(self, obj=None, id=None): |
|
48 # Convenience function using get_model avoids a circular import when |
|
49 # using this model |
|
50 ContentType = get_model("contenttypes", "contenttype") |
|
51 if obj: |
|
52 return ContentType.objects.get_for_model(obj) |
|
53 elif id: |
|
54 return ContentType.objects.get_for_id(id) |
|
55 else: |
|
56 # This should never happen. I love comments like this, don't you? |
|
57 raise Exception("Impossible arguments to GFK.get_content_type!") |
|
58 |
|
59 def __get__(self, instance, instance_type=None): |
|
60 if instance is None: |
|
61 raise AttributeError, u"%s must be accessed via instance" % self.name |
|
62 |
|
63 try: |
|
64 return getattr(instance, self.cache_attr) |
|
65 except AttributeError: |
|
66 rel_obj = None |
|
67 |
|
68 # Make sure to use ContentType.objects.get_for_id() to ensure that |
|
69 # lookups are cached (see ticket #5570). This takes more code than |
|
70 # the naive ``getattr(instance, self.ct_field)``, but has better |
|
71 # performance when dealing with GFKs in loops and such. |
|
72 f = self.model._meta.get_field(self.ct_field) |
|
73 ct_id = getattr(instance, f.get_attname(), None) |
|
74 if ct_id: |
|
75 ct = self.get_content_type(id=ct_id) |
|
76 try: |
|
77 rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field)) |
|
78 except ObjectDoesNotExist: |
|
79 pass |
|
80 setattr(instance, self.cache_attr, rel_obj) |
|
81 return rel_obj |
|
82 |
|
83 def __set__(self, instance, value): |
|
84 if instance is None: |
|
85 raise AttributeError, u"%s must be accessed via instance" % self.related.opts.object_name |
|
86 |
|
87 ct = None |
|
88 fk = None |
|
89 if value is not None: |
|
90 ct = self.get_content_type(obj=value) |
|
91 fk = value._get_pk_val() |
|
92 |
|
93 setattr(instance, self.ct_field, ct) |
|
94 setattr(instance, self.fk_field, fk) |
|
95 setattr(instance, self.cache_attr, value) |
|
96 |
|
97 class GenericRelation(RelatedField, Field): |
|
98 """Provides an accessor to generic related objects (i.e. comments)""" |
|
99 |
|
100 def __init__(self, to, **kwargs): |
|
101 kwargs['verbose_name'] = kwargs.get('verbose_name', None) |
|
102 kwargs['rel'] = GenericRel(to, |
|
103 related_name=kwargs.pop('related_name', None), |
|
104 limit_choices_to=kwargs.pop('limit_choices_to', None), |
|
105 symmetrical=kwargs.pop('symmetrical', True)) |
|
106 |
|
107 # Override content-type/object-id field names on the related class |
|
108 self.object_id_field_name = kwargs.pop("object_id_field", "object_id") |
|
109 self.content_type_field_name = kwargs.pop("content_type_field", "content_type") |
|
110 |
|
111 kwargs['blank'] = True |
|
112 kwargs['editable'] = False |
|
113 kwargs['serialize'] = False |
|
114 Field.__init__(self, **kwargs) |
|
115 |
|
116 def get_manipulator_field_objs(self): |
|
117 choices = self.get_choices_default() |
|
118 return [curry(oldforms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] |
|
119 |
|
120 def get_choices_default(self): |
|
121 return Field.get_choices(self, include_blank=False) |
|
122 |
|
123 def flatten_data(self, follow, obj = None): |
|
124 new_data = {} |
|
125 if obj: |
|
126 instance_ids = [instance._get_pk_val() for instance in getattr(obj, self.name).all()] |
|
127 new_data[self.name] = instance_ids |
|
128 return new_data |
|
129 |
|
130 def m2m_db_table(self): |
|
131 return self.rel.to._meta.db_table |
|
132 |
|
133 def m2m_column_name(self): |
|
134 return self.object_id_field_name |
|
135 |
|
136 def m2m_reverse_name(self): |
|
137 return self.model._meta.pk.column |
|
138 |
|
139 def contribute_to_class(self, cls, name): |
|
140 super(GenericRelation, self).contribute_to_class(cls, name) |
|
141 |
|
142 # Save a reference to which model this class is on for future use |
|
143 self.model = cls |
|
144 |
|
145 # Add the descriptor for the m2m relation |
|
146 setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self)) |
|
147 |
|
148 def contribute_to_related_class(self, cls, related): |
|
149 pass |
|
150 |
|
151 def set_attributes_from_rel(self): |
|
152 pass |
|
153 |
|
154 def get_internal_type(self): |
|
155 return "ManyToManyField" |
|
156 |
|
157 def db_type(self): |
|
158 # Since we're simulating a ManyToManyField, in effect, best return the |
|
159 # same db_type as well. |
|
160 return None |
|
161 |
|
162 class ReverseGenericRelatedObjectsDescriptor(object): |
|
163 """ |
|
164 This class provides the functionality that makes the related-object |
|
165 managers available as attributes on a model class, for fields that have |
|
166 multiple "remote" values and have a GenericRelation defined in their model |
|
167 (rather than having another model pointed *at* them). In the example |
|
168 "article.publications", the publications attribute is a |
|
169 ReverseGenericRelatedObjectsDescriptor instance. |
|
170 """ |
|
171 def __init__(self, field): |
|
172 self.field = field |
|
173 |
|
174 def __get__(self, instance, instance_type=None): |
|
175 if instance is None: |
|
176 raise AttributeError, "Manager must be accessed via instance" |
|
177 |
|
178 # This import is done here to avoid circular import importing this module |
|
179 from django.contrib.contenttypes.models import ContentType |
|
180 |
|
181 # Dynamically create a class that subclasses the related model's |
|
182 # default manager. |
|
183 rel_model = self.field.rel.to |
|
184 superclass = rel_model._default_manager.__class__ |
|
185 RelatedManager = create_generic_related_manager(superclass) |
|
186 |
|
187 qn = connection.ops.quote_name |
|
188 |
|
189 manager = RelatedManager( |
|
190 model = rel_model, |
|
191 instance = instance, |
|
192 symmetrical = (self.field.rel.symmetrical and instance.__class__ == rel_model), |
|
193 join_table = qn(self.field.m2m_db_table()), |
|
194 source_col_name = qn(self.field.m2m_column_name()), |
|
195 target_col_name = qn(self.field.m2m_reverse_name()), |
|
196 content_type = ContentType.objects.get_for_model(self.field.model), |
|
197 content_type_field_name = self.field.content_type_field_name, |
|
198 object_id_field_name = self.field.object_id_field_name |
|
199 ) |
|
200 |
|
201 return manager |
|
202 |
|
203 def __set__(self, instance, value): |
|
204 if instance is None: |
|
205 raise AttributeError, "Manager must be accessed via instance" |
|
206 |
|
207 manager = self.__get__(instance) |
|
208 manager.clear() |
|
209 for obj in value: |
|
210 manager.add(obj) |
|
211 |
|
212 def create_generic_related_manager(superclass): |
|
213 """ |
|
214 Factory function for a manager that subclasses 'superclass' (which is a |
|
215 Manager) and adds behavior for generic related objects. |
|
216 """ |
|
217 |
|
218 class GenericRelatedObjectManager(superclass): |
|
219 def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, |
|
220 join_table=None, source_col_name=None, target_col_name=None, content_type=None, |
|
221 content_type_field_name=None, object_id_field_name=None): |
|
222 |
|
223 super(GenericRelatedObjectManager, self).__init__() |
|
224 self.core_filters = core_filters or {} |
|
225 self.model = model |
|
226 self.content_type = content_type |
|
227 self.symmetrical = symmetrical |
|
228 self.instance = instance |
|
229 self.join_table = join_table |
|
230 self.join_table = model._meta.db_table |
|
231 self.source_col_name = source_col_name |
|
232 self.target_col_name = target_col_name |
|
233 self.content_type_field_name = content_type_field_name |
|
234 self.object_id_field_name = object_id_field_name |
|
235 self.pk_val = self.instance._get_pk_val() |
|
236 |
|
237 def get_query_set(self): |
|
238 query = { |
|
239 '%s__pk' % self.content_type_field_name : self.content_type.id, |
|
240 '%s__exact' % self.object_id_field_name : self.pk_val, |
|
241 } |
|
242 return superclass.get_query_set(self).filter(**query) |
|
243 |
|
244 def add(self, *objs): |
|
245 for obj in objs: |
|
246 setattr(obj, self.content_type_field_name, self.content_type) |
|
247 setattr(obj, self.object_id_field_name, self.pk_val) |
|
248 obj.save() |
|
249 add.alters_data = True |
|
250 |
|
251 def remove(self, *objs): |
|
252 for obj in objs: |
|
253 obj.delete() |
|
254 remove.alters_data = True |
|
255 |
|
256 def clear(self): |
|
257 for obj in self.all(): |
|
258 obj.delete() |
|
259 clear.alters_data = True |
|
260 |
|
261 def create(self, **kwargs): |
|
262 kwargs[self.content_type_field_name] = self.content_type |
|
263 kwargs[self.object_id_field_name] = self.pk_val |
|
264 obj = self.model(**kwargs) |
|
265 obj.save() |
|
266 return obj |
|
267 create.alters_data = True |
|
268 |
|
269 return GenericRelatedObjectManager |
|
270 |
|
271 class GenericRel(ManyToManyRel): |
|
272 def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True): |
|
273 self.to = to |
|
274 self.num_in_admin = 0 |
|
275 self.related_name = related_name |
|
276 self.filter_interface = None |
|
277 self.limit_choices_to = limit_choices_to or {} |
|
278 self.edit_inline = False |
|
279 self.raw_id_admin = False |
|
280 self.symmetrical = symmetrical |
|
281 self.multiple = True |
|
282 assert not (self.raw_id_admin and self.filter_interface), \ |
|
283 "Generic relations may not use both raw_id_admin and filter_interface" |