2
Classes allowing "generic" relations through ContentType and object-id fields.
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
16
class GenericForeignKey(object):
18
Provides a generic relation to any object through content-type/object-id
22
def __init__(self, ct_field="content_type", fk_field="object_id"):
23
self.ct_field = ct_field
24
self.fk_field = fk_field
26
def contribute_to_class(self, cls, name):
29
self.cache_attr = "_%s_cache" % name
30
cls._meta.add_virtual_field(self)
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)
35
# Connect myself as the descriptor for this field
36
setattr(cls, name, self)
38
def instance_pre_init(self, signal, sender, args, kwargs, **_kwargs):
40
Handles initializing an object with the generic FK instaed of
41
content-type/object-id fields.
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()
48
def get_content_type(self, obj=None, id=None):
49
# Convenience function using get_model avoids a circular import when
51
ContentType = get_model("contenttypes", "contenttype")
53
return ContentType.objects.get_for_model(obj)
55
return ContentType.objects.get_for_id(id)
57
# This should never happen. I love comments like this, don't you?
58
raise Exception("Impossible arguments to GFK.get_content_type!")
60
def __get__(self, instance, instance_type=None):
62
raise AttributeError, u"%s must be accessed via instance" % self.name
65
return getattr(instance, self.cache_attr)
66
except AttributeError:
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)
76
ct = self.get_content_type(id=ct_id)
78
rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field))
79
except ObjectDoesNotExist:
81
setattr(instance, self.cache_attr, rel_obj)
84
def __set__(self, instance, value):
86
raise AttributeError, u"%s must be accessed via instance" % self.related.opts.object_name
91
ct = self.get_content_type(obj=value)
92
fk = value._get_pk_val()
94
setattr(instance, self.ct_field, ct)
95
setattr(instance, self.fk_field, fk)
96
setattr(instance, self.cache_attr, value)
98
class GenericRelation(RelatedField, Field):
99
"""Provides an accessor to generic related objects (e.g. comments)"""
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))
108
# By its very nature, a GenericRelation doesn't create a table.
109
self.creates_table = False
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")
115
kwargs['blank'] = True
116
kwargs['editable'] = False
117
kwargs['serialize'] = False
118
Field.__init__(self, **kwargs)
120
def get_choices_default(self):
121
return Field.get_choices(self, include_blank=False)
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])
127
def m2m_db_table(self):
128
return self.rel.to._meta.db_table
130
def m2m_column_name(self):
131
return self.object_id_field_name
133
def m2m_reverse_name(self):
134
return self.model._meta.pk.column
136
def contribute_to_class(self, cls, name):
137
super(GenericRelation, self).contribute_to_class(cls, name)
139
# Save a reference to which model this class is on for future use
142
# Add the descriptor for the m2m relation
143
setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self))
145
def contribute_to_related_class(self, cls, related):
148
def set_attributes_from_rel(self):
151
def get_internal_type(self):
152
return "ManyToManyField"
155
# Since we're simulating a ManyToManyField, in effect, best return the
156
# same db_type as well.
159
def extra_filters(self, pieces, pos, negate):
161
Return an extra filter to the queryset so that the results are filtered
162
on the appropriate content type.
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),
172
class ReverseGenericRelatedObjectsDescriptor(object):
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.
181
def __init__(self, field):
184
def __get__(self, instance, instance_type=None):
186
raise AttributeError, "Manager must be accessed via instance"
188
# This import is done here to avoid circular import importing this module
189
from django.contrib.contenttypes.models import ContentType
191
# Dynamically create a class that subclasses the related model's
193
rel_model = self.field.rel.to
194
superclass = rel_model._default_manager.__class__
195
RelatedManager = create_generic_related_manager(superclass)
197
qn = connection.ops.quote_name
199
manager = RelatedManager(
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
213
def __set__(self, instance, value):
215
raise AttributeError, "Manager must be accessed via instance"
217
manager = self.__get__(instance)
222
def create_generic_related_manager(superclass):
224
Factory function for a manager that subclasses 'superclass' (which is a
225
Manager) and adds behavior for generic related objects.
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):
233
super(GenericRelatedObjectManager, self).__init__()
234
self.core_filters = core_filters or {}
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()
247
def get_query_set(self):
249
'%s__pk' % self.content_type_field_name : self.content_type.id,
250
'%s__exact' % self.object_id_field_name : self.pk_val,
252
return superclass.get_query_set(self).filter(**query)
254
def add(self, *objs):
256
setattr(obj, self.content_type_field_name, self.content_type)
257
setattr(obj, self.object_id_field_name, self.pk_val)
259
add.alters_data = True
261
def remove(self, *objs):
264
remove.alters_data = True
267
for obj in self.all():
269
clear.alters_data = True
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
277
return GenericRelatedObjectManager
279
class GenericRel(ManyToManyRel):
280
def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True):
282
self.related_name = related_name
283
self.limit_choices_to = limit_choices_to or {}
284
self.symmetrical = symmetrical
287
class BaseGenericInlineFormSet(BaseModelFormSet):
289
A formset for generic inline objects to a parent.
291
ct_field_name = "content_type"
292
ct_fk_field_name = "object_id"
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,
301
super(BaseGenericInlineFormSet, self).__init__(
302
queryset=self.get_queryset(), data=data, files=files,
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,
316
def save_new(self, form, commit=True):
317
# Avoid a circular import.
318
from django.contrib.contenttypes.models import ContentType
320
self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk,
321
self.ct_fk_field.get_attname(): self.instance.pk,
323
new_obj = self.model(**kwargs)
324
return save_instance(form, new_obj, commit=commit)
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,
332
formfield_callback=lambda f: f.formfield()):
334
Returns an ``GenericInlineFormSet`` for the given kwargs.
336
You must provide ``ct_field`` and ``object_id`` if they different from the
337
defaults ``content_type`` and ``object_id`` respectively.
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])
350
exclude = [ct_field.name, fk_field.name]
351
FormSet = modelformset_factory(model, form=form,
352
formfield_callback=formfield_callback,
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
360
class GenericInlineModelAdmin(InlineModelAdmin):
361
ct_field = "content_type"
362
ct_fk_field = "object_id"
363
formset = BaseGenericInlineFormSet
365
def get_formset(self, request, obj=None):
366
if self.declared_fieldsets:
367
fields = flatten_fieldsets(self.declared_fieldsets)
371
"ct_field": self.ct_field,
372
"fk_field": self.ct_fk_field,
374
"formfield_callback": self.formfield_for_dbfield,
375
"formset": self.formset,
381
return generic_inlineformset_factory(self.model, **defaults)
383
class GenericStackedInline(GenericInlineModelAdmin):
384
template = 'admin/edit_inline/stacked.html'
386
class GenericTabularInline(GenericInlineModelAdmin):
387
template = 'admin/edit_inline/tabular.html'