Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
@jacobian jacobian authored
View
51 django/core/management.py
@@ -211,35 +211,38 @@ def _get_sql_for_pending_references(klass, pending_references):
def _get_many_to_many_sql_for_model(klass):
from django.db import backend, get_creation_module
+ from django.db.models import GenericRel
+
data_types = get_creation_module().DATA_TYPES
opts = klass._meta
final_output = []
for f in opts.many_to_many:
- table_output = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + \
- style.SQL_TABLE(backend.quote_name(f.m2m_db_table())) + ' (']
- table_output.append(' %s %s %s,' % \
- (style.SQL_FIELD(backend.quote_name('id')),
- style.SQL_COLTYPE(data_types['AutoField']),
- style.SQL_KEYWORD('NOT NULL PRIMARY KEY')))
- table_output.append(' %s %s %s %s (%s),' % \
- (style.SQL_FIELD(backend.quote_name(f.m2m_column_name())),
- style.SQL_COLTYPE(data_types[get_rel_data_type(opts.pk)] % opts.pk.__dict__),
- style.SQL_KEYWORD('NOT NULL REFERENCES'),
- style.SQL_TABLE(backend.quote_name(opts.db_table)),
- style.SQL_FIELD(backend.quote_name(opts.pk.column))))
- table_output.append(' %s %s %s %s (%s),' % \
- (style.SQL_FIELD(backend.quote_name(f.m2m_reverse_name())),
- style.SQL_COLTYPE(data_types[get_rel_data_type(f.rel.to._meta.pk)] % f.rel.to._meta.pk.__dict__),
- style.SQL_KEYWORD('NOT NULL REFERENCES'),
- style.SQL_TABLE(backend.quote_name(f.rel.to._meta.db_table)),
- style.SQL_FIELD(backend.quote_name(f.rel.to._meta.pk.column))))
- table_output.append(' %s (%s, %s)' % \
- (style.SQL_KEYWORD('UNIQUE'),
- style.SQL_FIELD(backend.quote_name(f.m2m_column_name())),
- style.SQL_FIELD(backend.quote_name(f.m2m_reverse_name()))))
- table_output.append(');')
- final_output.append('\n'.join(table_output))
+ if not isinstance(f.rel, GenericRel):
+ table_output = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + \
+ style.SQL_TABLE(backend.quote_name(f.m2m_db_table())) + ' (']
+ table_output.append(' %s %s %s,' % \
+ (style.SQL_FIELD(backend.quote_name('id')),
+ style.SQL_COLTYPE(data_types['AutoField']),
+ style.SQL_KEYWORD('NOT NULL PRIMARY KEY')))
+ table_output.append(' %s %s %s %s (%s),' % \
+ (style.SQL_FIELD(backend.quote_name(f.m2m_column_name())),
+ style.SQL_COLTYPE(data_types[get_rel_data_type(opts.pk)] % opts.pk.__dict__),
+ style.SQL_KEYWORD('NOT NULL REFERENCES'),
+ style.SQL_TABLE(backend.quote_name(opts.db_table)),
+ style.SQL_FIELD(backend.quote_name(opts.pk.column))))
+ table_output.append(' %s %s %s %s (%s),' % \
+ (style.SQL_FIELD(backend.quote_name(f.m2m_reverse_name())),
+ style.SQL_COLTYPE(data_types[get_rel_data_type(f.rel.to._meta.pk)] % f.rel.to._meta.pk.__dict__),
+ style.SQL_KEYWORD('NOT NULL REFERENCES'),
+ style.SQL_TABLE(backend.quote_name(f.rel.to._meta.db_table)),
+ style.SQL_FIELD(backend.quote_name(f.rel.to._meta.pk.column))))
+ table_output.append(' %s (%s, %s)' % \
+ (style.SQL_KEYWORD('UNIQUE'),
+ style.SQL_FIELD(backend.quote_name(f.m2m_column_name())),
+ style.SQL_FIELD(backend.quote_name(f.m2m_reverse_name()))))
+ table_output.append(');')
+ final_output.append('\n'.join(table_output))
return final_output
def get_sql_delete(app):
View
1  django/db/models/__init__.py
@@ -8,6 +8,7 @@
from django.db.models.base import Model, AdminOptions
from django.db.models.fields import *
from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel, TABULAR, STACKED
+from django.db.models.fields.generic import GenericRelation, GenericRel, GenericForeignKey
from django.db.models import signals
from django.utils.functional import curry
from django.utils.text import capfirst
View
259 django/db/models/fields/generic.py
@@ -0,0 +1,259 @@
+"""
+Classes allowing "generic" relations through ContentType and object-id fields.
+"""
+
+from django import forms
+from django.core.exceptions import ObjectDoesNotExist
+from django.db import backend
+from django.db.models import signals
+from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
+from django.db.models.loading import get_model
+from django.dispatch import dispatcher
+from django.utils.functional import curry
+
+class GenericForeignKey(object):
+ """
+ Provides a generic relation to any object through content-type/object-id
+ fields.
+ """
+
+ def __init__(self, ct_field="content_type", fk_field="object_id"):
+ self.ct_field = ct_field
+ self.fk_field = fk_field
+
+ def contribute_to_class(self, cls, name):
+ # Make sure the fields exist (these raise FieldDoesNotExist,
+ # which is a fine error to raise here)
+ self.name = name
+ self.model = cls
+ self.cache_attr = "_%s_cache" % name
+
+ # For some reason I don't totally understand, using weakrefs here doesn't work.
+ dispatcher.connect(self.instance_pre_init, signal=signals.pre_init, sender=cls, weak=False)
+
+ # Connect myself as the descriptor for this field
+ setattr(cls, name, self)
+
+ def instance_pre_init(self, signal, sender, args, kwargs):
+ # Handle initalizing an object with the generic FK instaed of
+ # content-type/object-id fields.
+ if kwargs.has_key(self.name):
+ value = kwargs.pop(self.name)
+ kwargs[self.ct_field] = self.get_content_type(value)
+ kwargs[self.fk_field] = value._get_pk_val()
+
+ def get_content_type(self, obj):
+ # Convenience function using get_model avoids a circular import when using this model
+ ContentType = get_model("contenttypes", "contenttype")
+ return ContentType.objects.get_for_model(obj)
+
+ def __get__(self, instance, instance_type=None):
+ if instance is None:
+ raise AttributeError, "%s must be accessed via instance" % self.name
+
+ try:
+ return getattr(instance, self.cache_attr)
+ except AttributeError:
+ rel_obj = None
+ ct = getattr(instance, self.ct_field)
+ if ct:
+ 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):
+ if instance is None:
+ raise AttributeError, "%s must be accessed via instance" % self.related.opts.object_name
+
+ ct = None
+ fk = None
+ if value is not None:
+ ct = self.get_content_type(value)
+ fk = value._get_pk_val()
+
+ setattr(instance, self.ct_field, ct)
+ setattr(instance, self.fk_field, fk)
+ setattr(instance, self.cache_attr, value)
+
+class GenericRelation(RelatedField, Field):
+ """Provides an accessor to generic related objects (i.e. comments)"""
+
+ def __init__(self, to, **kwargs):
+ kwargs['verbose_name'] = kwargs.get('verbose_name', None)
+ kwargs['rel'] = GenericRel(to,
+ related_name=kwargs.pop('related_name', None),
+ limit_choices_to=kwargs.pop('limit_choices_to', None),
+ symmetrical=kwargs.pop('symmetrical', True))
+
+ # 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")
+
+ kwargs['blank'] = True
+ kwargs['editable'] = False
+ Field.__init__(self, **kwargs)
+
+ def get_manipulator_field_objs(self):
+ choices = self.get_choices_default()
+ return [curry(forms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)]
+
+ def get_choices_default(self):
+ return Field.get_choices(self, include_blank=False)
+
+ def flatten_data(self, follow, obj = None):
+ new_data = {}
+ if obj:
+ instance_ids = [instance._get_pk_val() for instance in getattr(obj, self.name).all()]
+ new_data[self.name] = instance_ids
+ return new_data
+
+ def m2m_db_table(self):
+ return self.rel.to._meta.db_table
+
+ def m2m_column_name(self):
+ return self.object_id_field_name
+
+ def m2m_reverse_name(self):
+ return self.model._meta.pk.attname
+
+ def contribute_to_class(self, cls, name):
+ super(GenericRelation, self).contribute_to_class(cls, name)
+
+ # Save a reference to which model this class is on for future use
+ self.model = cls
+
+ # Add the descriptor for the m2m relation
+ setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self))
+
+ def contribute_to_related_class(self, cls, related):
+ pass
+
+ def set_attributes_from_rel(self):
+ pass
+
+ def get_internal_type(self):
+ return "ManyToManyField"
+
+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):
+ self.field = field
+
+ def __get__(self, instance, instance_type=None):
+ if instance is None:
+ raise AttributeError, "Manager must be accessed via instance"
+
+ # This import is done here to avoid circular import importing this module
+ from django.contrib.contenttypes.models import ContentType
+
+ # Dynamically create a class that subclasses the related model's
+ # default manager.
+ rel_model = self.field.rel.to
+ superclass = rel_model._default_manager.__class__
+ RelatedManager = create_generic_related_manager(superclass)
+
+ manager = RelatedManager(
+ model = rel_model,
+ instance = instance,
+ symmetrical = (self.field.rel.symmetrical and instance.__class__ == rel_model),
+ join_table = backend.quote_name(self.field.m2m_db_table()),
+ source_col_name = backend.quote_name(self.field.m2m_column_name()),
+ target_col_name = backend.quote_name(self.field.m2m_reverse_name()),
+ content_type = ContentType.objects.get_for_model(self.field.model),
+ content_type_field_name = self.field.content_type_field_name,
+ object_id_field_name = self.field.object_id_field_name
+ )
+
+ return manager
+
+ def __set__(self, instance, value):
+ if instance is None:
+ raise AttributeError, "Manager must be accessed via instance"
+
+ 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, core_filters=None, instance=None, symmetrical=None,
+ join_table=None, source_col_name=None, target_col_name=None, content_type=None,
+ content_type_field_name=None, object_id_field_name=None):
+
+ super(GenericRelatedObjectManager, self).__init__()
+ self.core_filters = core_filters or {}
+ self.model = model
+ self.content_type = content_type
+ self.symmetrical = symmetrical
+ self.instance = instance
+ self.join_table = join_table
+ self.join_table = model._meta.db_table
+ 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.pk_val = self.instance._get_pk_val()
+
+ def get_query_set(self):
+ query = {
+ '%s__pk' % self.content_type_field_name : self.content_type.id,
+ '%s__exact' % self.object_id_field_name : self.pk_val,
+ }
+ return superclass.get_query_set(self).filter(**query)
+
+ def add(self, *objs):
+ for obj in objs:
+ setattr(obj, self.content_type_field_name, self.content_type)
+ setattr(obj, self.object_id_field_name, self.pk_val)
+ obj.save()
+ add.alters_data = True
+
+ def remove(self, *objs):
+ for obj in objs:
+ obj.delete()
+ remove.alters_data = True
+
+ def clear(self):
+ for obj in self.all():
+ 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
+ obj = self.model(**kwargs)
+ obj.save()
+ return obj
+ create.alters_data = True
+
+ return GenericRelatedObjectManager
+
+class GenericRel(ManyToManyRel):
+ def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True):
+ self.to = to
+ self.num_in_admin = 0
+ self.related_name = related_name
+ self.filter_interface = None
+ self.limit_choices_to = limit_choices_to or {}
+ self.edit_inline = False
+ self.raw_id_admin = False
+ self.symmetrical = symmetrical
+ self.multiple = True
+ assert not (self.raw_id_admin and self.filter_interface), \
+ "Generic relations may not use both raw_id_admin and filter_interface"
View
0  tests/modeltests/generic_relations/__init__.py
No changes.
View
108 tests/modeltests/generic_relations/models.py
@@ -0,0 +1,108 @@
+"""
+33. Generic relations
+
+Generic relations let an object have a foreign key to any object through a
+content-type/object-id field. A generic foreign key can point to any object,
+be it animal, vegetable, or mineral.
+
+The cannonical example is tags (although this example implementation is *far*
+from complete).
+"""
+
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+
+class TaggedItem(models.Model):
+ """A tag on an item."""
+ tag = models.SlugField()
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+
+ content_object = models.GenericForeignKey()
+
+ class Meta:
+ ordering = ["tag"]
+
+ def __str__(self):
+ return self.tag
+
+class Animal(models.Model):
+ common_name = models.CharField(maxlength=150)
+ latin_name = models.CharField(maxlength=150)
+
+ tags = models.GenericRelation(TaggedItem)
+
+ def __str__(self):
+ return self.common_name
+
+class Vegetable(models.Model):
+ name = models.CharField(maxlength=150)
+ is_yucky = models.BooleanField(default=True)
+
+ tags = models.GenericRelation(TaggedItem)
+
+ def __str__(self):
+ return self.name
+
+class Mineral(models.Model):
+ name = models.CharField(maxlength=150)
+ hardness = models.PositiveSmallIntegerField()
+
+ # note the lack of an explicit GenericRelation here...
+
+ def __str__(self):
+ return self.name
+
+API_TESTS = """
+# Create the world in 7 lines of code...
+>>> lion = Animal(common_name="Lion", latin_name="Panthera leo")
+>>> platypus = Animal(common_name="Platypus", latin_name="Ornithorhynchus anatinus")
+>>> eggplant = Vegetable(name="Eggplant", is_yucky=True)
+>>> bacon = Vegetable(name="Bacon", is_yucky=False)
+>>> quartz = Mineral(name="Quartz", hardness=7)
+>>> for o in (lion, platypus, eggplant, bacon, quartz):
+... o.save()
+
+# Objects with declared GenericRelations can be tagged directly -- the API
+# mimics the many-to-many API
+>>> lion.tags.create(tag="yellow")
+<TaggedItem: yellow>
+>>> lion.tags.create(tag="hairy")
+<TaggedItem: hairy>
+>>> bacon.tags.create(tag="fatty")
+<TaggedItem: fatty>
+>>> bacon.tags.create(tag="salty")
+<TaggedItem: salty>
+
+>>> lion.tags.all()
+[<TaggedItem: hairy>, <TaggedItem: yellow>]
+>>> bacon.tags.all()
+[<TaggedItem: fatty>, <TaggedItem: salty>]
+
+# You can easily access the content object like a foreign key
+>>> t = TaggedItem.objects.get(tag="salty")
+>>> t.content_object
+<Vegetable: Bacon>
+
+# Recall that the Mineral class doesn't have an explicit GenericRelation
+# defined. That's OK since you can create TaggedItems explicitally.
+>>> tag1 = TaggedItem(content_object=quartz, tag="shiny")
+>>> tag2 = TaggedItem(content_object=quartz, tag="clearish")
+>>> tag1.save()
+>>> tag2.save()
+
+# However, not having the convience takes a small toll when it comes
+# to do lookups
+>>> from django.contrib.contenttypes.models import ContentType
+>>> ctype = ContentType.objects.get_for_model(quartz)
+>>> TaggedItem.objects.filter(content_type__pk=ctype.id, object_id=quartz.id)
+[<TaggedItem: clearish>, <TaggedItem: shiny>]
+
+# You can set a generic foreign key in the way you'd expect
+>>> tag1.content_object = platypus
+>>> tag1.save()
+>>> platypus.tags.all()
+[<TaggedItem: shiny>]
+>>> TaggedItem.objects.filter(content_type__pk=ctype.id, object_id=quartz.id)
+[<TaggedItem: clearish>]
+"""

0 comments on commit bca5327

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