Fixed #9199 -- We were erroneously only prepending "www" to the domain if we
[django.git] / django / contrib / contenttypes / generic.py
blob0504592ebb415bfdb88e30723d1be6e46cb2e6d9
1 """
2 Classes allowing "generic" relations through ContentType and object-id fields.
3 """
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):
17 """
18 Provides a generic relation to any object through content-type/object-id
19 fields.
20 """
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):
27 self.name = name
28 self.model = cls
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):
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()
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!")
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
64 try:
65 return getattr(instance, self.cache_attr)
66 except AttributeError:
67 rel_obj = None
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
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
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()
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
140 self.model = cls
142 # Add the descriptor for the m2m relation
143 setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self))
145 def contribute_to_related_class(self, cls, related):
146 pass
148 def set_attributes_from_rel(self):
149 pass
151 def get_internal_type(self):
152 return "ManyToManyField"
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
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.
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)]
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):
182 self.field = field
184 def __get__(self, instance, instance_type=None):
185 if instance is 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
192 # default manager.
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(
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
211 return manager
213 def __set__(self, instance, value):
214 if instance is None:
215 raise AttributeError, "Manager must be accessed via instance"
217 manager = self.__get__(instance)
218 manager.clear()
219 for obj in value:
220 manager.add(obj)
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 {}
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()
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,
252 return superclass.get_query_set(self).filter(**query)
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
261 def remove(self, *objs):
262 for obj in objs:
263 obj.delete()
264 remove.alters_data = True
266 def clear(self):
267 for obj in self.all():
268 obj.delete()
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):
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
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,
303 prefix=self.rel_name
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
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,
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,
331 max_num=0,
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.
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
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)
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,
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'