Merge branch 'master' into master

This commit is contained in:
Adnan Umer 2017-05-26 15:19:05 +05:00 committed by GitHub
commit 24fd09d007
23 changed files with 360 additions and 76 deletions

View File

@ -1,21 +1,22 @@
sudo: false
language: python
python:
- 2.7
- 3.5
- 2.7
- 3.5
services:
- postgresql
- postgresql
addons:
postgresql: '9.4'
install:
- pip install tox
before_script:
- psql -c "CREATE DATABASE dts_test_project;" -U postgres
script:
- tox -e py${TRAVIS_PYTHON_VERSION/./}-dj${DJANGO/./}
install: pip install -q tox-travis
env:
- DJANGO=1.8
- DJANGO=1.10
- DJANGO=1.11
- 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

61
docs/advanced_usage.rst Normal file
View File

@ -0,0 +1,61 @@
==============
Advanced Usage
==============
Custom tenant strategies (custom middleware support)
====================================================
By default, ``django-tenant-schemas``'s strategies for determining the correct tenant involve extracting it from the URL (e.g. ``mytenant.mydomain.com``). This is done through a middleware, typically ``TenantMiddleware``.
In some situations, it might be useful to use **alternative tenant selection strategies**. For example, consider a website with a fixed URL. An approach for this website might be to pass the tenant through a special header, or to determine it in some other manner based on the request (e.g. using an OAuth token mapped to a tenant). ``django-tenant-schemas`` offer an **easily extensible way to provide your own middleware** with minimal code changes.
To add custom tenant selection strategies, you need to **subclass the** ``BaseTenantMiddleware`` **class and implement its** ``get_tenant`` **method**. This method accepts the current ``request`` object through which you can determine the tenant to use. In addition, for backwards-compatibility reasons, the method also accepts the tenant model class (``TENANT_MODEL``) and the ``hostname`` of the current request. **You should return an instance of your** ``TENANT_MODEL`` **class** from this function.
After creating your middleware, you should make it the top-most middleware in your list. You should only have one subclass of ``BaseTenantMiddleware`` per project.
Note that you might also wish to extend the other provided middleware classes, such as ``TenantMiddleware``. For example, you might want to chain several strategies together, and you could do so by subclassing the original strategies and manipulating the call to ``super``'s ``get_tenant``.
Example: Determine tenant from HTTP header
------------------------------------------
Suppose you wanted to determine the current tenant based on a request header (``X-DTS-SCHEMA``). You might implement a simple middleware such as:
.. code-block:: python
class XHeaderTenantMiddleware(BaseTenantMiddleware):
"""
Determines tenant by the value of the ``X-DTS-SCHEMA`` HTTP header.
"""
def get_tenant(self, model, hostname, request):
schema_name = request.META.get('HTTP_X_DTS_SCHEMA', get_public_schema_name())
return model.objects.get(schema_name=schema_name)
Your application could now specify the tenant with the ``X-DTS-SCHEMA`` HTTP header. In scenarios where you are configuring individual tenant websites by yourself, each with its own ``nginx`` configuration to redirect to the right tenant, you could use a configuration such as the one below:
.. code-block:: nginx
# /etc/nginx/conf.d/multitenant.conf
upstream web {
server localhost:8000;
}
server {
listen 80 default_server;
server_name _;
location / {
proxy_pass http://web;
proxy_set_header Host $host;
}
}
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://web;
proxy_set_header Host $host;
proxy_set_header X-DTS-SCHEMA example; # triggers XHeaderTenantMiddleware
}
}

View File

@ -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,
@ -24,7 +22,7 @@ import datetime
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.autosectionlabel',]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

View File

@ -48,9 +48,10 @@ Contents
.. toctree::
:maxdepth: 2
install
use
advanced_usage
examples
templates
test

View File

@ -38,6 +38,8 @@ If you'd like to raise ``DisallowedHost`` and a HTTP 400 response instead, use t
If you'd like to serve the public tenant for unrecognised hostnames instead, use ``tenant_schemas.middleware.DefaultTenantMiddleware``. To use a tenant other than the public tenant, create a subclass and register it instead.
If you'd like a different tenant selection technique (e.g. using an HTTP Header), you can define a custom middleware. See :ref:`Advanced Usage`.
.. code-block:: python
from tenant_schemas.middleware import DefaultTenantMiddleware
@ -173,12 +175,6 @@ Optional Settings
The schema name that will be treated as ``public``, that is, where the ``SHARED_APPS`` will be created.
.. attribute:: TENANT_CREATION_FAKES_MIGRATIONS
:Default: ``'True'``
Sets if the models will be synced directly to the last version and all migration subsequently faked. Useful in the cases where migrations can not be faked and need to be ran individually. Be aware that setting this to `False` may significantly slow down the process of creating tenants.
Tenant View-Routing
-------------------

View File

@ -36,9 +36,7 @@ Because django will not create tenants for you during your tests, we have packed
Running tests faster
--------------------
Using the ``TenantTestCase`` can make running your tests really slow quite early in your project. This is due to the fact that it drops, recreates the test schema and runs migrations for every ``TenantTestCase`` you have. If you want to gain speed, there's a ``FastTenantTestCase`` where the test schema will be created and migrations ran only one time. The gain in speed is noticiable but be aware that by using this you will be perpertraiting state between your test cases, please make sure your they wont be affected by this.
Running tests using ``TenantTestCase`` can start being a bottleneck once the number of tests grow. If you do not care that the state between tests is kept, an alternative is to use the class ``FastTenantTestCase``. Unlike ``TenantTestCase``, the test schema and its migrations will only be created and ran once. This is a significant improvement in speed coming at the cost of shared state.
Running tests using ``TenantTestCase`` can start being a bottleneck once the number of tests grow. ``TenantTestCase`` drops, recreates and executes migrations for the test schema every time for every ``TenantTestCase`` you have. If you do not care that the state between tests is kept, an alternative is to use the class ``FastTenantTestCase``. Unlike ``TenantTestCase``, the test schema and its migrations will only be created and ran once. This is a significant improvement in speed coming at the cost of shared state.
.. code-block:: python

View File

@ -98,6 +98,26 @@ The options given to ``migrate_schemas`` are also passed to every ``migrate``. H
``migrate_schemas`` raises an exception when an tenant schema is missing.
migrate_schemas in parallel
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Once the number of tenants grow, migrating all the tenants can become a bottleneck. To speed up this process, you can run tenant migrations in parallel like this:
.. code-block:: bash
python manage.py migrate_schemas --executor=parallel
In fact, you can write your own executor which will run tenant migrations in
any way you want, just take a look at ``tenant_schemas/migration_executors``.
The ``parallel`` executor accepts the following settings:
* ``TENANT_PARALLEL_MIGRATION_MAX_PROCESSES`` (default: 2) - maximum number of
processes for migration pool (this is to avoid exhausting the database
connection pool)
* ``TENANT_PARALLEL_MIGRATION_CHUNKS`` (default: 2) - number of migrations to be
sent at once to every worker
tenant_command
~~~~~~~~~~~~~~

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python
from os.path import exists
from version import get_git_version
try:
@ -15,6 +16,7 @@ setup(
author_email='carneiro.be@gmail.com',
packages=[
'tenant_schemas',
'tenant_schemas.migration_executors',
'tenant_schemas.postgresql_backend',
'tenant_schemas.management',
'tenant_schemas.management.commands',

View File

@ -51,12 +51,14 @@ def best_practice(app_configs, **kwargs):
obj="django.conf.settings",
hint="This is necessary to overwrite built-in django "
"management commands with their schema-aware "
"implementations."))
"implementations.",
id="tenant_schemas.W001"))
if not settings.TENANT_APPS:
errors.append(
Error("TENANT_APPS is empty.",
hint="Maybe you don't need this app?"))
hint="Maybe you don't need this app?",
id="tenant_schemas.E001"))
if hasattr(settings, 'PG_EXTRA_SEARCH_PATHS'):
if get_public_schema_name() in settings.PG_EXTRA_SEARCH_PATHS:
@ -74,25 +76,29 @@ def best_practice(app_configs, **kwargs):
if not settings.SHARED_APPS:
errors.append(
Warning("SHARED_APPS is empty."))
Warning("SHARED_APPS is empty.",
id="tenant_schemas.W002"))
if not set(settings.TENANT_APPS).issubset(INSTALLED_APPS):
delta = set(settings.TENANT_APPS).difference(INSTALLED_APPS)
errors.append(
Error("You have TENANT_APPS that are not in INSTALLED_APPS",
hint=[a for a in settings.TENANT_APPS if a in delta]))
hint=[a for a in settings.TENANT_APPS if a in delta],
id="tenant_schemas.E002"))
if not set(settings.SHARED_APPS).issubset(INSTALLED_APPS):
delta = set(settings.SHARED_APPS).difference(INSTALLED_APPS)
errors.append(
Error("You have SHARED_APPS that are not in INSTALLED_APPS",
hint=[a for a in settings.SHARED_APPS if a in delta]))
hint=[a for a in settings.SHARED_APPS if a in delta],
id="tenant_schemas.E003"))
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'",
id="tenant_schemas.W003"
))
return errors

View File

@ -149,11 +149,14 @@ class SyncCommon(BaseCommand):
help=('Database state will be brought to the state after that '
'migration. Use the name "zero" to unapply all migrations.'))
parser.add_argument("-s", "--schema", dest="schema_name")
parser.add_argument('--executor', action='store', dest='executor', default=None,
help='Executor for running migrations [standard (default)|parallel]')
def handle(self, *args, **options):
self.sync_tenant = options.get('tenant')
self.sync_public = options.get('shared')
self.schema_name = options.get('schema_name')
self.executor = options.get('executor')
self.installed_apps = settings.INSTALLED_APPS
self.args = args
self.options = options

View File

@ -1,11 +1,10 @@
import django
from django.conf import settings
from django.core.management.commands.migrate import Command as MigrateCommand
from django.db import connection
from tenant_schemas.management.commands import SyncCommon
from tenant_schemas.utils import get_tenant_model, get_public_schema_name, schema_exists
from tenant_schemas.migration_executors import get_executor
from tenant_schemas.utils import get_public_schema_name, get_tenant_model, schema_exists
if django.VERSION >= (1, 9, 0):
from django.db.migrations.exceptions import MigrationSchemaMissing
@ -34,35 +33,21 @@ class Command(SyncCommon):
super(Command, self).handle(*args, **options)
self.PUBLIC_SCHEMA_NAME = get_public_schema_name()
executor = get_executor(codename=self.executor)(self.args, self.options)
if self.sync_public and not self.schema_name:
self.schema_name = self.PUBLIC_SCHEMA_NAME
if self.sync_public:
self.run_migrations(self.schema_name, settings.SHARED_APPS)
executor.run_migrations(tenants=[self.schema_name])
if self.sync_tenant:
if self.schema_name and self.schema_name != self.PUBLIC_SCHEMA_NAME:
if not schema_exists(self.schema_name):
raise MigrationSchemaMissing('Schema "{}" does not exist'.format(
self.schema_name))
else:
self.run_migrations(self.schema_name, settings.TENANT_APPS)
tenants = [self.schema_name]
else:
all_tenants = get_tenant_model().objects.exclude(schema_name=get_public_schema_name())
for tenant in all_tenants:
self.run_migrations(tenant.schema_name, settings.TENANT_APPS)
def run_migrations(self, schema_name, included_apps):
if int(self.options.get('verbosity', 1)) >= 1:
self._notice("=== Running migrate for schema %s" % schema_name)
if not schema_exists(schema_name):
raise MigrationSchemaMissing('Schema "{}" does not exist'.format(
schema_name))
connection.set_schema(schema_name)
command = MigrateCommand()
command.execute(*self.args, **self.options)
connection.set_schema_to_public()
def _notice(self, output):
self.stdout.write(self.style.NOTICE(output))
tenants = get_tenant_model().objects.exclude(schema_name=get_public_schema_name()).values_list(
'schema_name', flat=True)
executor.run_migrations(tenants=tenants)

View File

@ -13,38 +13,54 @@ if django.VERSION >= (1, 10, 0):
else:
MIDDLEWARE_MIXIN = object
"""
These middlewares should be placed at the very top of the middleware stack.
Selects the proper database schema using request information. Can fail in
various ways which is better than corrupting or revealing data.
class TenantMiddleware(MIDDLEWARE_MIXIN):
"""
This middleware should be placed at the very top of the middleware stack.
Selects the proper database schema using the request host. Can fail in
various ways which is better than corrupting or revealing data.
"""
Extend BaseTenantMiddleware for a custom tenant selection strategy,
such as inspecting the header, or extracting it from some OAuth token.
"""
class BaseTenantMiddleware(MIDDLEWARE_MIXIN):
TENANT_NOT_FOUND_EXCEPTION = Http404
"""
Subclass and override this to achieve desired behaviour. Given a
request, return the tenant to use. Tenant should be an instance
of TENANT_MODEL. We have three parameters for backwards compatibility
(the request would be enough).
"""
def get_tenant(self, model, hostname, request):
raise NotImplementedError
def hostname_from_request(self, request):
""" Extracts hostname from request. Used for custom requests filtering.
By default removes the request's port and common prefixes.
"""
return remove_www(request.get_host().split(':')[0]).lower()
def get_tenant(self, model, hostname, request):
return model.objects.get(domain_url=hostname)
def process_request(self, request):
# Connection needs first to be at the public schema, as this is where
# the tenant metadata is stored.
connection.set_schema_to_public()
hostname = self.hostname_from_request(request)
hostname = self.hostname_from_request(request)
TenantModel = get_tenant_model()
try:
request.tenant = self.get_tenant(TenantModel, hostname, request)
connection.set_tenant(request.tenant)
# get_tenant must be implemented by extending this class.
tenant = self.get_tenant(TenantModel, hostname, request)
assert isinstance(tenant, TenantModel)
except TenantModel.DoesNotExist:
raise self.TENANT_NOT_FOUND_EXCEPTION(
'No tenant for hostname "%s"' % hostname)
'No tenant for {!r}'.format(request.get_host()))
except AssertionError:
raise self.TENANT_NOT_FOUND_EXCEPTION(
'Invalid tenant {!r}'.format(request.tenant))
request.tenant = tenant
connection.set_tenant(request.tenant)
# Content type can no longer be cached as public and tenant schemas
# have different models. If someone wants to change this, the cache
@ -59,6 +75,14 @@ class TenantMiddleware(MIDDLEWARE_MIXIN):
if hasattr(settings, 'PUBLIC_SCHEMA_URLCONF') and request.tenant.schema_name == get_public_schema_name():
request.urlconf = settings.PUBLIC_SCHEMA_URLCONF
class TenantMiddleware(BaseTenantMiddleware):
"""
Selects the proper database schema using the request host. E.g. <my_tenant>.<my_domain>
"""
def get_tenant(self, model, hostname, request):
return model.objects.get(domain_url=hostname)
class SuspiciousTenantMiddleware(TenantMiddleware):
"""

View File

@ -0,0 +1,15 @@
import os
from tenant_schemas.migration_executors.base import MigrationExecutor
from tenant_schemas.migration_executors.parallel import ParallelExecutor
from tenant_schemas.migration_executors.standard import StandardExecutor
def get_executor(codename=None):
codename = codename or os.environ.get('EXECUTOR', StandardExecutor.codename)
for klass in MigrationExecutor.__subclasses__():
if klass.codename == codename:
return klass
raise NotImplementedError('No executor with codename %s' % codename)

View File

@ -0,0 +1,64 @@
import sys
from django.core.management.commands.migrate import Command as MigrateCommand
from django.db import transaction
from tenant_schemas.utils import get_public_schema_name
def run_migrations(args, options, executor_codename, schema_name, allow_atomic=True):
from django.core.management import color
from django.core.management.base import OutputWrapper
from django.db import connection
style = color.color_style()
def style_func(msg):
return '[%s:%s] %s' % (
style.NOTICE(executor_codename),
style.NOTICE(schema_name),
msg
)
stdout = OutputWrapper(sys.stdout)
stdout.style_func = style_func
stderr = OutputWrapper(sys.stderr)
stderr.style_func = style_func
if int(options.get('verbosity', 1)) >= 1:
stdout.write(style.NOTICE("=== Running migrate for schema %s" % schema_name))
connection.set_schema(schema_name)
MigrateCommand(stdout=stdout, stderr=stderr).execute(*args, **options)
try:
transaction.commit()
connection.close()
connection.connection = None
except transaction.TransactionManagementError:
if not allow_atomic:
raise
# We are in atomic transaction, don't close connections
pass
connection.set_schema_to_public()
class MigrationExecutor(object):
codename = None
def __init__(self, args, options):
self.args = args
self.options = options
def run_migrations(self, tenants):
public_schema_name = get_public_schema_name()
if public_schema_name in tenants:
run_migrations(self.args, self.options, self.codename, public_schema_name)
tenants.pop(tenants.index(public_schema_name))
self.run_tenant_migrations(tenants)
def run_tenant_migrations(self, tenant):
raise NotImplementedError

View File

@ -0,0 +1,30 @@
import functools
import multiprocessing
from django.conf import settings
from tenant_schemas.migration_executors.base import MigrationExecutor, run_migrations
class ParallelExecutor(MigrationExecutor):
codename = 'parallel'
def run_tenant_migrations(self, tenants):
if tenants:
processes = getattr(settings, 'TENANT_PARALLEL_MIGRATION_MAX_PROCESSES', 2)
chunks = getattr(settings, 'TENANT_PARALLEL_MIGRATION_CHUNKS', 2)
from django.db import connection
connection.close()
connection.connection = None
run_migrations_p = functools.partial(
run_migrations,
self.args,
self.options,
self.codename,
allow_atomic=False
)
p = multiprocessing.Pool(processes=processes)
p.map(run_migrations_p, tenants, chunks)

View File

@ -0,0 +1,9 @@
from tenant_schemas.migration_executors.base import MigrationExecutor, run_migrations
class StandardExecutor(MigrationExecutor):
codename = 'standard'
def run_tenant_migrations(self, tenants):
for schema_name in tenants:
run_migrations(self.args, self.options, self.codename, schema_name)

View File

@ -6,6 +6,27 @@ from tenant_schemas.signals import post_schema_sync
from tenant_schemas.utils import get_public_schema_name, schema_exists
class TenantQueryset(models.QuerySet):
"""
QuerySet for instances that inherit from the TenantMixin.
"""
def delete(self):
"""
Make sure we call the delete method of each object in the queryset so
that safety checks and schema deletion (if requested) are executed
even when using bulk delete.
"""
counter, counter_dict = 0, {}
for obj in self:
result = obj.delete()
if result is not None:
current_counter, current_counter_dict = result
counter += current_counter
counter_dict.update(current_counter_dict)
if counter:
return counter, counter_dict
class TenantMixin(models.Model):
"""
All tenant models must inherit this class.
@ -27,6 +48,7 @@ class TenantMixin(models.Model):
domain_url = models.CharField(max_length=128, unique=True)
schema_name = models.CharField(max_length=63, unique=True,
validators=[_check_schema_name])
objects = TenantQueryset.as_manager()
class Meta:
abstract = True
@ -69,7 +91,7 @@ class TenantMixin(models.Model):
cursor = connection.cursor()
cursor.execute('DROP SCHEMA IF EXISTS %s CASCADE' % self.schema_name)
super(TenantMixin, self).delete(*args, **kwargs)
return super(TenantMixin, self).delete(*args, **kwargs)
def create_schema(self, check_if_exists=False, sync_schema=True,
verbosity=1):

View File

@ -9,6 +9,7 @@ import django.db.utils
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)

View File

@ -1,10 +1,12 @@
from django.conf import settings
from django.core.management import call_command
from django.conf import settings
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'
ALLOWED_TEST_DOMAIN = '.test.com'

View File

@ -63,7 +63,8 @@ class AppConfigTests(TestCase):
obj="django.conf.settings",
hint="This is necessary to overwrite built-in django "
"management commands with their schema-aware "
"implementations."),
"implementations.",
id="tenant_schemas.W001"),
])
@override_settings(INSTALLED_APPS=[
@ -83,7 +84,8 @@ class AppConfigTests(TestCase):
def test_tenant_apps_empty(self):
self.assertBestPractice([
Error("TENANT_APPS is empty.",
hint="Maybe you don't need this app?"),
hint="Maybe you don't need this app?",
id="tenant_schemas.E001"),
])
@override_settings(PG_EXTRA_SEARCH_PATHS=['public', 'demo1', 'demo2'])
@ -101,7 +103,8 @@ class AppConfigTests(TestCase):
@override_settings(SHARED_APPS=())
def test_shared_apps_empty(self):
self.assertBestPractice([
Warning("SHARED_APPS is empty."),
Warning("SHARED_APPS is empty.",
id="tenant_schemas.W002"),
])
@override_settings(TENANT_APPS=(
@ -111,7 +114,8 @@ class AppConfigTests(TestCase):
def test_tenant_app_missing_from_install_apps(self):
self.assertBestPractice([
Error("You have TENANT_APPS that are not in INSTALLED_APPS",
hint=['django.contrib.flatpages']),
hint=['django.contrib.flatpages'],
id="tenant_schemas.E002"),
])
@override_settings(SHARED_APPS=(
@ -127,5 +131,6 @@ class AppConfigTests(TestCase):
def test_shared_app_missing_from_install_apps(self):
self.assertBestPractice([
Error("You have SHARED_APPS that are not in INSTALLED_APPS",
hint=['django.contrib.flatpages']),
hint=['django.contrib.flatpages'],
id="tenant_schemas.E003"),
])

View File

@ -99,6 +99,35 @@ class TenantDataAndSettingsTest(BaseTestCase):
self.assertFalse(schema_exists(tenant.schema_name))
Tenant.auto_drop_schema = False
def test_auto_drop_schema_bulk_delete(self):
"""
When bulk deleting tenants, it should also drop the schemas of
tenants that have auto_drop_schema set to True.
"""
Tenant.auto_drop_schema = True
schemas = ['auto_drop_schema1', 'auto_drop_schema2']
for schema in schemas:
self.assertFalse(schema_exists(schema))
tenant = Tenant(
domain_url='%s.test.com' % schema,
schema_name=schema
)
tenant.save(verbosity=BaseTestCase.get_verbosity())
self.assertTrue(schema_exists(tenant.schema_name))
# Force pending trigger events to be executed
cursor = connection.cursor()
cursor.execute('SET CONSTRAINTS ALL IMMEDIATE')
# get a queryset of our 2 tenants and do a bulk delete
Tenant.objects.filter(schema_name__in=schemas).delete()
# verify that the schemas where deleted
for schema in schemas:
self.assertFalse(schema_exists(schema))
Tenant.auto_drop_schema = False
def test_switching_search_path(self):
tenant1 = Tenant(domain_url='something.test.com',
schema_name='tenant1')

View File

@ -1,4 +1,5 @@
import inspect
from django.conf import settings
from django.core.management import call_command
from django.db import connection

13
tox.ini
View File

@ -1,5 +1,12 @@
[tox]
envlist = py{27,35}-dj{18,110,111}
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 +24,10 @@ changedir = dts_test_project
passenv = PG_NAME PG_USER PG_PASSWORD PG_HOST PG_PORT
setenv =
standard: MIGRATION_EXECUTOR=standard
parallel: MIGRATION_EXECUTOR=parallel
commands =
coverage run manage.py test --noinput {posargs:tenant_schemas}
coverage report -m --include=../tenant_schemas/*