remove multitenant, now in hobo (#6491)
This commit is contained in:
parent
1db4b2429e
commit
43d6b99c0f
|
@ -1,99 +0,0 @@
|
||||||
Multitenant
|
|
||||||
-----------
|
|
||||||
|
|
||||||
An application for making a Django application multitenant for Entr'ouvert
|
|
||||||
customers.
|
|
||||||
|
|
||||||
Based on https://django-tenant-schemas.readthedocs.org/
|
|
||||||
|
|
||||||
It is developed, tested and supported on Django 1.7, but it should work with
|
|
||||||
Django 1.6 + south.
|
|
||||||
|
|
||||||
|
|
||||||
Install
|
|
||||||
-------
|
|
||||||
|
|
||||||
See also : https://django-tenant-schemas.readthedocs.org/
|
|
||||||
|
|
||||||
Set the tenant model:
|
|
||||||
|
|
||||||
TENANT_MODEL = 'multitenant.Tenant'
|
|
||||||
|
|
||||||
Where are tenants:
|
|
||||||
|
|
||||||
TENANT_BASE = '/var/lib/<project>/tenants'
|
|
||||||
|
|
||||||
Add the middlewares for multitenant, they must be first:
|
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = (
|
|
||||||
'entrouvert.djommon.multitenant.middleware.TenantMiddleware',
|
|
||||||
'entrouvert.djommon.multitenant.middleware.JSONSettingsMiddleware',
|
|
||||||
'entrouvert.djommon.multitenant.middleware.PythonSettingsMiddleware',
|
|
||||||
) + MIDDLEWARE_CLASSES
|
|
||||||
|
|
||||||
Define the shared applications:
|
|
||||||
|
|
||||||
SHARED_APPS = (
|
|
||||||
'tenant_schemas',
|
|
||||||
'entrouvert.djommon.multitenant',
|
|
||||||
# those are needed for the public apps to work
|
|
||||||
# add also any application needed by the public app
|
|
||||||
'django.contrib.auth',
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'django.contrib.sessions',
|
|
||||||
'django.contrib.messages',
|
|
||||||
'django.contrib.staticfiles',
|
|
||||||
)
|
|
||||||
|
|
||||||
TENANT_APPS = INSTALLED_APPS
|
|
||||||
|
|
||||||
INSTALLED_APPS = ('entrouvert.djommon.multitenant',
|
|
||||||
'tenant_schemas') + INSTALLED_APPS
|
|
||||||
|
|
||||||
# or, with Django 1.6 or older:
|
|
||||||
# INSTALLED_APPS += ('tenant_schemas', 'entrouvert.djommon.multitenant')
|
|
||||||
|
|
||||||
Use multitenant database engine:
|
|
||||||
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'tenant_schemas.postgresql_backend',
|
|
||||||
'NAME': '<db_name>',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
DATABASE_ROUTERS = (
|
|
||||||
'tenant_schemas.routers.TenantSyncRouter',
|
|
||||||
)
|
|
||||||
|
|
||||||
# With Django 1.6 or older, use multitenant south adapter:
|
|
||||||
# SOUTH_DATABASE_ADAPTERS = {'default': 'south.db.postgresql_psycopg2'}
|
|
||||||
|
|
||||||
Add the multitenant filesystem template loader and configure where the
|
|
||||||
multitenant templates are located:
|
|
||||||
|
|
||||||
TEMPLATE_LOADERS = (
|
|
||||||
'entrouvert.djommon.multitenant.template_loader.FilesystemLoader',
|
|
||||||
) + TEMPLATE_LOADERS
|
|
||||||
TENANT_TEMPLATE_DIRS = (TENANT_BASE,)
|
|
||||||
|
|
||||||
TEMPLATE_CONTEXT_PROCESSORS = (
|
|
||||||
'django.core.context_processors.request',
|
|
||||||
) + TEMPLATE_CONTEXT_PROCESSORS
|
|
||||||
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
|
|
||||||
Create a tenant:
|
|
||||||
manage.py create_tenant www.example.net
|
|
||||||
Migration of all tenants:
|
|
||||||
manage.py migrate_schemas
|
|
||||||
Add a super user to tenant:
|
|
||||||
manage.py tenant_command createsuperuser --domain www.example.net
|
|
||||||
|
|
||||||
Tenants are created in TENANT_BASE directory, for example :
|
|
||||||
/var/lib/project/tenants/www.example.net/
|
|
||||||
templates/ <-- override project templates
|
|
||||||
static/ <-- to be handled by HTTP server
|
|
||||||
media/
|
|
||||||
Each tenant is a PostgreSQL schema, named www_example_net
|
|
|
@ -1,171 +0,0 @@
|
||||||
# this file derive from django-tenant-schemas
|
|
||||||
# Author: Bernardo Pires Carneiro
|
|
||||||
# Email: carneiro.be@gmail.com
|
|
||||||
# License: MIT license
|
|
||||||
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
|
|
||||||
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, CommandError
|
|
||||||
from django.db import connection
|
|
||||||
try:
|
|
||||||
from django.utils.six.moves import input
|
|
||||||
except ImportError:
|
|
||||||
input = raw_input
|
|
||||||
from tenant_schemas.utils import get_public_schema_name
|
|
||||||
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
|
|
||||||
|
|
||||||
|
|
||||||
class BaseTenantCommand(BaseCommand):
|
|
||||||
"""
|
|
||||||
Generic command class useful for iterating any existing command
|
|
||||||
over all schemata. The actual command name is expected in the
|
|
||||||
class variable COMMAND_NAME of the subclass.
|
|
||||||
"""
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Sets option_list and help dynamically.
|
|
||||||
"""
|
|
||||||
obj = super(BaseTenantCommand, cls).__new__(cls, *args, **kwargs)
|
|
||||||
|
|
||||||
app_name = get_commands()[obj.COMMAND_NAME]
|
|
||||||
if isinstance(app_name, BaseCommand):
|
|
||||||
# If the command is already loaded, use it directly.
|
|
||||||
cmdclass = app_name
|
|
||||||
else:
|
|
||||||
cmdclass = load_command_class(app_name, obj.COMMAND_NAME)
|
|
||||||
|
|
||||||
# inherit the options from the original command
|
|
||||||
obj.option_list = cmdclass.option_list
|
|
||||||
obj.option_list += (
|
|
||||||
make_option("-d", "--domain", dest="domain"),
|
|
||||||
)
|
|
||||||
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,
|
|
||||||
getattr(cmdclass, 'help', 'none'))
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def execute_command(self, tenant, command_name, *args, **options):
|
|
||||||
verbosity = int(options.get('verbosity'))
|
|
||||||
|
|
||||||
if verbosity >= 1:
|
|
||||||
print()
|
|
||||||
print(self.style.NOTICE("=== Switching to schema '") \
|
|
||||||
+ self.style.SQL_TABLE(tenant.schema_name)\
|
|
||||||
+ self.style.NOTICE("' then calling %s:" % command_name))
|
|
||||||
|
|
||||||
connection.set_tenant(tenant)
|
|
||||||
|
|
||||||
# call the original command with the args it knows
|
|
||||||
call_command(command_name, *args, **options)
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
"""
|
|
||||||
Iterates a command over all registered schemata.
|
|
||||||
"""
|
|
||||||
if options['domain']:
|
|
||||||
# only run on a particular schema
|
|
||||||
connection.set_schema_to_public()
|
|
||||||
self.execute_command(TenantMiddleware.get_tenant_by_hostname(options['domain']), self.COMMAND_NAME, *args, **options)
|
|
||||||
else:
|
|
||||||
for tenant in TenantMiddleware.get_tenants():
|
|
||||||
if not(options['skip_public'] and tenant.schema_name == get_public_schema_name()):
|
|
||||||
self.execute_command(tenant, self.COMMAND_NAME, *args, **options)
|
|
||||||
|
|
||||||
|
|
||||||
class InteractiveTenantOption(object):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(InteractiveTenantOption, self).__init__(*args, **kwargs)
|
|
||||||
self.option_list += (
|
|
||||||
make_option("-d", "--domain", dest="domain", help="specify tenant domain"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_tenant_from_options_or_interactive(self, **options):
|
|
||||||
all_tenants = list(TenantMiddleware.get_tenants())
|
|
||||||
|
|
||||||
if not all_tenants:
|
|
||||||
raise CommandError("""There are no tenants in the system.
|
|
||||||
To learn how create a tenant, see:
|
|
||||||
https://django-tenant-schemas.readthedocs.org/en/latest/use.html#creating-a-tenant""")
|
|
||||||
|
|
||||||
if options.get('domain'):
|
|
||||||
domain = options['domain']
|
|
||||||
else:
|
|
||||||
while True:
|
|
||||||
domain = input("Enter Tenant Domain ('?' to list): ")
|
|
||||||
if domain == '?':
|
|
||||||
print('\n'.join(["%s (schema %s)" % (t.domain_url, t.schema_name) for t in all_tenants]))
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
if domain not in [t.domain_url for t in all_tenants]:
|
|
||||||
raise CommandError("Invalid tenant, '%s'" % (domain,))
|
|
||||||
|
|
||||||
return TenantMiddleware.get_tenant_by_hostname(domain)
|
|
||||||
|
|
||||||
|
|
||||||
class TenantWrappedCommand(InteractiveTenantOption, BaseCommand):
|
|
||||||
"""
|
|
||||||
Generic command class useful for running any existing command
|
|
||||||
on a particular tenant. The actual command name is expected in the
|
|
||||||
class variable COMMAND_NAME of the subclass.
|
|
||||||
"""
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
obj = super(TenantWrappedCommand, cls).__new__(cls, *args, **kwargs)
|
|
||||||
obj.command_instance = obj.COMMAND()
|
|
||||||
obj.option_list = obj.command_instance.option_list
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
tenant = self.get_tenant_from_options_or_interactive(**options)
|
|
||||||
connection.set_tenant(tenant)
|
|
||||||
|
|
||||||
self.command_instance.execute(*args, **options)
|
|
||||||
|
|
||||||
|
|
||||||
class SyncCommon(BaseCommand):
|
|
||||||
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("-d", "--domain", dest="domain"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
self.sync_tenant = options.get('tenant')
|
|
||||||
self.sync_public = options.get('shared')
|
|
||||||
self.domain = options.get('domain')
|
|
||||||
self.installed_apps = settings.INSTALLED_APPS
|
|
||||||
self.args = args
|
|
||||||
self.options = options
|
|
||||||
|
|
||||||
if self.domain:
|
|
||||||
self.schema_name = TenantMiddleware.hostname2schema(domain)
|
|
||||||
else:
|
|
||||||
self.schema_name = options.get('schema_name')
|
|
||||||
|
|
||||||
if self.schema_name:
|
|
||||||
if self.sync_public:
|
|
||||||
raise CommandError("domain should only be used with the --tenant switch.")
|
|
||||||
elif self.schema_name == get_public_schema_name():
|
|
||||||
self.sync_public = True
|
|
||||||
else:
|
|
||||||
self.sync_tenant = True
|
|
||||||
elif not self.sync_public and not self.sync_tenant:
|
|
||||||
# no options set, sync both
|
|
||||||
self.sync_tenant = True
|
|
||||||
self.sync_public = True
|
|
||||||
|
|
||||||
if hasattr(settings, 'TENANT_APPS'):
|
|
||||||
self.tenant_apps = settings.TENANT_APPS
|
|
||||||
if hasattr(settings, 'SHARED_APPS'):
|
|
||||||
self.shared_apps = settings.SHARED_APPS
|
|
||||||
|
|
||||||
def _notice(self, output):
|
|
||||||
self.stdout.write(self.style.NOTICE(output))
|
|
|
@ -1,19 +0,0 @@
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
|
|
||||||
from django.db import connection
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "Create schemas for all declared tenants"
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
verbosity = int(options.get('verbosity'))
|
|
||||||
|
|
||||||
connection.set_schema_to_public()
|
|
||||||
all_tenants = TenantMiddleware.get_tenants()
|
|
||||||
for tenant in all_tenants:
|
|
||||||
if verbosity >= 1:
|
|
||||||
print
|
|
||||||
print self.style.NOTICE("=== Creating schema ") \
|
|
||||||
+ self.style.SQL_TABLE(tenant.schema_name)
|
|
||||||
|
|
||||||
tenant.create_schema(check_if_exists=True)
|
|
|
@ -1,36 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
from django.db import connection
|
|
||||||
from django.core.management.base import CommandError, BaseCommand
|
|
||||||
|
|
||||||
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "Create tenant(s) by hostname(s)"
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
verbosity = int(options.get('verbosity'))
|
|
||||||
if not args:
|
|
||||||
raise CommandError("you must give at least one tenant hostname")
|
|
||||||
|
|
||||||
for hostname in args:
|
|
||||||
try:
|
|
||||||
tenant_base = TenantMiddleware.base()
|
|
||||||
except AttributeError:
|
|
||||||
raise CommandError("you must configure TENANT_BASE in your settings")
|
|
||||||
if not tenant_base:
|
|
||||||
raise CommandError("you must set a value to TENANT_BASE in your settings")
|
|
||||||
tenant_dir = os.path.join(tenant_base, hostname)
|
|
||||||
if not os.path.exists(tenant_dir):
|
|
||||||
os.mkdir(tenant_dir, 0755)
|
|
||||||
for folder in ('media', 'static', 'templates'):
|
|
||||||
path = os.path.join(tenant_dir, folder)
|
|
||||||
if not os.path.exists(path):
|
|
||||||
os.mkdir(path, 0755)
|
|
||||||
connection.set_schema_to_public()
|
|
||||||
tenant = TenantMiddleware.get_tenant_by_hostname(hostname)
|
|
||||||
if verbosity >= 1:
|
|
||||||
print
|
|
||||||
print self.style.NOTICE("=== Creating schema ") \
|
|
||||||
+ self.style.SQL_TABLE(tenant.schema_name)
|
|
||||||
tenant.create_schema(check_if_exists=True)
|
|
|
@ -1,11 +0,0 @@
|
||||||
# this file derive from django-tenant-schemas
|
|
||||||
# Author: Bernardo Pires Carneiro
|
|
||||||
# Email: carneiro.be@gmail.com
|
|
||||||
# License: MIT license
|
|
||||||
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
|
|
||||||
from entrouvert.djommon.multitenant.management.commands import TenantWrappedCommand
|
|
||||||
from django.contrib.auth.management.commands import createsuperuser
|
|
||||||
|
|
||||||
|
|
||||||
class Command(TenantWrappedCommand):
|
|
||||||
COMMAND = createsuperuser.Command
|
|
|
@ -1,30 +0,0 @@
|
||||||
import urllib2
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from django.core.management import call_command
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
|
||||||
|
|
||||||
from tenant_schemas.utils import tenant_context
|
|
||||||
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'Deploy a tenant from hobo'
|
|
||||||
|
|
||||||
def handle(self, base_url, **options):
|
|
||||||
environment = json.load(sys.stdin)
|
|
||||||
for service in environment['services']:
|
|
||||||
if service['base_url'] == base_url:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise CommandError('Service %s not found' % base_url)
|
|
||||||
hostname = urllib2.urlparse.urlsplit(base_url).netloc
|
|
||||||
|
|
||||||
call_command('create_tenant', hostname)
|
|
||||||
|
|
||||||
tenant = TenantMiddleware.get_tenant_by_hostname(hostname)
|
|
||||||
with tenant_context(tenant):
|
|
||||||
self.deploy_tenant(environment, service, options)
|
|
||||||
|
|
||||||
def deploy_tenant(self, environment, service, options):
|
|
||||||
pass
|
|
|
@ -1,91 +0,0 @@
|
||||||
# this file derive from django-tenant-schemas
|
|
||||||
# Author: Bernardo Pires Carneiro
|
|
||||||
# Email: carneiro.be@gmail.com
|
|
||||||
# License: MIT license
|
|
||||||
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
|
|
||||||
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 entrouvert.djommon.multitenant.middleware import TenantMiddleware
|
|
||||||
from entrouvert.djommon.multitenant.management.commands import SyncCommon
|
|
||||||
|
|
||||||
|
|
||||||
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.domain)
|
|
||||||
|
|
||||||
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'
|
|
||||||
|
|
||||||
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=False)
|
|
||||||
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 = TenantMiddleware.get_tenant_by_hostname(schema_name)
|
|
||||||
self._migrate_schema(tenant)
|
|
||||||
else:
|
|
||||||
all_tenants = TenantMiddleware.get_tenants()
|
|
||||||
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,14 +0,0 @@
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
requires_model_validation = True
|
|
||||||
can_import_settings = True
|
|
||||||
option_list = BaseCommand.option_list
|
|
||||||
|
|
||||||
def handle(self, **options):
|
|
||||||
all_tenants = TenantMiddleware.get_tenants()
|
|
||||||
|
|
||||||
for tenant in all_tenants:
|
|
||||||
print("{0} {1}".format(tenant.schema_name, tenant.domain_url))
|
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
# this file derive from django-tenant-schemas
|
|
||||||
# Author: Bernardo Pires Carneiro
|
|
||||||
# Email: carneiro.be@gmail.com
|
|
||||||
# License: MIT license
|
|
||||||
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
|
|
||||||
import django
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.management.base import CommandError, BaseCommand
|
|
||||||
|
|
||||||
if django.VERSION < (1, 7, 0):
|
|
||||||
try:
|
|
||||||
from south.management.commands.migrate import Command as MigrateCommand
|
|
||||||
except ImportError:
|
|
||||||
MigrateCommand = BaseCommand
|
|
||||||
else:
|
|
||||||
MigrateCommand = BaseCommand
|
|
||||||
|
|
||||||
class Command(MigrateCommand):
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
database = options.get('database', 'default')
|
|
||||||
if (settings.DATABASES[database]['ENGINE'] == 'tenant_schemas.postgresql_backend' or
|
|
||||||
MigrateCommand is BaseCommand):
|
|
||||||
raise CommandError("migrate has been disabled, for database '{}'. Use migrate_schemas "
|
|
||||||
"instead. Please read the documentation if you don't know why you "
|
|
||||||
"shouldn't call migrate directly!".format(database))
|
|
||||||
super(Command, self).handle(*args, **options)
|
|
|
@ -1,68 +0,0 @@
|
||||||
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 tenant_schemas.utils import get_public_schema_name
|
|
||||||
from entrouvert.djommon.multitenant.middleware import TenantMiddleware, TenantNotFound
|
|
||||||
from entrouvert.djommon.multitenant.management.commands import SyncCommon
|
|
||||||
|
|
||||||
|
|
||||||
class MigrateSchemasCommand(SyncCommon):
|
|
||||||
help = "Updates database schema. Manages both apps with migrations and those without."
|
|
||||||
|
|
||||||
def run_from_argv(self, argv):
|
|
||||||
"""
|
|
||||||
Changes the option_list to use the options from the wrapped command.
|
|
||||||
Adds schema parameter to specify which schema will be used when
|
|
||||||
executing the wrapped command.
|
|
||||||
"""
|
|
||||||
self.option_list += MigrateCommand.option_list
|
|
||||||
super(MigrateSchemasCommand, self).run_from_argv(argv)
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
super(MigrateSchemasCommand, self).handle(*args, **options)
|
|
||||||
self.PUBLIC_SCHEMA_NAME = get_public_schema_name()
|
|
||||||
|
|
||||||
if self.sync_public and not self.schema_name:
|
|
||||||
self.schema_name = self.PUBLIC_SCHEMA_NAME
|
|
||||||
|
|
||||||
if self.sync_public:
|
|
||||||
self.run_migrations(self.schema_name, settings.SHARED_APPS)
|
|
||||||
if self.sync_tenant:
|
|
||||||
if self.schema_name and self.schema_name != self.PUBLIC_SCHEMA_NAME:
|
|
||||||
self.run_migrations(self.schema_name, settings.TENANT_APPS)
|
|
||||||
else:
|
|
||||||
all_tenants = TenantMiddleware.get_tenants()
|
|
||||||
for tenant in all_tenants:
|
|
||||||
self.run_migrations(tenant.schema_name, settings.TENANT_APPS)
|
|
||||||
|
|
||||||
def run_migrations(self, schema_name, included_apps):
|
|
||||||
self._notice("=== Running migrate for schema %s" % schema_name)
|
|
||||||
connection.set_schema(schema_name)
|
|
||||||
command = MigrateCommand()
|
|
||||||
|
|
||||||
defaults = {}
|
|
||||||
for opt in MigrateCommand.option_list:
|
|
||||||
if opt.dest in self.options:
|
|
||||||
defaults[opt.dest] = self.options[opt.dest]
|
|
||||||
elif opt.default is NO_DEFAULT:
|
|
||||||
defaults[opt.dest] = None
|
|
||||||
else:
|
|
||||||
defaults[opt.dest] = opt.default
|
|
||||||
|
|
||||||
command.execute(*self.args, **defaults)
|
|
||||||
connection.set_schema_to_public()
|
|
||||||
|
|
||||||
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,105 +0,0 @@
|
||||||
# this file derive from django-tenant-schemas
|
|
||||||
# Author: Bernardo Pires Carneiro
|
|
||||||
# Email: carneiro.be@gmail.com
|
|
||||||
# License: MIT license
|
|
||||||
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
|
|
||||||
import django
|
|
||||||
|
|
||||||
if django.VERSION < (1, 7, 0):
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import connection
|
|
||||||
from south import migration
|
|
||||||
from south.migration.base import Migrations
|
|
||||||
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
|
|
||||||
from entrouvert.djommon.management.commands.safemigrate import Command as SafeMigrateCommand
|
|
||||||
from entrouvert.djommon.multitenant.management.commands.sync_schemas import Command as MTSyncCommand
|
|
||||||
from entrouvert.djommon.multitenant.management.commands.migrate_schemas import Command as MTMigrateCommand
|
|
||||||
from entrouvert.djommon.multitenant.management.commands import SyncCommon
|
|
||||||
|
|
||||||
|
|
||||||
class SafeMigrateCommand(SyncCommon):
|
|
||||||
help = "Safely migrate schemas with South"
|
|
||||||
option_list = MTMigrateCommand.option_list
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
super(Command, self).handle(*args, **options)
|
|
||||||
|
|
||||||
MTSyncCommand().execute(*args, **options)
|
|
||||||
connection.set_schema_to_public()
|
|
||||||
if self.sync_public:
|
|
||||||
self.fake_public_apps()
|
|
||||||
if self.sync_tenant:
|
|
||||||
self.fake_tenant_apps(self.domain)
|
|
||||||
connection.set_schema_to_public()
|
|
||||||
MTMigrateCommand().execute(*args, **options)
|
|
||||||
|
|
||||||
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'
|
|
||||||
|
|
||||||
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 _fake_schema(self, tenant):
|
|
||||||
connection.set_tenant(tenant, include_public=False)
|
|
||||||
SafeMigrateCommand().fake_if_needed()
|
|
||||||
|
|
||||||
def fake_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 fake_if_needed for schema: %s" % schema_name)
|
|
||||||
connection.set_schema_to_public()
|
|
||||||
tenant = TenantMiddleware.get_tenant_by_hostname(schema_name)
|
|
||||||
self._fake_schema(tenant)
|
|
||||||
else:
|
|
||||||
all_tenants = TenantMiddleware.get_tenants()
|
|
||||||
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 fake_if_needed for schema %s" % tenant.schema_name)
|
|
||||||
self._fake_schema(tenant)
|
|
||||||
|
|
||||||
self._restore_south_settings()
|
|
||||||
|
|
||||||
def fake_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 fake_if_needed for schema public")
|
|
||||||
SafeMigrateCommand().fake_if_needed()
|
|
||||||
|
|
||||||
self._clear_south_cache()
|
|
||||||
self._restore_south_settings()
|
|
||||||
|
|
||||||
if django.VERSION < (1, 7, 0):
|
|
||||||
Command = SafeMigrateCommand
|
|
||||||
else:
|
|
||||||
raise RuntimeError('Django 1.7: please use migrate_schemas')
|
|
|
@ -1,87 +0,0 @@
|
||||||
# this file derive from django-tenant-schemas
|
|
||||||
# Author: Bernardo Pires Carneiro
|
|
||||||
# Email: carneiro.be@gmail.com
|
|
||||||
# License: MIT license
|
|
||||||
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
|
|
||||||
import django
|
|
||||||
from django.core.management.base import CommandError
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
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 entrouvert.djommon.multitenant.middleware import TenantMiddleware
|
|
||||||
from entrouvert.djommon.multitenant.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 CommandError('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
|
|
||||||
|
|
||||||
# save original settings
|
|
||||||
for model in get_models(include_auto_created=True):
|
|
||||||
setattr(model._meta, 'was_managed', model._meta.managed)
|
|
||||||
|
|
||||||
ContentType.objects.clear_cache()
|
|
||||||
|
|
||||||
if self.sync_public:
|
|
||||||
self.sync_public_apps()
|
|
||||||
if self.sync_tenant:
|
|
||||||
self.sync_tenant_apps(self.domain)
|
|
||||||
|
|
||||||
# restore settings
|
|
||||||
for model in get_models(include_auto_created=True):
|
|
||||||
model._meta.managed = model._meta.was_managed
|
|
||||||
|
|
||||||
def _set_managed_apps(self, included_apps):
|
|
||||||
""" sets which apps are managed by syncdb """
|
|
||||||
for model in get_models(include_auto_created=True):
|
|
||||||
model._meta.managed = False
|
|
||||||
|
|
||||||
verbosity = int(self.options.get('verbosity'))
|
|
||||||
for app_model in get_apps():
|
|
||||||
app_name = app_model.__name__.replace('.models', '')
|
|
||||||
if app_name in included_apps:
|
|
||||||
for model in get_models(app_model, include_auto_created=True):
|
|
||||||
model._meta.managed = model._meta.was_managed
|
|
||||||
if model._meta.managed and verbosity >= 3:
|
|
||||||
self._notice("=== Include Model: %s: %s" % (app_name, model.__name__))
|
|
||||||
|
|
||||||
def _sync_tenant(self, tenant):
|
|
||||||
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):
|
|
||||||
apps = self.tenant_apps or self.installed_apps
|
|
||||||
self._set_managed_apps(apps)
|
|
||||||
if schema_name:
|
|
||||||
tenant = TenantMiddleware.get_tenant_by_hostname(schema_name)
|
|
||||||
self._sync_tenant(tenant)
|
|
||||||
else:
|
|
||||||
all_tenants = TenantMiddleware.get_tenants()
|
|
||||||
if not all_tenants:
|
|
||||||
self._notice("No tenants found!")
|
|
||||||
|
|
||||||
for tenant in all_tenants:
|
|
||||||
self._sync_tenant(tenant)
|
|
||||||
|
|
||||||
def sync_public_apps(self):
|
|
||||||
self._notice("=== Running syncdb for schema public")
|
|
||||||
apps = self.shared_apps or self.installed_apps
|
|
||||||
self._set_managed_apps(apps)
|
|
||||||
SyncdbCommand().execute(**self.options)
|
|
|
@ -1,26 +0,0 @@
|
||||||
# this file derive from django-tenant-schemas
|
|
||||||
# Author: Bernardo Pires Carneiro
|
|
||||||
# Email: carneiro.be@gmail.com
|
|
||||||
# License: MIT license
|
|
||||||
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
|
|
||||||
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)
|
|
|
@ -1,40 +0,0 @@
|
||||||
# this file derive from django-tenant-schemas
|
|
||||||
# Author: Bernardo Pires Carneiro
|
|
||||||
# Email: carneiro.be@gmail.com
|
|
||||||
# License: MIT license
|
|
||||||
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
|
|
||||||
from optparse import make_option
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
|
||||||
from django.core.management import call_command, get_commands, load_command_class
|
|
||||||
from django.db import connection
|
|
||||||
from entrouvert.djommon.multitenant.management.commands import InteractiveTenantOption
|
|
||||||
|
|
||||||
|
|
||||||
class Command(InteractiveTenantOption, BaseCommand):
|
|
||||||
help = "Wrapper around django commands for use with an individual tenant"
|
|
||||||
|
|
||||||
def run_from_argv(self, argv):
|
|
||||||
"""
|
|
||||||
Changes the option_list to use the options from the wrapped command.
|
|
||||||
Adds schema parameter to specifiy which schema will be used when
|
|
||||||
executing the wrapped command.
|
|
||||||
"""
|
|
||||||
# load the command object.
|
|
||||||
try:
|
|
||||||
app_name = get_commands()[argv[2]]
|
|
||||||
except KeyError:
|
|
||||||
raise CommandError("Unknown command: %r" % argv[2])
|
|
||||||
|
|
||||||
if isinstance(app_name, BaseCommand):
|
|
||||||
# if the command is already loaded, use it directly.
|
|
||||||
klass = app_name
|
|
||||||
else:
|
|
||||||
klass = load_command_class(app_name, argv[2])
|
|
||||||
|
|
||||||
super(Command, self).run_from_argv(argv)
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
tenant = self.get_tenant_from_options_or_interactive(**options)
|
|
||||||
connection.set_tenant(tenant)
|
|
||||||
|
|
||||||
call_command(*args, **options)
|
|
|
@ -1,180 +0,0 @@
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import glob
|
|
||||||
|
|
||||||
from django.conf import settings, UserSettingsHolder
|
|
||||||
from django.db import connection
|
|
||||||
from django.http import Http404
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from tenant_schemas.utils import get_tenant_model, remove_www_and_dev, get_public_schema_name
|
|
||||||
|
|
||||||
SENTINEL = object()
|
|
||||||
|
|
||||||
class TenantNotFound(RuntimeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class TenantMiddleware(object):
|
|
||||||
"""
|
|
||||||
This middleware should be placed at the very top of the middleware stack.
|
|
||||||
Selects the proper database schema using the request host. Can fail in
|
|
||||||
various ways which is better than corrupting or revealing data...
|
|
||||||
"""
|
|
||||||
@classmethod
|
|
||||||
def base(cls):
|
|
||||||
return settings.TENANT_BASE
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def hostname2schema(cls, hostname):
|
|
||||||
'''Convert hostname to PostgreSQL schema name'''
|
|
||||||
if hostname in getattr(settings, 'TENANT_MAPPING', {}):
|
|
||||||
return settings.TENANT_MAPPING[hostname]
|
|
||||||
return hostname.replace('.', '_').replace('-', '_')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_tenant_by_hostname(cls, hostname):
|
|
||||||
'''Retrieve a tenant object for this hostname'''
|
|
||||||
if not os.path.exists(os.path.join(cls.base(), hostname)):
|
|
||||||
raise TenantNotFound
|
|
||||||
schema = cls.hostname2schema(hostname)
|
|
||||||
return get_tenant_model()(schema_name=schema, domain_url=hostname)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_tenants(cls):
|
|
||||||
self = cls()
|
|
||||||
for path in glob.glob(os.path.join(cls.base(), '*')):
|
|
||||||
hostname = os.path.basename(path)
|
|
||||||
yield get_tenant_model()(
|
|
||||||
schema_name=self.hostname2schema(hostname),
|
|
||||||
domain_url=hostname)
|
|
||||||
|
|
||||||
def process_request(self, request):
|
|
||||||
# connection needs first to be at the public schema, as this is where the
|
|
||||||
# tenant informations are saved
|
|
||||||
connection.set_schema_to_public()
|
|
||||||
hostname_without_port = remove_www_and_dev(request.get_host().split(':')[0])
|
|
||||||
|
|
||||||
try:
|
|
||||||
request.tenant = self.get_tenant_by_hostname(hostname_without_port)
|
|
||||||
except TenantNotFound:
|
|
||||||
raise Http404
|
|
||||||
connection.set_tenant(request.tenant)
|
|
||||||
|
|
||||||
# content type can no longer be cached as public and tenant schemas have different
|
|
||||||
# models. if someone wants to change this, the cache needs to be separated between
|
|
||||||
# public and shared schemas. if this cache isn't cleared, this can cause permission
|
|
||||||
# problems. for example, on public, a particular model has id 14, but on the tenants
|
|
||||||
# it has the id 15. if 14 is cached instead of 15, the permissions for the wrong
|
|
||||||
# model will be fetched.
|
|
||||||
ContentType.objects.clear_cache()
|
|
||||||
|
|
||||||
# do we have a public-specific token?
|
|
||||||
if hasattr(settings, 'PUBLIC_SCHEMA_URLCONF') and request.tenant.schema_name == get_public_schema_name():
|
|
||||||
request.urlconf = settings.PUBLIC_SCHEMA_URLCONF
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TenantSettingBaseMiddleware(object):
|
|
||||||
'''Base middleware classe for loading settings based on tenants
|
|
||||||
|
|
||||||
Child classes MUST override the load_tenant_settings() method.
|
|
||||||
'''
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.tenants_settings = {}
|
|
||||||
|
|
||||||
def get_tenant_settings(self, wrapped, tenant):
|
|
||||||
'''Get last loaded settings for tenant, try to update it by loading
|
|
||||||
settings again is last loading time is less recent thant settings data
|
|
||||||
store. Compare with last modification time is done in the
|
|
||||||
load_tenant_settings() method.
|
|
||||||
'''
|
|
||||||
tenant_settings, last_time = self.tenants_settings.get(tenant.schema_name, (None,None))
|
|
||||||
if tenant_settings is None:
|
|
||||||
tenant_settings = UserSettingsHolder(wrapped)
|
|
||||||
tenant_settings, last_time = self.load_tenant_settings(wrapped, tenant, tenant_settings, last_time)
|
|
||||||
self.tenants_settings[tenant.schema_name] = tenant_settings, last_time
|
|
||||||
return tenant_settings
|
|
||||||
|
|
||||||
def load_tenant_settings(self, wrapped, tenant, tenant_settings, last_time):
|
|
||||||
'''Load tenant settings into tenant_settings object, eventually skip if
|
|
||||||
last_time is more recent than last update time for settings and return
|
|
||||||
the new value for tenant_settings and last_time'''
|
|
||||||
raise NotImplemented
|
|
||||||
|
|
||||||
def process_request(self, request):
|
|
||||||
if not hasattr(request, '_old_settings_wrapped'):
|
|
||||||
request._old_settings_wrapped = []
|
|
||||||
request._old_settings_wrapped.append(settings._wrapped)
|
|
||||||
settings._wrapped = self.get_tenant_settings(settings._wrapped, request.tenant)
|
|
||||||
|
|
||||||
def process_response(self, request, response):
|
|
||||||
if hasattr(request, '_old_settings_wrapped') and request._old_settings_wrapped:
|
|
||||||
settings._wrapped = request._old_settings_wrapped.pop()
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class FileBasedTenantSettingBaseMiddleware(TenantSettingBaseMiddleware):
|
|
||||||
FILENAME = None
|
|
||||||
|
|
||||||
def load_tenant_settings(self, wrapped, tenant, tenant_settings, last_time):
|
|
||||||
path = os.path.join(settings.TENANT_BASE, tenant.domain_url, self.FILENAME)
|
|
||||||
try:
|
|
||||||
new_time = os.stat(path).st_mtime
|
|
||||||
except OSError:
|
|
||||||
# file was removed
|
|
||||||
if not last_time is None:
|
|
||||||
return UserSettingsHolder(wrapped), None
|
|
||||||
else:
|
|
||||||
if last_time is None or new_time >= last_time:
|
|
||||||
# file is new
|
|
||||||
tenant_settings = UserSettingsHolder(wrapped)
|
|
||||||
self.load_file(tenant_settings, path)
|
|
||||||
return tenant_settings, new_time
|
|
||||||
# nothing has changed
|
|
||||||
return tenant_settings, last_time
|
|
||||||
|
|
||||||
|
|
||||||
class JSONSettingsMiddleware(FileBasedTenantSettingBaseMiddleware):
|
|
||||||
'''Load settings from a JSON file whose path is given by:
|
|
||||||
|
|
||||||
os.path.join(settings.TENANT_BASE % schema_name, 'settings.json')
|
|
||||||
|
|
||||||
The JSON file must be a dictionnary whose key/value will override
|
|
||||||
current settings.
|
|
||||||
'''
|
|
||||||
FILENAME = 'settings.json'
|
|
||||||
|
|
||||||
def load_file(sef, tenant_settings, path):
|
|
||||||
with file(path) as f:
|
|
||||||
json_settings = json.load(f)
|
|
||||||
for key in json_settings:
|
|
||||||
setattr(tenant_settings, key, json_settings[key])
|
|
||||||
|
|
||||||
|
|
||||||
class DictAdapter(dict):
|
|
||||||
'''Give dict interface to plain objects'''
|
|
||||||
def __init__(self, wrapped):
|
|
||||||
self.wrapped = wrapped
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
setattr(self.wrapped, key, value)
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
try:
|
|
||||||
return getattr(self.wrapped, key)
|
|
||||||
except AttributeError:
|
|
||||||
raise KeyError
|
|
||||||
|
|
||||||
|
|
||||||
class PythonSettingsMiddleware(FileBasedTenantSettingBaseMiddleware):
|
|
||||||
'''Load settings from a file whose path is given by:
|
|
||||||
|
|
||||||
os.path.join(settings.TENANT_BASE % schema_name, 'settings.py')
|
|
||||||
|
|
||||||
The file is executed in the same context as the classic settings file
|
|
||||||
using execfile.
|
|
||||||
'''
|
|
||||||
FILENAME = 'settings.py'
|
|
||||||
|
|
||||||
def load_file(self, tenant_settings, path):
|
|
||||||
execfile(path, DictAdapter(tenant_settings))
|
|
|
@ -1,11 +0,0 @@
|
||||||
from tenant_schemas.models import TenantMixin
|
|
||||||
|
|
||||||
class Tenant(TenantMixin):
|
|
||||||
# default true, schema will be automatically created and synced when it is saved
|
|
||||||
auto_create_schema = False
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return u'%s' % self.schema_name
|
|
|
@ -1,24 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import SuspiciousOperation
|
|
||||||
from django.utils._os import safe_join
|
|
||||||
|
|
||||||
from django.db import connection
|
|
||||||
|
|
||||||
from django.core.files.storage import FileSystemStorage
|
|
||||||
|
|
||||||
__all__ = ('TenantFileSystemStorage', )
|
|
||||||
|
|
||||||
class TenantFileSystemStorage(FileSystemStorage):
|
|
||||||
'''Lookup files first in $TENANT_BASE/<tenant.schema>/media/ then in default location'''
|
|
||||||
def path(self, name):
|
|
||||||
if connection.tenant:
|
|
||||||
location = safe_join(settings.TENANT_BASE, connection.tenant.domain_url, 'media')
|
|
||||||
else:
|
|
||||||
location = self.location
|
|
||||||
try:
|
|
||||||
path = safe_join(location, name)
|
|
||||||
except ValueError:
|
|
||||||
raise SuspiciousOperation("Attempted access to '%s' denied." % name)
|
|
||||||
return os.path.normpath(path)
|
|
|
@ -1,115 +0,0 @@
|
||||||
"""
|
|
||||||
Wrapper class that takes a list of template loaders as an argument and attempts
|
|
||||||
to load templates from them in order, caching the result.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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.utils.encoding import force_bytes
|
|
||||||
from django.utils._os import safe_join
|
|
||||||
from django.db import connection
|
|
||||||
|
|
||||||
class CachedLoader(BaseLoader):
|
|
||||||
is_usable = True
|
|
||||||
|
|
||||||
def __init__(self, loaders):
|
|
||||||
self.template_cache = {}
|
|
||||||
self._loaders = loaders
|
|
||||||
self._cached_loaders = []
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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.schema_name), 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:
|
|
||||||
template, origin = self.find_template(template_name, template_dirs)
|
|
||||||
if not hasattr(template, 'render'):
|
|
||||||
try:
|
|
||||||
template = get_template_from_string(template, origin, template_name)
|
|
||||||
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
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"Empty the template cache."
|
|
||||||
self.template_cache.clear()
|
|
||||||
|
|
||||||
class FilesystemLoader(BaseLoader):
|
|
||||||
is_usable = True
|
|
||||||
|
|
||||||
def get_template_sources(self, 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
|
|
||||||
template dirs are excluded from the result set, for security reasons.
|
|
||||||
"""
|
|
||||||
if not connection.tenant:
|
|
||||||
return
|
|
||||||
if not template_dirs:
|
|
||||||
try:
|
|
||||||
template_dirs = settings.TENANT_TEMPLATE_DIRS
|
|
||||||
except AttributeError:
|
|
||||||
raise ImproperlyConfigured('To use %s.%s you must define the TENANT_TEMPLATE_DIRS' % (__name__, FilesystemLoader.__name__))
|
|
||||||
for template_dir in template_dirs:
|
|
||||||
try:
|
|
||||||
yield safe_join(template_dir, connection.tenant.domain_url, 'templates', template_name)
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
# The template dir name was a bytestring that wasn't valid UTF-8.
|
|
||||||
raise
|
|
||||||
except ValueError:
|
|
||||||
# The joined path was located outside of this particular
|
|
||||||
# template_dir (it might be inside another one, so this isn't
|
|
||||||
# fatal).
|
|
||||||
pass
|
|
||||||
|
|
||||||
def load_template_source(self, template_name, template_dirs=None):
|
|
||||||
tried = []
|
|
||||||
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)
|
|
||||||
except IOError:
|
|
||||||
tried.append(filepath)
|
|
||||||
if tried:
|
|
||||||
error_msg = "Tried %s" % tried
|
|
||||||
else:
|
|
||||||
error_msg = "Your TENANT_TEMPLATE_DIRS setting is empty. Change it to point to at least one template directory."
|
|
||||||
raise TemplateDoesNotExist(error_msg)
|
|
||||||
load_template_source.is_usable = True
|
|
|
@ -1,128 +0,0 @@
|
||||||
"""
|
|
||||||
Test multitenant framework
|
|
||||||
"""
|
|
||||||
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import StringIO
|
|
||||||
|
|
||||||
from django.conf.urls import patterns
|
|
||||||
from django.test import TestCase, Client
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.template.response import TemplateResponse
|
|
||||||
|
|
||||||
try:
|
|
||||||
from django.test import override_settings
|
|
||||||
except ImportError: # django < 1.7
|
|
||||||
from django.test.utils import override_settings
|
|
||||||
|
|
||||||
|
|
||||||
def json_key(request, *args, **kwargs):
|
|
||||||
from django.conf import settings
|
|
||||||
return HttpResponse(settings.JSON_KEY + ' json')
|
|
||||||
|
|
||||||
def python_key(request, *args, **kwargs):
|
|
||||||
from django.conf import settings
|
|
||||||
return HttpResponse(settings.PYTHON_KEY + ' python')
|
|
||||||
|
|
||||||
def template(request, *args, **kwargs):
|
|
||||||
return TemplateResponse(request, 'tenant.html')
|
|
||||||
|
|
||||||
def upload(request):
|
|
||||||
from django.core.files.storage import default_storage
|
|
||||||
default_storage.save('upload', request.FILES['upload'])
|
|
||||||
return HttpResponse('')
|
|
||||||
|
|
||||||
def download(request):
|
|
||||||
from django.core.files.storage import default_storage
|
|
||||||
return HttpResponse(default_storage.open('upload').read())
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
|
||||||
('^json_key/$', json_key),
|
|
||||||
('^python_key/$', python_key),
|
|
||||||
('^template/$', template),
|
|
||||||
('^upload/$', upload),
|
|
||||||
('^download/$', download),
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(
|
|
||||||
ROOT_URLCONF=__name__,
|
|
||||||
MIDDLEWARE_CLASSES=(
|
|
||||||
'entrouvert.djommon.multitenant.middleware.TenantMiddleware',
|
|
||||||
'entrouvert.djommon.multitenant.middleware.JSONSettingsMiddleware',
|
|
||||||
'entrouvert.djommon.multitenant.middleware.PythonSettingsMiddleware',
|
|
||||||
),
|
|
||||||
TEMPLATE_LOADERS = (
|
|
||||||
'entrouvert.djommon.multitenant.template_loader.FilesystemLoader',
|
|
||||||
),
|
|
||||||
DEFAULT_FILE_STORAGE = 'entrouvert.djommon.multitenant.storage.TenantFileSystemStorage',
|
|
||||||
)
|
|
||||||
class SimpleTest(TestCase):
|
|
||||||
TENANTS = ['tenant1', 'tenant2']
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.tenant_base = tempfile.mkdtemp()
|
|
||||||
for tenant in self.TENANTS:
|
|
||||||
tenant_dir = os.path.join(self.tenant_base, tenant)
|
|
||||||
os.mkdir(tenant_dir)
|
|
||||||
settings_py = os.path.join(tenant_dir, 'settings.json')
|
|
||||||
with file(settings_py, 'w') as f:
|
|
||||||
json.dump({'JSON_KEY': tenant}, f)
|
|
||||||
settings_json = os.path.join(tenant_dir, 'settings.py')
|
|
||||||
with file(settings_json, 'w') as f:
|
|
||||||
print >>f, 'PYTHON_KEY = %r' % tenant
|
|
||||||
templates_dir = os.path.join(tenant_dir, 'templates')
|
|
||||||
os.mkdir(templates_dir)
|
|
||||||
tenant_html = os.path.join(templates_dir, 'tenant.html')
|
|
||||||
with file(tenant_html, 'w') as f:
|
|
||||||
print >>f, tenant + ' template',
|
|
||||||
media_dir = os.path.join(tenant_dir, 'media')
|
|
||||||
os.mkdir(media_dir)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
shutil.rmtree(self.tenant_base, ignore_errors=True)
|
|
||||||
|
|
||||||
def tenant_settings(self):
|
|
||||||
return self.settings(
|
|
||||||
TENANT_BASE=self.tenant_base,
|
|
||||||
TENANT_TEMPLATE_DIRS=(self.tenant_base,)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_tenants(self):
|
|
||||||
with self.tenant_settings():
|
|
||||||
for tenant in self.TENANTS:
|
|
||||||
c = Client(HTTP_HOST=tenant)
|
|
||||||
response = c.get('/json_key/')
|
|
||||||
self.assertEqual(response.content, tenant + ' json')
|
|
||||||
response = c.get('/python_key/')
|
|
||||||
self.assertEqual(response.content, tenant + ' python')
|
|
||||||
response = c.get('/template/')
|
|
||||||
self.assertEqual(response.content, tenant + ' template')
|
|
||||||
|
|
||||||
def test_list_tenants(self):
|
|
||||||
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
|
|
||||||
from tenant_schemas.utils import get_tenant_model
|
|
||||||
|
|
||||||
with self.tenant_settings():
|
|
||||||
l1 = set(map(str, TenantMiddleware.get_tenants()))
|
|
||||||
l2 = set(str(get_tenant_model()(schema_name=tenant,
|
|
||||||
domain_url=tenant)) for tenant in self.TENANTS)
|
|
||||||
self.assertEquals(l1, l2)
|
|
||||||
|
|
||||||
def test_storage(self):
|
|
||||||
from django.core.files.base import ContentFile
|
|
||||||
with self.tenant_settings():
|
|
||||||
for tenant in self.TENANTS:
|
|
||||||
c = Client(HTTP_HOST=tenant)
|
|
||||||
uploaded_file_path = os.path.join(self.tenant_base, tenant, 'media', 'upload')
|
|
||||||
self.assertFalse(os.path.exists(uploaded_file_path), uploaded_file_path)
|
|
||||||
response = c.post('/upload/', {'upload': ContentFile(tenant + ' upload', name='upload.txt')})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.content, '')
|
|
||||||
self.assertTrue(os.path.exists(uploaded_file_path))
|
|
||||||
self.assertEqual(file(uploaded_file_path).read(), tenant + ' upload')
|
|
||||||
response = c.get('/download/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.content, tenant + ' upload')
|
|
|
@ -1 +0,0 @@
|
||||||
# Create your views here.
|
|
Reference in New Issue