Resolved conflicts
This commit is contained in:
commit
73fdfdff8e
|
@ -12,11 +12,11 @@ env:
|
|||
- DJANGO=1.8
|
||||
- DJANGO=1.9
|
||||
- DJANGO=1.10
|
||||
- DJANGO=1.11
|
||||
matrix:
|
||||
fast_finish: true
|
||||
script: tox
|
||||
before_script: psql -c "CREATE DATABASE dts_test_project;" -U postgres
|
||||
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: bcarneiro
|
||||
|
|
|
@ -11,8 +11,6 @@
|
|||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import datetime
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
|
@ -94,8 +92,8 @@ pygments_style = 'sphinx'
|
|||
|
||||
intersphinx_mapping = {
|
||||
'django': (
|
||||
'https://docs.djangoproject.com/en/1.10/',
|
||||
'https://docs.djangoproject.com/en/1.10/_objects/'),
|
||||
'https://docs.djangoproject.com/en/1.11/',
|
||||
'https://docs.djangoproject.com/en/1.11/_objects/'),
|
||||
}
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
==================
|
||||
=====
|
||||
Tests
|
||||
==================
|
||||
=====
|
||||
|
||||
Running the tests
|
||||
-----------------
|
||||
Run these tests from the project ``dts_test_project``, it comes prepacked with the correct settings file and extra apps to enable tests to ensure different apps can exist in ``SHARED_APPS`` and ``TENANT_APPS``.
|
||||
|
@ -27,7 +28,7 @@ Because django will not create tenants for you during your tests, we have packed
|
|||
class BaseSetup(TenantTestCase):
|
||||
def setUp(self):
|
||||
self.c = TenantClient(self.tenant)
|
||||
|
||||
|
||||
def test_user_profile_view(self):
|
||||
response = self.c.get(reverse('user_profile'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
|
17
docs/use.rst
17
docs/use.rst
|
@ -1,15 +1,16 @@
|
|||
===========================
|
||||
Using django-tenant-schemas
|
||||
===========================
|
||||
|
||||
Supported versions
|
||||
------------------
|
||||
You can use ``django-tenant-schemas`` with currently maintained versions of Django -- see the `Django's release process <https://docs.djangoproject.com/en/1.10/internals/release-process/>`_ and the present list of `Supported Versions <https://www.djangoproject.com/download/#supported-versions>`_.
|
||||
You can use ``django-tenant-schemas`` with currently maintained versions of Django -- see the `Django's release process <https://docs.djangoproject.com/en/1.11/internals/release-process/>`_ and the present list of `Supported Versions <https://www.djangoproject.com/download/#supported-versions>`_.
|
||||
|
||||
It is necessary to use a PostgreSQL database. ``django-tenant-schemas`` will ensure compatibility with the minimum required version of the latest Django release. At this time that is PostgreSQL 9.2, the minimum for Django 1.10.
|
||||
It is necessary to use a PostgreSQL database. ``django-tenant-schemas`` will ensure compatibility with the minimum required version of the latest Django release. At this time that is PostgreSQL 9.3, the minimum for Django 1.11.
|
||||
|
||||
Creating a Tenant
|
||||
-----------------
|
||||
Creating a tenant works just like any other model in django. The first thing we should do is to create the ``public`` tenant to make our main website available. We'll use the previous model we defined for ``Client``.
|
||||
Creating a tenant works just like any other model in Django. The first thing we should do is to create the ``public`` tenant to make our main website available. We'll use the previous model we defined for ``Client``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -43,7 +44,7 @@ Any call to the methods ``filter``, ``get``, ``save``, ``delete`` or any other f
|
|||
|
||||
Management commands
|
||||
-------------------
|
||||
Every command except tenant_command runs by default on all tenants. You can also create your own commands that run on every tenant by inheriting ``BaseTenantCommand``.
|
||||
By default, base commands run on the public tenant but you can also own commands that run on a specific tenant by inheriting ``BaseTenantCommand``.
|
||||
|
||||
For example, if you have the following ``do_foo`` command in the ``foo`` app:
|
||||
|
||||
|
@ -57,7 +58,7 @@ For example, if you have the following ``do_foo`` command in the ``foo`` app:
|
|||
def handle(self, *args, **options):
|
||||
do_foo()
|
||||
|
||||
You could create a wrapper command ``tenant_do_foo`` by using ``BaseTenantCommand`` like so:
|
||||
You could create a wrapper command by using ``BaseTenantCommand``:
|
||||
|
||||
``foo/management/commands/tenant_do_foo.py``
|
||||
|
||||
|
@ -68,11 +69,13 @@ You could create a wrapper command ``tenant_do_foo`` by using ``BaseTenantComman
|
|||
class Command(BaseTenantCommand):
|
||||
COMMAND_NAME = 'do_foo'
|
||||
|
||||
To run only a particular schema, there is an optional argument called ``--schema``.
|
||||
To run the command on a particular schema, there is an optional argument called ``--schema``.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./manage.py migrate_schemas --schema=customer1
|
||||
./manage.py tenant_command do_foo --schema=customer1
|
||||
|
||||
If you omit the ``schema`` argument, the interactive shell will ask you to select one.
|
||||
|
||||
migrate_schemas
|
||||
~~~~~~~~~~~~~~~
|
||||
|
|
|
@ -82,10 +82,10 @@ DATABASES = {
|
|||
'default': {
|
||||
'ENGINE': 'tenant_schemas.postgresql_backend',
|
||||
'NAME': os.environ.get('PG_NAME', 'dts_test_project'),
|
||||
'USER': os.environ.get('PG_USER', 'postgres'),
|
||||
'PASSWORD': os.environ.get('PG_PASSWORD', 'root'),
|
||||
'HOST': os.environ.get('PG_HOST', 'localhost'),
|
||||
'PORT': int(os.environ.get('PG_PORT', '5432')),
|
||||
'USER': os.environ.get('PG_USER'),
|
||||
'PASSWORD': os.environ.get('PG_PASSWORD'),
|
||||
'HOST': os.environ.get('PG_HOST'),
|
||||
'PORT': int(os.environ.get('PG_PORT')) if os.environ.get('PG_PORT') else None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 1.7.0
|
||||
current_version = 1.8.0
|
||||
commit = True
|
||||
tag = True
|
||||
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
import re
|
||||
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
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
|
||||
from tenant_schemas.postgresql_backend.introspection import DatabaseSchemaIntrospection
|
||||
from tenant_schemas.utils import get_limit_set_calls, get_public_schema_name
|
||||
|
||||
ORIGINAL_BACKEND = getattr(settings, 'ORIGINAL_BACKEND', 'django.db.backends.postgresql_psycopg2')
|
||||
|
||||
original_backend = import_module(ORIGINAL_BACKEND + '.base')
|
||||
# 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', [])
|
||||
|
||||
|
@ -109,12 +105,16 @@ class DatabaseWrapper(original_backend.DatabaseWrapper):
|
|||
category=DeprecationWarning)
|
||||
return self.tenant
|
||||
|
||||
def _cursor(self):
|
||||
def _cursor(self, name=None):
|
||||
"""
|
||||
Here it happens. We hope every Django db operation using PostgreSQL
|
||||
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
|
||||
# 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.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
|
||||
# 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
|
||||
# we do not have to worry that it's not the good one
|
||||
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):
|
||||
self.search_path_set = False
|
||||
else:
|
||||
self.search_path_set = True
|
||||
|
||||
if name:
|
||||
cursor_for_search_path.close()
|
||||
|
||||
return cursor
|
||||
|
||||
|
||||
|
@ -157,10 +169,3 @@ class FakeTenant:
|
|||
"""
|
||||
def __init__(self, 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
|
||||
|
||||
FieldInfo = namedtuple('FieldInfo', FieldInfo._fields + ('default',))
|
||||
fields = FieldInfo._fields
|
||||
if 'default' not in fields:
|
||||
fields += ('default',)
|
||||
|
||||
FieldInfo = namedtuple('FieldInfo', fields)
|
||||
|
||||
|
||||
class DatabaseSchemaIntrospection(BaseDatabaseIntrospection):
|
||||
|
|
|
@ -1,15 +1,29 @@
|
|||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.db import connection
|
||||
from django.test import TestCase
|
||||
|
||||
from tenant_schemas.utils import get_public_schema_name
|
||||
from tenant_schemas.utils import get_tenant_model
|
||||
from tenant_schemas.utils import get_public_schema_name, get_tenant_model
|
||||
|
||||
ALLOWED_TEST_DOMAIN = '.test.com'
|
||||
|
||||
|
||||
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
|
||||
def setUpClass(cls):
|
||||
cls.sync_shared()
|
||||
cls.add_allowed_test_domain()
|
||||
tenant_domain = 'tenant.test.com'
|
||||
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?
|
||||
|
@ -21,6 +35,7 @@ class TenantTestCase(TestCase):
|
|||
connection.set_schema_to_public()
|
||||
cls.tenant.delete()
|
||||
|
||||
cls.remove_allowed_test_domain()
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('DROP SCHEMA IF EXISTS test CASCADE')
|
||||
|
||||
|
@ -33,10 +48,10 @@ class TenantTestCase(TestCase):
|
|||
|
||||
|
||||
class FastTenantTestCase(TenantTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.sync_shared()
|
||||
cls.add_allowed_test_domain()
|
||||
tenant_domain = 'tenant.test.com'
|
||||
|
||||
TenantModel = get_tenant_model()
|
||||
|
@ -51,3 +66,4 @@ class FastTenantTestCase(TenantTestCase):
|
|||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
connection.set_schema_to_public()
|
||||
cls.remove_allowed_test_domain()
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import inspect
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.db import connection
|
||||
|
@ -20,6 +21,8 @@ class BaseTestCase(TestCase):
|
|||
'django.contrib.contenttypes',
|
||||
'django.contrib.auth', )
|
||||
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
|
||||
# a blank public schema for this set of tests.
|
||||
|
@ -29,6 +32,13 @@ class BaseTestCase(TestCase):
|
|||
% (get_public_schema_name(), get_public_schema_name()))
|
||||
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):
|
||||
connection.set_schema_to_public()
|
||||
super(BaseTestCase, self).setUp()
|
||||
|
|
4
tox.ini
4
tox.ini
|
@ -1,11 +1,12 @@
|
|||
[tox]
|
||||
envlist = py{27,35}-dj{18,19,110}-{standard,parallel}
|
||||
envlist = py{27,35}-dj{18,19,110,111}-{standard,parallel}
|
||||
|
||||
[travis:env]
|
||||
DJANGO =
|
||||
1.8: dj18-{standard,parallel}
|
||||
1.9: dj19-{standard,parallel}
|
||||
1.10: dj110-{standard,parallel}
|
||||
1.11: dj111-{standard,parallel}
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
|
@ -17,6 +18,7 @@ deps =
|
|||
dj18: Django~=1.8.0
|
||||
dj19: Django~=1.9.0
|
||||
dj110: Django~=1.10.0
|
||||
dj111: Django~=1.11rc1
|
||||
|
||||
changedir = dts_test_project
|
||||
|
||||
|
|
Loading…
Reference in New Issue