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'