diff --git a/.gitignore b/.gitignore
index bab3b71..c18fed3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@ dist/
docs/_build/
htmlcov/
venv/
+.venv/
diff --git a/.travis.yml b/.travis.yml
index c320375..42a2019 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,3 @@
-# https://travis-ci.org/django-polymorphic/django-polymorphic
-dist: xenial
-sudo: false
language: python
services:
@@ -11,32 +8,38 @@ addons:
matrix:
fast_finish: true
include:
- # Django 1.11: Python 2.7, 3.5, or 3.6
- - { env: TOXENV=py27-django111, python: 2.7 }
- - { env: TOXENV=py35-django111, python: 3.5 }
- - { env: TOXENV=py36-django111, python: 3.6 }
- - { env: TOXENV=py36-django111-postgres DB=postgres, python: 3.6 }
- # Django 2.0: Python 3.5, or 3.6
- - { env: TOXENV=py35-django20, python: 3.5 }
- - { env: TOXENV=py36-django20, python: 3.6 }
- - { env: TOXENV=py36-django20-postgres DB=postgres, python: 3.6 }
- # Django 2.1: Python 3.6, or 3.7
+ # Django 2.1: Python 3.5, 3.6, or 3.7
+ - { env: TOXENV=py35-django21, python: 3.5 }
- { env: TOXENV=py36-django21, python: 3.6 }
- { env: TOXENV=py37-django21, python: 3.7 }
- { env: TOXENV=py37-django21-postgres DB=postgres, python: 3.7 }
- # Django 2.2: Python 3.6, or 3.7
+ # Django 2.2: Python 3.5, 3.6, 3.7 or 3.8
+ - { env: TOXENV=py35-django22, python: 3.5 }
- { env: TOXENV=py36-django22, python: 3.6 }
- { env: TOXENV=py37-django22, python: 3.7 }
- - { env: TOXENV=py37-django22-postgres DB=postgres, python: 3.7 }
+ - { env: TOXENV=py38-django22, python: 3.8 }
+ - { env: TOXENV=py38-django22-postgres DB=postgres, python: 3.8 }
+ # Django 3.0: Python 3.6, 3.7 or 3.8
+ - { env: TOXENV=py36-django30, python: 3.6 }
+ - { env: TOXENV=py37-django30, python: 3.7 }
+ - { env: TOXENV=py38-django30, python: 3.8 }
+ - { env: TOXENV=py38-django30-postgres DB=postgres, python: 3.8 }
+ # Django 3.1: Python 3.6, 3.7 or 3.8
+ - { env: TOXENV=py36-django31, python: 3.6 }
+ - { env: TOXENV=py37-django31, python: 3.7 }
+ - { env: TOXENV=py38-django31, python: 3.8 }
+ - { env: TOXENV=py38-django31-postgres DB=postgres, python: 3.8 }
# Django development master (direct from GitHub source):
- { env: TOXENV=py36-djangomaster, python: 3.6 }
- { env: TOXENV=py37-djangomaster, python: 3.7 }
- - { env: TOXENV=py37-djangomaster-postgres DB=postgres, python: 3.7 }
+ - { env: TOXENV=py38-djangomaster, python: 3.8 }
+ - { env: TOXENV=py38-djangomaster-postgres DB=postgres, python: 3.8 }
allow_failures:
- env: TOXENV=py36-djangomaster
- env: TOXENV=py37-djangomaster
- - env: TOXENV=py37-djangomaster-postgres DB=postgres
+ - env: TOXENV=py38-djangomaster
+ - env: TOXENV=py38-djangomaster-postgres DB=postgres
cache:
directories:
@@ -57,7 +60,3 @@ script:
after_success:
- coverage xml -i
- codecov
-
-branches:
- only:
- - master
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 81590be..0d7efd1 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -13,12 +13,14 @@ Contributors
* Abel Daniel
* Adam Chainz
* Adam Wentz
+* Adam Donaghy
* Andrew Ingram (contributed setup.py)
* Al Johri
* Alex Alvarez
* Andrew Dodd
* Angel Velasquez
* Austin Matsick
+* Bastien Vallet
* Ben Konrath
* Bert Constantin
* Bertrand Bordage
diff --git a/README.rst b/README.rst
index 76a95b9..15d49be 100644
--- a/README.rst
+++ b/README.rst
@@ -62,7 +62,8 @@ Django to perform an ``INNER JOIN`` to fetch the model fields from the database.
While taking this in mind, there are valid reasons for using subclassed models.
That's what this library is designed for!
-The current release of *django-polymorphic* supports Django 1.11, 2.0, 2.1, 2.2 and Python 2.7 and 3.5+ is supported.
+The current release of *django-polymorphic* supports Django 2.1, 2.2, 3.0, 3.1
+and Python 3.5+ is supported.
For older Django versions, install *django-polymorphic==1.3*.
For more information, see the `documentation at Read the Docs `_.
diff --git a/docs/_ext/djangodummy/requirements.txt b/docs/_ext/djangodummy/requirements.txt
index 62f1e49..de7dbd2 100644
--- a/docs/_ext/djangodummy/requirements.txt
+++ b/docs/_ext/djangodummy/requirements.txt
@@ -1,5 +1,5 @@
# for readthedocs
# Remaining requirements are picked up from setup.py
-Django == 2.2.3
+Django == 2.2.13
django-extra-views == 0.12.0
sphinxcontrib-django == 0.4
diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 426c0d4..f086aad 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,7 +1,14 @@
Changelog
=========
-Changes in 2.1.2 (2019-17-15)
+Changes in 3.0.0 (2020-08-21)
+-----------------------------
+
+* Support for Django 3.X
+* Dropped support for python 2.X
+* A lot of various fixes and improvements by various authors. Thanks a lot!
+
+Changes in 2.1.2 (2019-07-15)
-----------------------------
* Fix ``PolymorphicInlineModelAdmin`` media jQuery include for Django 2.0+
diff --git a/docs/migrating.rst b/docs/migrating.rst
index 866da3b..2db2355 100644
--- a/docs/migrating.rst
+++ b/docs/migrating.rst
@@ -29,7 +29,6 @@ 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
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index c40cb06..c659fbd 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.11, 2.0 and Python 2.7 and 3.5+ is supported.
+The current release of *django-polymorphic* supports Django 1.11, 2.0, 2.1, 2.2 and Python 2.7 and 3.5+ is supported.
For older Django versions, use *django-polymorphic==1.3*.
Making Your Models Polymorphic
diff --git a/example/orders/models.py b/example/orders/models.py
index cf12286..97ec22e 100644
--- a/example/orders/models.py
+++ b/example/orders/models.py
@@ -1,12 +1,10 @@
from django.db import models
from django.utils.dates import MONTHS_3
-from django.utils.six import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from polymorphic.models import PolymorphicModel
-@python_2_unicode_compatible
class Order(models.Model):
"""
An example order that has polymorphic relations
@@ -23,7 +21,6 @@ class Order(models.Model):
return self.title
-@python_2_unicode_compatible
class Payment(PolymorphicModel):
"""
A generic payment model.
diff --git a/polymorphic/admin/childadmin.py b/polymorphic/admin/childadmin.py
index db30c9f..98d7d39 100644
--- a/polymorphic/admin/childadmin.py
+++ b/polymorphic/admin/childadmin.py
@@ -5,7 +5,7 @@ import inspect
from django.contrib import admin
from django.urls import resolve
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from polymorphic.utils import get_base_polymorphic_model
diff --git a/polymorphic/admin/filters.py b/polymorphic/admin/filters.py
index 171599b..37efc7f 100644
--- a/polymorphic/admin/filters.py
+++ b/polymorphic/admin/filters.py
@@ -1,6 +1,6 @@
from django.contrib import admin
from django.core.exceptions import PermissionDenied
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
class PolymorphicChildModelFilter(admin.SimpleListFilter):
diff --git a/polymorphic/admin/forms.py b/polymorphic/admin/forms.py
index 08e950f..8a705f0 100644
--- a/polymorphic/admin/forms.py
+++ b/polymorphic/admin/forms.py
@@ -1,6 +1,6 @@
from django import forms
from django.contrib.admin.widgets import AdminRadioSelect
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
class PolymorphicModelChoiceForm(forms.Form):
diff --git a/polymorphic/admin/helpers.py b/polymorphic/admin/helpers.py
index 122f467..130ef31 100644
--- a/polymorphic/admin/helpers.py
+++ b/polymorphic/admin/helpers.py
@@ -8,7 +8,7 @@ import json
from django.contrib.admin.helpers import AdminField, InlineAdminForm, InlineAdminFormSet
from django.utils.encoding import force_text
from django.utils.text import capfirst
-from django.utils.translation import ugettext
+from django.utils.translation import gettext
from polymorphic.formsets import BasePolymorphicModelFormSet
@@ -96,7 +96,7 @@ class PolymorphicInlineAdminFormSet(InlineAdminFormSet):
"name": "#%s" % self.formset.prefix,
"options": {
"prefix": self.formset.prefix,
- "addText": ugettext("Add another %(verbose_name)s")
+ "addText": gettext("Add another %(verbose_name)s")
% {"verbose_name": capfirst(verbose_name)},
"childTypes": [
{
@@ -105,7 +105,7 @@ class PolymorphicInlineAdminFormSet(InlineAdminFormSet):
}
for model in self.formset.child_forms.keys()
],
- "deleteText": ugettext("Remove"),
+ "deleteText": gettext("Remove"),
},
}
)
diff --git a/polymorphic/admin/parentadmin.py b/polymorphic/admin/parentadmin.py
index c9f7598..f56bde8 100644
--- a/polymorphic/admin/parentadmin.py
+++ b/polymorphic/admin/parentadmin.py
@@ -1,8 +1,6 @@
"""
The parent admin displays the list view of the base model.
"""
-import sys
-
from django.contrib import admin
from django.contrib.admin.helpers import AdminErrorList, AdminForm
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
@@ -11,26 +9,16 @@ from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.db import models
from django.http import Http404, HttpResponseRedirect
from django.template.response import TemplateResponse
+from django.urls import URLResolver
from django.utils.encoding import force_text
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from polymorphic.utils import get_base_polymorphic_model
from .forms import PolymorphicModelChoiceForm
-try:
- # Django 2.0+
- from django.urls import URLResolver
-except ImportError:
- # Django < 2.0
- from django.urls import RegexURLResolver as URLResolver
-
-
-if sys.version_info[0] >= 3:
- long = int
-
class RegistrationClosed(RuntimeError):
"The admin model can't be registered anymore at this point."
@@ -146,13 +134,14 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
"""
self._lazy_setup()
choices = []
- for model in self.get_child_models():
+ content_types = ContentType.objects.get_for_models(*self.get_child_models(), for_concrete_models=False)
+
+ for model, ct in content_types.items():
perm_function_name = "has_{0}_permission".format(action)
model_admin = self._get_real_admin_by_model(model)
perm_function = getattr(model_admin, perm_function_name)
if not perm_function(request):
continue
- ct = ContentType.objects.get_for_model(model, for_concrete_model=False)
choices.append((ct.id, model._meta.verbose_name))
return choices
@@ -293,9 +282,9 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
try:
pos = path.find("/")
if pos == -1:
- object_id = long(path)
+ object_id = int(path)
else:
- object_id = long(path[0:pos])
+ object_id = int(path[0:pos])
except ValueError:
raise Http404(
"No ct_id parameter, unable to find admin subclass for path '{0}'.".format(
diff --git a/polymorphic/base.py b/polymorphic/base.py
index 81d988a..1bf6059 100644
--- a/polymorphic/base.py
+++ b/polymorphic/base.py
@@ -2,8 +2,6 @@
"""
PolymorphicModel Meta Class
"""
-from __future__ import absolute_import
-
import inspect
import os
import sys
diff --git a/polymorphic/compat.py b/polymorphic/compat.py
index e52a3a9..1589eb2 100644
--- a/polymorphic/compat.py
+++ b/polymorphic/compat.py
@@ -1,22 +1,4 @@
"""Compatibility with Python 2 (taken from 'django.utils.six')"""
-import sys
-
-PY2 = sys.version_info[0] == 2
-PY3 = sys.version_info[0] == 3
-
-
-if PY3:
- string_types = (str,)
- integer_types = (int,)
- class_types = (type,)
- text_type = str
- binary_type = bytes
-
- MAXSIZE = sys.maxsize
-else:
- string_types = (basestring,)
- integer_types = (int, long)
-
def with_metaclass(meta, *bases):
class metaclass(type):
@@ -24,22 +6,3 @@ def with_metaclass(meta, *bases):
return meta(name, bases, d)
return type.__new__(metaclass, "temporary_class", (), {})
-
-
-def python_2_unicode_compatible(klass):
- """
- A decorator that defines __unicode__ and __str__ methods under Python 2.
- Under Python 3 it does nothing.
-
- To support Python 2 and 3 with a single code base, define a __str__ method
- returning text and apply this decorator to the class.
- """
- if PY2:
- if "__str__" not in klass.__dict__:
- raise ValueError(
- "@python_2_unicode_compatible cannot be applied "
- "to %s because it doesn't define __str__()." % klass.__name__
- )
- klass.__unicode__ = klass.__str__
- klass.__str__ = lambda self: self.__unicode__().encode("utf-8")
- return klass
diff --git a/polymorphic/contrib/extra_views.py b/polymorphic/contrib/extra_views.py
index f3e050d..9822c47 100644
--- a/polymorphic/contrib/extra_views.py
+++ b/polymorphic/contrib/extra_views.py
@@ -4,8 +4,6 @@ The ``extra_views.advanced`` provides a method to combine that with a create/upd
This package provides classes that support both options for polymorphic formsets.
"""
-from __future__ import absolute_import
-
import extra_views
from django.core.exceptions import ImproperlyConfigured
diff --git a/polymorphic/managers.py b/polymorphic/managers.py
index a43840b..5e894d4 100644
--- a/polymorphic/managers.py
+++ b/polymorphic/managers.py
@@ -2,17 +2,14 @@
"""
The manager class for use in the models.
"""
-from __future__ import unicode_literals
from django.db import models
-from polymorphic.compat import python_2_unicode_compatible
from polymorphic.query import PolymorphicQuerySet
__all__ = ("PolymorphicManager", "PolymorphicQuerySet")
-@python_2_unicode_compatible
class PolymorphicManager(models.Manager):
"""
Manager for PolymorphicModel
diff --git a/polymorphic/models.py b/polymorphic/models.py
index 63731e8..5c34ff7 100644
--- a/polymorphic/models.py
+++ b/polymorphic/models.py
@@ -2,8 +2,6 @@
"""
Seamless Polymorphic Inheritance for Django Models
"""
-from __future__ import absolute_import
-
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.fields.related import (
diff --git a/polymorphic/query.py b/polymorphic/query.py
index 4dc4d30..8b0259b 100644
--- a/polymorphic/query.py
+++ b/polymorphic/query.py
@@ -2,16 +2,14 @@
"""
QuerySet for PolymorphicModel
"""
-from __future__ import absolute_import
-
import copy
from collections import defaultdict
+from django import get_version as get_django_version
from django.contrib.contenttypes.models import ContentType
-from django.db.models import FieldDoesNotExist
+from django.core.exceptions import FieldDoesNotExist
from django.db.models.query import ModelIterable, Q, QuerySet
-from . import compat
from .query_translate import (
translate_polymorphic_field_path,
translate_polymorphic_filter_definitions_in_args,
@@ -160,24 +158,40 @@ class PolymorphicQuerySet(QuerySet):
# Implementation in _translate_polymorphic_filter_defnition."""
return self.filter(not_instance_of=args)
- def _filter_or_exclude(self, negate, *args, **kwargs):
- # We override this internal Django functon as it is used for all filter member functions.
- q_objects = translate_polymorphic_filter_definitions_in_args(
- self.model, args, using=self.db
- )
- # filter_field='data'
- additional_args = translate_polymorphic_filter_definitions_in_kwargs(
- self.model, kwargs, using=self.db
- )
- return super(PolymorphicQuerySet, self)._filter_or_exclude(
- negate, *(list(q_objects) + additional_args), **kwargs
- )
+ # Makes _filter_or_exclude compatible with the change in signature introduced in django at 9c9a3fe
+ if get_django_version() >= "3.2":
+ def _filter_or_exclude(self, negate, args, kwargs):
+ # We override this internal Django function as it is used for all filter member functions.
+ q_objects = translate_polymorphic_filter_definitions_in_args(
+ queryset_model=self.model, args=args, using=self.db
+ )
+ # filter_field='data'
+ additional_args = translate_polymorphic_filter_definitions_in_kwargs(
+ queryset_model=self.model, kwargs=kwargs, using=self.db
+ )
+ args = list(q_objects) + additional_args
+ return super(PolymorphicQuerySet, self)._filter_or_exclude(
+ negate=negate, args=args, kwargs=kwargs
+ )
+ else:
+ def _filter_or_exclude(self, negate, *args, **kwargs):
+ # We override this internal Django function as it is used for all filter member functions.
+ q_objects = translate_polymorphic_filter_definitions_in_args(
+ self.model, args, using=self.db
+ )
+ # filter_field='data'
+ additional_args = translate_polymorphic_filter_definitions_in_kwargs(
+ self.model, kwargs, using=self.db
+ )
+ return super(PolymorphicQuerySet, self)._filter_or_exclude(
+ negate, *(list(q_objects) + additional_args), **kwargs
+ )
def order_by(self, *field_names):
"""translate the field paths in the args, then call vanilla order_by."""
field_names = [
translate_polymorphic_field_path(self.model, a)
- if isinstance(a, compat.string_types)
+ if isinstance(a, str)
else a # allow expressions to pass unchanged
for a in field_names
]
@@ -524,4 +538,4 @@ class PolymorphicQuerySet(QuerySet):
if not self.model.polymorphic_query_multiline_output:
return olist
clist = PolymorphicQuerySet._p_list_class(olist)
- return clist
+ return clist
\ No newline at end of file
diff --git a/polymorphic/query_translate.py b/polymorphic/query_translate.py
index cbecf10..45328a9 100644
--- a/polymorphic/query_translate.py
+++ b/polymorphic/query_translate.py
@@ -2,14 +2,12 @@
"""
PolymorphicQuerySet support functions
"""
-from __future__ import absolute_import
-
import copy
from collections import deque
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import FieldError
+from django.core.exceptions import FieldError, FieldDoesNotExist
from django.db import models
from django.db.models import Q
from django.db.models.fields.related import ForeignObjectRel, RelatedField
@@ -144,7 +142,7 @@ def translate_polymorphic_field_path(queryset_model, field_path):
into modela__modelb__modelc__field3.
Returns: translated path (unchanged, if no translation needed)
"""
- if not isinstance(field_path, compat.string_types):
+ if not isinstance(field_path, str):
raise ValueError("Expected field name as string: {0}".format(field_path))
classname, sep, pure_field_path = field_path.partition("___")
@@ -189,7 +187,7 @@ def translate_polymorphic_field_path(queryset_model, field_path):
# Can also test whether the field exists in the related object to avoid ambiguity between
# class names and field names, but that never happens when your class names are in CamelCase.
return field_path # No exception raised, field does exist.
- except models.FieldDoesNotExist:
+ except FieldDoesNotExist:
pass
submodels = _get_all_sub_models(queryset_model)
diff --git a/polymorphic/showfields.py b/polymorphic/showfields.py
index ffa14f5..8da8330 100644
--- a/polymorphic/showfields.py
+++ b/polymorphic/showfields.py
@@ -3,13 +3,9 @@ import re
from django.db import models
-from . import compat
-from .compat import python_2_unicode_compatible
-
RE_DEFERRED = re.compile("_Deferred_.*")
-@python_2_unicode_compatible
class ShowFieldBase(object):
""" base class for the ShowField... model mixins, does the work """
@@ -42,7 +38,7 @@ class ShowFieldBase(object):
out += content.__class__.__name__
elif issubclass(field_type, models.ManyToManyField):
out += "%d" % content.count()
- elif isinstance(content, compat.integer_types):
+ elif isinstance(content, int):
out += str(content)
elif content is None:
out += "None"
diff --git a/polymorphic/templatetags/polymorphic_admin_tags.py b/polymorphic/templatetags/polymorphic_admin_tags.py
index 9914b84..aa68843 100644
--- a/polymorphic/templatetags/polymorphic_admin_tags.py
+++ b/polymorphic/templatetags/polymorphic_admin_tags.py
@@ -1,7 +1,5 @@
from django.template import Library, Node, TemplateSyntaxError
-from polymorphic import compat
-
register = Library()
@@ -31,7 +29,7 @@ class BreadcrumbScope(Node):
# Instead, have an assignment tag that inserts that in the template.
base_opts = self.base_opts.resolve(context)
new_vars = {}
- if base_opts and not isinstance(base_opts, compat.string_types):
+ if base_opts and not isinstance(base_opts, str):
new_vars = {
"app_label": base_opts.app_label, # What this is all about
"opts": base_opts,
diff --git a/polymorphic/templatetags/polymorphic_formset_tags.py b/polymorphic/templatetags/polymorphic_formset_tags.py
index 6a78eca..0b2895f 100644
--- a/polymorphic/templatetags/polymorphic_formset_tags.py
+++ b/polymorphic/templatetags/polymorphic_formset_tags.py
@@ -3,7 +3,7 @@ import json
from django.template import Library
from django.utils.encoding import force_text
from django.utils.text import capfirst
-from django.utils.translation import ugettext
+from django.utils.translation import gettext
from polymorphic.formsets import BasePolymorphicModelFormSet
@@ -44,10 +44,10 @@ def as_script_options(formset):
"prefix": formset.prefix,
"pkFieldName": formset.model._meta.pk.name,
"addText": getattr(formset, "add_text", None)
- or ugettext("Add another %(verbose_name)s")
+ or gettext("Add another %(verbose_name)s")
% {"verbose_name": capfirst(verbose_name)},
"showAddButton": getattr(formset, "show_add_button", True),
- "deleteText": ugettext("Delete"),
+ "deleteText": gettext("Delete"),
}
if isinstance(formset, BasePolymorphicModelFormSet):
diff --git a/polymorphic/tests/migrations/0001_initial.py b/polymorphic/tests/migrations/0001_initial.py
index 4707a36..38106a3 100644
--- a/polymorphic/tests/migrations/0001_initial.py
+++ b/polymorphic/tests/migrations/0001_initial.py
@@ -1450,6 +1450,13 @@ class Migration(migrations.Migration):
to="tests.ParentModelWithManager",
),
),
+ migrations.AddField(
+ model_name="childmodelwithmanager",
+ name="field1",
+ field=models.CharField(
+ max_length=10,
+ ),
+ ),
migrations.AddField(
model_name="childmodelwithmanager",
name="polymorphic_ctype",
diff --git a/polymorphic/tests/models.py b/polymorphic/tests/models.py
index 96e361f..494b98f 100644
--- a/polymorphic/tests/models.py
+++ b/polymorphic/tests/models.py
@@ -222,6 +222,7 @@ class ParentModelWithManager(PolymorphicModel):
class ChildModelWithManager(PolymorphicModel):
# Also test whether foreign keys receive the manager:
+ field1 = models.CharField(max_length=10) # needed as MyManager uses it
fk = models.ForeignKey(
ParentModelWithManager, on_delete=models.CASCADE, related_name="childmodel_set"
)
diff --git a/polymorphic/tests/test_multidb.py b/polymorphic/tests/test_multidb.py
index eec9e5a..dee08d4 100644
--- a/polymorphic/tests/test_multidb.py
+++ b/polymorphic/tests/test_multidb.py
@@ -1,5 +1,3 @@
-from __future__ import print_function
-
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.test import TestCase
@@ -20,7 +18,7 @@ from polymorphic.tests.models import (
class MultipleDatabasesTests(TestCase):
- multi_db = True
+ databases = ["default", "secondary"]
def test_save_to_non_default_database(self):
Model2A.objects.db_manager("secondary").create(field1="A1")
diff --git a/polymorphic/tests/test_orm.py b/polymorphic/tests/test_orm.py
index 62231f7..8a55c8e 100644
--- a/polymorphic/tests/test_orm.py
+++ b/polymorphic/tests/test_orm.py
@@ -238,22 +238,12 @@ class PolymorphicTests(TransactionTestCase):
objects_deferred[0].__dict__,
"field1 was not deferred (using defer())",
)
- self.assertRegex(
- repr(objects_deferred[0]),
- r"",
- )
- self.assertRegex(
- repr(objects_deferred[1]),
- r"",
- )
- self.assertRegex(
- repr(objects_deferred[2]),
- r"",
- )
- self.assertRegex(
- repr(objects_deferred[3]),
- r"",
- )
+
+ # Check that we have exactly one deferred field ('field1') per resulting object.
+ for obj in objects_deferred:
+ deferred_fields = obj.get_deferred_fields()
+ self.assertEqual(1, len(deferred_fields))
+ self.assertIn("field1", deferred_fields)
objects_only = Model2A.objects.only("pk", "polymorphic_ctype", "field1")
@@ -271,48 +261,37 @@ class PolymorphicTests(TransactionTestCase):
self.assertNotIn(
"field4", objects_only[3].__dict__, "field4 was not deferred (using only())"
)
- self.assertRegex(
- repr(objects_only[0]), r""
- )
- self.assertRegex(
- repr(objects_only[1]),
- r"",
- )
- self.assertRegex(
- repr(objects_only[2]),
- r"",
- )
- self.assertRegex(
- repr(objects_only[3]),
- r"",
- )
+ self.assertNotIn("field1", objects_only[0].get_deferred_fields())
+
+ self.assertIn("field2", objects_only[1].get_deferred_fields())
+
+ # objects_only[2] has several deferred fields, ensure they are all set as such.
+ model2c_deferred = objects_only[2].get_deferred_fields()
+ self.assertIn("field2", model2c_deferred)
+ self.assertIn("field3", model2c_deferred)
+ self.assertIn("model2a_ptr_id", model2c_deferred)
+
+ # objects_only[3] has a few more fields that should be set as deferred.
+ model2d_deferred = objects_only[3].get_deferred_fields()
+ self.assertIn("field2", model2d_deferred)
+ self.assertIn("field3", model2d_deferred)
+ self.assertIn("field4", model2d_deferred)
+ self.assertIn("model2a_ptr_id", model2d_deferred)
+ self.assertIn("model2b_ptr_id", model2d_deferred)
ModelX.objects.create(field_b="A1", field_x="A2")
ModelY.objects.create(field_b="B1", field_y="B2")
+ # If we defer a field on a descendent, the parent's field is not deferred.
objects_deferred = Base.objects.defer("ModelY___field_y")
- self.assertRegex(
- repr(objects_deferred[0]),
- r"",
- )
- self.assertRegex(
- repr(objects_deferred[1]),
- r"",
- )
+ self.assertNotIn("field_y", objects_deferred[0].get_deferred_fields())
+ self.assertIn("field_y", objects_deferred[1].get_deferred_fields())
objects_only = Base.objects.only(
"polymorphic_ctype", "ModelY___field_y", "ModelX___field_x"
)
- self.assertRegex(
- repr(objects_only[0]),
- r"",
- )
- self.assertRegex(
- repr(objects_only[1]),
- r"",
- )
+ self.assertIn("field_b", objects_only[0].get_deferred_fields())
+ self.assertIn("field_b", objects_only[1].get_deferred_fields())
def test_defer_related_fields(self):
self.create_model2abcd()
@@ -481,21 +460,22 @@ class PolymorphicTests(TransactionTestCase):
def test_foreignkey_field(self):
self.create_model2abcd()
- object2a = Model2A.base_objects.get(field1="C1")
+ object2a = Model2A.objects.get(field1="C1")
self.assertEqual(object2a.model2b.__class__, Model2B)
- object2b = Model2B.base_objects.get(field1="C1")
+ object2b = Model2B.objects.get(field1="C1")
self.assertEqual(object2b.model2c.__class__, Model2C)
def test_onetoone_field(self):
self.create_model2abcd()
+ # FIXME: We should not use base_objects here.
a = Model2A.base_objects.get(field1="C1")
b = One2OneRelatingModelDerived.objects.create(
one2one=a, field1="f1", field2="f2"
)
- # this result is basically wrong, probably due to Django cacheing (we used base_objects), but should not be a problem
+ # FIXME: this result is basically wrong, probably due to Django cacheing (we used base_objects), but should not be a problem
self.assertEqual(b.one2one.__class__, Model2A)
self.assertEqual(b.one2one_id, b.one2one.id)
@@ -581,32 +561,18 @@ class PolymorphicTests(TransactionTestCase):
select={"topic": "tests_modelextraexternal.topic"},
where=["tests_modelextraa.id = tests_modelextraexternal.id"],
)
- if compat.PY3:
- self.assertEqual(
- repr(objects[0]),
- '',
- )
- self.assertEqual(
- repr(objects[1]),
- '',
- )
- self.assertEqual(
- repr(objects[2]),
- '',
- )
- else:
- self.assertEqual(
- repr(objects[0]),
- '',
- )
- self.assertEqual(
- repr(objects[1]),
- '',
- )
- self.assertEqual(
- repr(objects[2]),
- '',
- )
+ self.assertEqual(
+ repr(objects[0]),
+ '',
+ )
+ self.assertEqual(
+ repr(objects[1]),
+ '',
+ )
+ self.assertEqual(
+ repr(objects[2]),
+ '',
+ )
self.assertEqual(len(objects), 3)
def test_instance_of_filter(self):
@@ -822,7 +788,6 @@ class PolymorphicTests(TransactionTestCase):
self.assertIs(type(ModelWithMyManager.objects), MyManager)
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()
@@ -845,7 +810,6 @@ class PolymorphicTests(TransactionTestCase):
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()
@@ -855,7 +819,6 @@ class PolymorphicTests(TransactionTestCase):
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)
def test_user_defined_queryset_as_manager(self):
self.create_model2abcd()
@@ -878,7 +841,6 @@ class PolymorphicTests(TransactionTestCase):
type(ModelWithMyManager2._default_manager).__name__,
"PolymorphicManagerFromMyManagerQuerySet",
)
- self.assertIs(type(ModelWithMyManager2.base_objects), models.Manager)
def test_manager_inheritance(self):
# by choice of MRO, should be MyManager from MROBase1.
diff --git a/polymorphic/utils.py b/polymorphic/utils.py
index 14c9a9f..8c00758 100644
--- a/polymorphic/utils.py
+++ b/polymorphic/utils.py
@@ -1,5 +1,3 @@
-import sys
-
from django.contrib.contenttypes.models import ContentType
from django.db import DEFAULT_DB_ALIAS
@@ -59,12 +57,9 @@ 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
+ from functools import cmp_to_key
- return sorted(classes, key=cmp_to_key(_compare_mro))
+ return sorted(classes, key=cmp_to_key(_compare_mro))
def get_base_polymorphic_model(ChildModel, allow_abstract=False):
diff --git a/runtests.py b/runtests.py
index 75a7253..ac8d9f1 100755
--- a/runtests.py
+++ b/runtests.py
@@ -74,6 +74,7 @@ if not settings.configured:
],
POLYMORPHIC_TEST_SWAPPABLE="polymorphic.swappedmodel",
ROOT_URLCONF=None,
+ SECRET_KEY="supersecret"
)
diff --git a/setup.cfg b/setup.cfg
index a805f90..a576a6e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,12 +1,12 @@
[metadata]
name = django-polymorphic
-version = 2.1.2
+version = 3.0.0
description = Seamless polymorphic inheritance for Django models
long_description = file:README.rst
author = Bert Constantin
author_email = bert.constantin@gmx.de
maintainer = Christopher Glass
-maintainer_email = tribaal@gmail.com
+maintainer_email = tribaal@ubuntu.com
url = https://github.com/django-polymorphic/django-polymorphic
download_url = https://github.com/django-polymorphic/django-polymorphic/tarball/master
keywords = django, polymorphic
@@ -14,25 +14,26 @@ classifiers =
Development Status :: 5 - Production/Stable
Environment :: Web Environment
Framework :: Django
- Framework :: Django :: 1.11
- Framework :: Django :: 2.0
Framework :: Django :: 2.1
Framework :: Django :: 2.2
+ Framework :: Django :: 3.0
+ Framework :: Django :: 3.1
Intended Audience :: Developers
License :: OSI Approved :: BSD License
Operating System :: OS Independent
- Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
+ Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
+ Programming Language :: Python :: 3.8
Topic :: Database
[options]
packages = find:
include_package_data = True
install_requires =
- Django >= 1.11
+ Django >= 2.1
[options.packages.find]
exclude =
diff --git a/tox.ini b/tox.ini
index 8ccc8fe..2e4d2c3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,9 +1,9 @@
[tox]
envlist =
- py27-django{111}
- py35-django{111,20}
- py36-django{111,20,21,22,master}
- py37-django{21,22,master}
+ py35-django{21,22}
+ py36-django{21,22,30,31}
+ py37-django{21,22,30,31}
+ py38-django{21,22,30,31}
docs
[testenv]
@@ -12,12 +12,13 @@ setenv =
postgres: DEFAULT_DATABASE = postgres:///default
postgres: SECONDARY_DATABASE = postgres:///secondary
deps =
+ ipdb
coverage
dj-database-url
- django111: Django >= 1.11, < 2.0
- django20: Django ~= 2.0
django21: Django ~= 2.1
django22: Django ~= 2.2
+ django30: Django ~= 3.0
+ django31: Django ~= 3.1
djangomaster: https://github.com/django/django/archive/master.tar.gz
postgres: psycopg2
commands =