Merge branch 'master' of github.com:bernardopires/django-tenant-schemas
This commit is contained in:
commit
f1e322ae6c
|
@ -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.
|
||||
|
||||
::
|
||||
|
||||
|
|
|
@ -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 <use>`.
|
||||
|
||||
You can also specify extra schemas that should be visible to all queries using
|
||||
|
@ -145,34 +140,6 @@ globally.
|
|||
available globally. This helps avoid issues caused by hiding the public
|
||||
schema from queries.
|
||||
|
||||
South Migrations
|
||||
================
|
||||
If you're on Django 1.6 or older, this app supports `South <http://south.aeracode.org/>`_ so if you haven't configured it yet and would like to:
|
||||
|
||||
For Django 1.1 or below
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
SOUTH_DATABASE_ADAPTER = 'south.db.postgresql_psycopg2'
|
||||
|
||||
For Django 1.2 or above
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
SOUTH_DATABASE_ADAPTERS = {
|
||||
'default': 'south.db.postgresql_psycopg2',
|
||||
}
|
||||
|
||||
You can list ``south`` under ``TENANT_APPS`` and ``SHARED_APPS`` if you want.
|
||||
|
||||
We override ``south``'s ``syncdb`` and ``migrate`` command, so you'll need to change your ``INSTALLED_APPS`` to
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
INSTALLED_APPS = SHARED_APPS + TENANT_APPS + ('tenant_schemas',)
|
||||
|
||||
This makes sure ``tenant_schemas`` is the last on the list and therefore always has precedence when running an overridden command.
|
||||
|
||||
Optional Settings
|
||||
=================
|
||||
|
||||
|
|
25
docs/use.rst
25
docs/use.rst
|
@ -66,12 +66,12 @@ To run only a particular schema, there is an optional argument called ``--schema
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
./manage.py sync_schemas --schema=customer1
|
||||
./manage.py migrate_schemas --schema=customer1
|
||||
|
||||
migrate_schemas
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
If you're on Django 1.7 or newer, ``migrate_schemas`` is the most important command on this app. The way it works is that it calls Django's ``migrate`` in two different ways. First, it calls ``migrate`` for the ``public`` schema, only syncing the shared apps. Then it runs ``migrate`` for every tenant in the database, this time only syncing the tenant apps.
|
||||
``migrate_schemas`` is the most important command on this app. The way it works is that it calls Django's ``migrate`` in two different ways. First, it calls ``migrate`` for the ``public`` schema, only syncing the shared apps. Then it runs ``migrate`` for every tenant in the database, this time only syncing the tenant apps.
|
||||
|
||||
.. warning::
|
||||
|
||||
|
@ -87,27 +87,6 @@ The options given to ``migrate_schemas`` are also passed to every ``migrate``. H
|
|||
|
||||
./manage.py migrate_schemas --list
|
||||
|
||||
sync_schemas
|
||||
~~~~~~~~~~~~
|
||||
|
||||
If you're on Django 1.6 or older, we also packed ``sync_schemas``. It will also respect the ``SHARED_APPS`` and ``TENANT_APPS`` settings, so if you're syncing the ``public`` schema it will only sync ``SHARED_APPS``. If you're syncing tenants, it will only migrate ``TENANT_APPS``.
|
||||
|
||||
.. warning::
|
||||
|
||||
You should never directly call ``syncdb``. We perform some magic in order to make ``syncdb`` only sync the appropriate apps.
|
||||
|
||||
The options given to ``sync_schemas`` are passed to every ``syncdb``. So if you use South, you may find this handy
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./manage.py sync_schemas --migrate
|
||||
|
||||
You can also use the option ``--tenant`` to only sync tenant apps or ``--shared`` to only sync shared apps.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./manage.py sync_schemas --shared # will only sync the public schema
|
||||
|
||||
tenant_command
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -2,10 +2,10 @@
|
|||
Django settings for dts_test_project project.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.6/topics/settings/
|
||||
https://docs.djangoproject.com/en/1.8/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.6/ref/settings/
|
||||
https://docs.djangoproject.com/en/1.8/ref/settings/
|
||||
"""
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
|
@ -14,7 +14,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
|||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/
|
||||
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'cl1)b#c&xmm36z3e(quna-vb@ab#&gpjtdjtpyzh!qn%bc^xxn'
|
||||
|
@ -48,11 +48,7 @@ TENANT_MODEL = "customers.Client" # app.Model
|
|||
|
||||
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
|
||||
|
||||
import django
|
||||
if django.VERSION >= (1, 7, 0):
|
||||
INSTALLED_APPS = list(set(TENANT_APPS + SHARED_APPS))
|
||||
else:
|
||||
INSTALLED_APPS = TENANT_APPS + SHARED_APPS + ('tenant_schemas',)
|
||||
INSTALLED_APPS = list(set(TENANT_APPS + SHARED_APPS))
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
|
@ -69,7 +65,7 @@ WSGI_APPLICATION = 'dts_test_project.wsgi.application'
|
|||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.6/ref/settings/#databases
|
||||
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
|
@ -104,7 +100,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
|||
)
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.6/topics/i18n/
|
||||
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
|
@ -118,7 +114,7 @@ USE_TZ = True
|
|||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.6/howto/static-files/
|
||||
# https://docs.djangoproject.com/en/1.8/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
|
|
|
@ -18,11 +18,7 @@
|
|||
</ul><br>
|
||||
<p>Just run the command below on your shell to sync <code>SHARED_APPS</code>. Make sure your environment
|
||||
has <code>Django</code> and <code>django-tenant-schemas</code> available.</p>
|
||||
{% if DJANGO17 %}
|
||||
<pre>$ python manage.py migrate_schemas --shared</pre>
|
||||
{% else %}
|
||||
<pre>$ python manage.py sync_schemas --shared</pre>
|
||||
{% endif %}
|
||||
<p>When you're done refresh this page.</p>
|
||||
{% elif no_public_tenant %}
|
||||
<h2>Second Step: Create a public tenant</h2>
|
||||
|
|
|
@ -143,11 +143,7 @@ TENANT_APPS = (
|
|||
|
||||
TENANT_MODEL = "customers.Client" # app.Model
|
||||
|
||||
import django
|
||||
if django.VERSION >= (1, 7, 0):
|
||||
INSTALLED_APPS = list(set(TENANT_APPS + SHARED_APPS))
|
||||
else:
|
||||
INSTALLED_APPS = TENANT_APPS + SHARED_APPS + ('tenant_schemas',)
|
||||
INSTALLED_APPS = list(set(TENANT_APPS + SHARED_APPS))
|
||||
|
||||
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import django
|
||||
from django.conf.urls import url
|
||||
from tenant_tutorial.views import HomeView
|
||||
|
||||
|
@ -6,6 +5,3 @@ from tenant_tutorial.views import HomeView
|
|||
urlpatterns = [
|
||||
url(r'^$', HomeView.as_view()),
|
||||
]
|
||||
|
||||
if django.VERSION < (1, 9, 0):
|
||||
urlpatterns = django.conf.urls.patterns('', *urlpatterns)
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import django
|
||||
from django.conf.urls import url
|
||||
from customers.views import TenantView
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', TenantView.as_view()),
|
||||
]
|
||||
|
||||
if django.VERSION < (1, 9, 0):
|
||||
urlpatterns = django.conf.urls.patterns('', *urlpatterns)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import django
|
||||
from customers.models import Client
|
||||
from django.conf import settings
|
||||
from django.db import utils
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from tenant_schemas.utils import remove_www
|
||||
from customers.models import Client
|
||||
|
||||
|
||||
class HomeView(TemplateView):
|
||||
|
@ -14,9 +14,6 @@ class HomeView(TemplateView):
|
|||
|
||||
hostname_without_port = remove_www(self.request.get_host().split(':')[0])
|
||||
|
||||
if django.VERSION >= (1, 7, 0):
|
||||
context['DJANGO17'] = True
|
||||
|
||||
try:
|
||||
Client.objects.get(schema_name='public')
|
||||
except utils.DatabaseError:
|
||||
|
|
5
setup.py
5
setup.py
|
@ -8,8 +8,6 @@ try:
|
|||
except ImportError:
|
||||
from distutils.core import setup
|
||||
|
||||
__version__ = "1.4.0"
|
||||
|
||||
setup(
|
||||
name='django-tenant-schemas',
|
||||
version=get_git_version(),
|
||||
|
@ -20,7 +18,6 @@ setup(
|
|||
'tenant_schemas.postgresql_backend',
|
||||
'tenant_schemas.management',
|
||||
'tenant_schemas.management.commands',
|
||||
'tenant_schemas.management.commands.legacy',
|
||||
'tenant_schemas.templatetags',
|
||||
'tenant_schemas.test',
|
||||
'tenant_schemas.tests',
|
||||
|
@ -36,7 +33,7 @@ setup(
|
|||
'Programming Language :: Python',
|
||||
],
|
||||
install_requires=[
|
||||
'Django >= 1.6.0',
|
||||
'Django >= 1.8.0',
|
||||
'psycopg2',
|
||||
],
|
||||
zip_safe=False,
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import django
|
||||
import warnings
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from tenant_schemas.utils import get_public_schema_name, get_tenant_model
|
||||
|
||||
from tenant_schemas.utils import get_public_schema_name, get_tenant_model
|
||||
|
||||
recommended_config = """
|
||||
Warning: You should put 'tenant_schemas' at the end of INSTALLED_APPS:
|
||||
|
@ -23,9 +21,6 @@ if not settings.TENANT_APPS:
|
|||
if not hasattr(settings, 'TENANT_MODEL'):
|
||||
raise ImproperlyConfigured('TENANT_MODEL setting not set')
|
||||
|
||||
if django.VERSION < (1, 7, 0) and settings.INSTALLED_APPS[-1] != 'tenant_schemas':
|
||||
warnings.warn(recommended_config, SyntaxWarning)
|
||||
|
||||
if 'tenant_schemas.routers.TenantSyncRouter' not in settings.DATABASE_ROUTERS:
|
||||
raise ImproperlyConfigured("DATABASE_ROUTERS setting must contain "
|
||||
"'tenant_schemas.routers.TenantSyncRouter'.")
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import django
|
||||
from optparse import make_option
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command, get_commands, load_command_class
|
||||
from django.core.management.base import BaseCommand, NoArgsCommand, CommandError
|
||||
from django.db import connection, DEFAULT_DB_ALIAS
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import connection
|
||||
|
||||
try:
|
||||
from django.utils.six.moves import input
|
||||
|
@ -32,16 +30,6 @@ class BaseTenantCommand(BaseCommand):
|
|||
else:
|
||||
cmdclass = load_command_class(app_name, obj.COMMAND_NAME)
|
||||
|
||||
if django.VERSION < (1, 8, 0):
|
||||
# inherit the options from the original command
|
||||
obj.option_list = cmdclass.option_list
|
||||
obj.option_list += (
|
||||
make_option("-s", "--schema", dest="schema_name"),
|
||||
)
|
||||
obj.option_list += (
|
||||
make_option("-p", "--skip-public", dest="skip_public", action="store_true", default=False),
|
||||
)
|
||||
|
||||
# prepend the command's original help with the info about schemata iteration
|
||||
obj.help = "Calls %s for all registered schemata. You can use regular %s options. " \
|
||||
"Original help for %s: %s" % (obj.COMMAND_NAME, obj.COMMAND_NAME, obj.COMMAND_NAME,
|
||||
|
@ -83,12 +71,6 @@ class BaseTenantCommand(BaseCommand):
|
|||
|
||||
|
||||
class InteractiveTenantOption(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InteractiveTenantOption, self).__init__(*args, **kwargs)
|
||||
if django.VERSION < (1, 8, 0):
|
||||
self.option_list += (
|
||||
make_option("-s", "--schema", dest="schema_name", help="specify tenant schema"),
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("-s", "--schema", dest="schema_name", help="specify tenant schema")
|
||||
|
@ -143,28 +125,8 @@ class TenantWrappedCommand(InteractiveTenantOption, BaseCommand):
|
|||
|
||||
|
||||
class SyncCommon(BaseCommand):
|
||||
if django.VERSION < (1, 8, 0):
|
||||
option_list = (
|
||||
make_option('--tenant', action='store_true', dest='tenant', default=False,
|
||||
help='Tells Django to populate only tenant applications.'),
|
||||
make_option('--shared', action='store_true', dest='shared', default=False,
|
||||
help='Tells Django to populate only shared applications.'),
|
||||
make_option('--app_label', action='store', dest='app_label', nargs='?',
|
||||
help='App label of an application to synchronize the state.'),
|
||||
make_option('--migration_name', action='store', dest='migration_name', nargs='?',
|
||||
help=('Database state will be brought to the state after that '
|
||||
'migration. Use the name "zero" to unapply all migrations.')),
|
||||
make_option("-s", "--schema", dest="schema_name"),
|
||||
)
|
||||
|
||||
def __init__(self, stdout=None, stderr=None, no_color=False):
|
||||
if django.VERSION >= (1, 8, 0):
|
||||
super(SyncCommon, self).__init__(stdout, stderr, no_color)
|
||||
else:
|
||||
super(SyncCommon, self).__init__()
|
||||
|
||||
def add_arguments(self, parser):
|
||||
# for django 1.8 and above
|
||||
parser.add_argument('--tenant', action='store_true', dest='tenant', default=False,
|
||||
help='Tells Django to populate only tenant applications.')
|
||||
parser.add_argument('--shared', action='store_true', dest='shared', default=False,
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
"""
|
||||
Commands for Django <=1.6 (before south was integrated to Django).
|
||||
"""
|
|
@ -1,88 +0,0 @@
|
|||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from south import migration
|
||||
from south.migration.base import Migrations
|
||||
from south.management.commands.migrate import Command as MigrateCommand
|
||||
from tenant_schemas.management.commands import SyncCommon
|
||||
from tenant_schemas.utils import get_tenant_model, get_public_schema_name
|
||||
|
||||
|
||||
class Command(SyncCommon):
|
||||
help = "Migrate schemas with South"
|
||||
option_list = MigrateCommand.option_list + SyncCommon.option_list
|
||||
|
||||
def handle(self, *args, **options):
|
||||
super(Command, self).handle(*args, **options)
|
||||
|
||||
if self.sync_public:
|
||||
self.migrate_public_apps()
|
||||
if self.sync_tenant:
|
||||
self.migrate_tenant_apps(self.schema_name)
|
||||
|
||||
def _set_managed_apps(self, included_apps, excluded_apps):
|
||||
""" while sync_schemas works by setting which apps are managed, on south we set which apps should be ignored """
|
||||
ignored_apps = []
|
||||
if excluded_apps:
|
||||
for item in excluded_apps:
|
||||
if item not in included_apps:
|
||||
ignored_apps.append(item)
|
||||
|
||||
for app in ignored_apps:
|
||||
app_label = app.split('.')[-1]
|
||||
settings.SOUTH_MIGRATION_MODULES[app_label] = 'ignore'
|
||||
|
||||
self._clear_south_cache()
|
||||
|
||||
def _save_south_settings(self):
|
||||
self._old_south_modules = None
|
||||
if hasattr(settings, "SOUTH_MIGRATION_MODULES") and settings.SOUTH_MIGRATION_MODULES is not None:
|
||||
self._old_south_modules = settings.SOUTH_MIGRATION_MODULES.copy()
|
||||
else:
|
||||
settings.SOUTH_MIGRATION_MODULES = dict()
|
||||
|
||||
def _restore_south_settings(self):
|
||||
settings.SOUTH_MIGRATION_MODULES = self._old_south_modules
|
||||
|
||||
def _clear_south_cache(self):
|
||||
for mig in list(migration.all_migrations()):
|
||||
delattr(mig._application, "migrations")
|
||||
Migrations._clear_cache()
|
||||
|
||||
def _migrate_schema(self, tenant):
|
||||
connection.set_tenant(tenant, include_public=True)
|
||||
MigrateCommand().execute(*self.args, **self.options)
|
||||
|
||||
def migrate_tenant_apps(self, schema_name=None):
|
||||
self._save_south_settings()
|
||||
|
||||
apps = self.tenant_apps or self.installed_apps
|
||||
self._set_managed_apps(included_apps=apps, excluded_apps=self.shared_apps)
|
||||
|
||||
if schema_name:
|
||||
self._notice("=== Running migrate for schema: %s" % schema_name)
|
||||
connection.set_schema_to_public()
|
||||
tenant = get_tenant_model().objects.get(schema_name=schema_name)
|
||||
self._migrate_schema(tenant)
|
||||
else:
|
||||
all_tenants = get_tenant_model().objects.exclude(schema_name=get_public_schema_name())
|
||||
if not all_tenants:
|
||||
self._notice("No tenants found")
|
||||
|
||||
for tenant in all_tenants:
|
||||
Migrations._dependencies_done = False # very important, the dependencies need to be purged from cache
|
||||
self._notice("=== Running migrate for schema %s" % tenant.schema_name)
|
||||
self._migrate_schema(tenant)
|
||||
|
||||
self._restore_south_settings()
|
||||
|
||||
def migrate_public_apps(self):
|
||||
self._save_south_settings()
|
||||
|
||||
apps = self.shared_apps or self.installed_apps
|
||||
self._set_managed_apps(included_apps=apps, excluded_apps=self.tenant_apps)
|
||||
|
||||
self._notice("=== Running migrate for schema public")
|
||||
MigrateCommand().execute(*self.args, **self.options)
|
||||
|
||||
self._clear_south_cache()
|
||||
self._restore_south_settings()
|
|
@ -1,6 +1,7 @@
|
|||
import django
|
||||
from django.conf import settings
|
||||
from django.core.management.base import CommandError, BaseCommand
|
||||
|
||||
from tenant_schemas.management.commands.migrate_schemas import Command as MigrateSchemasCommand
|
||||
from tenant_schemas.utils import django_is_in_test_mode
|
||||
|
||||
try:
|
||||
|
@ -21,6 +22,5 @@ class Command(MigrateCommand):
|
|||
super(Command, self).handle(*args, **options)
|
||||
|
||||
|
||||
if django.VERSION >= (1, 7, 0) and django_is_in_test_mode():
|
||||
from .migrate_schemas import MigrateSchemasCommand
|
||||
if django_is_in_test_mode():
|
||||
Command = MigrateSchemasCommand
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
import django
|
||||
from optparse import NO_DEFAULT
|
||||
|
||||
if django.VERSION >= (1, 7, 0):
|
||||
from django.core.management.commands.migrate import Command as MigrateCommand
|
||||
from django.db.migrations.recorder import MigrationRecorder
|
||||
from django.db import connection
|
||||
from django.conf import settings
|
||||
from django.core.management.commands.migrate import Command as MigrateCommand
|
||||
from django.db import connection
|
||||
|
||||
from tenant_schemas.utils import get_tenant_model, get_public_schema_name, schema_exists
|
||||
from tenant_schemas.management.commands import SyncCommon
|
||||
from tenant_schemas.utils import get_tenant_model, get_public_schema_name, schema_exists
|
||||
|
||||
|
||||
class MigrateSchemasCommand(SyncCommon):
|
||||
class Command(SyncCommon):
|
||||
help = "Updates database schema. Manages both apps with migrations and those without."
|
||||
|
||||
def __init__(self, stdout=None, stderr=None, no_color=False):
|
||||
|
@ -19,18 +14,15 @@ class MigrateSchemasCommand(SyncCommon):
|
|||
Changes the option_list to use the options from the wrapped migrate command.
|
||||
"""
|
||||
self.option_list += MigrateCommand.option_list
|
||||
if django.VERSION >= (1, 8, 0):
|
||||
super(MigrateSchemasCommand, self).__init__(stdout, stderr, no_color)
|
||||
else:
|
||||
super(MigrateSchemasCommand, self).__init__()
|
||||
super(Command, self).__init__(stdout, stderr, no_color)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super(MigrateSchemasCommand, self).add_arguments(parser)
|
||||
super(Command, self).add_arguments(parser)
|
||||
command = MigrateCommand()
|
||||
command.add_arguments(parser)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
super(MigrateSchemasCommand, self).handle(*args, **options)
|
||||
super(Command, self).handle(*args, **options)
|
||||
self.PUBLIC_SCHEMA_NAME = get_public_schema_name()
|
||||
|
||||
if self.sync_public and not self.schema_name:
|
||||
|
@ -60,9 +52,3 @@ class MigrateSchemasCommand(SyncCommon):
|
|||
|
||||
def _notice(self, output):
|
||||
self.stdout.write(self.style.NOTICE(output))
|
||||
|
||||
|
||||
if django.VERSION >= (1, 7, 0):
|
||||
Command = MigrateSchemasCommand
|
||||
else:
|
||||
from .legacy.migrate_schemas import Command
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
import django
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.models import get_apps, get_models
|
||||
if "south" in settings.INSTALLED_APPS:
|
||||
from south.management.commands.syncdb import Command as SyncdbCommand
|
||||
else:
|
||||
from django.core.management.commands.syncdb import Command as SyncdbCommand
|
||||
from django.db import connection
|
||||
from tenant_schemas.utils import get_tenant_model, get_public_schema_name
|
||||
from tenant_schemas.management.commands import SyncCommon
|
||||
|
||||
|
||||
class Command(SyncCommon):
|
||||
help = "Sync schemas based on TENANT_APPS and SHARED_APPS settings"
|
||||
option_list = SyncdbCommand.option_list + SyncCommon.option_list
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if django.VERSION >= (1, 7, 0):
|
||||
raise RuntimeError('This command is only meant to be used for 1.6'
|
||||
' and older version of django. For 1.7, use'
|
||||
' `migrate_schemas` instead.')
|
||||
super(Command, self).handle(*args, **options)
|
||||
|
||||
if "south" in settings.INSTALLED_APPS:
|
||||
self.options["migrate"] = False
|
||||
|
||||
# Content types may be different on tenants, so reset the cache
|
||||
ContentType.objects.clear_cache()
|
||||
|
||||
if self.sync_public:
|
||||
self.sync_public_apps()
|
||||
if self.sync_tenant:
|
||||
self.sync_tenant_apps(self.schema_name)
|
||||
|
||||
def _sync_tenant(self, tenant):
|
||||
if int(self.options.get('verbosity', 1)) >= 1:
|
||||
self._notice("=== Running syncdb for schema: %s" % tenant.schema_name)
|
||||
connection.set_tenant(tenant, include_public=False)
|
||||
SyncdbCommand().execute(**self.options)
|
||||
|
||||
def sync_tenant_apps(self, schema_name=None):
|
||||
if schema_name:
|
||||
tenant = get_tenant_model().objects.filter(schema_name=schema_name).get()
|
||||
self._sync_tenant(tenant)
|
||||
else:
|
||||
all_tenants = get_tenant_model().objects.exclude(schema_name=get_public_schema_name())
|
||||
if not all_tenants:
|
||||
self._notice("No tenants found!")
|
||||
|
||||
for tenant in all_tenants:
|
||||
self._sync_tenant(tenant)
|
||||
|
||||
def sync_public_apps(self):
|
||||
SyncdbCommand().execute(**self.options)
|
||||
self._notice("=== Running syncdb for schema public")
|
|
@ -1,21 +0,0 @@
|
|||
from django.core.management.base import CommandError
|
||||
from django.conf import settings
|
||||
from tenant_schemas.utils import django_is_in_test_mode
|
||||
|
||||
if 'south' in settings.INSTALLED_APPS:
|
||||
from south.management.commands import syncdb
|
||||
else:
|
||||
from django.core.management.commands import syncdb
|
||||
|
||||
|
||||
class Command(syncdb.Command):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
database = options.get('database', 'default')
|
||||
if (settings.DATABASES[database]['ENGINE'] == 'tenant_schemas.postgresql_backend' and not
|
||||
django_is_in_test_mode()):
|
||||
raise CommandError("syncdb has been disabled, for database '{0}'. "
|
||||
"Use sync_schemas instead. Please read the "
|
||||
"documentation if you don't know why "
|
||||
"you shouldn't call syncdb directly!".format(database))
|
||||
super(Command, self).handle(*args, **options)
|
|
@ -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):
|
||||
"""
|
||||
|
@ -92,27 +93,9 @@ class TenantMixin(models.Model):
|
|||
cursor.execute('CREATE SCHEMA %s' % self.schema_name)
|
||||
|
||||
if sync_schema:
|
||||
if django.VERSION >= (1, 7, 0,):
|
||||
call_command('migrate_schemas',
|
||||
schema_name=self.schema_name,
|
||||
interactive=False,
|
||||
verbosity=verbosity)
|
||||
else:
|
||||
# default is faking all migrations and syncing directly to the current models state
|
||||
fake_all_migrations = getattr(settings, 'TENANT_CREATION_FAKES_MIGRATIONS', True)
|
||||
call_command('sync_schemas',
|
||||
schema_name=self.schema_name,
|
||||
tenant=True,
|
||||
public=False,
|
||||
interactive=False,
|
||||
migrate_all=fake_all_migrations,
|
||||
verbosity=verbosity)
|
||||
|
||||
# run/fake all migrations
|
||||
if 'south' in settings.INSTALLED_APPS and not django_is_in_test_mode():
|
||||
call_command('migrate_schemas',
|
||||
fake=fake_all_migrations,
|
||||
schema_name=self.schema_name,
|
||||
verbosity=verbosity)
|
||||
call_command('migrate_schemas',
|
||||
schema_name=self.schema_name,
|
||||
interactive=False,
|
||||
verbosity=verbosity)
|
||||
|
||||
connection.set_schema_to_public()
|
||||
|
|
|
@ -1,22 +1,302 @@
|
|||
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.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):
|
||||
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')
|
||||
|
||||
# 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.
|
||||
"""
|
||||
cursor.execute(self._get_relations_query, {
|
||||
'schema': self.connection.schema_name,
|
||||
'table': table_name,
|
||||
})
|
||||
relations = {}
|
||||
for row in cursor.fetchall():
|
||||
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
|
||||
|
|
|
@ -6,81 +6,92 @@ multi-tenant setting
|
|||
import hashlib
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.template.base import TemplateDoesNotExist
|
||||
from django.template.loader import (BaseLoader, get_template_from_string,
|
||||
find_template_loader, make_origin)
|
||||
from django.template.base import TemplateDoesNotExist, Template
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils._os import safe_join
|
||||
from django.db import connection
|
||||
from tenant_schemas.postgresql_backend.base import FakeTenant
|
||||
from django.template.loaders.base import Loader as BaseLoader
|
||||
|
||||
from tenant_schemas.postgresql_backend.base import FakeTenant
|
||||
import django
|
||||
|
||||
class CachedLoader(BaseLoader):
|
||||
is_usable = True
|
||||
|
||||
def __init__(self, loaders):
|
||||
def __init__(self, engine, loaders):
|
||||
self.template_cache = {}
|
||||
self._loaders = loaders
|
||||
self._cached_loaders = []
|
||||
self.find_template_cache = {}
|
||||
self.loaders = engine.get_template_loaders(loaders)
|
||||
super(CachedLoader, self).__init__(engine)
|
||||
|
||||
@property
|
||||
def loaders(self):
|
||||
# Resolve loaders on demand to avoid circular imports
|
||||
if not self._cached_loaders:
|
||||
# Set self._cached_loaders atomically. Otherwise, another thread
|
||||
# could see an incomplete list. See #17303.
|
||||
cached_loaders = []
|
||||
for loader in self._loaders:
|
||||
cached_loaders.append(find_template_loader(loader))
|
||||
self._cached_loaders = cached_loaders
|
||||
return self._cached_loaders
|
||||
@staticmethod
|
||||
def cache_key(template_name, template_dirs):
|
||||
if connection.tenant and template_dirs:
|
||||
return '-'.join([str(connection.tenant.pk), template_name,
|
||||
hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()])
|
||||
if template_dirs:
|
||||
# If template directories were specified, use a hash to differentiate
|
||||
return '-'.join([template_name, hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()])
|
||||
else:
|
||||
return template_name
|
||||
|
||||
def find_template(self, name, dirs=None):
|
||||
for loader in self.loaders:
|
||||
try:
|
||||
template, display_name = loader(name, dirs)
|
||||
return template, make_origin(display_name, loader, name, dirs)
|
||||
except TemplateDoesNotExist:
|
||||
pass
|
||||
raise TemplateDoesNotExist(name)
|
||||
"""
|
||||
Helper method. Lookup the template :param name: in all the configured loaders
|
||||
"""
|
||||
key = self.cache_key(name, dirs)
|
||||
try:
|
||||
result = self.find_template_cache[key]
|
||||
except KeyError:
|
||||
result = None
|
||||
for loader in self.loaders:
|
||||
try:
|
||||
template, display_name = loader(name, dirs)
|
||||
except TemplateDoesNotExist:
|
||||
pass
|
||||
else:
|
||||
origin = self.engine.make_origin(display_name, loader, name, dirs)
|
||||
result = template, origin
|
||||
break
|
||||
self.find_template_cache[key] = result
|
||||
if result:
|
||||
return result
|
||||
else:
|
||||
self.template_cache[key] = TemplateDoesNotExist
|
||||
raise TemplateDoesNotExist(name)
|
||||
|
||||
def load_template(self, template_name, template_dirs=None):
|
||||
if connection.tenant:
|
||||
key = '-'.join([str(connection.tenant.pk), template_name])
|
||||
else:
|
||||
key = template_name
|
||||
if template_dirs:
|
||||
# If template directories were specified, use a hash to
|
||||
# differentiate
|
||||
if connection.tenant:
|
||||
key = '-'.join([str(connection.tenant.pk), template_name,
|
||||
hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()])
|
||||
else:
|
||||
key = '-'.join([template_name, hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()])
|
||||
|
||||
if key not in self.template_cache:
|
||||
key = self.cache_key(template_name, template_dirs)
|
||||
template_tuple = self.template_cache.get(key)
|
||||
# A cached previous failure:
|
||||
if template_tuple is TemplateDoesNotExist:
|
||||
raise TemplateDoesNotExist
|
||||
elif template_tuple is None:
|
||||
template, origin = self.find_template(template_name, template_dirs)
|
||||
if not hasattr(template, 'render'):
|
||||
try:
|
||||
template = get_template_from_string(template, origin, template_name)
|
||||
template = Template(template, origin, template_name, self.engine)
|
||||
except TemplateDoesNotExist:
|
||||
# If compiling the template we found raises TemplateDoesNotExist,
|
||||
# back off to returning the source and display name for the template
|
||||
# we were asked to load. This allows for correct identification (later)
|
||||
# of the actual template that does not exist.
|
||||
return template, origin
|
||||
self.template_cache[key] = template
|
||||
return self.template_cache[key], None
|
||||
self.template_cache[key] = (template, origin)
|
||||
self.template_cache[key] = (template, None)
|
||||
return self.template_cache[key]
|
||||
|
||||
def reset(self):
|
||||
"Empty the template cache."
|
||||
"""
|
||||
Empty the template cache.
|
||||
"""
|
||||
self.template_cache.clear()
|
||||
|
||||
|
||||
class FilesystemLoader(BaseLoader):
|
||||
is_usable = True
|
||||
|
||||
def get_template_sources(self, template_name, template_dirs=None):
|
||||
@staticmethod
|
||||
def get_template_sources(template_name, template_dirs=None):
|
||||
"""
|
||||
Returns the absolute paths to "template_name", when appended to each
|
||||
directory in "template_dirs". Any paths that don't lie inside one of the
|
||||
|
@ -97,9 +108,9 @@ class FilesystemLoader(BaseLoader):
|
|||
for template_dir in template_dirs:
|
||||
try:
|
||||
if '%s' in template_dir:
|
||||
yield safe_join(template_dir % connection.tenant.domain_url, template_name)
|
||||
yield safe_join(template_dir % connection.tenant.schema_name, template_name)
|
||||
else:
|
||||
yield safe_join(template_dir, connection.tenant.domain_url, template_name)
|
||||
yield safe_join(template_dir, connection.tenant.schema_name, template_name)
|
||||
except UnicodeDecodeError:
|
||||
# The template dir name was a bytestring that wasn't valid UTF-8.
|
||||
raise
|
||||
|
@ -114,7 +125,7 @@ class FilesystemLoader(BaseLoader):
|
|||
for filepath in self.get_template_sources(template_name, template_dirs):
|
||||
try:
|
||||
with open(filepath, 'rb') as fp:
|
||||
return (fp.read().decode(settings.FILE_CHARSET), filepath)
|
||||
return fp.read().decode(settings.FILE_CHARSET), filepath
|
||||
except IOError:
|
||||
tried.append(filepath)
|
||||
if tried:
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import django
|
||||
from django.core.management import call_command
|
||||
from django.db import connection
|
||||
from django.test import TestCase
|
||||
|
||||
from tenant_schemas.utils import get_tenant_model
|
||||
from tenant_schemas.utils import get_public_schema_name
|
||||
from tenant_schemas.utils import get_tenant_model
|
||||
|
||||
|
||||
class TenantTestCase(TestCase):
|
||||
|
@ -27,17 +26,7 @@ class TenantTestCase(TestCase):
|
|||
|
||||
@classmethod
|
||||
def sync_shared(cls):
|
||||
if django.VERSION >= (1, 7, 0):
|
||||
call_command('migrate_schemas',
|
||||
schema_name=get_public_schema_name(),
|
||||
interactive=False,
|
||||
verbosity=0)
|
||||
else:
|
||||
call_command('sync_schemas',
|
||||
schema_name=get_public_schema_name(),
|
||||
tenant=False,
|
||||
public=True,
|
||||
interactive=False,
|
||||
migrate_all=True,
|
||||
verbosity=0,
|
||||
)
|
||||
call_command('migrate_schemas',
|
||||
schema_name=get_public_schema_name(),
|
||||
interactive=False,
|
||||
verbosity=0)
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import django
|
||||
import json
|
||||
from StringIO import StringIO
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management import call_command
|
||||
from django.db import connection
|
||||
from StringIO import StringIO
|
||||
|
||||
from dts_test_app.models import DummyModel, ModelWithFkToPublicUser
|
||||
|
||||
from tenant_schemas.test.cases import TenantTestCase
|
||||
from tenant_schemas.tests.models import Tenant, NonAutoSyncTenant
|
||||
from tenant_schemas.tests.testcases import BaseTestCase
|
||||
|
@ -124,7 +124,7 @@ class TenantSyncTest(BaseTestCase):
|
|||
Tests if the shared apps and the tenant apps get synced correctly
|
||||
depending on if the public schema or a tenant is being synced.
|
||||
"""
|
||||
MIGRATION_TABLE_SIZE = 1 if django.VERSION >= (1, 7, 0) else 0
|
||||
MIGRATION_TABLE_SIZE = 1
|
||||
|
||||
def test_shared_apps_does_not_sync_tenant_apps(self):
|
||||
"""
|
||||
|
@ -217,12 +217,11 @@ class TenantCommandTest(BaseTestCase):
|
|||
Tenant(domain_url='localhost', schema_name='public').save(verbosity=BaseTestCase.get_verbosity())
|
||||
|
||||
out = StringIO()
|
||||
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)
|
||||
call_command('tenant_command',
|
||||
args=('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}]'),
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
import types
|
||||
|
||||
from django.apps import AppConfig
|
||||
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(self):
|
||||
"""
|
||||
Verifies that app_labels handle Django 1.7+ AppConfigs properly.
|
||||
https://docs.djangoproject.com/en/1.7/ref/applications/
|
||||
"""
|
||||
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'],
|
||||
)
|
|
@ -52,17 +52,8 @@ class BaseTestCase(TestCase):
|
|||
|
||||
@classmethod
|
||||
def sync_shared(cls):
|
||||
if django.VERSION >= (1, 7, 0):
|
||||
call_command('migrate_schemas',
|
||||
schema_name=get_public_schema_name(),
|
||||
interactive=False,
|
||||
verbosity=cls.get_verbosity(),
|
||||
run_syncdb=True)
|
||||
else:
|
||||
call_command('sync_schemas',
|
||||
schema_name=get_public_schema_name(),
|
||||
tenant=False,
|
||||
public=True,
|
||||
interactive=False,
|
||||
migrate_all=True,
|
||||
verbosity=cls.get_verbosity())
|
||||
call_command('migrate_schemas',
|
||||
schema_name=get_public_schema_name(),
|
||||
interactive=False,
|
||||
verbosity=cls.get_verbosity(),
|
||||
run_syncdb=True)
|
||||
|
|
|
@ -4,10 +4,11 @@ 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
|
||||
|
||||
|
||||
|
@ -108,6 +109,12 @@ def schema_exists(schema_name):
|
|||
|
||||
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.
|
||||
|
||||
https://docs.djangoproject.com/en/1.8/ref/applications/#django.apps.AppConfig.label
|
||||
"""
|
||||
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]
|
||||
|
||||
|
|
Loading…
Reference in New Issue