Revision ccec1ff4
Added by Benjamin Dauvergne over 10 years ago
entrouvert/djommon/multitenant/management/commands/__init__.py | ||
---|---|---|
1 |
# this file derive from django-tenant-schemas |
|
2 |
# Author: Bernardo Pires Carneiro |
|
3 |
# Email: carneiro.be@gmail.com |
|
4 |
# License: MIT license |
|
5 |
# Home-page: http://github.com/bcarneiro/django-tenant-schemas |
|
6 |
from optparse import make_option |
|
7 |
from django.conf import settings |
|
8 |
from django.core.management import call_command, get_commands, load_command_class |
|
9 |
from django.core.management.base import BaseCommand, CommandError |
|
10 |
from django.db import connection |
|
11 |
try: |
|
12 |
from django.utils.six.moves import input |
|
13 |
except ImportError: |
|
14 |
input = raw_input |
|
15 |
from tenant_schemas.utils import get_public_schema_name |
|
16 |
from entrouvert.djommon.multitenant.middleware import TenantMiddleware |
|
17 |
|
|
18 |
|
|
19 |
class BaseTenantCommand(BaseCommand): |
|
20 |
""" |
|
21 |
Generic command class useful for iterating any existing command |
|
22 |
over all schemata. The actual command name is expected in the |
|
23 |
class variable COMMAND_NAME of the subclass. |
|
24 |
""" |
|
25 |
def __new__(cls, *args, **kwargs): |
|
26 |
""" |
|
27 |
Sets option_list and help dynamically. |
|
28 |
""" |
|
29 |
obj = super(BaseTenantCommand, cls).__new__(cls, *args, **kwargs) |
|
30 |
|
|
31 |
app_name = get_commands()[obj.COMMAND_NAME] |
|
32 |
if isinstance(app_name, BaseCommand): |
|
33 |
# If the command is already loaded, use it directly. |
|
34 |
cmdclass = app_name |
|
35 |
else: |
|
36 |
cmdclass = load_command_class(app_name, obj.COMMAND_NAME) |
|
37 |
|
|
38 |
# inherit the options from the original command |
|
39 |
obj.option_list = cmdclass.option_list |
|
40 |
obj.option_list += ( |
|
41 |
make_option("-d", "--domain", dest="domain"), |
|
42 |
) |
|
43 |
obj.option_list += ( |
|
44 |
make_option("-p", "--skip-public", dest="skip_public", action="store_true", default=False), |
|
45 |
) |
|
46 |
|
|
47 |
# prepend the command's original help with the info about schemata iteration |
|
48 |
obj.help = "Calls %s for all registered schemata. You can use regular %s options. "\ |
|
49 |
"Original help for %s: %s" % (obj.COMMAND_NAME, obj.COMMAND_NAME, obj.COMMAND_NAME, |
|
50 |
getattr(cmdclass, 'help', 'none')) |
|
51 |
return obj |
|
52 |
|
|
53 |
def execute_command(self, tenant, command_name, *args, **options): |
|
54 |
verbosity = int(options.get('verbosity')) |
|
55 |
|
|
56 |
if verbosity >= 1: |
|
57 |
print() |
|
58 |
print(self.style.NOTICE("=== Switching to schema '") \ |
|
59 |
+ self.style.SQL_TABLE(tenant.schema_name)\ |
|
60 |
+ self.style.NOTICE("' then calling %s:" % command_name)) |
|
61 |
|
|
62 |
connection.set_tenant(tenant) |
|
63 |
|
|
64 |
# call the original command with the args it knows |
|
65 |
call_command(command_name, *args, **options) |
|
66 |
|
|
67 |
def handle(self, *args, **options): |
|
68 |
""" |
|
69 |
Iterates a command over all registered schemata. |
|
70 |
""" |
|
71 |
if options['domain']: |
|
72 |
# only run on a particular schema |
|
73 |
connection.set_schema_to_public() |
|
74 |
self.execute_command(TenantMiddleware.get_tenant_by_hostname(options['domain']), self.COMMAND_NAME, *args, **options) |
|
75 |
else: |
|
76 |
for tenant in TenantMiddleware.get_tenants(): |
|
77 |
if not(options['skip_public'] and tenant.schema_name == get_public_schema_name()): |
|
78 |
self.execute_command(tenant, self.COMMAND_NAME, *args, **options) |
|
79 |
|
|
80 |
|
|
81 |
class InteractiveTenantOption(object): |
|
82 |
def __init__(self, *args, **kwargs): |
|
83 |
super(InteractiveTenantOption, self).__init__(*args, **kwargs) |
|
84 |
self.option_list += ( |
|
85 |
make_option("-d", "--domain", dest="domain", help="specify tenant domain"), |
|
86 |
) |
|
87 |
|
|
88 |
def get_tenant_from_options_or_interactive(self, **options): |
|
89 |
all_tenants = list(TenantMiddleware.get_tenants()) |
|
90 |
|
|
91 |
if not all_tenants: |
|
92 |
raise CommandError("""There are no tenants in the system. |
|
93 |
To learn how create a tenant, see: |
|
94 |
https://django-tenant-schemas.readthedocs.org/en/latest/use.html#creating-a-tenant""") |
|
95 |
|
|
96 |
if options.get('domain'): |
|
97 |
tenant_schema = options['domain'] |
|
98 |
else: |
|
99 |
while True: |
|
100 |
tenant_schema = input("Enter Tenant Domain ('?' to list schemas): ") |
|
101 |
if tenant_schema == '?': |
|
102 |
print('\n'.join(["%s - %s" % (t.schema_name, t.domain_url,) for t in all_tenants])) |
|
103 |
else: |
|
104 |
break |
|
105 |
|
|
106 |
if tenant_schema not in [t.schema_name for t in all_tenants]: |
|
107 |
raise CommandError("Invalid tenant schema, '%s'" % (tenant_schema,)) |
|
108 |
|
|
109 |
return TenantMiddleware.get_tenant_by_hostname(tenant_schema) |
|
110 |
|
|
111 |
|
|
112 |
class TenantWrappedCommand(InteractiveTenantOption, BaseCommand): |
|
113 |
""" |
|
114 |
Generic command class useful for running any existing command |
|
115 |
on a particular tenant. The actual command name is expected in the |
|
116 |
class variable COMMAND_NAME of the subclass. |
|
117 |
""" |
|
118 |
def __new__(cls, *args, **kwargs): |
|
119 |
obj = super(TenantWrappedCommand, cls).__new__(cls, *args, **kwargs) |
|
120 |
obj.command_instance = obj.COMMAND() |
|
121 |
obj.option_list = obj.command_instance.option_list |
|
122 |
return obj |
|
123 |
|
|
124 |
def handle(self, *args, **options): |
|
125 |
tenant = self.get_tenant_from_options_or_interactive(**options) |
|
126 |
connection.set_tenant(tenant) |
|
127 |
|
|
128 |
self.command_instance.execute(*args, **options) |
|
129 |
|
|
130 |
|
|
131 |
class SyncCommon(BaseCommand): |
|
132 |
option_list = ( |
|
133 |
make_option('--tenant', action='store_true', dest='tenant', default=False, |
|
134 |
help='Tells Django to populate only tenant applications.'), |
|
135 |
make_option('--shared', action='store_true', dest='shared', default=False, |
|
136 |
help='Tells Django to populate only shared applications.'), |
|
137 |
make_option("-d", "--domain", dest="domain"), |
|
138 |
) |
|
139 |
|
|
140 |
def handle(self, *args, **options): |
|
141 |
self.sync_tenant = options.get('tenant') |
|
142 |
self.sync_public = options.get('shared') |
|
143 |
self.domain = options.get('domain') |
|
144 |
self.installed_apps = settings.INSTALLED_APPS |
|
145 |
self.args = args |
|
146 |
self.options = options |
|
147 |
|
|
148 |
if self.schema_name: |
|
149 |
if self.sync_public: |
|
150 |
raise CommandError("schema should only be used with the --tenant switch.") |
|
151 |
elif self.schema_name == get_public_schema_name(): |
|
152 |
self.sync_public = True |
|
153 |
else: |
|
154 |
self.sync_tenant = True |
|
155 |
elif not self.sync_public and not self.sync_tenant: |
|
156 |
# no options set, sync both |
|
157 |
self.sync_tenant = True |
|
158 |
self.sync_public = True |
|
159 |
|
|
160 |
if hasattr(settings, 'TENANT_APPS'): |
|
161 |
self.tenant_apps = settings.TENANT_APPS |
|
162 |
if hasattr(settings, 'SHARED_APPS'): |
|
163 |
self.shared_apps = settings.SHARED_APPS |
|
164 |
|
|
165 |
def _notice(self, output): |
|
166 |
self.stdout.write(self.style.NOTICE(output)) |
entrouvert/djommon/multitenant/management/commands/createsuperuser.py | ||
---|---|---|
1 |
# this file derive from django-tenant-schemas |
|
2 |
# Author: Bernardo Pires Carneiro |
|
3 |
# Email: carneiro.be@gmail.com |
|
4 |
# License: MIT license |
|
5 |
# Home-page: http://github.com/bcarneiro/django-tenant-schemas |
|
6 |
from entrouvert.djommon.multitenant.management.commands import TenantWrappedCommand |
|
7 |
from django.contrib.auth.management.commands import createsuperuser |
|
8 |
|
|
9 |
|
|
10 |
class Command(TenantWrappedCommand): |
|
11 |
COMMAND = createsuperuser.Command |
entrouvert/djommon/multitenant/management/commands/migrate.py | ||
---|---|---|
1 |
# this file derive from django-tenant-schemas |
|
2 |
# Author: Bernardo Pires Carneiro |
|
3 |
# Email: carneiro.be@gmail.com |
|
4 |
# License: MIT license |
|
5 |
# Home-page: http://github.com/bcarneiro/django-tenant-schemas |
|
6 |
from django.conf import settings |
|
7 |
from django.core.management.base import CommandError, BaseCommand |
|
8 |
try: |
|
9 |
from south.management.commands.migrate import Command as MigrateCommand |
|
10 |
except ImportError: |
|
11 |
MigrateCommand = BaseCommand |
|
12 |
|
|
13 |
|
|
14 |
class Command(MigrateCommand): |
|
15 |
|
|
16 |
def handle(self, *args, **options): |
|
17 |
database = options.get('database', 'default') |
|
18 |
if (settings.DATABASES[database]['ENGINE'] == 'tenant_schemas.postgresql_backend' or |
|
19 |
MigrateCommand is BaseCommand): |
|
20 |
raise CommandError("migrate has been disabled, for database '{}'. Use migrate_schemas " |
|
21 |
"instead. Please read the documentation if you don't know why you " |
|
22 |
"shouldn't call migrate directly!".format(database)) |
|
23 |
super(Command, self).handle(*args, **options) |
entrouvert/djommon/multitenant/management/commands/migrate_schemas.py | ||
---|---|---|
1 |
# this file derive from django-tenant-schemas |
|
2 |
# Author: Bernardo Pires Carneiro |
|
3 |
# Email: carneiro.be@gmail.com |
|
4 |
# License: MIT license |
|
5 |
# Home-page: http://github.com/bcarneiro/django-tenant-schemas |
|
6 |
from django.conf import settings |
|
7 |
from django.db import connection |
|
8 |
from south import migration |
|
9 |
from south.migration.base import Migrations |
|
10 |
from south.management.commands.migrate import Command as MigrateCommand |
|
11 |
from entrouvert.djommon.multitenant.middleware import TenantMiddleware |
|
12 |
from entrouvert.djommon.multitenant.management.commands import SyncCommon |
|
13 |
|
|
14 |
|
|
15 |
class Command(SyncCommon): |
|
16 |
help = "Migrate schemas with South" |
|
17 |
option_list = MigrateCommand.option_list + SyncCommon.option_list |
|
18 |
|
|
19 |
def handle(self, *args, **options): |
|
20 |
super(Command, self).handle(*args, **options) |
|
21 |
|
|
22 |
if self.sync_public: |
|
23 |
self.migrate_public_apps() |
|
24 |
if self.sync_tenant: |
|
25 |
self.migrate_tenant_apps(self.schema_name) |
|
26 |
|
|
27 |
def _set_managed_apps(self, included_apps, excluded_apps): |
|
28 |
""" while sync_schemas works by setting which apps are managed, on south we set which apps should be ignored """ |
|
29 |
ignored_apps = [] |
|
30 |
if excluded_apps: |
|
31 |
for item in excluded_apps: |
|
32 |
if item not in included_apps: |
|
33 |
ignored_apps.append(item) |
|
34 |
|
|
35 |
for app in ignored_apps: |
|
36 |
app_label = app.split('.')[-1] |
|
37 |
settings.SOUTH_MIGRATION_MODULES[app_label] = 'ignore' |
|
38 |
|
|
39 |
def _save_south_settings(self): |
|
40 |
self._old_south_modules = None |
|
41 |
if hasattr(settings, "SOUTH_MIGRATION_MODULES") and settings.SOUTH_MIGRATION_MODULES is not None: |
|
42 |
self._old_south_modules = settings.SOUTH_MIGRATION_MODULES.copy() |
|
43 |
else: |
|
44 |
settings.SOUTH_MIGRATION_MODULES = dict() |
|
45 |
|
|
46 |
def _restore_south_settings(self): |
|
47 |
settings.SOUTH_MIGRATION_MODULES = self._old_south_modules |
|
48 |
|
|
49 |
def _clear_south_cache(self): |
|
50 |
for mig in list(migration.all_migrations()): |
|
51 |
delattr(mig._application, "migrations") |
|
52 |
Migrations._clear_cache() |
|
53 |
|
|
54 |
def _migrate_schema(self, tenant): |
|
55 |
connection.set_tenant(tenant, include_public=False) |
|
56 |
MigrateCommand().execute(*self.args, **self.options) |
|
57 |
|
|
58 |
def migrate_tenant_apps(self, schema_name=None): |
|
59 |
self._save_south_settings() |
|
60 |
|
|
61 |
apps = self.tenant_apps or self.installed_apps |
|
62 |
self._set_managed_apps(included_apps=apps, excluded_apps=self.shared_apps) |
|
63 |
|
|
64 |
if schema_name: |
|
65 |
self._notice("=== Running migrate for schema: %s" % schema_name) |
|
66 |
connection.set_schema_to_public() |
|
67 |
tenant = TenantMiddleware.get_tenant_by_hostname(schema_name) |
|
68 |
self._migrate_schema(tenant) |
|
69 |
else: |
|
70 |
all_tenants = TenantMiddleware.get_tenants() |
|
71 |
if not all_tenants: |
|
72 |
self._notice("No tenants found") |
|
73 |
|
|
74 |
for tenant in all_tenants: |
|
75 |
Migrations._dependencies_done = False # very important, the dependencies need to be purged from cache |
|
76 |
self._notice("=== Running migrate for schema %s" % tenant.schema_name) |
|
77 |
self._migrate_schema(tenant) |
|
78 |
|
|
79 |
self._restore_south_settings() |
|
80 |
|
|
81 |
def migrate_public_apps(self): |
|
82 |
self._save_south_settings() |
|
83 |
|
|
84 |
apps = self.shared_apps or self.installed_apps |
|
85 |
self._set_managed_apps(included_apps=apps, excluded_apps=self.tenant_apps) |
|
86 |
|
|
87 |
self._notice("=== Running migrate for schema public") |
|
88 |
MigrateCommand().execute(*self.args, **self.options) |
|
89 |
|
|
90 |
self._clear_south_cache() |
|
91 |
self._restore_south_settings() |
entrouvert/djommon/multitenant/management/commands/sync_schemas.py | ||
---|---|---|
1 |
# this file derive from django-tenant-schemas |
|
2 |
# Author: Bernardo Pires Carneiro |
|
3 |
# Email: carneiro.be@gmail.com |
|
4 |
# License: MIT license |
|
5 |
# Home-page: http://github.com/bcarneiro/django-tenant-schemas |
|
6 |
from django.conf import settings |
|
7 |
from django.contrib.contenttypes.models import ContentType |
|
8 |
from django.db.models import get_apps, get_models |
|
9 |
if "south" in settings.INSTALLED_APPS: |
|
10 |
from south.management.commands.syncdb import Command as SyncdbCommand |
|
11 |
else: |
|
12 |
from django.core.management.commands.syncdb import Command as SyncdbCommand |
|
13 |
from django.db import connection |
|
14 |
from entrouvert.djommon.multitenant.middleware import TenantMiddleware |
|
15 |
from entrouvert.djommon.multitenant.management.commands import SyncCommon |
|
16 |
|
|
17 |
|
|
18 |
class Command(SyncCommon): |
|
19 |
help = "Sync schemas based on TENANT_APPS and SHARED_APPS settings" |
|
20 |
option_list = SyncdbCommand.option_list + SyncCommon.option_list |
|
21 |
|
|
22 |
def handle(self, *args, **options): |
|
23 |
super(Command, self).handle(*args, **options) |
|
24 |
|
|
25 |
if "south" in settings.INSTALLED_APPS: |
|
26 |
self.options["migrate"] = False |
|
27 |
|
|
28 |
# save original settings |
|
29 |
for model in get_models(include_auto_created=True): |
|
30 |
setattr(model._meta, 'was_managed', model._meta.managed) |
|
31 |
|
|
32 |
ContentType.objects.clear_cache() |
|
33 |
|
|
34 |
if self.sync_public: |
|
35 |
self.sync_public_apps() |
|
36 |
if self.sync_tenant: |
|
37 |
self.sync_tenant_apps(self.schema_name) |
|
38 |
|
|
39 |
# restore settings |
|
40 |
for model in get_models(include_auto_created=True): |
|
41 |
model._meta.managed = model._meta.was_managed |
|
42 |
|
|
43 |
def _set_managed_apps(self, included_apps): |
|
44 |
""" sets which apps are managed by syncdb """ |
|
45 |
for model in get_models(include_auto_created=True): |
|
46 |
model._meta.managed = False |
|
47 |
|
|
48 |
verbosity = int(self.options.get('verbosity')) |
|
49 |
for app_model in get_apps(): |
|
50 |
app_name = app_model.__name__.replace('.models', '') |
|
51 |
if app_name in included_apps: |
|
52 |
for model in get_models(app_model, include_auto_created=True): |
|
53 |
model._meta.managed = model._meta.was_managed |
|
54 |
if model._meta.managed and verbosity >= 3: |
|
55 |
self._notice("=== Include Model: %s: %s" % (app_name, model.__name__)) |
|
56 |
|
|
57 |
def _sync_tenant(self, tenant): |
|
58 |
self._notice("=== Running syncdb for schema: %s" % tenant.schema_name) |
|
59 |
connection.set_tenant(tenant, include_public=False) |
|
60 |
SyncdbCommand().execute(**self.options) |
|
61 |
|
|
62 |
def sync_tenant_apps(self, schema_name=None): |
|
63 |
apps = self.tenant_apps or self.installed_apps |
|
64 |
self._set_managed_apps(apps) |
|
65 |
if schema_name: |
|
66 |
tenant = TenantMiddleware.get_tenant_by_hostname(schema_name) |
|
67 |
self._sync_tenant(tenant) |
|
68 |
else: |
|
69 |
all_tenants = TenantMiddleware.get_tenants() |
|
70 |
if not all_tenants: |
|
71 |
self._notice("No tenants found!") |
|
72 |
|
|
73 |
for tenant in all_tenants: |
|
74 |
self._sync_tenant(tenant) |
|
75 |
|
|
76 |
def sync_public_apps(self): |
|
77 |
apps = self.shared_apps or self.installed_apps |
|
78 |
self._set_managed_apps(apps) |
|
79 |
SyncdbCommand().execute(**self.options) |
|
80 |
self._notice("=== Running syncdb for schema public") |
entrouvert/djommon/multitenant/management/commands/syncdb.py | ||
---|---|---|
1 |
# this file derive from django-tenant-schemas |
|
2 |
# Author: Bernardo Pires Carneiro |
|
3 |
# Email: carneiro.be@gmail.com |
|
4 |
# License: MIT license |
|
5 |
# Home-page: http://github.com/bcarneiro/django-tenant-schemas |
|
6 |
from django.core.management.base import CommandError |
|
7 |
from django.conf import settings |
|
8 |
from tenant_schemas.utils import django_is_in_test_mode |
|
9 |
|
|
10 |
try: |
|
11 |
from south.management.commands import syncdb |
|
12 |
except ImportError: |
|
13 |
from django.core.management.commands import syncdb |
|
14 |
|
|
15 |
|
|
16 |
class Command(syncdb.Command): |
|
17 |
|
|
18 |
def handle(self, *args, **options): |
|
19 |
database = options.get('database', 'default') |
|
20 |
if (settings.DATABASES[database]['ENGINE'] == 'tenant_schemas.postgresql_backend' and not |
|
21 |
django_is_in_test_mode()): |
|
22 |
raise CommandError("syncdb has been disabled, for database '{}'. " |
|
23 |
"Use sync_schemas instead. Please read the " |
|
24 |
"documentation if you don't know why " |
|
25 |
"you shouldn't call syncdb directly!".format(database)) |
|
26 |
super(Command, self).handle(*args, **options) |
entrouvert/djommon/multitenant/management/commands/tenant_command.py | ||
---|---|---|
1 |
# this file derive from django-tenant-schemas |
|
2 |
# Author: Bernardo Pires Carneiro |
|
3 |
# Email: carneiro.be@gmail.com |
|
4 |
# License: MIT license |
|
5 |
# Home-page: http://github.com/bcarneiro/django-tenant-schemas |
|
6 |
from optparse import make_option |
|
7 |
from django.core.management.base import BaseCommand, CommandError |
|
8 |
from django.core.management import call_command, get_commands, load_command_class |
|
9 |
from django.db import connection |
|
10 |
from entrouvert.djommon.multitenant.management.commands import InteractiveTenantOption |
|
11 |
|
|
12 |
|
|
13 |
class Command(InteractiveTenantOption, BaseCommand): |
|
14 |
help = "Wrapper around django commands for use with an individual tenant" |
|
15 |
|
|
16 |
def run_from_argv(self, argv): |
|
17 |
""" |
|
18 |
Changes the option_list to use the options from the wrapped command. |
|
19 |
Adds schema parameter to specifiy which schema will be used when |
|
20 |
executing the wrapped command. |
|
21 |
""" |
|
22 |
# load the command object. |
|
23 |
try: |
|
24 |
app_name = get_commands()[argv[2]] |
|
25 |
except KeyError: |
|
26 |
raise CommandError("Unknown command: %r" % argv[2]) |
|
27 |
|
|
28 |
if isinstance(app_name, BaseCommand): |
|
29 |
# if the command is already loaded, use it directly. |
|
30 |
klass = app_name |
|
31 |
else: |
|
32 |
klass = load_command_class(app_name, argv[2]) |
|
33 |
|
|
34 |
self.option_list = klass.option_list + ( |
|
35 |
make_option("-d", "--domain", dest="domain", help="specify tenant schema"), |
|
36 |
) |
|
37 |
|
|
38 |
super(Command, self).run_from_argv(argv) |
|
39 |
|
|
40 |
def handle(self, *args, **options): |
|
41 |
tenant = self.get_tenant_from_options_or_interactive(**options) |
|
42 |
connection.set_tenant(tenant) |
|
43 |
|
|
44 |
call_command(*args, **options) |
Also available in: Unified diff
Import django-tenant-schemas commands to adapt them to our way of managing tenants
refs #5106