From 2ff5e5e765f6d5489d2ad27af10fccaf98a9d640 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 3 Feb 2006 20:36:50 +0000 Subject: [PATCH] magic-removal: Added ReverseManyRelatedObjectsDescriptor, which implements many-to-many lookup -- e.g. poll_obj.sites rather than poll_obj.get_site_list(). Many-to-many addition/deletion still needs to be done. git-svn-id: http://code.djangoproject.com/svn/django/branches/magic-removal@2247 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/db/models/base.py | 16 --------- django/db/models/fields/related.py | 56 ++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/django/db/models/base.py b/django/db/models/base.py index 82aeaedd58..46baccb45d 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -357,22 +357,6 @@ class Model(object): setattr(self, cachename, get_image_dimensions(filename)) return getattr(self, cachename) - def _get_many_to_many_objects(self, field_with_rel): - cache_var = '_%s_cache' % field_with_rel.name - if not hasattr(self, cache_var): - rel_opts = field_with_rel.rel.to._meta - sql = "SELECT %s FROM %s a, %s b WHERE a.%s = b.%s AND b.%s = %%s %s" % \ - (','.join(['a.%s' % backend.quote_name(f.column) for f in rel_opts.fields]), - backend.quote_name(rel_opts.db_table), - backend.quote_name(field_with_rel.get_m2m_db_table(self._meta)), - backend.quote_name(rel_opts.pk.column), - backend.quote_name(rel_opts.object_name.lower() + '_id'), - backend.quote_name(self._meta.object_name.lower() + '_id'), rel_opts.get_order_sql('a')) - cursor = connection.cursor() - cursor.execute(sql, [self._get_pk_val()]) - setattr(self, cache_var, [field_with_rel.rel.to(*row) for row in cursor.fetchall()]) - return getattr(self, cache_var) - def _set_many_to_many_objects(self, id_list, field_with_rel): current_ids = [obj._get_pk_val() for obj in self._get_many_to_many_objects(field_with_rel)] ids_to_add, ids_to_delete = dict([(i, 1) for i in id_list]), [] diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 4b45cddc39..e2e9bfe6ee 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1,3 +1,4 @@ +from django.db import backend from django.db.models import signals from django.db.models.fields import AutoField, Field, IntegerField from django.db.models.related import RelatedObject @@ -126,14 +127,59 @@ class ManyRelatedObjectsDescriptor(object): # Prepare the manager. # TODO: Fix this hack? - # We're setting self.manager.model here because - # self.manager._prepare() expects that self.manager.model is - # set. This is slightly hackish. + # We're setting manager.model here because manager._prepare() expects + # that manager.model is set. This is slightly hackish. manager.model = self.related.model manager._prepare() return manager +class ReverseManyRelatedObjectsDescriptor(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 ManyToManyField defined in their + # model (rather than having another model pointed *at* them). + # In the example "poll.sites", the sites attribute is a + # ManyRelatedObjectsDescriptor instance. + def __init__(self, m2m_field): + self.field = m2m_field + self.rel_model = m2m_field.rel.to + + def __get__(self, instance, instance_type=None): + if instance is None: + raise AttributeError, "Manager must be accessed via instance" + + qn = backend.quote_name + this_opts = instance.__class__._meta + rel_opts = self.rel_model._meta + join_table = backend.quote_name(self.field.get_m2m_db_table(this_opts)) + + # Dynamically create a class that subclasses the related + # model's default manager. + superclass = self.rel_model._default_manager.__class__ + class_ = types.ClassType('RelatedManager', (superclass,), {}) + # Override get_query_set on the RelatedManager + def get_query_set(self): + return superclass.get_query_set(self).extra( + tables=(join_table,), + where=[ + '%s.%s = %s.%s' % (qn(rel_opts.db_table), qn(rel_opts.pk.column), join_table, rel_opts.object_name.lower() + '_id'), + '%s.%s = %%s' % (join_table, this_opts.object_name.lower() + '_id') + ], + params = [instance._get_pk_val()] + ) + class_.get_query_set = get_query_set + manager = class_() + + # Prepare the manager. + # TODO: Fix this hack? + # We're setting manager.model here because manager._prepare() expects + # that manager.model is set. This is slightly hackish. + manager.model = self.rel_model + manager._prepare() + + return manager + class ForeignKey(RelatedField, Field): empty_strings_allowed = False def __init__(self, to, to_field=None, **kwargs): @@ -336,9 +382,7 @@ class ManyToManyField(RelatedField, Field): def contribute_to_class(self, cls, name): super(ManyToManyField, self).contribute_to_class(cls, name) - # Add "get_thingie" methods for many-to-many related objects. - # EXAMPLES: Poll.get_site_list(), Story.get_byline_list() - setattr(cls, 'get_%s_list' % self.rel.singular, curry(cls._get_many_to_many_objects, field_with_rel=self)) + setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self)) # Add "set_thingie" methods for many-to-many related objects. # EXAMPLES: Poll.set_sites(), Story.set_bylines()