Source code for django.contrib.contenttypes.fields

from __future__ import unicode_literals

from collections import defaultdict

from django.core import checks
from django.core.exceptions import ObjectDoesNotExist
from django.db import connection
from django.db import models, router, transaction, DEFAULT_DB_ALIAS
from django.db.models import signals, FieldDoesNotExist, DO_NOTHING
from django.db.models.base import ModelBase
from django.db.models.fields.related import ForeignObject, ForeignObjectRel
from django.db.models.related import PathInfo
from django.db.models.sql.datastructures import Col
from django.contrib.contenttypes.models import ContentType
from django.utils import six
from django.utils.deprecation import RenameMethodsBase, RemovedInDjango18Warning
from django.utils.encoding import smart_text, python_2_unicode_compatible

class RenameGenericForeignKeyMethods(RenameMethodsBase):
    renamed_methods = (
        ('get_prefetch_query_set', 'get_prefetch_queryset', RemovedInDjango18Warning),

[docs]class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)): """ Provides a generic relation to any object through content-type/object-id fields. """ def __init__(self, ct_field="content_type", fk_field="object_id", for_concrete_model=True): self.ct_field = ct_field self.fk_field = fk_field self.for_concrete_model = for_concrete_model self.editable = False def contribute_to_class(self, cls, name): = name self.model = cls self.cache_attr = "_%s_cache" % name cls._meta.add_virtual_field(self) # Only run pre-initialization field assignment on non-abstract models if not cls._meta.abstract: signals.pre_init.connect(self.instance_pre_init, sender=cls) setattr(cls, name, self) def __str__(self): model = self.model app = model._meta.app_label return '%s.%s.%s' % (app, model._meta.object_name, def check(self, **kwargs): errors = [] errors.extend(self._check_field_name()) errors.extend(self._check_object_id_field()) errors.extend(self._check_content_type_field()) return errors def _check_field_name(self): if"_"): return [ checks.Error( 'Field names must not end with an underscore.', hint=None, obj=self, id='fields.E001', ) ] else: return [] def _check_object_id_field(self): try: self.model._meta.get_field(self.fk_field) except FieldDoesNotExist: return [ checks.Error( "The GenericForeignKey object ID references the non-existent field '%s'." % self.fk_field, hint=None, obj=self, id='contenttypes.E001', ) ] else: return [] def _check_content_type_field(self): """ Check if field named `field_name` in model `model` exists and is valid content_type field (is a ForeignKey to ContentType). """ try: field = self.model._meta.get_field(self.ct_field) except FieldDoesNotExist: return [ checks.Error( "The GenericForeignKey content type references the non-existent field '%s.%s'." % ( self.model._meta.object_name, self.ct_field ), hint=None, obj=self, id='contenttypes.E002', ) ] else: if not isinstance(field, models.ForeignKey): return [ checks.Error( "'%s.%s' is not a ForeignKey." % ( self.model._meta.object_name, self.ct_field ), hint="GenericForeignKeys must use a ForeignKey to 'contenttypes.ContentType' as the 'content_type' field.", obj=self, id='contenttypes.E003', ) ] elif != ContentType: return [ checks.Error( "'%s.%s' is not a ForeignKey to 'contenttypes.ContentType'." % ( self.model._meta.object_name, self.ct_field ), hint="GenericForeignKeys must use a ForeignKey to 'contenttypes.ContentType' as the 'content_type' field.", obj=self, id='contenttypes.E004', ) ] else: return [] def instance_pre_init(self, signal, sender, args, kwargs, **_kwargs): """ Handles initializing an object with the generic FK instead of content-type/object-id fields. """ if in kwargs: value = kwargs.pop( if value is not None: kwargs[self.ct_field] = self.get_content_type(obj=value) kwargs[self.fk_field] = value._get_pk_val() else: kwargs[self.ct_field] = None kwargs[self.fk_field] = None def get_content_type(self, obj=None, id=None, using=None): if obj is not None: return ContentType.objects.db_manager(obj._state.db).get_for_model( obj, for_concrete_model=self.for_concrete_model) elif id is not None: return ContentType.objects.db_manager(using).get_for_id(id) else: # This should never happen. I love comments like this, don't you? raise Exception("Impossible arguments to GFK.get_content_type!") def get_prefetch_queryset(self, instances, queryset=None): if queryset is not None: raise ValueError("Custom queryset can't be used for this lookup.") # For efficiency, group the instances by content type and then do one # query per model fk_dict = defaultdict(set) # We need one instance for each group in order to get the right db: instance_dict = {} ct_attname = self.model._meta.get_field(self.ct_field).get_attname() for instance in instances: # We avoid looking for values if either ct_id or fkey value is None ct_id = getattr(instance, ct_attname) if ct_id is not None: fk_val = getattr(instance, self.fk_field) if fk_val is not None: fk_dict[ct_id].add(fk_val) instance_dict[ct_id] = instance ret_val = [] for ct_id, fkeys in fk_dict.items(): instance = instance_dict[ct_id] ct = self.get_content_type(id=ct_id, using=instance._state.db) ret_val.extend(ct.get_all_objects_for_this_type(pk__in=fkeys)) # For doing the join in Python, we have to match both the FK val and the # content type, so we use a callable that returns a (fk, class) pair. def gfk_key(obj): ct_id = getattr(obj, ct_attname) if ct_id is None: return None else: model = self.get_content_type(id=ct_id, using=obj._state.db).model_class() return (, self.fk_field)), model) return (ret_val, lambda obj: (obj._get_pk_val(), obj.__class__), gfk_key, True, self.cache_attr) def is_cached(self, instance): return hasattr(instance, self.cache_attr) def __get__(self, instance, instance_type=None): if instance is None: return self try: return getattr(instance, self.cache_attr) except AttributeError: rel_obj = None # Make sure to use ContentType.objects.get_for_id() to ensure that # lookups are cached (see ticket #5570). This takes more code than # the naive ``getattr(instance, self.ct_field)``, but has better # performance when dealing with GFKs in loops and such. f = self.model._meta.get_field(self.ct_field) ct_id = getattr(instance, f.get_attname(), None) if ct_id is not None: ct = self.get_content_type(id=ct_id, using=instance._state.db) try: rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field)) except ObjectDoesNotExist: pass setattr(instance, self.cache_attr, rel_obj) return rel_obj def __set__(self, instance, value): ct = None fk = None if value is not None: ct = self.get_content_type(obj=value) fk = value._get_pk_val() setattr(instance, self.ct_field, ct) setattr(instance, self.fk_field, fk) setattr(instance, self.cache_attr, value)
[docs]class GenericRelation(ForeignObject): """Provides an accessor to generic related objects (e.g. comments)""" def __init__(self, to, **kwargs): kwargs['verbose_name'] = kwargs.get('verbose_name', None) kwargs['rel'] = GenericRel( self, to, related_query_name=kwargs.pop('related_query_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), ) # Override content-type/object-id field names on the related class self.object_id_field_name = kwargs.pop("object_id_field", "object_id") self.content_type_field_name = kwargs.pop("content_type_field", "content_type") self.for_concrete_model = kwargs.pop("for_concrete_model", True) kwargs['blank'] = True kwargs['editable'] = False kwargs['serialize'] = False # This construct is somewhat of an abuse of ForeignObject. This field # represents a relation from pk to object_id field. But, this relation # isn't direct, the join is generated reverse along foreign key. So, # the from_field is object_id field, to_field is pk because of the # reverse join. super(GenericRelation, self).__init__( to, to_fields=[], from_fields=[self.object_id_field_name], **kwargs) def check(self, **kwargs): errors = super(GenericRelation, self).check(**kwargs) errors.extend(self._check_generic_foreign_key_existence()) return errors def _check_generic_foreign_key_existence(self): target = if isinstance(target, ModelBase): # Using `vars` is very ugly approach, but there is no better one, # because GenericForeignKeys are not considered as fields and, # therefore, are not included in `target._meta.local_fields`. fields = target._meta.virtual_fields if any(isinstance(field, GenericForeignKey) and field.ct_field == self.content_type_field_name and field.fk_field == self.object_id_field_name for field in fields): return [] else: return [ checks.Error( ("The GenericRelation defines a relation with the model " "'%s.%s', but that model does not have a GenericForeignKey.") % ( target._meta.app_label, target._meta.object_name ), hint=None, obj=self, id='contenttypes.E004', ) ] else: return [] def resolve_related_fields(self): self.to_fields = [] return [([0],] def get_path_info(self): opts = target = opts.get_field_by_name(self.object_id_field_name)[0] return [PathInfo(self.model._meta, opts, (target,), self.rel, True, False)] def get_reverse_path_info(self): opts = self.model._meta from_opts = return [PathInfo(from_opts, opts, (,), self, not self.unique, False)] def get_choices_default(self): return super(GenericRelation, self).get_choices(include_blank=False) def value_to_string(self, obj): qs = getattr(obj, return smart_text([instance._get_pk_val() for instance in qs]) def contribute_to_class(self, cls, name): super(GenericRelation, self).contribute_to_class(cls, name, virtual_only=True) # Save a reference to which model this class is on for future use self.model = cls # Add the descriptor for the relation setattr(cls,, ReverseGenericRelatedObjectsDescriptor(self, self.for_concrete_model)) def set_attributes_from_rel(self): pass def get_internal_type(self): return "ManyToManyField" def get_content_type(self): """ Returns the content type associated with this field's model. """ return ContentType.objects.get_for_model(self.model, for_concrete_model=self.for_concrete_model) def get_extra_restriction(self, where_class, alias, remote_alias): field =[0] contenttype_pk = self.get_content_type().pk cond = where_class() lookup = field.get_lookup('exact')(Col(remote_alias, field, field), contenttype_pk) cond.add(lookup, 'AND') return cond def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS): """ Return all objects related to ``objs`` via this ``GenericRelation``. """ return**{ "%s__pk" % self.content_type_field_name: ContentType.objects.db_manager(using).get_for_model( self.model, for_concrete_model=self.for_concrete_model).pk, "%s__in" % self.object_id_field_name: [ for obj in objs] })
class ReverseGenericRelatedObjectsDescriptor(object): """ This class provides the functionality that makes the related-object managers available as attributes on a model class, for fields that have multiple "remote" values and have a GenericRelation defined in their model (rather than having another model pointed *at* them). In the example "article.publications", the publications attribute is a ReverseGenericRelatedObjectsDescriptor instance. """ def __init__(self, field, for_concrete_model=True): self.field = field self.for_concrete_model = for_concrete_model def __get__(self, instance, instance_type=None): if instance is None: return self # Dynamically create a class that subclasses the related model's # default manager. rel_model = superclass = rel_model._default_manager.__class__ RelatedManager = create_generic_related_manager(superclass) qn = connection.ops.quote_name content_type = ContentType.objects.db_manager(instance._state.db).get_for_model( instance, for_concrete_model=self.for_concrete_model) join_cols = self.field.get_joining_columns(reverse_join=True)[0] manager = RelatedManager( model=rel_model, instance=instance, source_col_name=qn(join_cols[0]), target_col_name=qn(join_cols[1]), content_type=content_type, content_type_field_name=self.field.content_type_field_name, object_id_field_name=self.field.object_id_field_name, prefetch_cache_name=self.field.attname, ) return manager def __set__(self, instance, value): manager = self.__get__(instance) manager.clear() for obj in value: manager.add(obj) def create_generic_related_manager(superclass): """ Factory function for a manager that subclasses 'superclass' (which is a Manager) and adds behavior for generic related objects. """ class GenericRelatedObjectManager(superclass): def __init__(self, model=None, instance=None, symmetrical=None, source_col_name=None, target_col_name=None, content_type=None, content_type_field_name=None, object_id_field_name=None, prefetch_cache_name=None): super(GenericRelatedObjectManager, self).__init__() self.model = model self.content_type = content_type self.symmetrical = symmetrical self.instance = instance self.source_col_name = source_col_name self.target_col_name = target_col_name self.content_type_field_name = content_type_field_name self.object_id_field_name = object_id_field_name self.prefetch_cache_name = prefetch_cache_name self.pk_val = self.instance._get_pk_val() self.core_filters = { '%s__pk' % content_type_field_name:, '%s' % object_id_field_name: instance._get_pk_val(), } def __call__(self, **kwargs): # We use **kwargs rather than a kwarg argument to enforce the # `manager='manager_name'` syntax. manager = getattr(self.model, kwargs.pop('manager')) manager_class = create_generic_related_manager(manager.__class__) return manager_class( model=self.model, instance=self.instance, symmetrical=self.symmetrical, source_col_name=self.source_col_name, target_col_name=self.target_col_name, content_type=self.content_type, content_type_field_name=self.content_type_field_name, object_id_field_name=self.object_id_field_name, prefetch_cache_name=self.prefetch_cache_name, ) do_not_call_in_templates = True def __str__(self): return repr(self) def get_queryset(self): try: return self.instance._prefetched_objects_cache[self.prefetch_cache_name] except (AttributeError, KeyError): db = self._db or router.db_for_read(self.model, instance=self.instance) return super(GenericRelatedObjectManager, self).get_queryset().using(db).filter(**self.core_filters) def get_prefetch_queryset(self, instances, queryset=None): if queryset is None: queryset = super(GenericRelatedObjectManager, self).get_queryset() queryset._add_hints(instance=instances[0]) queryset = queryset.using(queryset._db or self._db) query = { '%s__pk' % self.content_type_field_name:, '%s__in' % self.object_id_field_name: set(obj._get_pk_val() for obj in instances) } # We (possibly) need to convert object IDs to the type of the # instances' PK in order to match up instances: object_id_converter = instances[0] return (queryset.filter(**query), lambda relobj: object_id_converter(getattr(relobj, self.object_id_field_name)), lambda obj: obj._get_pk_val(), False, self.prefetch_cache_name) def add(self, *objs): for obj in objs: if not isinstance(obj, self.model): raise TypeError("'%s' instance expected" % self.model._meta.object_name) setattr(obj, self.content_type_field_name, self.content_type) setattr(obj, self.object_id_field_name, self.pk_val) add.alters_data = True def remove(self, *objs, **kwargs): if not objs: return bulk = kwargs.pop('bulk', True) self._clear(self.filter(pk__in=[ for o in objs]), bulk) remove.alters_data = True def clear(self, **kwargs): bulk = kwargs.pop('bulk', True) self._clear(self, bulk) clear.alters_data = True def _clear(self, queryset, bulk): db = router.db_for_write(self.model, instance=self.instance) queryset = queryset.using(db) if bulk: queryset.delete() else: with transaction.commit_on_success_unless_managed(using=db, savepoint=False): for obj in queryset: obj.delete() _clear.alters_data = True def create(self, **kwargs): kwargs[self.content_type_field_name] = self.content_type kwargs[self.object_id_field_name] = self.pk_val db = router.db_for_write(self.model, instance=self.instance) return super(GenericRelatedObjectManager, self).using(db).create(**kwargs) create.alters_data = True def get_or_create(self, **kwargs): kwargs[self.content_type_field_name] = self.content_type kwargs[self.object_id_field_name] = self.pk_val db = router.db_for_write(self.model, instance=self.instance) return super(GenericRelatedObjectManager, self).using(db).get_or_create(**kwargs) get_or_create.alters_data = True def update_or_create(self, **kwargs): kwargs[self.content_type_field_name] = self.content_type kwargs[self.object_id_field_name] = self.pk_val db = router.db_for_write(self.model, instance=self.instance) return super(GenericRelatedObjectManager, self).using(db).update_or_create(**kwargs) update_or_create.alters_data = True return GenericRelatedObjectManager class GenericRel(ForeignObjectRel): def __init__(self, field, to, related_name=None, limit_choices_to=None, related_query_name=None): super(GenericRel, self).__init__(field=field, to=to, related_name=related_query_name or '+', limit_choices_to=limit_choices_to, on_delete=DO_NOTHING, related_query_name=related_query_name)