From baaf29895c41cfafcd130d5a8f5ab548e5e9c308 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Mon, 11 May 2009 10:10:03 +0000 Subject: [PATCH] Fixed #10953, #10955: proxies of proxies now work correctly, though I still don't quite understand why you'd want to do such a thing. Thanks, Armin Ronacher. git-svn-id: http://code.djangoproject.com/svn/django/trunk@10738 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/db/models/base.py | 5 +- django/db/models/manager.py | 2 +- django/db/models/options.py | 9 +- django/db/models/sql/query.py | 28 ++++- tests/modeltests/proxy_models/models.py | 142 +++++++++++++++++++++++- 5 files changed, 177 insertions(+), 9 deletions(-) diff --git a/django/db/models/base.py b/django/db/models/base.py index 05cd0d9ea1..3a3fbf16ac 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -116,6 +116,8 @@ class ModelBase(type): new_class._meta.local_many_to_many): raise FieldError("Proxy model '%s' contains model fields." % name) + while base._meta.proxy: + base = base._meta.proxy_for_model new_class._meta.setup_proxy(base) # Do the appropriate setup for any model parents. @@ -123,6 +125,7 @@ class ModelBase(type): if isinstance(f, OneToOneField)]) for base in parents: + original_base = base if not hasattr(base, '_meta'): # Things without _meta aren't functional models, so they're # uninteresting parents. @@ -167,7 +170,7 @@ class ModelBase(type): # Proxy models inherit the non-abstract managers from their base, # unless they have redefined any of them. if is_proxy: - new_class.copy_managers(base._meta.concrete_managers) + new_class.copy_managers(original_base._meta.concrete_managers) # Inherit virtual fields (like GenericForeignKey) from the parent # class diff --git a/django/db/models/manager.py b/django/db/models/manager.py index 57844f28fe..52612d8f64 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -57,7 +57,7 @@ class Manager(object): setattr(model, name, ManagerDescriptor(self)) if not getattr(model, '_default_manager', None) or self.creation_counter < model._default_manager.creation_counter: model._default_manager = self - if model._meta.abstract or self._inherited: + if model._meta.abstract or (self._inherited and not self.model._meta.proxy): model._meta.abstract_managers.append((self.creation_counter, name, self)) else: diff --git a/django/db/models/options.py b/django/db/models/options.py index c7ae983541..34dd2aac34 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -461,8 +461,13 @@ class Options(object): if ancestor in self.parents: return self.parents[ancestor] for parent in self.parents: - if parent._meta.get_ancestor_link(ancestor): - return self.parents[parent] + # Tries to get a link field from the immediate parent + parent_link = parent._meta.get_ancestor_link(ancestor) + if parent_link: + # In case of a proxied model, the first link + # of the chain to the ancestor is that parent + # links + return self.parents[parent] or parent_link def get_ordered_objects(self): "Returns a list of Options objects that are ordered with respect to this object." diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index bafa1e93ea..394e30ba97 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -778,7 +778,9 @@ class BaseQuery(object): qn2 = self.connection.ops.quote_name aliases = set() only_load = self.deferred_to_columns() - proxied_model = opts.proxy and opts.proxy_for_model or 0 + # Skip all proxy to the root proxied model + proxied_model = get_proxied_model(opts) + if start_alias: seen = {None: start_alias} for field, model in opts.get_fields_with_model(): @@ -1301,7 +1303,10 @@ class BaseQuery(object): opts = self.model._meta root_alias = self.tables[0] seen = {None: root_alias} - proxied_model = opts.proxy and opts.proxy_for_model or 0 + + # Skip all proxy to the root proxied model + proxied_model = get_proxied_model(opts) + for field, model in opts.get_fields_with_model(): if model not in seen: if model is proxied_model: @@ -1376,6 +1381,13 @@ class BaseQuery(object): alias = root_alias alias_chain = [] for int_model in opts.get_base_chain(model): + # Proxy model have elements in base chain + # with no parents, assign the new options + # object and skip to the next base in that + # case + if not int_opts.parents[int_model]: + int_opts = int_model._meta + continue lhs_col = int_opts.parents[int_model].column dedupe = lhs_col in opts.duplicate_targets if dedupe: @@ -1720,7 +1732,9 @@ class BaseQuery(object): raise MultiJoin(pos + 1) if model: # The field lives on a base class of the current model. - proxied_model = opts.proxy and opts.proxy_for_model or 0 + # Skip the chain of proxy to the concrete proxied model + proxied_model = get_proxied_model(opts) + for int_model in opts.get_base_chain(model): if int_model is proxied_model: opts = int_model._meta @@ -2423,3 +2437,11 @@ def add_to_dict(data, key, value): data[key].add(value) else: data[key] = set([value]) + +def get_proxied_model(opts): + int_opts = opts + proxied_model = None + while int_opts.proxy: + proxied_model = int_opts.proxy_for_model + int_opts = proxied_model._meta + return proxied_model diff --git a/tests/modeltests/proxy_models/models.py b/tests/modeltests/proxy_models/models.py index ab381129cf..baa58cba03 100644 --- a/tests/modeltests/proxy_models/models.py +++ b/tests/modeltests/proxy_models/models.py @@ -82,6 +82,87 @@ class MyPersonProxy(MyPerson): class LowerStatusPerson(MyPersonProxy): status = models.CharField(max_length=80) +class User(models.Model): + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +class UserProxy(User): + class Meta: + proxy = True + +class UserProxyProxy(UserProxy): + class Meta: + proxy = True + +# We can still use `select_related()` to include related models in our querysets. +class Country(models.Model): + name = models.CharField(max_length=50) + +class State(models.Model): + name = models.CharField(max_length=50) + country = models.ForeignKey(Country) + + def __unicode__(self): + return self.name + +class StateProxy(State): + class Meta: + proxy = True + +# Proxy models still works with filters (on related fields) +# and select_related, even when mixed with model inheritance +class BaseUser(models.Model): + name = models.CharField(max_length=255) + +class TrackerUser(BaseUser): + status = models.CharField(max_length=50) + +class ProxyTrackerUser(TrackerUser): + class Meta: + proxy = True + + +class Issue(models.Model): + summary = models.CharField(max_length=255) + assignee = models.ForeignKey(TrackerUser) + + def __unicode__(self): + return ':'.join((self.__class__.__name__,self.summary,)) + +class Bug(Issue): + version = models.CharField(max_length=50) + reporter = models.ForeignKey(BaseUser) + +class ProxyBug(Bug): + """ + Proxy of an inherited class + """ + class Meta: + proxy = True + + +class ProxyProxyBug(ProxyBug): + """ + A proxy of proxy model with related field + """ + class Meta: + proxy = True + +class Improvement(Issue): + """ + A model that has relation to a proxy model + or to a proxy of proxy model + """ + version = models.CharField(max_length=50) + reporter = models.ForeignKey(ProxyTrackerUser) + associated_bug = models.ForeignKey(ProxyProxyBug) + +class ProxyImprovement(Improvement): + class Meta: + proxy = True + __test__ = {'API_TESTS' : """ # The MyPerson model should be generating the same database queries as the # Person model (when the same manager is used in each case). @@ -119,6 +200,11 @@ False >>> LowerStatusPerson.objects.all() [] +# Correct type when querying a proxy of proxy + +>>> MyPersonProxy.objects.all() +[, , ] + # And now for some things that shouldn't work... # # All base classes must be non-abstract @@ -178,6 +264,58 @@ FieldError: Proxy model 'NoNewFields' contains model fields. >>> ctype = ContentType.objects.get_for_model >>> ctype(Person) is ctype(OtherPerson) True + +>>> MyPersonProxy.objects.all() +[, ] + +>>> u = User.objects.create(name='Bruce') +>>> User.objects.all() +[] +>>> UserProxy.objects.all() +[] +>>> UserProxyProxy.objects.all() +[] + +# We can still use `select_related()` to include related models in our querysets. +>>> country = Country.objects.create(name='Australia') +>>> state = State.objects.create(name='New South Wales', country=country) + +>>> State.objects.select_related() +[] +>>> StateProxy.objects.select_related() +[] +>>> StateProxy.objects.get(name='New South Wales') + +>>> StateProxy.objects.select_related().get(name='New South Wales') + + +>>> contributor = TrackerUser.objects.create(name='Contributor',status='contrib') +>>> someone = BaseUser.objects.create(name='Someone') +>>> _ = Bug.objects.create(summary='fix this', version='1.1beta', +... assignee=contributor, reporter=someone) +>>> pcontributor = ProxyTrackerUser.objects.create(name='OtherContributor', +... status='proxy') +>>> _ = Improvement.objects.create(summary='improve that', version='1.1beta', +... assignee=contributor, reporter=pcontributor, +... associated_bug=ProxyProxyBug.objects.all()[0]) + +# Related field filter on proxy +>>> ProxyBug.objects.get(version__icontains='beta') + + +# Select related + filter on proxy +>>> ProxyBug.objects.select_related().get(version__icontains='beta') + + +# Proxy of proxy, select_related + filter +>>> ProxyProxyBug.objects.select_related().get(version__icontains='beta') + + +# Select related + filter on a related proxy field +>>> ProxyImprovement.objects.select_related().get(reporter__name__icontains='butor') + + +# Select related + filter on a related proxy of proxy field +>>> ProxyImprovement.objects.select_related().get(associated_bug__summary__icontains='fix') + """} - -