From 2a740e9b109b77ce9ffc7e5d700081c836724b5f Mon Sep 17 00:00:00 2001 From: Bert Constantin Date: Sat, 30 Jan 2010 20:30:37 +0100 Subject: [PATCH] doc- and minor code updates --- DOCS.rst | 97 +++++++++++++++++++------------------- README.rst | 73 ++++++++++++++++------------ pexp/models.py | 8 ++++ polymorphic/polymorphic.py | 49 +++++++++++++------ polymorphic/tests.py | 4 ++ 5 files changed, 138 insertions(+), 93 deletions(-) diff --git a/DOCS.rst b/DOCS.rst index 58036d7..02ca4e7 100644 --- a/DOCS.rst +++ b/DOCS.rst @@ -9,8 +9,6 @@ Requirements Django 1.1 (or later) and Python 2.5 (or later). This code has been tested on Django 1.1.1 / 1.2 alpha and Python 2.5.4 / 2.6.4 on Linux. -Django's ContentType framework is used (part of Django). - Testing ------- @@ -32,16 +30,14 @@ to your liking, then run:: Using polymorphic models in your own projects --------------------------------------------- -The best way for now is probably to just copy the ``polymorphic`` directory -into your project dir. If you want to use the management command -``polymorphic_dumpdata``, then you also need to add ``polymorphic`` -to your INSTALLED_APPS setting. The ContentType framework -(``django.contrib.contenttypes``) needs to be listed in INSTALLED_APPS -as well (usually it already is). +The easiest way for now is to just copy the ``polymorphic`` directory +(under ``django_polymorphic``) into your Django project dir. -It's also still possible to use ``polymorphic.py`` only, as a single file -add-on module, copied to somewhere where it can be imported (like your -own app dir). +If you want to use the management command ``polymorphic_dumpdata``, then +you need to add ``polymorphic`` to your INSTALLED_APPS setting as well. + +In any case, the ContentType framework (``django.contrib.contenttypes``) +needs to be listed in INSTALLED_APPS (usually it already is). Defining Polymorphic Models @@ -193,8 +189,14 @@ manage.py dumpdata Django's standard ``dumpdata`` command requires non-polymorphic behaviour from the querysets it uses and produces incomplete results with polymorphic models. Django_polymorphic includes - a slightly modified version, named ``polymorphic_dumpdata``. - Just use this command instead of Django's (see "installation/testing"). + a slightly modified version, named ``polymorphic_dumpdata`` + that fixes this. Just use this command instead of Django's + (see "installation/testing"). + + Please note that there are problems using ContentType together + with Django's seralisation or fixtures (and all polymorphic models + use ContentType). This issue seems to be resolved with Django 1.2 + (changeset 11863): http://code.djangoproject.com/ticket/7052 Custom Managers, Querysets & Inheritance @@ -221,12 +223,12 @@ Django as automatic manager for several purposes, including accessing related objects. It must not filter objects and it's safest to use the plain ``PolymorphicManager`` here. -Manager Inheritance / Propagation ---------------------------------- +Manager Inheritance +------------------- -Polymorphic models unconditionally propagate/inherit all managers from -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 +Polymorphic models inherit/propagate all managers from their +base models, as long as these are polymorphic. This means that all +managers defined in polymorphic base models work just the same as if they were defined in the new model. An example (inheriting from MyModel above):: @@ -332,18 +334,18 @@ cases. Alternatively, if the SQL query execution time is significantly longer even in common use cases, this may still be acceptable in exchange for the added functionality. -General -------------------- +In General +---------- Let's not forget that all of the above is just about optimization. The current implementation already works well - and perhaps well enough for the majority of applications. Also, it seems that further optimization (down to one DB request) -would be restricted to a small area of the code, straightforward -to implement, and mostly independent from the rest of the module. -So this optimization can be done at any later time (like when -it's needed). +would be restricted to a relatively small area of the code, and +be mostly independent from the rest of the module. +So it seems this optimization can be done at any later time +(like when it's needed). Unsupported Methods, Restrictions & Caveats @@ -360,26 +362,31 @@ Currently Unsupported Queryset Methods enhancement. + ``defer()`` and ``only()``: Full support, including slight polymorphism - enhancements, seems to be straighforward - (depends on '_get_real_instances'). + enhancements, seems to be straighforward (depends on '_get_real_instances'). + ``extra()``: Does not really work with the current implementation of '_get_real_instances'. It's unclear if it should be supported. -+ ``select_related()``: This would probably need Django core support - for traversing the reverse model inheritance OneToOne relations - with Django's select_related(), e.g.: - ``select_related('modela__modelb__foreignkeyfield')``. - Also needs more thought/investigation. ++ ``select_related()`` works just as usual, but it can not (yet) be used + to select relations in derived models + (like ``ModelA.objects.select_related('ModelC___fieldxy')`` ) -+ ``distinct()`` needs more thought and investigation as well ++ ``distinct()`` needs more thought and investigation + ++ ``values()`` & ``values_list()``: Implementation seems to be mostly straighforward -+ ``values()`` & ``values_list()``: Implementation seems to be mostly - straighforward Restrictions & Caveats ---------------------- +* ``Django 1.1 only``: When ContentType is used in models, Django's + seralisation or fixtures cannot be used. 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 + * Diamond shaped inheritance: There seems to be a general problem with diamond shaped multiple model inheritance with Django models (tested with V1.1). @@ -394,13 +401,6 @@ Restrictions & Caveats ContentType table needs to be corrected too, if the db content should stay usable after the rename. -* The use of ContentType together with Django's seralisation or - fixtures seems to pose problems up to Django 1.1. 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 - * For all objects that are not instances of the base class, but instances of a subclass, the base class fields are currently transferred twice from the database (an artefact of the current @@ -411,19 +411,20 @@ Restrictions & Caveats forward relation fields, Django internally tries to use our polymorphic manager/queryset in some places, which of course it should not. Currently this is solved with a hacky __getattribute__ - in PolymorphicModel, which causes some overhead. A minor patch t + in PolymorphicModel, which causes some overhead. A minor patch to Django core would probably get rid of that. In General ---------- -It's important to consider that this code is still very new and experimental. +It's important to consider that this code is very new and +to some extent still experimental. Please see the docs for +current restrictions, caveats, 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's still very early and API changes, code reorganisations or further -schema changes are still a possibility. +It does seem to work very well for a number of people, but +API changes, code reorganisations or further schema changes +are still a possibility. There may also remain larger bugs +and problems in the code that have not yet been found. Links diff --git a/README.rst b/README.rst index a2667f7..9268586 100644 --- a/README.rst +++ b/README.rst @@ -1,19 +1,12 @@ -**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. +Release Notes, Usage, Code +-------------------------- +* Please see `here for release notes, news and discussion`_ (Google Group) +* Installation and usage: `Documentation and Examples`_ (or the short `Overview`_) +* The code is on GitHub_ and Bitbucket_ and can also be downloaded as TGZ_ or ZIP_ -Usage, Examples, Installation & Documentation, Links ----------------------------------------------------- - -* Please see the `Documentation and Examples`_ (or the short `Overview`_) -* News & comments: `News, Comments, Questions & Discussion`_ -* 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 -.. _News, Comments, Questions & Discussion: http://groups.google.de/group/django-polymorphic/topics +.. _here for release notes, news and discussion: http://groups.google.de/group/django-polymorphic/topics +.. _Documentation and Examples: http://bserve.webhop.org/wiki/django_polymorphic/doc .. _GitHub: http://github.com/bconstantin/django_polymorphic .. _Bitbucket: http://bitbucket.org/bconstantin/django_polymorphic .. _TGZ: http://github.com/bconstantin/django_polymorphic/tarball/master @@ -24,12 +17,15 @@ Usage, Examples, Installation & Documentation, Links What is django_polymorphic good for? ------------------------------------ -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. +It implements seamless polymorphic inheritance for Django models. -Example: -If the models ``ArtProject`` and ``ResearchProject`` inherit from the model ``Project``, -and we have saved one of each of them into the database, then we can do:: +This means: objects being retrieved from the database are always returned +back with the same type/class and fields they were created and saved with. + +An example: +If we defined the model ``Project`` as a base class for our models +``ArtProject`` and ``ResearchProject``, and we have stored one of +each into the database, then we can do:: >>> Project.objects.all() . @@ -41,23 +37,40 @@ It doesn't matter how these objects are retrieved: be it through the model's own managers/querysets, ForeignKeys, ManyToManyFields or OneToOneFields. -``django_polymorphic`` does this only for models that explicitly request -this behaviour (and implicitely for their submodels). +As seen in this example, the resulting querysets are polymorphic, +and will typically deliver objects of several different types in +a single query result. -The resulting querysets are polymorphic, i.e they may deliver -objects of several different types in a single query result. +django_polymorphic does this only for models that explicitely enable it +(and for their submodels). + +Please see the `Documentation and Examples`_ for more information +(also included as the file ``DOCS.rst`` with the source). Status ------ -It's important to consider that this code is still very new and -experimental. Please see the docs for current restrictions, caveats, -and performance implications. +It's important to consider that this code is very new and +to some extent still experimental. Please see the docs for +current restrictions, caveats, and performance implications. -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 -schema changes are still a possibility. +It does seem to work very well for a number of people, but +API changes, code reorganisations or further schema changes +are still a possibility. There may also remain larger bugs +and problems in the code that have not yet been found. -Right now it's suitable only for the more enterprising early adopters. +License +------- + +django_polymorphic uses the same license as Django (BSD-like). + + +Database Schema Change on January 26 +------------------------------------ + +| The update from January 26 changed the database schema (more info in the commit-log_). +| Sorry for any inconvenience. But this should be the final DB schema now. + +.. _commit-log: http://github.com/bconstantin/django_polymorphic/commit/c2b420aea06637966a208329ef7ec853889fa4c7 diff --git a/pexp/models.py b/pexp/models.py index 06f3a90..b7e5879 100644 --- a/pexp/models.py +++ b/pexp/models.py @@ -16,3 +16,11 @@ class ModelB(ModelA): field2 = models.CharField(max_length=10) class ModelC(ModelB): field3 = models.CharField(max_length=10) + +class SModelA(ShowFieldsAndTypes, PolymorphicModel): + field1 = models.CharField(max_length=10) +class SModelB(SModelA): + field2 = models.CharField(max_length=10) +class SModelC(SModelB): + field3 = models.CharField(max_length=10) + diff --git a/polymorphic/polymorphic.py b/polymorphic/polymorphic.py index 2491067..a2bda57 100644 --- a/polymorphic/polymorphic.py +++ b/polymorphic/polymorphic.py @@ -3,7 +3,7 @@ Fully Polymorphic Django Models =============================== -Please see the examples and documentation here: +For an overview, examples, documentation and updates please see here: http://bserve.webhop.org/wiki/django_polymorphic @@ -62,6 +62,10 @@ class PolymorphicManager(models.Manager): ################################################################################### ### PolymorphicQuerySet +# PolymorphicQuerySet Q objects (and filter()) support these additional key words. +# These are forbidden as field names (a descriptive exception is raised) +POLYMORPHIC_SPECIAL_Q_KWORDS = [ 'instance_of', 'not_instance_of'] + class PolymorphicQuerySet(QuerySet): """ QuerySet for PolymorphicModel @@ -73,15 +77,19 @@ class PolymorphicQuerySet(QuerySet): """ def instance_of(self, *args): + """Filter the queryset to only include the classes in args (and their subclasses). + Implementation in _translate_polymorphic_filter_defnition.""" return self.filter(instance_of=args) def not_instance_of(self, *args): + """Filter the queryset to exclude the classes in args (and their subclasses). + Implementation in _translate_polymorphic_filter_defnition.""" return self.filter(not_instance_of=args) def _filter_or_exclude(self, negate, *args, **kwargs): - """ we override this internal Django functon since it is used for all filtering """ - _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' + "We override this internal Django functon as it is used for all filter member functions." + _translate_polymorphic_filter_definitions_in_args(self.model, args) # the Q objects + additional_args = _translate_polymorphic_filter_definitions_in_kwargs(self.model, kwargs) # filter_field='data' return super(PolymorphicQuerySet, self)._filter_or_exclude(negate, *(list(args) + additional_args), **kwargs) # these queryset functions are not yet supported @@ -154,13 +162,13 @@ class PolymorphicQuerySet(QuerySet): def iterator(self): """ 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). + By overriding it, we modify the objects that this queryset returns + when it is evaluated (or its get method or other object-returning methods are called). Here we do the same as: base_result_objects=list(super(PolymorphicQuerySet, self).iterator()) - real_results=self._get_get_real_instances(base_result_objects) + real_results=self._get_real_instances(base_result_objects) for o in real_results: yield o but it requests the objects in chunks from the database, @@ -196,9 +204,9 @@ class PolymorphicQuerySet(QuerySet): # These functions implement the additional filter- and Q-object functionality. # They form a kind of small framework for easily adding more # 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 of them. -def _translate_polymorphic_filter_defnitions_in_kwargs(queryset_model, kwargs): +def _translate_polymorphic_filter_definitions_in_kwargs(queryset_model, kwargs): """ Translate the keyword argument list for PolymorphicQuerySet.filter() @@ -214,8 +222,9 @@ def _translate_polymorphic_filter_defnitions_in_kwargs(queryset_model, kwargs): """ additional_args = [] for field_path, val in kwargs.items(): - # normal filter expression => ignore + new_expr = _translate_polymorphic_filter_defnition(queryset_model, field_path, val) + if type(new_expr) == tuple: # replace kwargs element del(kwargs[field_path]) @@ -227,7 +236,7 @@ def _translate_polymorphic_filter_defnitions_in_kwargs(queryset_model, kwargs): return additional_args -def _translate_polymorphic_filter_defnitions_in_args(queryset_model, args): +def _translate_polymorphic_filter_definitions_in_args(queryset_model, args): """ Translate the non-keyword argument list for PolymorphicQuerySet.filter() @@ -288,7 +297,7 @@ def _translate_polymorphic_filter_defnition(queryset_model, field_path, field_va def _translate_polymorphic_field_path(queryset_model, field_path): """ - Translate a field path from keyword argument, as used for + Translate a field path from a keyword argument, as used for PolymorphicQuerySet.filter()-like functions (and Q objects). E.g.: ModelC___field3 is translated into modela__modelb__modelc__field3 @@ -412,10 +421,13 @@ class PolymorphicModelBase(ModelBase): def __new__(self, model_name, bases, attrs): #print; print '###', model_name, '- bases:', bases - + # create new model new_class = self.call_superclass_new_method(model_name, bases, attrs) + # check if the model fields are all allowed + self.validate_model_fields(new_class) + # create list of all managers to be inherited from the base classes inherited_managers = new_class.get_inherited_managers(attrs) @@ -494,6 +506,13 @@ class PolymorphicModelBase(ModelBase): if do_app_label_workaround: del(meta.app_label) return new_class + def validate_model_fields(self): + "check if all fields names are allowed (i.e. not in POLYMORPHIC_SPECIAL_Q_KWORDS)" + for f in self._meta.fields: + if f.name in POLYMORPHIC_SPECIAL_Q_KWORDS: + e = 'PolymorphicModel: "%s" - field name "%s" is not allowed in polymorphic models' + raise AssertionError(e % (self.__name__, f.name) ) + @classmethod def validate_model_manager(self, manager, model_name, manager_name): """check if the manager is derived from PolymorphicManager @@ -638,7 +657,7 @@ class PolymorphicModel(models.Model): class ShowFields(object): - """ mixin that shows the object's class, it's fields and field contents """ + """ model mixin that shows the object's class, it's fields and field contents """ def __repr__(self): out = 'id %d, ' % (self.id); last = self._meta.fields[-1] for f in self._meta.fields: @@ -654,7 +673,7 @@ class ShowFields(object): class ShowFieldsAndTypes(object): - """ like ShowFields, but also show field types """ + """ model mixin, like ShowFields, but also show field types """ def __repr__(self): out = 'id %d, ' % (self.id); last = self._meta.fields[-1] for f in self._meta.fields: diff --git a/polymorphic/tests.py b/polymorphic/tests.py index 7bab067..cd31973 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -89,6 +89,10 @@ class MgrInheritB(MgrInheritA): class MgrInheritC(ShowFieldsAndTypes, MgrInheritB): pass +# test bad field name +#class TestBadFieldModel(PolymorphicModel): +# instance_of = models.CharField(max_length=10) + # validation error: "polymorphic.relatednameclash: Accessor for field 'polymorphic_ctype' clashes # with related field 'ContentType.relatednameclash_set'." (reported by Andrew Ingram) # fixed with related_name