Project

General

Profile

0008-Import-django-tenant-schemas-commands-to-adapt-them-.patch

Benjamin Dauvergne, 02 September 2014 03:36 PM

Download (21.2 KB)

View differences:

Subject: [PATCH 08/11] Import django-tenant-schemas commands to adapt them to
 our way of managing tenants

 .../multitenant/management/commands/__init__.py    |  166 ++++++++++++++++++++
 .../management/commands/createsuperuser.py         |   11 ++
 .../multitenant/management/commands/migrate.py     |   23 +++
 .../management/commands/migrate_schemas.py         |   91 +++++++++++
 .../management/commands/sync_schemas.py            |   80 ++++++++++
 .../multitenant/management/commands/syncdb.py      |   26 +++
 .../management/commands/tenant_command.py          |   44 ++++++
 7 files changed, 441 insertions(+)
 create mode 100644 entrouvert/djommon/multitenant/management/commands/createsuperuser.py
 create mode 100644 entrouvert/djommon/multitenant/management/commands/migrate.py
 create mode 100644 entrouvert/djommon/multitenant/management/commands/migrate_schemas.py
 create mode 100644 entrouvert/djommon/multitenant/management/commands/sync_schemas.py
 create mode 100644 entrouvert/djommon/multitenant/management/commands/syncdb.py
 create mode 100644 entrouvert/djommon/multitenant/management/commands/tenant_command.py
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)
0
-