diff --git a/.gitignore b/.gitignore index 21038d5..2c86369 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .cache/ .eggs/ .installed.cfg +.idea bin develop-eggs django_multisite.egg-info/ @@ -15,3 +16,5 @@ multisite/*.egg-info .coverage .tox/ .pytest_cache/ +.env +build diff --git a/README.rst b/README.rst index 67f39b3..d0c08ee 100644 --- a/README.rst +++ b/README.rst @@ -35,6 +35,12 @@ Replace your SITE_ID in settings.py to:: from multisite import SiteID SITE_ID = SiteID(default=1) +Beginning with django 3.2 the sites framework performs a check to ensure that :code:`SITE_ID` is an integer. +Add the following to the top of settings.py to use our check to allow integer or :code:`SiteID`:: + + from multisite import checks + + Add these to your INSTALLED_APPS:: INSTALLED_APPS = [ diff --git a/multisite/admin.py b/multisite/admin.py index 374708f..d51b5ac 100644 --- a/multisite/admin.py +++ b/multisite/admin.py @@ -1,6 +1,3 @@ -from __future__ import unicode_literals -from __future__ import absolute_import - from django.contrib import admin from django.contrib.admin.views.main import ChangeList from django.contrib.sites.models import Site @@ -19,6 +16,7 @@ class AliasAdmin(admin.ModelAdmin): readonly_fields = ('is_canonical',) search_fields = ('domain',) + admin.site.register(Alias, AliasAdmin) @@ -36,6 +34,7 @@ def get_queryset(self, request): qs = qs.order_by(*ordering) return qs + # HACK: Monkeypatch AliasInline into SiteAdmin SiteAdmin.inlines = type(SiteAdmin.inlines)([AliasInline]) + SiteAdmin.inlines diff --git a/multisite/checks.py b/multisite/checks.py new file mode 100644 index 0000000..a26b66d --- /dev/null +++ b/multisite/checks.py @@ -0,0 +1,20 @@ +from django.conf import settings +from django.contrib.sites import checks +from django.core.checks import Error + +from . import SiteID + + +def check_site_id(app_configs, **kwargs): + """ Patch sites check because django 3.2 insists on an integer value for SITE_ID """ + if ( + hasattr(settings, 'SITE_ID') and + not isinstance(settings.SITE_ID, (type(None), int, SiteID)) + ): + return [ + Error('The SITE_ID setting must be an integer or SiteID', id='sites.E101'), + ] + return [] + + +checks.check_site_id = check_site_id diff --git a/multisite/forms.py b/multisite/forms.py index a100888..0eeeca1 100644 --- a/multisite/forms.py +++ b/multisite/forms.py @@ -1,7 +1,3 @@ -from __future__ import unicode_literals -from __future__ import absolute_import - - from django.contrib.sites.admin import SiteAdmin from django.core.exceptions import ValidationError diff --git a/multisite/hacks.py b/multisite/hacks.py index d0e2002..f275889 100644 --- a/multisite/hacks.py +++ b/multisite/hacks.py @@ -1,6 +1,3 @@ -from __future__ import unicode_literals -from __future__ import absolute_import - import sys from django.conf import settings diff --git a/multisite/hosts.py b/multisite/hosts.py index 90525a9..7a01971 100644 --- a/multisite/hosts.py +++ b/multisite/hosts.py @@ -1,6 +1,3 @@ -from __future__ import unicode_literals -from __future__ import absolute_import - from django.utils.functional import empty, SimpleLazyObject @@ -41,4 +38,5 @@ def __iter__(self): for host in self.alias_model.objects.values_list('domain'): yield host[0] + ALLOWED_HOSTS = IterableLazyObject(lambda: AllowedHosts()) diff --git a/multisite/management/commands/update_public_suffix_list.py b/multisite/management/commands/update_public_suffix_list.py index 88e6fdb..d71f301 100644 --- a/multisite/management/commands/update_public_suffix_list.py +++ b/multisite/management/commands/update_public_suffix_list.py @@ -1,7 +1,3 @@ -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import absolute_import - import logging import os import tempfile diff --git a/multisite/managers.py b/multisite/managers.py index c4fee11..df94697 100644 --- a/multisite/managers.py +++ b/multisite/managers.py @@ -1,10 +1,7 @@ # -*- coding: utf-8 -* -from __future__ import unicode_literals -from __future__ import absolute_import - -from django.db import models from django.contrib.sites import managers -from django.db.models.fields import FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist +from django.db import models from django.db.models.sql import constants diff --git a/multisite/middleware.py b/multisite/middleware.py index 5f37828..f444160 100644 --- a/multisite/middleware.py +++ b/multisite/middleware.py @@ -1,47 +1,29 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals -from __future__ import absolute_import - import os import tempfile -try: - from urlparse import urlsplit, urlunsplit -except ImportError: - from urllib.parse import urlsplit, urlunsplit +from urllib.parse import urlsplit, urlunsplit import django from django.conf import settings from django.contrib.sites.models import Site, SITE_CACHE from django.core.exceptions import DisallowedHost from django.core import mail - from django.core.cache import caches - -try: - # Django > 1.10 uses MiddlewareMixin - from django.utils.deprecation import MiddlewareMixin -except ImportError: - MiddlewareMixin = object - from django.core.exceptions import ImproperlyConfigured - -try: - from django.urls import get_callable -except ImportError: - # Django < 1.10 compatibility - from django.core.urlresolvers import get_callable - +from django.urls import get_callable from django.db.models.signals import pre_save, post_delete, post_init from django.http import Http404, HttpResponsePermanentRedirect from hashlib import md5 as md5_constructor +from django.utils.deprecation import MiddlewareMixin + from .models import Alias class DynamicSiteMiddleware(MiddlewareMixin): - def __init__(self, *args, **kwargs): - super(DynamicSiteMiddleware, self).__init__(*args, **kwargs) + def __init__(self, get_response=None): + super().__init__(get_response) if not hasattr(settings.SITE_ID, 'set'): raise TypeError('Invalid type for settings.SITE_ID: %s' % type(settings.SITE_ID).__name__) @@ -222,8 +204,8 @@ def site_deleted_hook(self, *args, **kwargs): class CookieDomainMiddleware(MiddlewareMixin): - def __init__(self, *args, **kwargs): - super(CookieDomainMiddleware, self).__init__(*args, **kwargs) + def __init__(self, get_response=None): + super().__init__(get_response) self.depth = int(getattr(settings, 'MULTISITE_COOKIE_DOMAIN_DEPTH', 0)) if self.depth < 0: raise ValueError( diff --git a/multisite/migrations/0002_auto_20210520_1618.py b/multisite/migrations/0002_auto_20210520_1618.py new file mode 100644 index 0000000..994682b --- /dev/null +++ b/multisite/migrations/0002_auto_20210520_1618.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.23 on 2021-05-20 16:18 + +from django.db import migrations, models +import multisite.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('multisite', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='alias', + name='is_canonical', + field=models.BooleanField(default=None, editable=False, help_text='Does this domain name match the one in site?', null=True, validators=[multisite.models.validate_true_or_none], verbose_name='is canonical?'), + ), + ] diff --git a/multisite/models.py b/multisite/models.py index 48b493d..eb2cfb5 100644 --- a/multisite/models.py +++ b/multisite/models.py @@ -1,6 +1,3 @@ -from __future__ import unicode_literals -from __future__ import absolute_import - import operator from functools import reduce @@ -11,15 +8,10 @@ from django.db.models import Q from django.db.models.signals import pre_save, post_save from django.db.models.signals import post_migrate -from django.utils.encoding import python_2_unicode_compatible -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from .hacks import use_framework_for_site_cache -try: - xrange -except NameError: # python3 - xrange = range _site_domain = Site._meta.get_field('domain') @@ -85,7 +77,7 @@ def _expand_netloc(cls, host, port=None): bits = host.split('.') result = [] - for i in xrange(0, (len(bits) + 1)): + for i in range(0, (len(bits) + 1)): if i == 0: host = '.'.join(bits[i:]) else: @@ -149,7 +141,6 @@ def validate_true_or_none(value): raise ValidationError(u'%r must be True or None' % value) -@python_2_unicode_compatible class Alias(models.Model): """ Model for domain-name aliases for Site objects. @@ -168,9 +159,11 @@ class Alias(models.Model): site = models.ForeignKey( Site, related_name='aliases', on_delete=models.CASCADE ) - is_canonical = models.NullBooleanField( + is_canonical = models.BooleanField( _('is canonical?'), - default=None, editable=False, + default=None, + editable=False, + null=True, validators=[validate_true_or_none], help_text=_('Does this domain name match the one in site?'), ) diff --git a/multisite/template/loaders/filesystem.py b/multisite/template/loaders/filesystem.py index b061754..772e8df 100644 --- a/multisite/template/loaders/filesystem.py +++ b/multisite/template/loaders/filesystem.py @@ -1,25 +1,20 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals -from __future__ import absolute_import - import os + from django.conf import settings from django.contrib.sites.models import Site from django.template.loaders.filesystem import Loader as FilesystemLoader -from django import VERSION as django_version class Loader(FilesystemLoader): def get_template_sources(self, *args, **kwargs): template_name = args[0] domain = Site.objects.get_current().domain - default_dir = getattr(settings, 'MULTISITE_DEFAULT_TEMPLATE_DIR', - 'default') + default_dir = getattr( + settings, 'MULTISITE_DEFAULT_TEMPLATE_DIR', 'default' + ) for tname in (os.path.join(domain, template_name), os.path.join(default_dir, template_name)): - if django_version < (2, 0, 0): - args = [tname, None] - else: - args = [tname] + args = [tname] for item in super(Loader, self).get_template_sources(*args, **kwargs): yield item diff --git a/multisite/template_loader.py b/multisite/template_loader.py index 5461d86..1270a68 100644 --- a/multisite/template_loader.py +++ b/multisite/template_loader.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals -from __future__ import absolute_import from .template.loaders.filesystem import Loader diff --git a/multisite/test_settings.py b/multisite/test_settings.py index 0f437b7..7519daa 100644 --- a/multisite/test_settings.py +++ b/multisite/test_settings.py @@ -1,4 +1,3 @@ -import django from multisite import SiteID SECRET_KEY = "iufoj=mibkpdz*%bob952x(%49rqgv8gg45k36kjcg76&-y5=!" @@ -21,11 +20,5 @@ MIDDLEWARE = [ 'multisite.middleware.DynamicSiteMiddleware', ] -if django.VERSION < (1,10,0): - # we are backwards compatible, but the settings file format has changed post-1.10: - # https://docs.djangoproject.com/en/1.10/topics/http/middleware/#upgrading-pre-django-1-10-style-middleware - MIDDLEWARE_CLASSES = list(MIDDLEWARE) - del MIDDLEWARE - TEST_RUNNER = 'django.test.runner.DiscoverRunner' diff --git a/multisite/tests.py b/multisite/tests.py index 2b46b4a..11fb6b7 100644 --- a/multisite/tests.py +++ b/multisite/tests.py @@ -9,26 +9,18 @@ This file uses relative imports and so cannot be run standalone. """ - -from __future__ import unicode_literals -from __future__ import absolute_import - import django import logging import os import pytest import sys import tempfile -import warnings -from unittest import skipUnless +from io import StringIO +from unittest import mock, skipUnless -try: - from unittest import mock -except ImportError: - import mock +from django.urls import re_path from django.conf import settings -from django.conf.urls import url from django.contrib.sites.models import Site from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.management import call_command @@ -36,9 +28,8 @@ from django.template.loader import get_template from django.test import TestCase, override_settings from django.test.client import RequestFactory as DjangoRequestFactory -from django.utils.six import StringIO -from multisite import SiteDomain, SiteID, threadlocals +from multisite import SiteDomain, SiteID from .hacks import use_framework_for_site_cache from .hosts import ALLOWED_HOSTS, AllowedHosts, IterableLazyObject @@ -79,7 +70,7 @@ def test_get_current_site(self): # So create one: # (This is only used by test_integration) urlpatterns = [ - url(r'^domain/$', lambda request, *args, **kwargs: HttpResponse(str(Site.objects.get_current()))) + re_path(r'^domain/$', lambda request, *args, **kwargs: HttpResponse(str(Site.objects.get_current()))) ] @pytest.mark.django_db diff --git a/multisite/threadlocals.py b/multisite/threadlocals.py index ec57c5f..47e8cad 100644 --- a/multisite/threadlocals.py +++ b/multisite/threadlocals.py @@ -1,17 +1,7 @@ # -*- coding: utf-8 -* -from __future__ import unicode_literals -from __future__ import absolute_import - import sys - -from django.utils import six from contextlib import contextmanager -from warnings import warn - -try: - from threading import local -except ImportError: - from django.utils._threading_local import local +from threading import local from django.core.exceptions import ImproperlyConfigured @@ -46,7 +36,7 @@ def __init__(self, default=None, *args, **kwargs): ``default``, if specified, determines the default SITE_ID, if that is unset. """ - if default is not None and not isinstance(default, six.integer_types): + if default is not None and not isinstance(default, int): raise ValueError("%r is not a valid default." % default) self.default = default self.reset() @@ -63,21 +53,21 @@ def __int__(self): return self.site_id def __lt__(self, other): - if isinstance(other, six.integer_types): + if isinstance(other, int): return self.__int__() < other elif isinstance(other, SiteID): return self.__int__() < other.__int__() return True def __le__(self, other): - if isinstance(other, six.integer_types): + if isinstance(other, int): return self.__int__() <= other elif isinstance(other, SiteID): return self.__int__() <= other.__int__() return True def __eq__(self, other): - if isinstance(other, six.integer_types): + if isinstance(other, int): return self.__int__() == other elif isinstance(other, SiteID): return self.__int__() == other.__int__() @@ -137,7 +127,7 @@ def __init__(self, default, *args, **kwargs): # http://python-future.org/compatible_idioms.html#basestring and # https://github.com/PythonCharmers/python-future/blob/master/src/past/types/basestring.py # are not super informative, so just fall back on a literal version check: - if not isinstance(default, basestring if sys.version_info.major == 2 else str): + if not isinstance(default, str): raise TypeError("%r is not a valid default domain." % default) self.default_domain = default self.default = None diff --git a/setup.py b/setup.py index 757fc44..80f6390 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,13 @@ import os -import sys from setuptools import find_packages, setup _dir_ = os.path.dirname(__file__) - -if sys.version_info < (3, 4): - install_requires = ['Django>=1.8,<2.0', 'tldextract>=1.2'] -else: - install_requires = ['Django>=1.8,<2.3', 'tldextract>=1.2'] +install_requires = [ + 'Django>=2.2,<4.0', + 'tldextract>=1.2,<3' +] def long_description(): @@ -17,6 +15,7 @@ def long_description(): with open(os.path.join(_dir_, 'README.rst')) as f: return f.read() + here = os.path.abspath(_dir_) version = {} with open(os.path.join(here, 'multisite', '__version__.py')) as f: @@ -49,10 +48,10 @@ def long_description(): 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Software Development :: Libraries', diff --git a/tox.ini b/tox.ini index 044bc32..4425f52 100644 --- a/tox.ini +++ b/tox.ini @@ -8,9 +8,10 @@ setenv= PYTHONPATH = {toxinidir}:{env:PYTHONPATH:} usedevelop = True envlist = - py36-django{2.2,2.1,2.0,1.11} - py35-django{2.1,2.0,1.11,1.10,1.9,1.8} - py27-django{1.11,1.10,1.9,1.8} + py36-django{2.2,3.0,3.1,3.2} + py37-django{2.2,3.0,3.1,3.2} + py38-django{2.2,3.0,3.1,3.2} + py39-django{2.2,3.0,3.1,3.2} [testenv] commands = pytest --cov --cov-config .coveragerc --pyargs multisite @@ -21,11 +22,7 @@ deps = pytest-pythonpath pytest-django - py27: mock django2.2: Django>=2.2,<2.3 - django2.1: Django>=2.1,<2.2 - django2.0: Django>=2.0,<2.1 - django1.11: Django>=1.11,<2.0 - django1.10: Django>=1.10,<1.11 - django1.9: Django>=1.9,<1.10 - django1.8: Django>=1.8,<1.9 + django3.0: Django>=3.0,<3.1 + django3.1: Django>=3.1,<3.2 + django3.2: Django>=3.2,<4.0