Project

General

Profile

« Previous | Next » 

Revision 43d6b99c

Added by Thomas Noël (congés → 26 février) almost 9 years ago

  • ID 43d6b99c0f1273d62706c733a3a5e57ee30b712d
  • Parent 1db4b242

remove multitenant, now in hobo (#6491)

View differences:

entrouvert/djommon/multitenant/README
1
Multitenant
2
-----------
3

  
4
An application for making a Django application multitenant for Entr'ouvert
5
customers.
6

  
7
Based on https://django-tenant-schemas.readthedocs.org/
8

  
9
It is developed, tested and supported on Django 1.7, but it should work with
10
Django 1.6 + south.
11

  
12

  
13
Install
14
-------
15

  
16
See also : https://django-tenant-schemas.readthedocs.org/
17

  
18
Set the tenant model:
19

  
20
    TENANT_MODEL = 'multitenant.Tenant'
21

  
22
Where are tenants:
23

  
24
    TENANT_BASE = '/var/lib/<project>/tenants'
25

  
26
Add the middlewares for multitenant, they must be first:
27

  
28
    MIDDLEWARE_CLASSES = (
29
        'entrouvert.djommon.multitenant.middleware.TenantMiddleware',
30
        'entrouvert.djommon.multitenant.middleware.JSONSettingsMiddleware',
31
        'entrouvert.djommon.multitenant.middleware.PythonSettingsMiddleware',
32
    ) + MIDDLEWARE_CLASSES
33

  
34
Define the shared applications:
35

  
36
    SHARED_APPS = (
37
        'tenant_schemas',
38
        'entrouvert.djommon.multitenant',
39
        # those are needed for the public apps to work
40
        # add also any application needed by the public app
41
        'django.contrib.auth',
42
        'django.contrib.contenttypes',
43
        'django.contrib.sessions',
44
        'django.contrib.messages',
45
        'django.contrib.staticfiles',
46
    )
47

  
48
    TENANT_APPS = INSTALLED_APPS
49

  
50
    INSTALLED_APPS = ('entrouvert.djommon.multitenant',
51
        'tenant_schemas') + INSTALLED_APPS
52

  
53
    # or, with Django 1.6 or older:
54
    # INSTALLED_APPS += ('tenant_schemas', 'entrouvert.djommon.multitenant')
55

  
56
Use multitenant database engine:
57

  
58
    DATABASES = {
59
        'default': {
60
            'ENGINE': 'tenant_schemas.postgresql_backend',
61
            'NAME': '<db_name>',
62
        },
63
    }
64
    DATABASE_ROUTERS = (
65
        'tenant_schemas.routers.TenantSyncRouter',
66
    )
67

  
68
    # With Django 1.6 or older, use multitenant south adapter:
69
    # SOUTH_DATABASE_ADAPTERS = {'default': 'south.db.postgresql_psycopg2'}
70

  
71
Add the multitenant filesystem template loader and configure where the
72
multitenant templates are located:
73

  
74
    TEMPLATE_LOADERS = (
75
        'entrouvert.djommon.multitenant.template_loader.FilesystemLoader',
76
    ) + TEMPLATE_LOADERS
77
    TENANT_TEMPLATE_DIRS = (TENANT_BASE,)
78

  
79
    TEMPLATE_CONTEXT_PROCESSORS = (
80
        'django.core.context_processors.request',
81
    ) + TEMPLATE_CONTEXT_PROCESSORS
82

  
83

  
84
Usage
85
-----
86

  
87
Create a tenant:
88
   manage.py create_tenant www.example.net
89
Migration of all tenants:
90
   manage.py migrate_schemas
91
Add a super user to tenant:
92
   manage.py tenant_command createsuperuser --domain www.example.net
93

  
94
Tenants are created in TENANT_BASE directory, for example :
95
    /var/lib/project/tenants/www.example.net/
96
               templates/  <-- override project templates
97
               static/     <-- to be handled by HTTP server
98
               media/
99
Each tenant is a PostgreSQL schema, named www_example_net
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
            domain = options['domain']
98
        else:
99
            while True:
100
                domain = input("Enter Tenant Domain ('?' to list): ")
101
                if domain == '?':
102
                    print('\n'.join(["%s (schema %s)" % (t.domain_url, t.schema_name) for t in all_tenants]))
103
                else:
104
                    break
105

  
106
        if domain not in [t.domain_url for t in all_tenants]:
107
            raise CommandError("Invalid tenant, '%s'" % (domain,))
108

  
109
        return TenantMiddleware.get_tenant_by_hostname(domain)
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.domain:
149
            self.schema_name = TenantMiddleware.hostname2schema(domain)
150
        else:
151
            self.schema_name = options.get('schema_name')
152

  
153
        if self.schema_name:
154
            if self.sync_public:
155
                raise CommandError("domain should only be used with the --tenant switch.")
156
            elif self.schema_name == get_public_schema_name():
157
                self.sync_public = True
158
            else:
159
                self.sync_tenant = True
160
        elif not self.sync_public and not self.sync_tenant:
161
            # no options set, sync both
162
            self.sync_tenant = True
163
            self.sync_public = True
164

  
165
        if hasattr(settings, 'TENANT_APPS'):
166
            self.tenant_apps = settings.TENANT_APPS
167
        if hasattr(settings, 'SHARED_APPS'):
168
            self.shared_apps = settings.SHARED_APPS
169

  
170
    def _notice(self, output):
171
        self.stdout.write(self.style.NOTICE(output))
entrouvert/djommon/multitenant/management/commands/create_schemas.py
1
from django.core.management.base import BaseCommand
2
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
3
from django.db import connection
4

  
5
class Command(BaseCommand):
6
    help = "Create schemas for all declared tenants"
7

  
8
    def handle(self, *args, **options):
9
        verbosity = int(options.get('verbosity'))
10

  
11
        connection.set_schema_to_public()
12
        all_tenants = TenantMiddleware.get_tenants()
13
        for tenant in all_tenants:
14
            if verbosity >= 1:
15
                print
16
                print self.style.NOTICE("=== Creating schema ") \
17
                    + self.style.SQL_TABLE(tenant.schema_name)
18

  
19
            tenant.create_schema(check_if_exists=True)
entrouvert/djommon/multitenant/management/commands/create_tenant.py
1
import os
2

  
3
from django.db import connection
4
from django.core.management.base import CommandError, BaseCommand
5

  
6
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
7

  
8
class Command(BaseCommand):
9
    help = "Create tenant(s) by hostname(s)"
10

  
11
    def handle(self, *args, **options):
12
        verbosity = int(options.get('verbosity'))
13
        if not args:
14
            raise CommandError("you must give at least one tenant hostname")
15

  
16
        for hostname in args:
17
            try:
18
                tenant_base = TenantMiddleware.base()
19
            except AttributeError:
20
                raise CommandError("you must configure TENANT_BASE in your settings")
21
            if not tenant_base:
22
                raise CommandError("you must set a value to TENANT_BASE in your settings")
23
            tenant_dir = os.path.join(tenant_base, hostname)
24
            if not os.path.exists(tenant_dir):
25
                os.mkdir(tenant_dir, 0755)
26
            for folder in ('media', 'static', 'templates'):
27
                path = os.path.join(tenant_dir, folder)
28
                if not os.path.exists(path):
29
                    os.mkdir(path, 0755)
30
            connection.set_schema_to_public()
31
            tenant = TenantMiddleware.get_tenant_by_hostname(hostname)
32
            if verbosity >= 1:
33
                print
34
                print self.style.NOTICE("=== Creating schema ") \
35
                    + self.style.SQL_TABLE(tenant.schema_name)
36
            tenant.create_schema(check_if_exists=True)
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/deploy.py
1
import urllib2
2
import json
3
import sys
4

  
5
from django.core.management import call_command
6
from django.core.management.base import BaseCommand, CommandError
7

  
8
from tenant_schemas.utils import tenant_context
9
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
10

  
11
class Command(BaseCommand):
12
    help = 'Deploy a tenant from hobo'
13

  
14
    def handle(self, base_url, **options):
15
        environment = json.load(sys.stdin)
16
        for service in environment['services']:
17
            if service['base_url'] == base_url:
18
                break
19
        else:
20
            raise CommandError('Service %s not found' % base_url)
21
        hostname = urllib2.urlparse.urlsplit(base_url).netloc
22

  
23
        call_command('create_tenant', hostname)
24

  
25
        tenant = TenantMiddleware.get_tenant_by_hostname(hostname)
26
        with tenant_context(tenant):
27
            self.deploy_tenant(environment, service, options)
28

  
29
    def deploy_tenant(self, environment, service, options):
30
        pass
entrouvert/djommon/multitenant/management/commands/legacy/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.domain)
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/list_tenants.py
1
from django.core.management.base import BaseCommand
2
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
3

  
4
class Command(BaseCommand):
5
    requires_model_validation = True
6
    can_import_settings = True
7
    option_list = BaseCommand.option_list
8

  
9
    def handle(self, **options):
10
        all_tenants = TenantMiddleware.get_tenants()
11

  
12
        for tenant in all_tenants:
13
            print("{0} {1}".format(tenant.schema_name, tenant.domain_url))
14

  
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
import django
7
from django.conf import settings
8
from django.core.management.base import CommandError, BaseCommand
9

  
10
if django.VERSION < (1, 7, 0):
11
    try:
12
        from south.management.commands.migrate import Command as MigrateCommand
13
    except ImportError:
14
        MigrateCommand = BaseCommand
15
else:
16
    MigrateCommand = BaseCommand
17

  
18
class Command(MigrateCommand):
19

  
20
    def handle(self, *args, **options):
21
        database = options.get('database', 'default')
22
        if (settings.DATABASES[database]['ENGINE'] == 'tenant_schemas.postgresql_backend' or
23
                MigrateCommand is BaseCommand):
24
            raise CommandError("migrate has been disabled, for database '{}'. Use migrate_schemas "
25
                               "instead. Please read the documentation if you don't know why you "
26
                               "shouldn't call migrate directly!".format(database))
27
        super(Command, self).handle(*args, **options)
entrouvert/djommon/multitenant/management/commands/migrate_schemas.py
1
import django
2
from optparse import NO_DEFAULT
3

  
4
if django.VERSION >= (1, 7, 0):
5
    from django.core.management.commands.migrate import Command as MigrateCommand
6
    from django.db.migrations.recorder import MigrationRecorder
7
from django.db import connection
8
from django.conf import settings
9

  
10
from tenant_schemas.utils import get_public_schema_name
11
from entrouvert.djommon.multitenant.middleware import TenantMiddleware, TenantNotFound
12
from entrouvert.djommon.multitenant.management.commands import SyncCommon
13

  
14

  
15
class MigrateSchemasCommand(SyncCommon):
16
    help = "Updates database schema. Manages both apps with migrations and those without."
17

  
18
    def run_from_argv(self, argv):
19
        """
20
        Changes the option_list to use the options from the wrapped command.
21
        Adds schema parameter to specify which schema will be used when
22
        executing the wrapped command.
23
        """
24
        self.option_list += MigrateCommand.option_list
25
        super(MigrateSchemasCommand, self).run_from_argv(argv)
26

  
27
    def handle(self, *args, **options):
28
        super(MigrateSchemasCommand, self).handle(*args, **options)
29
        self.PUBLIC_SCHEMA_NAME = get_public_schema_name()
30

  
31
        if self.sync_public and not self.schema_name:
32
            self.schema_name = self.PUBLIC_SCHEMA_NAME
33

  
34
        if self.sync_public:
35
            self.run_migrations(self.schema_name, settings.SHARED_APPS)
36
        if self.sync_tenant:
37
            if self.schema_name and self.schema_name != self.PUBLIC_SCHEMA_NAME:
38
                self.run_migrations(self.schema_name, settings.TENANT_APPS)
39
            else:
40
                all_tenants = TenantMiddleware.get_tenants()
41
                for tenant in all_tenants:
42
                    self.run_migrations(tenant.schema_name, settings.TENANT_APPS)
43

  
44
    def run_migrations(self, schema_name, included_apps):
45
        self._notice("=== Running migrate for schema %s" % schema_name)
46
        connection.set_schema(schema_name)
47
        command = MigrateCommand()
48

  
49
        defaults = {}
50
        for opt in MigrateCommand.option_list:
51
            if opt.dest in self.options:
52
                defaults[opt.dest] = self.options[opt.dest]
53
            elif opt.default is NO_DEFAULT:
54
                defaults[opt.dest] = None
55
            else:
56
                defaults[opt.dest] = opt.default
57

  
58
        command.execute(*self.args, **defaults)
59
        connection.set_schema_to_public()
60

  
61
    def _notice(self, output):
62
        self.stdout.write(self.style.NOTICE(output))
63

  
64

  
65
if django.VERSION >= (1, 7, 0):
66
    Command = MigrateSchemasCommand
67
else:
68
    from .legacy.migrate_schemas import Command
entrouvert/djommon/multitenant/management/commands/safemigrate_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
import django
7

  
8
if django.VERSION < (1, 7, 0):
9
    from django.conf import settings
10
    from django.db import connection
11
    from south import migration
12
    from south.migration.base import Migrations
13
    from entrouvert.djommon.multitenant.middleware import TenantMiddleware
14
    from entrouvert.djommon.management.commands.safemigrate import Command as SafeMigrateCommand
15
    from entrouvert.djommon.multitenant.management.commands.sync_schemas import Command as MTSyncCommand
16
    from entrouvert.djommon.multitenant.management.commands.migrate_schemas import Command as MTMigrateCommand
17
from entrouvert.djommon.multitenant.management.commands import SyncCommon
18

  
19

  
20
class SafeMigrateCommand(SyncCommon):
21
    help = "Safely migrate schemas with South"
22
    option_list = MTMigrateCommand.option_list
23

  
24
    def handle(self, *args, **options):
25
        super(Command, self).handle(*args, **options)
26

  
27
        MTSyncCommand().execute(*args, **options)
28
        connection.set_schema_to_public()
29
        if self.sync_public:
30
            self.fake_public_apps()
31
        if self.sync_tenant:
32
            self.fake_tenant_apps(self.domain)
33
        connection.set_schema_to_public()
34
        MTMigrateCommand().execute(*args, **options)
35

  
36
    def _set_managed_apps(self, included_apps, excluded_apps):
37
        """ while sync_schemas works by setting which apps are managed, on south we set which apps should be ignored """
38
        ignored_apps = []
39
        if excluded_apps:
40
            for item in excluded_apps:
41
                if item not in included_apps:
42
                    ignored_apps.append(item)
43

  
44
        for app in ignored_apps:
45
            app_label = app.split('.')[-1]
46
            settings.SOUTH_MIGRATION_MODULES[app_label] = 'ignore'
47

  
48
    def _save_south_settings(self):
49
        self._old_south_modules = None
50
        if hasattr(settings, "SOUTH_MIGRATION_MODULES") and settings.SOUTH_MIGRATION_MODULES is not None:
51
            self._old_south_modules = settings.SOUTH_MIGRATION_MODULES.copy()
52
        else:
53
            settings.SOUTH_MIGRATION_MODULES = dict()
54

  
55
    def _restore_south_settings(self):
56
        settings.SOUTH_MIGRATION_MODULES = self._old_south_modules
57

  
58
    def _clear_south_cache(self):
59
        for mig in list(migration.all_migrations()):
60
            delattr(mig._application, "migrations")
61
        Migrations._clear_cache()
62

  
63
    def _fake_schema(self, tenant):
64
        connection.set_tenant(tenant, include_public=False)
65
        SafeMigrateCommand().fake_if_needed()
66

  
67
    def fake_tenant_apps(self, schema_name=None):
68
        self._save_south_settings()
69

  
70
        apps = self.tenant_apps or self.installed_apps
71
        self._set_managed_apps(included_apps=apps, excluded_apps=self.shared_apps)
72

  
73
        if schema_name:
74
            self._notice("=== Running fake_if_needed for schema: %s" % schema_name)
75
            connection.set_schema_to_public()
76
            tenant = TenantMiddleware.get_tenant_by_hostname(schema_name)
77
            self._fake_schema(tenant)
78
        else:
79
            all_tenants = TenantMiddleware.get_tenants()
80
            if not all_tenants:
81
                self._notice("No tenants found")
82

  
83
            for tenant in all_tenants:
84
                Migrations._dependencies_done = False  # very important, the dependencies need to be purged from cache
85
                self._notice("=== Running fake_if_needed for schema %s" % tenant.schema_name)
86
                self._fake_schema(tenant)
87

  
88
        self._restore_south_settings()
89

  
90
    def fake_public_apps(self):
91
        self._save_south_settings()
92

  
93
        apps = self.shared_apps or self.installed_apps
94
        self._set_managed_apps(included_apps=apps, excluded_apps=self.tenant_apps)
95

  
96
        self._notice("=== Running fake_if_needed for schema public")
97
        SafeMigrateCommand().fake_if_needed()
98

  
99
        self._clear_south_cache()
100
        self._restore_south_settings()
101

  
102
if django.VERSION < (1, 7, 0):
103
    Command = SafeMigrateCommand
104
else:
105
    raise RuntimeError('Django 1.7: please use migrate_schemas')
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
import django
7
from django.core.management.base import CommandError
8

  
9
from django.conf import settings
10
from django.contrib.contenttypes.models import ContentType
11
from django.db.models import get_apps, get_models
12
if 'south' in settings.INSTALLED_APPS:
13
    from south.management.commands.syncdb import Command as SyncdbCommand
14
else:
15
    from django.core.management.commands.syncdb import Command as SyncdbCommand
16
from django.db import connection
17
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
18
from entrouvert.djommon.multitenant.management.commands import SyncCommon
19

  
20

  
21
class Command(SyncCommon):
22
    help = "Sync schemas based on TENANT_APPS and SHARED_APPS settings"
23
    option_list = SyncdbCommand.option_list + SyncCommon.option_list
24

  
25
    def handle(self, *args, **options):
26
        if django.VERSION >= (1, 7, 0):
27
            raise CommandError('This command is only meant to be used for 1.6'
28
                               ' and older version of django. For 1.7, use'
29
                               ' `migrate_schemas` instead.')
30
        super(Command, self).handle(*args, **options)
31

  
32
        if "south" in settings.INSTALLED_APPS:
33
            self.options["migrate"] = False
34

  
35
        # save original settings
36
        for model in get_models(include_auto_created=True):
37
            setattr(model._meta, 'was_managed', model._meta.managed)
38

  
39
        ContentType.objects.clear_cache()
40

  
41
        if self.sync_public:
42
            self.sync_public_apps()
43
        if self.sync_tenant:
44
            self.sync_tenant_apps(self.domain)
45

  
46
        # restore settings
47
        for model in get_models(include_auto_created=True):
48
            model._meta.managed = model._meta.was_managed
49

  
50
    def _set_managed_apps(self, included_apps):
51
        """ sets which apps are managed by syncdb """
52
        for model in get_models(include_auto_created=True):
53
            model._meta.managed = False
54

  
55
        verbosity = int(self.options.get('verbosity'))
56
        for app_model in get_apps():
57
            app_name = app_model.__name__.replace('.models', '')
58
            if app_name in included_apps:
59
                for model in get_models(app_model, include_auto_created=True):
60
                    model._meta.managed = model._meta.was_managed
61
                    if model._meta.managed and verbosity >= 3:
62
                        self._notice("=== Include Model: %s: %s" % (app_name, model.__name__))
63

  
64
    def _sync_tenant(self, tenant):
65
        self._notice("=== Running syncdb for schema: %s" % tenant.schema_name)
66
        connection.set_tenant(tenant, include_public=False)
67
        SyncdbCommand().execute(**self.options)
68

  
69
    def sync_tenant_apps(self, schema_name=None):
70
        apps = self.tenant_apps or self.installed_apps
71
        self._set_managed_apps(apps)
72
        if schema_name:
73
            tenant = TenantMiddleware.get_tenant_by_hostname(schema_name)
74
            self._sync_tenant(tenant)
75
        else:
76
            all_tenants = TenantMiddleware.get_tenants()
77
            if not all_tenants:
78
                self._notice("No tenants found!")
79

  
80
            for tenant in all_tenants:
81
                self._sync_tenant(tenant)
82

  
83
    def sync_public_apps(self):
84
        self._notice("=== Running syncdb for schema public")
85
        apps = self.shared_apps or self.installed_apps
86
        self._set_managed_apps(apps)
87
        SyncdbCommand().execute(**self.options)
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
if 'south' in settings.INSTALLED_APPS:
11
    from south.management.commands import syncdb
12
else:
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 '{0}'. "
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
        super(Command, self).run_from_argv(argv)
35

  
36
    def handle(self, *args, **options):
37
        tenant = self.get_tenant_from_options_or_interactive(**options)
38
        connection.set_tenant(tenant)
39

  
40
        call_command(*args, **options)
entrouvert/djommon/multitenant/middleware.py
1
import os
2
import json
3
import glob
4

  
5
from django.conf import settings, UserSettingsHolder
6
from django.db import connection
7
from django.http import Http404
8
from django.contrib.contenttypes.models import ContentType
9
from tenant_schemas.utils import get_tenant_model, remove_www_and_dev, get_public_schema_name
10

  
11
SENTINEL = object()
12

  
13
class TenantNotFound(RuntimeError):
14
    pass
15

  
16
class TenantMiddleware(object):
17
    """
18
    This middleware should be placed at the very top of the middleware stack.
19
    Selects the proper database schema using the request host. Can fail in
20
    various ways which is better than corrupting or revealing data...
21
    """
22
    @classmethod
23
    def base(cls):
24
        return settings.TENANT_BASE
25

  
26
    @classmethod
27
    def hostname2schema(cls, hostname):
28
        '''Convert hostname to PostgreSQL schema name'''
29
        if hostname in getattr(settings, 'TENANT_MAPPING', {}):
30
            return settings.TENANT_MAPPING[hostname]
31
        return hostname.replace('.', '_').replace('-', '_')
32

  
33
    @classmethod
34
    def get_tenant_by_hostname(cls, hostname):
35
        '''Retrieve a tenant object for this hostname'''
36
        if not os.path.exists(os.path.join(cls.base(), hostname)):
37
            raise TenantNotFound
38
        schema = cls.hostname2schema(hostname)
39
        return get_tenant_model()(schema_name=schema, domain_url=hostname)
40

  
41
    @classmethod
42
    def get_tenants(cls):
43
        self = cls()
44
        for path in glob.glob(os.path.join(cls.base(), '*')):
45
            hostname = os.path.basename(path)
46
            yield get_tenant_model()(
47
                    schema_name=self.hostname2schema(hostname),
48
                    domain_url=hostname)
49

  
50
    def process_request(self, request):
51
        # connection needs first to be at the public schema, as this is where the
52
        # tenant informations are saved
53
        connection.set_schema_to_public()
54
        hostname_without_port = remove_www_and_dev(request.get_host().split(':')[0])
55

  
56
        try:
57
            request.tenant = self.get_tenant_by_hostname(hostname_without_port)
58
        except TenantNotFound:
59
            raise Http404
60
        connection.set_tenant(request.tenant)
61

  
62
        # content type can no longer be cached as public and tenant schemas have different
63
        # models. if someone wants to change this, the cache needs to be separated between
64
        # public and shared schemas. if this cache isn't cleared, this can cause permission
65
        # problems. for example, on public, a particular model has id 14, but on the tenants
66
        # it has the id 15. if 14 is cached instead of 15, the permissions for the wrong
67
        # model will be fetched.
68
        ContentType.objects.clear_cache()
69

  
70
        # do we have a public-specific token?
71
        if hasattr(settings, 'PUBLIC_SCHEMA_URLCONF') and request.tenant.schema_name == get_public_schema_name():
72
            request.urlconf = settings.PUBLIC_SCHEMA_URLCONF
73

  
74

  
75

  
76

  
77
class TenantSettingBaseMiddleware(object):
78
    '''Base middleware classe for loading settings based on tenants
79

  
80
       Child classes MUST override the load_tenant_settings() method.
81
    '''
82
    def __init__(self, *args, **kwargs):
83
        self.tenants_settings = {}
84

  
85
    def get_tenant_settings(self, wrapped, tenant):
86
        '''Get last loaded settings for tenant, try to update it by loading
87
           settings again is last loading time is less recent thant settings data
88
           store. Compare with last modification time is done in the
89
           load_tenant_settings() method.
90
        '''
91
        tenant_settings, last_time = self.tenants_settings.get(tenant.schema_name, (None,None))
92
        if tenant_settings is None:
93
            tenant_settings = UserSettingsHolder(wrapped)
94
        tenant_settings, last_time = self.load_tenant_settings(wrapped, tenant, tenant_settings, last_time)
95
        self.tenants_settings[tenant.schema_name] = tenant_settings, last_time
96
        return tenant_settings
97

  
98
    def load_tenant_settings(self, wrapped, tenant, tenant_settings, last_time):
99
        '''Load tenant settings into tenant_settings object, eventually skip if
100
           last_time is more recent than last update time for settings and return
101
           the new value for tenant_settings and last_time'''
102
        raise NotImplemented
103

  
104
    def process_request(self, request):
105
        if not hasattr(request, '_old_settings_wrapped'):
106
            request._old_settings_wrapped = []
107
        request._old_settings_wrapped.append(settings._wrapped)
108
        settings._wrapped = self.get_tenant_settings(settings._wrapped, request.tenant)
109

  
110
    def process_response(self, request, response):
111
        if hasattr(request, '_old_settings_wrapped') and request._old_settings_wrapped:
112
            settings._wrapped = request._old_settings_wrapped.pop()
113
        return response
114

  
115

  
116
class FileBasedTenantSettingBaseMiddleware(TenantSettingBaseMiddleware):
117
    FILENAME = None
118

  
119
    def load_tenant_settings(self, wrapped, tenant, tenant_settings, last_time):
120
        path = os.path.join(settings.TENANT_BASE, tenant.domain_url, self.FILENAME)
121
        try:
122
            new_time = os.stat(path).st_mtime
123
        except OSError:
124
            # file was removed
125
            if not last_time is None:
126
                return UserSettingsHolder(wrapped), None
127
        else:
128
            if last_time is None or new_time >= last_time:
129
                # file is new
130
                tenant_settings = UserSettingsHolder(wrapped)
131
                self.load_file(tenant_settings, path)
132
                return tenant_settings, new_time
133
        # nothing has changed
134
        return tenant_settings, last_time
135

  
136

  
137
class JSONSettingsMiddleware(FileBasedTenantSettingBaseMiddleware):
138
    '''Load settings from a JSON file whose path is given by:
139

  
140
            os.path.join(settings.TENANT_BASE % schema_name, 'settings.json')
141

  
142
       The JSON file must be a dictionnary whose key/value will override
143
       current settings.
144
    '''
145
    FILENAME = 'settings.json'
146

  
147
    def load_file(sef, tenant_settings, path):
148
        with file(path) as f:
149
            json_settings = json.load(f)
150
            for key in json_settings:
151
                setattr(tenant_settings, key, json_settings[key])
152

  
153

  
154
class DictAdapter(dict):
155
    '''Give dict interface to plain objects'''
156
    def __init__(self, wrapped):
157
        self.wrapped = wrapped
158

  
159
    def __setitem__(self, key, value):
160
        setattr(self.wrapped, key, value)
161

  
162
    def __getitem__(self, key):
163
        try:
164
            return getattr(self.wrapped, key)
165
        except AttributeError:
166
            raise KeyError
167

  
168

  
169
class PythonSettingsMiddleware(FileBasedTenantSettingBaseMiddleware):
170
    '''Load settings from a file whose path is given by:
171

  
172
            os.path.join(settings.TENANT_BASE % schema_name, 'settings.py')
173

  
174
       The file is executed in the same context as the classic settings file
175
       using execfile.
176
    '''
177
    FILENAME = 'settings.py'
178

  
179
    def load_file(self, tenant_settings, path):
180
        execfile(path, DictAdapter(tenant_settings))
entrouvert/djommon/multitenant/models.py
1
from tenant_schemas.models import TenantMixin
2

  
3
class Tenant(TenantMixin):
4
    # default true, schema will be automatically created and synced when it is saved
5
    auto_create_schema = False
6

  
7
    def save(self):
8
        pass
9

  
10
    def __unicode__(self):
11
        return u'%s' % self.schema_name
entrouvert/djommon/multitenant/storage.py
1
import os
2

  
3
from django.conf import settings
4
from django.core.exceptions import SuspiciousOperation
5
from django.utils._os import safe_join
6

  
7
from django.db import connection
8

  
9
from django.core.files.storage import FileSystemStorage
10

  
11
__all__ = ('TenantFileSystemStorage', )
12

  
13
class TenantFileSystemStorage(FileSystemStorage):
14
    '''Lookup files first in $TENANT_BASE/<tenant.schema>/media/ then in default location'''
15
    def path(self, name):
16
        if connection.tenant:
17
            location = safe_join(settings.TENANT_BASE, connection.tenant.domain_url, 'media')
18
        else:
19
            location = self.location
20
        try:
21
            path = safe_join(location, name)
22
        except ValueError:
23
            raise SuspiciousOperation("Attempted access to '%s' denied." % name)
24
        return os.path.normpath(path)
entrouvert/djommon/multitenant/template_loader.py
1
"""
2
Wrapper class that takes a list of template loaders as an argument and attempts
3
to load templates from them in order, caching the result.
4
"""
5

  
6
import hashlib
7
from django.conf import settings
8
from django.core.exceptions import ImproperlyConfigured
9
from django.template.base import TemplateDoesNotExist
10
from django.template.loader import BaseLoader, get_template_from_string, find_template_loader, make_origin
11
from django.utils.encoding import force_bytes
12
from django.utils._os import safe_join
13
from django.db import connection
14

  
15
class CachedLoader(BaseLoader):
16
    is_usable = True
17

  
18
    def __init__(self, loaders):
19
        self.template_cache = {}
20
        self._loaders = loaders
21
        self._cached_loaders = []
22

  
23
    @property
24
    def loaders(self):
25
        # Resolve loaders on demand to avoid circular imports
26
        if not self._cached_loaders:
27
            # Set self._cached_loaders atomically. Otherwise, another thread
28
            # could see an incomplete list. See #17303.
29
            cached_loaders = []
30
            for loader in self._loaders:
31
                cached_loaders.append(find_template_loader(loader))
32
            self._cached_loaders = cached_loaders
33
        return self._cached_loaders
34

  
35
    def find_template(self, name, dirs=None):
36
        for loader in self.loaders:
37
            try:
38
                template, display_name = loader(name, dirs)
39
                return (template, make_origin(display_name, loader, name, dirs))
40
            except TemplateDoesNotExist:
41
                pass
42
        raise TemplateDoesNotExist(name)
43

  
44
    def load_template(self, template_name, template_dirs=None):
45
        if connection.tenant:
46
            key = '-'.join([str(connection.tenant.pk), template_name])
47
        else:
48
            key = template_name
49
        if template_dirs:
50
            # If template directories were specified, use a hash to differentiate
51
            if connection.tenant:
52
                key = '-'.join([str(connection.tenant.schema_name), template_name, hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()])
53
            else:
54
                key = '-'.join([template_name, hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()])
55

  
56
        if key not in self.template_cache:
57
            template, origin = self.find_template(template_name, template_dirs)
58
            if not hasattr(template, 'render'):
59
                try:
60
                    template = get_template_from_string(template, origin, template_name)
61
                except TemplateDoesNotExist:
62
                    # If compiling the template we found raises TemplateDoesNotExist,
63
                    # back off to returning the source and display name for the template
64
                    # we were asked to load. This allows for correct identification (later)
65
                    # of the actual template that does not exist.
66
                    return template, origin
67
            self.template_cache[key] = template
68
        return self.template_cache[key], None
69

  
70
    def reset(self):
71
        "Empty the template cache."
72
        self.template_cache.clear()
73

  
74
class FilesystemLoader(BaseLoader):
75
    is_usable = True
76

  
77
    def get_template_sources(self, template_name, template_dirs=None):
78
        """
79
        Returns the absolute paths to "template_name", when appended to each
80
        directory in "template_dirs". Any paths that don't lie inside one of the
81
        template dirs are excluded from the result set, for security reasons.
82
        """
83
        if not connection.tenant:
84
            return
85
        if not template_dirs:
86
            try:
87
                template_dirs = settings.TENANT_TEMPLATE_DIRS
88
            except AttributeError:
89
                raise ImproperlyConfigured('To use %s.%s you must define the TENANT_TEMPLATE_DIRS' % (__name__, FilesystemLoader.__name__))
90
        for template_dir in template_dirs:
91
            try:
92
                yield safe_join(template_dir, connection.tenant.domain_url, 'templates', template_name)
93
            except UnicodeDecodeError:
94
                # The template dir name was a bytestring that wasn't valid UTF-8.
95
                raise
96
            except ValueError:
97
                # The joined path was located outside of this particular
98
                # template_dir (it might be inside another one, so this isn't
99
                # fatal).
100
                pass
101

  
102
    def load_template_source(self, template_name, template_dirs=None):
103
        tried = []
104
        for filepath in self.get_template_sources(template_name, template_dirs):
105
            try:
106
                with open(filepath, 'rb') as fp:
107
                    return (fp.read().decode(settings.FILE_CHARSET), filepath)
108
            except IOError:
109
                tried.append(filepath)
110
        if tried:
111
            error_msg = "Tried %s" % tried
112
        else:
113
            error_msg = "Your TENANT_TEMPLATE_DIRS setting is empty. Change it to point to at least one template directory."
114
        raise TemplateDoesNotExist(error_msg)
115
    load_template_source.is_usable = True
entrouvert/djommon/multitenant/tests.py
1
"""
2
Test multitenant framework
3
"""
4

  
5
import tempfile
6
import shutil
7
import os
8
import json
9
import StringIO
10

  
11
from django.conf.urls import patterns
12
from django.test import TestCase, Client
13
from django.http import HttpResponse
14
from django.template.response import TemplateResponse
15

  
16
try:
17
    from django.test import override_settings
18
except ImportError: # django < 1.7
19
    from django.test.utils import override_settings
20

  
21

  
22
def json_key(request, *args, **kwargs):
23
    from django.conf import settings
24
    return HttpResponse(settings.JSON_KEY + ' json')
25

  
26
def python_key(request, *args, **kwargs):
27
    from django.conf import settings
28
    return HttpResponse(settings.PYTHON_KEY + ' python')
29

  
30
def template(request, *args, **kwargs):
31
    return TemplateResponse(request, 'tenant.html')
32

  
33
def upload(request):
34
    from django.core.files.storage import default_storage
35
    default_storage.save('upload', request.FILES['upload'])
36
    return HttpResponse('')
37

  
38
def download(request):
39
    from django.core.files.storage import default_storage
40
    return HttpResponse(default_storage.open('upload').read())
41

  
42
urlpatterns = patterns('',
43
        ('^json_key/$', json_key),
44
        ('^python_key/$', python_key),
45
        ('^template/$', template),
46
        ('^upload/$', upload),
47
        ('^download/$', download),
48
)
49

  
50
@override_settings(
51
        ROOT_URLCONF=__name__,
52
        MIDDLEWARE_CLASSES=(
53
            'entrouvert.djommon.multitenant.middleware.TenantMiddleware',
54
            'entrouvert.djommon.multitenant.middleware.JSONSettingsMiddleware',
55
            'entrouvert.djommon.multitenant.middleware.PythonSettingsMiddleware',
56
        ),
57
        TEMPLATE_LOADERS = (
58
           'entrouvert.djommon.multitenant.template_loader.FilesystemLoader',
59
        ),
60
        DEFAULT_FILE_STORAGE = 'entrouvert.djommon.multitenant.storage.TenantFileSystemStorage',
61
)
62
class SimpleTest(TestCase):
63
    TENANTS = ['tenant1', 'tenant2']
64

  
65
    def setUp(self):
66
        self.tenant_base = tempfile.mkdtemp()
67
        for tenant in self.TENANTS:
68
            tenant_dir = os.path.join(self.tenant_base, tenant)
69
            os.mkdir(tenant_dir)
70
            settings_py = os.path.join(tenant_dir, 'settings.json')
71
            with file(settings_py, 'w') as f:
72
                json.dump({'JSON_KEY': tenant}, f)
73
            settings_json = os.path.join(tenant_dir, 'settings.py')
74
            with file(settings_json, 'w') as f:
75
                print >>f, 'PYTHON_KEY = %r' % tenant
76
            templates_dir = os.path.join(tenant_dir, 'templates')
77
            os.mkdir(templates_dir)
78
            tenant_html = os.path.join(templates_dir, 'tenant.html')
79
            with file(tenant_html, 'w') as f:
80
                print >>f, tenant + ' template',
81
            media_dir = os.path.join(tenant_dir, 'media')
82
            os.mkdir(media_dir)
83

  
84
    def tearDown(self):
85
        shutil.rmtree(self.tenant_base, ignore_errors=True) 
86

  
87
    def tenant_settings(self):
88
        return self.settings(
89
                TENANT_BASE=self.tenant_base,
90
                TENANT_TEMPLATE_DIRS=(self.tenant_base,)
91
        )
92

  
93
    def test_tenants(self):
94
        with self.tenant_settings():
95
            for tenant in self.TENANTS:
96
                c = Client(HTTP_HOST=tenant)
97
                response = c.get('/json_key/')
98
                self.assertEqual(response.content, tenant + ' json')
99
                response = c.get('/python_key/')
100
                self.assertEqual(response.content, tenant + ' python')
101
                response = c.get('/template/')
102
                self.assertEqual(response.content, tenant + ' template')
103

  
104
    def test_list_tenants(self):
105
        from entrouvert.djommon.multitenant.middleware import TenantMiddleware
106
        from tenant_schemas.utils import get_tenant_model
107

  
108
        with self.tenant_settings():
109
            l1 = set(map(str, TenantMiddleware.get_tenants()))
110
            l2 = set(str(get_tenant_model()(schema_name=tenant,
111
                        domain_url=tenant)) for tenant in self.TENANTS)
112
            self.assertEquals(l1, l2)
113

  
114
    def test_storage(self):
115
        from django.core.files.base import ContentFile
116
        with self.tenant_settings():
117
            for tenant in self.TENANTS:
118
                c = Client(HTTP_HOST=tenant)
119
                uploaded_file_path = os.path.join(self.tenant_base, tenant, 'media', 'upload')
120
                self.assertFalse(os.path.exists(uploaded_file_path), uploaded_file_path)
121
                response = c.post('/upload/', {'upload': ContentFile(tenant + ' upload', name='upload.txt')})
122
                self.assertEqual(response.status_code, 200)
123
                self.assertEqual(response.content, '')
124
                self.assertTrue(os.path.exists(uploaded_file_path))
125
                self.assertEqual(file(uploaded_file_path).read(), tenant + ' upload')
126
                response = c.get('/download/')
127
                self.assertEqual(response.status_code, 200)
128
                self.assertEqual(response.content, tenant + ' upload')
entrouvert/djommon/multitenant/views.py
1
# Create your views here.

Also available in: Unified diff