Fixed ContentType related field accessor clash (an error emitted by model validation),

by adding related_name to the ContentType ForeignKey. Thanks to  Andrew Ingram.
This happened if a polymorphc model used a ContentType ForeignKey.
Plus minor documentation updates.
fix_request_path_info
Bert Constantin 2010-01-29 14:53:53 +01:00
parent 2055d03688
commit c7ac78e08d
4 changed files with 61 additions and 61 deletions

View File

@ -190,7 +190,7 @@ Non-Polymorphic Queries
manage.py dumpdata manage.py dumpdata
------------------ ------------------
Django's standard ``dumpdata`` requires non-polymorphic Django's standard ``dumpdata`` command requires non-polymorphic
behaviour from the querysets it uses and produces incomplete behaviour from the querysets it uses and produces incomplete
results with polymorphic models. Django_polymorphic includes results with polymorphic models. Django_polymorphic includes
a slightly modified version, named ``polymorphic_dumpdata``. a slightly modified version, named ``polymorphic_dumpdata``.
@ -224,7 +224,7 @@ the plain ``PolymorphicManager`` here.
Manager Inheritance / Propagation Manager Inheritance / Propagation
--------------------------------- ---------------------------------
Polymorphic models unconditionally propagate (or inherit) all managers from Polymorphic models unconditionally propagate/inherit all managers from
their base models, as long as these are polymorphic. This means that all their base models, as long as these are polymorphic. This means that all
managers inherited from polymorphic base models work just the same as if managers inherited from polymorphic base models work just the same as if
they were defined in the new model. they were defined in the new model.
@ -267,21 +267,21 @@ query ::
performs one SQL query to retrieve ``ModelA`` objects and one additional performs one SQL query to retrieve ``ModelA`` objects and one additional
query for each unique derived class occurring in result_objects. query for each unique derived class occurring in result_objects.
The best case for retrieving 100 objects is 1 db query if all are The best case for retrieving 100 objects is 1 db query if all are
class ``ModelA``. If 50 objects are ``ModelA`` and 50 are ModelB, then two class ``ModelA``. If 50 objects are ``ModelA`` and 50 are ``ModelB``, then
queries are executed. If result_objects contains only the base model two queries are executed. If result_objects contains only the base model
type (``ModelA``), the polymorphic models are just as efficient as plain type (``ModelA``), the polymorphic models are just as efficient as plain
Django models (in terms of executed queries). The pathological worst Django models (in terms of executed queries). The pathological worst
case is 101 db queries if result_objects contains 100 different case is 101 db queries if result_objects contains 100 different
object types (with all of them subclasses of ``ModelA``). object types (with all of them subclasses of ``ModelA``).
Performance ist relative: when Django users create their own Performance ist relative: when Django users create their own
polymorphic ad-hoc solution (without a module like ``polymorphic.py``), polymorphic ad-hoc solution (without a tool like ``django_polymorphic``),
this usually results in a variation of :: this usually results in a variation of ::
result_objects = [ o.get_real_instance() for o in BaseModel.objects.filter(...) ] result_objects = [ o.get_real_instance() for o in BaseModel.objects.filter(...) ]
which of has really bad performance. Relative to this, the which has really bad performance. Relative to this, the
performance of the current ``polymorphic.py`` is pretty good. performance of the current ``django_polymorphic`` is pretty good.
It may well be efficient enough for the majority of use cases. It may well be efficient enough for the majority of use cases.
Chunking: The implementation always requests objects in chunks of Chunking: The implementation always requests objects in chunks of
@ -380,7 +380,7 @@ Currently Unsupported Queryset Methods
Restrictions & Caveats Restrictions & Caveats
---------------------- ----------------------
+ Diamond shaped inheritance: There seems to be a general problem * Diamond shaped inheritance: There seems to be a general problem
with diamond shaped multiple model inheritance with Django models with diamond shaped multiple model inheritance with Django models
(tested with V1.1). (tested with V1.1).
An example is here: http://code.djangoproject.com/ticket/10808. An example is here: http://code.djangoproject.com/ticket/10808.
@ -388,22 +388,25 @@ Restrictions & Caveats
by subclassing it instead of modifying Django core (as we do here by subclassing it instead of modifying Django core (as we do here
with PolymorphicModel). with PolymorphicModel).
+ 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). If a model or an app is renamed, then Django's PolymorphicModel). If a model or an app is renamed, then Django's
ContentType table needs to be corrected too, if the db content ContentType table needs to be corrected too, if the db content
should stay usable after the rename. should stay usable after the rename.
+ The stability of the ``ContentType`` ids when combined with Django's * The use of ContentType together with Django's seralisation or
serialisation / fixtures has not yet been sufficiently fixtures seems to pose problems up to Django 1.1. This issue
investigated (please see issue 4 on github). 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
+ For all objects that are not instances of the base class type, but * For all objects that are not instances of the base class, but
instances of a subclass, the base class fields are currently instances of a subclass, the base class fields are currently
transferred twice from the database (an artefact of the current transferred twice from the database (an artefact of the current
implementation's simplicity). implementation's simplicity).
+ __getattribute__ hack: For base model inheritance back relation * __getattribute__ hack: For base model inheritance back relation
fields (like basemodel_ptr), as well as implicit model inheritance fields (like basemodel_ptr), as well as implicit model inheritance
forward relation fields, Django internally tries to use our forward relation fields, Django internally tries to use our
polymorphic manager/queryset in some places, which of course it polymorphic manager/queryset in some places, which of course it

View File

@ -9,11 +9,11 @@ Usage, Examples, Installation & Documentation, Links
---------------------------------------------------- ----------------------------------------------------
* Please see the `Documentation and Examples`_ (or the short `Overview`_) * Please see the `Documentation and Examples`_ (or the short `Overview`_)
* If you have comments, questions or suggestions: `Comments & Discussion`_ * News & comments: `News, Comments, Questions & Discussion`_
* The code can be found on GitHub_ and Bitbucket_, or downloaded as TGZ_ or ZIP_ * The code can be found on GitHub_ and Bitbucket_, or downloaded as TGZ_ or ZIP_
.. _Documentation and Examples: http://bserve.webhop.org/wiki/django_polymorphic/doc .. _Documentation and Examples: http://bserve.webhop.org/wiki/django_polymorphic/doc
.. _Comments & Discussion: http://django-polymorphic.blogspot.com/2010/01/messages.html .. _News, Comments, Questions & Discussion: http://groups.google.de/group/django-polymorphic/topics
.. _GitHub: http://github.com/bconstantin/django_polymorphic .. _GitHub: http://github.com/bconstantin/django_polymorphic
.. _Bitbucket: http://bitbucket.org/bconstantin/django_polymorphic .. _Bitbucket: http://bitbucket.org/bconstantin/django_polymorphic
.. _TGZ: http://github.com/bconstantin/django_polymorphic/tarball/master .. _TGZ: http://github.com/bconstantin/django_polymorphic/tarball/master
@ -28,8 +28,8 @@ It causes objects being retrieved from the database to always be returned back
with the same type/class and fields they were created and saved with. with the same type/class and fields they were created and saved with.
Example: Example:
If ``ArtProject`` and ``ResearchProject`` inherit from the model ``Project``, If the models ``ArtProject`` and ``ResearchProject`` inherit from the model ``Project``,
and we have saved one of each into the database:: and we have saved one of each of them into the database, then we can do::
>>> Project.objects.all() >>> Project.objects.all()
. .
@ -41,7 +41,8 @@ It doesn't matter how these objects are retrieved: be it through the
model's own managers/querysets, ForeignKeys, ManyToManyFields model's own managers/querysets, ForeignKeys, ManyToManyFields
or OneToOneFields. or OneToOneFields.
``django_polymorphic`` does this only for models that explicitly request this behaviour. ``django_polymorphic`` does this only for models that explicitly request
this behaviour (and implicitely for their submodels).
The resulting querysets are polymorphic, i.e they may deliver The resulting querysets are polymorphic, i.e they may deliver
objects of several different types in a single query result. objects of several different types in a single query result.
@ -54,30 +55,9 @@ It's important to consider that this code is still very new and
experimental. Please see the docs for current restrictions, caveats, experimental. Please see the docs for current restrictions, caveats,
and performance implications. and performance implications.
Right now it's suitable only for the more enterprising early adopters.
It does seem to work well for a number of people (including me), but It does seem to work well for a number of people (including me), but
it's still very early and API changes, code reorganisations or further it's still very early and API changes, code reorganisations or further
schema changes are still a possibility. schema changes are still a possibility.
Right now it's suitable only for the more enterprising early adopters.
News
----
**2010-1-29:**
Restructured django_polymorphic into a regular Django add-on
application. This is needed for the management commands, and
also seems to be a generally good idea for future enhancements
as well (and it makes sure the tests are always included).
The ``poly`` app - until now being used for test purposes only
- has been renamed to ``polymorphic``. See DOCS.rst
("installation/testing") for more info.
**2010-1-26:**
IMPORTANT - database schema change (more info in change log).
I hope I got this change in early enough before anyone started to use
polymorphic.py in earnest. Sorry for any inconvenience.
This should be the final DB schema now.

View File

@ -79,10 +79,17 @@ class PolymorphicQuerySet(QuerySet):
return self.filter(not_instance_of=args) return self.filter(not_instance_of=args)
def _filter_or_exclude(self, negate, *args, **kwargs): def _filter_or_exclude(self, negate, *args, **kwargs):
_translate_polymorphic_filter_specs_in_args(self.model, args) """ we override this internal Django functon since it is used for all filtering """
additional_args = _translate_polymorphic_filter_specs_in_kwargs(self.model, kwargs) _translate_polymorphic_filter_defnitions_in_args(self.model, args) # the Q objects
additional_args = _translate_polymorphic_filter_defnitions_in_kwargs(self.model, kwargs) # filter_field='data'
return super(PolymorphicQuerySet, self)._filter_or_exclude(negate, *(list(args) + additional_args), **kwargs) return super(PolymorphicQuerySet, self)._filter_or_exclude(negate, *(list(args) + additional_args), **kwargs)
# these queryset functions are not yet supported
def defer(self, *args, **kwargs): raise NotImplementedError
def only(self, *args, **kwargs): raise NotImplementedError
def aggregate(self, *args, **kwargs): raise NotImplementedError
def annotate(self, *args, **kwargs): raise NotImplementedError
def _get_real_instances(self, base_result_objects): def _get_real_instances(self, base_result_objects):
""" """
Polymorphic object loader Polymorphic object loader
@ -95,8 +102,9 @@ class PolymorphicQuerySet(QuerySet):
base class query. The class of all of them is self.model (our base model). base class query. The class of all of them is self.model (our base model).
Some, many or all of these objects were not created and stored as Some, many or all of these objects were not created and stored as
class self.model, but as a class derived from self.model. We want to fetch class self.model, but as a class derived from self.model. We want to re-fetch
these objects from the db so we can return them just as they were saved. these objects from the db as their original class so we can return them
just as they were created/saved.
We identify these objects by looking at o.polymorphic_ctype, which specifies We identify these objects by looking at o.polymorphic_ctype, which specifies
the real class of these objects (the class at the time they were saved). the real class of these objects (the class at the time they were saved).
@ -145,7 +153,11 @@ class PolymorphicQuerySet(QuerySet):
def iterator(self): def iterator(self):
""" """
This function does the same as: This function is used by Django for all object retrieval.
By overriding it, we modify the objects that ths queryset returns
when it is evaluated (or it's get method or other object-returning methods are called).
Here we do the same as:
base_result_objects=list(super(PolymorphicQuerySet, self).iterator()) base_result_objects=list(super(PolymorphicQuerySet, self).iterator())
real_results=self._get_get_real_instances(base_result_objects) real_results=self._get_get_real_instances(base_result_objects)
@ -173,12 +185,6 @@ class PolymorphicQuerySet(QuerySet):
if reached_end: raise StopIteration if reached_end: raise StopIteration
# these queryset functions are not yet supported
def defer(self, *args, **kwargs): raise NotImplementedError
def only(self, *args, **kwargs): raise NotImplementedError
def aggregate(self, *args, **kwargs): raise NotImplementedError
def annotate(self, *args, **kwargs): raise NotImplementedError
def __repr__(self): def __repr__(self):
result = [ repr(o) for o in self.all() ] result = [ repr(o) for o in self.all() ]
return '[ ' + ',\n '.join(result) + ' ]' return '[ ' + ',\n '.join(result) + ' ]'
@ -192,7 +198,7 @@ class PolymorphicQuerySet(QuerySet):
# functionality to filters and Q objects. # functionality to filters and Q objects.
# Probably a more general queryset enhancement class could be made out them. # Probably a more general queryset enhancement class could be made out them.
def _translate_polymorphic_filter_specs_in_kwargs(queryset_model, kwargs): def _translate_polymorphic_filter_defnitions_in_kwargs(queryset_model, kwargs):
""" """
Translate the keyword argument list for PolymorphicQuerySet.filter() Translate the keyword argument list for PolymorphicQuerySet.filter()
@ -209,7 +215,7 @@ def _translate_polymorphic_filter_specs_in_kwargs(queryset_model, kwargs):
additional_args = [] additional_args = []
for field_path, val in kwargs.items(): for field_path, val in kwargs.items():
# normal filter expression => ignore # normal filter expression => ignore
new_expr = _translate_polymorphic_filter_spec(queryset_model, field_path, val) new_expr = _translate_polymorphic_filter_defnition(queryset_model, field_path, val)
if type(new_expr) == tuple: if type(new_expr) == tuple:
# replace kwargs element # replace kwargs element
del(kwargs[field_path]) del(kwargs[field_path])
@ -221,7 +227,7 @@ def _translate_polymorphic_filter_specs_in_kwargs(queryset_model, kwargs):
return additional_args return additional_args
def _translate_polymorphic_filter_specs_in_args(queryset_model, args): def _translate_polymorphic_filter_defnitions_in_args(queryset_model, args):
""" """
Translate the non-keyword argument list for PolymorphicQuerySet.filter() Translate the non-keyword argument list for PolymorphicQuerySet.filter()
@ -229,6 +235,8 @@ def _translate_polymorphic_filter_specs_in_args(queryset_model, args):
polymorphic functionality with their vanilla django equivalents. polymorphic functionality with their vanilla django equivalents.
We traverse the Q object tree for this (which is simple). We traverse the Q object tree for this (which is simple).
TODO: investigate: we modify the Q-objects ina args in-place. Is this OK?
Modifies: args list Modifies: args list
""" """
@ -240,7 +248,7 @@ def _translate_polymorphic_filter_specs_in_args(queryset_model, args):
if type(child) == tuple: if type(child) == tuple:
# this Q object child is a tuple => a kwarg like Q( instance_of=ModelB ) # this Q object child is a tuple => a kwarg like Q( instance_of=ModelB )
key, val = child key, val = child
new_expr = _translate_polymorphic_filter_spec(queryset_model, key, val) new_expr = _translate_polymorphic_filter_defnition(queryset_model, key, val)
if new_expr: if new_expr:
node.children[i] = new_expr node.children[i] = new_expr
else: else:
@ -251,7 +259,7 @@ def _translate_polymorphic_filter_specs_in_args(queryset_model, args):
if isinstance(q, models.Q): if isinstance(q, models.Q):
tree_node_correct_field_specs(q) tree_node_correct_field_specs(q)
def _translate_polymorphic_filter_spec(queryset_model, field_path, field_val): def _translate_polymorphic_filter_defnition(queryset_model, field_path, field_val):
""" """
Translate a keyword argument (field_path=field_val), as used for Translate a keyword argument (field_path=field_val), as used for
PolymorphicQuerySet.filter()-like functions (and Q objects). PolymorphicQuerySet.filter()-like functions (and Q objects).
@ -531,7 +539,10 @@ class PolymorphicModel(models.Model):
class Meta: class Meta:
abstract = True abstract = True
polymorphic_ctype = models.ForeignKey(ContentType, null=True, editable=False) # TODO: %(class)s alone is not really enough, we also need to include app_label - patch for Django needed?
# see: django/db/models/fields/related.py/RelatedField
polymorphic_ctype = models.ForeignKey(ContentType,
null=True, editable=False, related_name='polymorphic_%(class)s_set')
# some applications want to know the name of the fields that are added to its models # some applications want to know the name of the fields that are added to its models
polymorphic_internal_model_fields = [ 'polymorphic_ctype' ] polymorphic_internal_model_fields = [ 'polymorphic_ctype' ]
@ -540,8 +551,7 @@ class PolymorphicModel(models.Model):
base_objects = models.Manager() base_objects = models.Manager()
def pre_save_polymorphic(self): def pre_save_polymorphic(self):
""" """Normally not needed.
Normally not needed.
This function may be called manually in special use-cases. When the object This function may be called manually in special use-cases. When the object
is saved for the first time, we store its real class in polymorphic_ctype. is saved for the first time, we store its real class in polymorphic_ctype.
When the object later is retrieved by PolymorphicQuerySet, it uses this When the object later is retrieved by PolymorphicQuerySet, it uses this

View File

@ -6,6 +6,7 @@ from django.test import TestCase
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.db.models import Q from django.db.models import Q
from django.db import models from django.db import models
from django.contrib.contenttypes.models import ContentType
from models import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet, ShowFields, ShowFieldsAndTypes from models import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet, ShowFields, ShowFieldsAndTypes
@ -88,6 +89,12 @@ class MgrInheritB(MgrInheritA):
class MgrInheritC(ShowFieldsAndTypes, MgrInheritB): class MgrInheritC(ShowFieldsAndTypes, MgrInheritB):
pass pass
# validation error: "polymorphic.relatednameclash: Accessor for field 'polymorphic_ctype' clashes
# with related field 'ContentType.relatednameclash_set'." (reported by Andrew Ingram)
# fixed with related_name
class RelatedNameClash(PolymorphicModel):
ctype = models.ForeignKey(ContentType, null=True, editable=False)
class testclass(TestCase): class testclass(TestCase):
def test_diamond_inheritance(self): def test_diamond_inheritance(self):