This commit is contained in:
Bernardo Pires 2014-12-27 17:01:08 +01:00
parent ec37a5fb6b
commit 1019e0186c
18 changed files with 74 additions and 61 deletions

View File

@ -9,4 +9,4 @@ class DummyModel(models.Model):
name = models.CharField(max_length=100)
def __unicode__(self):
return self.name
return self.name

View File

@ -2,4 +2,4 @@ from django import forms
class GenerateUsersForm(forms.Form):
pass
pass

View File

@ -5,4 +5,4 @@ from tenant_schemas.models import TenantMixin
class Client(TenantMixin):
name = models.CharField(max_length=100)
description = models.TextField(max_length=200)
created_on = models.DateField(auto_now_add=True)
created_on = models.DateField(auto_now_add=True)

View File

@ -78,7 +78,6 @@ STATICFILES_DIRS = (
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
# Make this unique, and don't share it with anybody.
@ -88,7 +87,6 @@ SECRET_KEY = 'as-%*_93v=r5*p_7cu8-%o6b&x^g+q$#*e*fl)k)x0-t=%q0qa'
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
# 'django.template.loaders.eggs.Loader',
)
MIDDLEWARE_CLASSES = (

View File

@ -1,6 +1,6 @@
from django.conf.urls import patterns
from django.conf.urls import patterns, url
from tenant_tutorial.views import HomeView
urlpatterns = patterns('',
(r'^$', HomeView.as_view()),
)
url(r'^$', HomeView.as_view()),
)

View File

@ -1,6 +1,6 @@
from django.conf.urls import patterns
from django.conf.urls import patterns, url
from customers.views import TenantView
urlpatterns = patterns('',
(r'^$', TenantView.as_view()),
)
url(r'^$', TenantView.as_view()),
)

View File

@ -3,4 +3,4 @@ from django.contrib.auth.management.commands import createsuperuser
class Command(TenantWrappedCommand):
COMMAND = createsuperuser.Command
COMMAND = createsuperuser.Command

View File

@ -30,7 +30,7 @@ class Command(SyncCommon):
for app in ignored_apps:
app_label = app.split('.')[-1]
settings.SOUTH_MIGRATION_MODULES[app_label] = 'ignore'
self._clear_south_cache()
def _save_south_settings(self):

View File

@ -18,10 +18,6 @@ class Command(SyncCommon):
def handle(self, *args, **options):
super(Command, self).handle(*args, **options)
if 'tenant_schemas.routers.TenantSyncRouter' not in settings.DATABASE_ROUTERS:
raise ImproperlyConfigured("DATABASE_ROUTERS setting must contain "
"'tenant_schemas.routers.TenantSyncRouter'.")
if "south" in settings.INSTALLED_APPS:
self.options["migrate"] = False

View File

@ -2,14 +2,15 @@ from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import connection
from django.shortcuts import get_object_or_404
from tenant_schemas.utils import get_tenant_model, remove_www, get_public_schema_name
from tenant_schemas.utils import (get_tenant_model, remove_www,
get_public_schema_name)
class TenantMiddleware(object):
"""
This middleware should be placed at the very top of the middleware stack.
Selects the proper database schema using the request host. Can fail in
various ways which is better than corrupting or revealing data...
various ways which is better than corrupting or revealing data.
"""
def hostname_from_request(self, request):
""" Extracts hostname from request. Used for custom requests filtering.
@ -18,22 +19,24 @@ class TenantMiddleware(object):
return remove_www(request.get_host().split(':')[0])
def process_request(self, request):
# connection needs first to be at the public schema, as this is where the
# tenant metadata is stored
# Connection needs first to be at the public schema, as this is where
# the tenant metadata is stored.
connection.set_schema_to_public()
hostname = self.hostname_from_request(request)
request.tenant = get_object_or_404(get_tenant_model(), domain_url=hostname)
request.tenant = get_object_or_404(
get_tenant_model(), domain_url=hostname)
connection.set_tenant(request.tenant)
# content type can no longer be cached as public and tenant schemas have different
# models. if someone wants to change this, the cache needs to be separated between
# public and shared schemas. if this cache isn't cleared, this can cause permission
# problems. for example, on public, a particular model has id 14, but on the tenants
# it has the id 15. if 14 is cached instead of 15, the permissions for the wrong
# model will be fetched.
# Content type can no longer be cached as public and tenant schemas
# have different models. If someone wants to change this, the cache
# needs to be separated between public and shared schemas. If this
# cache isn't cleared, this can cause permission problems. For example,
# on public, a particular model has id 14, but on the tenants it has
# the id 15. if 14 is cached instead of 15, the permissions for the
# wrong model will be fetched.
ContentType.objects.clear_cache()
# do we have a public-specific token?
# Do we have a public-specific urlconf?
if hasattr(settings, 'PUBLIC_SCHEMA_URLCONF') and request.tenant.schema_name == get_public_schema_name():
request.urlconf = settings.PUBLIC_SCHEMA_URLCONF

View File

@ -8,17 +8,26 @@ from tenant_schemas.utils import get_public_schema_name
class TenantMixin(models.Model):
auto_drop_schema = False # USE THIS WITH CAUTION!
# set this flag to true on a parent class if
# you want the schema to be automatically
# removed after tenant remove.
"""
All tenant models must inherit this class.
"""
auto_create_schema = True # set this flag to false on a parent class if
# you don't want the schema to be automatically
# created upon save.
auto_drop_schema = False
"""
USE THIS WITH CAUTION!
Set this flag to true on a parent class if you want the schema to be
automatically deleted if the tenant row gets deleted.
"""
auto_create_schema = True
"""
Set this flag to false on a parent class if you don't want the schema
to be automatically created upon save.
"""
domain_url = models.CharField(max_length=128, unique=True)
schema_name = models.CharField(max_length=63, unique=True, validators=[_check_schema_name])
schema_name = models.CharField(max_length=63, unique=True,
validators=[_check_schema_name])
class Meta:
abstract = True
@ -27,10 +36,11 @@ class TenantMixin(models.Model):
is_new = self.pk is None
if is_new and connection.schema_name != get_public_schema_name():
raise Exception("Can't create tenant outside the public schema. Current schema is %s."
% connection.schema_name)
raise Exception("Can't create tenant outside the public schema. "
"Current schema is %s." % connection.schema_name)
elif not is_new and connection.schema_name not in (self.schema_name, get_public_schema_name()):
raise Exception("Can't update tenant outside it's own schema or the public schema. Current schema is %s."
raise Exception("Can't update tenant outside it's own schema or "
"the public schema. Current schema is %s."
% connection.schema_name)
super(TenantMixin, self).save(*args, **kwargs)
@ -40,17 +50,19 @@ class TenantMixin(models.Model):
self.create_schema(check_if_exists=True, verbosity=verbosity)
post_schema_sync.send(sender=TenantMixin, tenant=self)
except:
# We failed creating the tenant, delete what we created and re-raise the exception
# We failed creating the tenant, delete what we created and
# re-raise the exception
self.delete(force_drop=True)
raise
def delete(self, force_drop=False, *args, **kwargs):
"""
Drops the schema related to the tenant instance. Just drop the schema if the parent
class model has the attribute auto_drop_schema set to True.
Deletes this row. Drops the tenant's schema if the attribute
auto_drop_schema set to True.
"""
if connection.schema_name not in (self.schema_name, get_public_schema_name()):
raise Exception("Can't delete tenant outside it's own schema or the public schema. Current schema is %s."
raise Exception("Can't delete tenant outside it's own schema or "
"the public schema. Current schema is %s."
% connection.schema_name)
if schema_exists(self.schema_name) and (self.auto_drop_schema or force_drop):
@ -60,11 +72,12 @@ class TenantMixin(models.Model):
super(TenantMixin, self).delete(*args, **kwargs)
def create_schema(self, check_if_exists=False, sync_schema=True, verbosity=1):
def create_schema(self, check_if_exists=False, sync_schema=True,
verbosity=1):
"""
Creates the schema 'schema_name' for this tenant. Optionally checks if the schema
already exists before creating it. Returns true if the schema was created, false
otherwise.
Creates the schema 'schema_name' for this tenant. Optionally checks if
the schema already exists before creating it. Returns true if the
schema was created, false otherwise.
"""
# safety check
@ -79,14 +92,15 @@ class TenantMixin(models.Model):
transaction.commit_unless_managed()
if sync_schema:
# default is faking all migrations and syncing directly to the current models state
# Default is faking all migrations and syncing directly to the
# current models state.
fake_all_migrations = getattr(settings, 'TENANT_CREATION_FAKES_MIGRATIONS', True)
call_command('sync_schemas',
schema_name=self.schema_name,
tenant=True,
public=False,
interactive=False, # don't ask to create an admin user
interactive=False, # don't ask to create an admin
migrate_all=fake_all_migrations,
verbosity=verbosity,
)
@ -99,4 +113,3 @@ class TenantMixin(models.Model):
verbosity=verbosity)
connection.set_schema_to_public()
return True

View File

@ -3,4 +3,4 @@ from django.dispatch import Signal
post_schema_sync = Signal(providing_args=['tenant'])
post_schema_sync.__doc__ = """
Sent after a tenant has been saved, its schema created and synced
"""
"""

View File

@ -7,7 +7,8 @@ import hashlib
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.template.base import TemplateDoesNotExist
from django.template.loader import BaseLoader, get_template_from_string, find_template_loader, make_origin
from django.template.loader import (BaseLoader, get_template_from_string,
find_template_loader, make_origin)
from django.utils.encoding import force_bytes
from django.utils._os import safe_join
from django.db import connection
@ -37,7 +38,7 @@ class CachedLoader(BaseLoader):
for loader in self.loaders:
try:
template, display_name = loader(name, dirs)
return (template, make_origin(display_name, loader, name, dirs))
return template, make_origin(display_name, loader, name, dirs)
except TemplateDoesNotExist:
pass
raise TemplateDoesNotExist(name)
@ -48,7 +49,8 @@ class CachedLoader(BaseLoader):
else:
key = template_name
if template_dirs:
# If template directories were specified, use a hash to differentiate
# If template directories were specified, use a hash to
# differentiate
if connection.tenant:
key = '-'.join([str(connection.tenant.pk), template_name,
hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()])

View File

@ -16,4 +16,4 @@ class SchemaURLNode(URLNode):
@register.tag
def url(parser, token):
return SchemaURLNode(default_url(parser,token))
return SchemaURLNode(default_url(parser, token))

View File

@ -14,4 +14,4 @@ class NonAutoSyncTenant(TenantMixin):
auto_create_schema = False
class Meta:
app_label = 'tenant_schemas'
app_label = 'tenant_schemas'

View File

@ -177,7 +177,7 @@ class TenantSyncTest(BaseTestCase):
self.assertIn('django_session', tenant_tables)
def test_content_types_is_not_mandatory(self):
"""
"""
Tests that even if content types is in SHARED_APPS, it's
not required in TENANT_APPS.
"""

View File

@ -49,4 +49,4 @@ class BaseTestCase(TestCase):
interactive=False,
migrate_all=True,
verbosity=0,
)
)

View File

@ -55,11 +55,12 @@ def clean_tenant_url(url_string):
def remove_www_and_dev(hostname):
"""
"""
Legacy function - just in case someone is still using the old name
"""
return remove_www(hostname)
def remove_www(hostname):
"""
Removes www. from the beginning of the address. Only for
@ -102,4 +103,4 @@ def app_labels(apps_list):
"""
Returns a list of app labels of the given apps_list
"""
return [app.split('.')[-1] for app in apps_list]
return [app.split('.')[-1] for app in apps_list]