Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Added generic foreign key support to Django. Much thanks to Ian Holsm…

…an and

Luke Plant -- most of this code is theirs.  Documentation is to follow; for now
see the example/unit test.  Fixes #529.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@3134 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit bca5327b21eb2e3ee18292cbe532d6d0071201d8 1 parent 174e334
Jacob Kaplan-Moss authored June 16, 2006
51  django/core/management.py
@@ -211,35 +211,38 @@ def _get_sql_for_pending_references(klass, pending_references):
211 211
 
212 212
 def _get_many_to_many_sql_for_model(klass):
213 213
     from django.db import backend, get_creation_module
  214
+    from django.db.models import GenericRel
  215
+    
214 216
     data_types = get_creation_module().DATA_TYPES
215 217
 
216 218
     opts = klass._meta
217 219
     final_output = []
218 220
     for f in opts.many_to_many:
219  
-        table_output = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + \
220  
-            style.SQL_TABLE(backend.quote_name(f.m2m_db_table())) + ' (']
221  
-        table_output.append('    %s %s %s,' % \
222  
-            (style.SQL_FIELD(backend.quote_name('id')),
223  
-            style.SQL_COLTYPE(data_types['AutoField']),
224  
-            style.SQL_KEYWORD('NOT NULL PRIMARY KEY')))
225  
-        table_output.append('    %s %s %s %s (%s),' % \
226  
-            (style.SQL_FIELD(backend.quote_name(f.m2m_column_name())),
227  
-            style.SQL_COLTYPE(data_types[get_rel_data_type(opts.pk)] % opts.pk.__dict__),
228  
-            style.SQL_KEYWORD('NOT NULL REFERENCES'),
229  
-            style.SQL_TABLE(backend.quote_name(opts.db_table)),
230  
-            style.SQL_FIELD(backend.quote_name(opts.pk.column))))
231  
-        table_output.append('    %s %s %s %s (%s),' % \
232  
-            (style.SQL_FIELD(backend.quote_name(f.m2m_reverse_name())),
233  
-            style.SQL_COLTYPE(data_types[get_rel_data_type(f.rel.to._meta.pk)] % f.rel.to._meta.pk.__dict__),
234  
-            style.SQL_KEYWORD('NOT NULL REFERENCES'),
235  
-            style.SQL_TABLE(backend.quote_name(f.rel.to._meta.db_table)),
236  
-            style.SQL_FIELD(backend.quote_name(f.rel.to._meta.pk.column))))
237  
-        table_output.append('    %s (%s, %s)' % \
238  
-            (style.SQL_KEYWORD('UNIQUE'),
239  
-            style.SQL_FIELD(backend.quote_name(f.m2m_column_name())),
240  
-            style.SQL_FIELD(backend.quote_name(f.m2m_reverse_name()))))
241  
-        table_output.append(');')
242  
-        final_output.append('\n'.join(table_output))
  221
+        if not isinstance(f.rel, GenericRel):
  222
+            table_output = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + \
  223
+                style.SQL_TABLE(backend.quote_name(f.m2m_db_table())) + ' (']
  224
+            table_output.append('    %s %s %s,' % \
  225
+                (style.SQL_FIELD(backend.quote_name('id')),
  226
+                style.SQL_COLTYPE(data_types['AutoField']),
  227
+                style.SQL_KEYWORD('NOT NULL PRIMARY KEY')))
  228
+            table_output.append('    %s %s %s %s (%s),' % \
  229
+                (style.SQL_FIELD(backend.quote_name(f.m2m_column_name())),
  230
+                style.SQL_COLTYPE(data_types[get_rel_data_type(opts.pk)] % opts.pk.__dict__),
  231
+                style.SQL_KEYWORD('NOT NULL REFERENCES'),
  232
+                style.SQL_TABLE(backend.quote_name(opts.db_table)),
  233
+                style.SQL_FIELD(backend.quote_name(opts.pk.column))))
  234
+            table_output.append('    %s %s %s %s (%s),' % \
  235
+                (style.SQL_FIELD(backend.quote_name(f.m2m_reverse_name())),
  236
+                style.SQL_COLTYPE(data_types[get_rel_data_type(f.rel.to._meta.pk)] % f.rel.to._meta.pk.__dict__),
  237
+                style.SQL_KEYWORD('NOT NULL REFERENCES'),
  238
+                style.SQL_TABLE(backend.quote_name(f.rel.to._meta.db_table)),
  239
+                style.SQL_FIELD(backend.quote_name(f.rel.to._meta.pk.column))))
  240
+            table_output.append('    %s (%s, %s)' % \
  241
+                (style.SQL_KEYWORD('UNIQUE'),
  242
+                style.SQL_FIELD(backend.quote_name(f.m2m_column_name())),
  243
+                style.SQL_FIELD(backend.quote_name(f.m2m_reverse_name()))))
  244
+            table_output.append(');')
  245
+            final_output.append('\n'.join(table_output))
243 246
     return final_output
244 247
 
245 248
 def get_sql_delete(app):
1  django/db/models/__init__.py
@@ -8,6 +8,7 @@
8 8
 from django.db.models.base import Model, AdminOptions
9 9
 from django.db.models.fields import *
10 10
 from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel, TABULAR, STACKED
  11
+from django.db.models.fields.generic import GenericRelation, GenericRel, GenericForeignKey
11 12
 from django.db.models import signals
12 13
 from django.utils.functional import curry
13 14
 from django.utils.text import capfirst
259  django/db/models/fields/generic.py
... ...
@@ -0,0 +1,259 @@
  1
+"""
  2
+Classes allowing "generic" relations through ContentType and object-id fields.
  3
+"""
  4
+
  5
+from django import forms
  6
+from django.core.exceptions import ObjectDoesNotExist
  7
+from django.db import backend
  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
+        # Handle initalizing an object with the generic FK instaed of 
  39
+        # content-type/object-id fields.        
  40
+        if kwargs.has_key(self.name):
  41
+            value = kwargs.pop(self.name)
  42
+            kwargs[self.ct_field] = self.get_content_type(value)
  43
+            kwargs[self.fk_field] = value._get_pk_val()
  44
+            
  45
+    def get_content_type(self, obj):
  46
+        # Convenience function using get_model avoids a circular import when using this model
  47
+        ContentType = get_model("contenttypes", "contenttype")
  48
+        return ContentType.objects.get_for_model(obj)
  49
+        
  50
+    def __get__(self, instance, instance_type=None):
  51
+        if instance is None:
  52
+            raise AttributeError, "%s must be accessed via instance" % self.name
  53
+
  54
+        try:
  55
+            return getattr(instance, self.cache_attr)
  56
+        except AttributeError:
  57
+            rel_obj = None
  58
+            ct = getattr(instance, self.ct_field)
  59
+            if ct:
  60
+                try:
  61
+                    rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field))
  62
+                except ObjectDoesNotExist:
  63
+                    pass
  64
+            setattr(instance, self.cache_attr, rel_obj)
  65
+            return rel_obj
  66
+
  67
+    def __set__(self, instance, value):
  68
+        if instance is None:
  69
+            raise AttributeError, "%s must be accessed via instance" % self.related.opts.object_name
  70
+
  71
+        ct = None
  72
+        fk = None
  73
+        if value is not None:
  74
+            ct = self.get_content_type(value)
  75
+            fk = value._get_pk_val()
  76
+
  77
+        setattr(instance, self.ct_field, ct)
  78
+        setattr(instance, self.fk_field, fk)
  79
+        setattr(instance, self.cache_attr, value)
  80
+    
  81
+class GenericRelation(RelatedField, Field):
  82
+    """Provides an accessor to generic related objects (i.e. comments)"""
  83
+
  84
+    def __init__(self, to, **kwargs):
  85
+        kwargs['verbose_name'] = kwargs.get('verbose_name', None)
  86
+        kwargs['rel'] = GenericRel(to, 
  87
+                            related_name=kwargs.pop('related_name', None),
  88
+                            limit_choices_to=kwargs.pop('limit_choices_to', None),
  89
+                            symmetrical=kwargs.pop('symmetrical', True))
  90
+                            
  91
+        # Override content-type/object-id field names on the related class
  92
+        self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
  93
+        self.content_type_field_name = kwargs.pop("content_type_field", "content_type")                
  94
+        
  95
+        kwargs['blank'] = True
  96
+        kwargs['editable'] = False
  97
+        Field.__init__(self, **kwargs)
  98
+
  99
+    def get_manipulator_field_objs(self):
  100
+        choices = self.get_choices_default()
  101
+        return [curry(forms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)]
  102
+
  103
+    def get_choices_default(self):
  104
+        return Field.get_choices(self, include_blank=False)
  105
+
  106
+    def flatten_data(self, follow, obj = None):
  107
+        new_data = {}
  108
+        if obj:
  109
+            instance_ids = [instance._get_pk_val() for instance in getattr(obj, self.name).all()]
  110
+            new_data[self.name] = instance_ids
  111
+        return new_data
  112
+
  113
+    def m2m_db_table(self):
  114
+        return self.rel.to._meta.db_table
  115
+
  116
+    def m2m_column_name(self):
  117
+        return self.object_id_field_name
  118
+        
  119
+    def m2m_reverse_name(self):
  120
+        return self.model._meta.pk.attname
  121
+
  122
+    def contribute_to_class(self, cls, name):
  123
+        super(GenericRelation, self).contribute_to_class(cls, name)
  124
+
  125
+        # Save a reference to which model this class is on for future use
  126
+        self.model = cls
  127
+
  128
+        # Add the descriptor for the m2m relation
  129
+        setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self))
  130
+
  131
+    def contribute_to_related_class(self, cls, related):
  132
+        pass
  133
+        
  134
+    def set_attributes_from_rel(self):
  135
+        pass
  136
+
  137
+    def get_internal_type(self):
  138
+        return "ManyToManyField"
  139
+        
  140
+class ReverseGenericRelatedObjectsDescriptor(object):
  141
+    """
  142
+    This class provides the functionality that makes the related-object
  143
+    managers available as attributes on a model class, for fields that have
  144
+    multiple "remote" values and have a GenericRelation defined in their model
  145
+    (rather than having another model pointed *at* them). In the example
  146
+    "article.publications", the publications attribute is a
  147
+    ReverseGenericRelatedObjectsDescriptor instance.
  148
+    """
  149
+    def __init__(self, field):
  150
+        self.field = field
  151
+
  152
+    def __get__(self, instance, instance_type=None):
  153
+        if instance is None:
  154
+            raise AttributeError, "Manager must be accessed via instance"
  155
+
  156
+        # This import is done here to avoid circular import importing this module
  157
+        from django.contrib.contenttypes.models import ContentType
  158
+
  159
+        # Dynamically create a class that subclasses the related model's
  160
+        # default manager.
  161
+        rel_model = self.field.rel.to
  162
+        superclass = rel_model._default_manager.__class__
  163
+        RelatedManager = create_generic_related_manager(superclass)
  164
+
  165
+        manager = RelatedManager(
  166
+            model = rel_model,
  167
+            instance = instance,
  168
+            symmetrical = (self.field.rel.symmetrical and instance.__class__ == rel_model),
  169
+            join_table = backend.quote_name(self.field.m2m_db_table()),
  170
+            source_col_name = backend.quote_name(self.field.m2m_column_name()),
  171
+            target_col_name = backend.quote_name(self.field.m2m_reverse_name()),
  172
+            content_type = ContentType.objects.get_for_model(self.field.model),
  173
+            content_type_field_name = self.field.content_type_field_name,
  174
+            object_id_field_name = self.field.object_id_field_name
  175
+        )
  176
+
  177
+        return manager
  178
+
  179
+    def __set__(self, instance, value):
  180
+        if instance is None:
  181
+            raise AttributeError, "Manager must be accessed via instance"
  182
+
  183
+        manager = self.__get__(instance)
  184
+        manager.clear()
  185
+        for obj in value:
  186
+            manager.add(obj)
  187
+
  188
+def create_generic_related_manager(superclass):
  189
+    """
  190
+    Factory function for a manager that subclasses 'superclass' (which is a
  191
+    Manager) and adds behavior for generic related objects.
  192
+    """
  193
+    
  194
+    class GenericRelatedObjectManager(superclass):
  195
+        def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
  196
+                     join_table=None, source_col_name=None, target_col_name=None, content_type=None,
  197
+                     content_type_field_name=None, object_id_field_name=None):
  198
+            
  199
+            super(GenericRelatedObjectManager, self).__init__()
  200
+            self.core_filters = core_filters or {}
  201
+            self.model = model
  202
+            self.content_type = content_type
  203
+            self.symmetrical = symmetrical
  204
+            self.instance = instance
  205
+            self.join_table = join_table
  206
+            self.join_table = model._meta.db_table
  207
+            self.source_col_name = source_col_name
  208
+            self.target_col_name = target_col_name
  209
+            self.content_type_field_name = content_type_field_name
  210
+            self.object_id_field_name = object_id_field_name
  211
+            self.pk_val = self.instance._get_pk_val()
  212
+                        
  213
+        def get_query_set(self):
  214
+            query = {
  215
+                '%s__pk' % self.content_type_field_name : self.content_type.id, 
  216
+                '%s__exact' % self.object_id_field_name : self.pk_val,
  217
+            }
  218
+            return superclass.get_query_set(self).filter(**query)
  219
+
  220
+        def add(self, *objs):
  221
+            for obj in objs:
  222
+                setattr(obj, self.content_type_field_name, self.content_type)
  223
+                setattr(obj, self.object_id_field_name, self.pk_val)
  224
+                obj.save()
  225
+        add.alters_data = True
  226
+
  227
+        def remove(self, *objs):
  228
+            for obj in objs:
  229
+                obj.delete()
  230
+        remove.alters_data = True
  231
+
  232
+        def clear(self):
  233
+            for obj in self.all():
  234
+                obj.delete()
  235
+        clear.alters_data = True
  236
+
  237
+        def create(self, **kwargs):
  238
+            kwargs[self.content_type_field_name] = self.content_type
  239
+            kwargs[self.object_id_field_name] = self.pk_val
  240
+            obj = self.model(**kwargs)
  241
+            obj.save()
  242
+            return obj
  243
+        create.alters_data = True
  244
+
  245
+    return GenericRelatedObjectManager
  246
+
  247
+class GenericRel(ManyToManyRel):
  248
+    def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True):
  249
+        self.to = to
  250
+        self.num_in_admin = 0
  251
+        self.related_name = related_name
  252
+        self.filter_interface = None
  253
+        self.limit_choices_to = limit_choices_to or {}
  254
+        self.edit_inline = False
  255
+        self.raw_id_admin = False
  256
+        self.symmetrical = symmetrical
  257
+        self.multiple = True
  258
+        assert not (self.raw_id_admin and self.filter_interface), \
  259
+            "Generic relations may not use both raw_id_admin and filter_interface"
0  tests/modeltests/generic_relations/__init__.py
No changes.
108  tests/modeltests/generic_relations/models.py
... ...
@@ -0,0 +1,108 @@
  1
+"""
  2
+33. Generic relations
  3
+
  4
+Generic relations let an object have a foreign key to any object through a
  5
+content-type/object-id field. A generic foreign key can point to any object,
  6
+be it animal, vegetable, or mineral.
  7
+
  8
+The cannonical example is tags (although this example implementation is *far*
  9
+from complete).
  10
+"""
  11
+
  12
+from django.db import models
  13
+from django.contrib.contenttypes.models import ContentType
  14
+
  15
+class TaggedItem(models.Model):
  16
+    """A tag on an item."""
  17
+    tag = models.SlugField()
  18
+    content_type = models.ForeignKey(ContentType)
  19
+    object_id = models.PositiveIntegerField()
  20
+    
  21
+    content_object = models.GenericForeignKey()
  22
+    
  23
+    class Meta:
  24
+        ordering = ["tag"]
  25
+    
  26
+    def __str__(self):
  27
+        return self.tag
  28
+
  29
+class Animal(models.Model):
  30
+    common_name = models.CharField(maxlength=150)
  31
+    latin_name = models.CharField(maxlength=150)
  32
+    
  33
+    tags = models.GenericRelation(TaggedItem)
  34
+
  35
+    def __str__(self):
  36
+        return self.common_name
  37
+        
  38
+class Vegetable(models.Model):
  39
+    name = models.CharField(maxlength=150)
  40
+    is_yucky = models.BooleanField(default=True)
  41
+    
  42
+    tags = models.GenericRelation(TaggedItem)
  43
+    
  44
+    def __str__(self):
  45
+        return self.name
  46
+    
  47
+class Mineral(models.Model):
  48
+    name = models.CharField(maxlength=150)
  49
+    hardness = models.PositiveSmallIntegerField()
  50
+    
  51
+    # note the lack of an explicit GenericRelation here...
  52
+    
  53
+    def __str__(self):
  54
+        return self.name
  55
+        
  56
+API_TESTS = """
  57
+# Create the world in 7 lines of code...
  58
+>>> lion = Animal(common_name="Lion", latin_name="Panthera leo")
  59
+>>> platypus = Animal(common_name="Platypus", latin_name="Ornithorhynchus anatinus")
  60
+>>> eggplant = Vegetable(name="Eggplant", is_yucky=True)
  61
+>>> bacon = Vegetable(name="Bacon", is_yucky=False)
  62
+>>> quartz = Mineral(name="Quartz", hardness=7)
  63
+>>> for o in (lion, platypus, eggplant, bacon, quartz):
  64
+...     o.save()
  65
+
  66
+# Objects with declared GenericRelations can be tagged directly -- the API
  67
+# mimics the many-to-many API
  68
+>>> lion.tags.create(tag="yellow")
  69
+<TaggedItem: yellow>
  70
+>>> lion.tags.create(tag="hairy")
  71
+<TaggedItem: hairy>
  72
+>>> bacon.tags.create(tag="fatty")
  73
+<TaggedItem: fatty>
  74
+>>> bacon.tags.create(tag="salty")
  75
+<TaggedItem: salty>
  76
+
  77
+>>> lion.tags.all()
  78
+[<TaggedItem: hairy>, <TaggedItem: yellow>]
  79
+>>> bacon.tags.all()
  80
+[<TaggedItem: fatty>, <TaggedItem: salty>]
  81
+
  82
+# You can easily access the content object like a foreign key
  83
+>>> t = TaggedItem.objects.get(tag="salty")
  84
+>>> t.content_object
  85
+<Vegetable: Bacon>
  86
+
  87
+# Recall that the Mineral class doesn't have an explicit GenericRelation
  88
+# defined. That's OK since you can create TaggedItems explicitally.
  89
+>>> tag1 = TaggedItem(content_object=quartz, tag="shiny")
  90
+>>> tag2 = TaggedItem(content_object=quartz, tag="clearish")
  91
+>>> tag1.save()
  92
+>>> tag2.save()
  93
+
  94
+# However, not having the convience takes a small toll when it comes
  95
+# to do lookups
  96
+>>> from django.contrib.contenttypes.models import ContentType
  97
+>>> ctype = ContentType.objects.get_for_model(quartz)
  98
+>>> TaggedItem.objects.filter(content_type__pk=ctype.id, object_id=quartz.id)
  99
+[<TaggedItem: clearish>, <TaggedItem: shiny>]
  100
+
  101
+# You can set a generic foreign key in the way you'd expect
  102
+>>> tag1.content_object = platypus
  103
+>>> tag1.save()
  104
+>>> platypus.tags.all()
  105
+[<TaggedItem: shiny>]
  106
+>>> TaggedItem.objects.filter(content_type__pk=ctype.id, object_id=quartz.id)
  107
+[<TaggedItem: clearish>]
  108
+"""

0 notes on commit bca5327

Please sign in to comment.
Something went wrong with that request. Please try again.