Skip to content

Commit

Permalink
Merge 9b97a05 into d11d492
Browse files Browse the repository at this point in the history
  • Loading branch information
charettes committed Jan 5, 2016
2 parents d11d492 + 9b97a05 commit e5ee69c
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 2 deletions.
25 changes: 23 additions & 2 deletions tenancy/models.py
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
60 changes: 60 additions & 0 deletions tenancy/operations.py
@@ -0,0 +1,60 @@
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 key, 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" % key, connection.ops.max_name_length())
options.update(
managed=True,
db_table=db_schema_table(tenant, db_table),
)
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)
15 changes: 15 additions & 0 deletions tests/test_operations.py
@@ -0,0 +1,15 @@
from __future__ import unicode_literals

from django.core.management import call_command
from django.test.utils import captured_stdout, override_settings

from .utils import TenancyTestCase


@override_settings(MIGRATION_MODULES={'tests': 'tests.test_operations_migrations'})
class TestTenantSchemaOperations(TenancyTestCase):
def test_migrate_forwards(self):
with captured_stdout() as stdout:
call_command('migrate', 'tests', interactive=False, stdout=stdout)
with captured_stdout() as stdout:
call_command('migrate', 'tests', 'zero', interactive=False, stdout=stdout)
24 changes: 24 additions & 0 deletions tests/test_operations_migrations/0001_initial.py
@@ -0,0 +1,24 @@
# -*- 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='BaseDefinition',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
],
bases=(models.Model,),
options={
'managed': Managed('tenancy.Tenant'),
}
),
DeleteTenantModel(name='BaseDefinition'),
]
Empty file.

0 comments on commit e5ee69c

Please sign in to comment.