Merge branch 'proxy_models_15' - add proxy model support!
commit
7c2fb9dbfa
|
|
@ -61,3 +61,15 @@ if 'UUIDModelA' in globals():
|
|||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -47,3 +47,26 @@ if 'UUIDField' in globals():
|
|||
field2 = models.CharField(max_length=10)
|
||||
class UUIDModelC(UUIDModelB):
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Copyright:
|
|||
This code and affiliated files are (C) by Bert Constantin and individual contributors.
|
||||
Please see LICENSE and AUTHORS for more information.
|
||||
"""
|
||||
import django
|
||||
from polymorphic_model import PolymorphicModel
|
||||
from manager import PolymorphicManager
|
||||
from query import PolymorphicQuerySet
|
||||
|
|
@ -26,32 +27,33 @@ def get_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
|
||||
from django.utils.encoding import smart_unicode
|
||||
if for_concrete_model:
|
||||
model = model._meta.concrete_model
|
||||
elif model._deferred:
|
||||
model = model._meta.proxy_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]
|
||||
ct = self._get_from_cache(opts)
|
||||
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)},
|
||||
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
|
||||
|
||||
ContentTypeManager.get_for_model__original = ContentTypeManager.get_for_model
|
||||
ContentTypeManager.get_for_model = get_for_model
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
|
|||
"""
|
||||
choices = []
|
||||
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))
|
||||
return choices
|
||||
|
||||
|
|
|
|||
|
|
@ -140,6 +140,11 @@ class PolymorphicModelBase(ModelBase):
|
|||
self.validate_model_manager(manager, self.__name__, key)
|
||||
add_managers.append((base.__name__, key, manager))
|
||||
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
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class PolymorphicModel(models.Model):
|
|||
(used by PolymorphicQuerySet._get_real_instances)
|
||||
"""
|
||||
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):
|
||||
"""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
|
||||
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):
|
||||
"""Normally not needed.
|
||||
If a non-polymorphic manager (like base_objects) has been used to
|
||||
|
|
|
|||
|
|
@ -17,6 +17,22 @@ from django.db.models.query import CHUNK_SIZE # this is 100 for Dj
|
|||
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
|
||||
|
||||
|
|
@ -135,25 +151,31 @@ class PolymorphicQuerySet(QuerySet):
|
|||
# - also record the correct result order in "ordered_id_list"
|
||||
# - store objects that already have the correct class into "results"
|
||||
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:
|
||||
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=...)
|
||||
assert not base_object.pk in base_result_objects_by_id, (
|
||||
"django_polymorphic: result objects do not have unique primary keys - model " + unicode(self.model)
|
||||
)
|
||||
|
||||
if not base_object.pk in base_result_objects_by_id:
|
||||
base_result_objects_by_id[base_object.pk] = base_object
|
||||
|
||||
# this object is not a derived object and already the real instance => store it right away
|
||||
if (base_object.polymorphic_ctype_id == self_model_content_type_id):
|
||||
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 derived and its real instance needs to be retrieved
|
||||
# => store it's id into the bin for this model type
|
||||
else:
|
||||
idlist_per_model[base_object.get_real_instance_class()].append(base_object.pk)
|
||||
real_concrete_class = base_object.get_real_instance_class()
|
||||
real_concrete_class_id = base_object.get_real_concrete_instance_class_id()
|
||||
|
||||
if real_concrete_class_id == self_concrete_model_class_id:
|
||||
# Real and base classes share the same concrete ancestor,
|
||||
# upcast it and put it in the results
|
||||
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
|
||||
# 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 extra() select fields from the base objects to the real objects.
|
||||
# TODO: defer(), only(): support for these would be around here
|
||||
for modelclass, idlist in idlist_per_model.items():
|
||||
qs = modelclass.base_objects.filter(pk__in=idlist) # use pk__in instead ####
|
||||
qs.dup_select_related(self) # copy select related configuration to new qs
|
||||
for real_concrete_class, idlist in idlist_per_model.items():
|
||||
real_objects = real_concrete_class.base_objects.filter(pk__in=idlist) # use pk__in instead ####
|
||||
real_objects.dup_select_related(self) # copy select related configuration to new qs
|
||||
|
||||
for o in qs:
|
||||
o_pk = getattr(o, pk_name)
|
||||
for real_object in real_objects:
|
||||
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:
|
||||
for anno_field_name in self.query.aggregates.keys():
|
||||
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:
|
||||
for select_field_name in self.query.extra_select.keys():
|
||||
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
|
||||
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)
|
||||
if self.query.aggregates:
|
||||
annotate_names = self.query.aggregates.keys() # get annotate field list
|
||||
for o in resultlist:
|
||||
o.polymorphic_annotate_names = annotate_names
|
||||
for real_object in resultlist:
|
||||
real_object.polymorphic_annotate_names = annotate_names
|
||||
|
||||
# set polymorphic_extra_select_names in all objects (currently just used for debugging/printing)
|
||||
if self.query.extra_select:
|
||||
extra_select_names = self.query.extra_select.keys() # get extra select field list
|
||||
for o in resultlist:
|
||||
o.polymorphic_extra_select_names = extra_select_names
|
||||
for real_object in resultlist:
|
||||
real_object.polymorphic_extra_select_names = extra_select_names
|
||||
|
||||
return resultlist
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
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__():
|
||||
q = q | q_class_with_subclasses(subclass)
|
||||
return q
|
||||
|
|
|
|||
|
|
@ -196,7 +196,6 @@ class Middle(Top):
|
|||
class Bottom(Middle):
|
||||
author = models.CharField(max_length=50)
|
||||
|
||||
|
||||
class UUIDProject(ShowFieldTypeAndContent, PolymorphicModel):
|
||||
uuid_primary_key = UUIDField(primary_key = True)
|
||||
topic = models.CharField(max_length = 30)
|
||||
|
|
@ -213,6 +212,13 @@ class UUIDPlainB(UUIDPlainA):
|
|||
class UUIDPlainC(UUIDPlainB):
|
||||
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
|
||||
class ProxiedBase(ShowFieldTypeAndContent, PolymorphicModel):
|
||||
name = models.CharField(max_length=10)
|
||||
|
|
@ -686,6 +692,30 @@ class PolymorphicTests(TestCase):
|
|||
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):
|
||||
"""
|
||||
Polymorphic abilities should also work when the base model is a proxy object.
|
||||
|
|
|
|||
Loading…
Reference in New Issue