extra(): Re-implemented. Now is polymorphic (nearly) without restrictions.

Added test cases + docs.
fix_request_path_info
Bert Constantin 2010-10-23 10:58:44 +02:00
parent 6c8d28cbbc
commit 19adbdaf2c
6 changed files with 84 additions and 49 deletions

View File

@ -262,14 +262,11 @@ About Queryset Methods
to select relations in derived models to select relations in derived models
(like ``ModelA.objects.select_related('ModelC___fieldxy')`` ) (like ``ModelA.objects.select_related('ModelC___fieldxy')`` )
* ``extra()`` by default works exactly like the original version, * ``extra()`` works as expected (returns polymorphic results) but
with the resulting queryset not being polymorphic. There is currently has one restriction: The resulting objects are required to have
experimental support for a polymorphic extra() via the keyword a unique primary key within the result set - otherwise an error is thrown
argument ``polymorphic=True`` (only the ``where`` and (this case could be made to work, however it may be mostly unneeded)..
``order_by`` and ``params`` arguments of extra() should be used then). The keyword-argument "polymorphic" is no longer supported.
The behaviour of extra() may change in the future, so it's best if you use
``base_objects=ModelA.base_objects.extra(...)`` instead if you want to
sure to get non-polymorphic behaviour.
+ ``get_real_instances(base_objects_list_or_queryset)`` allows you to turn a + ``get_real_instances(base_objects_list_or_queryset)`` allows you to turn a
queryset or list of base model objects efficiently into the real objects. queryset or list of base model objects efficiently into the real objects.
@ -488,19 +485,6 @@ Restrictions & Caveats
for the methods of the polymorphic querysets. Please see above for the methods of the polymorphic querysets. Please see above
for ``translate_polymorphic_Q_object``. for ``translate_polymorphic_Q_object``.
* Django 1.1 only - the names of polymorphic models must be unique
in the whole project, even if they are in two different apps.
This results from a restriction in the Django 1.1 "related_name"
option (fixed in Django 1.2).
+ Django 1.1 only - when ContentType is used in models, Django's
seralisation or fixtures cannot be used (all polymorphic models
use ContentType). This issue seems to be resolved for Django 1.2
(changeset 11863: Fixed #7052, Added support for natural keys in serialization).
+ http://code.djangoproject.com/ticket/7052
+ http://stackoverflow.com/questions/853796/problems-with-contenttypes-when-loading-a-fixture-in-django
* A reference (``ContentType``) to the real/leaf model is stored * A reference (``ContentType``) to the real/leaf model is stored
in the base model (the base model directly inheriting from in the base model (the base model directly inheriting from
PolymorphicModel). You need to be aware of this when using the PolymorphicModel). You need to be aware of this when using the
@ -510,6 +494,20 @@ Restrictions & Caveats
table needs to be corrected/copied too. This is of course generally table needs to be corrected/copied too. This is of course generally
the case for any models using Django's ContentType. the case for any models using Django's ContentType.
+ Django 1.1 only - the names of polymorphic models must be unique
in the whole project, even if they are in two different apps.
This results from a restriction in the Django 1.1 "related_name"
option (fixed in Django 1.2).
* Django 1.1 only - when ContentType is used in models, Django's
seralisation or fixtures cannot be used (all polymorphic models
use ContentType). This issue seems to be resolved for Django 1.2
(changeset 11863: Fixed #7052, Added support for natural keys in serialization).
+ http://code.djangoproject.com/ticket/7052
+ http://stackoverflow.com/questions/853796/problems-with-contenttypes-when-loading-a-fixture-in-django
Project Status Project Status

View File

@ -30,11 +30,10 @@ class Command(NoArgsCommand):
print Project.objects.all() print Project.objects.all()
print print
"""
ModelA.objects.all().delete() ModelA.objects.all().delete()
a=ModelA.objects.create(field1='A1') a=ModelA.objects.create(field1='A1')
b=ModelB.objects.create(field1='B1', field2='B2') b=ModelB.objects.create(field1='B1', field2='B2')
c=ModelC.objects.create(field1='C1', field2='C2', field3='C3') c=ModelC.objects.create(field1='C1', field2='C2', field3='C3')
print ModelA.objects.all() print ModelA.objects.all()
print print
"""

View File

@ -13,7 +13,7 @@ class ArtProject(Project):
class ResearchProject(Project): class ResearchProject(Project):
supervisor = models.CharField(max_length=30) supervisor = models.CharField(max_length=30)
class ModelA(ShowFieldType, PolymorphicModel): class ModelA(ShowFieldTypeAndContent, PolymorphicModel):
field1 = models.CharField(max_length=10) field1 = models.CharField(max_length=10)
class ModelB(ModelA): class ModelB(ModelA):
field2 = models.CharField(max_length=10) field2 = models.CharField(max_length=10)

View File

@ -84,23 +84,11 @@ class PolymorphicQuerySet(QuerySet):
self.polymorphic_disabled = True self.polymorphic_disabled = True
return super(PolymorphicQuerySet, self).aggregate(*args, **kwargs) return super(PolymorphicQuerySet, self).aggregate(*args, **kwargs)
def extra(self, *args, **kwargs): # Since django_polymorphic 'V1.0 beta2', extra() always returns polymorphic results.
"""since django_polymorphic 'V1.0 beta2' extra() returns polymorphic results by default. # The resulting objects are required to have a unique primary key within the result set
Currently, for polymorphic queries, only the parameters 'where','order_by', 'params' are # (otherwise an error is thrown).
supported and an error is thrown if other parameters are given. # The "polymorphic" keyword argument is not supported anymore.
#def extra(self, *args, **kwargs):
For Django V1.1, extra() is not supported anymore (however it still works and returns
non-polymorphic results as this is needed in django.db.models.base.save_base)."""
polymorphic_by_default = not ( django_VERSION[0] <= 1 and django_VERSION[1] <= 1 )
self.polymorphic_disabled = not kwargs.pop('polymorphic',polymorphic_by_default)
if not self.polymorphic_disabled:
for key in kwargs.keys():
if key not in ['where','order_by', 'params']:
assert False,("django_polymorphic: extras() does not yet support keyword argument '%s'."
+ "You may use 'base_objects.extra()' instead - please see 'extra(' and 'get_real_instances' in DOCS.rst.") % (key,)
return super(PolymorphicQuerySet, self).extra(*args, **kwargs)
def get_real_instances(self, base_result_objects): def get_real_instances(self, base_result_objects):
""" """
@ -144,6 +132,11 @@ class PolymorphicQuerySet(QuerySet):
self_model_content_type_id = ContentType.objects.get_for_model(self.model).pk self_model_content_type_id = ContentType.objects.get_for_model(self.model).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=...)
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) )
base_result_objects_by_id[base_object.pk] = base_object 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 # this object is not a derived object and already the real instance => store it right away
@ -158,6 +151,7 @@ class PolymorphicQuerySet(QuerySet):
# For each model in "idlist_per_model" request its objects (the real model) # For each model in "idlist_per_model" request its objects (the real model)
# from the db and store them in results[]. # from the db and store them in results[].
# 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.
# 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 modelclass, idlist in idlist_per_model.items():
qs = modelclass.base_objects.filter(id__in=idlist) qs = modelclass.base_objects.filter(id__in=idlist)
@ -165,9 +159,15 @@ class PolymorphicQuerySet(QuerySet):
for o in qs: for o in qs:
if self.query.aggregates: if self.query.aggregates:
for anno in self.query.aggregates.keys(): for anno_field_name in self.query.aggregates.keys():
attr = getattr(base_result_objects_by_id[o.pk], anno) attr = getattr(base_result_objects_by_id[o.pk], anno_field_name)
setattr(o, anno, attr) setattr(o, 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)
results[o.pk] = o results[o.pk] = o
# re-create correct order and return result list # re-create correct order and return result list
@ -175,10 +175,17 @@ 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 fields list annotate_names=self.query.aggregates.keys() # get annotate field list
for o in resultlist: for o in resultlist:
o.polymorphic_annotate_names=annotate_names o.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
return resultlist return resultlist
def iterator(self): def iterator(self):

View File

@ -40,16 +40,23 @@ class ShowFieldBase(object):
else: else:
out += ': "' + unicode(o) + '"' out += ': "' + unicode(o) + '"'
if hasattr(self,'polymorphic_annotate_names'): def get_dynamic_fields(field_list, title):
out += ' - Ann: ' out = ' - '+title+': '
for an in self.polymorphic_annotate_names: for an in field_list:
if an != self.polymorphic_annotate_names[0]: if an != field_list[0]:
out += ', ' out += ', '
out += an out += an
if self.polymorphic_showfield_type: if self.polymorphic_showfield_type:
out += ' (' + type(getattr(self, an)).__name__ + ')' out += ' (' + type(getattr(self, an)).__name__ + ')'
if self.polymorphic_showfield_content: if self.polymorphic_showfield_content:
out += ': "' + unicode(getattr(self, an)) + '"' out += ': "' + unicode(getattr(self, an)) + '"'
return out
if hasattr(self,'polymorphic_annotate_names'):
out+=get_dynamic_fields(self.polymorphic_annotate_names, 'Ann')
if hasattr(self,'polymorphic_extra_select_names'):
out+=get_dynamic_fields(self.polymorphic_extra_select_names, 'Extra')
return out+'>' return out+'>'

View File

@ -33,6 +33,15 @@ class Model2C(Model2B):
class Model2D(Model2C): class Model2D(Model2C):
field4 = models.CharField(max_length=10) field4 = models.CharField(max_length=10)
class ModelExtraA(ShowFieldTypeAndContent, PolymorphicModel):
field1 = models.CharField(max_length=10)
class ModelExtraB(ModelExtraA):
field2 = models.CharField(max_length=10)
class ModelExtraC(ModelExtraB):
field3 = models.CharField(max_length=10)
class ModelExtraExternal(models.Model):
topic = models.CharField(max_length=10)
class ModelShow1(ShowFieldType,PolymorphicModel): class ModelShow1(ShowFieldType,PolymorphicModel):
field1 = models.CharField(max_length=10) field1 = models.CharField(max_length=10)
m2m = models.ManyToManyField('self') m2m = models.ManyToManyField('self')
@ -360,6 +369,21 @@ __test__ = {"doctest": """
[ <Model2B: id 2, field1 (CharField), field2 (CharField)>, [ <Model2B: id 2, field1 (CharField), field2 (CharField)>,
<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)> ] <Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)> ]
>>> Model2A.objects.extra(select={"select_test": "field1 = 'A1'"}, where=["field1 = 'A1' OR field1 = 'B1'"], order_by = ['-id'] )
[ <Model2B: id 2, field1 (CharField), field2 (CharField) - Extra: select_test (int)>,
<Model2A: id 1, field1 (CharField) - Extra: select_test (int)> ]
>>> o=ModelExtraA.objects.create(field1='A1')
>>> o=ModelExtraB.objects.create(field1='B1', field2='B2')
>>> o=ModelExtraC.objects.create(field1='C1', field2='C2', field3='C3')
>>> o=ModelExtraExternal.objects.create(topic='extra1')
>>> o=ModelExtraExternal.objects.create(topic='extra2')
>>> o=ModelExtraExternal.objects.create(topic='extra3')
>>> ModelExtraA.objects.extra(tables=["polymorphic_modelextraexternal"], select={"topic":"polymorphic_modelextraexternal.topic"}, where=["polymorphic_modelextraa.id = polymorphic_modelextraexternal.id"] )
[ <ModelExtraA: id 1, field1 (CharField): "A1" - Extra: topic (unicode): "extra1">,
<ModelExtraB: id 2, field1 (CharField): "B1", field2 (CharField): "B2" - Extra: topic (unicode): "extra2">,
<ModelExtraC: id 3, field1 (CharField): "C1", field2 (CharField): "C2", field3 (CharField): "C3" - Extra: topic (unicode): "extra3"> ]
### class filtering, instance_of, not_instance_of ### class filtering, instance_of, not_instance_of