[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