Skip to content

Commit

Permalink
Merge 18eb5b6 into 510c7d0
Browse files Browse the repository at this point in the history
  • Loading branch information
lorinkoz committed Apr 23, 2020
2 parents 510c7d0 + 18eb5b6 commit 7896686
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 34 deletions.
4 changes: 4 additions & 0 deletions django_pgschemas/apps.py
Expand Up @@ -27,6 +27,8 @@ def _check_public_schema(self):
raise ImproperlyConfigured("TENANTS['public'] cannot contain a 'WS_URLCONF' key.")
if "DOMAINS" in settings.TENANTS["public"]:
raise ImproperlyConfigured("TENANTS['public'] cannot contain a 'DOMAINS' key.")
if "FALLBACK_DOMAINS" in settings.TENANTS["public"]:
raise ImproperlyConfigured("TENANTS['public'] cannot contain a 'FALLBACK_DOMAINS' key.")

def _check_default_schemas(self):
if not isinstance(settings.TENANTS.get("default"), dict):
Expand All @@ -35,6 +37,8 @@ def _check_default_schemas(self):
raise ImproperlyConfigured("TENANTS['default'] must contain a 'URLCONF' key.")
if "DOMAINS" in settings.TENANTS["default"]:
raise ImproperlyConfigured("TENANTS['default'] cannot contain a 'DOMAINS' key.")
if "FALLBACK_DOMAINS" in settings.TENANTS["default"]:
raise ImproperlyConfigured("TENANTS['default'] cannot contain a 'FALLBACK_DOMAINS' key.")
if (
"CLONE_REFERENCE" in settings.TENANTS["default"]
and settings.TENANTS["default"]["CLONE_REFERENCE"] in settings.TENANTS
Expand Down
43 changes: 28 additions & 15 deletions django_pgschemas/middleware.py
Expand Up @@ -46,20 +46,33 @@ def __call__(self, request):
try:
domain = DomainModel.objects.select_related("tenant").get(domain=hostname, folder="")
except DomainModel.DoesNotExist:
raise self.TENANT_NOT_FOUND_EXCEPTION("No tenant for hostname '%s'" % hostname)
tenant = domain.tenant
tenant.domain_url = hostname
tenant.folder = None
request.strip_tenant_from_path = lambda x: x
if prefix and domain.folder == prefix:
tenant.folder = prefix
request.strip_tenant_from_path = lambda x: re.sub(r"^/{}/".format(prefix), "/", x)
clear_url_caches() # Required to remove previous tenant prefix from cache (#8)
domain = None
if domain:
tenant = domain.tenant
tenant.domain_url = hostname
tenant.folder = None
request.strip_tenant_from_path = lambda x: x
if prefix and domain.folder == prefix:
tenant.folder = prefix
request.strip_tenant_from_path = lambda x: re.sub(r"^/{}/".format(prefix), "/", x)
clear_url_caches() # Required to remove previous tenant prefix from cache (#8)

if tenant:
request.tenant = tenant
urlconf = get_urlconf_from_schema(tenant)
request.urlconf = urlconf
set_urlconf(urlconf)
connection.set_schema(tenant)
# Checking fallback domains
if not tenant:
for schema, data in settings.TENANTS.items():
if schema in ["public", "default"]:
continue
if hostname in data.get("FALLBACK_DOMAINS", []):
tenant = SchemaDescriptor.create(schema_name=schema, domain_url=hostname)
break

# No tenant found from domain / folder
if not tenant:
raise self.TENANT_NOT_FOUND_EXCEPTION("No tenant for hostname '%s'" % hostname)

request.tenant = tenant
urlconf = get_urlconf_from_schema(tenant)
request.urlconf = urlconf
set_urlconf(urlconf)
connection.set_schema(tenant)
return self.get_response(request)
6 changes: 3 additions & 3 deletions django_pgschemas/urlresolvers.py
Expand Up @@ -116,9 +116,9 @@ def get_urlconf_from_schema(schema):
if schema_name in ["public", "default"]:
continue
if schema.domain_url in data["DOMAINS"]:
if "URLCONF" in data:
return data["URLCONF"]
return None
return data["URLCONF"]
if schema.domain_url in data.get("FALLBACK_DOMAINS", []):
return data["URLCONF"]
return None

# Checking for dynamic tenants
Expand Down
73 changes: 73 additions & 0 deletions docs/advanced.rst
Expand Up @@ -58,6 +58,79 @@ that it is kept up to date for future tenant creation.
corresponding database entry for it. It's a special case of a static
tenant, and it cannot be routed.

Fallback domains
----------------

If there is only one domain available, and no possibility to use subdomain
routing, the URLs for accessing your different tenants might look like::

mydomain.com -> main site
mydomain.com/customer1 -> customer 1
mydomain.com/customer2 -> customer 2

In this case, due to the order in which domains are tested, it is not possible
to put ``mydomain.com`` as domain for the main tenant without blocking all
dynamic schemas from getting routed. When
``django_pgschemas.middleware.TenantMiddleware`` is checking which tenant to
route from the incoming domain, it checks for static tenants first, then for
dynamic tenants. If ``mydomain.com`` is used for the main tenant (which is
static), then URLs like ``mydomain.com/customer1/some/url/`` will match the
main tenant always.

For a case like this, we provide a setting called ``FALLBACK_DOMAINS``. If no
tenant is found for an incoming combination of domain and subfolder, then,
static tenants are checked again for the fallback domains.

Something like this would be the proper configuration for the present case:

.. code-block:: python
TENANTS = {
"public": {
"APPS": [
"django.contrib.contenttypes",
"django.contrib.staticfiles",
# ...
"django_pgschemas",
"shared_app",
# ...
],
"TENANT_MODEL": "shared_app.Client",
"DOMAIN_MODEL": "shared_app.Domain",
},
"main": {
"APPS": [
"django.contrib.auth",
"django.contrib.sessions",
# ...
"main_app",
],
"DOMAINS": [], # <--- No domain here
"FALLBACK_DOMAINS": ["mydomain.com"], # <--- This is checked last
"URLCONF": "main_app.urls",
},
"default": {
"APPS": [
"django.contrib.auth",
"django.contrib.sessions",
# ...
"tenant_app",
# ...
],
"URLCONF": "tenant_app.urls",
}
}
This example assumes that dynamic tenants will get their domains set to
``mydomain.com`` with a tenant specific subfolder, like ``client1`` or
``client2``.

Here, an incoming request for ``mydomain.com/client1/some/url/`` will fail for
the main tenant, then match against an existing dynamic tenant. On the other
hand, an incoming request for ``mydomain.com/some/url/`` will fail for all
static tenants, then fail for all dynamic tenants, and will finally match
against the fallback domains of the main tenant.

Management commands
-------------------

Expand Down
1 change: 1 addition & 0 deletions dpgs_sandbox/settings.py
Expand Up @@ -38,6 +38,7 @@
"URLCONF": "app_main.urls",
"WS_URLCONF": "app_main.ws_urls",
"DOMAINS": ["test.com"],
"FALLBACK_DOMAINS": ["everyone.test.com"],
},
"blog": {
"APPS": ["shared_common", "app_blog", "django.contrib.sessions"],
Expand Down
5 changes: 5 additions & 0 deletions dpgs_sandbox/tests/test_apps.py
Expand Up @@ -91,6 +91,11 @@ def test_domains_on_default(self):
with self.assertRaises(ImproperlyConfigured):
self.app_config._check_default_schemas()

@override_settings(TENANTS={"default": {**settings_default, "FALLBACK_DOMAINS": ""}})
def test_fallback_domains_on_default(self):
with self.assertRaises(ImproperlyConfigured):
self.app_config._check_default_schemas()

def test_repeated_clone_reference(self):
with override_settings(TENANTS={"public": {}, "default": {**settings_default, "CLONE_REFERENCE": "public"}}):
with self.assertRaises(ImproperlyConfigured):
Expand Down
55 changes: 39 additions & 16 deletions dpgs_sandbox/tests/test_middleware.py
Expand Up @@ -33,17 +33,18 @@ def fake_get_response(request):
DomainModel(domain="everyone.test.com", folder="tenant1", tenant=tenant1).save()
DomainModel(domain="tenant2.test.com", tenant=tenant2).save()
DomainModel(domain="everyone.test.com", folder="tenant2", tenant=tenant2).save()
DomainModel(domain="special.test.com", folder="tenant2", tenant=tenant2).save()

def test_static_tenants(self):
# www
def test_static_tenants_www(self):
request = self.factory.get("/", HTTP_HOST="www.test.com")
modified_request = self.middleware(request)
self.assertTrue(modified_request.tenant)
self.assertEqual(modified_request.tenant.schema_name, "www")
self.assertEqual(modified_request.tenant.domain_url, "test.com")
self.assertEqual(modified_request.tenant.folder, None)
self.assertEqual(modified_request.urlconf, "app_main.urls")
# blog

def test_static_tenants_blog(self):
request = self.factory.get("/some/random/url/", HTTP_HOST="blog.test.com")
modified_request = self.middleware(request)
self.assertTrue(modified_request.tenant)
Expand All @@ -52,58 +53,80 @@ def test_static_tenants(self):
self.assertEqual(modified_request.tenant.folder, None)
self.assertEqual(modified_request.urlconf, "app_blog.urls")

def test_dynamic_tenants(self):
# tenant1 by domain
def test_dynamic_tenants_tenant1_domain(self):
request = self.factory.get("/tenant2/", HTTP_HOST="tenant1.test.com")
modified_request = self.middleware(request)
self.assertTrue(modified_request.tenant)
self.assertEqual(modified_request.tenant.schema_name, "tenant1")
self.assertEqual(modified_request.tenant.domain_url, "tenant1.test.com")
self.assertEqual(modified_request.tenant.folder, None)
self.assertEqual(modified_request.urlconf, "app_tenants.urls")
# tenant2 by domain

def test_dynamic_tenants_tenant2_domain(self):
request = self.factory.get("/tenant1/", HTTP_HOST="tenant2.test.com")
modified_request = self.middleware(request)
self.assertTrue(modified_request.tenant)
self.assertEqual(modified_request.tenant.schema_name, "tenant2")
self.assertEqual(modified_request.tenant.domain_url, "tenant2.test.com")
self.assertEqual(modified_request.tenant.folder, None)
self.assertEqual(modified_request.urlconf, "app_tenants.urls")
# tenant1 by folder

def test_dynamic_tenants_tenant1_folder(self):
request = self.factory.get("/tenant1/some/random/url/", HTTP_HOST="everyone.test.com")
modified_request = self.middleware(request)
self.assertTrue(modified_request.tenant)
self.assertEqual(modified_request.tenant.schema_name, "tenant1")
self.assertEqual(modified_request.tenant.domain_url, "everyone.test.com")
self.assertEqual(modified_request.tenant.folder, "tenant1")
self.assertEqual(modified_request.urlconf, "app_tenants.urls_dynamically_tenant_prefixed")
# tenant2 by folder

def test_dynamic_tenants_tenant2_folder(self):
request = self.factory.get("/tenant2/some/random/url/", HTTP_HOST="everyone.test.com")
modified_request = self.middleware(request)
self.assertTrue(modified_request.tenant)
self.assertEqual(modified_request.tenant.schema_name, "tenant2")
self.assertEqual(modified_request.tenant.domain_url, "everyone.test.com")
self.assertEqual(modified_request.tenant.folder, "tenant2")
self.assertEqual(modified_request.urlconf, "app_tenants.urls_dynamically_tenant_prefixed")
# tenant1 by folder with short path

def test_dynamic_tenants_tenant1_folder_short(self):
request = self.factory.get("/tenant1/", HTTP_HOST="everyone.test.com")
modified_request = self.middleware(request)
self.assertTrue(modified_request.tenant)
self.assertEqual(modified_request.tenant.schema_name, "tenant1")
self.assertEqual(modified_request.tenant.domain_url, "everyone.test.com")
self.assertEqual(modified_request.tenant.folder, "tenant1")
self.assertEqual(modified_request.urlconf, "app_tenants.urls_dynamically_tenant_prefixed")
# make sure on-the-fly urlconf can be imported

def test_dynamic_module_can_be_imported(self):
request = self.factory.get("/tenant1/", HTTP_HOST="everyone.test.com")
modified_request = self.middleware(request)
import_module(modified_request.urlconf)
# wrong subdomain

def test_wrong_subdomain(self):
request = self.factory.get("/some/random/url/", HTTP_HOST="bad-domain.test.com")
with self.assertRaises(Http404):
self.middleware(request)
# wrong folder
request = self.factory.get("/wrong-tenant/", HTTP_HOST="everyone.test.com")

def test_no_folder(self):
request = self.factory.get("/", HTTP_HOST="special.test.com")
with self.assertRaises(Http404):
self.middleware(request)
# no folder

def test_fallback_domain_root(self):
request = self.factory.get("/", HTTP_HOST="everyone.test.com")
with self.assertRaises(Http404):
self.middleware(request)
modified_request = self.middleware(request)
self.assertTrue(modified_request.tenant)
self.assertEqual(modified_request.tenant.schema_name, "www")
self.assertEqual(modified_request.tenant.domain_url, "everyone.test.com")
self.assertEqual(modified_request.tenant.folder, None)
self.assertEqual(modified_request.urlconf, "app_main.urls")

def test_fallback_domain_folder(self):
request = self.factory.get("/some/random/url/", HTTP_HOST="everyone.test.com")
modified_request = self.middleware(request)
self.assertTrue(modified_request.tenant)
self.assertEqual(modified_request.tenant.schema_name, "www")
self.assertEqual(modified_request.tenant.domain_url, "everyone.test.com")
self.assertEqual(modified_request.tenant.folder, None)
self.assertEqual(modified_request.urlconf, "app_main.urls")

0 comments on commit 7896686

Please sign in to comment.