Import django-tenant-schemas commands to adapt them to our way of managing tenants
refs #5106
This commit is contained in:
parent
e133c6ceb4
commit
b9bdb8d833
|
@ -0,0 +1,166 @@
|
||||||
|
# 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'):
|
||||||
|
tenant_schema = options['domain']
|
||||||
|
else:
|
||||||
|
while True:
|
||||||
|
tenant_schema = input("Enter Tenant Domain ('?' to list schemas): ")
|
||||||
|
if tenant_schema == '?':
|
||||||
|
print('\n'.join(["%s - %s" % (t.schema_name, t.domain_url,) for t in all_tenants]))
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if tenant_schema not in [t.schema_name for t in all_tenants]:
|
||||||
|
raise CommandError("Invalid tenant schema, '%s'" % (tenant_schema,))
|
||||||
|
|
||||||
|
return TenantMiddleware.get_tenant_by_hostname(tenant_schema)
|
||||||
|
|
||||||
|
|
||||||
|
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.schema_name:
|
||||||
|
if self.sync_public:
|
||||||
|
raise CommandError("schema 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))
|
|
@ -0,0 +1,11 @@
|
||||||
|
# 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
|
|
@ -0,0 +1,23 @@
|
||||||
|
# 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.core.management.base import CommandError, BaseCommand
|
||||||
|
try:
|
||||||
|
from south.management.commands.migrate import Command as MigrateCommand
|
||||||
|
except ImportError:
|
||||||
|
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)
|
|
@ -0,0 +1,91 @@
|
||||||
|
# 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.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'
|
||||||
|
|
||||||
|
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()
|
|
@ -0,0 +1,80 @@
|
||||||
|
# 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.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):
|
||||||
|
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.schema_name)
|
||||||
|
|
||||||
|
# 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):
|
||||||
|
apps = self.shared_apps or self.installed_apps
|
||||||
|
self._set_managed_apps(apps)
|
||||||
|
SyncdbCommand().execute(**self.options)
|
||||||
|
self._notice("=== Running syncdb for schema public")
|
|
@ -0,0 +1,26 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
try:
|
||||||
|
from south.management.commands import syncdb
|
||||||
|
except ImportError:
|
||||||
|
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 '{}'. "
|
||||||
|
"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)
|
|
@ -0,0 +1,44 @@
|
||||||
|
# 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])
|
||||||
|
|
||||||
|
self.option_list = klass.option_list + (
|
||||||
|
make_option("-d", "--domain", dest="domain", help="specify tenant schema"),
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
Loading…
Reference in New Issue