Skip to content

Commit

Permalink
Added tenant model schema operations.
Browse files Browse the repository at this point in the history
  • Loading branch information
charettes committed Jan 6, 2016
1 parent ed96111 commit 5ec7531
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 2 deletions.
25 changes: 23 additions & 2 deletions tenancy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.db.models.deletion import DO_NOTHING
from django.db.models.fields import Field
from django.dispatch.dispatcher import receiver
from django.utils.deconstruct import deconstructible
from django.utils.six import itervalues, string_types, with_metaclass
from django.utils.six.moves import copyreg

Expand Down Expand Up @@ -144,6 +145,26 @@ def natural_key(self):
return (self.name,)


@deconstructible
class Managed(object):
"""
Sentinel object used to detect tenant managed models.
"""

def __init__(self, tenant_model):
self.tenant_model = tenant_model

def __bool__(self):
# Evaluates to False in order to prevent Django from managing the model.
return False

# Remove when dropping support for Python 2.7
__nonzero__ = bool

def __eq__(self, other):
return isinstance(other, Managed) and other.tenant_model == self.tenant_model


def meta(Meta=None, **opts):
"""
Create a class with specified opts as attributes to be used as model
Expand Down Expand Up @@ -237,7 +258,7 @@ def __new__(cls, name, bases, attrs):
if getattr(Meta, 'proxy', False):
model = super_new(
cls, name, bases,
dict(attrs, meta=meta(Meta, managed=False))
dict(attrs, meta=meta(Meta, managed=Managed(settings.TENANT_MODEL)))
)
cls.references[model] = cls.reference(model, Meta)
else:
Expand All @@ -259,7 +280,7 @@ def __new__(cls, name, bases, attrs):
related_names[m2m.name] = get_remote_field(m2m).related_name
model = super_new(
cls, name, bases,
dict(attrs, Meta=meta(Meta, managed=False))
dict(attrs, Meta=meta(Meta, managed=Managed(settings.TENANT_MODEL)))
)
cls.references[model] = cls.reference(model, Meta, related_names)
opts = model._meta
Expand Down
61 changes: 61 additions & 0 deletions tenancy/operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from __future__ import unicode_literals

from django.apps import apps
from django.db.backends.utils import truncate_name
from django.db.migrations import operations
from django.db.migrations.operations.base import Operation
from django.utils.six import iteritems

from .models import Managed, db_schema_table


class TenantModelOperation(Operation):
def get_tenant_model(self, app_label, from_state, to_state):
raise NotImplementedError

def create_tenant_project_state(self, tenant, state, connection):
managed = Managed("%s.%s" % (tenant._meta.app_label, tenant._meta.object_name))
project_state = state.clone()
for (app_label, model_name), model_state in iteritems(project_state.models):
options = model_state.options
if options.get('managed') == managed:
db_table = options.get('db_table')
if not db_table:
db_table = truncate_name("%s_%s" % (app_label, model_name), connection.ops.max_name_length())
options.update(
managed=True,
db_table=db_schema_table(tenant, db_table),
)
project_state.reload_model(app_label, model_name)
return project_state

def tenant_operation(self, tenant_model, operation, app_label, schema_editor, from_state, to_state):
connection = schema_editor.connection
for tenant in tenant_model._base_manager.all():
tenant_from_state = self.create_tenant_project_state(tenant, from_state, connection)
tenant_to_state = self.create_tenant_project_state(tenant, to_state, connection)
operation(app_label, schema_editor, tenant_from_state, tenant_to_state)

def database_forwards(self, app_label, schema_editor, from_state, to_state):
tenant_model = self.get_tenant_model(app_label, from_state, to_state)
operation = super(TenantModelOperation, self).database_forwards
self.tenant_operation(tenant_model, operation, app_label, schema_editor, from_state, to_state)

def database_backwards(self, app_label, schema_editor, from_state, to_state):
tenant_model = self.get_tenant_model(app_label, to_state, from_state)
operation = super(TenantModelOperation, self).database_backwards
self.tenant_operation(tenant_model, operation, app_label, schema_editor, from_state, to_state)


class CreateTenantModel(TenantModelOperation, operations.CreateModel):
def get_tenant_model(self, app_label, from_state, to_state):
model_state = to_state.models[app_label, self.name_lower]
managed = model_state.options.get('managed')
return apps.get_model(managed.tenant_model)


class DeleteTenantModel(TenantModelOperation, operations.DeleteModel):
def get_tenant_model(self, app_label, from_state, to_state):
model_state = from_state.models[app_label, self.name_lower]
managed = model_state.options.get('managed')
return apps.get_model(managed.tenant_model)
46 changes: 46 additions & 0 deletions tests/test_operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from __future__ import unicode_literals

from django.core.management import call_command
from django.db import connection
from django.test.utils import override_settings
from django.utils.six import StringIO

from tenancy.models import Tenant, db_schema_table

from .utils import TenancyTestCase


@override_settings(MIGRATION_MODULES={'tests': 'tests.test_operations_migrations'})
class TestTenantSchemaOperations(TenancyTestCase):
def get_tenant_table_name(self, tenant, table_name):
return table_name if connection.vendor == 'postgresql' else db_schema_table(tenant, table_name)

def get_tenant_table_names(self, tenant):
if connection.vendor == 'postgresql':
cursor = connection.cursor()
cursor.execute("SET search_path = %s, public" % tenant.db_schema)
table_names = connection.introspection.table_names()
if connection.vendor == 'postgresql':
cursor.execute('RESET search_path')
return table_names

def assertTenantTableExists(self, tenant, table_name):
table_name = self.get_tenant_table_name(tenant, table_name)
table_names = self.get_tenant_table_names(tenant)
msg = "Table '%s' doesn't exist, existing table_names are: %s"
self.assertIn(table_name, table_names, msg % (table_name, ', '.join(table_names)))

def assertTenantTableDoesntExists(self, tenant, table_name):
table_name = self.get_tenant_table_name(tenant, table_name)
table_names = self.get_tenant_table_names(tenant)
self.assertNotIn(table_name, table_names, "Table '%s' exists." % table_name)

def test_create_and_delete_model(self):
call_command('migrate', 'tests', interactive=False, stdout=StringIO.StringIO())
for tenant in Tenant.objects.all():
self.assertTenantTableExists(tenant, 'tests_created')
self.assertTenantTableDoesntExists(tenant, 'tests_deleted')
call_command('migrate', 'tests', 'zero', interactive=False, stdout=StringIO.StringIO())
for tenant in Tenant.objects.all():
self.assertTenantTableDoesntExists(tenant, 'tests_created')
self.assertTenantTableDoesntExists(tenant, 'tests_deleted')
34 changes: 34 additions & 0 deletions tests/test_operations_migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models

from tenancy.models import Managed
from tenancy.operations import CreateTenantModel, DeleteTenantModel


class Migration(migrations.Migration):

operations = [
CreateTenantModel(
name='Created',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
],
bases=(models.Model,),
options={
'managed': Managed('tenancy.Tenant'),
}
),
CreateTenantModel(
name='Deleted',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
],
bases=(models.Model,),
options={
'managed': Managed('tenancy.Tenant'),
}
),
DeleteTenantModel('Deleted'),
]
Empty file.

0 comments on commit 5ec7531

Please sign in to comment.