From 19adbdaf2cc7fbc545468fc6ed8265dc75244bad Mon Sep 17 00:00:00 2001 From: Bert Constantin Date: Sat, 23 Oct 2010 10:58:44 +0200 Subject: [PATCH] extra(): Re-implemented. Now is polymorphic (nearly) without restrictions. Added test cases + docs. --- DOCS.rst | 40 +++++++++++++------------- pexp/management/commands/pcmd.py | 3 +- pexp/models.py | 2 +- polymorphic/query.py | 49 ++++++++++++++++++-------------- polymorphic/showfields.py | 15 +++++++--- polymorphic/tests.py | 24 ++++++++++++++++ 6 files changed, 84 insertions(+), 49 deletions(-) diff --git a/DOCS.rst b/DOCS.rst index e7c59b7..4c921e4 100644 --- a/DOCS.rst +++ b/DOCS.rst @@ -262,14 +262,11 @@ About Queryset Methods to select relations in derived models (like ``ModelA.objects.select_related('ModelC___fieldxy')`` ) -* ``extra()`` by default works exactly like the original version, - with the resulting queryset not being polymorphic. There is - experimental support for a polymorphic extra() via the keyword - argument ``polymorphic=True`` (only the ``where`` and - ``order_by`` and ``params`` arguments of extra() should be used then). - 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. +* ``extra()`` works as expected (returns polymorphic results) but + currently has one restriction: The resulting objects are required to have + a unique primary key within the result set - otherwise an error is thrown + (this case could be made to work, however it may be mostly unneeded).. + The keyword-argument "polymorphic" is no longer supported. + ``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. @@ -488,19 +485,6 @@ Restrictions & Caveats for the methods of the polymorphic querysets. Please see above 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 in the base model (the base model directly inheriting from 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 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 diff --git a/pexp/management/commands/pcmd.py b/pexp/management/commands/pcmd.py index 48fa4b7..92c3ece 100644 --- a/pexp/management/commands/pcmd.py +++ b/pexp/management/commands/pcmd.py @@ -30,11 +30,10 @@ class Command(NoArgsCommand): print Project.objects.all() print - """ ModelA.objects.all().delete() a=ModelA.objects.create(field1='A1') b=ModelB.objects.create(field1='B1', field2='B2') c=ModelC.objects.create(field1='C1', field2='C2', field3='C3') print ModelA.objects.all() print - """ + diff --git a/pexp/models.py b/pexp/models.py index cc9ba54..be20b81 100644 --- a/pexp/models.py +++ b/pexp/models.py @@ -13,7 +13,7 @@ class ArtProject(Project): class ResearchProject(Project): supervisor = models.CharField(max_length=30) -class ModelA(ShowFieldType, PolymorphicModel): +class ModelA(ShowFieldTypeAndContent, PolymorphicModel): field1 = models.CharField(max_length=10) class ModelB(ModelA): field2 = models.CharField(max_length=10) diff --git a/polymorphic/query.py b/polymorphic/query.py index 252c7cf..5d1e525 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -84,23 +84,11 @@ class PolymorphicQuerySet(QuerySet): self.polymorphic_disabled = True return super(PolymorphicQuerySet, self).aggregate(*args, **kwargs) - def extra(self, *args, **kwargs): - """since django_polymorphic 'V1.0 beta2' extra() returns polymorphic results by default. - Currently, for polymorphic queries, only the parameters 'where','order_by', 'params' are - supported and an error is thrown if other parameters are given. - - 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) + # Since django_polymorphic 'V1.0 beta2', extra() always returns polymorphic results. + # The resulting objects are required to have a unique primary key within the result set + # (otherwise an error is thrown). + # The "polymorphic" keyword argument is not supported anymore. + #def extra(self, *args, **kwargs): 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 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) ) + 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 @@ -158,6 +151,7 @@ class PolymorphicQuerySet(QuerySet): # For each model in "idlist_per_model" request its objects (the real model) # 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 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(id__in=idlist) @@ -165,9 +159,15 @@ class PolymorphicQuerySet(QuerySet): for o in qs: if self.query.aggregates: - for anno in self.query.aggregates.keys(): - attr = getattr(base_result_objects_by_id[o.pk], anno) - setattr(o, anno, attr) + 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) + + 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 # 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) 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: 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 def iterator(self): diff --git a/polymorphic/showfields.py b/polymorphic/showfields.py index f297c10..0979018 100644 --- a/polymorphic/showfields.py +++ b/polymorphic/showfields.py @@ -40,16 +40,23 @@ class ShowFieldBase(object): else: out += ': "' + unicode(o) + '"' - if hasattr(self,'polymorphic_annotate_names'): - out += ' - Ann: ' - for an in self.polymorphic_annotate_names: - if an != self.polymorphic_annotate_names[0]: + def get_dynamic_fields(field_list, title): + out = ' - '+title+': ' + for an in field_list: + if an != field_list[0]: out += ', ' out += an if self.polymorphic_showfield_type: out += ' (' + type(getattr(self, an)).__name__ + ')' if self.polymorphic_showfield_content: 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+'>' diff --git a/polymorphic/tests.py b/polymorphic/tests.py index a130c76..930d084 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -33,6 +33,15 @@ class Model2C(Model2B): class Model2D(Model2C): 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): field1 = models.CharField(max_length=10) m2m = models.ManyToManyField('self') @@ -360,6 +369,21 @@ __test__ = {"doctest": """ [ , ] +>>> Model2A.objects.extra(select={"select_test": "field1 = 'A1'"}, where=["field1 = 'A1' OR field1 = 'B1'"], order_by = ['-id'] ) +[ , + ] + +>>> 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"] ) +[ , + , + ] + ### class filtering, instance_of, not_instance_of