From 6013de93d30a405241ea5f3c94037c8f19eb251d Mon Sep 17 00:00:00 2001 From: Tadas Dailyda Date: Tue, 25 Jul 2017 14:02:15 +0300 Subject: [PATCH 01/21] tests for abstract/swappable model initialization (cherry picked from commit 0fea8a4d92638e39950225e2fb41c246d22441e1) --- polymorphic/tests/__init__.py | 17 +++++++++++++++++ runtests.py | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/polymorphic/tests/__init__.py b/polymorphic/tests/__init__.py index b16a917..32e1838 100644 --- a/polymorphic/tests/__init__.py +++ b/polymorphic/tests/__init__.py @@ -400,6 +400,23 @@ class DateModel(PolymorphicModel): date = models.DateTimeField() +# Define abstract and swappable (being swapped for SwappedModel) models +# To test manager validation (should be skipped for such models) +class AbstractModel(PolymorphicModel): + class Meta: + abstract = True + + +class SwappableModel(AbstractModel): + class Meta: + swappable = 'POLYMORPHIC_TEST_SWAPPABLE' + + +class SwappedModel(AbstractModel): + pass + + + # Import tests from .test_admin import * from .test_orm import * diff --git a/runtests.py b/runtests.py index 0e76c03..f0a1609 100755 --- a/runtests.py +++ b/runtests.py @@ -57,7 +57,8 @@ if not settings.configured: ), }, }, - ] + ], + POLYMORPHIC_TEST_SWAPPABLE='polymorphic.swappedmodel', ) From 731fd51a67654a587c0a9b28815931df2be7500a Mon Sep 17 00:00:00 2001 From: Tadas Dailyda Date: Tue, 25 Jul 2017 14:11:37 +0300 Subject: [PATCH 02/21] skip manager validation on swapped models --- polymorphic/base.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/polymorphic/base.py b/polymorphic/base.py index b6a5a74..89fa482 100644 --- a/polymorphic/base.py +++ b/polymorphic/base.py @@ -94,11 +94,12 @@ class PolymorphicModelBase(ModelBase): new_class._default_manager = user_manager._copy_to_model(new_class) new_class._default_manager._inherited = False # the default mgr was defined by the user, not inherited - # validate resulting default manager - if django.VERSION >= (1, 10) and not new_class._meta.abstract: - self.validate_model_manager(new_class.objects, model_name, 'objects') - else: - self.validate_model_manager(new_class._default_manager, model_name, '_default_manager') + # validate resulting default manager (only on non-abstract and non-swapped models) + if not new_class._meta.abstract and not new_class._meta.swapped: + if django.VERSION >= (1, 10): + self.validate_model_manager(new_class.objects, model_name, 'objects') + else: + self.validate_model_manager(new_class._default_manager, model_name, '_default_manager') # for __init__ function of this class (monkeypatching inheritance accessors) new_class.polymorphic_super_sub_accessors_replaced = False From 925c4de34fd1ace22e2606f42094b17a014096a9 Mon Sep 17 00:00:00 2001 From: Krzysztof Nazarewski Date: Wed, 5 Jul 2017 13:45:28 +0200 Subject: [PATCH 03/21] `declared_fieldsets` typo fix (cherry picked from commit 697df65c7b4d31f322ffcf777cdef3540db1eddd) --- polymorphic/admin/childadmin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polymorphic/admin/childadmin.py b/polymorphic/admin/childadmin.py index e492fed..0a197a8 100644 --- a/polymorphic/admin/childadmin.py +++ b/polymorphic/admin/childadmin.py @@ -169,7 +169,7 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): def get_fieldsets(self, request, obj=None): # If subclass declares fieldsets, this is respected - if (hasattr(self, 'declared_fieldset') and self.declared_fieldsets) \ + if (hasattr(self, 'declared_fieldsets') and self.declared_fieldsets) \ or not self.base_fieldsets: return super(PolymorphicChildModelAdmin, self).get_fieldsets(request, obj) From f126f5f304abf67ac7f160c9521dd720ed3beb8f Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Tue, 1 Aug 2017 11:38:36 +0200 Subject: [PATCH 04/21] Provide a better error message when polymorphic_ctype_id is Null refs #51, #140, #304 (cherry picked from commit fb8eed78ad604a0db7ae221de79a1b6ff6c88286) --- polymorphic/models.py | 11 +++++++++++ polymorphic/tests/test_orm.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/polymorphic/models.py b/polymorphic/models.py index f0af431..4759d0a 100644 --- a/polymorphic/models.py +++ b/polymorphic/models.py @@ -17,6 +17,10 @@ from .query_translate import translate_polymorphic_Q_object # PolymorphicModel +class PolymorphicTypeUndefined(LookupError): + pass + + class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)): """ Abstract base class that provides polymorphic behaviour @@ -82,6 +86,13 @@ class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)): retrieve objects, then the real class/type of these objects may be determined using this method. """ + if self.polymorphic_ctype_id is None: + raise PolymorphicTypeUndefined(( + "The model {}#{} does not have a `polymorphic_ctype_id` value defined.\n" + "If you created models outside polymorphic, e.g. through an import or migration, " + "make sure the `polymorphic_ctype_id` field points to the ContentType ID of the model subclass." + ).format(self.__class__.__name__, self.pk)) + # the following line would be the easiest way to do this, but it produces sql queries # return self.polymorphic_ctype.model_class() # so we use the following version, which uses the ContentType manager cache. diff --git a/polymorphic/tests/test_orm.py b/polymorphic/tests/test_orm.py index 3489783..76e24b7 100644 --- a/polymorphic/tests/test_orm.py +++ b/polymorphic/tests/test_orm.py @@ -5,6 +5,7 @@ from django.db.models import Case, Count, Q, When from django.test import TestCase from django.utils import six from polymorphic.contrib.guardian import get_polymorphic_base_content_type +from polymorphic.models import PolymorphicTypeUndefined from polymorphic.tests import * # all models @@ -783,6 +784,16 @@ class PolymorphicTests(TestCase): ctype = get_polymorphic_base_content_type(Model2D) self.assertEqual(ctype.name, 'model2a') + def test_null_polymorphic_id(self): + """Test that a proper error message is displayed when the database lacks the ``polymorphic_ctype_id``""" + Model2A.objects.create(field1='A1') + Model2B.objects.create(field1='A1', field2='B2') + Model2B.objects.create(field1='A1', field2='B2') + Model2A.objects.all().update(polymorphic_ctype_id=None) + + with self.assertRaises(PolymorphicTypeUndefined): + list(Model2A.objects.all()) + def qrepr(data): """ From db46dbb446a69761b7af58446739ec6322be3d52 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Tue, 1 Aug 2017 11:40:55 +0200 Subject: [PATCH 05/21] Also introduce a new exception type for invalid database IDs (cherry picked from commit 4835cd244ee371ed6f719c4c33925f92a54def16) --- polymorphic/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/polymorphic/models.py b/polymorphic/models.py index 4759d0a..3e4013b 100644 --- a/polymorphic/models.py +++ b/polymorphic/models.py @@ -21,6 +21,10 @@ class PolymorphicTypeUndefined(LookupError): pass +class PolymorphicTypeInvalid(RuntimeError): + pass + + class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)): """ Abstract base class that provides polymorphic behaviour @@ -105,7 +109,7 @@ class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)): if model is not None \ and not issubclass(model, self.__class__) \ and not issubclass(model, self.__class__._meta.proxy_for_model): - raise RuntimeError("ContentType {0} for {1} #{2} does not point to a subclass!".format( + raise PolymorphicTypeInvalid("ContentType {0} for {1} #{2} does not point to a subclass!".format( self.polymorphic_ctype_id, model, self.pk, )) return model From 467e6f517e53cbb8858fca6408d168693d045e7c Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Tue, 1 Aug 2017 12:11:42 +0200 Subject: [PATCH 06/21] Improve reset_polymorphic_ctype() for reliability and test it. This function can now be safely used on a set of models. (cherry picked from commit 171d14f36948ca7a625565621824b992eaff8178) --- polymorphic/tests/test_utils.py | 37 +++++++++++++++++++++++++++++++++ polymorphic/utils.py | 32 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 polymorphic/tests/test_utils.py diff --git a/polymorphic/tests/test_utils.py b/polymorphic/tests/test_utils.py new file mode 100644 index 0000000..ac56de9 --- /dev/null +++ b/polymorphic/tests/test_utils.py @@ -0,0 +1,37 @@ +from unittest import TestCase + +from polymorphic.models import PolymorphicTypeUndefined +from polymorphic.tests import Model2A, Model2B, Model2C, Model2D +from polymorphic.tests.test_orm import qrepr +from polymorphic.utils import sort_by_subclass, reset_polymorphic_ctype + + +class UtilsTests(TestCase): + maxDiff = 1000 + + def test_sort_by_subclass(self): + self.assertEqual( + sort_by_subclass(Model2D, Model2B, Model2D, Model2A, Model2C), + [Model2A, Model2B, Model2C, Model2D, Model2D] + ) + + def test_reset_polymorphic_ctype(self): + """ + Test the the polymorphic_ctype_id can be restored. + """ + Model2A.objects.create(field1='A1') + Model2D.objects.create(field1='A1', field2='B2', field3='C3', field4='D4') + Model2B.objects.create(field1='A1', field2='B2') + Model2B.objects.create(field1='A1', field2='B2') + Model2A.objects.all().update(polymorphic_ctype_id=None) + + with self.assertRaises(PolymorphicTypeUndefined): + list(Model2A.objects.all()) + + reset_polymorphic_ctype(Model2D, Model2B, Model2D, Model2A, Model2C) + self.assertEqual(repr(list(Model2A.objects.all())), ( + '[,' + ' ,' + ' ,' + ' ]' + )) diff --git a/polymorphic/utils.py b/polymorphic/utils.py index 8f1ac75..a8d968b 100644 --- a/polymorphic/utils.py +++ b/polymorphic/utils.py @@ -1,3 +1,5 @@ +import sys + from django.contrib.contenttypes.models import ContentType from django.db import DEFAULT_DB_ALIAS @@ -13,6 +15,8 @@ def reset_polymorphic_ctype(*models, **filters): """ using = filters.pop('using', DEFAULT_DB_ALIAS) ignore_existing = filters.pop('ignore_existing', False) + + models = sort_by_subclass(*models) if ignore_existing: # When excluding models, make sure we don't ignore the models we # just assigned the an content type to. hence, start with child first. @@ -27,3 +31,31 @@ def reset_polymorphic_ctype(*models, **filters): if filters: qs = qs.filter(**filters) qs.update(polymorphic_ctype=new_ct) + + +def _compare_mro(cls1, cls2): + if cls1 is cls2: + return 0 + + try: + index1 = cls1.mro().index(cls2) + except ValueError: + return -1 # cls2 not inherited by 1 + + try: + index2 = cls2.mro().index(cls1) + except ValueError: + return 1 # cls1 not inherited by 2 + + return (index1 > index2) - (index1 < index2) # python 3 compatible cmp. + + +def sort_by_subclass(*classes): + """ + Sort a series of models by their inheritance order. + """ + if sys.version_info[0] == 2: + return sorted(classes, cmp=_compare_mro) + else: + from functools import cmp_to_key + return sorted(classes, key=cmp_to_key(_compare_mro)) From 171df51428594e728e5d67738e5b54befe940dd6 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Tue, 1 Aug 2017 13:51:42 +0200 Subject: [PATCH 07/21] Ensure consistent ordering in testing reset_polymorphic_ctype Amends: 171d14f36948ca7a625565621824b992eaff8178 (cherry picked from commit 77797bfa12073dd56021ba23b2c5dfd8ea84a0d4) --- polymorphic/tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polymorphic/tests/test_utils.py b/polymorphic/tests/test_utils.py index ac56de9..ee3d6b1 100644 --- a/polymorphic/tests/test_utils.py +++ b/polymorphic/tests/test_utils.py @@ -29,7 +29,7 @@ class UtilsTests(TestCase): list(Model2A.objects.all()) reset_polymorphic_ctype(Model2D, Model2B, Model2D, Model2A, Model2C) - self.assertEqual(repr(list(Model2A.objects.all())), ( + self.assertEqual(repr(list(Model2A.objects.order_by('pk'))), ( '[,' ' ,' ' ,' From 1c110ae4bb0aba6354d829d84eb7e4e6d34c368d Mon Sep 17 00:00:00 2001 From: trbs Date: Wed, 30 Aug 2017 21:17:05 +0200 Subject: [PATCH 08/21] fix error with .defer and child models that use the same parent When using .defer on a PolymorphicQuerySet with multiple childs that subclass from the same polymorphic parent model yield an error like: >>> Base.objects.defer('ModelY___field_y') Traceback (most recent call last): ... FieldDoesNotExist: ModelX has no field named 'field_y' (cherry picked from commit 9500a21f82dd1b31f88d88e46bf8aa2411a701e9) --- polymorphic/query.py | 5 +++++ polymorphic/tests/__init__.py | 1 + polymorphic/tests/test_orm.py | 17 +++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/polymorphic/query.py b/polymorphic/query.py index 3d881d8..4430c2b 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -9,6 +9,7 @@ from collections import defaultdict import django from django.contrib.contenttypes.models import ContentType +from django.db.models import FieldDoesNotExist from django.db.models.query import Q, QuerySet from django.utils import six @@ -402,6 +403,10 @@ class PolymorphicQuerySet(QuerySet): # now a superclass of real_concrete_class. Thus it's # sufficient to just use the field name. translated_field_name = field.rpartition('___')[-1] + try: + real_concrete_class._meta.get_field(translated_field_name) + except FieldDoesNotExist: + continue else: raise diff --git a/polymorphic/tests/__init__.py b/polymorphic/tests/__init__.py index 32e1838..6fd2a2a 100644 --- a/polymorphic/tests/__init__.py +++ b/polymorphic/tests/__init__.py @@ -82,6 +82,7 @@ class ModelShow2_plain(ModelShow1_plain): class Base(ShowFieldType, PolymorphicModel): field_b = models.CharField(max_length=10) + polymorphic_showfield_deferred = True class ModelX(Base): diff --git a/polymorphic/tests/test_orm.py b/polymorphic/tests/test_orm.py index 76e24b7..bc30622 100644 --- a/polymorphic/tests/test_orm.py +++ b/polymorphic/tests/test_orm.py @@ -178,6 +178,23 @@ class PolymorphicTests(TestCase): '') + ModelX.objects.create(field_b="A1", field_x="A2") + ModelY.objects.create(field_b="B1", field_y="B2") + + objects_deferred = Base.objects.defer('ModelY___field_y') + self.assertEqual(repr(objects_deferred[0]), + '') + self.assertEqual(repr(objects_deferred[1]), + '') + + objects_only = Base.objects.only( + 'polymorphic_ctype', 'ModelY___field_y', 'ModelX___field_x', + ) + self.assertEqual(repr(objects_only[0]), + '') + self.assertEqual(repr(objects_only[1]), + '') + def test_defer_related_fields(self): self.create_model2abcd() From fa9612d49c2ff3430e737a46721ffd82f90d784c Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 16 Apr 2018 11:21:26 +0200 Subject: [PATCH 09/21] Small Django 1.8/19 difference for .only() --- polymorphic/tests/test_orm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/polymorphic/tests/test_orm.py b/polymorphic/tests/test_orm.py index bc30622..ced9a64 100644 --- a/polymorphic/tests/test_orm.py +++ b/polymorphic/tests/test_orm.py @@ -188,6 +188,7 @@ class PolymorphicTests(TestCase): '') objects_only = Base.objects.only( + 'pk', # required for Django 1.8 / 1.9 'polymorphic_ctype', 'ModelY___field_y', 'ModelX___field_x', ) self.assertEqual(repr(objects_only[0]), From 874b60ec4086591e95ed45605df870239dbefbef Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sat, 30 Sep 2017 16:21:21 +0200 Subject: [PATCH 10/21] Added `get_base_polymorphic_model()` to detect the common base class for a polymorphic model. (cherry picked from commit 04d4181e17cd9239b667ec24e45fec137cb4a40b) --- polymorphic/tests/test_utils.py | 42 ++++++++++++++++++++++++++++++--- polymorphic/utils.py | 15 ++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/polymorphic/tests/test_utils.py b/polymorphic/tests/test_utils.py index ee3d6b1..9a34772 100644 --- a/polymorphic/tests/test_utils.py +++ b/polymorphic/tests/test_utils.py @@ -1,9 +1,9 @@ from unittest import TestCase -from polymorphic.models import PolymorphicTypeUndefined -from polymorphic.tests import Model2A, Model2B, Model2C, Model2D +from polymorphic.models import PolymorphicTypeUndefined, PolymorphicModel +from polymorphic.tests import Model2A, Model2B, Model2C, Model2D, Enhance_Inherit, Enhance_Base from polymorphic.tests.test_orm import qrepr -from polymorphic.utils import sort_by_subclass, reset_polymorphic_ctype +from polymorphic.utils import reset_polymorphic_ctype, sort_by_subclass, get_base_polymorphic_model class UtilsTests(TestCase): @@ -35,3 +35,39 @@ class UtilsTests(TestCase): ' ,' ' ]' )) + + def test_get_base_polymorphic_model(self): + """ + Test that finding the base polymorphic model works. + """ + # Finds the base from every level (including lowest) + self.assertIs(get_base_polymorphic_model(Model2D), Model2A) + self.assertIs(get_base_polymorphic_model(Model2C), Model2A) + self.assertIs(get_base_polymorphic_model(Model2B), Model2A) + self.assertIs(get_base_polymorphic_model(Model2A), Model2A) + + # Properly handles multiple inheritance + self.assertIs(get_base_polymorphic_model(Enhance_Inherit), Enhance_Base) + + # Ignores PolymorphicModel itself. + self.assertIs(get_base_polymorphic_model(PolymorphicModel), None) + + def test_get_base_polymorphic_model_skip_abstract(self): + """ + Skipping abstract models that can't be used for querying. + """ + class A(PolymorphicModel): + class Meta: + abstract = True + + class B(A): + pass + + class C(B): + pass + + self.assertIs(get_base_polymorphic_model(A), None) + self.assertIs(get_base_polymorphic_model(B), B) + self.assertIs(get_base_polymorphic_model(C), B) + + self.assertIs(get_base_polymorphic_model(C, allow_abstract=True), A) diff --git a/polymorphic/utils.py b/polymorphic/utils.py index a8d968b..0bc0c41 100644 --- a/polymorphic/utils.py +++ b/polymorphic/utils.py @@ -2,6 +2,9 @@ import sys from django.contrib.contenttypes.models import ContentType from django.db import DEFAULT_DB_ALIAS +from polymorphic.models import PolymorphicModel + +from polymorphic.base import PolymorphicModelBase def reset_polymorphic_ctype(*models, **filters): @@ -59,3 +62,15 @@ def sort_by_subclass(*classes): else: from functools import cmp_to_key return sorted(classes, key=cmp_to_key(_compare_mro)) + + +def get_base_polymorphic_model(ChildModel, allow_abstract=False): + """ + First the first concrete model in the inheritance chain that inherited from the PolymorphicModel. + """ + for Model in reversed(ChildModel.mro()): + if isinstance(Model, PolymorphicModelBase) and \ + Model is not PolymorphicModel and \ + (allow_abstract or not Model._meta.abstract): + return Model + return None From fe7de17ecfbb3a317ecac85f986796cbe912459e Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sat, 30 Sep 2017 16:35:02 +0200 Subject: [PATCH 11/21] Make the admin `base_model` setting optional. It can be detected using get_base_polymorphic_model() (cherry picked from commit cf0cb2478f8f644f0bd0ffc3652fb9af871bf755) --- docs/admin.rst | 8 ++++---- docs/third-party.rst | 4 ++-- example/pexp/admin.py | 10 ++++------ polymorphic/admin/childadmin.py | 9 +++++++-- polymorphic/admin/parentadmin.py | 9 ++++++--- 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/docs/admin.rst b/docs/admin.rst index 39fc68f..caea29b 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -63,7 +63,7 @@ The models are taken from :ref:`advanced-features`. class ModelAChildAdmin(PolymorphicChildModelAdmin): """ Base admin class for all child models """ - base_model = ModelA + base_model = ModelA # Optional, explicitly set here. # By using these `base_...` attributes instead of the regular ModelAdmin `form` and `fieldsets`, # the additional fields of the child models are automatically added to the admin form. @@ -75,13 +75,13 @@ The models are taken from :ref:`advanced-features`. @admin.register(ModelB) class ModelBAdmin(ModelAChildAdmin): - base_model = ModelB + base_model = ModelB # Explicitly set here! # define custom features here @admin.register(ModelC) class ModelCAdmin(ModelBAdmin): - base_model = ModelC + base_model = ModelC # Explicitly set here! show_in_index = True # makes child model admin visible in main admin site # define custom features here @@ -89,7 +89,7 @@ The models are taken from :ref:`advanced-features`. @admin.register(ModelA) class ModelAParentAdmin(PolymorphicParentModelAdmin): """ The parent model admin """ - base_model = ModelA + base_model = ModelA # Optional, explicitly set here. child_models = (ModelB, ModelC) list_filter = (PolymorphicChildModelFilter,) # This is optional. diff --git a/docs/third-party.rst b/docs/third-party.rst index f1d412a..50105f9 100644 --- a/docs/third-party.rst +++ b/docs/third-party.rst @@ -69,7 +69,7 @@ The admin :ref:`admin example ` becomes: class ModelAChildAdmin(PolymorphicChildModelAdmin, VersionAdmin): - base_model = ModelA + base_model = ModelA # optional, explicitly set here. base_form = ... base_fieldsets = ( ... @@ -83,7 +83,7 @@ The admin :ref:`admin example ` becomes: class ModelAParentAdmin(VersionAdmin, PolymorphicParentModelAdmin): - base_model = ModelA + base_model = ModelA # optional, explicitly set here. child_models = ( (ModelB, ModelBAdmin), (ModelC, ModelCAdmin), diff --git a/example/pexp/admin.py b/example/pexp/admin.py index d359c31..a04ec8b 100644 --- a/example/pexp/admin.py +++ b/example/pexp/admin.py @@ -4,13 +4,13 @@ from pexp.models import * class ProjectAdmin(PolymorphicParentModelAdmin): - base_model = Project + base_model = Project # Can be set explicitly. list_filter = (PolymorphicChildModelFilter,) child_models = (Project, ArtProject, ResearchProject) class ProjectChildAdmin(PolymorphicChildModelAdmin): - base_model = Project + base_model = Project # Can be set explicitly. # On purpose, only have the shared fields here. # The fields of the derived model should still be displayed. @@ -27,13 +27,12 @@ admin.site.register(ResearchProject, ProjectChildAdmin) class UUIDModelAAdmin(PolymorphicParentModelAdmin): - base_model = UUIDModelA list_filter = (PolymorphicChildModelFilter,) child_models = (UUIDModelA, UUIDModelB) class UUIDModelAChildAdmin(PolymorphicChildModelAdmin): - base_model = UUIDModelA + pass admin.site.register(UUIDModelA, UUIDModelAAdmin) @@ -42,13 +41,12 @@ admin.site.register(UUIDModelC, UUIDModelAChildAdmin) class ProxyAdmin(PolymorphicParentModelAdmin): - base_model = ProxyBase list_filter = (PolymorphicChildModelFilter,) child_models = (ProxyA, ProxyB) class ProxyChildAdmin(PolymorphicChildModelAdmin): - base_model = ProxyBase + pass admin.site.register(ProxyBase, ProxyAdmin) diff --git a/polymorphic/admin/childadmin.py b/polymorphic/admin/childadmin.py index 0a197a8..6132bf4 100644 --- a/polymorphic/admin/childadmin.py +++ b/polymorphic/admin/childadmin.py @@ -8,6 +8,7 @@ from django.core.urlresolvers import resolve from django.utils import six from django.utils.translation import ugettext_lazy as _ +from polymorphic.utils import get_base_polymorphic_model from ..admin import PolymorphicParentModelAdmin @@ -25,8 +26,6 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): * It adds the base model to the template lookup paths. * It allows to set ``base_form`` so the derived class will automatically include other fields in the form. * It allows to set ``base_fieldsets`` so the derived class will automatically display any extra fields. - - The ``base_model`` attribute must be set. """ base_model = None base_form = None @@ -34,6 +33,12 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): extra_fieldset_title = _("Contents") # Default title for extra fieldset show_in_index = False + def __init__(self, model, admin_site, *args, **kwargs): + super(PolymorphicChildModelAdmin, self).__init__(model, admin_site, *args, **kwargs) + + if self.base_model is None: + self.base_model = get_base_polymorphic_model(model) + def get_form(self, request, obj=None, **kwargs): # The django admin validation requires the form to have a 'class Meta: model = ..' # attribute, or it will complain that the fields are missing. diff --git a/polymorphic/admin/parentadmin.py b/polymorphic/admin/parentadmin.py index da7dffc..c9c00f8 100644 --- a/polymorphic/admin/parentadmin.py +++ b/polymorphic/admin/parentadmin.py @@ -19,6 +19,7 @@ from django.utils.http import urlencode from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ +from polymorphic.utils import get_base_polymorphic_model from .forms import PolymorphicModelChoiceForm @@ -37,9 +38,8 @@ class ChildAdminNotRegistered(RuntimeError): class PolymorphicParentModelAdmin(admin.ModelAdmin): """ A admin interface that can displays different change/delete pages, depending on the polymorphic model. - To use this class, two variables need to be defined: + To use this class, one attribute need to be defined: - * :attr:`base_model` should * :attr:`child_models` should be a list of (Model, Admin) tuples Alternatively, the following methods can be implemented: @@ -51,7 +51,7 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_child_models`. """ - #: The base model that the class uses + #: The base model that the class uses (auto-detected if not set explicitly) base_model = None #: The child models that should be displayed @@ -72,6 +72,9 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): super(PolymorphicParentModelAdmin, self).__init__(model, admin_site, *args, **kwargs) self._is_setup = False + if self.base_model is None: + self.base_model = get_base_polymorphic_model(model) + def _lazy_setup(self): if self._is_setup: return From 7de1366f6fe58e833a2fb72f78a71d0e4a6a152e Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sat, 30 Sep 2017 16:35:21 +0200 Subject: [PATCH 12/21] Improve docs of PolymorphicChildModelAdmin attributes (cherry picked from commit c4375248768263024d8d44fcaad9c6bfb181fdc3) --- polymorphic/admin/childadmin.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/polymorphic/admin/childadmin.py b/polymorphic/admin/childadmin.py index 6132bf4..cad47d6 100644 --- a/polymorphic/admin/childadmin.py +++ b/polymorphic/admin/childadmin.py @@ -27,10 +27,23 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): * It allows to set ``base_form`` so the derived class will automatically include other fields in the form. * It allows to set ``base_fieldsets`` so the derived class will automatically display any extra fields. """ + + #: The base model that the class uses (auto-detected if not set explicitly) base_model = None + + #: By setting ``base_form`` instead of ``form``, any subclass fields are automatically added to the form. + #: This is useful when your model admin class is inherited by others. base_form = None + + #: By setting ``base_fieldsets`` instead of ``fieldsets``, + #: any subclass fields can be automatically added. + #: This is useful when your model admin class is inherited by others. base_fieldsets = None - extra_fieldset_title = _("Contents") # Default title for extra fieldset + + #: Default title for extra fieldset + extra_fieldset_title = _("Contents") + + #: Whether the child admin model should be visible in the admin index page. show_in_index = False def __init__(self, model, admin_site, *args, **kwargs): From e00e6d7e39329c213fc98ffca32ec248649d4499 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sat, 30 Sep 2017 16:46:26 +0200 Subject: [PATCH 13/21] Fix deleteText of |as_script_options (cherry picked from commit 9f6e0716f668102dffc6796af112efcdc24bd6cc) --- polymorphic/templatetags/polymorphic_formset_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polymorphic/templatetags/polymorphic_formset_tags.py b/polymorphic/templatetags/polymorphic_formset_tags.py index 575299c..ec0375a 100644 --- a/polymorphic/templatetags/polymorphic_formset_tags.py +++ b/polymorphic/templatetags/polymorphic_formset_tags.py @@ -48,7 +48,7 @@ def as_script_options(formset): 'verbose_name': capfirst(verbose_name), }, 'showAddButton': getattr(formset, 'show_add_button', True), - 'deleteText': ugettext('Verwijder'), + 'deleteText': ugettext('Delete'), } if isinstance(formset, BasePolymorphicModelFormSet): From 5c0b561ba2fd6401435f6743b8f2f52273ae4316 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sun, 8 Oct 2017 21:53:12 +0200 Subject: [PATCH 14/21] Add UnsupportedChildType error for formsets This also replaces the PolymorphicInlineModelAdmin.get_get_child_inline_instance() and BasePolymorphicModelFormSet._construct_form() lookup with UnsupportedChildType (cherry picked from commit cafaf95f0650a38465711ab5bf7ff6f411c809e7) --- polymorphic/admin/inlines.py | 5 +++-- polymorphic/formsets/__init__.py | 2 ++ polymorphic/formsets/models.py | 22 ++++++++++++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/polymorphic/admin/inlines.py b/polymorphic/admin/inlines.py index deee7bb..bb25350 100644 --- a/polymorphic/admin/inlines.py +++ b/polymorphic/admin/inlines.py @@ -10,7 +10,8 @@ from django.contrib.admin.utils import flatten_fieldsets from django.core.exceptions import ImproperlyConfigured from django.forms import Media -from polymorphic.formsets import polymorphic_child_forms_factory, BasePolymorphicInlineFormSet, PolymorphicFormSetChild +from polymorphic.formsets import polymorphic_child_forms_factory, BasePolymorphicInlineFormSet, \ + PolymorphicFormSetChild, UnsupportedChildType from polymorphic.formsets.utils import add_media from .helpers import PolymorphicInlineSupportMixin @@ -89,7 +90,7 @@ class PolymorphicInlineModelAdmin(InlineModelAdmin): try: return self._child_inlines_lookup[model] except KeyError: - raise ValueError("Model '{0}' not found in child_inlines".format(model.__name__)) + raise UnsupportedChildType("Model '{0}' not found in child_inlines".format(model.__name__)) def get_formset(self, request, obj=None, **kwargs): """ diff --git a/polymorphic/formsets/__init__.py b/polymorphic/formsets/__init__.py index 02c47ae..1f3b335 100644 --- a/polymorphic/formsets/__init__.py +++ b/polymorphic/formsets/__init__.py @@ -12,6 +12,7 @@ from .models import ( BasePolymorphicModelFormSet, BasePolymorphicInlineFormSet, PolymorphicFormSetChild, + UnsupportedChildType, polymorphic_modelformset_factory, polymorphic_inlineformset_factory, polymorphic_child_forms_factory, @@ -27,6 +28,7 @@ __all__ = ( 'BasePolymorphicModelFormSet', 'BasePolymorphicInlineFormSet', 'PolymorphicFormSetChild', + 'UnsupportedChildType', 'polymorphic_modelformset_factory', 'polymorphic_inlineformset_factory', 'polymorphic_child_forms_factory', diff --git a/polymorphic/formsets/models.py b/polymorphic/formsets/models.py index b13adf8..9bac2b1 100644 --- a/polymorphic/formsets/models.py +++ b/polymorphic/formsets/models.py @@ -6,9 +6,16 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured, ValidationError from django.forms.models import ModelForm, BaseModelFormSet, BaseInlineFormSet, modelform_factory, modelformset_factory, inlineformset_factory from django.utils.functional import cached_property + +from polymorphic.models import PolymorphicModel from .utils import add_media +class UnsupportedChildType(LookupError): + pass + + + class PolymorphicFormSetChild(object): """ Metadata to define the inline of a polymorphic child. @@ -168,7 +175,7 @@ class BasePolymorphicModelFormSet(BaseModelFormSet): model = ContentType.objects.get_for_id(ct_id).model_class() if model not in self.child_forms: # Perform basic validation, as we skip the ChoiceField here. - raise ValidationError("Child model type {0} is not part of the formset".format(model)) + raise UnsupportedChildType("Child model type {0} is not part of the formset".format(model)) else: if 'instance' in defaults: model = defaults['instance'].get_real_concrete_instance_class() # respect proxy models @@ -201,7 +208,18 @@ class BasePolymorphicModelFormSet(BaseModelFormSet): """ if not self.child_forms: raise ImproperlyConfigured("No 'child_forms' defined in {0}".format(self.__class__.__name__)) - return self.child_forms[model] + if not issubclass(model, PolymorphicModel): + raise TypeError("Expect polymorphic model type, not {0}".format(model)) + + try: + return self.child_forms[model] + except KeyError: + # This may happen when the query returns objects of a type that was not handled by the formset. + raise UnsupportedChildType( + "The '{0}' found a '{1}' model in the queryset, " + "but no form class is registered to display it.".format( + self.__class__.__name__, model.__name__ + )) def is_multipart(self): """ From 551e93c47c09b013ba16c3b11d3626db960a6388 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sun, 8 Oct 2017 22:21:44 +0200 Subject: [PATCH 15/21] Fix support for proxy models in formsets and admin inlines (cherry picked from commit c2768f810157e61e50f4d4c6a3fc6d2835290f92) --- polymorphic/admin/generic.py | 2 +- polymorphic/admin/helpers.py | 2 +- polymorphic/formsets/models.py | 9 ++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/polymorphic/admin/generic.py b/polymorphic/admin/generic.py index bd3453f..91fc082 100644 --- a/polymorphic/admin/generic.py +++ b/polymorphic/admin/generic.py @@ -42,7 +42,7 @@ class GenericPolymorphicInlineModelAdmin(PolymorphicInlineModelAdmin, GenericInl Expose the ContentType that the child relates to. This can be used for the ``polymorphic_ctype`` field. """ - return ContentType.objects.get_for_model(self.model) + return ContentType.objects.get_for_model(self.model, for_concrete_model=False) def get_formset_child(self, request, obj=None, **kwargs): # Similar to GenericInlineModelAdmin.get_formset(), diff --git a/polymorphic/admin/helpers.py b/polymorphic/admin/helpers.py index d7fdef6..52d70ab 100644 --- a/polymorphic/admin/helpers.py +++ b/polymorphic/admin/helpers.py @@ -42,7 +42,7 @@ class PolymorphicInlineAdminFormSet(InlineAdminFormSet): """ for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()): # Output the form - model = original.get_real_concrete_instance_class() + model = original.get_real_instance_class() child_inline = self.opts.get_child_inline_instance(model) view_on_site_url = self.opts.get_view_on_site_url(original) diff --git a/polymorphic/formsets/models.py b/polymorphic/formsets/models.py index 9bac2b1..9c193bd 100644 --- a/polymorphic/formsets/models.py +++ b/polymorphic/formsets/models.py @@ -15,7 +15,6 @@ class UnsupportedChildType(LookupError): pass - class PolymorphicFormSetChild(object): """ Metadata to define the inline of a polymorphic child. @@ -47,7 +46,7 @@ class PolymorphicFormSetChild(object): Expose the ContentType that the child relates to. This can be used for the ``polymorphic_ctype`` field. """ - return ContentType.objects.get_for_model(self.model) + return ContentType.objects.get_for_model(self.model, for_concrete_model=False) def get_form(self, **kwargs): """ @@ -162,7 +161,7 @@ class BasePolymorphicModelFormSet(BaseModelFormSet): if self.is_bound: if 'instance' in defaults: # Object is already bound to a model, won't change the content type - model = defaults['instance'].get_real_concrete_instance_class() # respect proxy models + model = defaults['instance'].get_real_instance_class() # allow proxy models else: # Extra or empty form, use the provided type. # Note this completely tru @@ -178,7 +177,7 @@ class BasePolymorphicModelFormSet(BaseModelFormSet): raise UnsupportedChildType("Child model type {0} is not part of the formset".format(model)) else: if 'instance' in defaults: - model = defaults['instance'].get_real_concrete_instance_class() # respect proxy models + model = defaults['instance'].get_real_instance_class() # allow proxy models elif 'polymorphic_ctype' in defaults.get('initial', {}): model = defaults['initial']['polymorphic_ctype'].model_class() elif i < len(self.queryset_data): @@ -197,7 +196,7 @@ class BasePolymorphicModelFormSet(BaseModelFormSet): def add_fields(self, form, index): """Add a hidden field for the content type.""" - ct = ContentType.objects.get_for_model(form._meta.model) + ct = ContentType.objects.get_for_model(form._meta.model, for_concrete_model=False) choices = [(ct.pk, ct)] # Single choice, existing forms can't change the value. form.fields['polymorphic_ctype'] = forms.ChoiceField(choices=choices, initial=ct.pk, required=False, widget=forms.HiddenInput) super(BasePolymorphicModelFormSet, self).add_fields(form, index) From ca1a982c34a9b7e062b825145e480f8698862a9e Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sun, 8 Oct 2017 22:42:56 +0200 Subject: [PATCH 16/21] Make sure reset_polymorphic_ctype() supports proxy models (cherry picked from commit 04b5fb423cc08878eca0aeff6c7d2e509907dcf0) --- polymorphic/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polymorphic/utils.py b/polymorphic/utils.py index 0bc0c41..956bc46 100644 --- a/polymorphic/utils.py +++ b/polymorphic/utils.py @@ -26,7 +26,7 @@ def reset_polymorphic_ctype(*models, **filters): models = reversed(models) for new_model in models: - new_ct = ContentType.objects.db_manager(using).get_for_model(new_model) + new_ct = ContentType.objects.db_manager(using).get_for_model(new_model, for_concrete_model=False) qs = new_model.objects.db_manager(using) if ignore_existing: From 9f92b0614dc7c40a5ff8458c888d7b67d1f6e10c Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 20 Nov 2017 15:18:43 +0100 Subject: [PATCH 17/21] Fixed applabel__ModelName___field looksups Closes: #286 (cherry picked from commit 8f0932b71e03937668c781ddc27a159f4182cc82) --- polymorphic/query_translate.py | 4 +++- polymorphic/tests/test_orm.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/polymorphic/query_translate.py b/polymorphic/query_translate.py index c7d7310..136ecd8 100644 --- a/polymorphic/query_translate.py +++ b/polymorphic/query_translate.py @@ -7,6 +7,8 @@ from __future__ import absolute_import import copy import django from functools import reduce + +from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models.fields.related import ForeignObjectRel, RelatedField @@ -148,7 +150,7 @@ def translate_polymorphic_field_path(queryset_model, field_path): if '__' in classname: # the user has app label prepended to class name via __ => use Django's get_model function appname, sep, classname = classname.partition('__') - model = models.get_model(appname, classname) + model = apps.get_model(appname, classname) assert model, 'PolymorphicModel: model %s (in app %s) not found!' % (model.__name__, appname) if not issubclass(model, queryset_model): e = 'PolymorphicModel: queryset filter error: "' + model.__name__ + '" is not derived from "' + queryset_model.__name__ + '"' diff --git a/polymorphic/tests/test_orm.py b/polymorphic/tests/test_orm.py index ced9a64..682ff65 100644 --- a/polymorphic/tests/test_orm.py +++ b/polymorphic/tests/test_orm.py @@ -427,6 +427,18 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(objects[0]), '') self.assertEqual(repr(objects[1]), '') + def test_polymorphic_applabel___filter(self): + self.create_model2abcd() + + self.assertEqual(Model2B._meta.app_label, 'polymorphic') + objects = Model2A.objects.filter(Q(polymorphic__Model2B___field2='B2') | Q(polymorphic__Model2C___field3='C3')) + self.assertQuerysetEqual( + objects, + [Model2B, Model2C], + transform=lambda o: o.__class__, + ordered=False, + ) + def test_query_filter_exclude_is_immutable(self): # given q_to_reuse = Q(Model2B___field2='something') From d1c58459665e74e3d99ff815d07124948e059af4 Mon Sep 17 00:00:00 2001 From: Tadas Dailyda Date: Mon, 20 Nov 2017 16:15:10 +0200 Subject: [PATCH 18/21] further childadmin fieldsets improvements (cherry picked from commit 15426dc695f67a06ce46539b3f44208057bb67a5) --- polymorphic/admin/childadmin.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/polymorphic/admin/childadmin.py b/polymorphic/admin/childadmin.py index cad47d6..be1b022 100644 --- a/polymorphic/admin/childadmin.py +++ b/polymorphic/admin/childadmin.py @@ -62,8 +62,8 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): # If the derived class sets the model explicitly, respect that setting. kwargs.setdefault('form', self.base_form or self.form) - # prevent infinite recursion in django 1.6+ - if not getattr(self, 'declared_fieldsets', None): + # prevent infinite recursion when this is called from get_subclass_fields + if not hasattr(self, 'fieldsets') and not hasattr(self, 'fields'): kwargs.setdefault('fields', None) return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs) @@ -185,10 +185,14 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): # ---- Extra: improving the form/fieldset default display ---- + def get_base_fieldsets(self, request, obj=None): + return self.base_fieldsets + def get_fieldsets(self, request, obj=None): - # If subclass declares fieldsets, this is respected - if (hasattr(self, 'declared_fieldsets') and self.declared_fieldsets) \ - or not self.base_fieldsets: + base_fieldsets = self.get_base_fieldsets(request, obj) + + # If subclass declares fieldsets or fields, this is respected + if self.fieldsets or self.fields or not self.base_fieldsets: return super(PolymorphicChildModelAdmin, self).get_fieldsets(request, obj) # Have a reasonable default fieldsets, @@ -197,11 +201,11 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): if other_fields: return ( - self.base_fieldsets[0], + base_fieldsets[0], (self.extra_fieldset_title, {'fields': other_fields}), - ) + self.base_fieldsets[1:] + ) + base_fieldsets[1:] else: - return self.base_fieldsets + return base_fieldsets def get_subclass_fields(self, request, obj=None): # Find out how many fields would really be on the form, @@ -215,7 +219,7 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): subclass_fields = list(six.iterkeys(form.base_fields)) + list(self.get_readonly_fields(request, obj)) # Find which fields are not part of the common fields. - for fieldset in self.base_fieldsets: + for fieldset in self.get_base_fieldsets(request, obj): for field in fieldset[1]['fields']: try: subclass_fields.remove(field) From 5fe504d40da2a2b213c025b6e04d0cc98bd76ade Mon Sep 17 00:00:00 2001 From: Tadas Dailyda Date: Mon, 20 Nov 2017 17:34:05 +0200 Subject: [PATCH 19/21] fix detecting whether childadmin has explicitly defined fields/fieldsets (cherry picked from commit 180df8e98c84df02c06412adb0c8c29a44ba9330) --- polymorphic/admin/childadmin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/polymorphic/admin/childadmin.py b/polymorphic/admin/childadmin.py index be1b022..726decf 100644 --- a/polymorphic/admin/childadmin.py +++ b/polymorphic/admin/childadmin.py @@ -63,8 +63,8 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): kwargs.setdefault('form', self.base_form or self.form) # prevent infinite recursion when this is called from get_subclass_fields - if not hasattr(self, 'fieldsets') and not hasattr(self, 'fields'): - kwargs.setdefault('fields', None) + if not self.fieldsets and not self.fields: + kwargs.setdefault('fields', '__all__') return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs) From 8f7f255580a4e54b72a450a42d2fdabe48dd591a Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sat, 30 Sep 2017 16:33:52 +0200 Subject: [PATCH 20/21] Fixed template settings in example app (cherry picked from commit 19497960c7ac4a082ea5e89024433d3254db8182) --- example/example/settings.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/example/example/settings.py b/example/example/settings.py index abb389b..1514227 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -1,7 +1,6 @@ import os DEBUG = True -TEMPLATE_DEBUG = DEBUG ADMINS = ( # ('Your Name', 'your_email@example.com'), @@ -42,11 +41,6 @@ STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.AppDirectoriesFinder', ) -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -) - MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -55,12 +49,30 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.messages.middleware.MessageMiddleware', ) +TEMPLATES=[{ + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": (), + "OPTIONS": { + "loaders": ( + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", + ), + "context_processors": ( + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.request", + "django.template.context_processors.static", + "django.contrib.messages.context_processors.messages", + "django.contrib.auth.context_processors.auth", + ), + }, +}] + ROOT_URLCONF = 'example.urls' WSGI_APPLICATION = 'example.wsgi.application' -TEMPLATE_DIRS = () - INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.admin', From 31dd8b6138a49fd6be7364a9734e6c1a73c59543 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 16 Apr 2018 11:33:14 +0200 Subject: [PATCH 21/21] Release v1.3.1 --- docs/changelog.rst | 22 ++++++++++++++++++++++ docs/conf.py | 4 ++-- polymorphic/__init__.py | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0236d66..5fa72a7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,28 @@ Changelog ========= +Version 1.3.1 (2018-04-16) +-------------------------- + +Backported various fixes from 2.x to support older Django versions: + +* Added ``PolymorphicTypeUndefined`` exception for incomplete imported models. + When a data migration or import creates an polymorphic model, + the ``polymorphic_ctype_id`` field should be filled in manually too. + The ``polymorphic.utils.reset_polymorphic_ctype`` function can be used for that. +* Added ``PolymorphicTypeInvalid`` exception when database was incorrectly imported. +* Added ``polymorphic.utils.get_base_polymorphic_model()`` to find the base model for types. +* Using ``base_model`` on the polymorphic admins is no longer required, as this can be autodetected. +* Fixed manager errors for swappable models. +* Fixed ``deleteText`` of ``|as_script_options`` template filter. +* Fixed ``.filter(applabel__ModelName___field=...)`` lookups. +* Fixed proxy model support in formsets. +* Fixed error with .defer and child models that use the same parent. +* Fixed error message when ``polymorphic_ctype_id`` is null. +* Fixed fieldsets recursion in the admin. +* Improved ``polymorphic.utils.reset_polymorphic_ctype()`` to accept models in random ordering. + + Version 1.3 (2017-08-01) ------------------------ diff --git a/docs/conf.py b/docs/conf.py index b9b053f..468deab 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,9 +61,9 @@ copyright = u'2013, Bert Constantin, Chris Glass, Diederik van der Boor' # built documents. # # The short X.Y version. -version = '1.3' +version = '1.3.1' # The full version, including alpha/beta/rc tags. -release = '1.3' +release = '1.3.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/polymorphic/__init__.py b/polymorphic/__init__.py index 77d7fe2..714ff51 100644 --- a/polymorphic/__init__.py +++ b/polymorphic/__init__.py @@ -8,4 +8,4 @@ Please see LICENSE and AUTHORS for more information. """ # See PEP 440 (https://www.python.org/dev/peps/pep-0440/) -__version__ = "1.3" +__version__ = "1.3.1"