app/django/contrib/contenttypes/generic.py
changeset 54 03e267d67478
child 323 ff1a9aa48cfd
equal deleted inserted replaced
53:57b4279d8c4e 54:03e267d67478
       
     1 """
       
     2 Classes allowing "generic" relations through ContentType and object-id fields.
       
     3 """
       
     4 
       
     5 from django import oldforms
       
     6 from django.core.exceptions import ObjectDoesNotExist
       
     7 from django.db import connection
       
     8 from django.db.models import signals
       
     9 from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
       
    10 from django.db.models.loading import get_model
       
    11 from django.dispatch import dispatcher
       
    12 from django.utils.functional import curry
       
    13 
       
    14 class GenericForeignKey(object):
       
    15     """
       
    16     Provides a generic relation to any object through content-type/object-id
       
    17     fields.
       
    18     """
       
    19 
       
    20     def __init__(self, ct_field="content_type", fk_field="object_id"):
       
    21         self.ct_field = ct_field
       
    22         self.fk_field = fk_field
       
    23 
       
    24     def contribute_to_class(self, cls, name):
       
    25         # Make sure the fields exist (these raise FieldDoesNotExist,
       
    26         # which is a fine error to raise here)
       
    27         self.name = name
       
    28         self.model = cls
       
    29         self.cache_attr = "_%s_cache" % name
       
    30 
       
    31         # For some reason I don't totally understand, using weakrefs here doesn't work.
       
    32         dispatcher.connect(self.instance_pre_init, signal=signals.pre_init, sender=cls, weak=False)
       
    33 
       
    34         # Connect myself as the descriptor for this field
       
    35         setattr(cls, name, self)
       
    36 
       
    37     def instance_pre_init(self, signal, sender, args, kwargs):
       
    38         """
       
    39         Handles initializing an object with the generic FK instaed of
       
    40         content-type/object-id fields.
       
    41         """
       
    42         if self.name in kwargs:
       
    43             value = kwargs.pop(self.name)
       
    44             kwargs[self.ct_field] = self.get_content_type(obj=value)
       
    45             kwargs[self.fk_field] = value._get_pk_val()
       
    46 
       
    47     def get_content_type(self, obj=None, id=None):
       
    48         # Convenience function using get_model avoids a circular import when
       
    49         # using this model
       
    50         ContentType = get_model("contenttypes", "contenttype")
       
    51         if obj:
       
    52             return ContentType.objects.get_for_model(obj)
       
    53         elif id:
       
    54             return ContentType.objects.get_for_id(id)
       
    55         else:
       
    56             # This should never happen. I love comments like this, don't you?
       
    57             raise Exception("Impossible arguments to GFK.get_content_type!")
       
    58 
       
    59     def __get__(self, instance, instance_type=None):
       
    60         if instance is None:
       
    61             raise AttributeError, u"%s must be accessed via instance" % self.name
       
    62 
       
    63         try:
       
    64             return getattr(instance, self.cache_attr)
       
    65         except AttributeError:
       
    66             rel_obj = None
       
    67 
       
    68             # Make sure to use ContentType.objects.get_for_id() to ensure that
       
    69             # lookups are cached (see ticket #5570). This takes more code than
       
    70             # the naive ``getattr(instance, self.ct_field)``, but has better
       
    71             # performance when dealing with GFKs in loops and such.
       
    72             f = self.model._meta.get_field(self.ct_field)
       
    73             ct_id = getattr(instance, f.get_attname(), None)
       
    74             if ct_id:
       
    75                 ct = self.get_content_type(id=ct_id)
       
    76                 try:
       
    77                     rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field))
       
    78                 except ObjectDoesNotExist:
       
    79                     pass
       
    80             setattr(instance, self.cache_attr, rel_obj)
       
    81             return rel_obj
       
    82 
       
    83     def __set__(self, instance, value):
       
    84         if instance is None:
       
    85             raise AttributeError, u"%s must be accessed via instance" % self.related.opts.object_name
       
    86 
       
    87         ct = None
       
    88         fk = None
       
    89         if value is not None:
       
    90             ct = self.get_content_type(obj=value)
       
    91             fk = value._get_pk_val()
       
    92 
       
    93         setattr(instance, self.ct_field, ct)
       
    94         setattr(instance, self.fk_field, fk)
       
    95         setattr(instance, self.cache_attr, value)
       
    96 
       
    97 class GenericRelation(RelatedField, Field):
       
    98     """Provides an accessor to generic related objects (i.e. comments)"""
       
    99 
       
   100     def __init__(self, to, **kwargs):
       
   101         kwargs['verbose_name'] = kwargs.get('verbose_name', None)
       
   102         kwargs['rel'] = GenericRel(to,
       
   103                             related_name=kwargs.pop('related_name', None),
       
   104                             limit_choices_to=kwargs.pop('limit_choices_to', None),
       
   105                             symmetrical=kwargs.pop('symmetrical', True))
       
   106 
       
   107         # Override content-type/object-id field names on the related class
       
   108         self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
       
   109         self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
       
   110 
       
   111         kwargs['blank'] = True
       
   112         kwargs['editable'] = False
       
   113         kwargs['serialize'] = False
       
   114         Field.__init__(self, **kwargs)
       
   115 
       
   116     def get_manipulator_field_objs(self):
       
   117         choices = self.get_choices_default()
       
   118         return [curry(oldforms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)]
       
   119 
       
   120     def get_choices_default(self):
       
   121         return Field.get_choices(self, include_blank=False)
       
   122 
       
   123     def flatten_data(self, follow, obj = None):
       
   124         new_data = {}
       
   125         if obj:
       
   126             instance_ids = [instance._get_pk_val() for instance in getattr(obj, self.name).all()]
       
   127             new_data[self.name] = instance_ids
       
   128         return new_data
       
   129 
       
   130     def m2m_db_table(self):
       
   131         return self.rel.to._meta.db_table
       
   132 
       
   133     def m2m_column_name(self):
       
   134         return self.object_id_field_name
       
   135 
       
   136     def m2m_reverse_name(self):
       
   137         return self.model._meta.pk.column
       
   138 
       
   139     def contribute_to_class(self, cls, name):
       
   140         super(GenericRelation, self).contribute_to_class(cls, name)
       
   141 
       
   142         # Save a reference to which model this class is on for future use
       
   143         self.model = cls
       
   144 
       
   145         # Add the descriptor for the m2m relation
       
   146         setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self))
       
   147 
       
   148     def contribute_to_related_class(self, cls, related):
       
   149         pass
       
   150 
       
   151     def set_attributes_from_rel(self):
       
   152         pass
       
   153 
       
   154     def get_internal_type(self):
       
   155         return "ManyToManyField"
       
   156 
       
   157     def db_type(self):
       
   158         # Since we're simulating a ManyToManyField, in effect, best return the
       
   159         # same db_type as well.
       
   160         return None
       
   161 
       
   162 class ReverseGenericRelatedObjectsDescriptor(object):
       
   163     """
       
   164     This class provides the functionality that makes the related-object
       
   165     managers available as attributes on a model class, for fields that have
       
   166     multiple "remote" values and have a GenericRelation defined in their model
       
   167     (rather than having another model pointed *at* them). In the example
       
   168     "article.publications", the publications attribute is a
       
   169     ReverseGenericRelatedObjectsDescriptor instance.
       
   170     """
       
   171     def __init__(self, field):
       
   172         self.field = field
       
   173 
       
   174     def __get__(self, instance, instance_type=None):
       
   175         if instance is None:
       
   176             raise AttributeError, "Manager must be accessed via instance"
       
   177 
       
   178         # This import is done here to avoid circular import importing this module
       
   179         from django.contrib.contenttypes.models import ContentType
       
   180 
       
   181         # Dynamically create a class that subclasses the related model's
       
   182         # default manager.
       
   183         rel_model = self.field.rel.to
       
   184         superclass = rel_model._default_manager.__class__
       
   185         RelatedManager = create_generic_related_manager(superclass)
       
   186 
       
   187         qn = connection.ops.quote_name
       
   188 
       
   189         manager = RelatedManager(
       
   190             model = rel_model,
       
   191             instance = instance,
       
   192             symmetrical = (self.field.rel.symmetrical and instance.__class__ == rel_model),
       
   193             join_table = qn(self.field.m2m_db_table()),
       
   194             source_col_name = qn(self.field.m2m_column_name()),
       
   195             target_col_name = qn(self.field.m2m_reverse_name()),
       
   196             content_type = ContentType.objects.get_for_model(self.field.model),
       
   197             content_type_field_name = self.field.content_type_field_name,
       
   198             object_id_field_name = self.field.object_id_field_name
       
   199         )
       
   200 
       
   201         return manager
       
   202 
       
   203     def __set__(self, instance, value):
       
   204         if instance is None:
       
   205             raise AttributeError, "Manager must be accessed via instance"
       
   206 
       
   207         manager = self.__get__(instance)
       
   208         manager.clear()
       
   209         for obj in value:
       
   210             manager.add(obj)
       
   211 
       
   212 def create_generic_related_manager(superclass):
       
   213     """
       
   214     Factory function for a manager that subclasses 'superclass' (which is a
       
   215     Manager) and adds behavior for generic related objects.
       
   216     """
       
   217 
       
   218     class GenericRelatedObjectManager(superclass):
       
   219         def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
       
   220                      join_table=None, source_col_name=None, target_col_name=None, content_type=None,
       
   221                      content_type_field_name=None, object_id_field_name=None):
       
   222 
       
   223             super(GenericRelatedObjectManager, self).__init__()
       
   224             self.core_filters = core_filters or {}
       
   225             self.model = model
       
   226             self.content_type = content_type
       
   227             self.symmetrical = symmetrical
       
   228             self.instance = instance
       
   229             self.join_table = join_table
       
   230             self.join_table = model._meta.db_table
       
   231             self.source_col_name = source_col_name
       
   232             self.target_col_name = target_col_name
       
   233             self.content_type_field_name = content_type_field_name
       
   234             self.object_id_field_name = object_id_field_name
       
   235             self.pk_val = self.instance._get_pk_val()
       
   236 
       
   237         def get_query_set(self):
       
   238             query = {
       
   239                 '%s__pk' % self.content_type_field_name : self.content_type.id,
       
   240                 '%s__exact' % self.object_id_field_name : self.pk_val,
       
   241             }
       
   242             return superclass.get_query_set(self).filter(**query)
       
   243 
       
   244         def add(self, *objs):
       
   245             for obj in objs:
       
   246                 setattr(obj, self.content_type_field_name, self.content_type)
       
   247                 setattr(obj, self.object_id_field_name, self.pk_val)
       
   248                 obj.save()
       
   249         add.alters_data = True
       
   250 
       
   251         def remove(self, *objs):
       
   252             for obj in objs:
       
   253                 obj.delete()
       
   254         remove.alters_data = True
       
   255 
       
   256         def clear(self):
       
   257             for obj in self.all():
       
   258                 obj.delete()
       
   259         clear.alters_data = True
       
   260 
       
   261         def create(self, **kwargs):
       
   262             kwargs[self.content_type_field_name] = self.content_type
       
   263             kwargs[self.object_id_field_name] = self.pk_val
       
   264             obj = self.model(**kwargs)
       
   265             obj.save()
       
   266             return obj
       
   267         create.alters_data = True
       
   268 
       
   269     return GenericRelatedObjectManager
       
   270 
       
   271 class GenericRel(ManyToManyRel):
       
   272     def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True):
       
   273         self.to = to
       
   274         self.num_in_admin = 0
       
   275         self.related_name = related_name
       
   276         self.filter_interface = None
       
   277         self.limit_choices_to = limit_choices_to or {}
       
   278         self.edit_inline = False
       
   279         self.raw_id_admin = False
       
   280         self.symmetrical = symmetrical
       
   281         self.multiple = True
       
   282         assert not (self.raw_id_admin and self.filter_interface), \
       
   283             "Generic relations may not use both raw_id_admin and filter_interface"