From 6f940b0ff883428ba7b165b14a6afa4a099cf32e Mon Sep 17 00:00:00 2001 From: Nathan Swain Date: Wed, 2 Aug 2023 10:16:53 -0600 Subject: [PATCH] Addition of Prefix Path (#974) * adding prefix path to the tethys platform through a new portal_config setting called: PREFIX_TO_PATH * edited test to handle the new path * adding one test to cover the new settings * test to check that the prefix was entered * resolving comments in soruce code, still needs tests * changes to 4/11 tests still needs some work * test_url passing for tethys_portal * test_setting for tethys_portal passing * tes_urls for gizmos with prefix * needs some test still * added url test for apps with prefix * handsoff test * test daks needs probably tests in the portal_url * removed uncessary tests * removed unnecessary tests * removing extra tests * change in static to be in templates rather than in the .less files with hardcoded paths Co-authored-by: Nathan Swain * fixed paths related problem with jobs tables, etc * changes for the pull requests * black formatting * black formatting * removed of extra parenthesis * added comments to the .less and .html files regarding moving part of the style to the tempaltes to render the staticfiles dynamically * added support to websockets and http_handlers * mistake for the base.html * mistake arcgis per hs-icon * black formatting * added the tests for the web sockets and https handlers * needed to reaload the tethys_portal.asgi and also tethys_apps.urls --------- Co-authored-by: romer8 --- .../test_base/test_handoff.py | 29 ++- .../test_tethys_apps/test_decorators.py | 195 +++++++++++++++- .../unit_tests/test_tethys_apps/test_urls.py | 65 ++++++ .../test_views/test_dask_dashboard.py | 15 +- .../test_tethys_gizmos/test_urls.py | 73 ++++++ .../test_tethys_portal/test_asgi.py | 64 +++++ .../test_tethys_portal/test_dependencies.py | 22 ++ .../test_tethys_portal/test_settings.py | 14 ++ .../test_tethys_portal/test_urls.py | 219 ++++++++++++++++++ .../test_views/test_admin.py | 5 +- .../test_tethys_portal/test_views/test_api.py | 72 +++++- .../test_tethys_services/test_urls.py | 58 +++++ tethys_apps/urls.py | 6 + .../dask_scheduler_dashboard.html | 2 +- tethys_compute/views/dask_dashboard.py | 2 + tethys_gizmos/gizmo_options/jobs_table.py | 7 + .../tethys_gizmos/css/tethys_map_view.min.css | 2 +- .../static/tethys_gizmos/js/jobs_table.js | 20 +- .../tethys_gizmos/less/tethys_map_view.less | 10 +- .../tethys_gizmos/gizmos/job_row_error.html | 2 +- .../tethys_gizmos/gizmos/jobs_table.html | 1 + .../tethys_gizmos/gizmos/map_view.html | 15 ++ tethys_portal/settings.py | 14 +- .../tethys_portal/css/account_form.min.css | 2 +- .../tethys_portal/css/admin_tweaks.min.css | 2 +- .../tethys_portal/less/account_form.less | 77 ++++-- .../tethys_portal/less/admin_tweaks.less | 24 +- tethys_portal/templates/admin/base.html | 25 ++ .../tethys_portal/accounts/base.html | 41 ++++ tethys_portal/urls.py | 33 ++- 30 files changed, 1047 insertions(+), 69 deletions(-) diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_handoff.py b/tests/unit_tests/test_tethys_apps/test_base/test_handoff.py index f984a2eaa..745c3b8a9 100644 --- a/tests/unit_tests/test_tethys_apps/test_base/test_handoff.py +++ b/tests/unit_tests/test_tethys_apps/test_base/test_handoff.py @@ -3,6 +3,7 @@ from types import FunctionType from unittest import mock from tethys_sdk.testing import TethysTestCase +from django.test import override_settings def test_function(*args): @@ -274,6 +275,21 @@ def test_with_app(self, mock_ta): class TestTestAppHandoff(TethysTestCase): + import sys + from importlib import reload, import_module + from django.conf import settings + from django.urls import clear_url_caches + + @classmethod + def reload_urlconf(self, urlconf=None): + self.clear_url_caches() + if urlconf is None: + urlconf = self.settings.ROOT_URLCONF + if urlconf in self.sys.modules: + self.reload(self.sys.modules[urlconf]) + else: + self.import_module(urlconf) + def set_up(self): self.c = self.get_test_client() self.user = self.create_test_user( @@ -281,10 +297,21 @@ def set_up(self): ) self.c.force_login(self.user) + @override_settings(PREFIX_URL="/") def tear_down(self): self.user.delete() + self.reload_urlconf() + @override_settings(PREFIX_URL="/") def test_test_app_handoff(self): - response = self.c.get('/handoff/test-app/test_name/?csv_url=""') + self.reload_urlconf() + response = self.c.get(f'/handoff/test-app/test_name/?csv_url=""') + + self.assertEqual(302, response.status_code) + + @override_settings(PREFIX_URL="test/prefix") + def test_test_app_handoff_with_prefix(self): + self.reload_urlconf() + response = self.c.get(f'/test/prefix/handoff/test-app/test_name/?csv_url=""') self.assertEqual(302, response.status_code) diff --git a/tests/unit_tests/test_tethys_apps/test_decorators.py b/tests/unit_tests/test_tethys_apps/test_decorators.py index e2c52c44c..eff7060cf 100644 --- a/tests/unit_tests/test_tethys_apps/test_decorators.py +++ b/tests/unit_tests/test_tethys_apps/test_decorators.py @@ -2,7 +2,7 @@ from unittest import mock from django.contrib.auth.models import AnonymousUser -from django.test import RequestFactory +from django.test import RequestFactory, TestCase from django.test.utils import override_settings from django.http import HttpResponseRedirect @@ -33,6 +33,7 @@ def create_projects(request, *args, **kwargs): self.assertEqual("expected_result", ret) @override_settings(ENABLE_OPEN_PORTAL=False) + @override_settings(LOGIN_URL="/accounts/login/") def test_login_required_open_portal_False_Fail(self): request = self.request_factory.get("/apps/test-app") request.user = AnonymousUser() @@ -207,3 +208,195 @@ def method(self, request, *args, **kwargs): f = Foo() self.assertEqual(f.method(request), "expected_result") + + +@override_settings(PREFIX_URL="test/prefix") +@override_settings(LOGIN_URL="/test/prefix/test/login/") +class DecoratorsWithPrefixTest(TestCase): + import sys + from importlib import reload, import_module + from django.conf import settings + from django.urls import clear_url_caches + + @classmethod + def reload_urlconf(self, urlconf=None): + self.clear_url_caches() + if urlconf is None: + urlconf = self.settings.ROOT_URLCONF + if urlconf in self.sys.modules: + self.reload(self.sys.modules[urlconf]) + else: + self.import_module(urlconf) + + def setUp(self): + self.request_factory = RequestFactory() + self.user = UserFactory() + self.reload_urlconf() + + @override_settings(PREFIX_URL="/") + def tearDown(self): + self.reload_urlconf() + pass + + @mock.patch("tethys_apps.decorators.messages") + @mock.patch("tethys_apps.decorators.has_permission", return_value=False) + def test_permission_required_message(self, _, mock_messages): + request = self.request_factory.get("/apps/test-app") + request.user = self.user + msg = "A different message." + + @permission_required("create_projects", message=msg) + def create_projects(request, *args, **kwargs): + return "expected_result" + + ret = create_projects(request) + + mock_messages.add_message.assert_called_with( + request, mock_messages.WARNING, msg + ) + self.assertIsInstance(ret, HttpResponseRedirect) + self.assertEqual("/test/prefix/apps/", ret.url) + + @override_settings(ENABLE_OPEN_PORTAL=False) + def test_login_required_open_portal_False_Fail(self): + request = self.request_factory.get("/apps/test-app") + request.user = AnonymousUser() + + @login_required() + def create_projects(request, *args, **kwargs): + return "expected_result" + + ret = create_projects(request) + + self.assertIsInstance(ret, HttpResponseRedirect) + self.assertIn("test/prefix/test/login/", ret.url) + + @mock.patch("tethys_apps.decorators.messages") + @mock.patch("tethys_apps.decorators.has_permission", return_value=False) + def test_permission_required_no_pass_authenticated(self, _, mock_messages): + request = self.request_factory.get("/apps/test-app") + request.user = self.user + + @permission_required("create_projects") + def create_projects(request, *args, **kwargs): + return "expected_result" + + ret = create_projects(request) + + mock_messages.add_message.assert_called() + self.assertIsInstance(ret, HttpResponseRedirect) + self.assertEqual("/test/prefix/apps/", ret.url) + + @mock.patch("tethys_apps.decorators.messages") + @mock.patch("tethys_apps.decorators.has_permission", return_value=False) + def test_permission_required_no_pass_authenticated_with_referrer( + self, _, mock_messages + ): + request = self.request_factory.get("/apps/test-app") + request.user = self.user + request.META["HTTP_REFERER"] = "http://testserver/foo/bar" + + @permission_required("create_projects") + def create_projects(request, *args, **kwargs): + return "expected_result" + + ret = create_projects(request) + + mock_messages.add_message.assert_called() + self.assertIsInstance(ret, HttpResponseRedirect) + self.assertEqual("/foo/bar", ret.url) + + def test_blank_permissions(self): + self.assertRaises(ValueError, permission_required) + + @mock.patch("tethys_apps.decorators.has_permission", return_value=True) + def test_multiple_permissions(self, mock_has_permission): + request = self.request_factory.get("/apps/test-app") + request.user = self.user + + @permission_required("create_projects", "delete_projects") + def multiple_permissions(request, *args, **kwargs): + return "expected_result" + + ret = multiple_permissions(request) + self.assertEqual(ret, "expected_result") + hp_call_args = mock_has_permission.call_args_list + self.assertEqual(2, len(hp_call_args)) + self.assertEqual("create_projects", hp_call_args[0][0][1]) + self.assertEqual("delete_projects", hp_call_args[1][0][1]) + + @mock.patch("tethys_apps.decorators.has_permission", return_value=True) + def test_multiple_permissions_OR(self, _): + request = self.request_factory.get("/apps/test-app") + request.user = self.user + + @permission_required("create_projects", "delete_projects", use_or=True) + def multiple_permissions_or(request, *args, **kwargs): + return "expected_result" + + self.assertEqual(multiple_permissions_or(request), "expected_result") + + @mock.patch("tethys_apps.decorators.tethys_portal_error", return_value=False) + @mock.patch("tethys_apps.decorators.has_permission", return_value=False) + @override_settings(DEBUG=True) + def test_permission_required_exception_403(self, _, mock_tp_error): + request = self.request_factory.get("/apps/test-app") + request.user = self.user + + @permission_required("create_projects", raise_exception=True) + def exception_403(request, *args, **kwargs): + return "expected_result" + + exception_403(request) + mock_tp_error.handler_403.assert_called_with(request) + + @mock.patch("tethys_apps.decorators.tethys_portal_error", return_value=False) + @mock.patch("tethys_apps.decorators.has_permission", return_value=False) + @override_settings(DEBUG=False) + def test_permission_required_exception_404(self, _, mock_tp_error): + request = self.request_factory.get("/apps/test-app") + request.user = self.user + + @permission_required("create_projects", raise_exception=True) + def exception_404(request, *args, **kwargs): + return "expected_result" + + exception_404(request) + mock_tp_error.handler_404.assert_called_with(request) + + def test_permission_required_no_request(self): + @permission_required("create_projects") + def no_request(request, *args, **kwargs): + return "expected_result" + + self.assertRaises(ValueError, no_request) + + @mock.patch("tethys_apps.decorators.has_permission", return_value=True) + def test_multiple_permissions_class_method(self, _): + request = self.request_factory.get("/apps/test-app") + request.user = self.user + + class Foo: + @permission_required("create_projects") + def method(self, request, *args, **kwargs): + return "expected_result" + + f = Foo() + + self.assertEqual(f.method(request), "expected_result") + + @mock.patch("tethys_apps.decorators.messages") + @mock.patch("tethys_apps.decorators.has_permission", return_value=False) + def test_permission_required_no_pass_not_authenticated(self, _, mock_messages): + request = self.request_factory.get("/apps/test-app") + request.user = AnonymousUser() + + @permission_required("create_projects") + def create_projects(request, *args, **kwargs): + return "expected_result" + + ret = create_projects(request) + + mock_messages.add_message.assert_called() + self.assertIsInstance(ret, HttpResponseRedirect) + self.assertIn("/test/prefix/accounts/login/", ret.url) diff --git a/tests/unit_tests/test_tethys_apps/test_urls.py b/tests/unit_tests/test_tethys_apps/test_urls.py index 2fd8cda2a..fb2694a59 100644 --- a/tests/unit_tests/test_tethys_apps/test_urls.py +++ b/tests/unit_tests/test_tethys_apps/test_urls.py @@ -1,5 +1,6 @@ from django.urls import reverse, resolve from tethys_sdk.testing import TethysTestCase +from django.test import override_settings class TestUrls(TethysTestCase): @@ -44,3 +45,67 @@ def test_urls(self): self.assertEqual( "tethysext.test_extension.controllers", resolver.func.__module__ ) + + +# probably need to test for extensions manually +@override_settings(PREFIX_URL="test/prefix") +class TestUrlsWithPrefix(TethysTestCase): + import sys + from importlib import reload, import_module + from django.conf import settings + from django.urls import clear_url_caches + + @classmethod + def reload_urlconf(self, urlconf=None): + self.clear_url_caches() + if urlconf is None: + urlconf = self.settings.ROOT_URLCONF + if urlconf in self.sys.modules: + self.reload(self.sys.modules[urlconf]) + else: + self.import_module(urlconf) + + def set_up(self): + self.reload_urlconf() + pass + + @override_settings(PREFIX_URL="/") + def tearDown(self): + self.reload_urlconf() + pass + + def test_urls(self): + # This executes the code at the module level + url = reverse("app_library") + resolver = resolve(url) + self.assertEqual("/test/prefix/apps/", url) + self.assertEqual("library", resolver.func.__name__) + self.assertEqual("tethys_apps.views", resolver.func.__module__) + + url = reverse("send_beta_feedback") + resolver = resolve(url) + self.assertEqual("/test/prefix/apps/send-beta-feedback/", url) + self.assertEqual("send_beta_feedback_email", resolver.func.__name__) + self.assertEqual("tethys_apps.views", resolver.func.__module__) + + url = reverse("test_app:home") + resolver = resolve(url) + self.assertEqual("/test/prefix/apps/test-app/", url) + self.assertEqual("home", resolver.func.__name__) + self.assertEqual("tethysapp.test_app.controllers", resolver.func.__module__) + + url = reverse("test_extension:home", kwargs={"var1": "foo", "var2": "bar"}) + resolver = resolve(url) + self.assertEqual("/test/prefix/extensions/test-extension/foo/bar/", url) + self.assertEqual("home", resolver.func.__name__) + self.assertEqual( + "tethysext.test_extension.controllers", resolver.func.__module__ + ) + + url = reverse("test_extension:home", args=["foo", "bar"]) + resolver = resolve(url) + self.assertEqual("/test/prefix/extensions/test-extension/foo/bar/", url) + self.assertEqual("home", resolver.func.__name__) + self.assertEqual( + "tethysext.test_extension.controllers", resolver.func.__module__ + ) diff --git a/tests/unit_tests/test_tethys_compute/test_views/test_dask_dashboard.py b/tests/unit_tests/test_tethys_compute/test_views/test_dask_dashboard.py index 0a199aa67..27a5fef56 100644 --- a/tests/unit_tests/test_tethys_compute/test_views/test_dask_dashboard.py +++ b/tests/unit_tests/test_tethys_compute/test_views/test_dask_dashboard.py @@ -1,6 +1,5 @@ import unittest from unittest import mock - from tethys_compute.views.dask_dashboard import dask_dashboard @@ -29,7 +28,7 @@ def test_dask_status_link(self, mock_dask_scheduler, mock_render): "tethys_compute/dask_scheduler_dashboard.html", rts_call_args[0][0][1] ) self.assertEqual( - "/admin/dask-dashboard/status/test_dask_id/", + f"/admin/dask-dashboard/status/test_dask_id/", rts_call_args[0][0][2]["status_link"], ) @@ -51,7 +50,7 @@ def test_dask_workers_link(self, mock_dask_scheduler, mock_render): "tethys_compute/dask_scheduler_dashboard.html", rts_call_args[0][0][1] ) self.assertEqual( - "/admin/dask-dashboard/workers/test_dask_id/", + f"/admin/dask-dashboard/workers/test_dask_id/", rts_call_args[0][0][2]["workers_link"], ) @@ -73,7 +72,7 @@ def test_dask_tasks_link(self, mock_dask_scheduler, mock_render): "tethys_compute/dask_scheduler_dashboard.html", rts_call_args[0][0][1] ) self.assertEqual( - "/admin/dask-dashboard/tasks/test_dask_id/", + f"/admin/dask-dashboard/tasks/test_dask_id/", rts_call_args[0][0][2]["tasks_link"], ) @@ -95,7 +94,7 @@ def test_dask_profile_link(self, mock_dask_scheduler, mock_render): "tethys_compute/dask_scheduler_dashboard.html", rts_call_args[0][0][1] ) self.assertEqual( - "/admin/dask-dashboard/profile/test_dask_id/", + f"/admin/dask-dashboard/profile/test_dask_id/", rts_call_args[0][0][2]["profile_link"], ) @@ -117,7 +116,7 @@ def test_dask_graph_link(self, mock_dask_scheduler, mock_render): "tethys_compute/dask_scheduler_dashboard.html", rts_call_args[0][0][1] ) self.assertEqual( - "/admin/dask-dashboard/graph/test_dask_id/", + f"/admin/dask-dashboard/graph/test_dask_id/", rts_call_args[0][0][2]["graph_link"], ) @@ -139,7 +138,7 @@ def test_dask_system_link(self, mock_dask_scheduler, mock_render): "tethys_compute/dask_scheduler_dashboard.html", rts_call_args[0][0][1] ) self.assertEqual( - "/admin/dask-dashboard/system/test_dask_id/", + f"/admin/dask-dashboard/system/test_dask_id/", rts_call_args[0][0][2]["systems_link"], ) @@ -161,6 +160,6 @@ def test_dask_groups_link(self, mock_dask_scheduler, mock_render): "tethys_compute/dask_scheduler_dashboard.html", rts_call_args[0][0][1] ) self.assertEqual( - "/admin/dask-dashboard/groups/test_dask_id/", + f"/admin/dask-dashboard/groups/test_dask_id/", rts_call_args[0][0][2]["groups_link"], ) diff --git a/tests/unit_tests/test_tethys_gizmos/test_urls.py b/tests/unit_tests/test_tethys_gizmos/test_urls.py index f4fb20e5e..83fe0eed5 100644 --- a/tests/unit_tests/test_tethys_gizmos/test_urls.py +++ b/tests/unit_tests/test_tethys_gizmos/test_urls.py @@ -1,5 +1,6 @@ from django.urls import reverse, resolve from tethys_sdk.testing import TethysTestCase +from django.test import override_settings class TestUrls(TethysTestCase): @@ -48,3 +49,75 @@ def test_ajax_urls_bokeh_row(self): "tethys_gizmos.views.gizmos.jobs_table", resolver.func.__module__ ) self.assertEqual("gizmos", resolver.namespaces[0]) + + +# we need to test for the JS that is calling the jobs directly +@override_settings(PREFIX_URL="test/prefix") +class TestUrlsWithPrefix(TethysTestCase): + import sys + from importlib import reload, import_module + from django.conf import settings + from django.urls import clear_url_caches + + @classmethod + def reload_urlconf(self, urlconf=None): + self.clear_url_caches() + if urlconf is None: + urlconf = self.settings.ROOT_URLCONF + if urlconf in self.sys.modules: + self.reload(self.sys.modules[urlconf]) + else: + self.import_module(urlconf) + + def set_up(self): + self.reload_urlconf() + pass + + @override_settings(PREFIX_URL="/") + def tearDown(self): + self.reload_urlconf() + pass + + def test_ajax_urls_delete_job(self): + url = reverse("gizmos:delete_job", kwargs={"job_id": "123"}) + resolver = resolve(url) + self.assertEqual("/test/prefix/developer/gizmos/ajax/123/action/delete", url) + self.assertEqual("delete", resolver.func.__name__) + self.assertEqual( + "tethys_gizmos.views.gizmos.jobs_table", resolver.func.__module__ + ) + self.assertEqual("gizmos", resolver.namespaces[0]) + + def test_ajax_urls_update_job_row(self): + url = reverse("gizmos:update_job_row", kwargs={"job_id": "123"}) + resolver = resolve(url) + self.assertEqual("/test/prefix/developer/gizmos/ajax/123/update-row", url) + self.assertEqual("update_row", resolver.func.__name__) + self.assertEqual( + "tethys_gizmos.views.gizmos.jobs_table", resolver.func.__module__ + ) + self.assertEqual("gizmos", resolver.namespaces[0]) + + def test_ajax_urls_update_workflow_nodes_row(self): + url = reverse("gizmos:update_workflow_nodes_row", kwargs={"job_id": "123"}) + resolver = resolve(url) + self.assertEqual( + f"/test/prefix/developer/gizmos/ajax/123/update-workflow-nodes-row", url + ) + self.assertEqual("update_workflow_nodes_row", resolver.func.__name__) + self.assertEqual( + "tethys_gizmos.views.gizmos.jobs_table", resolver.func.__module__ + ) + self.assertEqual("gizmos", resolver.namespaces[0]) + + def test_ajax_urls_bokeh_row(self): + url = reverse("gizmos:bokeh_row", kwargs={"job_id": "123", "type": "test"}) + resolver = resolve(url) + self.assertEqual( + f"/test/prefix/developer/gizmos/ajax/123/test/insert-bokeh-row", url + ) + self.assertEqual("bokeh_row", resolver.func.__name__) + self.assertEqual( + "tethys_gizmos.views.gizmos.jobs_table", resolver.func.__module__ + ) + self.assertEqual("gizmos", resolver.namespaces[0]) diff --git a/tests/unit_tests/test_tethys_portal/test_asgi.py b/tests/unit_tests/test_tethys_portal/test_asgi.py index d28858b9f..79692a19a 100644 --- a/tests/unit_tests/test_tethys_portal/test_asgi.py +++ b/tests/unit_tests/test_tethys_portal/test_asgi.py @@ -1,6 +1,8 @@ from tethys_sdk.testing import TethysTestCase import tethys_portal.asgi as asgi +from django.test import override_settings +from django.urls import URLPattern class TestAsgiApplication(TethysTestCase): @@ -14,3 +16,65 @@ def test_application(self): application = asgi.application self.assertIn("websocket", application.application_mapping) self.assertIn("http", application.application_mapping) + + +@override_settings(PREFIX_URL="test/prefix") +class TestAsgiApplication(TethysTestCase): + import sys + from importlib import reload, import_module + from django.conf import settings + from django.urls import clear_url_caches + + @classmethod + def reload_urlconf(self, urlconf=None): + self.clear_url_caches() + if urlconf is None: + urlconf = self.settings.ROOT_URLCONF + if urlconf in self.sys.modules: + self.reload(self.sys.modules[urlconf]) + else: + self.import_module(urlconf) + + def set_up(self): + self.reload_urlconf("tethys_apps.urls") + self.reload_urlconf("tethys_portal.asgi") + pass + + @override_settings(PREFIX_URL="/") + def tearDown(self): + self.reload_urlconf("tethys_apps.urls") + pass + + def test_websocket_path(self): + expected_path = r"^test/prefix/apps/test-app/test-app-ws/ws/$" + + # Get the URLRouter for "websocket" from the application + asgi_app = asgi.application.application_mapping["websocket"] + + # Get the URLRouter from the AuthMiddlewareStack + url_router = asgi_app.inner.inner.inner + # Check if the expected websocket path is in the URLRouter + self.assertTrue( + any( + isinstance(url_pattern, URLPattern) + and url_pattern.pattern._regex == expected_path + for url_pattern in url_router.routes + ) + ) + + def test_handlers_path(self): + expected_path = r"^test/prefix/apps/0/" + + # Get the URLRouter for "http" from the application + asgi_app = asgi.application.application_mapping["http"] + + # Get the URLRouter from the AuthMiddlewareStack + url_router = asgi_app.inner.inner.inner + # Check if the expected http path is in the URLRouter + self.assertTrue( + any( + isinstance(url_pattern, URLPattern) + and url_pattern.pattern._regex == expected_path + for url_pattern in url_router.routes + ) + ) diff --git a/tests/unit_tests/test_tethys_portal/test_dependencies.py b/tests/unit_tests/test_tethys_portal/test_dependencies.py index 576e646e2..eb6cf9cb5 100644 --- a/tests/unit_tests/test_tethys_portal/test_dependencies.py +++ b/tests/unit_tests/test_tethys_portal/test_dependencies.py @@ -24,6 +24,7 @@ def test_script_tag(self, mock_get_tag): mock_get_tag.assert_called_with("js") @override_settings(STATICFILES_USE_NPM=True) + @override_settings(STATIC_URL="/static") def test_get_tag_css(self): dependency = dependencies.StaticDependency( "name", "version", css_path="css_path" @@ -34,6 +35,18 @@ def test_get_tag_css(self): ) @override_settings(STATICFILES_USE_NPM=True) + @override_settings(STATIC_URL="/test/static") + def test_get_tag_css_static_url_setting(self): + dependency = dependencies.StaticDependency( + "name", "version", css_path="css_path" + ) + result = dependency._get_tag("css") + self.assertEqual( + result, '' + ) + + @override_settings(STATICFILES_USE_NPM=True) + @override_settings(STATIC_URL="/static") def test_get_tag_js(self): dependency = dependencies.StaticDependency( "name", "version", js_path="js_path", cdn_url="{npm_name}@{version}/{path}" @@ -41,6 +54,15 @@ def test_get_tag_js(self): result = dependency._get_tag("js") self.assertEqual(result, '') + @override_settings(STATICFILES_USE_NPM=True) + @override_settings(STATIC_URL="/test/static") + def test_get_tag_js_static_url_setting(self): + dependency = dependencies.StaticDependency( + "name", "version", js_path="js_path", cdn_url="{npm_name}@{version}/{path}" + ) + result = dependency._get_tag("js") + self.assertEqual(result, '') + @override_settings(STATICFILES_USE_NPM=False) def test_get_tag_integrity(self): dependency = dependencies.StaticDependency( diff --git a/tests/unit_tests/test_tethys_portal/test_settings.py b/tests/unit_tests/test_tethys_portal/test_settings.py index 3f43d3597..cd92dfda6 100644 --- a/tests/unit_tests/test_tethys_portal/test_settings.py +++ b/tests/unit_tests/test_tethys_portal/test_settings.py @@ -239,3 +239,17 @@ def test_deprecated_no_config_existing_db(self, mock_home, mock_warning, _): reload(settings) mock_warning.assert_called_once() self.assertEqual(mock_home.call_args_list[2].args[0], "psql") + + @mock.patch( + "tethys_portal.settings.yaml.safe_load", + return_value={ + "settings": { + "PREFIX_URL": "test", + } + }, + ) + def test_prefix_to_path_settings(self, _): + reload(settings) + self.assertEqual(settings.PREFIX_URL, "test") + self.assertEqual(settings.STATIC_URL, "/test/static/") + self.assertEqual(settings.LOGIN_URL, "/test/accounts/login/") diff --git a/tests/unit_tests/test_tethys_portal/test_urls.py b/tests/unit_tests/test_tethys_portal/test_urls.py index a84b67566..836a8eba4 100644 --- a/tests/unit_tests/test_tethys_portal/test_urls.py +++ b/tests/unit_tests/test_tethys_portal/test_urls.py @@ -1,5 +1,7 @@ from unittest import mock from django.test import override_settings + + from django.urls import reverse, resolve from tethys_sdk.testing import TethysTestCase @@ -192,3 +194,220 @@ def test_custom_register_controller_not_class_based_view( self.assertEqual(tethys_portal.urls.register_controller_setting, "test") self.assertEqual(tethys_portal.urls.register_controller, mock_controller) mock_func_extractor.assert_called_once() + + +@override_settings(PREFIX_URL="test/prefix") +@override_settings(LOGIN_URL="/test/prefix/test/login/") +class TestUrlsWithPrefix(TethysTestCase): + import sys + from importlib import reload, import_module + from django.conf import settings + from django.urls import clear_url_caches + + @classmethod + def reload_urlconf(self, urlconf=None): + self.clear_url_caches() + if urlconf is None: + urlconf = self.settings.ROOT_URLCONF + if urlconf in self.sys.modules: + self.reload(self.sys.modules[urlconf]) + else: + self.import_module(urlconf) + + def set_up(self): + self.reload_urlconf() + pass + + @override_settings(PREFIX_URL="/") + def tearDown(self): + self.reload_urlconf() + pass + + def test_account_urls_account_login(self): + url = reverse("accounts:login") + resolver = resolve(url) + self.assertEqual("/test/prefix/accounts/login/", url) + self.assertEqual("login_view", resolver.func.__name__) + self.assertEqual("tethys_portal.views.accounts", resolver.func.__module__) + + def test_admin_urls_account_login(self): + url = reverse("login_prefix") + resolver = resolve(url) + self.assertEqual("/test/prefix/test/login/", url) + self.assertEqual("login_view", resolver.func.__name__) + self.assertEqual("tethys_portal.views.accounts", resolver.func.__module__) + + def test_account_urls_accounts_logout(self): + url = reverse("accounts:logout") + resolver = resolve(url) + self.assertEqual("/test/prefix/accounts/logout/", url) + self.assertEqual("logout_view", resolver.func.__name__) + self.assertEqual("tethys_portal.views.accounts", resolver.func.__module__) + + def test_account_urls_accounts_register(self): + url = reverse("accounts:register") + resolver = resolve(url) + self.assertEqual("/test/prefix/accounts/register/", url) + self.assertEqual("register", resolver.func.__name__) + self.assertEqual("tethys_portal.views.accounts", resolver.func.__module__) + + def test_account_urls_accounts_password_reset(self): + url = reverse("accounts:password_reset") + resolver = resolve(url) + self.assertEqual("/test/prefix/accounts/password/reset/", url) + self.assertEqual("TethysPasswordResetView", resolver.func.__name__) + self.assertEqual("tethys_portal.views.email", resolver.func.__module__) + + def test_account_urls_accounts_password_reset_done(self): + url = reverse("accounts:password_reset_done") + resolver = resolve(url) + self.assertEqual("/test/prefix/accounts/password/reset/done/", url) + self.assertEqual("PasswordResetDoneView", resolver.func.__name__) + self.assertEqual("django.contrib.auth.views", resolver.func.__module__) + + def test_account_urls_accounts_password_confirm(self): + url = reverse( + "accounts:password_confirm", kwargs={"uidb64": "f00Bar", "token": "tok"} + ) + resolver = resolve(url) + self.assertEqual("/test/prefix/accounts/password/reset/f00Bar-tok/", url) + self.assertEqual("PasswordResetConfirmView", resolver.func.__name__) + self.assertEqual("django.contrib.auth.views", resolver.func.__module__) + + def test_account_urls_accounts_password_done(self): + url = reverse("accounts:password_done") + resolver = resolve(url) + self.assertEqual("/test/prefix/accounts/password/done/", url) + self.assertEqual("PasswordResetCompleteView", resolver.func.__name__) + self.assertEqual("django.contrib.auth.views", resolver.func.__module__) + + def test_oauth2_urls_login(self): + url = reverse("social:begin", kwargs={"backend": "foo"}) + resolver = resolve(url) + self.assertEqual("/test/prefix/oauth2/login/foo/", url) + self.assertEqual("auth", resolver.func.__name__) + self.assertEqual("tethys_portal.views.psa", resolver.func.__module__) + + def test_oauth2_urls_complete(self): + url = reverse("social:complete", kwargs={"backend": "foo"}) + resolver = resolve(url) + self.assertEqual("/test/prefix/oauth2/complete/foo/", url) + self.assertEqual("complete", resolver.func.__name__) + self.assertEqual("tethys_portal.views.psa", resolver.func.__module__) + + def test_oauth2_urls_disconnect(self): + url = reverse("social:disconnect", kwargs={"backend": "foo"}) + resolver = resolve(url) + self.assertEqual("/test/prefix/oauth2/disconnect/foo/", url) + self.assertEqual("disconnect", resolver.func.__name__) + self.assertEqual("social_django.views", resolver.func.__module__) + + def test_oauth2_urls_disconnect_individual(self): + url = reverse( + "social:disconnect_individual", + kwargs={"backend": "foo", "association_id": "123"}, + ) + resolver = resolve(url) + self.assertEqual("/test/prefix/oauth2/disconnect/foo/123/", url) + self.assertEqual("disconnect", resolver.func.__name__) + self.assertEqual("social_django.views", resolver.func.__module__) + + def test_oauth2_urls_tenant(self): + url = reverse("social:tenant", kwargs={"backend": "foo"}) + resolver = resolve(url) + self.assertEqual("/test/prefix/oauth2/tenant/foo/", url) + self.assertEqual("tenant", resolver.func.__name__) + self.assertEqual("tethys_portal.views.psa", resolver.func.__module__) + + def test_user_urls_profile(self): + url = reverse("user:profile") + resolver = resolve(url) + + self.assertEqual("/test/prefix/user/", url) + self.assertEqual("profile", resolver.func.__name__) + self.assertEqual("tethys_portal.views.user", resolver.func.__module__) + + def test_user_urls_settings(self): + url = reverse("user:settings") + resolver = resolve(url) + self.assertEqual("/test/prefix/user/settings/", url) + self.assertEqual("settings", resolver.func.__name__) + self.assertEqual("tethys_portal.views.user", resolver.func.__module__) + + def test_user_urls_change_password(self): + url = reverse("user:change_password") + resolver = resolve(url) + self.assertEqual("/test/prefix/user/change-password/", url) + self.assertEqual("change_password", resolver.func.__name__) + self.assertEqual("tethys_portal.views.user", resolver.func.__module__) + + def test_user_urls_disconnect(self): + url = reverse("user:change_password") + resolver = resolve(url) + self.assertEqual("/test/prefix/user/change-password/", url) + self.assertEqual("change_password", resolver.func.__name__) + self.assertEqual("tethys_portal.views.user", resolver.func.__module__) + + def test_user_urls_delete(self): + url = reverse("user:delete") + resolver = resolve(url) + self.assertEqual("/test/prefix/user/delete-account/", url) + self.assertEqual("delete_account", resolver.func.__name__) + self.assertEqual("tethys_portal.views.user", resolver.func.__module__) + + def test_urlpatterns_handoff_capabilities(self): + url = reverse("handoff_capabilities", kwargs={"app_name": "foo"}) + resolver = resolve(url) + self.assertEqual("/test/prefix/handoff/foo/", url) + self.assertEqual("handoff_capabilities", resolver.func.__name__) + self.assertEqual("tethys_apps.views", resolver.func.__module__) + + def test_urlpatterns_handoff(self): + url = reverse("handoff", kwargs={"app_name": "foo", "handler_name": "Bar"}) + resolver = resolve(url) + self.assertEqual("/test/prefix/handoff/foo/Bar/", url) + self.assertEqual("handoff", resolver.func.__name__) + self.assertEqual("tethys_apps.views", resolver.func.__module__) + + def test_urlpatterns_update_job_status(self): + url = reverse("update_job_status", kwargs={"job_id": "JI001"}) + resolver = resolve(url) + self.assertEqual("/test/prefix/update-job-status/JI001/", url) + self.assertEqual("update_job_status", resolver.func.__name__) + self.assertEqual("tethys_apps.views", resolver.func.__module__) + + def test_urlpatterns_update_dask_job_status(self): + url = reverse("update_dask_job_status", kwargs={"key": "123456789"}) + resolver = resolve(url) + self.assertEqual("/test/prefix/update-dask-job-status/123456789/", url) + self.assertEqual("update_dask_job_status", resolver.func.__name__) + self.assertEqual("tethys_apps.views", resolver.func.__module__) + + @override_settings(REGISTER_CONTROLLER="test") + @mock.patch("django.urls.re_path") + @mock.patch("tethys_apps.base.function_extractor.TethysFunctionExtractor") + def test_custom_register_controller(self, mock_func_extractor, _): + import tethys_portal.urls + from importlib import reload + + reload(tethys_portal.urls) + self.assertEqual(tethys_portal.urls.register_controller_setting, "test") + mock_func_extractor.assert_called_once() + + @override_settings(REGISTER_CONTROLLER="test") + @mock.patch("django.urls.re_path") + @mock.patch("tethys_apps.base.function_extractor.TethysFunctionExtractor") + def test_custom_register_controller_not_class_based_view( + self, mock_func_extractor, _ + ): + import tethys_portal.urls + from importlib import reload + + mock_controller = mock.MagicMock() + mock_controller.as_controller.side_effect = AttributeError + mock_func_extractor.return_value = mock.MagicMock(function=mock_controller) + + reload(tethys_portal.urls) + self.assertEqual(tethys_portal.urls.register_controller_setting, "test") + self.assertEqual(tethys_portal.urls.register_controller, mock_controller) + mock_func_extractor.assert_called_once() diff --git a/tests/unit_tests/test_tethys_portal/test_views/test_admin.py b/tests/unit_tests/test_tethys_portal/test_views/test_admin.py index 4328f50d2..5584eb59c 100644 --- a/tests/unit_tests/test_tethys_portal/test_views/test_admin.py +++ b/tests/unit_tests/test_tethys_portal/test_views/test_admin.py @@ -3,6 +3,8 @@ from tethys_portal.views.admin import clear_workspace from tethys_apps.models import TethysApp +from django.test import override_settings, TestCase + class TethysPortalTethysAppTests(unittest.TestCase): def setUp(self): @@ -22,6 +24,7 @@ def test_clear_workspace_display( app = TethysApp(name="app_name") mock_get_app_class.return_value = app mock_app.objects.get.return_value = app + # reload_urlconf() expected_context = { "app_name": mock_app.objects.get().name, @@ -45,13 +48,13 @@ def test_clear_workspace_successful( self, mock_redirect, mock_message, mock_app, mock_gaw, mock_get_app_class ): mock_request = mock.MagicMock(method="POST", POST="clear-workspace-submit") - app = TethysApp(name="app_name") mock_get_app_class.return_value = app mock_app.objects.get.return_value = app app.pre_delete_app_workspace = mock.MagicMock() app.post_delete_app_workspace = mock.MagicMock() mock_gaw.return_value = mock.MagicMock() + # reload_urlconf() clear_workspace(mock_request, "myapp") diff --git a/tests/unit_tests/test_tethys_portal/test_views/test_api.py b/tests/unit_tests/test_tethys_portal/test_views/test_api.py index cb12d5ea1..2c202cd37 100644 --- a/tests/unit_tests/test_tethys_portal/test_views/test_api.py +++ b/tests/unit_tests/test_tethys_portal/test_views/test_api.py @@ -1,17 +1,34 @@ +import sys +from importlib import reload, import_module from django.contrib.auth.models import User from django.http import JsonResponse, HttpResponse -from django.urls import reverse +from django.urls import reverse, clear_url_caches +from django.test import override_settings +from django.conf import settings from tethys_apps.base.testing.testing import TethysTestCase class TethysPortalApiTests(TethysTestCase): + def reload_urlconf(self, urlconf=None): + clear_url_caches() + if urlconf is None: + urlconf = settings.ROOT_URLCONF + if urlconf in sys.modules: + reload(sys.modules[urlconf]) + else: + import_module(urlconf) + def set_up(self): self.user = User.objects.create_user(username="foo") self.user.save() + pass - def tear_down(self): + @override_settings(PREFIX_URL="/") + def tearDown(self): self.user.delete() + self.reload_urlconf() + pass def test_get_csrf_not_authenticated(self): """Test get_csrf API endpoint not authenticated.""" @@ -63,7 +80,12 @@ def test_get_whoami_authenticated(self): self.assertEqual("foo", json["username"]) self.assertTrue(json["isAuthenticated"]) + @override_settings(STATIC_URL="/static") + @override_settings(PREFIX_URL="/") + @override_settings(LOGIN_URL="/accounts/login/") def test_get_app_valid_id(self): + self.reload_urlconf() + """Test get_app API endpoint with valid app id.""" response = self.client.get(reverse("api:get_app", kwargs={"app": "test-app"})) self.assertEqual(response.status_code, 200) @@ -88,10 +110,50 @@ def test_get_app_valid_id(self): self.assertEqual("test_app", json["urlNamespace"]) self.assertEqual("#2c3e50", json["color"]) self.assertEqual("/static/test_app/images/icon.gif", json["icon"]) - self.assertEqual("/apps/", json["exitUrl"]) - self.assertEqual("/apps/test-app/", json["rootUrl"]) + self.assertEqual(f"/apps/", json["exitUrl"]) + self.assertEqual(f"/apps/test-app/", json["rootUrl"]) + self.assertRegex( + json["settingsUrl"], + r"^/admin/tethys_apps/tethysapp/[0-9]+/change/$", + ) + + @override_settings(PREFIX_URL="test/prefix") + @override_settings(LOGIN_URL="/test/prefix/test/login/") + @override_settings(STATIC_URL="/test/prefix/test/static/") + def test_get_app_valid_id_with_prefix(self): + self.reload_urlconf() + + """Test get_app API endpoint with valid app id.""" + response = self.client.get(reverse("api:get_app", kwargs={"app": "test-app"})) + self.assertEqual(response.status_code, 200) + self.assertIsInstance(response, JsonResponse) + json = response.json() + self.assertIn("title", json) + self.assertIn("description", json) + self.assertIn("tags", json) + self.assertIn("package", json) + self.assertIn("urlNamespace", json) + self.assertIn("color", json) + self.assertIn("icon", json) + self.assertIn("exitUrl", json) + self.assertIn("rootUrl", json) + self.assertIn("settingsUrl", json) + self.assertEqual("Test App", json["title"]) + self.assertEqual( + "Place a brief description of your app here.", json["description"] + ) + self.assertEqual("", json["tags"]) + self.assertEqual("test_app", json["package"]) + self.assertEqual("test_app", json["urlNamespace"]) + self.assertEqual("#2c3e50", json["color"]) + self.assertEqual( + "/test/prefix/test/static/test_app/images/icon.gif", json["icon"] + ) + self.assertEqual("/test/prefix/apps/", json["exitUrl"]) + self.assertEqual("/test/prefix/apps/test-app/", json["rootUrl"]) self.assertRegex( - json["settingsUrl"], r"^/admin/tethys_apps/tethysapp/[0-9]+/change/$" + json["settingsUrl"], + r"^/test/prefix/admin/tethys_apps/tethysapp/[0-9]+/change/$", ) def test_get_app_invalid_id(self): diff --git a/tests/unit_tests/test_tethys_services/test_urls.py b/tests/unit_tests/test_tethys_services/test_urls.py index 84a956dd6..c6758ba0a 100644 --- a/tests/unit_tests/test_tethys_services/test_urls.py +++ b/tests/unit_tests/test_tethys_services/test_urls.py @@ -1,5 +1,6 @@ from django.urls import reverse, resolve from tethys_sdk.testing import TethysTestCase +from django.test.utils import override_settings class TestTethysServicesUrls(TethysTestCase): @@ -38,3 +39,60 @@ def test_urlpatterns_wpshome(self): self.assertEqual("/developer/services/wps/", url) self.assertEqual("wps_home", resolver.func.__name__) self.assertEqual("tethys_services.views", resolver.func.__module__) + + +@override_settings(PREFIX_URL="test/prefix") +class TestTethysServicesUrlsWithPrefix(TethysTestCase): + import sys + from importlib import reload, import_module + from django.conf import settings + from django.urls import clear_url_caches + + @classmethod + def reload_urlconf(self, urlconf=None): + self.clear_url_caches() + if urlconf is None: + urlconf = self.settings.ROOT_URLCONF + if urlconf in self.sys.modules: + self.reload(self.sys.modules[urlconf]) + else: + self.import_module(urlconf) + + def set_up(self): + self.reload_urlconf() + pass + + @override_settings(PREFIX_URL="/") + def tearDown(self): + self.reload_urlconf() + pass + + def test_service_urls_wps_services(self): + url = reverse("services:wps_service", kwargs={"service": "foo"}) + resolver = resolve(url) + self.assertEqual("/test/prefix/developer/services/wps/foo/", url) + self.assertEqual("wps_service", resolver.func.__name__) + self.assertEqual("tethys_services.views", resolver.func.__module__) + + def test_service_urls_wps_process(self): + url = reverse( + "services:wps_process", kwargs={"service": "foo", "identifier": "bar"} + ) + resolver = resolve(url) + self.assertEqual("/test/prefix/developer/services/wps/foo/process/bar/", url) + self.assertEqual("wps_process", resolver.func.__name__) + self.assertEqual("tethys_services.views", resolver.func.__module__) + + def test_urlpatterns_datasethome(self): + url = reverse("services:datasets_home") + resolver = resolve(url) + self.assertEqual("/test/prefix/developer/services/datasets/", url) + self.assertEqual("datasets_home", resolver.func.__name__) + self.assertEqual("tethys_services.views", resolver.func.__module__) + + def test_urlpatterns_wpshome(self): + url = reverse("services:wps_home") + resolver = resolve(url) + self.assertEqual("/test/prefix/developer/services/wps/", url) + self.assertEqual("wps_home", resolver.func.__name__) + self.assertEqual("tethys_services.views", resolver.func.__module__) diff --git a/tethys_apps/urls.py b/tethys_apps/urls.py index eb7147b2a..64512a808 100644 --- a/tethys_apps/urls.py +++ b/tethys_apps/urls.py @@ -12,8 +12,10 @@ from channels.routing import URLRouter from tethys_apps.harvester import SingletonHarvester from tethys_apps.views import library, send_beta_feedback_email +from django.conf import settings tethys_log = logging.getLogger("tethys." + __name__) +prefix_url = f"{settings.PREFIX_URL}" urlpatterns = [ re_path(r"^$", library, name="app_library"), @@ -31,6 +33,8 @@ http_handler_patterns = [] for namespace, urls in handler_url_patterns["http_handler_patterns"].items(): root_pattern = r"^apps/{0}/".format(namespace.replace("_", "-")) + if prefix_url is not None and prefix_url != "/": + root_pattern = rf"^{prefix_url}/apps/{0}/".format(namespace.replace("_", "-")) http_handler_patterns.append(re_path(root_pattern, URLRouter(urls))) # Add app url patterns to urlpatterns, namespaced per app appropriately @@ -61,6 +65,8 @@ def prepare_websocket_urls(app_websocket_url_patterns): for u in urls: url_str = str(u.pattern).replace("^", "") namespaced_url_str = f"^apps/{root_url}/{url_str}" + if prefix_url is not None and prefix_url != "/": + namespaced_url_str = f"^{prefix_url}/apps/{root_url}/{url_str}" namespaced_url = re_path(namespaced_url_str, u.callback, name=u.name) prepared_urls.append(namespaced_url) diff --git a/tethys_compute/templates/tethys_compute/dask_scheduler_dashboard.html b/tethys_compute/templates/tethys_compute/dask_scheduler_dashboard.html index f1f59278f..479b9bf74 100644 --- a/tethys_compute/templates/tethys_compute/dask_scheduler_dashboard.html +++ b/tethys_compute/templates/tethys_compute/dask_scheduler_dashboard.html @@ -23,7 +23,7 @@ ' + ''; +var base_ajax_url = ""; +$('.jobs-table').each(function(){ + $table = $(this); + base_ajax_url = $table.data('base-ajax-url'); +}); + function bind_action(action, on_success=()=>{}){ if($(action).hasClass('disabled')){ return; @@ -32,8 +38,7 @@ function bind_action(action, on_success=()=>{}){ var confirmation_message = $(action).data('confirmation-message'); var modal_url = $(action).data('modal-url'); var callback = $(action).data('callback'); - var url = '/developer/gizmos/ajax/' + job_id + '/action/' + callback; - + var url = base_ajax_url + job_id + '/action/' + callback; var do_action = function () { if(show_overlay){ $("#jobs_table_overlay").removeClass('d-none'); @@ -80,7 +85,7 @@ function load_log_content(job_id) { $("#jobs_table_logs_overlay").removeClass('d-none'); $('#ModalJobLogTitle').html('Logs for Job ID: ' + job_id); - var show_log_url = '/developer/gizmos/ajax/' + job_id + '/action/show-log'; + var show_log_url = base_ajax_url + job_id + '/action/show-log'; $.ajax({ url: show_log_url }).done(function(json){ @@ -116,7 +121,7 @@ function update_log_content(event, use_cache=true){ var key1 = $('#sub_job_select').val(); var key2 = $('#log_' + key1).val(); var content; - var log_content_url = '/developer/gizmos/ajax/' + job_id + '/log-content/' + key1; + var log_content_url = base_ajax_url + job_id + '/log-content/' + key1; if (key2 === undefined){ content = log_contents[key1]; }else{ @@ -287,7 +292,7 @@ function update_row(table_elem){ var column_fields = $(table).data('column-fields'); var refresh_interval = $(table).data('refresh-interval'); var job_id = $(table_elem).data('job-id'); - var update_url = '/developer/gizmos/ajax/' + job_id + '/update-row'; + var update_url = base_ajax_url + job_id + '/update-row'; var data = { column_fields: column_fields, @@ -295,7 +300,6 @@ function update_row(table_elem){ show_actions: show_actions, actions: actions, }; - $.ajax({ method: 'POST', url: update_url, @@ -375,7 +379,7 @@ function update_workflow_nodes_row(table_elem){ var job_id = $(table_elem).data('job-id'); var target_selector = "#" + $(table_elem).attr('id') + " td .workflow-nodes-graph"; var error_selector = target_selector + ' .loading-error'; - var update_url = '/developer/gizmos/ajax/' + job_id + '/update-workflow-nodes-row'; + var update_url = base_ajax_url + job_id + '/update-workflow-nodes-row'; $.ajax({ method: 'POST', @@ -416,7 +420,7 @@ function bokeh_nodes_row(table_elem){ var job_id = $(table_elem).data('job-id'); // options for type is individual-graph, individual-progress and individual-task-stream for now var type = 'individual-progress'; - var update_url = '/developer/gizmos/ajax/' + job_id + '/' + type + '/insert-bokeh-row'; + var update_url = base_ajax_url + job_id + '/' + type + '/insert-bokeh-row'; $.ajax({ method: 'POST', diff --git a/tethys_gizmos/static/tethys_gizmos/less/tethys_map_view.less b/tethys_gizmos/static/tethys_gizmos/less/tethys_map_view.less index 26dd98972..47c1eeb74 100644 --- a/tethys_gizmos/static/tethys_gizmos/less/tethys_map_view.less +++ b/tethys_gizmos/static/tethys_gizmos/less/tethys_map_view.less @@ -75,7 +75,15 @@ .tethys-map-view-draw-icon { display: inline-block; - background: url('/static/tethys_gizmos/images/map-view-drawing-icons.gif') no-repeat; + /* + Following line: + + background: url('/static/tethys_gizmos/images/map-view-drawing-icons.gif') no-repeat; + + were removed and put into the following template: tethys_gizmos/templates/tethys_gizmos/gizmos/map_view.html + This is because it needs to render the staticfiles dynamically in case the STATIC_URL setting is other than /static/ + + */ background-size: cover; height: 16px; width: 16px; diff --git a/tethys_gizmos/templates/tethys_gizmos/gizmos/job_row_error.html b/tethys_gizmos/templates/tethys_gizmos/gizmos/job_row_error.html index f1a3fefe9..d23ba4d8d 100644 --- a/tethys_gizmos/templates/tethys_gizmos/gizmos/job_row_error.html +++ b/tethys_gizmos/templates/tethys_gizmos/gizmos/job_row_error.html @@ -1,7 +1,7 @@ {% load static tethys_gizmos %} - + {{ error_msg }} diff --git a/tethys_gizmos/templates/tethys_gizmos/gizmos/jobs_table.html b/tethys_gizmos/templates/tethys_gizmos/gizmos/jobs_table.html index 711ed7980..6a8c070d5 100644 --- a/tethys_gizmos/templates/tethys_gizmos/gizmos/jobs_table.html +++ b/tethys_gizmos/templates/tethys_gizmos/gizmos/jobs_table.html @@ -16,6 +16,7 @@ data-actions="{{ actions|jsonify }}" data-enable-data-table="{{ enable_data_table|jsonify }}" data-data-table-options="{{ data_table_options|jsonify }}" + data-base-ajax-url="{{ base_ajax_url }}" > {% for column_name in column_names %} diff --git a/tethys_gizmos/templates/tethys_gizmos/gizmos/map_view.html b/tethys_gizmos/templates/tethys_gizmos/gizmos/map_view.html index 03759fce0..876d74296 100644 --- a/tethys_gizmos/templates/tethys_gizmos/gizmos/map_view.html +++ b/tethys_gizmos/templates/tethys_gizmos/gizmos/map_view.html @@ -1,5 +1,17 @@ {% load static tethys_gizmos %} +
diff --git a/tethys_portal/settings.py b/tethys_portal/settings.py index 20a9c98b9..0aa8dd715 100644 --- a/tethys_portal/settings.py +++ b/tethys_portal/settings.py @@ -349,8 +349,10 @@ } ] + # Static files (CSS, JavaScript, Images) -STATIC_URL = "/static/" +STATIC_URL = portal_config_settings.pop("STATIC_URL", "/static/") + STATICFILES_DIRS = [ BASE_DIR / "static", @@ -449,7 +451,7 @@ "localhost" # Server rp id for FIDO2, it's the full domain of your project ) FIDO_SERVER_NAME = "Tethys Portal" -FIDO_LOGIN_URL = "/auth/login" +FIDO_LOGIN_URL = portal_config_settings.pop("FIDO_LOGIN_URL", "/auth/login") MFA_CONFIG = portal_config_settings.pop("MFA_CONFIG", {}) @@ -509,3 +511,11 @@ CORS_CONFIG = portal_config_settings.pop("CORS_CONFIG", {}) for setting, value in CORS_CONFIG.items(): setattr(this_module, setting, value) + +LOGIN_URL = portal_config_settings.pop("LOGIN_URL", "/accounts/login/") +PREFIX_URL = portal_config_settings.pop("PREFIX_URL", "/") +if PREFIX_URL is not None and PREFIX_URL != "/": + PREFIX_URL = f"{PREFIX_URL.strip('/')}" + STATIC_URL = f"/{PREFIX_URL}/{STATIC_URL.strip('/')}/" + LOGIN_URL = f"/{PREFIX_URL}/{LOGIN_URL.strip('/')}/" + FIDO_LOGIN_URL = f"/{PREFIX_URL}/{FIDO_LOGIN_URL.strip('/')}/" diff --git a/tethys_portal/static/tethys_portal/css/account_form.min.css b/tethys_portal/static/tethys_portal/css/account_form.min.css index e2fa02721..77fce76e9 100644 --- a/tethys_portal/static/tethys_portal/css/account_form.min.css +++ b/tethys_portal/static/tethys_portal/css/account_form.min.css @@ -1 +1 @@ -.account-form-wrapper{position:relative;padding:20px;margin:50px 0;background:#ffffff;box-sizing:border-box;border:1px solid #dddddd;border-radius:10px}.account-form-wrapper .account-form-header{position:absolute;left:0;top:0;width:100%;padding:0 20px;background:#eeeeee;border-radius:10px 10px 0 0}.account-form-wrapper .account-form-header h1{color:#555555;font-weight:400}.account-form-wrapper.disconnect p{padding:20px 0}.account-form-wrapper .account-form-body{margin-top:60px}.account-form-wrapper .help-block{margin-top:10px}.btn-hydroshare{color:#555555;background-color:#ffffff;border-color:rgba(0,0,0,0.2)}.btn-hydroshare:hover{color:#ffffff;background-color:#71a43a;border-color:rgba(0,0,0,0.2)}.bi-hydroshare{background-image:url(/static/tethys_portal/images/hs-icon-sm.png);background-repeat:no-repeat;background-size:75%;background-position:4px 5px}.btn-arcgis{color:white;background-color:#0079C1;border-color:rgba(0,0,0,0.2)}.btn-arcgis:hover{background-color:#005E95}.bi-arcgis{background-image:url(/static/tethys_portal/images/arcgis-icon-sm.png);background-repeat:no-repeat;background-size:75%;background-position:4px 5px}.btn-okta{background-color:#ffffff;color:#555555;border-color:rgba(0,0,0,0.2)}.btn-okta .bi-circle{color:#007dc1}.btn-okta:hover{background-color:#007dc1;color:#ffffff}.btn-okta:hover .bi-circle{color:#ffffff;font-weight:600}.btn-okta:focus{background-color:#007dc1;color:#ffffff}.btn-okta:focus .bi-circle{color:#ffffff;font-weight:600}.btn-onelogin{background-color:#ffffff;color:#555555;border-color:rgba(0,0,0,0.2)}.btn-onelogin .bi-onelogin{color:#1c1f2a;background-image:url(/static/tethys_portal/images/onelogin_black_32.png);background-repeat:no-repeat;background-size:75%;background-position:4px 5px}.btn-onelogin:hover{background-color:#1c1f2a;color:#ffffff}.btn-onelogin:hover .bi-onelogin{background-image:url(/static/tethys_portal/images/onelogin_white_32.png)}.btn-onelogin:focus{background-color:#1c1f2a;color:#ffffff}.btn-onelogin:focus .bi-onelogin{background-image:url(/static/tethys_portal/images/onelogin_white_32.png)}.btn-windows{background-color:#ffffff;color:#555555;border-color:rgba(0,0,0,0.2)}.bi-windows{color:#3878d4}.btn-windows:hover{background-color:#3878d4;color:#ffffff}.btn-windows:hover .bi-windows{color:#ffffff}.btn-windows:focus{background-color:#3878d4;color:#ffffff}.btn-windows:focus .bi-windows{color:#ffffff}.btn-google{background-color:#ffffff;color:#555555}.bi-google{color:#dd4b39}.btn-google:hover{background-color:#dd4b39}.btn-google:hover .bi-google{color:#ffffff}.btn-google:focus{background-color:#dd4b39}.btn-google:focus .bi-google{color:#ffffff}.btn-linkedin{background-color:#ffffff;color:#555555}.bi-linkedin{color:#007bb6}.btn-linkedin:hover{background-color:#007bb6}.btn-linkedin:hover .bi-linkedin{color:#ffffff}.btn-linkedin:focus{background-color:#007bb6}.btn-linkedin:focus .bi-linkedin{color:#ffffff}.btn-facebook{background-color:#ffffff;color:#555555}.bi-facebook{color:#3b5998}.btn-facebook:hover{background-color:#3b5998}.btn-facebook:hover .bi-facebook{color:#ffffff}.btn-facebook:focus{background-color:#3b5998}.btn-facebook:focus .bi-facebook{color:#ffffff}.btn-custom-oauth{color:#555555;background-color:#ffffff;border-color:rgba(0,0,0,0.2)}.btn-custom-oauth:hover{color:#ffffff;background-color:#666666;border-color:rgba(0,0,0,0.2)}.btn-custom-oauth{background-color:#ffffff;color:#555555}.bi-unlock-fill{color:#666666}.btn-custom-oauth:hover{background-color:#666666}.btn-custom-oauth:hover .bi-unlock-alt{color:#ffffff}.btn-custom-oauth:focus{background-color:#666666}.btn-custom-oauth:focus .bi-unlock-alt{color:#ffffff}.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:15px;font-weight:500;width:100%;border-color:rgba(0,0,0,0.2)}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-block+.btn-block{margin-top:10px}.social-divide-or{position:relative;margin:30px 0;width:100%;text-align:center}.social-divide-or .line{width:100%;height:1px;background-color:#dddddd}.social-divide-or .or{position:absolute;left:50%;top:-15px;margin-left:-20px;background-color:#ffffff;padding:0 10px;font-size:20px;color:#555555}@media (max-width:992px){.account-form-wrapper{margin:0}} \ No newline at end of file +.account-form-wrapper{position:relative;padding:20px;margin:50px 0;background:#fff;box-sizing:border-box;border:1px solid #ddd;border-radius:10px}.account-form-wrapper .account-form-header{position:absolute;left:0;top:0;width:100%;padding:0 20px;background:#eee;border-radius:10px 10px 0 0}.account-form-wrapper .account-form-header h1{color:#555;font-weight:400}.account-form-wrapper.disconnect p{padding:20px 0}.account-form-wrapper .account-form-body{margin-top:60px}.account-form-wrapper .help-block,.btn-block+.btn-block{margin-top:10px}.btn-hydroshare{color:#555;background-color:#fff;border-color:rgba(0,0,0,.2)}.btn-arcgis,.btn-hydroshare:hover{color:#fff;border-color:rgba(0,0,0,.2)}.btn-hydroshare:hover{background-color:#71a43a}.btn-arcgis{background-color:#0079C1}.btn-arcgis:hover{background-color:#005E95}.btn-okta{background-color:#fff;color:#555;border-color:rgba(0,0,0,.2)}.btn-okta .bi-circle{color:#007dc1}.btn-okta:hover{background-color:#007dc1;color:#fff}.btn-okta:hover .bi-circle{color:#fff;font-weight:600}.btn-okta:focus{background-color:#007dc1;color:#fff}.btn-okta:focus .bi-circle{color:#fff;font-weight:600}.btn-onelogin{background-color:#fff;color:#555;border-color:rgba(0,0,0,.2)}.btn-onelogin .bi-onelogin{color:#1c1f2a}.btn-onelogin:focus,.btn-onelogin:hover{background-color:#1c1f2a;color:#fff}.btn-windows{background-color:#fff;color:#555;border-color:rgba(0,0,0,.2)}.bi-windows{color:#3878d4}.btn-windows:hover{background-color:#3878d4;color:#fff}.btn-windows:hover .bi-windows{color:#fff}.btn-windows:focus{background-color:#3878d4;color:#fff}.btn-windows:focus .bi-windows{color:#fff}.btn-google{background-color:#fff;color:#555}.btn-google:focus,.btn-google:hover{background-color:#dd4b39}.bi-google{color:#dd4b39}.btn-google:focus .bi-google,.btn-google:hover .bi-google{color:#fff}.btn-linkedin{background-color:#fff;color:#555}.btn-linkedin:focus,.btn-linkedin:hover{background-color:#007bb6}.bi-linkedin{color:#007bb6}.btn-linkedin:focus .bi-linkedin,.btn-linkedin:hover .bi-linkedin{color:#fff}.btn-facebook{background-color:#fff;color:#555}.btn-facebook:focus,.btn-facebook:hover{background-color:#3b5998}.bi-facebook{color:#3b5998}.btn-facebook:hover .bi-facebook{color:#fff}.btn-facebook:focus .bi-facebook{color:#fff}.btn-custom-oauth:hover{color:#fff;border-color:rgba(0,0,0,.2)}.btn-custom-oauth{border-color:rgba(0,0,0,.2);background-color:#fff;color:#555}.btn-custom-oauth:focus,.btn-custom-oauth:hover{background-color:#666}.bi-unlock-fill{color:#666}.btn-custom-oauth:focus .bi-unlock-fill,.btn-custom-oauth:hover .bi-unlock-fill{color:#fff}.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:15px;font-weight:500;width:100%;border-color:rgba(0,0,0,.2)}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,.2)}.social-divide-or{position:relative;margin:30px 0;width:100%;text-align:center}.social-divide-or .line{width:100%;height:1px;background-color:#ddd}.social-divide-or .or{position:absolute;left:50%;top:-15px;margin-left:-20px;background-color:#fff;padding:0 10px;font-size:20px;color:#555}@media (max-width:992px){.account-form-wrapper{margin:0}} \ No newline at end of file diff --git a/tethys_portal/static/tethys_portal/css/admin_tweaks.min.css b/tethys_portal/static/tethys_portal/css/admin_tweaks.min.css index b61497ce7..a78153c3a 100644 --- a/tethys_portal/static/tethys_portal/css/admin_tweaks.min.css +++ b/tethys_portal/static/tethys_portal/css/admin_tweaks.min.css @@ -1 +1 @@ -body{background-color:var(--body-bg);color:var(--body-fg)}ul li{list-style-type:none;font-size:initial;line-height:initial}.dashboard #content{width:auto}#content h1{font-size:30px;text-transform:capitalize}#container{width:auto;margin:0 50px}div.breadcrumbs{background:0 0;border:none;font-size:16px}div.breadcrumbs a{color:#2ca6ed}a:link,a:visited{color:#2ca6ed}a:hover{color:#0a84cb}.button.cancel-link{background-color:transparent}.object-tools a:link{background:#0A62A9;font-weight:500;font-size:12px}.object-tools a:link:hover{background-color:#1b73ba}.object-tools a.addlink{background:#0A62A9 url(/static/admin/img/tooltag-add.png) 95% center no-repeat;font-weight:500;font-size:12px;padding:3px 12px}.object-tools a.addlink:hover{background:#1b73ba url(/static/admin/img/tooltag-add.png) 95% center no-repeat}.module caption{font-size:16px;font-weight:400;background:#0a62a9;padding:10px;padding-left:8px;caption-side:top}.module table{width:100%}.module tr td,.module tr th{padding:8px}.module tr th{font-size:14px;font-weight:400;width:100%}.module h2{background:#0A62A9;color:#fff;font-size:14px;line-height:1.5;padding:8px;font-weight:500}thead th{background:var(--darkened-bg)}#content-related .module h2{background:#2ca6ed;color:#fff;font-size:14px;line-height:1.5;padding:8px;font-weight:500}.inline-group h2{background:#0A62A9;color:#fff;font-size:14px;line-height:1.5;padding:8px;font-weight:500}.inline-group .inline-related h3{color:#666;background:#efefef;font-size:12px;line-height:1.5;padding:8px;font-weight:500}.inline-group .tabular tr.add-row td{background:var(--body-bg);padding:8px}.inline-group .tabular th{width:auto}#changelist #toolbar{background:var(--darkened-bg);padding:5px}#changelist #toolbar #searchbar{height:30px}#changelist #toolbar form input[type=submit]{padding:4px 6px;background:#0A62A9;color:#fff}#changelist #toolbar form input[type=submit]:hover{background:#1b73ba}#changelist #changelist-filter{z-index:995}#changelist #changelist-filter h2{background:#0A62A9;font-size:16px;line-height:2;padding:8px;font-weight:500;margin-left:-2px}#changelist #changelist-filter h3{color:#666;font-weight:400;font-size:22px}#changelist #changelist-filter li.selected a{color:#2ca6ed!important}#changelist #changelist-form .actions{background:var(--body-bg);border-top:none;padding:5px}#changelist #changelist-form .actions .button{padding:4px 6px;background:#0A62A9;color:#fff}#changelist #changelist-form .actions .button:hover{background:#1b73ba}#changelist #changelist-form .paginator{background:var(--body-bg)}#changelist #changelist-form #result_list thead th{background:var(--darkened-bg);padding:5px}#changelist #changelist-form #result_list thead th.sorted{background:var(--darkened-bg)}#changelist #changelist-form #result_list tbody td{padding:5px}#changelist #changelist-form #result_list tbody th{width:auto}.submit-row{background:0 0;border:none;padding:10px 0}.submit-row a.deletelink{height:auto}.delete-confirmation form .cancel-link{height:auto}.button,.submit-row,[type=button],[type=submit],input{border-radius:5px}input[type=submit]{background:var(--darkened-bg);border:1px #888 solid;font-weight:500;color:var(--body-fg)}input[type=submit]:hover{background:var(--border-color)}input[type=submit][name=_save]{background:#1DA456;border:1px #0c9345 solid;color:#fff}input[type=submit][name=_save]:hover{background:#0c9345}.dynamic-setting_set .original p{display:none}#settingscategory_form .module:first-of-type{border:none}#settingscategory_form .module:first-of-type .form-row{border:none;padding:0}#settingscategory_form .module:first-of-type .form-row label{display:none}#settingscategory_form .module:first-of-type .form-row p{font-size:22px;margin:0;padding:0}#settingscategory_form .inline-group .tabular .module h2{display:none}#settingscategory_form .inline-group .tabular .module tbody td p{font-size:12px}#settingscategory_form .aligned{margin-bottom:10px}#settingscategory_form .aligned label+div.readonly{margin-left:0;font-size:34px;color:var(--body-quiet-color)}.inline-group .tabular fieldset table tbody tr.form-row td{padding-top:8px}.inline-group .tabular fieldset table tbody tr.form-row td.field-form-errors{margin:2px 0;padding:2px 3px;vertical-align:middle}.powered-by-tethys a{color:#fff} \ No newline at end of file +#container,.dashboard #content{width:auto}#settingscategory_form .inline-group .tabular .module h2,#settingscategory_form .module:first-of-type .form-row label,.dynamic-setting_set .original p{display:none}body{background-color:var(--body-bg);color:var(--body-fg)}ul li{list-style-type:none;font-size:initial;line-height:initial}#content h1{font-size:30px;text-transform:capitalize}#container{margin:0 50px}div.breadcrumbs{background:0 0;border:none;font-size:16px}a:link,a:visited,div.breadcrumbs a{color:#2ca6ed}a:hover{color:#0a84cb}.button.cancel-link{background-color:transparent}.object-tools a:link{background:#0A62A9;font-weight:500;font-size:12px}.object-tools a:link:hover{background-color:#1b73ba}.module caption{font-size:16px;font-weight:400;background:#0a62a9;padding:10px 10px 10px 8px;caption-side:top}.module table{width:100%}.module tr td,.module tr th{padding:8px}.module tr th{font-size:14px;font-weight:400;width:100%}#changelist #changelist-form #result_list tbody th,.inline-group .tabular th{width:auto}.module h2{background:#0A62A9;color:#fff;font-size:14px;line-height:1.5;padding:8px;font-weight:500}thead th{background:var(--darkened-bg)}#content-related .module h2{background:#2ca6ed;color:#fff;font-size:14px;line-height:1.5;padding:8px;font-weight:500}.inline-group h2{background:#0A62A9;color:#fff;font-size:14px;line-height:1.5;padding:8px;font-weight:500}.inline-group .inline-related h3{color:#666;background:#efefef;font-size:12px;line-height:1.5;padding:8px;font-weight:500}.inline-group .tabular tr.add-row td{background:var(--body-bg);padding:8px}#changelist #toolbar{background:var(--darkened-bg);padding:5px}#changelist #toolbar #searchbar{height:30px}#changelist #toolbar form input[type=submit]{padding:4px 6px;background:#0A62A9;color:#fff}#changelist #toolbar form input[type=submit]:hover{background:#1b73ba}#changelist #changelist-filter{z-index:995}#changelist #changelist-filter h2{background:#0A62A9;font-size:16px;line-height:2;padding:8px;font-weight:500;margin-left:-2px}#changelist #changelist-filter h3{color:#666;font-weight:400;font-size:22px}#changelist #changelist-filter li.selected a{color:#2ca6ed!important}#changelist #changelist-form .actions{background:var(--body-bg);border-top:none;padding:5px}#changelist #changelist-form .actions .button{padding:4px 6px;background:#0A62A9;color:#fff}#changelist #changelist-form .actions .button:hover{background:#1b73ba}#changelist #changelist-form .paginator{background:var(--body-bg)}#changelist #changelist-form #result_list thead th{background:var(--darkened-bg);padding:5px}#changelist #changelist-form #result_list thead th.sorted{background:var(--darkened-bg)}#changelist #changelist-form #result_list tbody td{padding:5px}.submit-row{background:0 0;border:none;padding:10px 0}.delete-confirmation form .cancel-link,.submit-row a.deletelink{height:auto}.button,.submit-row,[type=button],[type=submit],input{border-radius:5px}input[type=submit]{background:var(--darkened-bg);border:1px solid #888;font-weight:500;color:var(--body-fg)}input[type=submit]:hover{background:var(--border-color)}input[type=submit][name=_save]{background:#1DA456;border:1px solid #0c9345;color:#fff}input[type=submit][name=_save]:hover{background:#0c9345}#settingscategory_form .module:first-of-type{border:none}#settingscategory_form .module:first-of-type .form-row{border:none;padding:0}#settingscategory_form .module:first-of-type .form-row p{font-size:22px;margin:0;padding:0}#settingscategory_form .inline-group .tabular .module tbody td p{font-size:12px}#settingscategory_form .aligned{margin-bottom:10px}#settingscategory_form .aligned label+div.readonly{margin-left:0;font-size:34px;color:var(--body-quiet-color)}.inline-group .tabular fieldset table tbody tr.form-row td{padding-top:8px}.inline-group .tabular fieldset table tbody tr.form-row td.field-form-errors{margin:2px 0;padding:2px 3px;vertical-align:middle}.powered-by-tethys a{color:#fff} \ No newline at end of file diff --git a/tethys_portal/static/tethys_portal/less/account_form.less b/tethys_portal/static/tethys_portal/less/account_form.less index 1c5a14651..b6a5bf378 100644 --- a/tethys_portal/static/tethys_portal/less/account_form.less +++ b/tethys_portal/static/tethys_portal/less/account_form.less @@ -66,13 +66,26 @@ border-color: rgba(0,0,0,0.2); } } +/* + style for ... -.bi-hydroshare { - background-image:url(/static/tethys_portal/images/hs-icon-sm.png); - background-repeat: no-repeat; - background-size: 75%; - background-position: 4px 5px; -} + .bi-hydroshare { + background-image:url(/static/tethys_portal/images/hs-icon-sm.png); + background-repeat: no-repeat; + background-size: 75%; + background-position: 4px 5px; + } + .bi-arcgis { + background-image: url(/static/tethys_portal/images/hs-icon-sm.png); + background-repeat: no-repeat; + background-size: 75%; + background-position: 4px 5px; + } + + was removed and put into the following template: tethys_portal/templates/tethys_portal/accounts/base.html + This is because it needs to render the staticfiles dynamically in case the STATIC_URL setting is other than /static/ + +*/ .btn-arcgis { color: white; @@ -84,12 +97,6 @@ } } -.bi-arcgis { - background-image: url(/static/tethys_portal/images/arcgis-icon-sm.png); - background-repeat: no-repeat; - background-size: 75%; - background-position: 4px 5px; -} .btn-okta { background-color: #ffffff; @@ -125,32 +132,54 @@ background-color: #ffffff; color: @default-header-text-color; border-color: rgba(0,0,0,0.2); - + .bi-onelogin { - color: @onelogin-color; - background-image:url(/static/tethys_portal/images/onelogin_black_32.png); - background-repeat: no-repeat; - background-size: 75%; - background-position: 4px 5px; + color: @onelogin-color; + /* + Following lines: + + background-image:url(/static/tethys_portal/images/onelogin_black_32.png); + background-repeat: no-repeat; + background-size: 75%; + background-position: 4px 5px; + + were removed and put into the following template: tethys_portal/templates/tethys_portal/accounts/base.html + This is because it needs to render the staticfiles dynamically in case the STATIC_URL setting is other than /static/ + + */ } } .btn-onelogin:hover { background-color: @onelogin-color; color: #ffffff; + /* + Following lines: - .bi-onelogin { - background-image:url(/static/tethys_portal/images/onelogin_white_32.png); - } + .bi-onelogin { + background-image:url(/static/tethys_portal/images/onelogin_white_32.png); + } + + were removed and put into the following template: tethys_portal/templates/tethys_portal/accounts/base.html + This is because it needs to render the staticfiles dynamically in case the STATIC_URL setting is other than /static/ + + */ } .btn-onelogin:focus { background-color: @onelogin-color; color: #ffffff; + /* + Following lines: - .bi-onelogin { - background-image:url(/static/tethys_portal/images/onelogin_white_32.png); - } + .bi-onelogin { + background-image:url(/static/tethys_portal/images/onelogin_white_32.png); + } + + were removed and put into the following template: tethys_portal/templates/tethys_portal/accounts/base.html + This is because it needs to render the staticfiles dynamically in case the STATIC_URL setting is other than /static/ + + */ } .btn-windows { diff --git a/tethys_portal/static/tethys_portal/less/admin_tweaks.less b/tethys_portal/static/tethys_portal/less/admin_tweaks.less index f2822abce..21c886172 100644 --- a/tethys_portal/static/tethys_portal/less/admin_tweaks.less +++ b/tethys_portal/static/tethys_portal/less/admin_tweaks.less @@ -68,19 +68,25 @@ a:hover { background-color: @primary-color + #111111; } } +} +/* + style for ... - a.addlink { - background: @primary-color url(/static/admin/img/tooltag-add.png) 95% center no-repeat; - font-weight: 500; - font-size: 12px; - padding: 3px 12px; + a.addlink { + background: @primary-color url(/static/admin/img/tooltag-add.png) 95% center no-repeat; + font-weight: 500; + font-size: 12px; + padding: 3px 12px; - &:hover { - background: @primary-color + #111111 url(/static/admin/img/tooltag-add.png) 95% center no-repeat; + &:hover { + background: @primary-color + #111111 url(/static/admin/img/tooltag-add.png) 95% center no-repeat; + } } - } -} + was removed and put into the following template: tethys_portal/templates/admin/base.html + This is because it needs to render the staticfiles dynamically in case the STATIC_URL setting is other than /static/ + +*/ .module { caption { diff --git a/tethys_portal/templates/admin/base.html b/tethys_portal/templates/admin/base.html index eb79fdc3d..119b702db 100644 --- a/tethys_portal/templates/admin/base.html +++ b/tethys_portal/templates/admin/base.html @@ -10,6 +10,17 @@ {% if LANGUAGE_BIDI %}{% endif %} {{ block.super }} + + {% endblock %} diff --git a/tethys_portal/templates/tethys_portal/accounts/base.html b/tethys_portal/templates/tethys_portal/accounts/base.html index 399ba39bc..57aa7bcfc 100644 --- a/tethys_portal/templates/tethys_portal/accounts/base.html +++ b/tethys_portal/templates/tethys_portal/accounts/base.html @@ -7,10 +7,51 @@ {% block styles %} {{ block.super }} + + {% endblock %} diff --git a/tethys_portal/urls.py b/tethys_portal/urls.py index a8c5ad570..7c1b9c0c0 100644 --- a/tethys_portal/urls.py +++ b/tethys_portal/urls.py @@ -17,6 +17,8 @@ PasswordResetConfirmView, PasswordResetCompleteView, ) +from django.shortcuts import redirect + from social_django import views as psa_views, urls as psa_urls from tethys_apps.urls import extension_urls @@ -38,14 +40,16 @@ # ensure at least staff users logged in before accessing admin login page from django.contrib.admin.views.decorators import staff_member_required + +prefix_url = f"{settings.PREFIX_URL}" +login_url_setting = f"{settings.LOGIN_URL}" admin.site.login = staff_member_required( - admin.site.login, redirect_field_name="", login_url="/accounts/login/" + admin.site.login, + redirect_field_name="", + login_url=f"{login_url_setting}", ) admin.autodiscover() -admin.site.login = staff_member_required( - admin.site.login, redirect_field_name="", login_url="/accounts/login/" -) # Extend admin urls admin_url_list = admin.site.urls[0] @@ -226,3 +230,24 @@ handler403 = tethys_portal_error.handler_403 handler404 = tethys_portal_error.handler_404 handler500 = tethys_portal_error.handler_500 + +if prefix_url is not None and prefix_url != "/": + urlpatterns = [ + re_path(r"^$", lambda request: redirect(f"{prefix_url}/", permanent=True)), + re_path( + rf"^{prefix_url}/", + include((urlpatterns)), + ), + ] + +if ( + login_url_setting is not None + and login_url_setting != f"/{prefix_url}/accounts/login/" +): + urlpatterns.append( + re_path( + rf"^{login_url_setting.strip('/')}/", + tethys_portal_accounts.login_view, + name="login_prefix", + ) + )