PageRenderTime 68ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/django/contrib/contenttypes/generic.py

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