-
Notifications
You must be signed in to change notification settings - Fork 61
Tethys tenants #1210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
msouff
wants to merge
37
commits into
tethysplatform:main
Choose a base branch
from
Aquaveo:tethys_tenants
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Tethys tenants #1210
Changes from 12 commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
d218232
working on adding tenants
msouff 4d3301a
working on adding tenants
msouff b647cff
simplified tethys_tenant base structure
msouff 8336a6c
harverster or middleware won't work for tenant-aware apps
romer8 5eee441
more simplifications to the core tethys_tenants
msouff 03f8244
fixing broken tests
msouff 0fe616c
revert test_harvester.py
msouff 8267196
tests
msouff b05d84a
context_processor.request is already in there
msouff 3944006
removed tenants postgis backend. Not needed
msouff 2b97459
tests
msouff baee361
linting
msouff ccc1f67
apply tenant migration only if has_module django tenants; more test u…
msouff 0d62232
moved tethys_tenants app to django tenants settings
msouff a479437
working on documentation
msouff fb02b92
documentation
msouff a3e1b9a
fixed tests; more documentation
msouff 54e87d6
Updated migration to only be detected if django_tenants is installed
jakeymac 7ab9d65
Updated whats new page in docs
jakeymac 36bc74d
Added warning in settings to make sure users are using django_tenants…
jakeymac 0275ad3
Updated order of sections in production doc page
jakeymac 0d372f6
Updates to documentation
jakeymac 176258a
Fixed documentation
jakeymac 88f2d29
Merge remote-tracking branch 'upstream/main' into tethys_tenants
jakeymac f788438
Updated tethys installation script to use django_tenants postgresql b…
jakeymac c88c451
Formatting fixes
jakeymac ee310b0
Fixed tests
jakeymac 32718be
Updated configurations to display warnings to user concening tethys t…
jakeymac 8c3c5c5
Formatting
jakeymac 32a1eee
Merge branch 'main' into tethys_tenants
jakeymac a71e231
Updated documentation for tenants configurations
jakeymac 91f6c7c
flake fix
jakeymac 09af76a
Converted tests to pytest
jakeymac 1c97e0c
formatting fixes
jakeymac 21ac0b6
Updated default portal config in installation script to enable tenants
jakeymac ef3ddf1
Fixed config in install script
jakeymac 2cca9e7
One more portal_config fix in install script
jakeymac File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import importlib | ||
| from unittest import mock | ||
| from django.test import TestCase, RequestFactory | ||
| from django.contrib import admin | ||
| from django.http import Http404 | ||
| from django.contrib.admin.sites import AdminSite | ||
|
|
||
| from tethys_tenants import models | ||
| from tethys_tenants import admin as tethys_tenants_admin | ||
|
|
||
|
|
||
| class TethysTenantsAdminTest(TestCase): | ||
| def setUp(self): | ||
| self.factory = RequestFactory() | ||
| self.site = AdminSite() | ||
|
|
||
| def test_is_public_schema_function(self): | ||
| # Mock request with public tenant | ||
| public_request = mock.MagicMock() | ||
| public_request.tenant.schema_name = "public" | ||
| self.assertTrue(tethys_tenants_admin.is_public_schema(public_request)) | ||
|
|
||
| # Mock request with non-public tenant | ||
| tenant_request = mock.MagicMock() | ||
| tenant_request.tenant.schema_name = "tenant1" | ||
| self.assertFalse(tethys_tenants_admin.is_public_schema(tenant_request)) | ||
|
|
||
| def test_public_schema_only_decorator(self): | ||
| @tethys_tenants_admin.public_schema_only | ||
| def dummy_view(self, request): | ||
| return "success" | ||
|
|
||
| # Test with public schema | ||
| public_request = mock.MagicMock() | ||
| public_request.tenant.schema_name = "public" | ||
| result = dummy_view(None, public_request) | ||
| self.assertEqual(result, "success") | ||
|
|
||
| # Test with tenant schema - should raise Http404 | ||
| tenant_request = mock.MagicMock() | ||
| tenant_request.tenant.schema_name = "tenant1" | ||
| with self.assertRaises(Http404): | ||
| dummy_view(None, tenant_request) | ||
|
|
||
| def test_domain_admin_registration(self): | ||
| registry = admin.site._registry | ||
| self.assertIn(models.Domain, registry) | ||
| self.assertIsInstance(registry[models.Domain], tethys_tenants_admin.DomainAdmin) | ||
|
|
||
| def test_tenant_admin_registration(self): | ||
| registry = admin.site._registry | ||
| self.assertIn(models.Tenant, registry) | ||
| self.assertIsInstance(registry[models.Tenant], tethys_tenants_admin.TenantAdmin) | ||
|
|
||
| def test_tenant_admin_configuration(self): | ||
| admin_instance = tethys_tenants_admin.TenantAdmin(models.Tenant, self.site) | ||
| self.assertEqual(admin_instance.list_display, ("name",)) | ||
|
|
||
| def test_domain_admin_has_module_permission(self): | ||
| admin_instance = tethys_tenants_admin.DomainAdmin(models.Domain, self.site) | ||
|
|
||
| # Test with public schema | ||
| public_request = mock.MagicMock() | ||
| public_request.tenant.schema_name = "public" | ||
| self.assertTrue(admin_instance.has_module_permission(public_request)) | ||
|
|
||
| # Test with tenant schema | ||
| tenant_request = mock.MagicMock() | ||
| tenant_request.tenant.schema_name = "tenant1" | ||
| self.assertFalse(admin_instance.has_module_permission(tenant_request)) | ||
|
|
||
| def test_tenant_admin_has_module_permission(self): | ||
| admin_instance = tethys_tenants_admin.TenantAdmin(models.Tenant, self.site) | ||
|
|
||
| # Test with public schema | ||
| public_request = mock.MagicMock() | ||
| public_request.tenant.schema_name = "public" | ||
| self.assertTrue(admin_instance.has_module_permission(public_request)) | ||
|
|
||
| # Test with tenant schema | ||
| tenant_request = mock.MagicMock() | ||
| tenant_request.tenant.schema_name = "tenant1" | ||
| self.assertFalse(admin_instance.has_module_permission(tenant_request)) | ||
|
|
||
| def test_domain_admin_changelist_view_public_schema(self): | ||
| admin_instance = tethys_tenants_admin.DomainAdmin(models.Domain, self.site) | ||
|
|
||
| public_request = mock.MagicMock() | ||
| public_request.tenant.schema_name = "public" | ||
|
|
||
| with mock.patch( | ||
| "django.contrib.admin.ModelAdmin.changelist_view" | ||
| ) as mock_super: | ||
| mock_super.return_value = "success" | ||
| result = admin_instance.changelist_view(public_request) | ||
| self.assertEqual(result, "success") | ||
| mock_super.assert_called_once_with(public_request, None) | ||
|
|
||
| def test_domain_admin_changelist_view_tenant_schema(self): | ||
| admin_instance = tethys_tenants_admin.DomainAdmin(models.Domain, self.site) | ||
|
|
||
| tenant_request = mock.MagicMock() | ||
| tenant_request.tenant.schema_name = "tenant1" | ||
|
|
||
| with self.assertRaises(Http404): | ||
| admin_instance.changelist_view(tenant_request) | ||
|
|
||
| @mock.patch("tethys_portal.optional_dependencies.has_module", return_value=False) | ||
| def test_admin_graceful_handling_without_django_tenants(self, mock_has_module): | ||
| importlib.reload(tethys_tenants_admin) | ||
|
|
||
| # Verify has_module was called | ||
| mock_has_module.assert_called() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import importlib | ||
| from unittest import mock | ||
| from django.test import TestCase | ||
| from django.apps import apps | ||
|
|
||
| from tethys_tenants import apps as tenant_apps | ||
|
|
||
|
|
||
| class TethysTenantsAppsTest(TestCase): | ||
| def test_tethys_tenants_config(self): | ||
| app_config = apps.get_app_config("tethys_tenants") | ||
| name = app_config.name | ||
| verbose_name = app_config.verbose_name | ||
|
|
||
| self.assertEqual("tethys_tenants", name) | ||
| self.assertEqual("Tethys Tenants", verbose_name) | ||
| self.assertTrue(isinstance(app_config, tenant_apps.TethysTenantsConfig)) | ||
|
|
||
| @mock.patch("tethys_portal.optional_dependencies.has_module", return_value=False) | ||
| def test_tethys_tenants_config_unavailable(self, mock_has_module): | ||
| importlib.reload(tenant_apps) | ||
|
|
||
| # Verify has_module was called | ||
| mock_has_module.assert_called() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import importlib | ||
| from unittest import mock | ||
| from django.test import TestCase | ||
|
|
||
| from tethys_tenants import models | ||
|
|
||
|
|
||
| class TethysTenantsModelsTest(TestCase): | ||
| def test_tenant_model_exists(self): | ||
| self.assertTrue(hasattr(models.Tenant, "name")) | ||
| self.assertTrue(hasattr(models.Tenant, "created_on")) | ||
| self.assertTrue(hasattr(models.Tenant, "auto_create_schema")) | ||
| self.assertTrue(models.Tenant.auto_create_schema) | ||
|
|
||
| def test_domain_model_exists(self): | ||
| # Domain inherits from DomainMixin, so just checking it exists | ||
| self.assertIsNotNone(models.Domain) | ||
|
|
||
| @mock.patch("tethys_portal.optional_dependencies.has_module", return_value=False) | ||
| def test_models_not_imported_when_django_tenants_unavailable(self, mock_has_module): | ||
| importlib.reload(models) | ||
|
|
||
| mock_has_module.assert_called() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| """ | ||
| ******************************************************************************** | ||
| * Name: admin.py | ||
| * Author: Michael Souffront | ||
| * Created On: 2025 | ||
| * License: BSD 2-Clause | ||
| ******************************************************************************** | ||
| """ | ||
|
|
||
| from functools import wraps | ||
| from django.contrib import admin | ||
| from django.http import Http404 | ||
| from tethys_portal.optional_dependencies import has_module | ||
| from tethys_tenants.models import Tenant, Domain | ||
|
|
||
|
|
||
| if has_module("django_tenants"): | ||
|
|
||
| def is_public_schema(request): | ||
| """Check if current request is from public schema""" | ||
| return getattr(request.tenant, "schema_name", None) == "public" | ||
|
|
||
| def public_schema_only(view_func): | ||
| """Decorator to ensure view is only accessible from public schema""" | ||
|
|
||
| @wraps(view_func) | ||
| def wrapper(self, request, *args, **kwargs): | ||
| if not is_public_schema(request): | ||
| raise Http404("Page not found") | ||
| return view_func(self, request, *args, **kwargs) | ||
|
|
||
| return wrapper | ||
|
|
||
| @admin.register(Domain) | ||
| class DomainAdmin(admin.ModelAdmin): | ||
| list_display = ("domain", "tenant") | ||
|
|
||
| def has_module_permission(self, request): | ||
| return is_public_schema(request) | ||
|
|
||
| @public_schema_only | ||
| def changelist_view(self, request, extra_context=None): | ||
| return super().changelist_view(request, extra_context) | ||
|
|
||
| @public_schema_only | ||
| def change_view(self, request, object_id, form_url="", extra_context=None): | ||
| return super().change_view(request, object_id, form_url, extra_context) | ||
|
|
||
| @public_schema_only | ||
| def add_view(self, request, form_url="", extra_context=None): | ||
| return super().add_view(request, form_url, extra_context) | ||
|
|
||
| @admin.register(Tenant) | ||
| class TenantAdmin(admin.ModelAdmin): | ||
| list_display = ("name",) | ||
|
|
||
| def has_module_permission(self, request): | ||
| return is_public_schema(request) | ||
|
|
||
| @public_schema_only | ||
| def changelist_view(self, request, extra_context=None): | ||
| return super().changelist_view(request, extra_context) | ||
|
|
||
| @public_schema_only | ||
| def change_view(self, request, object_id, form_url="", extra_context=None): | ||
| return super().change_view(request, object_id, form_url, extra_context) | ||
|
|
||
| @public_schema_only | ||
| def add_view(self, request, form_url="", extra_context=None): | ||
| return super().add_view(request, form_url, extra_context) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| """ | ||
| ******************************************************************************** | ||
| * Name: apps.py | ||
| * Author: Michael Souffront | ||
| * Created On: 2025 | ||
| * License: BSD 2-Clause | ||
| ******************************************************************************** | ||
| """ | ||
|
|
||
| from django.apps import AppConfig | ||
| from tethys_portal.optional_dependencies import has_module | ||
|
|
||
|
|
||
| if has_module("django_tenants"): | ||
|
|
||
| class TethysTenantsConfig(AppConfig): | ||
| name = "tethys_tenants" | ||
| verbose_name = "Tethys Tenants" |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.