[Python-modules-commits] [django-polymorphic] 01/05: Import django-polymorphic_0.9.1.orig.tar.gz

Michael Fladischer fladi at moszumanska.debian.org
Sun Feb 21 10:04:02 UTC 2016


This is an automated email from the git hooks/post-receive script.

fladi pushed a commit to branch master
in repository django-polymorphic.

commit 67e597e7ff9b2a93336a0f8262f09efd9079a8db
Author: Michael Fladischer <FladischerMichael at fladi.at>
Date:   Sun Feb 21 10:55:35 2016 +0100

    Import django-polymorphic_0.9.1.orig.tar.gz
---
 .travis.yml                                        |   2 -
 docs/admin.rst                                     |   3 +
 docs/advanced.rst                                  |  13 +-
 docs/changelog.rst                                 |  37 ++++-
 docs/conf.py                                       |   4 +-
 docs/index.rst                                     |   1 +
 docs/migrating.rst                                 |  65 +++++++++
 docs/quickstart.rst                                |   2 +-
 docs/third-party.rst                               |  79 ++++++++---
 polymorphic/__init__.py                            |   2 +-
 polymorphic/admin.py                               |  55 +++++++-
 polymorphic/base.py                                |   2 +-
 polymorphic/managers.py                            |   7 +
 polymorphic/query.py                               | 148 ++++++++++++++++++--
 .../admin/polymorphic/object_history.html          |   6 +
 polymorphic/tests.py                               | 153 +++++++++++++++++++++
 16 files changed, 530 insertions(+), 49 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 006d5eb..e247041 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -24,8 +24,6 @@ matrix:
     env: DJANGO=">=1.6,<1.7"
   - python: "3.5"
     env: DJANGO=">=1.7,<1.8"
-  - python: "3.5"
-    env: DJANGO=">=1.8,<1.9"
 
   - python: "3.4"
     env: DJANGO=">=1.4,<1.5"
diff --git a/docs/admin.rst b/docs/admin.rst
index 7de3048..8e499c7 100644
--- a/docs/admin.rst
+++ b/docs/admin.rst
@@ -35,6 +35,9 @@ the performance hit of retrieving child models.
 This can be controlled by setting the ``polymorphic_list`` property on the
 parent admin.  Setting it to True will provide child models to the list template.
 
+Note: If you are using non-integer primary keys in your model, you have to edit ``pk_regex``, 
+for example ``pk_regex = '([\w-]+)'`` if you use UUIDs. Otherwise you cannot change model entries.
+
 The child models
 ----------------
 
diff --git a/docs/advanced.rst b/docs/advanced.rst
index dbc7f82..c9fcac4 100644
--- a/docs/advanced.rst
+++ b/docs/advanced.rst
@@ -176,8 +176,9 @@ About Queryset Methods
     methods now, it's best if you use ``Model.base_objects.values...`` as
     this is guaranteed to not change. 
 
-*   ``defer()`` and ``only()`` are not yet supported (support will be added
-    in the future). 
+*   ``defer()`` and ``only()`` work as expected. On Django 1.5+ they support
+    the ``ModelX___field`` syntax, but on Django 1.4 it is only possible to
+    pass fields on the base model into these methods.
 
 
 Using enhanced Q-objects in any Places
@@ -231,10 +232,10 @@ Restrictions & Caveats
 *   Database Performance regarding concrete Model inheritance in general.
     Please see the :ref:`performance`.
 
-*   Queryset methods ``values()``, ``values_list()``, ``select_related()``,
-    ``defer()`` and ``only()`` are not yet fully supported (see above).
-    ``extra()`` has one restriction: the resulting objects are required to have
-    a unique primary key within the result set.
+*   Queryset methods ``values()``, ``values_list()``, and ``select_related()``
+    are not yet fully supported (see above). ``extra()`` has one restriction:
+    the resulting objects are required to have a unique primary key within
+    the result set.
 
 *   Diamond shaped inheritance: There seems to be a general problem 
     with diamond shaped multiple model inheritance with Django models
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 5de4115..4c98807 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,5 +1,30 @@
 Changelog
-==========
+=========
+
+Version 0.9.1 (2016-02-18)
+--------------------------
+
+* Fixed support for ``PolymorphicManager.from_queryset()`` for custom query sets.
+* Fixed Django 1.7 ``changeform_view()`` redirection to the child admin site.
+  This fixes custom admin code that uses these views, such as django-reversion_'s ``revision_view()`` / ``recover_view()``.
+* Fixed ``.only('pk')`` field support.
+* Fixed ``object_history_template`` breadcrumb.
+  **NOTE:** when using django-reversion_ / django-reversion-compare_, make sure to implement
+  a ``admin/polymorphic/object_history.html`` template in your project that extends
+  from ``reversion/object_history.html`` or ``reversion-compare/object_history.html`` respectively.
+
+
+Version 0.9 (2016-02-17)
+------------------------
+
+* Added ``.only()`` and ``.defer()`` support.
+* Added support for Django 1.8 complex expressions in ``.annotate()`` / ``.aggregate()``.
+* Fix Django 1.9 handling of custom URLs.
+  The new change-URL redirect overlapped any custom URLs defined in the child admin.
+* Fix Django 1.9 support in the admin.
+* Fix missing ``history_view()`` redirection to the child admin, which is important for django-reversion_ support.
+  See the documentation for hints for :ref:`django-reversion-compare support <django-reversion-compare-support>`.
+
 
 Version 0.8.1 (2015-12-29)
 --------------------------
@@ -15,13 +40,13 @@ Version 0.8 (2015-12-28)
 * Renamed ``polymorphic.manager`` => ``polymorphic.managers`` for consistentcy.
 * **BACKWARDS INCOMPATIBILITY:** The import paths have changed to support Django 1.9.
   Instead of ``from polymorphic import X``,
-you'll have to import from the proper package. For example:
+  you'll have to import from the proper package. For example:
 
 .. code-block:: python
 
-    polymorphic.models import PolymorphicModel
-    polymorphic.managers import PolymorphicManager, PolymorphicQuerySet
-    polymorphic.showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent
+    from polymorphic.models import PolymorphicModel
+    from polymorphic.managers import PolymorphicManager, PolymorphicQuerySet
+    from polymorphic.showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent
 
 * **BACKWARDS INCOMPATIBILITY:** Removed ``__version__.py`` in favor of a standard ``__version__`` in ``polymorphic/__init__.py``.
 * **BACKWARDS INCOMPATIBILITY:** Removed automatic proxying of method calls to the queryset class.
@@ -188,3 +213,5 @@ For a detailed list of it's changes, see the :doc:`archived changelog <changelog
 
 .. _Grappelli: http://grappelliproject.com/
 .. _django-parler: https://github.com/edoburu/django-parler
+.. _django-reversion: https://github.com/etianen/django-reversion
+.. _django-reversion-compare: https://github.com/jedie/django-reversion-compare
diff --git a/docs/conf.py b/docs/conf.py
index c0e277a..44d4463 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -55,9 +55,9 @@ copyright = u'2013, Bert Constantin, Chris Glass, Diederik van der Boor'
 # built documents.
 #
 # The short X.Y version.
-version = '0.8.1'
+version = '0.9.1'
 # The full version, including alpha/beta/rc tags.
-release = '0.8.1'
+release = '0.9.1'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/docs/index.rst b/docs/index.rst
index ce707f1..67b5f69 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -57,6 +57,7 @@ Advanced topics
 .. toctree::
    :maxdepth: 2
 
+   migrating
    advanced
    managers
    third-party
diff --git a/docs/migrating.rst b/docs/migrating.rst
new file mode 100644
index 0000000..7848234
--- /dev/null
+++ b/docs/migrating.rst
@@ -0,0 +1,65 @@
+Migrating existing models to polymorphic
+========================================
+
+Existing models can be migrated to become polymorphic models.
+During the migrating, the ``polymorphic_ctype`` field needs to be filled in.
+
+This can be done in the following steps:
+
+#. Inherit your model from :class:`~polymorphic.models.PolymorphicModel`.
+#. Create a Django migration file to create the ``polymorphic_ctype_id`` database column.
+#. Make sure the proper :class:`~django.contrib.contenttypes.models.ContentType` value is filled in.
+
+Filling the content type value
+------------------------------
+
+The following Python code can be used to fill the value of a model:
+
+.. code-block:: python
+
+    from django.contrib.contenttypes.models import ContentType
+    from myapp.models import MyModel
+
+    new_ct = ContentType.objects.get_for_model(MyModel)
+    MyModel.objects.filter(polymorphic_ctype__isnull=True).update(polymorphic_ctype=new_ct)
+
+The creation and update of the ``polymorphic_ctype_id`` column
+can be included in a single Django migration. For example:
+
+.. code-block:: python
+
+    # -*- coding: utf-8 -*-
+    from __future__ import unicode_literals
+    from django.db import migrations, models
+
+
+    def forwards_func(apps, schema_editor):
+        MyModel = apps.get_model('myapp', 'MyModel')
+        ContentType = apps.get_model('contenttypes', 'ContentType')
+
+        new_ct = ContentType.objects.get_for_model(MyModel)
+        MyModel.objects.filter(polymorphic_ctype__isnull=True).update(polymorphic_ctype=new_ct)
+
+
+    def backwards_func(apps, schema_editor):
+        pass
+
+
+    class Migration(migrations.Migration):
+
+        dependencies = [
+            ('contenttypes', '0001_initial'),
+            ('myapp', '0001_initial'),
+        ]
+
+        operations = [
+            migrations.AddField(
+                model_name='mymodel',
+                name='polymorphic_ctype',
+                field=models.ForeignKey(related_name='polymorphic_myapp.mymodel_set+', editable=False, to='contenttypes.ContentType', null=True),
+            ),
+            migrations.RunPython(forwards_func, backwards_func),
+        ]
+
+It's recommended to let ``makemigrations`` create the migration file,
+and include the ``RunPython`` manually before running the migration.
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 85e7f61..f14c816 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -12,7 +12,7 @@ Update the settings file::
         'django.contrib.contenttypes',
     )
 
-The current release of *django-polymorphic* supports Django 1.4 till 1.8 and Python 3 is supported.
+The current release of *django-polymorphic* supports Django 1.4 till 1.9 and Python 3 is supported.
 
 Making Your Models Polymorphic
 ------------------------------
diff --git a/docs/third-party.rst b/docs/third-party.rst
index 03efe3d..1bac44b 100644
--- a/docs/third-party.rst
+++ b/docs/third-party.rst
@@ -1,23 +1,22 @@
 Third-party applications support
 ================================
 
-Django-reversion support
+django-reversion support
 ------------------------
 
-`Django-reversion <https://github.com/etianen/django-reversion>`_ works as
-expected with polymorphic models.  However, they require more setup than
-standard models.  We have to face these problems:
+Support for django-reversion_ works as expected with polymorphic models.
+However, they require more setup than standard models. That's become:
 
 * The children models are not registered in the admin site.
-  You will therefore need to manually register them to django-reversion.
-* Polymorphic models use
-  `multi-table inheritance <https://docs.djangoproject.com/en/dev/topics/db/models/#multi-table-inheritance>`_.
-  The django-reversion wiki explains
-  `how to deal with this <https://github.com/etianen/django-reversion/wiki/Low-level-API#multi-table-inheritance>`_.
+  You will therefore need to manually register them to django-reversion_.
+* Polymorphic models use `multi-table inheritance <https://docs.djangoproject.com/en/dev/topics/db/models/#multi-table-inheritance>`_.
+  See the `reversion documentation <http://django-reversion.readthedocs.org/en/latest/api.html#multi-table-inheritance>`_
+  how to deal with this by adding a ``follow`` field for the primary key.
+* Both admin classes redefine ``object_history_template``.
 
 
 Example
-.......
+~~~~~~~
 
 The admin :ref:`admin-example` becomes:
 
@@ -25,19 +24,19 @@ The admin :ref:`admin-example` becomes:
 
     from django.contrib import admin
     from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin
-    import reversion
-    from reversion import VersionAdmin
+    from reversion.admin import VersionAdmin
+    from reversion import revisions
     from .models import ModelA, ModelB, ModelC
 
 
-    class ModelAChildAdmin(PolymorphicChildModelAdmin):
+    class ModelAChildAdmin(PolymorphicChildModelAdmin, VersionAdmin):
         base_model = ModelA
         base_form = ...
         base_fieldsets = (
             ...
         )
 
-    class ModelBAdmin(VersionAdmin, ModelAChildAdmin):
+    class ModelBAdmin(ModelAChildAdmin, VersionAdmin):
         # define custom features here
 
     class ModelCAdmin(ModelBAdmin):
@@ -51,6 +50,54 @@ The admin :ref:`admin-example` becomes:
             (ModelC, ModelCAdmin),
         )
 
-    reversion.register(ModelB, follow=['modela_ptr'])
-    reversion.register(ModelC, follow=['modelb_ptr'])
+    revisions.register(ModelB, follow=['modela_ptr'])
+    revisions.register(ModelC, follow=['modelb_ptr'])
     admin.site.register(ModelA, ModelAParentAdmin)
+
+Redefine a :file:`admin/polymorphic/object_history.html` template, so it combines both worlds:
+
+.. code-block:: html+django
+
+    {% extends 'reversion/object_history.html' %}
+    {% load polymorphic_admin_tags %}
+
+    {% block breadcrumbs %}
+        {% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %}
+    {% endblock %}
+
+This makes sure both the reversion template is used, and the breadcrumb is corrected for the polymorphic model.
+
+.. _django-reversion-compare-support:
+
+django-reversion-compare support
+--------------------------------
+
+The django-reversion-compare_ views work as expected, the admin requires a little tweak.
+In your parent admin, include the following method:
+
+.. code-block:: python
+
+    def compare_view(self, request, object_id, extra_context=None):
+        """Redirect the reversion-compare view to the child admin."""
+        real_admin = self._get_real_admin(object_id)
+        return real_admin.compare_view(request, object_id, extra_context=extra_context)
+
+As the compare view resolves the the parent admin, it uses it's base model to find revisions.
+This doesn't work, since it needs to look for revisions of the child model. Using this tweak,
+the view of the actual child model is used, similar to the way the regular change and delete views are redirected.
+
+
+django-mptt support
+-------------------
+
+Combining polymorphic with django-mptt_ is certainly possible, but not straightforward.
+It involves combining both managers, querysets, models, meta-classes and admin classes
+using multiple inheritance.
+
+The django-polymorphic-tree_ package provides this out of the box.
+
+
+.. _django-reversion: https://github.com/etianen/django-reversion
+.. _django-reversion-compare: https://github.com/jedie/django-reversion-compare
+.. _django-mptt: https://github.com/django-mptt/django-mptt
+.. _django-polymorphic-tree: https://github.com/edoburu/django-polymorphic-tree
diff --git a/polymorphic/__init__.py b/polymorphic/__init__.py
index f9b34fe..73822aa 100644
--- a/polymorphic/__init__.py
+++ b/polymorphic/__init__.py
@@ -7,7 +7,7 @@ This code and affiliated files are (C) by Bert Constantin and individual contrib
 Please see LICENSE and AUTHORS for more information.
 """
 # See PEP 440 (https://www.python.org/dev/peps/pep-0440/)
-__version__ = "0.8.1"
+__version__ = "0.9.1"
 
 
 # Monkey-patch Django < 1.5 to allow ContentTypes for proxy models.
diff --git a/polymorphic/admin.py b/polymorphic/admin.py
index 93d00fd..ad40a90 100644
--- a/polymorphic/admin.py
+++ b/polymorphic/admin.py
@@ -126,6 +126,7 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
     def __init__(self, model, admin_site, *args, **kwargs):
         super(PolymorphicParentModelAdmin, self).__init__(model, admin_site, *args, **kwargs)
         self._child_admin_site = self.admin_site.__class__(name=self.admin_site.name)
+        self._child_admin_site.get_app_list = lambda request: ()  # HACK: workaround for Django 1.9
         self._is_setup = False
 
     def _lazy_setup(self):
@@ -261,6 +262,23 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
         real_admin = self._get_real_admin(object_id)
         return real_admin.change_view(request, object_id, *args, **kwargs)
 
+    if django.VERSION >= (1, 7):
+        def changeform_view(self, request, object_id=None, *args, **kwargs):
+            # The `changeform_view` is available as of Django 1.7, combining the add_view and change_view.
+            # As it's directly called by django-reversion, this method is also overwritten to make sure it
+            # also redirects to the child admin.
+            if object_id:
+                real_admin = self._get_real_admin(object_id)
+                return real_admin.changeform_view(request, object_id, *args, **kwargs)
+            else:
+                # Add view. As it should already be handled via `add_view`, this means something custom is done here!
+                return super(PolymorphicParentModelAdmin, self).changeform_view(request, object_id, *args, **kwargs)
+
+    def history_view(self, request, object_id, extra_context=None):
+        """Redirect the history view to the real admin."""
+        real_admin = self._get_real_admin(object_id)
+        return real_admin.history_view(request, object_id, extra_context=extra_context)
+
     def delete_view(self, request, object_id, extra_context=None):
         """Redirect the delete view to the real admin."""
         real_admin = self._get_real_admin(object_id)
@@ -298,9 +316,15 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
                 name='{0}_{1}_change'.format(*info)
             )
 
+            redirect_urls = []
             for i, oldurl in enumerate(urls):
                 if oldurl.name == new_change_url.name:
                     urls[i] = new_change_url
+        else:
+            # For Django 1.9, the redirect at the end acts as catch all.
+            # The custom urls need to be inserted before that.
+            redirect_urls = [pat for pat in urls if not pat.name]  # redirect URL has no name.
+            urls = [pat for pat in urls if pat.name]
 
         # Define the catch-all for custom views
         custom_urls = [
@@ -317,7 +341,7 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
             admin = self._get_real_admin_by_model(model)
             dummy_urls += admin.get_urls()
 
-        return urls + custom_urls + dummy_urls
+        return urls + custom_urls + dummy_urls + redirect_urls
 
     def subclass_view(self, request, path):
         """
@@ -501,6 +525,25 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin):
             "admin/delete_confirmation.html"
         ]
 
+    @property
+    def object_history_template(self):
+        opts = self.model._meta
+        app_label = opts.app_label
+
+        # Pass the base options
+        base_opts = self.base_model._meta
+        base_app_label = base_opts.app_label
+
+        return [
+            "admin/%s/%s/object_history.html" % (app_label, opts.object_name.lower()),
+            "admin/%s/object_history.html" % app_label,
+            # Added:
+            "admin/%s/%s/object_history.html" % (base_app_label, base_opts.object_name.lower()),
+            "admin/%s/object_history.html" % base_app_label,
+            "admin/polymorphic/object_history.html",
+            "admin/object_history.html"
+        ]
+
     def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
         context.update({
             'base_opts': self.base_model._meta,
@@ -513,6 +556,16 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin):
         }
         return super(PolymorphicChildModelAdmin, self).delete_view(request, object_id, extra_context)
 
+    def history_view(self, request, object_id, extra_context=None):
+        # Make sure the history view can also display polymorphic breadcrumbs
+        context = {
+            'base_opts': self.base_model._meta,
+        }
+        if extra_context:
+            context.update(extra_context)
+        return super(PolymorphicChildModelAdmin, self).history_view(request, object_id, extra_context=context)
+
+
     # ---- Extra: improving the form/fieldset default display ----
 
     def get_fieldsets(self, request, obj=None):
diff --git a/polymorphic/base.py b/polymorphic/base.py
index 8c6b154..4ef8761 100644
--- a/polymorphic/base.py
+++ b/polymorphic/base.py
@@ -167,7 +167,7 @@ class PolymorphicModelBase(ModelBase):
         for key, val in new_class.__dict__.items():
             if isinstance(val, ManagerDescriptor):
                 val = val.manager
-            if not isinstance(val, PolymorphicManager) or type(val) is PolymorphicManager:
+            if not isinstance(val, PolymorphicManager):
                 continue
 
             mgr_list.append((val.creation_counter, key, val))
diff --git a/polymorphic/managers.py b/polymorphic/managers.py
index f20cd83..8064792 100644
--- a/polymorphic/managers.py
+++ b/polymorphic/managers.py
@@ -53,3 +53,10 @@ class PolymorphicManager(models.Manager):
 
     def get_real_instances(self, base_result_objects=None):
         return self.all().get_real_instances(base_result_objects=base_result_objects)
+
+    if django.VERSION >= (1, 7):
+        @classmethod
+        def from_queryset(cls, queryset_class, class_name=None):
+            manager = super(PolymorphicManager, cls).from_queryset(queryset_class, class_name=class_name)
+            manager.queryset_class = queryset_class  # also set our version, Django uses _queryset_class
+            return manager
diff --git a/polymorphic/query.py b/polymorphic/query.py
index 61071dc..1b71bdd 100644
--- a/polymorphic/query.py
+++ b/polymorphic/query.py
@@ -4,15 +4,16 @@
 """
 from __future__ import absolute_import
 
+import copy
 from collections import defaultdict
 
 import django
-from django.db.models.query import QuerySet
+from django.db.models.query import QuerySet, Q
 from django.contrib.contenttypes.models import ContentType
 from django.utils import six
 
 from .query_translate import translate_polymorphic_filter_definitions_in_kwargs, translate_polymorphic_filter_definitions_in_args
-from .query_translate import translate_polymorphic_field_path
+from .query_translate import translate_polymorphic_field_path, translate_polymorphic_Q_object
 
 # chunk-size: maximum number of objects requested per db-request
 # by the polymorphic queryset.iterator() implementation; we use the same chunk size as Django
@@ -64,12 +65,21 @@ class PolymorphicQuerySet(QuerySet):
     def __init__(self, *args, **kwargs):
         "init our queryset object member variables"
         self.polymorphic_disabled = False
+        # A parallel structure to django.db.models.query.Query.deferred_loading,
+        # which we maintain with the untranslated field names passed to
+        # .defer() and .only() in order to be able to retranslate them when
+        # retrieving the real instance (so that the deferred fields apply
+        # to that queryset as well).
+        self.polymorphic_deferred_loading = (set([]), True)
         super(PolymorphicQuerySet, self).__init__(*args, **kwargs)
 
     def _clone(self, *args, **kwargs):
         "Django's _clone only copies its own variables, so we need to copy ours here"
         new = super(PolymorphicQuerySet, self)._clone(*args, **kwargs)
         new.polymorphic_disabled = self.polymorphic_disabled
+        new.polymorphic_deferred_loading = (
+            copy.copy(self.polymorphic_deferred_loading[0]),
+            self.polymorphic_deferred_loading[1])
         return new
 
     if django.VERSION >= (1, 7):
@@ -111,25 +121,115 @@ class PolymorphicQuerySet(QuerySet):
         new_args = [translate_polymorphic_field_path(self.model, a) for a in args]
         return super(PolymorphicQuerySet, self).order_by(*new_args, **kwargs)
 
+    def defer(self, *fields):
+        """
+        Translate the field paths in the args, then call vanilla defer.
+
+        Also retain a copy of the original fields passed, which we'll need
+        when we're retrieving the real instance (since we'll need to translate
+        them again, as the model will have changed).
+        """
+        new_fields = [translate_polymorphic_field_path(self.model, a) for a in fields]
+        clone = super(PolymorphicQuerySet, self).defer(*new_fields)
+        clone._polymorphic_add_deferred_loading(fields)
+        return clone
+
+    def only(self, *fields):
+        """
+        Translate the field paths in the args, then call vanilla only.
+
+        Also retain a copy of the original fields passed, which we'll need
+        when we're retrieving the real instance (since we'll need to translate
+        them again, as the model will have changed).
+        """
+        new_fields = [translate_polymorphic_field_path(self.model, a) for a in fields]
+        clone = super(PolymorphicQuerySet, self).only(*new_fields)
+        clone._polymorphic_add_immediate_loading(fields)
+        return clone
+
+    def _polymorphic_add_deferred_loading(self, field_names):
+        """
+        Follows the logic of django.db.models.query.Query.add_deferred_loading(),
+        but for the non-translated field names that were passed to self.defer().
+        """
+        existing, defer = self.polymorphic_deferred_loading
+        if defer:
+            # Add to existing deferred names.
+            self.polymorphic_deferred_loading = existing.union(field_names), True
+        else:
+            # Remove names from the set of any existing "immediate load" names.
+            self.polymorphic_deferred_loading = existing.difference(field_names), False
+
+    def _polymorphic_add_immediate_loading(self, field_names):
+        """
+        Follows the logic of django.db.models.query.Query.add_immediate_loading(),
+        but for the non-translated field names that were passed to self.only()
+        """
+        existing, defer = self.polymorphic_deferred_loading
+        field_names = set(field_names)
+        if 'pk' in field_names:
+            field_names.remove('pk')
+            field_names.add(self.model._meta.pk.name)
+
+        if defer:
+            # Remove any existing deferred names from the current set before
+            # setting the new names.
+            self.polymorphic_deferred_loading = field_names.difference(existing), False
+        else:
+            # Replace any existing "immediate load" field names.
+            self.polymorphic_deferred_loading = field_names, False
+
     def _process_aggregate_args(self, args, kwargs):
         """for aggregate and annotate kwargs: allow ModelX___field syntax for kwargs, forbid it for args.
         Modifies kwargs if needed (these are Aggregate objects, we translate the lookup member variable)"""
 
-        def patch_lookup(a):
-            if django.VERSION < (1, 8):
-                a.lookup = translate_polymorphic_field_path(self.model, a.lookup)
+        def patch_lookup_lt_18(a):
+            a.lookup = translate_polymorphic_field_path(self.model, a.lookup)
+
+                    
+        def patch_lookup_gte_18(a):
+            # With Django > 1.8, the field on which the aggregate operates is
+            # stored inside a complex query expression.
+            if isinstance(a, Q):
+                translate_polymorphic_Q_object(self.model, a)
+            elif hasattr(a, 'get_source_expressions'):
+                for source_expression in a.get_source_expressions():
+                    patch_lookup_gte_18(source_expression)
             else:
-                # With Django > 1.8, the field on which the aggregate operates is
-                # stored inside a query expression.
-                if hasattr(a, 'source_expressions'):
-                    a.source_expressions[0].name = translate_polymorphic_field_path(
-                        self.model, a.source_expressions[0].name)
-
-        get_lookup = lambda a: a.lookup if django.VERSION < (1, 8) else a.source_expressions[0].name
-
+                a.name = translate_polymorphic_field_path(self.model, a.name)
+                
+        ___lookup_assert_msg = 'PolymorphicModel: annotate()/aggregate(): ___ model lookup supported for keyword arguments only'
+        def test___lookup_for_args_lt_18(a):
+            assert '___' not in a.lookup, ___lookup_assert_msg
+            
+        def test___lookup_for_args_gte_18(a):
+            """ *args might be complex expressions too in django 1.8 so
+            the testing for a '___' is rather complex on this one """
+            if isinstance(a, Q):
+                def tree_node_test___lookup(my_model, node):
+                    " process all children of this Q node "
+                    for i in range(len(node.children)):
+                        child = node.children[i]
+            
+                        if type(child) == tuple:
+                            # this Q object child is a tuple => a kwarg like Q( instance_of=ModelB )
+                            assert '___' not in child[0], ___lookup_assert_msg
+                        else:
+                            # this Q object child is another Q object, recursively process this as well
+                            tree_node_test___lookup(my_model, child)
+                            
+                tree_node_test___lookup(self.model, a)
+            elif hasattr(a, 'get_source_expressions'):
+                for source_expression in a.get_source_expressions():
+                    test___lookup_for_args_gte_18(source_expression)
+            else:
+                assert '___' not in a.name, ___lookup_assert_msg
+                    
         for a in args:
-            assert '___' not in get_lookup(a), 'PolymorphicModel: annotate()/aggregate(): ___ model lookup supported for keyword arguments only'
+            test___lookup = test___lookup_for_args_lt_18 if django.VERSION < (1, 8) else test___lookup_for_args_gte_18
+            test___lookup(a)
         for a in six.itervalues(kwargs):
+            patch_lookup = patch_lookup_lt_18 if django.VERSION < (1, 8) else patch_lookup_gte_18
             patch_lookup(a)
 
     def annotate(self, *args, **kwargs):
@@ -250,6 +350,26 @@ class PolymorphicQuerySet(QuerySet):
             })
             real_objects.query.select_related = self.query.select_related  # copy select related configuration to new qs
 
+            # Copy deferred fields configuration to the new queryset
+            deferred_loading_fields = []
+            existing_fields = self.polymorphic_deferred_loading[0]
+            for field in existing_fields:
+                try:
+                    translated_field_name = translate_polymorphic_field_path(
+                        real_concrete_class, field)
+                except AssertionError:
+                    if '___' in field:
+                        # The originally passed argument to .defer() or .only()
+                        # was in the form Model2B___field2, where Model2B is
+                        # now a superclass of real_concrete_class. Thus it's
+                        # sufficient to just use the field name.
+                        translated_field_name = field.rpartition('___')[-1]
+                    else:
+                        raise
+
+                deferred_loading_fields.append(translated_field_name)
+            real_objects.query.deferred_loading = (set(deferred_loading_fields), self.query.deferred_loading[1])
+
             for real_object in real_objects:
                 o_pk = getattr(real_object, pk_name)
                 real_class = real_object.get_real_instance_class()
diff --git a/polymorphic/templates/admin/polymorphic/object_history.html b/polymorphic/templates/admin/polymorphic/object_history.html
new file mode 100644
index 0000000..4306b53
--- /dev/null
+++ b/polymorphic/templates/admin/polymorphic/object_history.html
@@ -0,0 +1,6 @@
+{% extends "admin/object_history.html" %}
+{% load polymorphic_admin_tags %}
+
+{% block breadcrumbs %}
+  {% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %}
+{% endblock %}
diff --git a/polymorphic/tests.py b/polymorphic/tests.py
index e1278b7..3e1aa4f 100644
--- a/polymorphic/tests.py
+++ b/polymorphic/tests.py
@@ -15,6 +15,8 @@ from django.db.models.query import QuerySet
 
 from django.test import TestCase
 from django.db.models import Q, Count
+if django.VERSION >= (1, 8):
+    from django.db.models import Case, When
 from django.db import models
 from django.contrib.contenttypes.models import ContentType
 from django.utils import six
@@ -186,6 +188,18 @@ class ModelWithMyManager(ShowFieldTypeAndContent, Model2A):
     objects = MyManager()
     field4 = models.CharField(max_length=10)
 
+
+class ModelWithMyManagerNoDefault(ShowFieldTypeAndContent, Model2A):
+    objects = PolymorphicManager()
+    my_objects = MyManager()
+    field4 = models.CharField(max_length=10)
+
+class ModelWithMyManagerDefault(ShowFieldTypeAndContent, Model2A):
+    my_objects = MyManager()
+    objects = PolymorphicManager()
+    field4 = models.CharField(max_length=10)
+
+
 if django.VERSION >= (1, 7):
     class ModelWithMyManager2(ShowFieldTypeAndContent, Model2A):
         objects = MyManagerQuerySet.as_manager()
@@ -540,6 +554,76 @@ class PolymorphicTests(TestCase):
         self.assertEqual(repr(objects[2]), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
         self.assertEqual(repr(objects[3]), '<Model2D: id 4, field1 (CharField), field2 (CharField), field3 (CharField), field4 (CharField)>')
 
+    def test_defer_fields(self):
+        self.create_model2abcd()
+
+        objects_deferred = Model2A.objects.defer('field1')
+        self.assertNotIn('field1', objects_deferred[0].__dict__,
+            'field1 was not deferred (using defer())')
+        self.assertEqual(repr(objects_deferred[0]),
+            '<Model2A_Deferred_field1: id 1, field1 (CharField)>')
+        self.assertEqual(repr(objects_deferred[1]),
+            '<Model2B_Deferred_field1: id 2, field1 (CharField), field2 (CharField)>')
+        self.assertEqual(repr(objects_deferred[2]),
+            '<Model2C_Deferred_field1: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
+        self.assertEqual(repr(objects_deferred[3]),
+            '<Model2D_Deferred_field1: id 4, field1 (CharField), field2 (CharField), field3 (CharField), field4 (CharField)>')
+
+        objects_only = Model2A.objects.only('pk', 'polymorphic_ctype', 'field1')
+        self.assertIn('field1', objects_only[0].__dict__,
+            'qs.only("field1") was used, but field1 was incorrectly deferred')
+        self.assertIn('field1', objects_only[3].__dict__,
+            'qs.only("field1") was used, but field1 was incorrectly deferred'
+            ' on a child model')
+        self.assertNotIn('field4', objects_only[3].__dict__,
+            'field4 was not deferred (using only())')
+        self.assertEqual(repr(objects_only[0]),
+            '<Model2A: id 1, field1 (CharField)>')
+        self.assertEqual(repr(objects_only[1]),
+            '<Model2B_Deferred_field2: '
+            'id 2, field1 (CharField), field2 (CharField)>')
+        self.assertEqual(repr(objects_only[2]),
+            '<Model2C_Deferred_field2_field3_model2a_ptr_id: '
+            'id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
+        self.assertEqual(repr(objects_only[3]),
+            '<Model2D_Deferred_field2_field3_field4_model2a_ptr_id_model2b_ptr_id: '
+            'id 4, field1 (CharField), field2 (CharField), field3 (CharField), field4 (CharField)>')
+
+    # A bug in Django 1.4 prevents using defer across reverse relations
+    # <https://code.djangoproject.com/ticket/14694>. Since polymorphic
+    # uses reverse relations to traverse down model inheritance, deferring
+    # fields in child models will not work in Django 1.4.
+    @skipIf(django.VERSION < (1, 5), "Django 1.4 does not support defer on related fields")
+    def test_defer_related_fields(self):
+        self.create_model2abcd()
+
+        objects_deferred_field4 = Model2A.objects.defer('Model2D___field4')
+        self.assertNotIn('field4', objects_deferred_field4[3].__dict__,
+            'field4 was not deferred (using defer(), traversing inheritance)')
+        self.assertEqual(repr(objects_deferred_field4[0]),
+            '<Model2A: id 1, field1 (CharField)>')
+        self.assertEqual(repr(objects_deferred_field4[1]),
+            '<Model2B: id 2, field1 (CharField), field2 (CharField)>')
+        self.assertEqual(repr(objects_deferred_field4[2]),
+            '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
+        self.assertEqual(repr(objects_deferred_field4[3]),
+            '<Model2D_Deferred_field4: id 4, field1 (CharField), field2 (CharField), field3 (CharField), field4 (CharField)>')
+
+        objects_only_field4 = Model2A.objects.only(
+            'polymorphic_ctype', 'field1',
+            'Model2B___id', 'Model2B___field2', 'Model2B___model2a_ptr',
+            'Model2C___id', 'Model2C___field3', 'Model2C___model2b_ptr',
+            'Model2D___id', 'Model2D___model2c_ptr')
+        self.assertEqual(repr(objects_only_field4[0]),
+            '<Model2A: id 1, field1 (CharField)>')
+        self.assertEqual(repr(objects_only_field4[1]),
+            '<Model2B: id 2, field1 (CharField), field2 (CharField)>')
+        self.assertEqual(repr(objects_only_field4[2]),
+            '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
+        self.assertEqual(repr(objects_only_field4[3]),
+            '<Model2D_Deferred_field4: id 4, field1 (CharField), field2 (CharField), field3 (CharField), field4 (CharField)>')
+
+
     def test_manual_get_real_instance(self):
         self.create_model2abcd()
 
@@ -830,6 +914,31 @@ class PolymorphicTests(TestCase):
         self.assertIs(type(ModelWithMyManager._default_manager), MyManager)
         self.assertIs(type(ModelWithMyManager.base_objects), models.Manager)
 
+    def test_user_defined_manager_as_secondary(self):
+        self.create_model2abcd()
+        ModelWithMyManagerNoDefault.objects.create(field1='D1a', field4='D4a')
+        ModelWithMyManagerNoDefault.objects.create(field1='D1b', field4='D4b')
+
+        objects = ModelWithMyManagerNoDefault.my_objects.all()   # MyManager should reverse the sorting of field1
+        self.assertEqual(repr(objects[0]), '<ModelWithMyManagerNoDefault: id 6, field1 (CharField) "D1b", field4 (CharField) "D4b">')
+        self.assertEqual(repr(objects[1]), '<ModelWithMyManagerNoDefault: id 5, field1 (CharField) "D1a", field4 (CharField) "D4a">')
+        self.assertEqual(len(objects), 2)
+
+        self.assertIs(type(ModelWithMyManagerNoDefault.my_objects), MyManager)
+        self.assertIs(type(ModelWithMyManagerNoDefault.objects), PolymorphicManager)
+        self.assertIs(type(ModelWithMyManagerNoDefault._default_manager), PolymorphicManager)
+        self.assertIs(type(ModelWithMyManagerNoDefault.base_objects), models.Manager)
+
+    def test_user_objects_manager_as_secondary(self):
+        self.create_model2abcd()
+        ModelWithMyManagerDefault.objects.create(field1='D1a', field4='D4a')
+        ModelWithMyManagerDefault.objects.create(field1='D1b', field4='D4b')
+
+        self.assertIs(type(ModelWithMyManagerDefault.my_objects), MyManager)
+        self.assertIs(type(ModelWithMyManagerDefault.objects), PolymorphicManager)
+        self.assertIs(type(ModelWithMyManagerDefault._default_manager), MyManager)
+        self.assertIs(type(ModelWithMyManagerDefault.base_objects), models.Manager)
+
     @skipIf(django.VERSION < (1, 7), "This test needs Django 1.7+")
     def test_user_defined_queryset_as_manager(self):
         self.create_model2abcd()
@@ -984,6 +1093,50 @@ class PolymorphicTests(TestCase):
 
         # test that we can delete the object
         t.delete()
+        
+    def test_polymorphic__aggregate(self):
+        """ test ModelX___field syntax on aggregate (should work for annotate either) """
+        
+        Model2A.objects.create(field1='A1')
+        Model2B.objects.create(field1='A1', field2='B2')
+        Model2B.objects.create(field1='A1', field2='B2')
+        
+        # aggregate using **kwargs
+        result = Model2A.objects.aggregate(cnt=Count('Model2B___field2'))
+        self.assertEqual(result, {'cnt': 2})
+        
+        # aggregate using **args
+        with self.assertRaisesMessage(AssertionError, 'PolymorphicModel: annotate()/aggregate(): ___ model lookup supported for keyword arguments only'):
+            Model2A.objects.aggregate(Count('Model2B___field2'))
+        
+        
+        
+    @skipIf(django.VERSION < (1,8,), "This test needs Django >=1.8")
+    def test_polymorphic__complex_aggregate(self):
+        """ test (complex expression on) aggregate (should work for annotate either) """
+        
+        Model2A.objects.create(field1='A1')
+        Model2B.objects.create(field1='A1', field2='B2')
+        Model2B.objects.create(field1='A1', field2='B2')
+        
+        # aggregate using **kwargs
+        result = Model2A.objects.aggregate(
+            cnt_a1=Count(Case(When(field1='A1', then=1))),
+            cnt_b2=Count(Case(When(Model2B___field2='B2', then=1))),
+        )
+        self.assertEqual(result, {'cnt_b2': 2, 'cnt_a1': 3})
+
+        # aggregate using **args
+        # we have to set the defaul alias or django won't except a complex expression
+        # on aggregate/annotate
+        def ComplexAgg(expression):
+            complexagg = Count(expression)*10
+            complexagg.default_alias = 'complexagg'
+            return complexagg
+        
+        with self.assertRaisesMessage(AssertionError, 'PolymorphicModel: annotate()/aggregate(): ___ model lookup supported for keyword arguments only'):
+            Model2A.objects.aggregate(ComplexAgg('Model2B___field2'))
+
 
 
 class RegressionTests(TestCase):

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/django-polymorphic.git



More information about the Python-modules-commits mailing list