Django 1.11 support (#463)
* Added Django 1.11 to Travis - related to #435 - 😉 to tomturner/django-tenants#120 * Replace direct use of import_module by proper load_backend Handle new 'django.db.backends.postgresql' name (Django 1.9) * Fix 'default' already present in _fields in Django 1.11 * Fix not existing access to original_backend.DatabaseError (Django 1.11) Removed exports because django.db.utils.DatabaseError and django.db.utils.IntegrityError must be used instead. This change must be stated in release notes. * The signature of DatabaseWrapper._cursor has changed in Django 1.11 Use the public API * Add missing allowed hosts for tests * Don't override default user or blocks Unix socket DB connection * Updated tox.ini for Django 1.11rc1 * Properly add '.test.com' to ALLOWED_HOSTS in test cases * Internal tests don't use TenantTestCase so set allowed hosts manually * Drop Django 1.9 support Django 1.8 is still within LTS until the end of 2017.
This commit is contained in:
parent
26692945c1
commit
36e1ba3800
|
@ -14,8 +14,8 @@ script:
|
||||||
- tox -e py${TRAVIS_PYTHON_VERSION/./}-dj${DJANGO/./}
|
- tox -e py${TRAVIS_PYTHON_VERSION/./}-dj${DJANGO/./}
|
||||||
env:
|
env:
|
||||||
- DJANGO=1.8
|
- DJANGO=1.8
|
||||||
- DJANGO=1.9
|
|
||||||
- DJANGO=1.10
|
- DJANGO=1.10
|
||||||
|
- DJANGO=1.11
|
||||||
deploy:
|
deploy:
|
||||||
provider: pypi
|
provider: pypi
|
||||||
user: bcarneiro
|
user: bcarneiro
|
||||||
|
|
|
@ -82,10 +82,10 @@ DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'tenant_schemas.postgresql_backend',
|
'ENGINE': 'tenant_schemas.postgresql_backend',
|
||||||
'NAME': os.environ.get('PG_NAME', 'dts_test_project'),
|
'NAME': os.environ.get('PG_NAME', 'dts_test_project'),
|
||||||
'USER': os.environ.get('PG_USER', 'postgres'),
|
'USER': os.environ.get('PG_USER'),
|
||||||
'PASSWORD': os.environ.get('PG_PASSWORD', 'root'),
|
'PASSWORD': os.environ.get('PG_PASSWORD'),
|
||||||
'HOST': os.environ.get('PG_HOST', 'localhost'),
|
'HOST': os.environ.get('PG_HOST'),
|
||||||
'PORT': int(os.environ.get('PG_PORT', '5432')),
|
'PORT': int(os.environ.get('PG_PORT')) if os.environ.get('PG_PORT') else None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,17 @@
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from django.conf import settings
|
|
||||||
try:
|
|
||||||
# Django versions >= 1.9
|
|
||||||
from django.utils.module_loading import import_module
|
|
||||||
except ImportError:
|
|
||||||
# Django versions < 1.9
|
|
||||||
from django.utils.importlib import import_module
|
|
||||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
|
||||||
from tenant_schemas.utils import get_public_schema_name, get_limit_set_calls
|
|
||||||
from tenant_schemas.postgresql_backend.introspection import DatabaseSchemaIntrospection
|
|
||||||
import django.db.utils
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
|
||||||
ORIGINAL_BACKEND = getattr(settings, 'ORIGINAL_BACKEND', 'django.db.backends.postgresql_psycopg2')
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||||
|
import django.db.utils
|
||||||
|
|
||||||
original_backend = import_module(ORIGINAL_BACKEND + '.base')
|
from tenant_schemas.utils import get_public_schema_name, get_limit_set_calls
|
||||||
|
from tenant_schemas.postgresql_backend.introspection import DatabaseSchemaIntrospection
|
||||||
|
|
||||||
|
ORIGINAL_BACKEND = getattr(settings, 'ORIGINAL_BACKEND', 'django.db.backends.postgresql_psycopg2')
|
||||||
|
# Django 1.9+ takes care to rename the default backend to 'django.db.backends.postgresql'
|
||||||
|
original_backend = django.db.utils.load_backend(ORIGINAL_BACKEND)
|
||||||
|
|
||||||
EXTRA_SEARCH_PATHS = getattr(settings, 'PG_EXTRA_SEARCH_PATHS', [])
|
EXTRA_SEARCH_PATHS = getattr(settings, 'PG_EXTRA_SEARCH_PATHS', [])
|
||||||
|
|
||||||
|
@ -109,12 +105,16 @@ class DatabaseWrapper(original_backend.DatabaseWrapper):
|
||||||
category=DeprecationWarning)
|
category=DeprecationWarning)
|
||||||
return self.tenant
|
return self.tenant
|
||||||
|
|
||||||
def _cursor(self):
|
def _cursor(self, name=None):
|
||||||
"""
|
"""
|
||||||
Here it happens. We hope every Django db operation using PostgreSQL
|
Here it happens. We hope every Django db operation using PostgreSQL
|
||||||
must go through this to get the cursor handle. We change the path.
|
must go through this to get the cursor handle. We change the path.
|
||||||
"""
|
"""
|
||||||
cursor = super(DatabaseWrapper, self)._cursor()
|
if name:
|
||||||
|
# Only supported and required by Django 1.11 (server-side cursor)
|
||||||
|
cursor = super(DatabaseWrapper, self)._cursor(name=name)
|
||||||
|
else:
|
||||||
|
cursor = super(DatabaseWrapper, self)._cursor()
|
||||||
|
|
||||||
# optionally limit the number of executions - under load, the execution
|
# optionally limit the number of executions - under load, the execution
|
||||||
# of `set search_path` can be quite time consuming
|
# of `set search_path` can be quite time consuming
|
||||||
|
@ -137,16 +137,28 @@ class DatabaseWrapper(original_backend.DatabaseWrapper):
|
||||||
search_paths = [self.schema_name]
|
search_paths = [self.schema_name]
|
||||||
|
|
||||||
search_paths.extend(EXTRA_SEARCH_PATHS)
|
search_paths.extend(EXTRA_SEARCH_PATHS)
|
||||||
|
|
||||||
|
if name:
|
||||||
|
# Named cursor can only be used once
|
||||||
|
cursor_for_search_path = self.connection.cursor()
|
||||||
|
else:
|
||||||
|
# Reuse
|
||||||
|
cursor_for_search_path = cursor
|
||||||
|
|
||||||
# In the event that an error already happened in this transaction and we are going
|
# In the event that an error already happened in this transaction and we are going
|
||||||
# to rollback we should just ignore database error when setting the search_path
|
# to rollback we should just ignore database error when setting the search_path
|
||||||
# if the next instruction is not a rollback it will just fail also, so
|
# if the next instruction is not a rollback it will just fail also, so
|
||||||
# we do not have to worry that it's not the good one
|
# we do not have to worry that it's not the good one
|
||||||
try:
|
try:
|
||||||
cursor.execute('SET search_path = {0}'.format(','.join(search_paths)))
|
cursor_for_search_path.execute('SET search_path = {0}'.format(','.join(search_paths)))
|
||||||
except (django.db.utils.DatabaseError, psycopg2.InternalError):
|
except (django.db.utils.DatabaseError, psycopg2.InternalError):
|
||||||
self.search_path_set = False
|
self.search_path_set = False
|
||||||
else:
|
else:
|
||||||
self.search_path_set = True
|
self.search_path_set = True
|
||||||
|
|
||||||
|
if name:
|
||||||
|
cursor_for_search_path.close()
|
||||||
|
|
||||||
return cursor
|
return cursor
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,10 +169,3 @@ class FakeTenant:
|
||||||
"""
|
"""
|
||||||
def __init__(self, schema_name):
|
def __init__(self, schema_name):
|
||||||
self.schema_name = schema_name
|
self.schema_name = schema_name
|
||||||
|
|
||||||
if ORIGINAL_BACKEND == "django.contrib.gis.db.backends.postgis":
|
|
||||||
DatabaseError = django.db.utils.DatabaseError
|
|
||||||
IntegrityError = psycopg2.IntegrityError
|
|
||||||
else:
|
|
||||||
DatabaseError = original_backend.DatabaseError
|
|
||||||
IntegrityError = original_backend.IntegrityError
|
|
||||||
|
|
|
@ -7,7 +7,11 @@ from django.db.backends.base.introspection import (
|
||||||
)
|
)
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
FieldInfo = namedtuple('FieldInfo', FieldInfo._fields + ('default',))
|
fields = FieldInfo._fields
|
||||||
|
if 'default' not in fields:
|
||||||
|
fields += ('default',)
|
||||||
|
|
||||||
|
FieldInfo = namedtuple('FieldInfo', fields)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseSchemaIntrospection(BaseDatabaseIntrospection):
|
class DatabaseSchemaIntrospection(BaseDatabaseIntrospection):
|
||||||
|
|
|
@ -1,15 +1,30 @@
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
|
from django.conf import settings
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from tenant_schemas.utils import get_public_schema_name
|
from tenant_schemas.utils import get_public_schema_name
|
||||||
from tenant_schemas.utils import get_tenant_model
|
from tenant_schemas.utils import get_tenant_model
|
||||||
|
|
||||||
|
ALLOWED_TEST_DOMAIN = '.test.com'
|
||||||
|
|
||||||
|
|
||||||
class TenantTestCase(TestCase):
|
class TenantTestCase(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def add_allowed_test_domain(cls):
|
||||||
|
# ALLOWED_HOSTS is a special setting of Django setup_test_environment so we can't modify it with helpers
|
||||||
|
if ALLOWED_TEST_DOMAIN not in settings.ALLOWED_HOSTS:
|
||||||
|
settings.ALLOWED_HOSTS += [ALLOWED_TEST_DOMAIN]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def remove_allowed_test_domain(cls):
|
||||||
|
if ALLOWED_TEST_DOMAIN in settings.ALLOWED_HOSTS:
|
||||||
|
settings.ALLOWED_HOSTS.remove(ALLOWED_TEST_DOMAIN)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
cls.sync_shared()
|
cls.sync_shared()
|
||||||
|
cls.add_allowed_test_domain()
|
||||||
tenant_domain = 'tenant.test.com'
|
tenant_domain = 'tenant.test.com'
|
||||||
cls.tenant = get_tenant_model()(domain_url=tenant_domain, schema_name='test')
|
cls.tenant = get_tenant_model()(domain_url=tenant_domain, schema_name='test')
|
||||||
cls.tenant.save(verbosity=0) # todo: is there any way to get the verbosity from the test command here?
|
cls.tenant.save(verbosity=0) # todo: is there any way to get the verbosity from the test command here?
|
||||||
|
@ -21,6 +36,7 @@ class TenantTestCase(TestCase):
|
||||||
connection.set_schema_to_public()
|
connection.set_schema_to_public()
|
||||||
cls.tenant.delete()
|
cls.tenant.delete()
|
||||||
|
|
||||||
|
cls.remove_allowed_test_domain()
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
cursor.execute('DROP SCHEMA IF EXISTS test CASCADE')
|
cursor.execute('DROP SCHEMA IF EXISTS test CASCADE')
|
||||||
|
|
||||||
|
@ -33,10 +49,10 @@ class TenantTestCase(TestCase):
|
||||||
|
|
||||||
|
|
||||||
class FastTenantTestCase(TenantTestCase):
|
class FastTenantTestCase(TenantTestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
cls.sync_shared()
|
cls.sync_shared()
|
||||||
|
cls.add_allowed_test_domain()
|
||||||
tenant_domain = 'tenant.test.com'
|
tenant_domain = 'tenant.test.com'
|
||||||
|
|
||||||
TenantModel = get_tenant_model()
|
TenantModel = get_tenant_model()
|
||||||
|
@ -51,3 +67,4 @@ class FastTenantTestCase(TenantTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
connection.set_schema_to_public()
|
connection.set_schema_to_public()
|
||||||
|
cls.remove_allowed_test_domain()
|
||||||
|
|
|
@ -20,6 +20,8 @@ class BaseTestCase(TestCase):
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
'django.contrib.auth', )
|
'django.contrib.auth', )
|
||||||
settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS
|
settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS
|
||||||
|
if '.test.com' not in settings.ALLOWED_HOSTS:
|
||||||
|
settings.ALLOWED_HOSTS += ['.test.com']
|
||||||
|
|
||||||
# Django calls syncdb by default for the test database, but we want
|
# Django calls syncdb by default for the test database, but we want
|
||||||
# a blank public schema for this set of tests.
|
# a blank public schema for this set of tests.
|
||||||
|
@ -29,6 +31,13 @@ class BaseTestCase(TestCase):
|
||||||
% (get_public_schema_name(), get_public_schema_name()))
|
% (get_public_schema_name(), get_public_schema_name()))
|
||||||
super(BaseTestCase, cls).setUpClass()
|
super(BaseTestCase, cls).setUpClass()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super(BaseTestCase, cls).tearDownClass()
|
||||||
|
|
||||||
|
if '.test.com' in settings.ALLOWED_HOSTS:
|
||||||
|
settings.ALLOWED_HOSTS.remove('.test.com')
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
connection.set_schema_to_public()
|
connection.set_schema_to_public()
|
||||||
super(BaseTestCase, self).setUp()
|
super(BaseTestCase, self).setUp()
|
||||||
|
|
7
tox.ini
7
tox.ini
|
@ -1,21 +1,22 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py{27,35}-dj{18,19,110}
|
envlist = py{27,35}-dj{18,19,110,111}
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
usedevelop = True
|
usedevelop = True
|
||||||
|
|
||||||
deps =
|
deps =
|
||||||
coverage
|
coverage
|
||||||
mock
|
mock
|
||||||
tblib
|
tblib
|
||||||
dj18: Django~=1.8.0
|
dj18: Django~=1.8.0
|
||||||
dj19: Django~=1.9.0
|
dj19: Django~=1.9.0
|
||||||
dj110: Django~=1.10.0
|
dj110: Django~=1.10.0
|
||||||
|
dj111: Django~=1.11rc1
|
||||||
|
|
||||||
changedir = dts_test_project
|
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
|
||||||
|
|
||||||
commands =
|
commands =
|
||||||
coverage run manage.py test --noinput {posargs:tenant_schemas}
|
coverage run manage.py test --noinput {posargs:tenant_schemas}
|
||||||
coverage report -m --include=../tenant_schemas/*
|
coverage report -m --include=../tenant_schemas/*
|
||||||
|
|
Loading…
Reference in New Issue