django.contrib.contenttypes.generic: 276 total statements, 0.0% covered

Generated: Wed 2013-03-13 10:33 CET

Source file: /media/Envs/Envs/filer-gallery/lib/python2.7/site-packages/django/contrib/contenttypes/generic.py

Stats: 0 executed, 256 missed, 20 excluded, 222 ignored

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