-----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCgAdFiEE543E/4saL+AzhKtGi03RayEzO9sFAl8/oo0ACgkQi03RayEz
 O9si9w/+KL4NODbR+ql4A1Q8twJjsjyz1iiHHIGMdEkEEIG3PyGai73MqoQSyQZI
 h4Zw16U1CEVxtwCNLzewdz3NsQ9ioK0ZlKjtyp7b6x+zU2p8cwHoFSleB++HH1Uq
 M/RM6ZR8mycPSDhtpkHMQVGjzC408Zvx/hipUCk4gjeVjM92svmKXubnhb0wba4P
 zKpvX7xEqaCLitblWtemKNMqPRApKva/xvntuYWj0DGsIFGvt+AHsZ4dPZCrGdyp
 Fvt+OZP0ZTDZnyCTADSmg4BM5Sa8qGcs9gntQI9nmnkJWWyq3451NrUKvWPoEZQ+
 7cN4Cmp3nclwu4AiK/LWnQnEX953AdR3xQD1vZ8om6yfndXDOWLu0f0UKyKZMAPn
 LkmQcPol6Hm4yUT4yGGxzd2pgMJ5ZbIJVGKjn7D0hytb5kf9Ii3SyEEniQTS9JfU
 I9990UmzFoJI42CfaqhXi1fTk8yPOSfHqo0eyVOw12Q7ALeoAAD/HWgZTp1HO+Cq
 aN5wtn+oX7OSFSfr6/aXLTyYc17ugj4XIkn7DY69NJPsrMELBcX7ThZ+eimuxwFC
 ytt8o0nI49jYwA3bcrmIY2wonV5tkipR2EKivzEW7uteEpjIqHAOLreS5ZLHDCjS
 wFNsKl0Y3xOXGR4RaOrY7oKkRSraeVtbzqpkzL/NT0WsgpROTZ8=
 =60xM
 -----END PGP SIGNATURE-----

Merge tag '3.0.0' into fix_request_path_info

3.0.0
fix_request_path_info
Pietro Brenna 2021-01-07 17:22:50 +01:00
commit f08f18e3bb
33 changed files with 158 additions and 237 deletions

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ dist/
docs/_build/ docs/_build/
htmlcov/ htmlcov/
venv/ venv/
.venv/

View File

@ -1,6 +1,3 @@
# https://travis-ci.org/django-polymorphic/django-polymorphic
dist: xenial
sudo: false
language: python language: python
services: services:
@ -11,32 +8,38 @@ addons:
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
# Django 1.11: Python 2.7, 3.5, or 3.6 # Django 2.1: Python 3.5, 3.6, or 3.7
- { env: TOXENV=py27-django111, python: 2.7 } - { env: TOXENV=py35-django21, python: 3.5 }
- { 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
- { env: TOXENV=py36-django21, python: 3.6 } - { env: TOXENV=py36-django21, python: 3.6 }
- { env: TOXENV=py37-django21, python: 3.7 } - { env: TOXENV=py37-django21, python: 3.7 }
- { env: TOXENV=py37-django21-postgres DB=postgres, 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=py36-django22, python: 3.6 }
- { env: TOXENV=py37-django22, python: 3.7 } - { 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): # Django development master (direct from GitHub source):
- { env: TOXENV=py36-djangomaster, python: 3.6 } - { env: TOXENV=py36-djangomaster, python: 3.6 }
- { env: TOXENV=py37-djangomaster, python: 3.7 } - { 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: allow_failures:
- env: TOXENV=py36-djangomaster - env: TOXENV=py36-djangomaster
- env: TOXENV=py37-djangomaster - env: TOXENV=py37-djangomaster
- env: TOXENV=py37-djangomaster-postgres DB=postgres - env: TOXENV=py38-djangomaster
- env: TOXENV=py38-djangomaster-postgres DB=postgres
cache: cache:
directories: directories:
@ -57,7 +60,3 @@ script:
after_success: after_success:
- coverage xml -i - coverage xml -i
- codecov - codecov
branches:
only:
- master

View File

@ -13,12 +13,14 @@ Contributors
* Abel Daniel * Abel Daniel
* Adam Chainz * Adam Chainz
* Adam Wentz * Adam Wentz
* Adam Donaghy
* Andrew Ingram (contributed setup.py) * Andrew Ingram (contributed setup.py)
* Al Johri * Al Johri
* Alex Alvarez * Alex Alvarez
* Andrew Dodd * Andrew Dodd
* Angel Velasquez * Angel Velasquez
* Austin Matsick * Austin Matsick
* Bastien Vallet
* Ben Konrath * Ben Konrath
* Bert Constantin * Bert Constantin
* Bertrand Bordage * Bertrand Bordage

View File

@ -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. While taking this in mind, there are valid reasons for using subclassed models.
That's what this library is designed for! 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 older Django versions, install *django-polymorphic==1.3*.
For more information, see the `documentation at Read the Docs <https://django-polymorphic.readthedocs.io/>`_. For more information, see the `documentation at Read the Docs <https://django-polymorphic.readthedocs.io/>`_.

View File

@ -1,5 +1,5 @@
# for readthedocs # for readthedocs
# Remaining requirements are picked up from setup.py # Remaining requirements are picked up from setup.py
Django == 2.2.3 Django == 2.2.13
django-extra-views == 0.12.0 django-extra-views == 0.12.0
sphinxcontrib-django == 0.4 sphinxcontrib-django == 0.4

0
docs/_static/.gitkeep vendored 100644
View File

View File

@ -1,7 +1,14 @@
Changelog 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+ * Fix ``PolymorphicInlineModelAdmin`` media jQuery include for Django 2.0+

View File

@ -29,7 +29,6 @@ can be included in a single Django migration. For example:
.. code-block:: python .. code-block:: python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -12,7 +12,7 @@ Update the settings file::
'django.contrib.contenttypes', '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*. For older Django versions, use *django-polymorphic==1.3*.
Making Your Models Polymorphic Making Your Models Polymorphic

View File

@ -1,12 +1,10 @@
from django.db import models from django.db import models
from django.utils.dates import MONTHS_3 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 django.utils.translation import ugettext_lazy as _
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
@python_2_unicode_compatible
class Order(models.Model): class Order(models.Model):
""" """
An example order that has polymorphic relations An example order that has polymorphic relations
@ -23,7 +21,6 @@ class Order(models.Model):
return self.title return self.title
@python_2_unicode_compatible
class Payment(PolymorphicModel): class Payment(PolymorphicModel):
""" """
A generic payment model. A generic payment model.

View File

@ -5,7 +5,7 @@ import inspect
from django.contrib import admin from django.contrib import admin
from django.urls import resolve 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 from polymorphic.utils import get_base_polymorphic_model

View File

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from django.core.exceptions import PermissionDenied 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): class PolymorphicChildModelFilter(admin.SimpleListFilter):

View File

@ -1,6 +1,6 @@
from django import forms from django import forms
from django.contrib.admin.widgets import AdminRadioSelect 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): class PolymorphicModelChoiceForm(forms.Form):

View File

@ -8,7 +8,7 @@ import json
from django.contrib.admin.helpers import AdminField, InlineAdminForm, InlineAdminFormSet from django.contrib.admin.helpers import AdminField, InlineAdminForm, InlineAdminFormSet
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.translation import ugettext from django.utils.translation import gettext
from polymorphic.formsets import BasePolymorphicModelFormSet from polymorphic.formsets import BasePolymorphicModelFormSet
@ -96,7 +96,7 @@ class PolymorphicInlineAdminFormSet(InlineAdminFormSet):
"name": "#%s" % self.formset.prefix, "name": "#%s" % self.formset.prefix,
"options": { "options": {
"prefix": self.formset.prefix, "prefix": self.formset.prefix,
"addText": ugettext("Add another %(verbose_name)s") "addText": gettext("Add another %(verbose_name)s")
% {"verbose_name": capfirst(verbose_name)}, % {"verbose_name": capfirst(verbose_name)},
"childTypes": [ "childTypes": [
{ {
@ -105,7 +105,7 @@ class PolymorphicInlineAdminFormSet(InlineAdminFormSet):
} }
for model in self.formset.child_forms.keys() for model in self.formset.child_forms.keys()
], ],
"deleteText": ugettext("Remove"), "deleteText": gettext("Remove"),
}, },
} }
) )

View File

@ -1,8 +1,6 @@
""" """
The parent admin displays the list view of the base model. The parent admin displays the list view of the base model.
""" """
import sys
from django.contrib import admin from django.contrib import admin
from django.contrib.admin.helpers import AdminErrorList, AdminForm from django.contrib.admin.helpers import AdminErrorList, AdminForm
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters 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.db import models
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.urls import URLResolver
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.safestring import mark_safe 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 polymorphic.utils import get_base_polymorphic_model
from .forms import PolymorphicModelChoiceForm 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): class RegistrationClosed(RuntimeError):
"The admin model can't be registered anymore at this point." "The admin model can't be registered anymore at this point."
@ -146,13 +134,14 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
""" """
self._lazy_setup() self._lazy_setup()
choices = [] 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) perm_function_name = "has_{0}_permission".format(action)
model_admin = self._get_real_admin_by_model(model) model_admin = self._get_real_admin_by_model(model)
perm_function = getattr(model_admin, perm_function_name) perm_function = getattr(model_admin, perm_function_name)
if not perm_function(request): if not perm_function(request):
continue continue
ct = ContentType.objects.get_for_model(model, for_concrete_model=False)
choices.append((ct.id, model._meta.verbose_name)) choices.append((ct.id, model._meta.verbose_name))
return choices return choices
@ -293,9 +282,9 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
try: try:
pos = path.find("/") pos = path.find("/")
if pos == -1: if pos == -1:
object_id = long(path) object_id = int(path)
else: else:
object_id = long(path[0:pos]) object_id = int(path[0:pos])
except ValueError: except ValueError:
raise Http404( raise Http404(
"No ct_id parameter, unable to find admin subclass for path '{0}'.".format( "No ct_id parameter, unable to find admin subclass for path '{0}'.".format(

View File

@ -2,8 +2,6 @@
""" """
PolymorphicModel Meta Class PolymorphicModel Meta Class
""" """
from __future__ import absolute_import
import inspect import inspect
import os import os
import sys import sys

View File

@ -1,22 +1,4 @@
"""Compatibility with Python 2 (taken from 'django.utils.six')""" """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): def with_metaclass(meta, *bases):
class metaclass(type): class metaclass(type):
@ -24,22 +6,3 @@ def with_metaclass(meta, *bases):
return meta(name, bases, d) return meta(name, bases, d)
return type.__new__(metaclass, "temporary_class", (), {}) 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

View File

@ -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. This package provides classes that support both options for polymorphic formsets.
""" """
from __future__ import absolute_import
import extra_views import extra_views
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured

View File

@ -2,17 +2,14 @@
""" """
The manager class for use in the models. The manager class for use in the models.
""" """
from __future__ import unicode_literals
from django.db import models from django.db import models
from polymorphic.compat import python_2_unicode_compatible
from polymorphic.query import PolymorphicQuerySet from polymorphic.query import PolymorphicQuerySet
__all__ = ("PolymorphicManager", "PolymorphicQuerySet") __all__ = ("PolymorphicManager", "PolymorphicQuerySet")
@python_2_unicode_compatible
class PolymorphicManager(models.Manager): class PolymorphicManager(models.Manager):
""" """
Manager for PolymorphicModel Manager for PolymorphicModel

View File

@ -2,8 +2,6 @@
""" """
Seamless Polymorphic Inheritance for Django Models Seamless Polymorphic Inheritance for Django Models
""" """
from __future__ import absolute_import
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.db.models.fields.related import ( from django.db.models.fields.related import (

View File

@ -2,16 +2,14 @@
""" """
QuerySet for PolymorphicModel QuerySet for PolymorphicModel
""" """
from __future__ import absolute_import
import copy import copy
from collections import defaultdict from collections import defaultdict
from django import get_version as get_django_version
from django.contrib.contenttypes.models import ContentType 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 django.db.models.query import ModelIterable, Q, QuerySet
from . import compat
from .query_translate import ( from .query_translate import (
translate_polymorphic_field_path, translate_polymorphic_field_path,
translate_polymorphic_filter_definitions_in_args, translate_polymorphic_filter_definitions_in_args,
@ -160,24 +158,40 @@ class PolymorphicQuerySet(QuerySet):
# Implementation in _translate_polymorphic_filter_defnition.""" # Implementation in _translate_polymorphic_filter_defnition."""
return self.filter(not_instance_of=args) return self.filter(not_instance_of=args)
def _filter_or_exclude(self, negate, *args, **kwargs): # Makes _filter_or_exclude compatible with the change in signature introduced in django at 9c9a3fe
# We override this internal Django functon as it is used for all filter member functions. if get_django_version() >= "3.2":
q_objects = translate_polymorphic_filter_definitions_in_args( def _filter_or_exclude(self, negate, args, kwargs):
self.model, args, using=self.db # We override this internal Django function as it is used for all filter member functions.
) q_objects = translate_polymorphic_filter_definitions_in_args(
# filter_field='data' queryset_model=self.model, args=args, using=self.db
additional_args = translate_polymorphic_filter_definitions_in_kwargs( )
self.model, kwargs, using=self.db # filter_field='data'
) additional_args = translate_polymorphic_filter_definitions_in_kwargs(
return super(PolymorphicQuerySet, self)._filter_or_exclude( queryset_model=self.model, kwargs=kwargs, using=self.db
negate, *(list(q_objects) + additional_args), **kwargs )
) 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): def order_by(self, *field_names):
"""translate the field paths in the args, then call vanilla order_by.""" """translate the field paths in the args, then call vanilla order_by."""
field_names = [ field_names = [
translate_polymorphic_field_path(self.model, a) translate_polymorphic_field_path(self.model, a)
if isinstance(a, compat.string_types) if isinstance(a, str)
else a # allow expressions to pass unchanged else a # allow expressions to pass unchanged
for a in field_names for a in field_names
] ]
@ -524,4 +538,4 @@ class PolymorphicQuerySet(QuerySet):
if not self.model.polymorphic_query_multiline_output: if not self.model.polymorphic_query_multiline_output:
return olist return olist
clist = PolymorphicQuerySet._p_list_class(olist) clist = PolymorphicQuerySet._p_list_class(olist)
return clist return clist

View File

@ -2,14 +2,12 @@
""" """
PolymorphicQuerySet support functions PolymorphicQuerySet support functions
""" """
from __future__ import absolute_import
import copy import copy
from collections import deque from collections import deque
from django.apps import apps from django.apps import apps
from django.contrib.contenttypes.models import ContentType 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 import models
from django.db.models import Q from django.db.models import Q
from django.db.models.fields.related import ForeignObjectRel, RelatedField 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. into modela__modelb__modelc__field3.
Returns: translated path (unchanged, if no translation needed) 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)) raise ValueError("Expected field name as string: {0}".format(field_path))
classname, sep, pure_field_path = field_path.partition("___") 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 # 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. # 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. return field_path # No exception raised, field does exist.
except models.FieldDoesNotExist: except FieldDoesNotExist:
pass pass
submodels = _get_all_sub_models(queryset_model) submodels = _get_all_sub_models(queryset_model)

View File

@ -3,13 +3,9 @@ import re
from django.db import models from django.db import models
from . import compat
from .compat import python_2_unicode_compatible
RE_DEFERRED = re.compile("_Deferred_.*") RE_DEFERRED = re.compile("_Deferred_.*")
@python_2_unicode_compatible
class ShowFieldBase(object): class ShowFieldBase(object):
""" base class for the ShowField... model mixins, does the work """ """ base class for the ShowField... model mixins, does the work """
@ -42,7 +38,7 @@ class ShowFieldBase(object):
out += content.__class__.__name__ out += content.__class__.__name__
elif issubclass(field_type, models.ManyToManyField): elif issubclass(field_type, models.ManyToManyField):
out += "%d" % content.count() out += "%d" % content.count()
elif isinstance(content, compat.integer_types): elif isinstance(content, int):
out += str(content) out += str(content)
elif content is None: elif content is None:
out += "None" out += "None"

View File

@ -1,7 +1,5 @@
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from polymorphic import compat
register = Library() register = Library()
@ -31,7 +29,7 @@ class BreadcrumbScope(Node):
# Instead, have an assignment tag that inserts that in the template. # Instead, have an assignment tag that inserts that in the template.
base_opts = self.base_opts.resolve(context) base_opts = self.base_opts.resolve(context)
new_vars = {} new_vars = {}
if base_opts and not isinstance(base_opts, compat.string_types): if base_opts and not isinstance(base_opts, str):
new_vars = { new_vars = {
"app_label": base_opts.app_label, # What this is all about "app_label": base_opts.app_label, # What this is all about
"opts": base_opts, "opts": base_opts,

View File

@ -3,7 +3,7 @@ import json
from django.template import Library from django.template import Library
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.translation import ugettext from django.utils.translation import gettext
from polymorphic.formsets import BasePolymorphicModelFormSet from polymorphic.formsets import BasePolymorphicModelFormSet
@ -44,10 +44,10 @@ def as_script_options(formset):
"prefix": formset.prefix, "prefix": formset.prefix,
"pkFieldName": formset.model._meta.pk.name, "pkFieldName": formset.model._meta.pk.name,
"addText": getattr(formset, "add_text", None) "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)}, % {"verbose_name": capfirst(verbose_name)},
"showAddButton": getattr(formset, "show_add_button", True), "showAddButton": getattr(formset, "show_add_button", True),
"deleteText": ugettext("Delete"), "deleteText": gettext("Delete"),
} }
if isinstance(formset, BasePolymorphicModelFormSet): if isinstance(formset, BasePolymorphicModelFormSet):

View File

@ -1450,6 +1450,13 @@ class Migration(migrations.Migration):
to="tests.ParentModelWithManager", to="tests.ParentModelWithManager",
), ),
), ),
migrations.AddField(
model_name="childmodelwithmanager",
name="field1",
field=models.CharField(
max_length=10,
),
),
migrations.AddField( migrations.AddField(
model_name="childmodelwithmanager", model_name="childmodelwithmanager",
name="polymorphic_ctype", name="polymorphic_ctype",

View File

@ -222,6 +222,7 @@ class ParentModelWithManager(PolymorphicModel):
class ChildModelWithManager(PolymorphicModel): class ChildModelWithManager(PolymorphicModel):
# Also test whether foreign keys receive the manager: # Also test whether foreign keys receive the manager:
field1 = models.CharField(max_length=10) # needed as MyManager uses it
fk = models.ForeignKey( fk = models.ForeignKey(
ParentModelWithManager, on_delete=models.CASCADE, related_name="childmodel_set" ParentModelWithManager, on_delete=models.CASCADE, related_name="childmodel_set"
) )

View File

@ -1,5 +1,3 @@
from __future__ import print_function
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.test import TestCase from django.test import TestCase
@ -20,7 +18,7 @@ from polymorphic.tests.models import (
class MultipleDatabasesTests(TestCase): class MultipleDatabasesTests(TestCase):
multi_db = True databases = ["default", "secondary"]
def test_save_to_non_default_database(self): def test_save_to_non_default_database(self):
Model2A.objects.db_manager("secondary").create(field1="A1") Model2A.objects.db_manager("secondary").create(field1="A1")

View File

@ -238,22 +238,12 @@ class PolymorphicTests(TransactionTestCase):
objects_deferred[0].__dict__, objects_deferred[0].__dict__,
"field1 was not deferred (using defer())", "field1 was not deferred (using defer())",
) )
self.assertRegex(
repr(objects_deferred[0]), # Check that we have exactly one deferred field ('field1') per resulting object.
r"<Model2A: id \d+, field1 \(CharField\), deferred\[field1\]>", for obj in objects_deferred:
) deferred_fields = obj.get_deferred_fields()
self.assertRegex( self.assertEqual(1, len(deferred_fields))
repr(objects_deferred[1]), self.assertIn("field1", deferred_fields)
r"<Model2B: id \d+, field1 \(CharField\), field2 \(CharField\), deferred\[field1\]>",
)
self.assertRegex(
repr(objects_deferred[2]),
r"<Model2C: id \d+, field1 \(CharField\), field2 \(CharField\), field3 \(CharField\), deferred\[field1\]>",
)
self.assertRegex(
repr(objects_deferred[3]),
r"<Model2D: id \d+, field1 \(CharField\), field2 \(CharField\), field3 \(CharField\), field4 \(CharField\), deferred\[field1\]>",
)
objects_only = Model2A.objects.only("pk", "polymorphic_ctype", "field1") objects_only = Model2A.objects.only("pk", "polymorphic_ctype", "field1")
@ -271,48 +261,37 @@ class PolymorphicTests(TransactionTestCase):
self.assertNotIn( self.assertNotIn(
"field4", objects_only[3].__dict__, "field4 was not deferred (using only())" "field4", objects_only[3].__dict__, "field4 was not deferred (using only())"
) )
self.assertRegex( self.assertNotIn("field1", objects_only[0].get_deferred_fields())
repr(objects_only[0]), r"<Model2A: id \d+, field1 \(CharField\)>"
) self.assertIn("field2", objects_only[1].get_deferred_fields())
self.assertRegex(
repr(objects_only[1]), # objects_only[2] has several deferred fields, ensure they are all set as such.
r"<Model2B: id \d+, field1 \(CharField\), field2 \(CharField\), deferred\[field2\]>", model2c_deferred = objects_only[2].get_deferred_fields()
) self.assertIn("field2", model2c_deferred)
self.assertRegex( self.assertIn("field3", model2c_deferred)
repr(objects_only[2]), self.assertIn("model2a_ptr_id", model2c_deferred)
r"<Model2C: id \d+, field1 \(CharField\), field2 \(CharField\), field3 \(CharField\), "
r"deferred\[field2,field3,model2a_ptr_id\]>", # objects_only[3] has a few more fields that should be set as deferred.
) model2d_deferred = objects_only[3].get_deferred_fields()
self.assertRegex( self.assertIn("field2", model2d_deferred)
repr(objects_only[3]), self.assertIn("field3", model2d_deferred)
r"<Model2D: id \d+, field1 \(CharField\), field2 \(CharField\), field3 \(CharField\), field4 \(CharField\), " self.assertIn("field4", model2d_deferred)
r"deferred\[field2,field3,field4,model2a_ptr_id,model2b_ptr_id\]>", self.assertIn("model2a_ptr_id", model2d_deferred)
) self.assertIn("model2b_ptr_id", model2d_deferred)
ModelX.objects.create(field_b="A1", field_x="A2") ModelX.objects.create(field_b="A1", field_x="A2")
ModelY.objects.create(field_b="B1", field_y="B2") 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") objects_deferred = Base.objects.defer("ModelY___field_y")
self.assertRegex( self.assertNotIn("field_y", objects_deferred[0].get_deferred_fields())
repr(objects_deferred[0]), self.assertIn("field_y", objects_deferred[1].get_deferred_fields())
r"<ModelX: id \d+, field_b \(CharField\), field_x \(CharField\)>",
)
self.assertRegex(
repr(objects_deferred[1]),
r"<ModelY: id \d+, field_b \(CharField\), field_y \(CharField\), deferred\[field_y\]>",
)
objects_only = Base.objects.only( objects_only = Base.objects.only(
"polymorphic_ctype", "ModelY___field_y", "ModelX___field_x" "polymorphic_ctype", "ModelY___field_y", "ModelX___field_x"
) )
self.assertRegex( self.assertIn("field_b", objects_only[0].get_deferred_fields())
repr(objects_only[0]), self.assertIn("field_b", objects_only[1].get_deferred_fields())
r"<ModelX: id \d+, field_b \(CharField\), field_x \(CharField\), deferred\[field_b\]>",
)
self.assertRegex(
repr(objects_only[1]),
r"<ModelY: id \d+, field_b \(CharField\), field_y \(CharField\), deferred\[field_b\]>",
)
def test_defer_related_fields(self): def test_defer_related_fields(self):
self.create_model2abcd() self.create_model2abcd()
@ -481,21 +460,22 @@ class PolymorphicTests(TransactionTestCase):
def test_foreignkey_field(self): def test_foreignkey_field(self):
self.create_model2abcd() self.create_model2abcd()
object2a = Model2A.base_objects.get(field1="C1") object2a = Model2A.objects.get(field1="C1")
self.assertEqual(object2a.model2b.__class__, Model2B) 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) self.assertEqual(object2b.model2c.__class__, Model2C)
def test_onetoone_field(self): def test_onetoone_field(self):
self.create_model2abcd() self.create_model2abcd()
# FIXME: We should not use base_objects here.
a = Model2A.base_objects.get(field1="C1") a = Model2A.base_objects.get(field1="C1")
b = One2OneRelatingModelDerived.objects.create( b = One2OneRelatingModelDerived.objects.create(
one2one=a, field1="f1", field2="f2" 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.__class__, Model2A)
self.assertEqual(b.one2one_id, b.one2one.id) self.assertEqual(b.one2one_id, b.one2one.id)
@ -581,32 +561,18 @@ class PolymorphicTests(TransactionTestCase):
select={"topic": "tests_modelextraexternal.topic"}, select={"topic": "tests_modelextraexternal.topic"},
where=["tests_modelextraa.id = tests_modelextraexternal.id"], where=["tests_modelextraa.id = tests_modelextraexternal.id"],
) )
if compat.PY3: self.assertEqual(
self.assertEqual( repr(objects[0]),
repr(objects[0]), '<ModelExtraA: id 1, field1 (CharField) "A1" - Extra: topic (str) "extra1">',
'<ModelExtraA: id 1, field1 (CharField) "A1" - Extra: topic (str) "extra1">', )
) self.assertEqual(
self.assertEqual( repr(objects[1]),
repr(objects[1]), '<ModelExtraB: id 2, field1 (CharField) "B1", field2 (CharField) "B2" - Extra: topic (str) "extra2">',
'<ModelExtraB: id 2, field1 (CharField) "B1", field2 (CharField) "B2" - Extra: topic (str) "extra2">', )
) self.assertEqual(
self.assertEqual( repr(objects[2]),
repr(objects[2]), '<ModelExtraC: id 3, field1 (CharField) "C1", field2 (CharField) "C2", field3 (CharField) "C3" - Extra: topic (str) "extra3">',
'<ModelExtraC: id 3, field1 (CharField) "C1", field2 (CharField) "C2", field3 (CharField) "C3" - Extra: topic (str) "extra3">', )
)
else:
self.assertEqual(
repr(objects[0]),
'<ModelExtraA: id 1, field1 (CharField) "A1" - Extra: topic (unicode) "extra1">',
)
self.assertEqual(
repr(objects[1]),
'<ModelExtraB: id 2, field1 (CharField) "B1", field2 (CharField) "B2" - Extra: topic (unicode) "extra2">',
)
self.assertEqual(
repr(objects[2]),
'<ModelExtraC: id 3, field1 (CharField) "C1", field2 (CharField) "C2", field3 (CharField) "C3" - Extra: topic (unicode) "extra3">',
)
self.assertEqual(len(objects), 3) self.assertEqual(len(objects), 3)
def test_instance_of_filter(self): def test_instance_of_filter(self):
@ -822,7 +788,6 @@ class PolymorphicTests(TransactionTestCase):
self.assertIs(type(ModelWithMyManager.objects), MyManager) self.assertIs(type(ModelWithMyManager.objects), MyManager)
self.assertIs(type(ModelWithMyManager._default_manager), MyManager) self.assertIs(type(ModelWithMyManager._default_manager), MyManager)
self.assertIs(type(ModelWithMyManager.base_objects), models.Manager)
def test_user_defined_manager_as_secondary(self): def test_user_defined_manager_as_secondary(self):
self.create_model2abcd() self.create_model2abcd()
@ -845,7 +810,6 @@ class PolymorphicTests(TransactionTestCase):
self.assertIs( self.assertIs(
type(ModelWithMyManagerNoDefault._default_manager), PolymorphicManager type(ModelWithMyManagerNoDefault._default_manager), PolymorphicManager
) )
self.assertIs(type(ModelWithMyManagerNoDefault.base_objects), models.Manager)
def test_user_objects_manager_as_secondary(self): def test_user_objects_manager_as_secondary(self):
self.create_model2abcd() self.create_model2abcd()
@ -855,7 +819,6 @@ class PolymorphicTests(TransactionTestCase):
self.assertIs(type(ModelWithMyManagerDefault.my_objects), MyManager) self.assertIs(type(ModelWithMyManagerDefault.my_objects), MyManager)
self.assertIs(type(ModelWithMyManagerDefault.objects), PolymorphicManager) self.assertIs(type(ModelWithMyManagerDefault.objects), PolymorphicManager)
self.assertIs(type(ModelWithMyManagerDefault._default_manager), MyManager) self.assertIs(type(ModelWithMyManagerDefault._default_manager), MyManager)
self.assertIs(type(ModelWithMyManagerDefault.base_objects), models.Manager)
def test_user_defined_queryset_as_manager(self): def test_user_defined_queryset_as_manager(self):
self.create_model2abcd() self.create_model2abcd()
@ -878,7 +841,6 @@ class PolymorphicTests(TransactionTestCase):
type(ModelWithMyManager2._default_manager).__name__, type(ModelWithMyManager2._default_manager).__name__,
"PolymorphicManagerFromMyManagerQuerySet", "PolymorphicManagerFromMyManagerQuerySet",
) )
self.assertIs(type(ModelWithMyManager2.base_objects), models.Manager)
def test_manager_inheritance(self): def test_manager_inheritance(self):
# by choice of MRO, should be MyManager from MROBase1. # by choice of MRO, should be MyManager from MROBase1.

View File

@ -1,5 +1,3 @@
import sys
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import DEFAULT_DB_ALIAS 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. Sort a series of models by their inheritance order.
""" """
if sys.version_info[0] == 2: from functools import cmp_to_key
return sorted(classes, cmp=_compare_mro)
else:
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): def get_base_polymorphic_model(ChildModel, allow_abstract=False):

View File

@ -74,6 +74,7 @@ if not settings.configured:
], ],
POLYMORPHIC_TEST_SWAPPABLE="polymorphic.swappedmodel", POLYMORPHIC_TEST_SWAPPABLE="polymorphic.swappedmodel",
ROOT_URLCONF=None, ROOT_URLCONF=None,
SECRET_KEY="supersecret"
) )

View File

@ -1,12 +1,12 @@
[metadata] [metadata]
name = django-polymorphic name = django-polymorphic
version = 2.1.2 version = 3.0.0
description = Seamless polymorphic inheritance for Django models description = Seamless polymorphic inheritance for Django models
long_description = file:README.rst long_description = file:README.rst
author = Bert Constantin author = Bert Constantin
author_email = bert.constantin@gmx.de author_email = bert.constantin@gmx.de
maintainer = Christopher Glass maintainer = Christopher Glass
maintainer_email = tribaal@gmail.com maintainer_email = tribaal@ubuntu.com
url = https://github.com/django-polymorphic/django-polymorphic url = https://github.com/django-polymorphic/django-polymorphic
download_url = https://github.com/django-polymorphic/django-polymorphic/tarball/master download_url = https://github.com/django-polymorphic/django-polymorphic/tarball/master
keywords = django, polymorphic keywords = django, polymorphic
@ -14,25 +14,26 @@ classifiers =
Development Status :: 5 - Production/Stable Development Status :: 5 - Production/Stable
Environment :: Web Environment Environment :: Web Environment
Framework :: Django Framework :: Django
Framework :: Django :: 1.11
Framework :: Django :: 2.0
Framework :: Django :: 2.1 Framework :: Django :: 2.1
Framework :: Django :: 2.2 Framework :: Django :: 2.2
Framework :: Django :: 3.0
Framework :: Django :: 3.1
Intended Audience :: Developers Intended Audience :: Developers
License :: OSI Approved :: BSD License License :: OSI Approved :: BSD License
Operating System :: OS Independent Operating System :: OS Independent
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Topic :: Database Topic :: Database
[options] [options]
packages = find: packages = find:
include_package_data = True include_package_data = True
install_requires = install_requires =
Django >= 1.11 Django >= 2.1
[options.packages.find] [options.packages.find]
exclude = exclude =

13
tox.ini
View File

@ -1,9 +1,9 @@
[tox] [tox]
envlist = envlist =
py27-django{111} py35-django{21,22}
py35-django{111,20} py36-django{21,22,30,31}
py36-django{111,20,21,22,master} py37-django{21,22,30,31}
py37-django{21,22,master} py38-django{21,22,30,31}
docs docs
[testenv] [testenv]
@ -12,12 +12,13 @@ setenv =
postgres: DEFAULT_DATABASE = postgres:///default postgres: DEFAULT_DATABASE = postgres:///default
postgres: SECONDARY_DATABASE = postgres:///secondary postgres: SECONDARY_DATABASE = postgres:///secondary
deps = deps =
ipdb
coverage coverage
dj-database-url dj-database-url
django111: Django >= 1.11, < 2.0
django20: Django ~= 2.0
django21: Django ~= 2.1 django21: Django ~= 2.1
django22: Django ~= 2.2 django22: Django ~= 2.2
django30: Django ~= 3.0
django31: Django ~= 3.1
djangomaster: https://github.com/django/django/archive/master.tar.gz djangomaster: https://github.com/django/django/archive/master.tar.gz
postgres: psycopg2 postgres: psycopg2
commands = commands =