From c61aca19e4f54d6d6ad1548447ee4eca04335c6f Mon Sep 17 00:00:00 2001
From: Krzysztof Nazarewski
Date: Tue, 10 Nov 2015 15:18:16 +0100
Subject: [PATCH 001/157] Added AppConfig handling to
tenant_schemas.utils.app_labels
---
tenant_schemas/utils.py | 37 ++++++++++++++++++++++++++++++++++---
1 file changed, 34 insertions(+), 3 deletions(-)
diff --git a/tenant_schemas/utils.py b/tenant_schemas/utils.py
index 11a7ac9..5d8961c 100644
--- a/tenant_schemas/utils.py
+++ b/tenant_schemas/utils.py
@@ -2,10 +2,11 @@ from contextlib import contextmanager
from django.conf import settings
from django.db import connection
try:
- from django.apps import apps
+ from django.apps import apps, AppConfig
get_model = apps.get_model
except ImportError:
from django.db.models.loading import get_model
+ AppConfig = None
from django.core import mail
@@ -102,9 +103,39 @@ def schema_exists(schema_name):
return exists
-
def app_labels(apps_list):
"""
Returns a list of app labels of the given apps_list
+
+
+ AppConfig handling test:
+ >>> import sys
+ >>> import types
+
+ Wrapping test in a function so we can return from it on Django <1.7
+ >>> def run_test():
+ ... label = 'test_app1'
+ ...
+ ... if AppConfig is None:
+ ... assert app_labels(['test_apps.' + label]) == [label]
+ ... return
+ ...
+ ... name = 'test_app'
+ ... sys.modules[name] = types.ModuleType(name)
+ ...
+ ... name = 'test_app.apps'
+ ... sys.modules[name] = types.ModuleType(name)
+ ... # noinspection PyPep8Naming
+ ... sys.modules[name].TestAppConfig = type(
+ ... 'TestAppConfig',
+ ... (AppConfig,),
+ ... dict(name='test_app', label=label, path='/tmp')
+ ... )
+ ... assert app_labels(['test_app.apps.TestAppConfig']) == [label]
+
+ >>> run_test()
"""
- return [app.split('.')[-1] for app in apps_list]
+ if AppConfig is None:
+ return [app.split('.')[-1] for app in apps_list]
+ return [AppConfig.create(app).label for app in apps_list]
+
From d337ce663b8d87746c476bd1ce79527e62134d7c Mon Sep 17 00:00:00 2001
From: Krzysztof Nazarewski
Date: Mon, 16 Nov 2015 16:41:14 +0100
Subject: [PATCH 002/157] moved app_labels() test from docstring to separate
TestCase and extended it,
---
tenant_schemas/tests/test_utils.py | 97 ++++++++++++++++++++++++++++++
tenant_schemas/utils.py | 32 ++--------
2 files changed, 101 insertions(+), 28 deletions(-)
create mode 100644 tenant_schemas/tests/test_utils.py
diff --git a/tenant_schemas/tests/test_utils.py b/tenant_schemas/tests/test_utils.py
new file mode 100644
index 0000000..931d5ce
--- /dev/null
+++ b/tenant_schemas/tests/test_utils.py
@@ -0,0 +1,97 @@
+from __future__ import absolute_import
+
+import sys
+import types
+
+import django
+from django.test import TestCase
+
+from tenant_schemas import utils
+
+
+class AppLabelsTestCase(TestCase):
+ def setUp(self):
+ self._modules = set()
+
+ def tearDown(self):
+ for name in self._modules:
+ sys.modules.pop(name, None)
+
+ def set_up_module(self, whole_name):
+ parts = whole_name.split('.')
+ name = ''
+ for part in parts:
+ name += ('.%s' % part) if name else part
+ module = types.ModuleType(name)
+ module.__path__ = ['/tmp']
+ self._modules.add(name)
+ sys.modules[name] = module
+ return sys.modules[whole_name]
+
+ def test_app_labels_1_7(self):
+ """
+ Verifies that app_labels handle Django 1.7+ AppConfigs properly.
+ https://docs.djangoproject.com/en/1.7/ref/applications/
+ """
+ if django.VERSION >= (1, 7):
+ from django.apps import AppConfig
+ else:
+ # set up poor man's mock for 1.7/1.8 behaviour.
+ from collections import namedtuple
+
+ class AppConfig(object):
+ call_count = 0
+ ret_map = {
+ 'example1': 'example1',
+ 'example2.apps.Example2AppConfig': 'example2_app',
+ }
+
+ @classmethod
+ def create(cls, app):
+ return namedtuple('AppConfig', ['label'])(
+ cls.ret_map[app]
+ )
+ dj_apps = self.set_up_module('django.apps')
+ dj_apps.AppConfig = AppConfig
+
+ reload(utils)
+
+ self.set_up_module('example1')
+ apps = self.set_up_module('example2.apps')
+
+ # set up AppConfig on the `test_app.apps` module
+ class Example2AppConfig(AppConfig):
+ name = 'example2'
+ label = 'example2_app' # with different name
+ path = '/tmp' # for whatever reason path is required
+
+ apps.Example2AppConfig = Example2AppConfig
+
+ self.assertEqual(
+ utils.app_labels([
+ 'example1',
+ 'example2.apps.Example2AppConfig'
+ ]),
+ ['example1', 'example2_app'],
+ )
+
+ def test_app_labels_pre_1_7(self):
+ if django.VERSION >= (1, 7):
+ # set AppConfig to None and reload utils module
+ from django import apps
+ AppConfig = apps.AppConfig
+ apps.AppConfig = None
+
+ reload(utils)
+
+ self.assertEqual(
+ utils.app_labels(
+ ['example1', 'example2.apps.Example2AppConfig']
+ ),
+ ['example1', 'Example2AppConfig']
+ )
+
+ if django.VERSION >= (1, 7):
+ # restore AppConfig
+ apps.AppConfig = AppConfig
+ reload(utils)
diff --git a/tenant_schemas/utils.py b/tenant_schemas/utils.py
index 5d8961c..ba57808 100644
--- a/tenant_schemas/utils.py
+++ b/tenant_schemas/utils.py
@@ -103,37 +103,13 @@ def schema_exists(schema_name):
return exists
+
def app_labels(apps_list):
"""
- Returns a list of app labels of the given apps_list
+ Returns a list of app labels of the given apps_list, now properly handles
+ new Django 1.7+ application registry.
-
- AppConfig handling test:
- >>> import sys
- >>> import types
-
- Wrapping test in a function so we can return from it on Django <1.7
- >>> def run_test():
- ... label = 'test_app1'
- ...
- ... if AppConfig is None:
- ... assert app_labels(['test_apps.' + label]) == [label]
- ... return
- ...
- ... name = 'test_app'
- ... sys.modules[name] = types.ModuleType(name)
- ...
- ... name = 'test_app.apps'
- ... sys.modules[name] = types.ModuleType(name)
- ... # noinspection PyPep8Naming
- ... sys.modules[name].TestAppConfig = type(
- ... 'TestAppConfig',
- ... (AppConfig,),
- ... dict(name='test_app', label=label, path='/tmp')
- ... )
- ... assert app_labels(['test_app.apps.TestAppConfig']) == [label]
-
- >>> run_test()
+ https://docs.djangoproject.com/en/1.8/ref/applications/#django.apps.AppConfig.label
"""
if AppConfig is None:
return [app.split('.')[-1] for app in apps_list]
From 9fe1d657f341886123e64def261e45d278c690a1 Mon Sep 17 00:00:00 2001
From: Gary Reynolds
Date: Tue, 19 Jan 2016 17:01:43 +1100
Subject: [PATCH 003/157] Add tox.ini
---
tox.ini | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
create mode 100644 tox.ini
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..bce61cd
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,19 @@
+[tox]
+envlist = py27-dj{17,18,19}
+
+[testenv]
+basepython =
+ py27: python2.7
+usedevelop = True
+
+deps =
+ dj17: Django~=1.7.0
+ dj18: Django~=1.8.0
+ dj19: Django~=1.9.0
+
+changedir = dts_test_project
+
+passenv = PG_USER PG_PASSWORD
+
+commands =
+ {envpython} manage.py test tenant_schemas.tests --noinput -v 2
From 1743b4867b883bfd499ae5a0024940ca7ec43b47 Mon Sep 17 00:00:00 2001
From: Gary Reynolds
Date: Tue, 19 Jan 2016 17:03:18 +1100
Subject: [PATCH 004/157] Add LOGGING configuration to test project
---
dts_test_project/dts_test_project/settings.py | 31 +++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/dts_test_project/dts_test_project/settings.py b/dts_test_project/dts_test_project/settings.py
index 5bfaec7..44fab54 100644
--- a/dts_test_project/dts_test_project/settings.py
+++ b/dts_test_project/dts_test_project/settings.py
@@ -122,3 +122,34 @@ USE_TZ = True
# https://docs.djangoproject.com/en/1.6/howto/static-files/
STATIC_URL = '/static/'
+
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'filters': {
+ 'require_debug_false': {
+ '()': 'django.utils.log.RequireDebugFalse'
+ },
+ },
+ 'formatters': {
+ 'simple': {
+ 'format': '%(levelname)-7s %(asctime)s %(message)s',
+ },
+ },
+ 'handlers': {
+ 'null': {
+ 'class': 'logging.NullHandler',
+ },
+ 'console': {
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'simple',
+ },
+ },
+ 'loggers': {
+ '': {
+ 'handlers': ['null'],
+ 'level': 'DEBUG',
+ 'propagate': True,
+ },
+ },
+}
From 57d53c955dbafefb8a6c48b26498724b5b731011 Mon Sep 17 00:00:00 2001
From: Gary Reynolds
Date: Tue, 19 Jan 2016 17:06:57 +1100
Subject: [PATCH 005/157] Add logging in utils.django_in_test_mode function.
---
tenant_schemas/utils.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/tenant_schemas/utils.py b/tenant_schemas/utils.py
index 11a7ac9..eb6b641 100644
--- a/tenant_schemas/utils.py
+++ b/tenant_schemas/utils.py
@@ -1,6 +1,9 @@
+import logging
from contextlib import contextmanager
+
from django.conf import settings
from django.db import connection
+
try:
from django.apps import apps
get_model = apps.get_model
@@ -8,6 +11,8 @@ except ImportError:
from django.db.models.loading import get_model
from django.core import mail
+logger = logging.getLogger(__name__)
+
@contextmanager
def schema_context(schema_name):
@@ -82,7 +87,9 @@ def django_is_in_test_mode():
I know this is very ugly! I'm looking for more elegant solutions.
See: http://stackoverflow.com/questions/6957016/detect-django-testing-mode
"""
- return hasattr(mail, 'outbox')
+ in_test_mode = hasattr(mail, 'outbox')
+ logger.debug('TEST_MODE=%s', in_test_mode)
+ return in_test_mode
def schema_exists(schema_name):
From 21048e333307ea7b0ccce52bda8d2ab04f5ede1e Mon Sep 17 00:00:00 2001
From: Gary Reynolds
Date: Tue, 19 Jan 2016 17:42:01 +1100
Subject: [PATCH 006/157] Update TenantCommandTest.test_command to run under
Django 1.8 and 1.9.
---
tenant_schemas/tests/test_tenants.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/tenant_schemas/tests/test_tenants.py b/tenant_schemas/tests/test_tenants.py
index 8ea7c82..8ac0645 100644
--- a/tenant_schemas/tests/test_tenants.py
+++ b/tenant_schemas/tests/test_tenants.py
@@ -217,8 +217,12 @@ class TenantCommandTest(BaseTestCase):
Tenant(domain_url='localhost', schema_name='public').save(verbosity=BaseTestCase.get_verbosity())
out = StringIO()
- call_command('tenant_command', 'dumpdata', 'tenant_schemas', natural_foreign=True,
- schema_name=get_public_schema_name(), stdout=out)
+ if django.VERSION >= (1, 8, 0):
+ call_command('tenant_command', args=('dumpdata', 'tenant_schemas'), natural_foreign=True,
+ schema_name=get_public_schema_name(), stdout=out)
+ else:
+ call_command('tenant_command', 'dumpdata', 'tenant_schemas', natural_foreign=True,
+ schema_name=get_public_schema_name(), stdout=out)
self.assertItemsEqual(
json.loads('[{"fields": {"domain_url": "localhost", "schema_name": "public"}, '
'"model": "tenant_schemas.tenant", "pk": 1}]'),
From 920a884a44761aa96174ba7c1a2d9e6f30177b28 Mon Sep 17 00:00:00 2001
From: Gary Reynolds
Date: Tue, 19 Jan 2016 17:50:22 +1100
Subject: [PATCH 007/157] Update dts_test_project.settings to work with
codeship CI
https://codeship.com/documentation/databases/postgresql/#django
---
dts_test_project/dts_test_project/settings.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/dts_test_project/dts_test_project/settings.py b/dts_test_project/dts_test_project/settings.py
index 44fab54..e428ba5 100644
--- a/dts_test_project/dts_test_project/settings.py
+++ b/dts_test_project/dts_test_project/settings.py
@@ -75,10 +75,9 @@ DATABASES = {
'default': {
'ENGINE': 'tenant_schemas.postgresql_backend',
'NAME': 'dts_test_project',
- 'USER': 'postgres',
- 'PASSWORD': 'root',
- 'HOST': 'localhost',
- 'PORT': '',
+ 'USER': os.environ.get('PG_USER', 'postgres'),
+ 'PASSWORD': os.environ.get('PG_PASSWORD', 'root'),
+ 'HOST': '127.0.0.1',
}
}
From 02517f9a4d9ec629676a99949fc5e064cd413b64 Mon Sep 17 00:00:00 2001
From: Gary Reynolds
Date: Tue, 19 Jan 2016 17:51:15 +1100
Subject: [PATCH 008/157] Set migration verbosity so as to not spam the output.
---
tenant_schemas/tests/testcases.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tenant_schemas/tests/testcases.py b/tenant_schemas/tests/testcases.py
index 29280d5..2d4f828 100644
--- a/tenant_schemas/tests/testcases.py
+++ b/tenant_schemas/tests/testcases.py
@@ -39,7 +39,7 @@ class BaseTestCase(TestCase):
for s in reversed(inspect.stack()):
options = s[0].f_locals.get('options')
if isinstance(options, dict):
- return int(options['verbosity'])
+ return int(options['verbosity']) - 2
return 1
@classmethod
From d54c947f70f0d65760aacf9ac218c0e39421ec25 Mon Sep 17 00:00:00 2001
From: Gary Reynolds
Date: Thu, 21 Jan 2016 07:28:44 +1100
Subject: [PATCH 009/157] Documentation update.
---
docs/involved.rst | 2 +-
docs/test.rst | 8 ++++++++
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/docs/involved.rst b/docs/involved.rst
index 84e7755..558659a 100644
--- a/docs/involved.rst
+++ b/docs/involved.rst
@@ -3,4 +3,4 @@ Get Involved!
=============
Suggestions, bugs, ideas, patches, questions
--------------------------------------------
-Are **highly** welcome! Feel free to write an issue for any feedback you have or send a pull request on `GitHub `. :)
+Are **highly** welcome! Feel free to write an issue for any feedback you have or send a pull request on `GitHub `_. :)
diff --git a/docs/test.rst b/docs/test.rst
index b9b2da6..1834a68 100644
--- a/docs/test.rst
+++ b/docs/test.rst
@@ -9,6 +9,12 @@ If you're using South, don't forget to set ``SOUTH_TESTS_MIGRATE = False``. Run
./manage.py test tenant_schemas.tests
+To run the test suite outsite of your application you can use tox_ to test all supported Django versions.
+
+.. code-block:: bash
+
+ tox
+
Updating your app's tests to work with tenant-schemas
-----------------------------------------------------
Because django will not create tenants for you during your tests, we have packed some custom test cases and other utilities. If you want a test to happen at any of the tenant's domain, you can use the test case ``TenantTestCase``. It will automatically create a tenant for you, set the connection's schema to tenant's schema and make it available at ``self.tenant``. We have also included a ``TenantRequestFactory`` and a ``TenantClient`` so that your requests will all take place at the tenant's domain automatically. Here's an example
@@ -25,3 +31,5 @@ Because django will not create tenants for you during your tests, we have packed
def test_user_profile_view(self):
response = self.c.get(reverse('user_profile'))
self.assertEqual(response.status_code, 200)
+
+.. _tox: https://tox.readthedocs.org/
From 538fdf48f1ce4ac5aef516196192bdbbc199adea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mikul=C3=A1=C5=A1=20Poul?=
Date: Sat, 20 Feb 2016 21:17:39 +0100
Subject: [PATCH 010/157] Raising an exception when a schema is missing, added
an option to migrate_schemas to ignore this error
---
docs/use.rst | 6 ++++++
tenant_schemas/management/commands/__init__.py | 5 +++++
.../management/commands/migrate_schemas.py | 12 +++++++++++-
3 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/docs/use.rst b/docs/use.rst
index cc2b2d1..235ceb4 100644
--- a/docs/use.rst
+++ b/docs/use.rst
@@ -87,6 +87,12 @@ The options given to ``migrate_schemas`` are also passed to every ``migrate``. H
./manage.py migrate_schemas --list
+``migrate_schemas`` raises an exception when an tenant schema is missing. You can ignore these exceptions with the ``--ignore-missing-schemas`` option.
+
+.. code-block:: bash
+
+ ./manage.py migrate_schemas --ignore-missing-schemas
+
sync_schemas
~~~~~~~~~~~~
diff --git a/tenant_schemas/management/commands/__init__.py b/tenant_schemas/management/commands/__init__.py
index 4542d03..d6bca36 100644
--- a/tenant_schemas/management/commands/__init__.py
+++ b/tenant_schemas/management/commands/__init__.py
@@ -175,11 +175,16 @@ 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("--ignore-missing-schemas", dest="ignore_missing_schemas",
+ action="store_true", default=False,
+ help=('Doesn\'t raise an exception when a schema is '
+ 'missing in the database.'))
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.ignore_missing_schemas = options.get("ignore_missing_schemas")
self.installed_apps = settings.INSTALLED_APPS
self.args = args
self.options = options
diff --git a/tenant_schemas/management/commands/migrate_schemas.py b/tenant_schemas/management/commands/migrate_schemas.py
index 3178600..ccf79e5 100644
--- a/tenant_schemas/management/commands/migrate_schemas.py
+++ b/tenant_schemas/management/commands/migrate_schemas.py
@@ -46,13 +46,23 @@ class MigrateSchemasCommand(SyncCommon):
else:
self.run_migrations(self.schema_name, settings.TENANT_APPS)
else:
- all_tenants = get_tenant_model().objects.exclude(schema_name=get_public_schema_name())
+ all_tenants = get_tenant_model().objects.exclude(schema_name=self.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):
+ if not self.ignore_missing_schemas:
+ raise RuntimeError('Schema "{}" does not exist'.format(
+ schema_name))
+ else:
+ if int(self.options.get('verbosity', 1)) >= 1:
+ self._notice("=== Schema %s doesn't exist, ignoring" % schema_name)
+ return
+
connection.set_schema(schema_name)
command = MigrateCommand()
command.execute(*self.args, **self.options)
From c8921aa571074819201bb86c78b6d3617d72b7fd Mon Sep 17 00:00:00 2001
From: Jacob Haslehurst
Date: Fri, 18 Mar 2016 15:06:56 +1100
Subject: [PATCH 011/157] fix `reverse` for Django 1.9
`reverse` no longer accepts the `prefix` argument
https://docs.djangoproject.com/en/1.9/ref/urlresolvers/#reverse
---
tenant_schemas/urlresolvers.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/tenant_schemas/urlresolvers.py b/tenant_schemas/urlresolvers.py
index 15c0598..810b58b 100644
--- a/tenant_schemas/urlresolvers.py
+++ b/tenant_schemas/urlresolvers.py
@@ -5,9 +5,8 @@ from django.utils.functional import lazy
from tenant_schemas.utils import clean_tenant_url
-def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None,
- current_app=None):
- url = reverse_default(viewname, urlconf, args, kwargs, prefix, current_app)
+def reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None):
+ url = reverse_default(viewname, urlconf, args, kwargs, current_app)
return clean_tenant_url(url)
reverse_lazy = lazy(reverse, str)
From 2e58510b558eead203572da20e0b73f865aae3fd Mon Sep 17 00:00:00 2001
From: Jacob Haslehurst
Date: Mon, 21 Mar 2016 11:42:57 +1100
Subject: [PATCH 012/157] explicitly pass kwargs to django reverse
---
tenant_schemas/urlresolvers.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/tenant_schemas/urlresolvers.py b/tenant_schemas/urlresolvers.py
index 810b58b..36f601a 100644
--- a/tenant_schemas/urlresolvers.py
+++ b/tenant_schemas/urlresolvers.py
@@ -6,7 +6,13 @@ from tenant_schemas.utils import clean_tenant_url
def reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None):
- url = reverse_default(viewname, urlconf, args, kwargs, current_app)
+ url = reverse_default(
+ viewname=viewname,
+ urlconf=urlconf,
+ args=args,
+ kwargs=kwargs,
+ current_app=current_app
+ )
return clean_tenant_url(url)
reverse_lazy = lazy(reverse, str)
From 0be38470e5b21d165d3110b27517cbd0a21bf9d8 Mon Sep 17 00:00:00 2001
From: Jacob Haslehurst
Date: Wed, 23 Mar 2016 12:13:05 +1100
Subject: [PATCH 013/157] re-add `prefix` argument to `reverse`
---
tenant_schemas/urlresolvers.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/tenant_schemas/urlresolvers.py b/tenant_schemas/urlresolvers.py
index 36f601a..ba17650 100644
--- a/tenant_schemas/urlresolvers.py
+++ b/tenant_schemas/urlresolvers.py
@@ -5,7 +5,8 @@ from django.utils.functional import lazy
from tenant_schemas.utils import clean_tenant_url
-def reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None):
+def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None,
+ current_app=None):
url = reverse_default(
viewname=viewname,
urlconf=urlconf,
From f8f96b169fe03fb667b2b65b82f84e1b26353e32 Mon Sep 17 00:00:00 2001
From: Jacob Haslehurst
Date: Wed, 23 Mar 2016 12:13:30 +1100
Subject: [PATCH 014/157] remove unused imports
---
tenant_schemas/urlresolvers.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/tenant_schemas/urlresolvers.py b/tenant_schemas/urlresolvers.py
index ba17650..00e36f6 100644
--- a/tenant_schemas/urlresolvers.py
+++ b/tenant_schemas/urlresolvers.py
@@ -1,5 +1,3 @@
-import warnings
-from django.conf import settings
from django.core.urlresolvers import reverse as reverse_default
from django.utils.functional import lazy
from tenant_schemas.utils import clean_tenant_url
From af631d5fe6d7ca034a0cab431addeccb2e13ebc3 Mon Sep 17 00:00:00 2001
From: Bernardo Pires
Date: Mon, 28 Mar 2016 12:09:38 +0200
Subject: [PATCH 015/157] Fixed the RemovedInDjango110Warning related to the
url patterns from the tutorial. Thanks @mikicz. Fixes #320
---
.../tenant_tutorial/tenant_tutorial/urls_public.py | 13 +++++++++----
.../tenant_tutorial/tenant_tutorial/urls_tenants.py | 13 +++++++++----
2 files changed, 18 insertions(+), 8 deletions(-)
diff --git a/examples/tenant_tutorial/tenant_tutorial/urls_public.py b/examples/tenant_tutorial/tenant_tutorial/urls_public.py
index 5a0b6b2..aac2f04 100644
--- a/examples/tenant_tutorial/tenant_tutorial/urls_public.py
+++ b/examples/tenant_tutorial/tenant_tutorial/urls_public.py
@@ -1,6 +1,11 @@
-from django.conf.urls import patterns, url
+import django
+from django.conf.urls import url
from tenant_tutorial.views import HomeView
-urlpatterns = patterns('',
- url(r'^$', HomeView.as_view()),
- )
+
+urlpatterns = [
+ url(r'^$', HomeView.as_view()),
+]
+
+if django.VERSION < (1, 9, 0):
+ urlpatterns = django.conf.urls.patterns('', *urlpatterns)
diff --git a/examples/tenant_tutorial/tenant_tutorial/urls_tenants.py b/examples/tenant_tutorial/tenant_tutorial/urls_tenants.py
index a113b93..a82100b 100644
--- a/examples/tenant_tutorial/tenant_tutorial/urls_tenants.py
+++ b/examples/tenant_tutorial/tenant_tutorial/urls_tenants.py
@@ -1,6 +1,11 @@
-from django.conf.urls import patterns, url
+import django
+from django.conf.urls import url
from customers.views import TenantView
-urlpatterns = patterns('',
- url(r'^$', TenantView.as_view()),
- )
+
+urlpatterns = [
+ url(r'^$', TenantView.as_view()),
+]
+
+if django.VERSION < (1, 9, 0):
+ urlpatterns = django.conf.urls.patterns('', *urlpatterns)
From 3d0e7463f32e396941f5807238cf4b2b357210c8 Mon Sep 17 00:00:00 2001
From: Bernardo Pires
Date: Mon, 28 Mar 2016 16:17:23 +0200
Subject: [PATCH 016/157] removed logging from utils
---
tenant_schemas/utils.py | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/tenant_schemas/utils.py b/tenant_schemas/utils.py
index eb6b641..eb5169a 100644
--- a/tenant_schemas/utils.py
+++ b/tenant_schemas/utils.py
@@ -1,4 +1,3 @@
-import logging
from contextlib import contextmanager
from django.conf import settings
@@ -11,7 +10,6 @@ except ImportError:
from django.db.models.loading import get_model
from django.core import mail
-logger = logging.getLogger(__name__)
@contextmanager
@@ -87,9 +85,7 @@ def django_is_in_test_mode():
I know this is very ugly! I'm looking for more elegant solutions.
See: http://stackoverflow.com/questions/6957016/detect-django-testing-mode
"""
- in_test_mode = hasattr(mail, 'outbox')
- logger.debug('TEST_MODE=%s', in_test_mode)
- return in_test_mode
+ return hasattr(mail, 'outbox')
def schema_exists(schema_name):
From 4d95f7a1195bac0b527648173318189980fb6fb4 Mon Sep 17 00:00:00 2001
From: Martin Samsula
Date: Thu, 31 Mar 2016 11:09:42 +0200
Subject: [PATCH 017/157] router multidb fix
---
tenant_schemas/routers.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tenant_schemas/routers.py b/tenant_schemas/routers.py
index ec484e2..ed5c784 100644
--- a/tenant_schemas/routers.py
+++ b/tenant_schemas/routers.py
@@ -14,6 +14,9 @@ class TenantSyncRouter(object):
from django.db import connection
from tenant_schemas.utils import get_public_schema_name, app_labels
+ if settings.DATABASES[db]['ENGINE'] != 'tenant_schemas.postgresql_backend':
+ return None
+
if isinstance(app_label, ModelBase):
# In django <1.7 the `app_label` parameter is actually `model`
app_label = app_label._meta.app_label
From bb536708c418e3f700d529af982602664d5b9c7d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mikul=C3=A1=C5=A1=20Poul?=
Date: Thu, 7 Apr 2016 17:23:41 +0200
Subject: [PATCH 018/157] Removed the --ignore-missing-schemas option, raising
a different exception
---
docs/use.rst | 6 +-----
tenant_schemas/management/commands/__init__.py | 5 -----
.../management/commands/migrate_schemas.py | 12 ++++--------
3 files changed, 5 insertions(+), 18 deletions(-)
diff --git a/docs/use.rst b/docs/use.rst
index 235ceb4..bbbfd38 100644
--- a/docs/use.rst
+++ b/docs/use.rst
@@ -87,11 +87,7 @@ The options given to ``migrate_schemas`` are also passed to every ``migrate``. H
./manage.py migrate_schemas --list
-``migrate_schemas`` raises an exception when an tenant schema is missing. You can ignore these exceptions with the ``--ignore-missing-schemas`` option.
-
-.. code-block:: bash
-
- ./manage.py migrate_schemas --ignore-missing-schemas
+``migrate_schemas`` raises an exception when an tenant schema is missing.
sync_schemas
~~~~~~~~~~~~
diff --git a/tenant_schemas/management/commands/__init__.py b/tenant_schemas/management/commands/__init__.py
index d6bca36..4542d03 100644
--- a/tenant_schemas/management/commands/__init__.py
+++ b/tenant_schemas/management/commands/__init__.py
@@ -175,16 +175,11 @@ 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("--ignore-missing-schemas", dest="ignore_missing_schemas",
- action="store_true", default=False,
- help=('Doesn\'t raise an exception when a schema is '
- 'missing in the database.'))
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.ignore_missing_schemas = options.get("ignore_missing_schemas")
self.installed_apps = settings.INSTALLED_APPS
self.args = args
self.options = options
diff --git a/tenant_schemas/management/commands/migrate_schemas.py b/tenant_schemas/management/commands/migrate_schemas.py
index ccf79e5..0ee4b30 100644
--- a/tenant_schemas/management/commands/migrate_schemas.py
+++ b/tenant_schemas/management/commands/migrate_schemas.py
@@ -6,6 +6,7 @@ if django.VERSION >= (1, 7, 0):
from django.db.migrations.recorder import MigrationRecorder
from django.db import connection
from django.conf import settings
+from django.db.migrations.exceptions import MigrationSchemaMissing
from tenant_schemas.utils import get_tenant_model, get_public_schema_name, schema_exists
from tenant_schemas.management.commands import SyncCommon
@@ -41,7 +42,7 @@ class MigrateSchemasCommand(SyncCommon):
if self.sync_tenant:
if self.schema_name and self.schema_name != self.PUBLIC_SCHEMA_NAME:
if not schema_exists(self.schema_name):
- raise RuntimeError('Schema "{}" does not exist'.format(
+ raise MigrationSchemaMissing('Schema "{}" does not exist'.format(
self.schema_name))
else:
self.run_migrations(self.schema_name, settings.TENANT_APPS)
@@ -55,13 +56,8 @@ class MigrateSchemasCommand(SyncCommon):
self._notice("=== Running migrate for schema %s" % schema_name)
if not schema_exists(schema_name):
- if not self.ignore_missing_schemas:
- raise RuntimeError('Schema "{}" does not exist'.format(
- schema_name))
- else:
- if int(self.options.get('verbosity', 1)) >= 1:
- self._notice("=== Schema %s doesn't exist, ignoring" % schema_name)
- return
+ raise MigrationSchemaMissing('Schema "{}" does not exist'.format(
+ schema_name))
connection.set_schema(schema_name)
command = MigrateCommand()
From 5b9014303503a4423e32f9e18622a337a5ad4a61 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mikul=C3=A1=C5=A1=20Poul?=
Date: Fri, 15 Apr 2016 12:54:53 +0200
Subject: [PATCH 019/157] Undid the change in getting the public schema name
---
tenant_schemas/management/commands/migrate_schemas.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tenant_schemas/management/commands/migrate_schemas.py b/tenant_schemas/management/commands/migrate_schemas.py
index 0ee4b30..7e485b5 100644
--- a/tenant_schemas/management/commands/migrate_schemas.py
+++ b/tenant_schemas/management/commands/migrate_schemas.py
@@ -47,7 +47,7 @@ class MigrateSchemasCommand(SyncCommon):
else:
self.run_migrations(self.schema_name, settings.TENANT_APPS)
else:
- all_tenants = get_tenant_model().objects.exclude(schema_name=self.PUBLIC_SCHEMA_NAME)
+ 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)
From 0be0b2d227b4210f0fa5f95be5cd417beae2eaed Mon Sep 17 00:00:00 2001
From: John Louis Del Rosario
Date: Fri, 15 Apr 2016 19:18:50 +0800
Subject: [PATCH 020/157] Only send post_sync_signal on successful schema
creation
This avoids masking any errors that might happen in signal handlers.
---
tenant_schemas/models.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/tenant_schemas/models.py b/tenant_schemas/models.py
index 1781051..672fb04 100644
--- a/tenant_schemas/models.py
+++ b/tenant_schemas/models.py
@@ -50,12 +50,13 @@ class TenantMixin(models.Model):
if is_new and self.auto_create_schema:
try:
self.create_schema(check_if_exists=True, verbosity=verbosity)
- post_schema_sync.send(sender=TenantMixin, tenant=self)
except:
# We failed creating the tenant, delete what we created and
# re-raise the exception
self.delete(force_drop=True)
raise
+ else:
+ post_schema_sync.send(sender=TenantMixin, tenant=self)
def delete(self, force_drop=False, *args, **kwargs):
"""
From 388f4b6e83a1703570198856487bf4f9ea7584d6 Mon Sep 17 00:00:00 2001
From: Marek Skrajnowski
Date: Mon, 18 Apr 2016 13:36:37 +0200
Subject: [PATCH 021/157] Fix: missing schema-aware introspection methods
---
.../migrations/0002_test_drop_unique.py | 32 ++
.../postgresql_backend/introspection.py | 330 +++++++++++++++++-
2 files changed, 345 insertions(+), 17 deletions(-)
create mode 100644 dts_test_project/dts_test_app/migrations/0002_test_drop_unique.py
diff --git a/dts_test_project/dts_test_app/migrations/0002_test_drop_unique.py b/dts_test_project/dts_test_app/migrations/0002_test_drop_unique.py
new file mode 100644
index 0000000..d8d5f12
--- /dev/null
+++ b/dts_test_project/dts_test_app/migrations/0002_test_drop_unique.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('dts_test_app', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='DummyModel',
+ name='unique_value',
+ field=models.IntegerField(blank=True, null=True, unique=True),
+ ),
+
+ migrations.AlterField(
+ model_name='DummyModel',
+ name='unique_value',
+ field=models.IntegerField(blank=True, null=True),
+ ),
+
+ migrations.RemoveField(
+ model_name='DummyModel',
+ name='unique_value',
+ ),
+ ]
diff --git a/tenant_schemas/postgresql_backend/introspection.py b/tenant_schemas/postgresql_backend/introspection.py
index 570654d..b7af082 100644
--- a/tenant_schemas/postgresql_backend/introspection.py
+++ b/tenant_schemas/postgresql_backend/introspection.py
@@ -1,22 +1,318 @@
import django
-from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection
-if django.VERSION >= (1, 8, 0):
- from django.db.backends.base.introspection import TableInfo
+from django.db.backends.postgresql_psycopg2.introspection import (
+ DatabaseIntrospection,
+ FieldInfo,
+)
+from django.utils.encoding import force_text
+
+
+def _build_table_info(row):
+ if django.VERSION < (1, 8, 0):
+ return row[0]
+ else:
+ from django.db.backends.base.introspection import TableInfo
+ return TableInfo(row[0], {'r': 't', 'v': 'v'}.get(row[1]))
+
+
+def _build_field_info(col, field_map):
+ col_name = force_text(col[0])
+
+ info_args = [col_name]
+ info_args.extend(col[1:6])
+
+ # is nullable
+ info_args.append(field_map[col_name][0] == 'YES')
+
+ # django 1.8 added default value to FieldInfo
+ if django.VERSION >= (1, 8, 0):
+ # default value
+ info_args.append(field_map[col_name][1])
+
+ return FieldInfo(*info_args)
class DatabaseSchemaIntrospection(DatabaseIntrospection):
+ _get_table_list_query = """
+ SELECT c.relname, c.relkind
+ FROM pg_catalog.pg_class c
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relkind IN ('r', 'v', '')
+ AND n.nspname = %(schema)s
+ AND pg_catalog.pg_table_is_visible(c.oid)
+ """
+
+ _get_table_description_query = """
+ SELECT column_name, is_nullable, column_default
+ FROM information_schema.columns
+ WHERE table_name = %(table)s
+ AND table_schema = %(schema)s
+ """
+
+ _get_relations_query = """
+ SELECT c2.relname, a1.attname, a2.attname, con.conkey, con.confkey
+ FROM pg_catalog.pg_constraint con
+ LEFT JOIN pg_catalog.pg_class c1 ON con.conrelid = c1.oid
+ LEFT JOIN pg_catalog.pg_class c2 ON con.confrelid = c2.oid
+ LEFT JOIN pg_catalog.pg_attribute a1 ON c1.oid = a1.attrelid
+ AND a1.attnum = con.conkey[1]
+ LEFT JOIN pg_catalog.pg_attribute a2 ON c2.oid = a2.attrelid
+ AND a2.attnum = con.confkey[1]
+ LEFT JOIN pg_catalog.pg_namespace n1 ON n1.oid = c1.connamespace
+ WHERE c1.relname = %(table)s
+ AND n1.nspname = %(schema)s
+ AND con.contype = 'f'
+ """
+
+ _get_key_columns_query = """
+ SELECT kcu.column_name, ccu.table_name AS referenced_table,
+ ccu.column_name AS referenced_column
+ FROM information_schema.constraint_column_usage ccu
+ LEFT JOIN information_schema.key_column_usage kcu
+ ON ccu.constraint_catalog = kcu.constraint_catalog
+ AND ccu.constraint_schema = kcu.constraint_schema
+ AND ccu.constraint_name = kcu.constraint_name
+ LEFT JOIN information_schema.table_constraints tc
+ ON ccu.constraint_catalog = tc.constraint_catalog
+ AND ccu.constraint_schema = tc.constraint_schema
+ AND ccu.constraint_name = tc.constraint_name
+ WHERE kcu.table_name = %(table)s
+ AND kcu.table_schame = %(schema)s
+ AND tc.constraint_type = 'FOREIGN KEY'
+ """
+
+ _get_indexes_query = """
+ SELECT attr.attname, idx.indkey, idx.indisunique, idx.indisprimary
+ FROM pg_catalog.pg_class c, pg_catalog.pg_class c2,
+ pg_catalog.pg_index idx, pg_catalog.pg_attribute attr,
+ pg_catalog.pg_namespace n
+ WHERE c.oid = idx.indrelid
+ AND idx.indexrelid = c2.oid
+ AND attr.attrelid = c.oid
+ AND attr.attnum = idx.indkey[0]
+ AND c.relnamespace = n.oid
+ AND c.relname = %(table)s
+ AND n.nspname = %(schema)s
+ """
+
+ _get_constaints_query = """
+ SELECT
+ kc.constraint_name,
+ kc.column_name,
+ c.constraint_type,
+ array(SELECT table_name::text || '.' || column_name::text
+ FROM information_schema.constraint_column_usage
+ WHERE constraint_name = kc.constraint_name)
+ FROM information_schema.key_column_usage AS kc
+ JOIN information_schema.table_constraints AS c ON
+ kc.table_schema = c.table_schema AND
+ kc.table_name = c.table_name AND
+ kc.constraint_name = c.constraint_name
+ WHERE
+ kc.table_schema = %(schema)s AND
+ kc.table_name = %(table)s
+ ORDER BY kc.ordinal_position ASC
+ """
+
+ _get_check_constraints_query = """
+ SELECT kc.constraint_name, kc.column_name
+ FROM information_schema.constraint_column_usage AS kc
+ JOIN information_schema.table_constraints AS c ON
+ kc.table_schema = c.table_schema AND
+ kc.table_name = c.table_name AND
+ kc.constraint_name = c.constraint_name
+ WHERE
+ c.constraint_type = 'CHECK' AND
+ kc.table_schema = %(schema)s AND
+ kc.table_name = %(table)s
+ """
+
+ _get_index_constraints_query = """
+ SELECT
+ c2.relname,
+ ARRAY(
+ SELECT (
+ SELECT attname
+ FROM pg_catalog.pg_attribute
+ WHERE attnum = i AND attrelid = c.oid
+ )
+ FROM unnest(idx.indkey) i
+ ),
+ idx.indisunique,
+ idx.indisprimary
+ FROM pg_catalog.pg_class c, pg_catalog.pg_class c2,
+ pg_catalog.pg_index idx, pg_catalog.pg_namespace n
+ WHERE c.oid = idx.indrelid
+ AND idx.indexrelid = c2.oid
+ AND n.oid = c.relnamespace
+ AND c.relname = %(table)s
+ AND n.nspname = %(schema)s
+ """
+
def get_table_list(self, cursor):
- "Returns a list of table names in the current database and schema."
- cursor.execute("""
- SELECT c.relname, c.relkind
- FROM pg_catalog.pg_class c
- LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
- WHERE c.relkind IN ('r', 'v', '')
- AND n.nspname = '%s'
- AND pg_catalog.pg_table_is_visible(c.oid)""" % self.connection.schema_name)
- if django.VERSION >= (1, 8, 0):
- return [TableInfo(row[0], {'r': 't', 'v': 'v'}.get(row[1]))
- for row in cursor.fetchall()
- if row[0] not in self.ignored_tables]
- else:
- return [row[0] for row in cursor.fetchall() if row[0] not in self.ignored_tables]
+ """
+ Returns a list of table names in the current database and schema.
+ """
+
+ cursor.execute(self._get_table_list_query, {
+ 'schema': self.connection.schema_name,
+ })
+
+ return [
+ _build_table_info(row)
+ for row in cursor.fetchall()
+ if row[0] not in self.ignored_tables
+ ]
+
+ def get_table_description(self, cursor, table_name):
+ """
+ Returns a description of the table, with the DB-API
+ cursor.description interface.
+ """
+
+ # As cursor.description does not return reliably the nullable property,
+ # we have to query the information_schema (#7783)
+ cursor.execute(self._get_table_description_query, {
+ 'schema': self.connection.schema_name,
+ 'table': table_name
+ })
+
+ field_map = {line[0]: line[1:] for line in cursor.fetchall()}
+
+ cursor.execute('SELECT * FROM %s.%s LIMIT 1' % (
+ self.connection.schema_name,
+ self.connection.ops.quote_name(table_name),
+ ))
+
+ return [
+ _build_field_info(line, field_map)
+ for line in cursor.description
+ ]
+
+ def get_relations(self, cursor, table_name):
+ """
+ Returns a dictionary of
+ {field_name: (field_name_other_table, other_table)}
+ representing all relationships to the given table.
+
+ Django 1.7:
+ Returns a dictionary of
+ {field_index: (field_index_other_table, other_table)}
+ representing all relationships to the given table. Indexes are 0-based.
+ """
+ cursor.execute(self._get_relations_query, {
+ 'schema': self.connection.schema_name,
+ 'table': table_name,
+ })
+ relations = {}
+ for row in cursor.fetchall():
+ if django.VERSION < (1, 8, 0):
+ # row[3] and row[4] are single-item lists,
+ # so grab the single item.
+ relations[row[3][0] - 1] = (row[4][0] - 1, row[0])
+ else:
+ relations[row[1]] = (row[2], row[0])
+ return relations
+
+ def get_key_columns(self, cursor, table_name):
+ cursor.execute(self._get_key_columns_query, {
+ 'schema': self.connection.schema_name,
+ 'table': table_name,
+ })
+ return list(cursor.fetchall())
+
+ def get_indexes(self, cursor, table_name):
+ # This query retrieves each index on the given table, including the
+ # first associated field name
+ cursor.execute(self._get_indexes_query, {
+ 'schema': self.connection.schema_name,
+ 'table': table_name,
+ })
+
+ indexes = {}
+ for row in cursor.fetchall():
+ # row[1] (idx.indkey) is stored in the DB as an array.
+ # It comes out as a string of space-separated integers.
+ # This designates the field indexes (1-based) of the fields
+ # that have indexes on the table. Here, we skip any indexes
+ # across multiple fields.
+ if ' ' in row[1]:
+ continue
+ if row[0] not in indexes:
+ indexes[row[0]] = {'primary_key': False, 'unique': False}
+ # It's possible to have the unique and PK constraints
+ # in separate indexes.
+ if row[3]:
+ indexes[row[0]]['primary_key'] = True
+ if row[2]:
+ indexes[row[0]]['unique'] = True
+ return indexes
+
+ def get_constraints(self, cursor, table_name):
+ """
+ Retrieves any constraints or keys (unique, pk, fk, check, index)
+ across one or more columns.
+ """
+ constraints = {}
+
+ # Loop over the key table, collecting things as constraints
+ # This will get PKs, FKs, and uniques, but not CHECK
+ cursor.execute(self._get_constaints_query, {
+ 'schema': self.connection.schema_name,
+ 'table': table_name,
+ })
+
+ for constraint, column, kind, used_cols in cursor.fetchall():
+ # If we're the first column, make the record
+ if constraint not in constraints:
+ constraints[constraint] = {
+ "columns": [],
+ "primary_key": kind.lower() == "primary key",
+ "unique": kind.lower() in ["primary key", "unique"],
+ "foreign_key":
+ tuple(used_cols[0].split(".", 1))
+ if kind.lower() == "foreign key"
+ else None,
+ "check": False,
+ "index": False,
+ }
+ # Record the details
+ constraints[constraint]['columns'].append(column)
+
+ # Now get CHECK constraint columns
+ cursor.execute(self._get_check_constraints_query, {
+ 'schema': self.connection.schema_name,
+ 'table': table_name,
+ })
+
+ for constraint, column in cursor.fetchall():
+ # If we're the first column, make the record
+ if constraint not in constraints:
+ constraints[constraint] = {
+ "columns": [],
+ "primary_key": False,
+ "unique": False,
+ "foreign_key": None,
+ "check": True,
+ "index": False,
+ }
+ # Record the details
+ constraints[constraint]['columns'].append(column)
+
+ # Now get indexes
+ cursor.execute(self._get_index_constraints_query, {
+ 'schema': self.connection.schema_name,
+ 'table': table_name,
+ })
+
+ for index, columns, unique, primary in cursor.fetchall():
+ if index not in constraints:
+ constraints[index] = {
+ "columns": list(columns),
+ "primary_key": primary,
+ "unique": unique,
+ "foreign_key": None,
+ "check": False,
+ "index": True,
+ }
+
+ return constraints
From cb8862d026750a5543867e246c0e8de09b21c4fa Mon Sep 17 00:00:00 2001
From: Gary Reynolds
Date: Tue, 17 May 2016 19:34:48 +1000
Subject: [PATCH 022/157] Remove Django < 1.8 compatibility
- resolves template loading in Django 1.8+ (thanks to @tomturner)
- removed code branches based on django.VERSION value
- updated documentation to remove details of removed functionality
Closes #289
---
README.rst | 3 +-
docs/install.rst | 39 +------
docs/use.rst | 25 +---
dts_test_project/dts_test_project/settings.py | 18 ++-
.../templates/index_public.html | 4 -
.../tenant_tutorial/settings.py | 6 +-
.../tenant_tutorial/urls_public.py | 4 -
.../tenant_tutorial/urls_tenants.py | 7 +-
.../tenant_tutorial/tenant_tutorial/views.py | 7 +-
setup.py | 5 +-
tenant_schemas/__init__.py | 7 +-
.../management/commands/__init__.py | 42 +------
.../management/commands/legacy/__init__.py | 3 -
.../commands/legacy/migrate_schemas.py | 88 --------------
tenant_schemas/management/commands/migrate.py | 6 +-
.../management/commands/migrate_schemas.py | 28 ++---
.../management/commands/sync_schemas.py | 57 ----------
tenant_schemas/management/commands/syncdb.py | 21 ----
tenant_schemas/models.py | 26 +----
.../postgresql_backend/introspection.py | 26 +----
tenant_schemas/template_loaders.py | 107 ++++++++++--------
tenant_schemas/test/cases.py | 21 +---
tenant_schemas/tests/test_tenants.py | 19 ++--
tenant_schemas/tests/test_utils.py | 48 +-------
tenant_schemas/tests/testcases.py | 19 +---
tox.ini | 3 +-
26 files changed, 121 insertions(+), 518 deletions(-)
delete mode 100644 tenant_schemas/management/commands/legacy/__init__.py
delete mode 100644 tenant_schemas/management/commands/legacy/migrate_schemas.py
delete mode 100644 tenant_schemas/management/commands/sync_schemas.py
delete mode 100644 tenant_schemas/management/commands/syncdb.py
diff --git a/README.rst b/README.rst
index c885f2c..b4d7edf 100644
--- a/README.rst
+++ b/README.rst
@@ -160,8 +160,7 @@ created ``Client`` inside an app named ``customers``, your
TENANT_MODEL = "customers.Client" # app.Model
-Now run ``migrate_schemas`` (``sync_schemas`` if you're on Django 1.6 and older),
-this will sync your apps to the ``public`` schema.
+Now run ``migrate_schemas`` to sync your apps to the ``public`` schema.
::
diff --git a/docs/install.rst b/docs/install.rst
index ffe586b..a564bea 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -73,7 +73,6 @@ Once you have defined your model, don't forget to create the migrations for it o
.. code-block:: bash
- # Django >= 1.7
python manage.py makemigrations customers
Configure Tenant and Shared Applications
@@ -113,20 +112,16 @@ You also have to set where your tenant model is.
TENANT_MODEL = "customers.Client" # app.Model
-Now run ``migrate_schemas --shared`` (``sync_schemas --shared`` if you're on Django 1.6 or older), this will create the shared apps on the ``public`` schema. Note: your database should be empty if this is the first time you're running this command.
+Now run ``migrate_schemas --shared`` to create the shared apps on the ``public`` schema. Note: your database should be empty if this is the first time you're running this command.
.. code-block:: bash
- # Django >= 1.7
python manage.py migrate_schemas --shared
- # Django < 1.7
- python manage.py sync_schemas --shared
-
.. warning::
- Never use ``migrate`` or ``syncdb`` as it would sync *all* your apps to ``public``!
-
+ Never use ``migrate`` as it would sync *all* your apps to ``public``!
+
Lastly, you need to create a tenant whose schema is ``public`` and it's address is your domain URL. Please see the section on :doc:`use
- django-tenant-schemas · Documentation
+ django-tenant-schemas · Documentation