Merge branch 'proxy_models_15' - add proxy model support!

fix_request_path_info
Diederik van der Boor 2013-04-08 00:32:55 +02:00
commit 7c2fb9dbfa
9 changed files with 158 additions and 53 deletions

View File

@ -61,3 +61,15 @@ if 'UUIDModelA' in globals():
admin.site.register(UUIDModelA, UUIDModelAAdmin) admin.site.register(UUIDModelA, UUIDModelAAdmin)
class ProxyChildAdmin(PolymorphicChildModelAdmin):
base_model = ProxyBase
class ProxyAdmin(PolymorphicParentModelAdmin):
base_model = ProxyBase
child_models = (
(ProxyA, ProxyChildAdmin),
(ProxyB, ProxyChildAdmin),
)
admin.site.register(ProxyBase, ProxyAdmin)

View File

@ -47,3 +47,26 @@ if 'UUIDField' in globals():
field2 = models.CharField(max_length=10) field2 = models.CharField(max_length=10)
class UUIDModelC(UUIDModelB): class UUIDModelC(UUIDModelB):
field3 = models.CharField(max_length=10) field3 = models.CharField(max_length=10)
class ProxyBase(PolymorphicModel):
title = models.CharField(max_length=200)
def __unicode__(self):
return u"<ProxyBase[type={0}]: {1}>".format(self.polymorphic_ctype, self.title)
class Meta:
ordering = ('title',)
class ProxyA(ProxyBase):
class Meta:
proxy = True
def __unicode__(self):
return u"<ProxyA: {0}>".format(self.title)
class ProxyB(ProxyBase):
class Meta:
proxy = True
def __unicode__(self):
return u"<ProxyB: {0}>".format(self.title)

View File

@ -6,6 +6,7 @@ Copyright:
This code and affiliated files are (C) by Bert Constantin and individual contributors. This code and affiliated files are (C) by Bert Constantin and individual contributors.
Please see LICENSE and AUTHORS for more information. Please see LICENSE and AUTHORS for more information.
""" """
import django
from polymorphic_model import PolymorphicModel from polymorphic_model import PolymorphicModel
from manager import PolymorphicManager from manager import PolymorphicManager
from query import PolymorphicQuerySet from query import PolymorphicQuerySet
@ -26,32 +27,33 @@ def get_version():
return version return version
# Proxied models need to have it's own ContentType # Monkey-patch Django < 1.5 to allow ContentTypes for proxy models.
if django.VERSION[:2] < (1, 5):
from django.contrib.contenttypes.models import ContentTypeManager
from django.utils.encoding import smart_unicode
def get_for_model(self, model, for_concrete_model=True):
from django.utils.encoding import smart_unicode
from django.contrib.contenttypes.models import ContentTypeManager if for_concrete_model:
from django.utils.encoding import smart_unicode model = model._meta.concrete_model
elif model._deferred:
model = model._meta.proxy_for_model
opts = model._meta
try:
ct = self._get_from_cache(opts)
except KeyError:
ct, created = self.get_or_create(
app_label = opts.app_label,
model = opts.object_name.lower(),
defaults = {'name': smart_unicode(opts.verbose_name_raw)},
)
self._add_to_cache(self.db, ct)
return ct
ContentTypeManager.get_for_model__original = ContentTypeManager.get_for_model
ContentTypeManager.get_for_model = get_for_model
def get_for_proxied_model(self, model):
"""
Returns the ContentType object for a given model, creating the
ContentType if necessary. Lookups are cached so that subsequent lookups
for the same model don't hit the database.
"""
opts = model._meta
key = (opts.app_label, opts.object_name.lower())
try:
ct = self.__class__._cache[self.db][key]
except KeyError:
# Load or create the ContentType entry. The smart_unicode() is
# needed around opts.verbose_name_raw because name_raw might be a
# django.utils.functional.__proxy__ object.
ct, created = self.get_or_create(
app_label=opts.app_label,
model=opts.object_name.lower(),
defaults={'name': smart_unicode(opts.verbose_name_raw)},
)
self._add_to_cache(self.db, ct)
return ct
ContentTypeManager.get_for_proxied_model = get_for_proxied_model

View File

@ -140,7 +140,7 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
""" """
choices = [] choices = []
for model, _ in self.get_child_models(): for model, _ in self.get_child_models():
ct = ContentType.objects.get_for_model(model) ct = ContentType.objects.get_for_model(model, for_concrete_model=False)
choices.append((ct.id, model._meta.verbose_name)) choices.append((ct.id, model._meta.verbose_name))
return choices return choices

View File

@ -140,6 +140,11 @@ class PolymorphicModelBase(ModelBase):
self.validate_model_manager(manager, self.__name__, key) self.validate_model_manager(manager, self.__name__, key)
add_managers.append((base.__name__, key, manager)) add_managers.append((base.__name__, key, manager))
add_managers_keys.add(key) add_managers_keys.add(key)
# The ordering in the base.__dict__ may randomly change depending on which method is added.
# Make sure base_objects is on top, and 'objects' and '_default_manager' follow afterwards.
# This makes sure that the _base_manager is also assigned properly.
add_managers = sorted(add_managers, key=lambda item: item[2].creation_counter, reverse=True)
return add_managers return add_managers
@classmethod @classmethod

View File

@ -85,7 +85,7 @@ class PolymorphicModel(models.Model):
(used by PolymorphicQuerySet._get_real_instances) (used by PolymorphicQuerySet._get_real_instances)
""" """
if not self.polymorphic_ctype_id: if not self.polymorphic_ctype_id:
self.polymorphic_ctype = ContentType.objects.get_for_model(self) self.polymorphic_ctype = ContentType.objects.get_for_model(self, for_concrete_model=False)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Overridden model save function which supports the polymorphism """Overridden model save function which supports the polymorphism
@ -103,6 +103,12 @@ class PolymorphicModel(models.Model):
# so we use the following version, which uses the CopntentType manager cache # so we use the following version, which uses the CopntentType manager cache
return ContentType.objects.get_for_id(self.polymorphic_ctype_id).model_class() return ContentType.objects.get_for_id(self.polymorphic_ctype_id).model_class()
def get_real_concrete_instance_class_id(self):
return ContentType.objects.get_for_model(self.get_real_instance_class(), for_concrete_model=True).pk
def get_real_concrete_instance_class(self):
return ContentType.objects.get_for_model(self.get_real_instance_class(), for_concrete_model=True).model_class()
def get_real_instance(self): def get_real_instance(self):
"""Normally not needed. """Normally not needed.
If a non-polymorphic manager (like base_objects) has been used to If a non-polymorphic manager (like base_objects) has been used to

View File

@ -17,6 +17,22 @@ from django.db.models.query import CHUNK_SIZE # this is 100 for Dj
Polymorphic_QuerySet_objects_per_request = CHUNK_SIZE Polymorphic_QuerySet_objects_per_request = CHUNK_SIZE
def transmogrify(cls, obj):
"""
Upcast a class to a different type without asking questions.
"""
if not '__init__' in obj.__dict__:
# Just assign __class__ to a different value.
new = obj
new.__class__ = cls
else:
# Run constructor, reassign values
new = cls()
for k,v in obj.__dict__.items():
new.__dict__[k] = v
return new
################################################################################### ###################################################################################
### PolymorphicQuerySet ### PolymorphicQuerySet
@ -135,25 +151,31 @@ class PolymorphicQuerySet(QuerySet):
# - also record the correct result order in "ordered_id_list" # - also record the correct result order in "ordered_id_list"
# - store objects that already have the correct class into "results" # - store objects that already have the correct class into "results"
base_result_objects_by_id = {} base_result_objects_by_id = {}
self_model_content_type_id = ContentType.objects.get_for_model(self.model).pk self_model_class_id = ContentType.objects.get_for_model(self.model, for_concrete_model=False).pk
self_concrete_model_class_id = ContentType.objects.get_for_model(self.model, for_concrete_model=True).pk
for base_object in base_result_objects: for base_object in base_result_objects:
ordered_id_list.append(base_object.pk) ordered_id_list.append(base_object.pk)
# check if id of the result object occeres more than once - this can happen e.g. with base_objects.extra(tables=...) # check if id of the result object occeres more than once - this can happen e.g. with base_objects.extra(tables=...)
assert not base_object.pk in base_result_objects_by_id, ( if not base_object.pk in base_result_objects_by_id:
"django_polymorphic: result objects do not have unique primary keys - model " + unicode(self.model) base_result_objects_by_id[base_object.pk] = base_object
)
base_result_objects_by_id[base_object.pk] = base_object if base_object.polymorphic_ctype_id == self_model_class_id:
# Real class is exactly the same as base class, go straight to results
results[base_object.pk] = base_object
# this object is not a derived object and already the real instance => store it right away else:
if (base_object.polymorphic_ctype_id == self_model_content_type_id): real_concrete_class = base_object.get_real_instance_class()
results[base_object.pk] = base_object real_concrete_class_id = base_object.get_real_concrete_instance_class_id()
# this object is derived and its real instance needs to be retrieved if real_concrete_class_id == self_concrete_model_class_id:
# => store it's id into the bin for this model type # Real and base classes share the same concrete ancestor,
else: # upcast it and put it in the results
idlist_per_model[base_object.get_real_instance_class()].append(base_object.pk) results[base_object.pk] = transmogrify(real_concrete_class, base_object)
else:
real_concrete_class = ContentType.objects.get_for_id(real_concrete_class_id).model_class()
idlist_per_model[real_concrete_class].append(base_object.pk)
# django's automatic ".pk" field does not always work correctly for # django's automatic ".pk" field does not always work correctly for
# custom fields in derived objects (unclear yet who to put the blame on). # custom fields in derived objects (unclear yet who to put the blame on).
@ -169,24 +191,29 @@ class PolymorphicQuerySet(QuerySet):
# Then we copy the annotate fields from the base objects to the real objects. # Then we copy the annotate fields from the base objects to the real objects.
# Then we copy the extra() select fields from the base objects to the real objects. # Then we copy the extra() select fields from the base objects to the real objects.
# TODO: defer(), only(): support for these would be around here # TODO: defer(), only(): support for these would be around here
for modelclass, idlist in idlist_per_model.items(): for real_concrete_class, idlist in idlist_per_model.items():
qs = modelclass.base_objects.filter(pk__in=idlist) # use pk__in instead #### real_objects = real_concrete_class.base_objects.filter(pk__in=idlist) # use pk__in instead ####
qs.dup_select_related(self) # copy select related configuration to new qs real_objects.dup_select_related(self) # copy select related configuration to new qs
for o in qs: for real_object in real_objects:
o_pk = getattr(o, pk_name) o_pk = getattr(real_object, pk_name)
real_class = real_object.get_real_instance_class()
# If the real class is a proxy, upcast it
if real_class != real_concrete_class:
real_object = transmogrify(real_class, real_object)
if self.query.aggregates: if self.query.aggregates:
for anno_field_name in self.query.aggregates.keys(): for anno_field_name in self.query.aggregates.keys():
attr = getattr(base_result_objects_by_id[o_pk], anno_field_name) attr = getattr(base_result_objects_by_id[o_pk], anno_field_name)
setattr(o, anno_field_name, attr) setattr(real_object, anno_field_name, attr)
if self.query.extra_select: if self.query.extra_select:
for select_field_name in self.query.extra_select.keys(): for select_field_name in self.query.extra_select.keys():
attr = getattr(base_result_objects_by_id[o_pk], select_field_name) attr = getattr(base_result_objects_by_id[o_pk], select_field_name)
setattr(o, select_field_name, attr) setattr(real_object, select_field_name, attr)
results[o_pk] = o results[o_pk] = real_object
# re-create correct order and return result list # re-create correct order and return result list
resultlist = [results[ordered_id] for ordered_id in ordered_id_list if ordered_id in results] resultlist = [results[ordered_id] for ordered_id in ordered_id_list if ordered_id in results]
@ -194,14 +221,14 @@ class PolymorphicQuerySet(QuerySet):
# set polymorphic_annotate_names in all objects (currently just used for debugging/printing) # set polymorphic_annotate_names in all objects (currently just used for debugging/printing)
if self.query.aggregates: if self.query.aggregates:
annotate_names = self.query.aggregates.keys() # get annotate field list annotate_names = self.query.aggregates.keys() # get annotate field list
for o in resultlist: for real_object in resultlist:
o.polymorphic_annotate_names = annotate_names real_object.polymorphic_annotate_names = annotate_names
# set polymorphic_extra_select_names in all objects (currently just used for debugging/printing) # set polymorphic_extra_select_names in all objects (currently just used for debugging/printing)
if self.query.extra_select: if self.query.extra_select:
extra_select_names = self.query.extra_select.keys() # get extra select field list extra_select_names = self.query.extra_select.keys() # get extra select field list
for o in resultlist: for real_object in resultlist:
o.polymorphic_extra_select_names = extra_select_names real_object.polymorphic_extra_select_names = extra_select_names
return resultlist return resultlist

View File

@ -220,7 +220,7 @@ def _create_model_filter_Q(modellist, not_instance_of=False):
assert False, 'PolymorphicModel: instance_of expects a list of (polymorphic) models or a single (polymorphic) model' assert False, 'PolymorphicModel: instance_of expects a list of (polymorphic) models or a single (polymorphic) model'
def q_class_with_subclasses(model): def q_class_with_subclasses(model):
q = Q(polymorphic_ctype=ContentType.objects.get_for_model(model)) q = Q(polymorphic_ctype=ContentType.objects.get_for_model(model, for_concrete_model=False))
for subclass in model.__subclasses__(): for subclass in model.__subclasses__():
q = q | q_class_with_subclasses(subclass) q = q | q_class_with_subclasses(subclass)
return q return q

View File

@ -196,7 +196,6 @@ class Middle(Top):
class Bottom(Middle): class Bottom(Middle):
author = models.CharField(max_length=50) author = models.CharField(max_length=50)
class UUIDProject(ShowFieldTypeAndContent, PolymorphicModel): class UUIDProject(ShowFieldTypeAndContent, PolymorphicModel):
uuid_primary_key = UUIDField(primary_key = True) uuid_primary_key = UUIDField(primary_key = True)
topic = models.CharField(max_length = 30) topic = models.CharField(max_length = 30)
@ -213,6 +212,13 @@ class UUIDPlainB(UUIDPlainA):
class UUIDPlainC(UUIDPlainB): class UUIDPlainC(UUIDPlainB):
field3 = models.CharField(max_length=10) field3 = models.CharField(max_length=10)
# base -> proxy
class ProxyBase(PolymorphicModel):
some_data = models.CharField(max_length=128)
class ProxyChild(ProxyBase):
class Meta:
proxy = True
# base -> proxy -> real models # base -> proxy -> real models
class ProxiedBase(ShowFieldTypeAndContent, PolymorphicModel): class ProxiedBase(ShowFieldTypeAndContent, PolymorphicModel):
name = models.CharField(max_length=10) name = models.CharField(max_length=10)
@ -686,6 +692,30 @@ class PolymorphicTests(TestCase):
self.assertIs(type(parent.childmodel_set.my_queryset_foo()), MyManagerQuerySet) self.assertIs(type(parent.childmodel_set.my_queryset_foo()), MyManagerQuerySet)
def test_proxy_models(self):
# prepare some data
for data in ('bleep bloop', 'I am a', 'computer'):
ProxyChild.objects.create(some_data=data)
# this caches ContentType queries so they don't interfere with our query counts later
list(ProxyBase.objects.all())
# one query per concrete class
with self.assertNumQueries(1):
items = list(ProxyBase.objects.all())
self.assertIsInstance(items[0], ProxyChild)
def test_content_types_for_proxy_models(self):
"""Checks if ContentType is capable of returning proxy models."""
from django.db.models import Model
from django.contrib.contenttypes.models import ContentType
ct = ContentType.objects.get_for_model(ProxyChild, for_concrete_model=False)
self.assertEqual(ProxyChild, ct.model_class())
def test_proxy_model_inheritance(self): def test_proxy_model_inheritance(self):
""" """
Polymorphic abilities should also work when the base model is a proxy object. Polymorphic abilities should also work when the base model is a proxy object.