Django

Code

root/django/trunk/django/contrib/contenttypes/generic.py

Revision 8884, 15.0 kB (checked in by mtredinnick, 3 months ago)

Fixed #8669 -- Use a consistent version of create() across the board for
model/field instance creation. Based on a patch from Richard Davies.

  • Property svn:eol-style set to native
Line 
1 """
2 Classes allowing "generic" relations through ContentType and object-id fields.
3 """
4
5 from django.core.exceptions import ObjectDoesNotExist
6 from django.db import connection
7 from django.db.models import signals
8 from django.db import models
9 from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
10 from django.db.models.loading import get_model
11 from django.forms import ModelForm
12 from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance
13 from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets
14 from django.utils.encoding import smart_unicode
15
16 class GenericForeignKey(object):
17     """
18     Provides a generic relation to any object through content-type/object-id
19     fields.
20     """
21
22     def __init__(self, ct_field="content_type", fk_field="object_id"):
23         self.ct_field = ct_field
24         self.fk_field = fk_field
25
26     def contribute_to_class(self, cls, name):
27         self.name = name
28         self.model = cls
29         self.cache_attr = "_%s_cache" % name
30         cls._meta.add_virtual_field(self)
31
32         # For some reason I don't totally understand, using weakrefs here doesn't work.
33         signals.pre_init.connect(self.instance_pre_init, sender=cls, weak=False)
34
35         # Connect myself as the descriptor for this field
36         setattr(cls, name, self)
37
38     def instance_pre_init(self, signal, sender, args, kwargs, **_kwargs):
39         """
40         Handles initializing an object with the generic FK instaed of
41         content-type/object-id fields.
42         """
43         if self.name in kwargs:
44             value = kwargs.pop(self.name)
45             kwargs[self.ct_field] = self.get_content_type(obj=value)
46             kwargs[self.fk_field] = value._get_pk_val()
47
48     def get_content_type(self, obj=None, id=None):
49         # Convenience function using get_model avoids a circular import when
50         # using this model
51         ContentType = get_model("contenttypes", "contenttype")
52         if obj:
53             return ContentType.objects.get_for_model(obj)
54         elif id:
55             return ContentType.objects.get_for_id(id)
56         else:
57             # This should never happen. I love comments like this, don't you?
58             raise Exception("Impossible arguments to GFK.get_content_type!")
59
60     def __get__(self, instance, instance_type=None):
61         if instance is None:
62             raise AttributeError, u"%s must be accessed via instance" % self.name
63
64         try:
65             return getattr(instance, self.cache_attr)
66         except AttributeError:
67             rel_obj = None
68
69             # Make sure to use ContentType.objects.get_for_id() to ensure that
70             # lookups are cached (see ticket #5570). This takes more code than
71             # the naive ``getattr(instance, self.ct_field)``, but has better
72             # performance when dealing with GFKs in loops and such.
73             f = self.model._meta.get_field(self.ct_field)
74             ct_id = getattr(instance, f.get_attname(), None)
75             if ct_id:
76                 ct = self.get_content_type(id=ct_id)
77                 try:
78                     rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field))
79                 except ObjectDoesNotExist:
80                     pass
81             setattr(instance, self.cache_attr, rel_obj)
82             return rel_obj
83
84     def __set__(self, instance, value):
85         if instance is None:
86             raise AttributeError, u"%s must be accessed via instance" % self.related.opts.object_name
87
88         ct = None
89         fk = None
90         if value is not None:
91             ct = self.get_content_type(obj=value)
92             fk = value._get_pk_val()
93
94         setattr(instance, self.ct_field, ct)
95         setattr(instance, self.fk_field, fk)
96         setattr(instance, self.cache_attr, value)
97
98 class GenericRelation(RelatedField, Field):
99     """Provides an accessor to generic related objects (e.g. comments)"""
100
101     def __init__(self, to, **kwargs):
102         kwargs['verbose_name'] = kwargs.get('verbose_name', None)
103         kwargs['rel'] = GenericRel(to,
104                             related_name=kwargs.pop('related_name', None),
105                             limit_choices_to=kwargs.pop('limit_choices_to', None),
106                             symmetrical=kwargs.pop('symmetrical', True))
107
108         # By its very nature, a GenericRelation doesn't create a table.
109         self.creates_table = False
110
111         # Override content-type/object-id field names on the related class
112         self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
113         self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
114
115         kwargs['blank'] = True
116         kwargs['editable'] = False
117         kwargs['serialize'] = False
118         Field.__init__(self, **kwargs)
119
120     def get_choices_default(self):
121         return Field.get_choices(self, include_blank=False)
122
123     def value_to_string(self, obj):
124         qs = getattr(obj, self.name).all()
125         return smart_unicode([instance._get_pk_val() for instance in qs])
126
127     def m2m_db_table(self):
128         return self.rel.to._meta.db_table
129
130     def m2m_column_name(self):
131         return self.object_id_field_name
132
133     def m2m_reverse_name(self):
134         return self.model._meta.pk.column
135
136     def contribute_to_class(self, cls, name):
137         super(GenericRelation, self).contribute_to_class(cls, name)
138
139         # Save a reference to which model this class is on for future use
140         self.model = cls
141
142         # Add the descriptor for the m2m relation
143         setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self))
144
145     def contribute_to_related_class(self, cls, related):
146         pass
147
148     def set_attributes_from_rel(self):
149         pass
150
151     def get_internal_type(self):
152         return "ManyToManyField"
153
154     def db_type(self):
155         # Since we're simulating a ManyToManyField, in effect, best return the
156         # same db_type as well.
157         return None
158
159     def extra_filters(self, pieces, pos, negate):
160         """
161         Return an extra filter to the queryset so that the results are filtered
162         on the appropriate content type.
163         """
164         if negate:
165             return []
166         ContentType = get_model("contenttypes", "contenttype")
167         content_type = ContentType.objects.get_for_model(self.model)
168         prefix = "__".join(pieces[:pos + 1])
169         return [("%s__%s" % (prefix, self.content_type_field_name),
170             content_type)]
171
172 class ReverseGenericRelatedObjectsDescriptor(object):
173     """
174     This class provides the functionality that makes the related-object
175     managers available as attributes on a model class, for fields that have
176     multiple "remote" values and have a GenericRelation defined in their model
177     (rather than having another model pointed *at* them). In the example
178     "article.publications", the publications attribute is a
179     ReverseGenericRelatedObjectsDescriptor instance.
180     """
181     def __init__(self, field):
182         self.field = field
183
184     def __get__(self, instance, instance_type=None):
185         if instance is None:
186             raise AttributeError, "Manager must be accessed via instance"
187
188         # This import is done here to avoid circular import importing this module
189         from django.contrib.contenttypes.models import ContentType
190
191         # Dynamically create a class that subclasses the related model's
192         # default manager.
193         rel_model = self.field.rel.to
194         superclass = rel_model._default_manager.__class__
195         RelatedManager = create_generic_related_manager(superclass)
196
197         qn = connection.ops.quote_name
198
199         manager = RelatedManager(
200             model = rel_model,
201             instance = instance,
202             symmetrical = (self.field.rel.symmetrical and instance.__class__ == rel_model),
203             join_table = qn(self.field.m2m_db_table()),
204             source_col_name = qn(self.field.m2m_column_name()),
205             target_col_name = qn(self.field.m2m_reverse_name()),
206             content_type = ContentType.objects.get_for_model(self.field.model),
207             content_type_field_name = self.field.content_type_field_name,
208             object_id_field_name = self.field.object_id_field_name
209         )
210
211         return manager
212
213     def __set__(self, instance, value):
214         if instance is None:
215             raise AttributeError, "Manager must be accessed via instance"
216
217         manager = self.__get__(instance)
218         manager.clear()
219         for obj in value:
220             manager.add(obj)
221
222 def create_generic_related_manager(superclass):
223     """
224     Factory function for a manager that subclasses 'superclass' (which is a
225     Manager) and adds behavior for generic related objects.
226     """
227
228     class GenericRelatedObjectManager(superclass):
229         def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
230                      join_table=None, source_col_name=None, target_col_name=None, content_type=None,
231                      content_type_field_name=None, object_id_field_name=None):
232
233             super(GenericRelatedObjectManager, self).__init__()
234             self.core_filters = core_filters or {}
235             self.model = model
236             self.content_type = content_type
237             self.symmetrical = symmetrical
238             self.instance = instance
239             self.join_table = join_table
240             self.join_table = model._meta.db_table
241             self.source_col_name = source_col_name
242             self.target_col_name = target_col_name
243             self.content_type_field_name = content_type_field_name
244             self.object_id_field_name = object_id_field_name
245             self.pk_val = self.instance._get_pk_val()
246
247         def get_query_set(self):
248             query = {
249                 '%s__pk' % self.content_type_field_name : self.content_type.id,
250                 '%s__exact' % self.object_id_field_name : self.pk_val,
251             }
252             return superclass.get_query_set(self).filter(**query)
253
254         def add(self, *objs):
255             for obj in objs:
256                 setattr(obj, self.content_type_field_name, self.content_type)
257                 setattr(obj, self.object_id_field_name, self.pk_val)
258                 obj.save()
259         add.alters_data = True
260
261         def remove(self, *objs):
262             for obj in objs:
263                 obj.delete()
264         remove.alters_data = True
265
266         def clear(self):
267             for obj in self.all():
268                 obj.delete()
269         clear.alters_data = True
270
271         def create(self, **kwargs):
272             kwargs[self.content_type_field_name] = self.content_type
273             kwargs[self.object_id_field_name] = self.pk_val
274             return super(GenericRelatedObjectManager, self).create(**kwargs)
275         create.alters_data = True
276
277     return GenericRelatedObjectManager
278
279 class GenericRel(ManyToManyRel):
280     def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True):
281         self.to = to
282         self.related_name = related_name
283         self.limit_choices_to = limit_choices_to or {}
284         self.symmetrical = symmetrical
285         self.multiple = True
286
287 class BaseGenericInlineFormSet(BaseModelFormSet):
288     """
289     A formset for generic inline objects to a parent.
290     """
291     ct_field_name = "content_type"
292     ct_fk_field_name = "object_id"
293
294     def __init__(self, data=None, files=None, instance=None, save_as_new=None):
295         opts = self.model._meta
296         self.instance = instance
297         self.rel_name = '-'.join((
298             opts.app_label, opts.object_name.lower(),
299             self.ct_field.name, self.ct_fk_field.name,
300         ))
301         super(BaseGenericInlineFormSet, self).__init__(
302             queryset=self.get_queryset(), data=data, files=files,
303             prefix=self.rel_name
304         )
305
306     def get_queryset(self):
307         # Avoid a circular import.
308         from django.contrib.contenttypes.models import ContentType
309         if self.instance is None:
310             return self.model._default_manager.empty()
311         return self.model._default_manager.filter(**{
312             self.ct_field.name: ContentType.objects.get_for_model(self.instance),
313             self.ct_fk_field.name: self.instance.pk,
314         })
315
316     def save_new(self, form, commit=True):
317         # Avoid a circular import.
318         from django.contrib.contenttypes.models import ContentType
319         kwargs = {
320             self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk,
321             self.ct_fk_field.get_attname(): self.instance.pk,
322         }
323         new_obj = self.model(**kwargs)
324         return save_instance(form, new_obj, commit=commit)
325
326 def generic_inlineformset_factory(model, form=ModelForm,
327                                   formset=BaseGenericInlineFormSet,
328                                   ct_field="content_type", fk_field="object_id",
329                                   fields=None, exclude=None,
330                                   extra=3, can_order=False, can_delete=True,
331                                   max_num=0,
332                                   formfield_callback=lambda f: f.formfield()):
333     """
334     Returns an ``GenericInlineFormSet`` for the given kwargs.
335
336     You must provide ``ct_field`` and ``object_id`` if they different from the
337     defaults ``content_type`` and ``object_id`` respectively.
338     """
339     opts = model._meta
340     # Avoid a circular import.
341     from django.contrib.contenttypes.models import ContentType
342     # if there is no field called `ct_field` let the exception propagate
343     ct_field = opts.get_field(ct_field)
344     if not isinstance(ct_field, models.ForeignKey) or ct_field.rel.to != ContentType:
345         raise Exception("fk_name '%s' is not a ForeignKey to ContentType" % ct_field)
346     fk_field = opts.get_field(fk_field) # let the exception propagate
347     if exclude is not None:
348         exclude.extend([ct_field.name, fk_field.name])
349     else:
350         exclude = [ct_field.name, fk_field.name]
351     FormSet = modelformset_factory(model, form=form,
352                                    formfield_callback=formfield_callback,
353                                    formset=formset,
354                                    extra=extra, can_delete=can_delete, can_order=can_order,
355                                    fields=fields, exclude=exclude, max_num=max_num)
356     FormSet.ct_field = ct_field
357     FormSet.ct_fk_field = fk_field
358     return FormSet
359
360 class GenericInlineModelAdmin(InlineModelAdmin):
361     ct_field = "content_type"
362     ct_fk_field = "object_id"
363     formset = BaseGenericInlineFormSet
364
365     def get_formset(self, request, obj=None):
366         if self.declared_fieldsets:
367             fields = flatten_fieldsets(self.declared_fieldsets)
368         else:
369             fields = None
370         defaults = {
371             "ct_field": self.ct_field,
372             "fk_field": self.ct_fk_field,
373             "form": self.form,
374             "formfield_callback": self.formfield_for_dbfield,
375             "formset": self.formset,
376             "extra": self.extra,
377             "can_delete": True,
378             "can_order": False,
379             "fields": fields,
380         }
381         return generic_inlineformset_factory(self.model, **defaults)
382
383 class GenericStackedInline(GenericInlineModelAdmin):
384     template = 'admin/edit_inline/stacked.html'
385
386 class GenericTabularInline(GenericInlineModelAdmin):
387     template = 'admin/edit_inline/tabular.html'
Note: See TracBrowser for help on using the browser.