def setUp(self): super(SharedAuthTest, self).setUp() settings.SHARED_APPS = ('django_tenants', 'customers', 'django.contrib.auth', 'django.contrib.contenttypes', ) settings.TENANT_APPS = ('dts_test_app', ) settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS self.sync_shared() self.public_tenant = get_tenant_model()(schema_name=get_public_schema_name()) self.public_tenant.save() self.public_domain = get_tenant_domain_model()(tenant=self.public_tenant, domain='test.com') self.public_domain.save() # Create a tenant self.tenant = get_tenant_model()(schema_name='tenant') self.tenant.save() self.domain = get_tenant_domain_model()(tenant=self.tenant, domain='tenant.test.com') self.domain.save() # Create some users with schema_context(get_public_schema_name()): # this could actually also be executed inside a tenant self.user1 = User(username='******', email="*****@*****.**") self.user1.save() self.user2 = User(username='******', email="*****@*****.**") self.user2.save() # Create instances on the tenant that point to the users on public with tenant_context(self.tenant): self.d1 = ModelWithFkToPublicUser(user=self.user1) self.d1.save() self.d2 = ModelWithFkToPublicUser(user=self.user2) self.d2.save()
def test_switching_search_path(self): tenant1 = get_tenant_model()(schema_name='tenant1') tenant1.save() domain1 = get_tenant_domain_model()(tenant=tenant1, domain='something.test.com') domain1.save() connection.set_schema_to_public() tenant2 = get_tenant_model()(schema_name='tenant2') tenant2.save() domain2 = get_tenant_domain_model()(tenant=tenant2, domain='example.com') domain2.save() # go to tenant1's path connection.set_tenant(tenant1) # add some data, 2 DummyModels for tenant1 DummyModel(name="Schemas are").save() DummyModel(name="awesome!").save() # switch temporarily to tenant2's path with tenant_context(tenant2): # add some data, 3 DummyModels for tenant2 DummyModel(name="Man,").save() DummyModel(name="testing").save() DummyModel(name="is great!").save() # we should be back to tenant1's path, test what we have self.assertEqual(2, DummyModel.objects.count()) # switch back to tenant2's path with tenant_context(tenant2): self.assertEqual(3, DummyModel.objects.count())
def test_files_are_saved_under_subdirectories_per_tenant(self): storage = TenantFileSystemStorage() connection.set_schema_to_public() tenant1 = utils.get_tenant_model()(schema_name='tenant1') tenant1.save() domain1 = utils.get_tenant_domain_model()(tenant=tenant1, domain='something.test.com') domain1.save() connection.set_schema_to_public() tenant2 = utils.get_tenant_model()(schema_name='tenant2') tenant2.save() domain2 = utils.get_tenant_domain_model()(tenant=tenant2, domain='example.com') domain2.save() # this file should be saved on the public schema public_file_name = storage.save('hello_world.txt', ContentFile('Hello World')) public_os_path = storage.path(public_file_name) public_url = storage.url(public_file_name) # switch to tenant1 with utils.tenant_context(tenant1): t1_file_name = storage.save('hello_from_1.txt', ContentFile('Hello T1')) t1_os_path = storage.path(t1_file_name) t1_url = storage.url(t1_file_name) # switch to tenant2 with utils.tenant_context(tenant2): t2_file_name = storage.save('hello_from_2.txt', ContentFile('Hello T2')) t2_os_path = storage.path(t2_file_name) t2_url = storage.url(t2_file_name) # assert the paths are correct self.assertTrue(public_os_path.endswith('apps_dir/media/public/%s' % public_file_name)) self.assertTrue(t1_os_path.endswith('apps_dir/media/tenant1/%s' % t1_file_name)) self.assertTrue(t2_os_path.endswith('apps_dir/media/tenant2/%s' % t2_file_name)) # assert urls are correct self.assertEqual(public_url, '/media/public/%s' % public_file_name) self.assertEqual(t1_url, '/media/tenant1/%s' % t1_file_name) self.assertEqual(t2_url, '/media/tenant2/%s' % t2_file_name) # assert contents are correct with open(public_os_path, 'r') as f: self.assertEqual(f.read(), 'Hello World') with open(t1_os_path, 'r') as f: self.assertEqual(f.read(), 'Hello T1') with open(t2_os_path, 'r') as f: self.assertEqual(f.read(), 'Hello T2')
def test_sync_tenant(self): """ When editing an existing tenant, all data should be kept. """ tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='something.test.com') domain.save() # go to tenant's path connection.set_tenant(tenant) # add some data DummyModel(name="Schemas are").save() DummyModel(name="awesome!").save() # edit tenant connection.set_schema_to_public() tenant.domain_urls = ['example.com'] tenant.save() connection.set_tenant(tenant) # test if data is still there self.assertEquals(DummyModel.objects.count(), 2) self.created = [domain, tenant]
def test_tenant_apps_and_shared_apps_can_have_the_same_apps(self): """ Tests that both SHARED_APPS and TENANT_APPS can have apps in common. In this case they should get synced to both tenant and public schemas. """ settings.SHARED_APPS = ('django_tenants', # 2 tables 'customers', 'django.contrib.auth', # 6 tables 'django.contrib.contenttypes', # 1 table 'django.contrib.sessions', ) # 1 table settings.TENANT_APPS = ('django.contrib.sessions', ) # 1 table settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS self.sync_shared() tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='arbitrary.test.com') domain.save() shared_tables = self.get_tables_list_in_schema(get_public_schema_name()) tenant_tables = self.get_tables_list_in_schema(tenant.schema_name) self.assertEqual(2+6+1+1+self.MIGRATION_TABLE_SIZE, len(shared_tables)) self.assertIn('django_session', shared_tables) self.assertEqual(1+self.MIGRATION_TABLE_SIZE, len(tenant_tables)) self.assertIn('django_session', tenant_tables)
def store_tenant_domain(self, **fields): try: domain = get_tenant_domain_model().objects.create(**fields) domain.save() except exceptions.ValidationError as e: self.stderr.write("Error: %s" % '; '.join(e.messages)) return None except IntegrityError: return None
def setUp(self): super(RoutesTestCase, self).setUp() self.factory = RequestFactory() self.tm = TenantMiddleware() self.tenant_domain = 'tenant.test.com' self.tenant = get_tenant_model()(schema_name='test') self.tenant.save() self.domain = get_tenant_domain_model()(tenant=self.tenant, domain=self.tenant_domain) self.domain.save()
def test_tenant_schema_is_created(self): """ When saving a tenant, it's schema should be created. """ tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='something.test.com') domain.save() self.assertTrue(schema_exists(tenant.schema_name))
def setup_test_tenant_and_domain(cls): cls.tenant = get_tenant_model()(schema_name=cls.get_test_schema_name()) cls.setup_tenant(cls.tenant) cls.tenant.save(verbosity=cls.get_verbosity()) # Set up domain tenant_domain = cls.get_test_tenant_domain() cls.domain = get_tenant_domain_model()(tenant=cls.tenant, domain=tenant_domain) cls.setup_domain(cls.domain) cls.domain.save() cls.use_new_tenant()
def setUp(self): self.sync_shared() self.tenant = get_tenant_model()(schema_name='test') self.tenant.save(verbosity=0) # todo: is there any way to get the verbosity from the test command here? # Set up domain tenant_domain = 'tenant.test.com' self.domain = get_tenant_domain_model()(tenant=self.tenant, domain=tenant_domain) self.domain.save() connection.set_tenant(self.tenant)
def setUpClass(cls): cls.sync_shared() cls.tenant = get_tenant_model()(schema_name='test') cls.tenant.save(verbosity=0) # todo: is there any way to get the verbosity from the test command here? # Set up domain tenant_domain = 'tenant.test.com' domain = get_tenant_domain_model()(domain=tenant_domain, tenant=cls.tenant) domain.save() connection.set_tenant(cls.tenant)
def process_request(self, request): connection.set_schema_to_public() hostname_without_port = remove_www_and_dev(request.get_host().split(':')[0]) try: domain = get_tenant_domain_model().objects.select_related('tenant').get(domain=hostname_without_port) request.tenant = domain.tenant except utils.DatabaseError: request.urlconf = settings.PUBLIC_SCHEMA_URLCONF return except get_tenant_domain_model().DoesNotExist: if hostname_without_port in ("127.0.0.1", "localhost"): request.urlconf = settings.PUBLIC_SCHEMA_URLCONF return else: raise Http404 connection.set_tenant(request.tenant) ContentType.objects.clear_cache() if hasattr(settings, 'PUBLIC_SCHEMA_URLCONF') and request.tenant.schema_name == get_public_schema_name(): request.urlconf = settings.PUBLIC_SCHEMA_URLCONF
def setUpClass(cls): super(RoutesTestCase, cls).setUpClass() settings.SHARED_APPS = ('django_tenants', 'customers') settings.TENANT_APPS = ('dts_test_app', 'django.contrib.contenttypes', 'django.contrib.auth', ) settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS cls.sync_shared() cls.public_tenant = get_tenant_model()(schema_name=get_public_schema_name()) cls.public_tenant.save() cls.public_domain = get_tenant_domain_model()(domain='test.com', tenant=cls.public_tenant) cls.public_domain.save()
def setUpClass(cls): cls.sync_shared() cls.add_allowed_test_domain() cls.tenant = get_tenant_model()(schema_name=cls.get_test_schema_name()) cls.setup_tenant(cls.tenant) cls.tenant.save(verbosity=cls.get_verbosity()) # Set up domain tenant_domain = cls.get_test_tenant_domain() cls.domain = get_tenant_domain_model()(tenant=cls.tenant, domain=tenant_domain) cls.setup_domain(cls.domain) cls.domain.save() connection.set_tenant(cls.tenant)
def test_non_auto_sync_tenant(self): """ When saving a tenant that has the flag auto_create_schema as False, the schema should not be created when saving the tenant. """ tenant = get_tenant_model()(schema_name='test') tenant.auto_create_schema = False tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='something.test.com') domain.save() self.assertFalse(schema_exists(tenant.schema_name))
def test_switching_tenant_without_previous_tenant(self): tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='something.test.com') domain.save() connection.tenant = None with tenant_context(tenant): DummyModel(name="No exception please").save() connection.tenant = None with schema_context(tenant.schema_name): DummyModel(name="Survived it!").save()
def setUpClass(cls): super(TenantDataAndSettingsTest, cls).setUpClass() settings.SHARED_APPS = ('django_tenants', 'customers') settings.TENANT_APPS = ('dts_test_app', 'django.contrib.contenttypes', 'django.contrib.auth', ) settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS cls.sync_shared() tenant = get_tenant_model()(schema_name=get_public_schema_name()) tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='test.com') domain.save()
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. """ tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='something.test.com') domain.save() shared_tables = self.get_tables_list_in_schema(get_public_schema_name()) tenant_tables = self.get_tables_list_in_schema(tenant.schema_name) self.assertEqual(2+1 + self.MIGRATION_TABLE_SIZE, len(shared_tables)) self.assertIn('django_session', tenant_tables) self.assertEqual(1+self.MIGRATION_TABLE_SIZE, len(tenant_tables)) self.assertIn('django_session', tenant_tables)
def test_tenant_apps_and_shared_apps_can_have_the_same_apps(self): """ Tests that both SHARED_APPS and TENANT_APPS can have apps in common. In this case they should get synced to both tenant and public schemas. """ tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='arbitrary.test.com') domain.save() shared_tables = self.get_tables_list_in_schema(get_public_schema_name()) tenant_tables = self.get_tables_list_in_schema(tenant.schema_name) self.assertEqual(2+6+1+1+self.MIGRATION_TABLE_SIZE, len(shared_tables)) self.assertIn('django_session', shared_tables) self.assertEqual(1+self.MIGRATION_TABLE_SIZE, len(tenant_tables)) self.assertIn('django_session', tenant_tables)
def process_request(self, request): # 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) domain_model = get_tenant_domain_model() try: domain = self.get_domain(domain_model, hostname) tenant = domain.tenant if not tenant: raise self.TENANT_NOT_FOUND_EXCEPTION('No tenant for hostname "%s"' % hostname) except domain_model.DoesNotExist: raise self.DOMAIN_NOT_FOUND_EXCEPTION('No domain for hostname "%s"' % hostname) # domain.tenant.domain_url = hostname # domain.tenant.current_domain = domain # request.tenant = domain.tenant # request.tenant_current_domain = domain # set 'current_domain' in 'tenant' to use across your application, e.g: # ... # from django.db import connection # if connection.tenant.current_domain.id == any_id: # do ... # ... tenant.current_domain = domain tenant.domain_url = hostname request.tenant = tenant request.tenant_current_domain = domain 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. ContentType.objects.clear_cache() # 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
def test_tenant_apps_does_not_sync_shared_apps(self): """ Tests that if an app is in TENANT_APPS, it does not get synced to the public schema. """ tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='arbitrary.test.com') domain.save() tenant_tables = self.get_tables_list_in_schema(tenant.schema_name) self.assertEqual(1+self.MIGRATION_TABLE_SIZE, len(tenant_tables)) self.assertIn('django_session', tenant_tables) connection.set_schema_to_public() domain.delete() tenant.delete(force_drop=True)
def setUpClass(cls): # This replaces TestCase.setUpClass so that we can do some setup in # different schemas. # It also drops the check whether the database supports transactions. cls.sync_shared() EmailTemplate.objects.get_or_create(name='audit/staff_member/invite') EmailTemplate.objects.get_or_create(name='audit/engagement/submit_to_auditor') TenantModel = get_tenant_model() try: cls.tenant = TenantModel.objects.get(schema_name=SCHEMA_NAME) except TenantModel.DoesNotExist: cls.tenant = TenantModel(schema_name=SCHEMA_NAME) cls.tenant.save(verbosity=0) cls.tenant.business_area_code = 'ZZZ' # Make sure country has a short code, it affects some results cls.tenant.country_short_code = 'TST' cls.tenant.save(verbosity=0) cls.domain = get_tenant_domain_model().objects.get_or_create(domain=TENANT_DOMAIN, tenant=cls.tenant) try: cls.tenant.counters except ObjectDoesNotExist: WorkspaceCounter.objects.create(workspace=cls.tenant) cls.cls_atomics = cls._enter_atomics() # Load fixtures for shared schema cls._load_fixtures() connection.set_tenant(cls.tenant) # Load fixtures for tenant schema cls._load_fixtures() try: cls.setUpTestData() except Exception: cls._rollback_atomics(cls.cls_atomics) raise
def test_tenant_apps_and_shared_apps_can_have_the_same_apps(self): """ Tests that both SHARED_APPS and TENANT_APPS can have apps in common. In this case they should get synced to both tenant and public schemas. """ tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='arbitrary.test.com') domain.save() shared_tables = self.get_tables_list_in_schema( get_public_schema_name()) tenant_tables = self.get_tables_list_in_schema(tenant.schema_name) self.assertEqual(2 + 6 + 1 + 1 + self.MIGRATION_TABLE_SIZE, len(shared_tables)) self.assertIn('django_session', shared_tables) self.assertEqual(1 + self.MIGRATION_TABLE_SIZE, len(tenant_tables)) self.assertIn('django_session', tenant_tables)
def setUpClass(cls): super().setUpClass() # reset owner for cls.tenant b/c we'd like for testing all tenants # to be owned by the same user cls.tenant.owner = cls.tester cls.tenant.name = 'For testing purposes' cls.tenant.save() # we give this 2nd user access to the tenant cls.tester2 = UserFactory() with utils.schema_context('public'): cls.tenant2 = utils.get_tenant_model()( schema_name='my_other_tenant', owner=cls.tester) cls.tenant2.save() cls.domain2 = utils.get_tenant_domain_model()( tenant=cls.tenant2, domain='other.example.com') cls.domain2.save()
def process_request(self, request): # 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) domain_model = get_tenant_domain_model() try: tenant = self.get_tenant(domain_model, hostname) except domain_model.DoesNotExist: raise self.TENANT_NOT_FOUND_EXCEPTION('No tenant for hostname "%s"' % hostname) tenant.domain_url = hostname request.tenant = tenant connection.set_tenant(request.tenant) # 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
def process_request(self, request): # 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) domain_model = get_tenant_domain_model() try: tenant = self.get_tenant(domain_model, hostname) except domain_model.DoesNotExist: raise self.TENANT_NOT_FOUND_EXCEPTION('No tenant for hostname "%s"' % hostname) tenant.domain_url = hostname request.tenant = tenant connection.set_tenant(request.tenant) # 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 set_urlconf(request.urlconf)
def test_tenant_apps_does_not_sync_shared_apps(self): """ Tests that if an app is in TENANT_APPS, it does not get synced to the public schema. """ settings.SHARED_APPS = ('django_tenants', 'customers', 'django.contrib.auth', 'django.contrib.contenttypes', ) settings.TENANT_APPS = ('django.contrib.sessions', ) # 1 table settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS self.sync_shared() tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='arbitrary.test.com') domain.save() tenant_tables = self.get_tables_list_in_schema(tenant.schema_name) self.assertEqual(1+self.MIGRATION_TABLE_SIZE, len(tenant_tables)) self.assertIn('django_session', tenant_tables)
def process_request(self, request): connection.set_schema_to_public() hostname = self.hostname_from_request(request) domain_model = get_tenant_domain_model() try: tenant = self.get_tenant(domain_model, hostname) request.tenant = tenant connection.set_tenant(request.tenant) except (domain_model.DoesNotExist, AssertionError): request.urlconf = settings.PUBLIC_SCHEMA_URLCONF set_urlconf(request.urlconf) return if hasattr( settings, 'PUBLIC_SCHEMA_URLCONF' ) and request.tenant.schema_name == get_public_schema_name(): request.urlconf = settings.PUBLIC_SCHEMA_URLCONF set_urlconf(request.urlconf)
def test_signal_on_tenant_create(self): """ Since migrate gets called on creating of a tenant, check the signal gets sent. """ executor = get_executor() tenant = get_tenant_model()(schema_name='test') with catch_signal(schema_migrated) as handler: tenant.save() if executor == 'simple': handler.assert_called_once_with(schema_name='test', sender=mock.ANY, signal=schema_migrated) elif executor == 'multiprocessing': # migrations run in a different process, therefore signal # will get sent in a different process as well handler.assert_not_called() domain = get_tenant_domain_model()(tenant=tenant, domain='something.test.com') domain.save() self.created = [domain, tenant]
def test_tenant_apps_does_not_sync_shared_apps(self): """ Tests that if an app is in TENANT_APPS, it does not get synced to the public schema. """ settings.SHARED_APPS = ( 'django_tenants', 'customers', 'django.contrib.auth', 'django.contrib.contenttypes', ) settings.TENANT_APPS = ('django.contrib.sessions', ) # 1 table settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS self.sync_shared() tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='arbitrary.test.com') domain.save() tenant_tables = self.get_tables_list_in_schema(tenant.schema_name) self.assertEqual(1 + self.MIGRATION_TABLE_SIZE, len(tenant_tables)) self.assertIn('django_session', tenant_tables)
def __call__(self, scope): if "headers" not in scope: raise ValueError( "MTSchemaMiddleware was passed a scope that did not have a headers key " "(make sure it is only passed HTTP or WebSocket connections)") for key, value in scope.get('headers', []): if key == b'host': hostname = value.decode('ascii').split(':')[0] from django_tenants.utils import get_tenant_domain_model domain_model = get_tenant_domain_model() domain = domain_model.objects.select_related('tenant')\ .get(domain=hostname) try: tenant = domain.tenant schema_name = tenant.schema_name except domain_model.DoesNotExist: raise Http404('No tenant for hostname "%s"' % hostname) break else: raise ValueError( "The headers key in the scope is invalid. " + "(make sure it is passed valid HTTP or WebSocket connections)") return self.inner( dict(scope, schema_name=schema_name, multitenant=True))
def test_tenant_schema_behaves_as_decorator(self): tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='something.test.com') domain.save() connection.tenant = None @tenant_context(tenant) def test_tenant_func(): DummyModel(name='No exception please').save() test_tenant_func() connection.tenant = None @schema_context(tenant.schema_name) def test_schema_func(): DummyModel(name='Survived it!').save() test_schema_func() self.created = [domain, tenant]
def setUp(self) -> None: self.factory = TenantRequestFactory(self.tenant) self.view = views.ListTenants.as_view() self.url = '/api/v2/internal/tenants/' self.user = CustUser.objects.create_user(username='******') # get_tenant_domain_model().objects.create( # domain='test.domain.url', tenant=self.tenant, is_primary=True # ) with schema_context(get_public_schema_name()): self.tenant1 = Tenant(name='TEST1', schema_name='test1') self.tenant1.auto_create_schema = False self.tenant2 = Tenant(name='TEST2', schema_name='test2') self.tenant2.auto_create_schema = False self.tenant3 = Tenant( name='all', schema_name=get_public_schema_name(), ) self.tenant3.auto_create_schema = False self.tenant1.save() self.tenant2.save() self.tenant3.save() get_tenant_domain_model().objects.create(domain='test1.domain.url', tenant=self.tenant1, is_primary=True) get_tenant_domain_model().objects.create(domain='test2.domain.url', tenant=self.tenant2, is_primary=True) get_tenant_domain_model().objects.create(domain='domain.url', tenant=self.tenant3, is_primary=True) self.supertenant_superuser = CustUser.objects.create_user( username='******', is_superuser=True) self.supertenant_user = CustUser.objects.create_user( username='******')
class Command(BaseCommand): help = 'Create a tenant' # Only use editable fields # noinspection PyProtectedMember tenant_fields = [ field for field in get_tenant_model()._meta.fields if field.editable and not field.primary_key ] # noinspection PyProtectedMember domain_fields = [ field for field in get_tenant_domain_model()._meta.fields if field.editable and not field.primary_key ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def add_arguments(self, parser): for field in self.tenant_fields: parser.add_argument('--%s' % field.attname, help='Specifies the %s for tenant.' % field.attname) for field in self.domain_fields: parser.add_argument( '--domain-%s' % field.attname, help="Specifies the %s for the tenant's domain." % field.attname) parser.add_argument('-s', action="store_true", help='Create a superuser afterwards.') def handle(self, *args, **options): tenant_data = {} for field in self.tenant_fields: input_value = options.get(field.attname, None) tenant_data[field.attname] = input_value domain_data = {} for field in self.domain_fields: input_value = options.get('domain_%s' % field.attname, None) domain_data[field.attname] = input_value while True: for field in self.tenant_fields: if tenant_data.get(field.attname, '') == '': input_msg = field.verbose_name default = field.get_default() if default: input_msg = "%s (leave blank to use '%s')" % ( input_msg, default) input_value = input(force_str( '%s: ' % input_msg)) or default tenant_data[field.attname] = input_value tenant = self.store_tenant(**tenant_data) if tenant is not None: break tenant_data = {} while True: domain_data['tenant_id'] = tenant.pk for field in self.domain_fields: if domain_data.get(field.attname, '') == '': input_msg = field.verbose_name default = field.get_default() if default: input_msg = "%s (leave blank to use '%s')" % ( input_msg, default) input_value = input(force_str( '%s: ' % input_msg)) or default domain_data[field.attname] = input_value domain = self.store_tenant_domain(**domain_data) if domain is not None: break domain_data = {} if options.get('s', None): self.stdout.write("Create superuser for %s" % tenant_data['schema_name']) call_command('create_tenant_superuser', schema_name=tenant_data['schema_name'], interactive=True) def store_tenant(self, **fields): try: tenant = get_tenant_model().objects.create(**fields) return tenant except exceptions.ValidationError as e: self.stderr.write("Error: %s" % '; '.join(e.messages)) return None except IntegrityError: return None def store_tenant_domain(self, **fields): try: domain = get_tenant_domain_model().objects.create(**fields) domain.save() return domain except exceptions.ValidationError as e: self.stderr.write("Error: %s" % '; '.join(e.messages)) return None except IntegrityError: return None
def test_files_are_saved_under_subdirectories_per_tenant(self): # Create new tenants tenant1 = utils.get_tenant_model()(schema_name="tenant1") tenant1.save() domain1 = utils.get_tenant_domain_model()( tenant=tenant1, domain="something.test.com" ) domain1.save() tenant2 = utils.get_tenant_model()(schema_name="tenant2") tenant2.save() domain2 = utils.get_tenant_domain_model()(tenant=tenant2, domain="example.com") domain2.save() connection.set_schema_to_public() # This file should be saved on the public schema public_file_name = self.storage.save("hello_world.txt", ContentFile("Hello World")) public_os_path = self.storage.path(public_file_name) public_url = self.storage.url(public_file_name) # Switch to tenant1 with utils.tenant_context(tenant1): storage = self.storage_class( location="{}/{}".format(self.temp_dir, connection.schema_name), base_url="/test_media_url/" ) t1_file_name = storage.save("hello_from_1.txt", ContentFile("Hello T1")) t1_os_path = storage.path(t1_file_name) t1_url = storage.url(t1_file_name) # Switch to tenant2 with utils.tenant_context(tenant2): storage = self.storage_class( location="{}/{}".format(self.temp_dir, connection.schema_name), base_url="/test_media_url/" ) t2_file_name = storage.save("hello_from_2.txt", ContentFile("Hello T2")) t2_os_path = storage.path(t2_file_name) t2_url = storage.url(t2_file_name) # Assert the paths are correct self.assertTrue( public_os_path.endswith("public/%s" % public_file_name) ) self.assertTrue(t1_os_path.endswith("tenant1/%s" % t1_file_name)) self.assertTrue(t2_os_path.endswith("tenant2/%s" % t2_file_name)) # Assert urls are correct self.assertEqual(public_url, "/test_media_url/public/%s" % public_file_name) self.assertEqual(t1_url, "/test_media_url/tenant1/%s" % t1_file_name) self.assertEqual(t2_url, "/test_media_url/tenant2/%s" % t2_file_name) # Assert contents are correct with open(public_os_path, "r") as f: self.assertEqual(f.read(), "Hello World") with open(t1_os_path, "r") as f: self.assertEqual(f.read(), "Hello T1") with open(t2_os_path, "r") as f: self.assertEqual(f.read(), "Hello T2")
class Command(BaseCommand): help = 'Clones a tenant' # Only use editable fields tenant_fields = [field for field in get_tenant_model()._meta.fields if field.editable and not field.primary_key] # noinspection PyProtectedMember domain_fields = [field for field in get_tenant_domain_model()._meta.fields if field.editable and not field.primary_key] def add_arguments(self, parser): parser.add_argument('--clone_from', help='Specifies which schema to clone.') parser.add_argument('--clone_tenant_fields', help='Clone the tenant fields.') parser.add_argument('--db_user', help='the user for the database the default is postgres') for field in self.tenant_fields: parser.add_argument('--%s' % field.attname, help='Specifies the %s for tenant.' % field.attname) for field in self.domain_fields: parser.add_argument('--domain-%s' % field.attname, help="Specifies the %s for the tenant's domain." % field.attname) parser.add_argument('-s', action="store_true", help='Create a superuser afterwards.') def _input(self, question): """Wrapper around 'input' for overriding while testing""" return input(question) def handle(self, *args, **options): tenant_model = get_tenant_model() all_tenants = tenant_model.objects.all() tenant_data = {} for field in self.tenant_fields: input_value = options.get(field.attname, None) tenant_data[field.attname] = input_value domain_data = {} for field in self.domain_fields: input_value = options.get('domain_%s' % field.attname, None) domain_data[field.attname] = input_value clone_schema_from = options.get('clone_from') while clone_schema_from == '' or clone_schema_from is None: while True: clone_schema_from = self._input("Clone Tenant Schema ('?' to list schemas): ") if clone_schema_from == '?': self.stdout.write('\n'.join(["%s" % t.schema_name for t in all_tenants])) else: break clone_tenant_fields = options.get('clone_tenant_fields') while clone_tenant_fields is None or clone_tenant_fields.lower() not in ['no', 'yes', 'true', 'false']: clone_tenant_fields = self._input("Clone Tenant tenant fields: ") if clone_tenant_fields.lower() in ['yes', 'true']: new_schema_name = options.get('schema_name') while new_schema_name == '' or new_schema_name is None: new_schema_name = self._input("New tenant schema name: ") tenant_data['schema_name'] = new_schema_name tenant = self.store_tenant(clone_schema_from=clone_schema_from, clone_tenant_fields=True, **tenant_data) else: while True: for field in self.tenant_fields: if tenant_data.get(field.attname, '') == '': input_msg = field.verbose_name default = field.get_default() if default: input_msg = "%s (leave blank to use '%s')" % (input_msg, default) input_value = self._input(force_str('%s: ' % input_msg)) or default tenant_data[field.attname] = input_value tenant = self.store_tenant(clone_schema_from=clone_schema_from, clone_tenant_fields=False, **tenant_data) if tenant is not None: break tenant_data = {} while True: domain_data['tenant_id'] = tenant.id for field in self.domain_fields: if domain_data.get(field.attname, '') == '': input_msg = field.verbose_name default = field.get_default() if default: input_msg = "%s (leave blank to use '%s')" % (input_msg, default) input_value = self._input(force_str('%s: ' % input_msg)) or default domain_data[field.attname] = input_value domain = self.store_tenant_domain(**domain_data) if domain is not None: break domain_data = {} def store_tenant(self, clone_schema_from, clone_tenant_fields, **fields): connection.set_schema_to_public() try: if clone_tenant_fields: tenant = get_tenant_model().objects.get(schema_name=clone_schema_from) tenant.pk = None tenant.schema_name = fields['schema_name'] else: tenant = get_tenant_model()(**fields) tenant.auto_create_schema = False tenant.save() clone_schema = CloneSchema() clone_schema.clone_schema(clone_schema_from, tenant.schema_name, set_connection=False) return tenant except exceptions.ValidationError as e: self.stderr.write("Error: %s" % '; '.join(e.messages)) return None except IntegrityError as e: self.stderr.write("Error: " + str(e)) return None def store_tenant_domain(self, **fields): try: domain = get_tenant_domain_model().objects.create(**fields) domain.save() return domain except exceptions.ValidationError as e: self.stderr.write("Error: %s" % '; '.join(e.messages)) return None except IntegrityError as e: self.stderr.write("Error: " + str(e)) return None
def test_files_are_saved_under_subdirectories_per_tenant(self): storage = TenantFileSystemStorage() connection.set_schema_to_public() tenant1 = utils.get_tenant_model()(schema_name='tenant1') tenant1.save() domain1 = utils.get_tenant_domain_model()(tenant=tenant1, domain='something.test.com') domain1.save() connection.set_schema_to_public() tenant2 = utils.get_tenant_model()(schema_name='tenant2') tenant2.save() domain2 = utils.get_tenant_domain_model()(tenant=tenant2, domain='example.com') domain2.save() # this file should be saved on the public schema public_file_name = storage.save('hello_world.txt', ContentFile('Hello World')) public_os_path = storage.path(public_file_name) public_url = storage.url(public_file_name) # switch to tenant1 with utils.tenant_context(tenant1): t1_file_name = storage.save('hello_from_1.txt', ContentFile('Hello T1')) t1_os_path = storage.path(t1_file_name) t1_url = storage.url(t1_file_name) # switch to tenant2 with utils.tenant_context(tenant2): t2_file_name = storage.save('hello_from_2.txt', ContentFile('Hello T2')) t2_os_path = storage.path(t2_file_name) t2_url = storage.url(t2_file_name) # assert the paths are correct self.assertTrue( public_os_path.endswith('apps_dir/media/public/%s' % public_file_name)) self.assertTrue( t1_os_path.endswith('apps_dir/media/tenant1/%s' % t1_file_name)) self.assertTrue( t2_os_path.endswith('apps_dir/media/tenant2/%s' % t2_file_name)) # assert urls are correct self.assertEqual(public_url, '/media/public/%s' % public_file_name) self.assertEqual(t1_url, '/media/tenant1/%s' % t1_file_name) self.assertEqual(t2_url, '/media/tenant2/%s' % t2_file_name) # assert contents are correct with open(public_os_path, 'r') as f: self.assertEqual(f.read(), 'Hello World') with open(t1_os_path, 'r') as f: self.assertEqual(f.read(), 'Hello T1') with open(t2_os_path, 'r') as f: self.assertEqual(f.read(), 'Hello T2')
class Command(BaseCommand): help = 'Clones a tenant' # Only use editable fields tenant_fields = [ field for field in get_tenant_model()._meta.fields if field.editable and not field.primary_key ] domain_fields = [ field for field in get_tenant_domain_model()._meta.fields if field.editable and not field.primary_key ] def __init__(self, *args, **kwargs): super(Command, self).__init__(*args, **kwargs) self.option_list = () if django.VERSION <= (1, 10, 0): self.option_list = BaseCommand.option_list self.option_list += (make_option( '--clone_from', help='Specifies which schema to clone.'), ) for field in self.tenant_fields: self.option_list += (make_option( '--%s' % field.name, help='Specifies the %s for tenant.' % field.name), ) for field in self.domain_fields: self.option_list += (make_option( '--%s' % field.name, help="Specifies the %s for the tenant's domain." % field.name), ) def handle(self, *args, **options): tenant_data = {} for field in self.tenant_fields: input_value = options.get(field.name, None) tenant_data[field.name] = input_value domain_data = {} for field in self.domain_fields: input_value = options.get(field.name, None) domain_data[field.name] = input_value clone_schema_from = options.get('clone_from') while clone_schema_from == '' or clone_schema_from is None: clone_schema_from = input(force_str('Clone schema from: ')) tenant = None while True: for field in self.tenant_fields: if tenant_data.get(field.name, '') == '': input_msg = field.verbose_name default = field.get_default() if default: input_msg = "%s (leave blank to use '%s')" % ( input_msg, default) input_value = input(force_str( '%s: ' % input_msg)) or default tenant_data[field.name] = input_value tenant = self.store_tenant(clone_schema_from, **tenant_data) if tenant is not None: break tenant_data = {} while True: domain_data['tenant'] = tenant for field in self.domain_fields: if domain_data.get(field.name, '') == '': input_msg = field.verbose_name default = field.get_default() if default: input_msg = "%s (leave blank to use '%s')" % ( input_msg, default) input_value = input(force_str( '%s: ' % input_msg)) or default domain_data[field.name] = input_value domain = self.store_tenant_domain(**domain_data) if domain is not None: break domain_data = {} def store_tenant(self, clone_schema_from, **fields): connection.set_schema_to_public() cursor = connection.cursor() try: tenant = get_tenant_model()(**fields) tenant.auto_create_schema = False tenant.save() clone_schema = CloneSchema(cursor) clone_schema.clone(clone_schema_from, tenant.schema_name) return tenant except exceptions.ValidationError as e: self.stderr.write("Error: %s" % '; '.join(e.messages)) return None except IntegrityError: return None def store_tenant_domain(self, **fields): try: domain = get_tenant_domain_model().objects.create(**fields) domain.save() return domain except exceptions.ValidationError as e: self.stderr.write("Error: %s" % '; '.join(e.messages)) return None except IntegrityError: return None
def process_request(self, request): # 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) print('IN PROCESS REQUEST - METHOD = ', request.method) print('IN PROCESS REQUEST - META = ', request.META) print('IN PROCESS REQUEST - HEADERS = ', request.headers) tenant_id = request.headers.get('x-tenant-id') print('X TENTANT = ', tenant_id) domain_model = get_tenant_domain_model() if request.method != 'OPTIONS' and request.method != 'GET': try: tenant_id = request.headers.get('X-TENANT-ID') print('TENANT ID = ', tenant_id) tenant_model = get_tenant_model() if tenant_model.objects.filter(tenant_id=tenant_id).exists(): tenant = tenant_model.objects.get(tenant_id=tenant_id) print('TENANT = ', tenant.__dict__) tenant.domain_url = hostname request.tenant = tenant connection.set_tenant(request.tenant) else: print('NO TENANT') return HttpResponse('Unauthorized - Invalid Tenant Id', status=401) except domain_model.DoesNotExist: raise self.TENANT_NOT_FOUND_EXCEPTION('No tenant for id "%s"' % tenant_id) # if request.method != 'OPTIONS': # try: # tenant_id = request.headers.get('X-TENANT-ID') # print('TENANT ID = ', tenant_id) # tenant_model = get_tenant_model() # tenant = tenant_model.objects.get(tenant_id=tenant_id) # print('TENANT = ', tenant.__dict__) # except domain_model.DoesNotExist: # raise self.TENANT_NOT_FOUND_EXCEPTION( # 'No tenant for id "%s"' % tenant_id) # tenant.domain_url = hostname # request.tenant = tenant # connection.set_tenant(request.tenant) # ** OLD DOMAIN METHOD*** # try: # tenant = self.get_tenant(domain_model, hostname) # print('TENANT = ', tenant) # except domain_model.DoesNotExist: # raise self.TENANT_NOT_FOUND_EXCEPTION('No tenant for hostname "%s"' % hostname) # tenant.domain_url = hostname # request.tenant = tenant # connection.set_tenant(request.tenant) # *************** # 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 set_urlconf(request.urlconf)