diff --git a/.travis.yml b/.travis.yml index 171aa155..0185f80e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,9 @@ language: python cache: pip python: - - 3.6 - - 3.7 - - 3.8 - - 3.9 + - "3.9" + - "3.11" + - "3.12" services: - postgresql @@ -23,10 +22,9 @@ script: - tox env: - - DJANGO=2.2 - - DJANGO=3.1 - DJANGO=3.2 - - DJANGO=4.0 + - DJANGO=4.2 + - DJANGO=5.1 deploy: provider: pypi diff --git a/dts_test_project/dts_test_app/migrations/0005_test_alter_autofield.py b/dts_test_project/dts_test_app/migrations/0005_test_alter_autofield.py new file mode 100644 index 00000000..ddc684d4 --- /dev/null +++ b/dts_test_project/dts_test_app/migrations/0005_test_alter_autofield.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dts_test_app', '0004_test_alter_unique'), + ] + + operations = [ + migrations.AlterField( + model_name='DummyModel', + name='id', + field=models.IntegerField() + ), + migrations.AlterField( + model_name='DummyModel', + name='id', + field=models.AutoField() + ), + ] diff --git a/dts_test_project/dts_test_project/settings.py b/dts_test_project/dts_test_project/settings.py index eec1bc21..31455034 100644 --- a/dts_test_project/dts_test_project/settings.py +++ b/dts_test_project/dts_test_project/settings.py @@ -10,6 +10,7 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os +import django BASE_DIR = os.path.dirname(os.path.dirname(__file__)) @@ -24,7 +25,19 @@ ALLOWED_HOSTS = [] -DEFAULT_FILE_STORAGE = "tenant_schemas.storage.TenantFileSystemStorage" +if django.VERSION > (4, 2): + STORAGES = { + "default": { + "BACKEND": "tenant_schemas.storage.TenantStaticFilesStorage" + }, + "staticfiles": { + "BACKEND": "tenant_schemas.storage.TenantStaticFilesStorage" + } + } +else: + STATICFILES_STORAGE = "tenant_schemas.storage.TenantStaticFilesStorage" + DEFAULT_FILE_STORAGE = "tenant_schemas.storage.TenantFileSystemStorage" + # Application definition @@ -126,7 +139,7 @@ STATIC_URL = "/static/" -STATICFILES_STORAGE = "tenant_schemas.storage.TenantStaticFilesStorage" +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" LOGGING = { "version": 1, diff --git a/setup.py b/setup.py index b09c3cb0..f28bfdd9 100755 --- a/setup.py +++ b/setup.py @@ -16,18 +16,17 @@ classifiers=[ "License :: OSI Approved :: MIT License", "Framework :: Django", - "Framework :: Django :: 1.11", - "Framework :: Django :: 2.0", - "Framework :: Django :: 2.1", - "Framework :: Django :: 2.2", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.1", "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Database", "Topic :: Software Development :: Libraries", ], - install_requires=["Django>=2.2", "ordered-set", "psycopg2-binary", "six"], + install_requires=["Django>=3.2", "ordered-set", "psycopg2-binary"], setup_requires=["setuptools-scm"], use_scm_version=True, zip_safe=False, diff --git a/tenant_schemas/apps.py b/tenant_schemas/apps.py index 6b16b621..fbec1fa1 100644 --- a/tenant_schemas/apps.py +++ b/tenant_schemas/apps.py @@ -1,3 +1,4 @@ +import django from django.apps import AppConfig, apps from django.conf import settings from django.core.checks import Critical, Error, Warning, register @@ -93,11 +94,24 @@ def best_practice(app_configs, **kwargs): hint=[a for a in settings.SHARED_APPS if a in delta], id="tenant_schemas.E003")) + if django.VERSION >= (4, 2): + storage_hint = "Set settings.STORAGES to %s" % { + "default": { + "BACKEND": "tenant_schemas.storage.TenantStaticFilesStorage" + }, + "staticfiles": { + "BACKEND": "tenant_schemas.storage.TenantStaticFilesStorage" + } + } + else: + storage_hint = ( + "Set settings.DEFAULT_FILE_STORAGE to " + "'tenant_schemas.storage.TenantFileSystemStorage'" + ) if not isinstance(default_storage, TenantStorageMixin): errors.append(Warning( "Your default storage engine is not tenant aware.", - hint="Set settings.DEFAULT_FILE_STORAGE to " - "'tenant_schemas.storage.TenantFileSystemStorage'", + hint=storage_hint, id="tenant_schemas.W003" )) diff --git a/tenant_schemas/management/commands/__init__.py b/tenant_schemas/management/commands/__init__.py index b0df26dc..863c92c5 100644 --- a/tenant_schemas/management/commands/__init__.py +++ b/tenant_schemas/management/commands/__init__.py @@ -1,4 +1,3 @@ -import django from django.conf import settings from django.core.management import ( call_command, @@ -7,7 +6,6 @@ ) from django.core.management.base import BaseCommand, CommandError from django.db import connection -from six.moves import input from tenant_schemas.utils import get_public_schema_name, get_tenant_model diff --git a/tenant_schemas/postgresql_backend/introspection.py b/tenant_schemas/postgresql_backend/introspection.py index d6fb9105..3ce4efa0 100644 --- a/tenant_schemas/postgresql_backend/introspection.py +++ b/tenant_schemas/postgresql_backend/introspection.py @@ -315,3 +315,35 @@ def get_constraints(self, cursor, table_name): "options": options, } return constraints + + _get_sequences_query = """ + SELECT + s.relname AS sequence_name, + a.attname AS colname + FROM + pg_class s + JOIN pg_depend d ON d.objid = s.oid + AND d.classid = 'pg_class'::regclass + AND d.refclassid = 'pg_class'::regclass + JOIN pg_attribute a ON d.refobjid = a.attrelid + AND d.refobjsubid = a.attnum + JOIN pg_class tbl ON tbl.oid = d.refobjid + AND tbl.relname = %(table)s + AND pg_catalog.pg_table_is_visible(tbl.oid) + JOIN pg_namespace n ON n.oid = tbl.relnamespace + WHERE + s.relkind = 'S' + AND n.nspname = %(schema)s; + """ + + def get_sequences(self, cursor, table_name, table_fields=()): + sequences = [] + cursor.execute(self._get_sequences_query, { + 'schema': self.connection.schema_name, + 'table': table_name, + }) + + for row in cursor.fetchall(): + sequences.append({'name': row[0], 'table': table_name, 'column': row[1]}) + return sequences + diff --git a/tenant_schemas/tests/test_routes.py b/tenant_schemas/tests/test_routes.py index ff3bc760..38ff6661 100644 --- a/tenant_schemas/tests/test_routes.py +++ b/tenant_schemas/tests/test_routes.py @@ -1,6 +1,3 @@ -import unittest - -import six from django.conf import settings from django.core.exceptions import DisallowedHost from django.http import Http404 @@ -19,7 +16,6 @@ class MissingDefaultTenantMiddleware(DefaultTenantMiddleware): DEFAULT_SCHEMA_NAME = "missing" -@unittest.skipIf(six.PY2, "Unexpectedly failing only on Python 2.7") class RoutesTestCase(BaseTestCase): @classmethod def setUpClass(cls): @@ -41,7 +37,7 @@ def setUp(self): super(RoutesTestCase, self).setUp() self.factory = RequestFactory() self.tm = TenantMiddleware(lambda r:r) - self.dtm = DefaultTenantMiddleware() + self.dtm = DefaultTenantMiddleware(lambda r:r) self.tenant_domain = "tenant.test.com" self.tenant = Tenant(domain_url=self.tenant_domain, schema_name="test") @@ -84,7 +80,7 @@ def test_non_existent_tenant_to_default_schema_routing(self): def test_non_existent_tenant_custom_middleware(self): """Route unrecognised hostnames to the 'test' tenant.""" - dtm = TestDefaultTenantMiddleware() + dtm = TestDefaultTenantMiddleware(lambda r:r) request = self.factory.get( self.url, HTTP_HOST=self.non_existent_tenant.domain_url ) @@ -94,7 +90,7 @@ def test_non_existent_tenant_custom_middleware(self): def test_non_existent_tenant_and_default_custom_middleware(self): """Route unrecognised hostnames to the 'missing' tenant.""" - dtm = MissingDefaultTenantMiddleware() + dtm = MissingDefaultTenantMiddleware(lambda r:r) request = self.factory.get( self.url, HTTP_HOST=self.non_existent_tenant.domain_url ) diff --git a/tox.ini b/tox.ini index aed42f0b..d6145698 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ [tox] envlist = - py{36,37,38,39}-dj{22,31,32}-{standard,parallel} + py{39,311,312}-dj{32,42,51}-{standard,parallel} skip_missing_interpreters = true [travis:env] DJANGO = - 2.2: dj22-{standard,parallel} - 3.1: dj31-{standard,parallel} 3.2: dj32-{standard,parallel} + 4.2: dj42-{standard,parallel} + 5.1: dj51-{standard,parallel} [docker:db] image = postgres:9.6 @@ -22,17 +22,17 @@ deps = coverage mock tblib - psycopg2-binary<=2.8.6 - dj22: Django>=2.2a1,<3.0 - dj31: Django>=3.1a1,<3.2 + psycopg2 dj32: Django>=3.2a1,<4.0 + dj42: Django>=4.2,<4.3 + dj51: Django>=5.1,<5.2 docker = pg96: db changedir = dts_test_project -passenv = PG_NAME PG_USER PG_PASSWORD PG_HOST PG_PORT +passenv = PG_NAME,PG_USER,PG_PASSWORD,PG_HOST,PG_PORT setenv = standard: MIGRATION_EXECUTOR=standard