From b029a08926f7fcd8d828396f952142843e6affbf Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Mon, 19 Feb 2024 16:59:22 -0600 Subject: [PATCH 01/18] initial consumerbase class --- tethys_apps/base/app_base.py | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tethys_apps/base/app_base.py b/tethys_apps/base/app_base.py index 0900053b6..91132e22f 100644 --- a/tethys_apps/base/app_base.py +++ b/tethys_apps/base/app_base.py @@ -14,6 +14,7 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.urls import re_path from django.utils.functional import classproperty +from channels.generic.websocket import AsyncWebsocketConsumer from .testing.environment import ( is_testing_environment, @@ -1838,3 +1839,50 @@ def post_delete_app_workspace(cls): """ Override this method to post-process the app workspace after it is emptied """ + + +class ConsumerBase(AsyncWebsocketConsumer): + _authorized = None + permissions = [] + + @property + async def authorized(self): + if self._authorized is None: + self._authorized = True + + return self._authorized + + async def onconnect(self): + """Custom class method to run custom code when user connects to the websocket + """ + pass + + async def ondisconnect(self, event): + """Custom class method to run custom code when user connects to the websocket + """ + pass + + async def onreceive(self, event): + """Custom class method to run custom code when user connects to the websocket + """ + pass + + async def connect(self): + """Class method to handle when user connects to the websocket + """ + await self.accept() + if await self.authorized: + await self.onconnect() + else: + # User not authorized for websocket access + await self.close(code=4004) + + async def disconnect(self, event): + """Class method to handle when user disconnects from the websocket + """ + await self.ondisconnect(event) + + async def receive(self, text_data): + """Class method to handle when websocket receives a message + """ + await self.onreceive(text_data) \ No newline at end of file From e35243d8f1cb03e830131e4885bd41050d19e2b9 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Tue, 20 Feb 2024 10:57:38 -0600 Subject: [PATCH 02/18] new TethysAsyncWebsocketConsumer class with stubbed methods for implementation also stubbed out other custom methods just in case they are needed in the future --- tethys_apps/base/app_base.py | 49 +++++++++++++++++++++++++-------- tethys_apps/base/permissions.py | 23 ++++++++++++++++ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/tethys_apps/base/app_base.py b/tethys_apps/base/app_base.py index 91132e22f..981da6ef4 100644 --- a/tethys_apps/base/app_base.py +++ b/tethys_apps/base/app_base.py @@ -21,7 +21,7 @@ get_test_db_name, TESTING_DB_FLAG, ) -from .permissions import Permission as TethysPermission, PermissionGroup +from .permissions import Permission as TethysPermission, PermissionGroup, scoped_user_has_permission, has_permission from .handoff import HandoffManager from .mixins import TethysBaseMixin from .workspace import get_app_workspace, get_user_workspace @@ -1841,29 +1841,54 @@ def post_delete_app_workspace(cls): """ -class ConsumerBase(AsyncWebsocketConsumer): - _authorized = None +class TethysAsyncWebsocketConsumer(AsyncWebsocketConsumer): + """ + Base class used to create a Django channel websocket consumer for Tethys + + Attributes: + permissions (string, list, tuple): List of permissions required to connect and use the websocket. + """ permissions = [] + _authorized = None + _perms = None + + @property + def perms(self): + if self._perms is None: + if type(self.permissions) in [list, tuple]: + self._perms = self.permissions + elif isinstance(self.permissions, str): + self._perms = self.permissions.split(",") + else: + raise TypeError("permissions must be a list, tuple, or comma separated string") + return self._perms @property async def authorized(self): if self._authorized is None: self._authorized = True - + for perm in self.perms: + if not await scoped_user_has_permission(self.scope, perm): + self._authorized = False return self._authorized - async def onconnect(self): + async def on_authorized_connect(self): """Custom class method to run custom code when user connects to the websocket """ pass - async def ondisconnect(self, event): + async def on_connect(self): """Custom class method to run custom code when user connects to the websocket """ pass - async def onreceive(self, event): - """Custom class method to run custom code when user connects to the websocket + async def on_disconnect(self, event): + """Custom class method to run custom code when user disconnects to the websocket + """ + pass + + async def on_receive(self, event): + """Custom class method to run custom code when websocket receives a message """ pass @@ -1871,8 +1896,10 @@ async def connect(self): """Class method to handle when user connects to the websocket """ await self.accept() + await self.on_connect() + if await self.authorized: - await self.onconnect() + await self.on_authorized_connect() else: # User not authorized for websocket access await self.close(code=4004) @@ -1880,9 +1907,9 @@ async def connect(self): async def disconnect(self, event): """Class method to handle when user disconnects from the websocket """ - await self.ondisconnect(event) + await self.on_disconnect(event) async def receive(self, text_data): """Class method to handle when websocket receives a message """ - await self.onreceive(text_data) \ No newline at end of file + await self.on_receive(text_data) \ No newline at end of file diff --git a/tethys_apps/base/permissions.py b/tethys_apps/base/permissions.py index d7d01a126..2ffe439d1 100644 --- a/tethys_apps/base/permissions.py +++ b/tethys_apps/base/permissions.py @@ -7,6 +7,7 @@ * License: ******************************************************************************** """ +from channels.db import database_sync_to_async class Permission: @@ -139,3 +140,25 @@ def my_controller(request): if user.has_perm(namespaced_perm, app): return True return False + + +@database_sync_to_async +def scoped_user_has_permission(scope, perm): + """_summary_ + + Args: + user_id (_type_): _description_ + + Returns: + _type_: _description_ + """ + from tethys_apps.utilities import get_active_app + + request_url = scope['path'] + user = scope['user'] + + app = get_active_app(url=request_url) + + namespaced_perm = "tethys_apps." + app.package + ":" + perm + + return user.has_perm(namespaced_perm, app) From b6be799ecdeb2f576643d72ab5f6eea4e4b53c15 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Tue, 20 Feb 2024 15:38:43 -0600 Subject: [PATCH 03/18] added tests and stubbed out methods for future development --- .../test_base/test_app_base.py | 83 +++++++++++++++++++ .../test_base/test_permissions.py | 34 ++++++++ tethys_apps/base/__init__.py | 2 +- tethys_apps/base/app_base.py | 2 +- tethys_sdk/base.py | 2 +- 5 files changed, 120 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py b/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py index a17745df7..baa1225cb 100644 --- a/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py +++ b/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py @@ -1480,3 +1480,86 @@ def test_remove_from_db_2(self, mock_ta, mock_log): # Check tethys log error mock_log.error.assert_called() + + +class TestTethysAsyncWebsocketConsumer(unittest.IsolatedAsyncioTestCase): + def setUp(self): + self.consumer = tethys_app_base.TethysAsyncWebsocketConsumer() + self.consumer.permissions = ["test_permission"] + self.consumer.scope = {"user": UserFactory(), "path": "path/to/app"} + + def tearDown(self): + pass + + def test_perms_list(self): + self.assertTrue(self.consumer.perms == ["test_permission"]) + + def test_perms_str(self): + self.consumer.permissions = "test_permission,test_permission1" + self.assertTrue(self.consumer.perms == ["test_permission", "test_permission1"]) + + def test_perms_exception(self): + self.consumer.permissions = {"test": "test_permsision"} + with self.assertRaises(TypeError) as context: + self.consumer.perms + + self.assertTrue(context.exception.args[0] == 'permissions must be a list, tuple, or comma separated string') + + @mock.patch("tethys_apps.base.app_base.scoped_user_has_permission") + async def test_authorized(self, mock_suhp): + self.consumer.permissions = ["test_permission", "test_permission1"] + mock_suhp.side_effect = [True, True] + self.assertTrue(await self.consumer.authorized) + + @mock.patch("tethys_apps.base.app_base.scoped_user_has_permission") + async def test_authorized(self, mock_suhp): + self.consumer.permissions = ["test_permission", "test_permission1"] + mock_suhp.side_effect = [True, False] + self.assertFalse(await self.consumer.authorized) + + async def test_on_authorized_connect(self): + await self.consumer.on_authorized_connect() + + async def test_on_connect(self): + await self.consumer.on_connect() + + async def test_on_disconnect(self): + event = {} + await self.consumer.on_disconnect(event) + + async def test_on_receive(self): + event = {} + await self.consumer.on_receive(event) + + @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_authorized_connect") + @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_connect") + @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.accept") + async def test_connect(self, mock_accept, mock_on_connect, mock_on_authorized_connect): + self.consumer._authorized = True + await self.consumer.connect() + mock_accept.assert_called_once() + mock_on_connect.assert_called_once() + mock_on_authorized_connect.assert_called_once() + + @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.close") + @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_connect") + @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.accept") + async def test_connect_not_authorized(self, mock_accept, mock_on_connect, mock_close): + self.consumer._authorized = False + await self.consumer.connect() + mock_accept.assert_called_once() + mock_on_connect.assert_called_once() + mock_close.assert_called_with(code=4004) + + @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_disconnect") + async def test_disconnect(self, mock_on_disconnect): + event = "event" + await self.consumer.disconnect(event) + mock_on_disconnect.assert_called_with(event) + + @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_receive") + async def test_receive(self, mock_on_receive): + text_data = "text_data" + await self.consumer.receive(text_data) + mock_on_receive.assert_called_with(text_data) + diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_permissions.py b/tests/unit_tests/test_tethys_apps/test_base/test_permissions.py index 37533a6df..425046d47 100644 --- a/tests/unit_tests/test_tethys_apps/test_base/test_permissions.py +++ b/tests/unit_tests/test_tethys_apps/test_base/test_permissions.py @@ -103,3 +103,37 @@ def test_has_permission_no(self, mock_app): mock_app.return_value = mock.MagicMock(package="test_package") result = tethys_permission.has_permission(request=request, perm="test_perm") self.assertFalse(result) + + +class TestAsyncPermissionGroup(unittest.IsolatedAsyncioTestCase): + def setUp(self): + self.user = UserFactory() + self.request_factory = RequestFactory() + self.name = "test_name" + self.permissions = ["foo", "bar"] + self.check_string = ''.format(self.name) + + def tearDown(self): + pass + + @mock.patch("tethys_apps.utilities.get_active_app") + async def test_scoped_user_has_permission(self, mock_app): + self.user.has_perm = mock.MagicMock(return_value=True) + scope = { + "user": self.user, + "path": "some/url/path" + } + mock_app.return_value = mock.MagicMock(package="test_package") + result = await tethys_permission.scoped_user_has_permission(scope=scope, perm="test_perm") + self.assertTrue(result) + + @mock.patch("tethys_apps.utilities.get_active_app") + async def test_scoped_user_has_permission_no(self, mock_app): + self.user.has_perm = mock.MagicMock(return_value=False) + scope = { + "user": self.user, + "path": "some/url/path" + } + mock_app.return_value = mock.MagicMock(package="test_package") + result = await tethys_permission.scoped_user_has_permission(scope=scope, perm="test_perm") + self.assertFalse(result) diff --git a/tethys_apps/base/__init__.py b/tethys_apps/base/__init__.py index 377f89e55..1786f1a03 100644 --- a/tethys_apps/base/__init__.py +++ b/tethys_apps/base/__init__.py @@ -8,7 +8,7 @@ ******************************************************************************** """ # DO NOT ERASE -from tethys_apps.base.app_base import TethysAppBase, TethysExtensionBase # noqa: F401 +from tethys_apps.base.app_base import TethysAppBase, TethysExtensionBase, TethysAsyncWebsocketConsumer # noqa: F401 from tethys_apps.base.bokeh_handler import with_request, with_workspaces # noqa: F401 from tethys_apps.base.url_map import url_map_maker # noqa: F401 from tethys_apps.base.workspace import TethysWorkspace # noqa: F401 diff --git a/tethys_apps/base/app_base.py b/tethys_apps/base/app_base.py index 981da6ef4..3335243b2 100644 --- a/tethys_apps/base/app_base.py +++ b/tethys_apps/base/app_base.py @@ -21,7 +21,7 @@ get_test_db_name, TESTING_DB_FLAG, ) -from .permissions import Permission as TethysPermission, PermissionGroup, scoped_user_has_permission, has_permission +from .permissions import Permission as TethysPermission, PermissionGroup, scoped_user_has_permission from .handoff import HandoffManager from .mixins import TethysBaseMixin from .workspace import get_app_workspace, get_user_workspace diff --git a/tethys_sdk/base.py b/tethys_sdk/base.py index 11b0aa363..f9be7b8d4 100644 --- a/tethys_sdk/base.py +++ b/tethys_sdk/base.py @@ -8,7 +8,7 @@ # flake8: noqa # DO NOT ERASE -from tethys_apps.base import TethysAppBase, TethysExtensionBase +from tethys_apps.base import TethysAppBase, TethysExtensionBase, TethysAsyncWebsocketConsumer from tethys_apps.base.url_map import url_map_maker from tethys_apps.base.controller import TethysController from tethys_apps.base.bokeh_handler import with_request, with_workspaces From 92a8a9347082a121ec7b9b11f734c06c91e9228a Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Tue, 20 Feb 2024 16:56:34 -0600 Subject: [PATCH 04/18] updated docs to use new method --- docs/tutorials/websockets.rst | 20 ++++++++++--------- .../test_base/test_app_base.py | 8 ++++++++ tethys_apps/base/app_base.py | 3 ++- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/tutorials/websockets.rst b/docs/tutorials/websockets.rst index 1e95dcea8..794bc72ce 100644 --- a/docs/tutorials/websockets.rst +++ b/docs/tutorials/websockets.rst @@ -37,17 +37,18 @@ a. Create a new file called ``consumers.py`` and add the following code: .. code-block:: python - from channels.generic.websocket import AsyncWebsocketConsumer + from tethys_sdk.base import TethysAsyncWebsocketConsumer from tethys_sdk.routing import consumer @consumer(name='dam_notification', url='dams/notifications') - class NotificationsConsumer(AsyncWebsocketConsumer): - async def connect(self): - await self.accept() + class NotificationsConsumer(TethysAsyncWebsocketConsumer): + permissions = [] + + async def on_authorized_connect(self): print("-----------WebSocket Connected-----------") - async def disconnect(self, close_code): + async def on_disconnect(self, close_code): pass .. note:: @@ -93,13 +94,14 @@ a. Update the ``consumer class`` to look like this. ... @consumer(name='dam_notification', url='dams/notifications') - class NotificationsConsumer(AsyncWebsocketConsumer): - async def connect(self): - await self.accept() + class NotificationsConsumer(TethysAsyncWebsocketConsumer): + permissions = [] + + async def on_authorized_connect(self): await self.channel_layer.group_add("notifications", self.channel_name) print(f"Added {self.channel_name} channel to notifications") - async def disconnect(self, close_code): + async def on_disconnect(self, close_code): await self.channel_layer.group_discard("notifications", self.channel_name) print(f"Removed {self.channel_name} channel from notifications") diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py b/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py index baa1225cb..7fc2c00f0 100644 --- a/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py +++ b/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py @@ -1559,7 +1559,15 @@ async def test_disconnect(self, mock_on_disconnect): @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_receive") async def test_receive(self, mock_on_receive): + self.consumer._authorized = True text_data = "text_data" await self.consumer.receive(text_data) mock_on_receive.assert_called_with(text_data) + @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_receive") + async def test_receive_not_authorized(self, mock_on_receive): + self.consumer._authorized = False + text_data = "text_data" + await self.consumer.receive(text_data) + mock_on_receive.assert_not_called() + diff --git a/tethys_apps/base/app_base.py b/tethys_apps/base/app_base.py index 3335243b2..7a7223e18 100644 --- a/tethys_apps/base/app_base.py +++ b/tethys_apps/base/app_base.py @@ -1912,4 +1912,5 @@ async def disconnect(self, event): async def receive(self, text_data): """Class method to handle when websocket receives a message """ - await self.on_receive(text_data) \ No newline at end of file + if await self.authorized: + await self.on_receive(text_data) \ No newline at end of file From cdd26b002489101e0206cbad512d480b86c0f701 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Wed, 21 Feb 2024 15:11:03 -0600 Subject: [PATCH 05/18] linted code and ran black formatter --- tests/unit_tests/__init__.py | 1 + .../test_base/test_app_base.py | 20 ++++++--- .../test_base/test_permissions.py | 18 ++++---- .../test_models/test_TethysApp.py | 1 + .../test_models/test_CondorScheduler.py | 1 + .../test_dask/test_DaskJobResult.py | 1 + .../test_dask/test_DaskScheduler.py | 1 + .../test_gizmo_options/test_base.py | 1 + tethys_apps/admin.py | 1 + tethys_apps/app_installation.py | 1 + tethys_apps/apps.py | 1 + tethys_apps/base/__init__.py | 7 +++- tethys_apps/base/app_base.py | 42 +++++++++---------- tethys_apps/base/bokeh_handler.py | 1 + tethys_apps/base/handoff.py | 9 ++-- tethys_apps/base/permissions.py | 5 ++- tethys_apps/base/url_map.py | 1 + tethys_apps/context_processors.py | 1 + tethys_apps/decorators.py | 1 + tethys_apps/harvester.py | 7 ++-- .../management/commands/collectworkspaces.py | 1 + .../management/commands/pre_collectstatic.py | 1 + tethys_apps/management/commands/syncstores.py | 1 + .../commands/tethys_app_uninstall.py | 1 + tethys_apps/models.py | 1 + tethys_apps/static_finders.py | 1 + tethys_apps/template_loaders.py | 1 + tethys_apps/urls.py | 1 + tethys_apps/utilities.py | 1 + tethys_apps/views.py | 1 + tethys_cli/__init__.py | 1 + tethys_cli/db_commands.py | 1 + tethys_cli/docker_commands.py | 1 + tethys_cli/gen_commands.py | 1 + tethys_cli/settings_commands.py | 1 + tethys_compute/__init__.py | 1 + tethys_compute/admin.py | 1 + tethys_compute/apps.py | 1 + tethys_compute/job_manager.py | 1 + tethys_compute/models/__init__.py | 1 + tethys_compute/models/basic_job.py | 1 + tethys_compute/models/condor/condor_base.py | 1 + tethys_compute/models/condor/condor_job.py | 1 + tethys_compute/models/condor/condor_py_job.py | 1 + .../models/condor/condor_py_workflow.py | 1 + .../models/condor/condor_scheduler.py | 1 + .../models/condor/condor_workflow.py | 1 + .../models/condor/condor_workflow_job_node.py | 1 + .../models/condor/condor_workflow_node.py | 1 + tethys_compute/models/dask/dask_job.py | 1 + tethys_compute/models/dask/dask_scheduler.py | 1 + tethys_compute/models/scheduler.py | 1 + tethys_compute/models/tethys_job.py | 1 + tethys_compute/scheduler_manager.py | 1 + tethys_config/__init__.py | 1 + tethys_config/admin.py | 1 + tethys_config/apps.py | 1 + tethys_config/context_processors.py | 1 + tethys_config/models.py | 1 + tethys_gizmos/admin.py | 1 + tethys_gizmos/gizmo_options/__init__.py | 1 + tethys_gizmos/gizmo_options/base.py | 1 + tethys_gizmos/gizmo_options/button.py | 1 + tethys_gizmos/gizmo_options/datatable_view.py | 1 + tethys_gizmos/gizmo_options/date_picker.py | 1 + tethys_gizmos/gizmo_options/map_view.py | 1 + tethys_gizmos/gizmo_options/message_box.py | 1 + tethys_gizmos/gizmo_options/range_slider.py | 1 + tethys_gizmos/gizmo_options/select_input.py | 1 + tethys_gizmos/gizmo_options/table_view.py | 1 + tethys_gizmos/gizmo_options/text_input.py | 1 + tethys_gizmos/gizmo_options/toggle_switch.py | 1 + tethys_gizmos/templatetags/tethys_gizmos.py | 1 + tethys_gizmos/urls.py | 1 + tethys_layouts/mixins/map_layout.py | 12 +++--- tethys_layouts/views/map_layout.py | 1 + tethys_layouts/views/tethys_layout.py | 1 + tethys_portal/__init__.py | 1 + tethys_portal/asgi.py | 1 + tethys_portal/forms.py | 1 + tethys_portal/middleware.py | 1 + tethys_portal/urls.py | 1 + tethys_portal/utilities.py | 1 + tethys_portal/views/accounts.py | 1 + tethys_portal/views/api.py | 10 +++-- tethys_portal/views/error.py | 1 + tethys_portal/views/home.py | 1 + tethys_portal/views/user.py | 1 + tethys_quotas/__init__.py | 1 + tethys_quotas/admin.py | 1 + tethys_quotas/apps.py | 1 + tethys_quotas/decorators.py | 1 + tethys_quotas/handlers/base.py | 1 + tethys_quotas/handlers/workspace.py | 1 + tethys_quotas/models/__init__.py | 1 + tethys_quotas/models/entity_quota.py | 1 + tethys_quotas/models/resource_quota.py | 1 + tethys_quotas/models/tethys_app_quota.py | 1 + tethys_quotas/models/user_quota.py | 1 + tethys_quotas/utilities.py | 1 + tethys_sdk/__init__.py | 1 + tethys_sdk/app_settings.py | 1 + tethys_sdk/base.py | 6 ++- tethys_sdk/compute.py | 1 + tethys_sdk/gizmos.py | 1 + tethys_sdk/handoff.py | 1 + tethys_sdk/jobs.py | 1 + tethys_sdk/layouts.py | 1 + tethys_sdk/permissions.py | 1 + tethys_sdk/services.py | 22 +++++++++- tethys_sdk/testing.py | 1 + tethys_sdk/workspaces.py | 1 + tethys_services/__init__.py | 1 + tethys_services/admin.py | 1 + tethys_services/apps.py | 1 + tethys_services/backends/arcgis_portal.py | 1 + tethys_services/backends/hydroshare.py | 1 + tethys_services/backends/hydroshare_beta.py | 1 + .../backends/hydroshare_playground.py | 1 + tethys_services/models.py | 1 + tethys_services/urls.py | 1 + tethys_services/utilities.py | 1 + tethys_services/views.py | 1 + 123 files changed, 211 insertions(+), 59 deletions(-) diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py index d3ed76d4f..ea66f1d5d 100644 --- a/tests/unit_tests/__init__.py +++ b/tests/unit_tests/__init__.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import uuid import factory from unittest import mock diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py b/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py index 7fc2c00f0..c93705ae1 100644 --- a/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py +++ b/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py @@ -1503,7 +1503,10 @@ def test_perms_exception(self): with self.assertRaises(TypeError) as context: self.consumer.perms - self.assertTrue(context.exception.args[0] == 'permissions must be a list, tuple, or comma separated string') + self.assertTrue( + context.exception.args[0] + == "permissions must be a list, tuple, or comma separated string" + ) @mock.patch("tethys_apps.base.app_base.scoped_user_has_permission") async def test_authorized(self, mock_suhp): @@ -1512,7 +1515,7 @@ async def test_authorized(self, mock_suhp): self.assertTrue(await self.consumer.authorized) @mock.patch("tethys_apps.base.app_base.scoped_user_has_permission") - async def test_authorized(self, mock_suhp): + async def test_authorized_not(self, mock_suhp): self.consumer.permissions = ["test_permission", "test_permission1"] mock_suhp.side_effect = [True, False] self.assertFalse(await self.consumer.authorized) @@ -1531,10 +1534,14 @@ async def test_on_receive(self): event = {} await self.consumer.on_receive(event) - @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_authorized_connect") + @mock.patch( + "tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_authorized_connect" + ) @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_connect") @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.accept") - async def test_connect(self, mock_accept, mock_on_connect, mock_on_authorized_connect): + async def test_connect( + self, mock_accept, mock_on_connect, mock_on_authorized_connect + ): self.consumer._authorized = True await self.consumer.connect() mock_accept.assert_called_once() @@ -1544,7 +1551,9 @@ async def test_connect(self, mock_accept, mock_on_connect, mock_on_authorized_co @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.close") @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_connect") @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.accept") - async def test_connect_not_authorized(self, mock_accept, mock_on_connect, mock_close): + async def test_connect_not_authorized( + self, mock_accept, mock_on_connect, mock_close + ): self.consumer._authorized = False await self.consumer.connect() mock_accept.assert_called_once() @@ -1570,4 +1579,3 @@ async def test_receive_not_authorized(self, mock_on_receive): text_data = "text_data" await self.consumer.receive(text_data) mock_on_receive.assert_not_called() - diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_permissions.py b/tests/unit_tests/test_tethys_apps/test_base/test_permissions.py index 425046d47..bf812fc80 100644 --- a/tests/unit_tests/test_tethys_apps/test_base/test_permissions.py +++ b/tests/unit_tests/test_tethys_apps/test_base/test_permissions.py @@ -119,21 +119,19 @@ def tearDown(self): @mock.patch("tethys_apps.utilities.get_active_app") async def test_scoped_user_has_permission(self, mock_app): self.user.has_perm = mock.MagicMock(return_value=True) - scope = { - "user": self.user, - "path": "some/url/path" - } + scope = {"user": self.user, "path": "some/url/path"} mock_app.return_value = mock.MagicMock(package="test_package") - result = await tethys_permission.scoped_user_has_permission(scope=scope, perm="test_perm") + result = await tethys_permission.scoped_user_has_permission( + scope=scope, perm="test_perm" + ) self.assertTrue(result) @mock.patch("tethys_apps.utilities.get_active_app") async def test_scoped_user_has_permission_no(self, mock_app): self.user.has_perm = mock.MagicMock(return_value=False) - scope = { - "user": self.user, - "path": "some/url/path" - } + scope = {"user": self.user, "path": "some/url/path"} mock_app.return_value = mock.MagicMock(package="test_package") - result = await tethys_permission.scoped_user_has_permission(scope=scope, perm="test_perm") + result = await tethys_permission.scoped_user_has_permission( + scope=scope, perm="test_perm" + ) self.assertFalse(result) diff --git a/tests/unit_tests/test_tethys_apps/test_models/test_TethysApp.py b/tests/unit_tests/test_tethys_apps/test_models/test_TethysApp.py index a94c665a2..078bdf270 100644 --- a/tests/unit_tests/test_tethys_apps/test_models/test_TethysApp.py +++ b/tests/unit_tests/test_tethys_apps/test_models/test_TethysApp.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from tethys_sdk.testing import TethysTestCase from tethys_apps.models import ( TethysApp, diff --git a/tests/unit_tests/test_tethys_compute/test_models/test_CondorScheduler.py b/tests/unit_tests/test_tethys_compute/test_models/test_CondorScheduler.py index da34163c5..ec3893103 100644 --- a/tests/unit_tests/test_tethys_compute/test_models/test_CondorScheduler.py +++ b/tests/unit_tests/test_tethys_compute/test_models/test_CondorScheduler.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from tethys_apps.base.testing.testing import TethysTestCase from tethys_compute.models import Scheduler, CondorScheduler diff --git a/tests/unit_tests/test_tethys_compute/test_models/test_dask/test_DaskJobResult.py b/tests/unit_tests/test_tethys_compute/test_models/test_dask/test_DaskJobResult.py index c14bea3fb..8b32e024a 100644 --- a/tests/unit_tests/test_tethys_compute/test_models/test_dask/test_DaskJobResult.py +++ b/tests/unit_tests/test_tethys_compute/test_models/test_dask/test_DaskJobResult.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from tethys_sdk.testing import TethysTestCase from tethys_compute.models.dask.dask_scheduler import DaskScheduler from tethys_compute.models.dask.dask_job import DaskJob diff --git a/tests/unit_tests/test_tethys_compute/test_models/test_dask/test_DaskScheduler.py b/tests/unit_tests/test_tethys_compute/test_models/test_dask/test_DaskScheduler.py index acaa5cbf3..d2285235a 100644 --- a/tests/unit_tests/test_tethys_compute/test_models/test_dask/test_DaskScheduler.py +++ b/tests/unit_tests/test_tethys_compute/test_models/test_dask/test_DaskScheduler.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from tethys_apps.base.testing.testing import TethysTestCase from tethys_compute.models import Scheduler, DaskScheduler from unittest import mock diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_base.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_base.py index 58758127c..3d680b92a 100644 --- a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_base.py +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_base.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import unittest import tethys_gizmos.gizmo_options.base as basetest diff --git a/tethys_apps/admin.py b/tethys_apps/admin.py index e8b7ad865..a9ddb27b4 100644 --- a/tethys_apps/admin.py +++ b/tethys_apps/admin.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import json import logging from django import forms diff --git a/tethys_apps/app_installation.py b/tethys_apps/app_installation.py index 4c52eb2e1..75cef1352 100644 --- a/tethys_apps/app_installation.py +++ b/tethys_apps/app_installation.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import os diff --git a/tethys_apps/apps.py b/tethys_apps/apps.py index 108cae352..3da1fd33a 100644 --- a/tethys_apps/apps.py +++ b/tethys_apps/apps.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import sys from django.apps import AppConfig diff --git a/tethys_apps/base/__init__.py b/tethys_apps/base/__init__.py index 1786f1a03..ce537bd37 100644 --- a/tethys_apps/base/__init__.py +++ b/tethys_apps/base/__init__.py @@ -7,8 +7,13 @@ * License: BSD 2-Clause ******************************************************************************** """ + # DO NOT ERASE -from tethys_apps.base.app_base import TethysAppBase, TethysExtensionBase, TethysAsyncWebsocketConsumer # noqa: F401 +from tethys_apps.base.app_base import ( # noqa: F401 + TethysAppBase, + TethysExtensionBase, + TethysAsyncWebsocketConsumer, +) from tethys_apps.base.bokeh_handler import with_request, with_workspaces # noqa: F401 from tethys_apps.base.url_map import url_map_maker # noqa: F401 from tethys_apps.base.workspace import TethysWorkspace # noqa: F401 diff --git a/tethys_apps/base/app_base.py b/tethys_apps/base/app_base.py index 7a7223e18..0e21b4592 100644 --- a/tethys_apps/base/app_base.py +++ b/tethys_apps/base/app_base.py @@ -21,7 +21,11 @@ get_test_db_name, TESTING_DB_FLAG, ) -from .permissions import Permission as TethysPermission, PermissionGroup, scoped_user_has_permission +from .permissions import ( + Permission as TethysPermission, + PermissionGroup, + scoped_user_has_permission, +) from .handoff import HandoffManager from .mixins import TethysBaseMixin from .workspace import get_app_workspace, get_user_workspace @@ -1848,6 +1852,7 @@ class TethysAsyncWebsocketConsumer(AsyncWebsocketConsumer): Attributes: permissions (string, list, tuple): List of permissions required to connect and use the websocket. """ + permissions = [] _authorized = None _perms = None @@ -1860,7 +1865,9 @@ def perms(self): elif isinstance(self.permissions, str): self._perms = self.permissions.split(",") else: - raise TypeError("permissions must be a list, tuple, or comma separated string") + raise TypeError( + "permissions must be a list, tuple, or comma separated string" + ) return self._perms @property @@ -1871,30 +1878,25 @@ async def authorized(self): if not await scoped_user_has_permission(self.scope, perm): self._authorized = False return self._authorized - + async def on_authorized_connect(self): - """Custom class method to run custom code when user connects to the websocket - """ + """Custom class method to run custom code when user connects to the websocket""" pass - + async def on_connect(self): - """Custom class method to run custom code when user connects to the websocket - """ + """Custom class method to run custom code when user connects to the websocket""" pass - + async def on_disconnect(self, event): - """Custom class method to run custom code when user disconnects to the websocket - """ + """Custom class method to run custom code when user disconnects to the websocket""" pass - + async def on_receive(self, event): - """Custom class method to run custom code when websocket receives a message - """ + """Custom class method to run custom code when websocket receives a message""" pass async def connect(self): - """Class method to handle when user connects to the websocket - """ + """Class method to handle when user connects to the websocket""" await self.accept() await self.on_connect() @@ -1905,12 +1907,10 @@ async def connect(self): await self.close(code=4004) async def disconnect(self, event): - """Class method to handle when user disconnects from the websocket - """ + """Class method to handle when user disconnects from the websocket""" await self.on_disconnect(event) async def receive(self, text_data): - """Class method to handle when websocket receives a message - """ + """Class method to handle when websocket receives a message""" if await self.authorized: - await self.on_receive(text_data) \ No newline at end of file + await self.on_receive(text_data) diff --git a/tethys_apps/base/bokeh_handler.py b/tethys_apps/base/bokeh_handler.py index cbe0c4433..c36a8a22f 100644 --- a/tethys_apps/base/bokeh_handler.py +++ b/tethys_apps/base/bokeh_handler.py @@ -6,6 +6,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # Native Imports from functools import wraps diff --git a/tethys_apps/base/handoff.py b/tethys_apps/base/handoff.py index 3dc94403a..944605d51 100644 --- a/tethys_apps/base/handoff.py +++ b/tethys_apps/base/handoff.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import inspect import json from django.shortcuts import redirect @@ -124,10 +125,10 @@ def handoff( json.dumps(error), content_type="application/javascript" ) - error[ - "message" - ] = "HTTP 400 Bad Request: No handoff handler '{0}' for app '{1}' found.".format( - manager.app.name, handler_name + error["message"] = ( + "HTTP 400 Bad Request: No handoff handler '{0}' for app '{1}' found.".format( + manager.app.name, handler_name + ) ) return HttpResponseBadRequest( json.dumps(error), content_type="application/javascript" diff --git a/tethys_apps/base/permissions.py b/tethys_apps/base/permissions.py index 2ffe439d1..e85c87c63 100644 --- a/tethys_apps/base/permissions.py +++ b/tethys_apps/base/permissions.py @@ -7,6 +7,7 @@ * License: ******************************************************************************** """ + from channels.db import database_sync_to_async @@ -154,8 +155,8 @@ def scoped_user_has_permission(scope, perm): """ from tethys_apps.utilities import get_active_app - request_url = scope['path'] - user = scope['user'] + request_url = scope["path"] + user = scope["user"] app = get_active_app(url=request_url) diff --git a/tethys_apps/base/url_map.py b/tethys_apps/base/url_map.py index 37f9339f8..fd89546fe 100644 --- a/tethys_apps/base/url_map.py +++ b/tethys_apps/base/url_map.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + DEFAULT_EXPRESSION = r"[0-9A-Za-z-_.]+" diff --git a/tethys_apps/context_processors.py b/tethys_apps/context_processors.py index 049d3ce44..eebf6d508 100644 --- a/tethys_apps/context_processors.py +++ b/tethys_apps/context_processors.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from tethys_apps.utilities import get_active_app from tethys_portal.dependencies import vendor_static_dependencies diff --git a/tethys_apps/decorators.py b/tethys_apps/decorators.py index 12c837bb4..591ba11c9 100644 --- a/tethys_apps/decorators.py +++ b/tethys_apps/decorators.py @@ -7,6 +7,7 @@ * License: ******************************************************************************** """ + from functools import wraps from urllib.parse import urlparse diff --git a/tethys_apps/harvester.py b/tethys_apps/harvester.py index cc0ec3830..7e8d07030 100644 --- a/tethys_apps/harvester.py +++ b/tethys_apps/harvester.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import inspect import logging import pkgutil @@ -208,9 +209,9 @@ def _harvest_extension_instances(self, extension_packages): # compile valid apps if validated_ext_instance: valid_ext_instances.append(validated_ext_instance) - valid_extension_modules[ - extension_name - ] = extension_package + valid_extension_modules[extension_name] = ( + extension_package + ) # Notify user that the app has been loaded loaded_extensions.append(extension_name) diff --git a/tethys_apps/management/commands/collectworkspaces.py b/tethys_apps/management/commands/collectworkspaces.py index a673ff0be..d38881a4b 100644 --- a/tethys_apps/management/commands/collectworkspaces.py +++ b/tethys_apps/management/commands/collectworkspaces.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import os import shutil diff --git a/tethys_apps/management/commands/pre_collectstatic.py b/tethys_apps/management/commands/pre_collectstatic.py index e0cb0075e..e276a015e 100644 --- a/tethys_apps/management/commands/pre_collectstatic.py +++ b/tethys_apps/management/commands/pre_collectstatic.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import os import shutil diff --git a/tethys_apps/management/commands/syncstores.py b/tethys_apps/management/commands/syncstores.py index 6c9a5ee5f..b083e9bbb 100644 --- a/tethys_apps/management/commands/syncstores.py +++ b/tethys_apps/management/commands/syncstores.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.core.management.base import BaseCommand from tethys_cli.cli_colors import TC_BLUE, TC_WARNING, TC_ENDC diff --git a/tethys_apps/management/commands/tethys_app_uninstall.py b/tethys_apps/management/commands/tethys_app_uninstall.py index fab965052..0783ee99b 100644 --- a/tethys_apps/management/commands/tethys_app_uninstall.py +++ b/tethys_apps/management/commands/tethys_app_uninstall.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import os import site import subprocess diff --git a/tethys_apps/models.py b/tethys_apps/models.py index ae8b01df0..ad1b45610 100644 --- a/tethys_apps/models.py +++ b/tethys_apps/models.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.dispatch import receiver import logging import uuid diff --git a/tethys_apps/static_finders.py b/tethys_apps/static_finders.py index a2adcc7b3..4ea35bae1 100644 --- a/tethys_apps/static_finders.py +++ b/tethys_apps/static_finders.py @@ -7,6 +7,7 @@ * License: ******************************************************************************** """ + import os from collections import OrderedDict as SortedDict from django.contrib.staticfiles import utils diff --git a/tethys_apps/template_loaders.py b/tethys_apps/template_loaders.py index b9a962d79..5b3226b32 100644 --- a/tethys_apps/template_loaders.py +++ b/tethys_apps/template_loaders.py @@ -7,6 +7,7 @@ * License: ******************************************************************************** """ + import io import errno from django.core.exceptions import SuspiciousFileOperation diff --git a/tethys_apps/urls.py b/tethys_apps/urls.py index 64512a808..442df2ef0 100644 --- a/tethys_apps/urls.py +++ b/tethys_apps/urls.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import logging from django.urls import include, re_path from channels.routing import URLRouter diff --git a/tethys_apps/utilities.py b/tethys_apps/utilities.py index dc3c14c27..f976716c1 100644 --- a/tethys_apps/utilities.py +++ b/tethys_apps/utilities.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import importlib import logging import os diff --git a/tethys_apps/views.py b/tethys_apps/views.py index ccbed923e..29938d139 100644 --- a/tethys_apps/views.py +++ b/tethys_apps/views.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import logging from django.shortcuts import render from django.http import HttpResponse, JsonResponse diff --git a/tethys_cli/__init__.py b/tethys_cli/__init__.py index 9dbad7c7a..397853a24 100644 --- a/tethys_cli/__init__.py +++ b/tethys_cli/__init__.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # Commandline interface for Tethys import argparse diff --git a/tethys_cli/db_commands.py b/tethys_cli/db_commands.py index fee76b98a..dd6c424aa 100644 --- a/tethys_cli/db_commands.py +++ b/tethys_cli/db_commands.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from pathlib import Path import shutil import argparse diff --git a/tethys_cli/docker_commands.py b/tethys_cli/docker_commands.py index c78aa5b9e..b564f2b10 100644 --- a/tethys_cli/docker_commands.py +++ b/tethys_cli/docker_commands.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import os import json from abc import ABC, abstractmethod diff --git a/tethys_cli/gen_commands.py b/tethys_cli/gen_commands.py index 3d2745071..049a94006 100644 --- a/tethys_cli/gen_commands.py +++ b/tethys_cli/gen_commands.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import json import os import string diff --git a/tethys_cli/settings_commands.py b/tethys_cli/settings_commands.py index 753cfe23c..4be4c3181 100644 --- a/tethys_cli/settings_commands.py +++ b/tethys_cli/settings_commands.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from pathlib import Path from pprint import pformat from argparse import Namespace diff --git a/tethys_compute/__init__.py b/tethys_compute/__init__.py index 9918d7901..f74c0ced3 100644 --- a/tethys_compute/__init__.py +++ b/tethys_compute/__init__.py @@ -7,4 +7,5 @@ * License: BSD 2-Clause ******************************************************************************** """ + default_app_config = "tethys_compute.apps.TethysComputeConfig" diff --git a/tethys_compute/admin.py b/tethys_compute/admin.py index cd078d20f..ce4e91d25 100644 --- a/tethys_compute/admin.py +++ b/tethys_compute/admin.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.contrib import admin from django.utils.html import format_html from django import forms diff --git a/tethys_compute/apps.py b/tethys_compute/apps.py index 832ac2034..aaba8606e 100644 --- a/tethys_compute/apps.py +++ b/tethys_compute/apps.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.apps import AppConfig diff --git a/tethys_compute/job_manager.py b/tethys_compute/job_manager.py index 9c022f842..ea3aa10a3 100644 --- a/tethys_compute/job_manager.py +++ b/tethys_compute/job_manager.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import logging diff --git a/tethys_compute/models/__init__.py b/tethys_compute/models/__init__.py index 371bd881e..7865ed1ba 100644 --- a/tethys_compute/models/__init__.py +++ b/tethys_compute/models/__init__.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.dispatch import receiver from django.db.models.signals import post_save diff --git a/tethys_compute/models/basic_job.py b/tethys_compute/models/basic_job.py index a2d295f3c..8dce775b0 100644 --- a/tethys_compute/models/basic_job.py +++ b/tethys_compute/models/basic_job.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from tethys_compute.models.tethys_job import TethysJob diff --git a/tethys_compute/models/condor/condor_base.py b/tethys_compute/models/condor/condor_base.py index 9f299b3b4..1cc5c5b3d 100644 --- a/tethys_compute/models/condor/condor_base.py +++ b/tethys_compute/models/condor/condor_base.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import os from abc import abstractmethod from pathlib import Path diff --git a/tethys_compute/models/condor/condor_job.py b/tethys_compute/models/condor/condor_job.py index 694c44fc5..6cbf5cf97 100644 --- a/tethys_compute/models/condor/condor_job.py +++ b/tethys_compute/models/condor/condor_job.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import shutil import logging diff --git a/tethys_compute/models/condor/condor_py_job.py b/tethys_compute/models/condor/condor_py_job.py index 6baf2e2e3..434f858fe 100644 --- a/tethys_compute/models/condor/condor_py_job.py +++ b/tethys_compute/models/condor/condor_py_job.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from tethys_portal.optional_dependencies import optional_import import os diff --git a/tethys_compute/models/condor/condor_py_workflow.py b/tethys_compute/models/condor/condor_py_workflow.py index 4cd4769c2..4f1f7b09c 100644 --- a/tethys_compute/models/condor/condor_py_workflow.py +++ b/tethys_compute/models/condor/condor_py_workflow.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from tethys_portal.optional_dependencies import optional_import from django.db import models diff --git a/tethys_compute/models/condor/condor_scheduler.py b/tethys_compute/models/condor/condor_scheduler.py index ef38db763..a3b5fb824 100644 --- a/tethys_compute/models/condor/condor_scheduler.py +++ b/tethys_compute/models/condor/condor_scheduler.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from django.db import models from tethys_compute.models.scheduler import Scheduler diff --git a/tethys_compute/models/condor/condor_workflow.py b/tethys_compute/models/condor/condor_workflow.py index 68ab94020..2f55efb24 100644 --- a/tethys_compute/models/condor/condor_workflow.py +++ b/tethys_compute/models/condor/condor_workflow.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import shutil import logging import os diff --git a/tethys_compute/models/condor/condor_workflow_job_node.py b/tethys_compute/models/condor/condor_workflow_job_node.py index 9700d9a42..d0e0ba033 100644 --- a/tethys_compute/models/condor/condor_workflow_job_node.py +++ b/tethys_compute/models/condor/condor_workflow_job_node.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from django.db.models.signals import pre_save from django.dispatch import receiver diff --git a/tethys_compute/models/condor/condor_workflow_node.py b/tethys_compute/models/condor/condor_workflow_node.py index fef7c190c..da2560f17 100644 --- a/tethys_compute/models/condor/condor_workflow_node.py +++ b/tethys_compute/models/condor/condor_workflow_node.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from abc import abstractmethod from django.db import models diff --git a/tethys_compute/models/dask/dask_job.py b/tethys_compute/models/dask/dask_job.py index 4f0be22a9..d08dac141 100644 --- a/tethys_compute/models/dask/dask_job.py +++ b/tethys_compute/models/dask/dask_job.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import logging import datetime import json diff --git a/tethys_compute/models/dask/dask_scheduler.py b/tethys_compute/models/dask/dask_scheduler.py index ea751b6b5..441df0471 100644 --- a/tethys_compute/models/dask/dask_scheduler.py +++ b/tethys_compute/models/dask/dask_scheduler.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import logging from django.db import models from tethys_compute.models.scheduler import Scheduler diff --git a/tethys_compute/models/scheduler.py b/tethys_compute/models/scheduler.py index 480bd34d8..7d8144d7b 100644 --- a/tethys_compute/models/scheduler.py +++ b/tethys_compute/models/scheduler.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from django.db import models from model_utils.managers import InheritanceManager diff --git a/tethys_compute/models/tethys_job.py b/tethys_compute/models/tethys_job.py index 20676c75e..b71ddc106 100644 --- a/tethys_compute/models/tethys_job.py +++ b/tethys_compute/models/tethys_job.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import logging import datetime import inspect diff --git a/tethys_compute/scheduler_manager.py b/tethys_compute/scheduler_manager.py index 33874657a..75f1e5aa4 100644 --- a/tethys_compute/scheduler_manager.py +++ b/tethys_compute/scheduler_manager.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from tethys_compute.models import Scheduler from tethys_compute.models.condor.condor_scheduler import CondorScheduler from tethys_compute.models.dask.dask_scheduler import DaskScheduler diff --git a/tethys_config/__init__.py b/tethys_config/__init__.py index d7f518841..0771ad4cd 100644 --- a/tethys_config/__init__.py +++ b/tethys_config/__init__.py @@ -7,5 +7,6 @@ * License: BSD 2-Clause ******************************************************************************** """ + # Load the custom app config default_app_config = "tethys_config.apps.TethysPortalConfig" diff --git a/tethys_config/admin.py b/tethys_config/admin.py index eb93ff469..84e587907 100644 --- a/tethys_config/admin.py +++ b/tethys_config/admin.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.contrib import admin from django.forms import Textarea from django.db import models diff --git a/tethys_config/apps.py b/tethys_config/apps.py index fd1195f62..7e5ce3a2a 100644 --- a/tethys_config/apps.py +++ b/tethys_config/apps.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.apps import AppConfig diff --git a/tethys_config/context_processors.py b/tethys_config/context_processors.py index 5b34caff8..453074a84 100644 --- a/tethys_config/context_processors.py +++ b/tethys_config/context_processors.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import datetime as dt from tethys_portal.optional_dependencies import optional_import, has_module diff --git a/tethys_config/models.py b/tethys_config/models.py index 008fa4031..30328efc6 100644 --- a/tethys_config/models.py +++ b/tethys_config/models.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.db import models diff --git a/tethys_gizmos/admin.py b/tethys_gizmos/admin.py index bd1fee0b0..41ed3b83b 100644 --- a/tethys_gizmos/admin.py +++ b/tethys_gizmos/admin.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # from django.contrib import admin # Register your models here. diff --git a/tethys_gizmos/gizmo_options/__init__.py b/tethys_gizmos/gizmo_options/__init__.py index d8e78b1f8..3290a5b64 100644 --- a/tethys_gizmos/gizmo_options/__init__.py +++ b/tethys_gizmos/gizmo_options/__init__.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # flake8: noqa from .date_picker import * from .button import * diff --git a/tethys_gizmos/gizmo_options/base.py b/tethys_gizmos/gizmo_options/base.py index ee0975b8f..a7989671c 100644 --- a/tethys_gizmos/gizmo_options/base.py +++ b/tethys_gizmos/gizmo_options/base.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import re diff --git a/tethys_gizmos/gizmo_options/button.py b/tethys_gizmos/gizmo_options/button.py index 4b5ec74ea..7a219deb4 100644 --- a/tethys_gizmos/gizmo_options/button.py +++ b/tethys_gizmos/gizmo_options/button.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from .base import TethysGizmoOptions __all__ = ["ButtonGroup", "Button"] diff --git a/tethys_gizmos/gizmo_options/datatable_view.py b/tethys_gizmos/gizmo_options/datatable_view.py index 1fd1a1d3f..0ba09db56 100644 --- a/tethys_gizmos/gizmo_options/datatable_view.py +++ b/tethys_gizmos/gizmo_options/datatable_view.py @@ -6,6 +6,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import re from json import dumps from tethys_portal.dependencies import vendor_static_dependencies diff --git a/tethys_gizmos/gizmo_options/date_picker.py b/tethys_gizmos/gizmo_options/date_picker.py index 1c2ee5d1e..5a8bdf49c 100644 --- a/tethys_gizmos/gizmo_options/date_picker.py +++ b/tethys_gizmos/gizmo_options/date_picker.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from .base import TethysGizmoOptions from tethys_portal.dependencies import vendor_static_dependencies diff --git a/tethys_gizmos/gizmo_options/map_view.py b/tethys_gizmos/gizmo_options/map_view.py index 57d5d1f70..d6306415e 100644 --- a/tethys_gizmos/gizmo_options/map_view.py +++ b/tethys_gizmos/gizmo_options/map_view.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import logging from tethys_portal.dependencies import vendor_static_dependencies from .base import TethysGizmoOptions, SecondaryGizmoOptions diff --git a/tethys_gizmos/gizmo_options/message_box.py b/tethys_gizmos/gizmo_options/message_box.py index a8b9fa7b0..47191e240 100644 --- a/tethys_gizmos/gizmo_options/message_box.py +++ b/tethys_gizmos/gizmo_options/message_box.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from .base import TethysGizmoOptions __all__ = ["MessageBox"] diff --git a/tethys_gizmos/gizmo_options/range_slider.py b/tethys_gizmos/gizmo_options/range_slider.py index 197da8b23..291031421 100644 --- a/tethys_gizmos/gizmo_options/range_slider.py +++ b/tethys_gizmos/gizmo_options/range_slider.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from .base import TethysGizmoOptions __all__ = ["RangeSlider"] diff --git a/tethys_gizmos/gizmo_options/select_input.py b/tethys_gizmos/gizmo_options/select_input.py index f5260cfa4..2d50127a5 100644 --- a/tethys_gizmos/gizmo_options/select_input.py +++ b/tethys_gizmos/gizmo_options/select_input.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import json from tethys_portal.dependencies import vendor_static_dependencies from .base import TethysGizmoOptions diff --git a/tethys_gizmos/gizmo_options/table_view.py b/tethys_gizmos/gizmo_options/table_view.py index d98202e97..245a26b69 100644 --- a/tethys_gizmos/gizmo_options/table_view.py +++ b/tethys_gizmos/gizmo_options/table_view.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from .base import TethysGizmoOptions __all__ = ["TableView"] diff --git a/tethys_gizmos/gizmo_options/text_input.py b/tethys_gizmos/gizmo_options/text_input.py index a4d74ef89..2c43c3634 100644 --- a/tethys_gizmos/gizmo_options/text_input.py +++ b/tethys_gizmos/gizmo_options/text_input.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from .base import TethysGizmoOptions __all__ = ["TextInput"] diff --git a/tethys_gizmos/gizmo_options/toggle_switch.py b/tethys_gizmos/gizmo_options/toggle_switch.py index ebd978dd8..a449f2b0a 100644 --- a/tethys_gizmos/gizmo_options/toggle_switch.py +++ b/tethys_gizmos/gizmo_options/toggle_switch.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from tethys_portal.dependencies import vendor_static_dependencies from .base import TethysGizmoOptions diff --git a/tethys_gizmos/templatetags/tethys_gizmos.py b/tethys_gizmos/templatetags/tethys_gizmos.py index 06eebd732..9032b40e6 100644 --- a/tethys_gizmos/templatetags/tethys_gizmos.py +++ b/tethys_gizmos/templatetags/tethys_gizmos.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import os import json import time diff --git a/tethys_gizmos/urls.py b/tethys_gizmos/urls.py index 78dc432dd..dc2c97dca 100644 --- a/tethys_gizmos/urls.py +++ b/tethys_gizmos/urls.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.urls import re_path, include from tethys_gizmos.views.gizmos import jobs_table as jobs_table_views diff --git a/tethys_layouts/mixins/map_layout.py b/tethys_layouts/mixins/map_layout.py index 4100c855c..22d20bc26 100644 --- a/tethys_layouts/mixins/map_layout.py +++ b/tethys_layouts/mixins/map_layout.py @@ -998,14 +998,14 @@ def generate_custom_color_ramp_divisions( divisions[f"{prefix}{i}"] = f"{(m * i + b):.{value_precision}f}" if color_ramp in cls.COLOR_RAMPS.keys(): - divisions[ - f"{color_prefix}{i}" - ] = f"{cls.COLOR_RAMPS[color_ramp][(i - first_division) % len(cls.COLOR_RAMPS[color_ramp])]}" + divisions[f"{color_prefix}{i}"] = ( + f"{cls.COLOR_RAMPS[color_ramp][(i - first_division) % len(cls.COLOR_RAMPS[color_ramp])]}" + ) else: # use default color ramp - divisions[ - f"{color_prefix}{i}" - ] = f"{cls.COLOR_RAMPS['Default'][(i - first_division) % len(cls.COLOR_RAMPS['Default'])]}" + divisions[f"{color_prefix}{i}"] = ( + f"{cls.COLOR_RAMPS['Default'][(i - first_division) % len(cls.COLOR_RAMPS['Default'])]}" + ) if no_data_value is not None: divisions["val_no_data"] = no_data_value return divisions diff --git a/tethys_layouts/views/map_layout.py b/tethys_layouts/views/map_layout.py index a9b18dfe4..ba0a89113 100644 --- a/tethys_layouts/views/map_layout.py +++ b/tethys_layouts/views/map_layout.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2021 ******************************************************************************** """ + from tethys_portal.optional_dependencies import optional_import from abc import ABCMeta import collections diff --git a/tethys_layouts/views/tethys_layout.py b/tethys_layouts/views/tethys_layout.py index a72213a16..b890ca93b 100644 --- a/tethys_layouts/views/tethys_layout.py +++ b/tethys_layouts/views/tethys_layout.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2021 ******************************************************************************** """ + import logging from django.http import HttpResponseNotFound, HttpResponse diff --git a/tethys_portal/__init__.py b/tethys_portal/__init__.py index 4eec5d35a..b7ef34eb3 100644 --- a/tethys_portal/__init__.py +++ b/tethys_portal/__init__.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + try: from ._version import version as __version__ from ._version import version_tuple # noqa: F401 diff --git a/tethys_portal/asgi.py b/tethys_portal/asgi.py index 153c0938c..334a2cb8e 100644 --- a/tethys_portal/asgi.py +++ b/tethys_portal/asgi.py @@ -2,6 +2,7 @@ ASGI entrypoint. Configures Django and then runs the application defined in the ASGI_APPLICATION setting. """ + import os from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter diff --git a/tethys_portal/forms.py b/tethys_portal/forms.py index 25a0e7538..cd250bd6d 100644 --- a/tethys_portal/forms.py +++ b/tethys_portal/forms.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django import forms from django.contrib.auth.models import User from django.contrib.auth.password_validation import validate_password diff --git a/tethys_portal/middleware.py b/tethys_portal/middleware.py index 7c0bf5177..52fce23d8 100644 --- a/tethys_portal/middleware.py +++ b/tethys_portal/middleware.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.conf import settings from django.contrib import messages from django.core.exceptions import PermissionDenied diff --git a/tethys_portal/urls.py b/tethys_portal/urls.py index 6beea748b..1feaa07c7 100644 --- a/tethys_portal/urls.py +++ b/tethys_portal/urls.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import logging from importlib import import_module diff --git a/tethys_portal/utilities.py b/tethys_portal/utilities.py index 16dee3e33..f6ec49d19 100644 --- a/tethys_portal/utilities.py +++ b/tethys_portal/utilities.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import datetime from uuid import UUID from django.contrib.auth import login diff --git a/tethys_portal/views/accounts.py b/tethys_portal/views/accounts.py index 752d35006..620135853 100644 --- a/tethys_portal/views/accounts.py +++ b/tethys_portal/views/accounts.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.conf import settings from django.shortcuts import render, redirect from django.contrib.auth import authenticate, login, logout diff --git a/tethys_portal/views/api.py b/tethys_portal/views/api.py index 9ab155315..27fd12d2d 100644 --- a/tethys_portal/views/api.py +++ b/tethys_portal/views/api.py @@ -57,7 +57,7 @@ def get_app(request, app): "icon": static(app.icon), "exitUrl": reverse("app_library"), "rootUrl": reverse(app.index_url), - "settingsUrl": f'{reverse("admin:index")}tethys_apps/tethysapp/{ app.id }/change/', + "settingsUrl": f'{reverse("admin:index")}tethys_apps/tethysapp/{app.id}/change/', } if request.user.is_authenticated: @@ -73,9 +73,11 @@ def get_app(request, app): pass metadata["customSettings"][s.name] = { - "type": s.type - if s.type_custom_setting == "SIMPLE" - else s.type_custom_setting, + "type": ( + s.type + if s.type_custom_setting == "SIMPLE" + else s.type_custom_setting + ), "value": v, } diff --git a/tethys_portal/views/error.py b/tethys_portal/views/error.py index 6f1a254e5..3346894ca 100644 --- a/tethys_portal/views/error.py +++ b/tethys_portal/views/error.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.shortcuts import render diff --git a/tethys_portal/views/home.py b/tethys_portal/views/home.py index 89669b78d..5e2256781 100644 --- a/tethys_portal/views/home.py +++ b/tethys_portal/views/home.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.shortcuts import render, redirect from django.conf import settings from tethys_config.models import get_custom_template diff --git a/tethys_portal/views/user.py b/tethys_portal/views/user.py index 9a2a3b01c..977bf223c 100644 --- a/tethys_portal/views/user.py +++ b/tethys_portal/views/user.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.conf import settings as django_settings from django.shortcuts import render, redirect from django.contrib.auth import logout diff --git a/tethys_quotas/__init__.py b/tethys_quotas/__init__.py index 5ec3de4b8..8e4d35f1c 100644 --- a/tethys_quotas/__init__.py +++ b/tethys_quotas/__init__.py @@ -6,4 +6,5 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + default_app_config = "tethys_quotas.apps.TethysQuotasConfig" diff --git a/tethys_quotas/admin.py b/tethys_quotas/admin.py index 221ede63a..334feb15f 100644 --- a/tethys_quotas/admin.py +++ b/tethys_quotas/admin.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from django.urls import reverse from django.contrib.contenttypes.models import ContentType from django.utils.html import format_html diff --git a/tethys_quotas/apps.py b/tethys_quotas/apps.py index b8b78aaea..374f78cf7 100644 --- a/tethys_quotas/apps.py +++ b/tethys_quotas/apps.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import logging from django.apps import AppConfig from django.db.utils import ProgrammingError, OperationalError diff --git a/tethys_quotas/decorators.py b/tethys_quotas/decorators.py index 56051fdca..844705216 100644 --- a/tethys_quotas/decorators.py +++ b/tethys_quotas/decorators.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import logging from django.utils.functional import wraps from django.http import HttpRequest diff --git a/tethys_quotas/handlers/base.py b/tethys_quotas/handlers/base.py index 2a2ba97a0..1f2bcf888 100644 --- a/tethys_quotas/handlers/base.py +++ b/tethys_quotas/handlers/base.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from abc import abstractmethod from tethys_quotas.utilities import get_resource_available diff --git a/tethys_quotas/handlers/workspace.py b/tethys_quotas/handlers/workspace.py index 9a07b29da..fe137c339 100644 --- a/tethys_quotas/handlers/workspace.py +++ b/tethys_quotas/handlers/workspace.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + from django.contrib.auth.models import User from tethys_apps.models import TethysApp from tethys_apps.base.workspace import _get_user_workspace, _get_app_workspace diff --git a/tethys_quotas/models/__init__.py b/tethys_quotas/models/__init__.py index 800718d98..343d9999a 100644 --- a/tethys_quotas/models/__init__.py +++ b/tethys_quotas/models/__init__.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from tethys_quotas.models.resource_quota import ResourceQuota # noqa: F401 from tethys_quotas.models.entity_quota import EntityQuota # noqa: F401 from tethys_quotas.models.user_quota import UserQuota # noqa: F401 diff --git a/tethys_quotas/models/entity_quota.py b/tethys_quotas/models/entity_quota.py index 7bc053f2b..84d85a331 100644 --- a/tethys_quotas/models/entity_quota.py +++ b/tethys_quotas/models/entity_quota.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import logging from django.db import models diff --git a/tethys_quotas/models/resource_quota.py b/tethys_quotas/models/resource_quota.py index 76037614c..2bb9ddf78 100644 --- a/tethys_quotas/models/resource_quota.py +++ b/tethys_quotas/models/resource_quota.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import logging import inspect from django.contrib.auth.models import User diff --git a/tethys_quotas/models/tethys_app_quota.py b/tethys_quotas/models/tethys_app_quota.py index 7ed15e6b4..c19ae8624 100644 --- a/tethys_quotas/models/tethys_app_quota.py +++ b/tethys_quotas/models/tethys_app_quota.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import logging from django.db import models diff --git a/tethys_quotas/models/user_quota.py b/tethys_quotas/models/user_quota.py index 753292027..37ccbaab7 100644 --- a/tethys_quotas/models/user_quota.py +++ b/tethys_quotas/models/user_quota.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import logging from django.db import models diff --git a/tethys_quotas/utilities.py b/tethys_quotas/utilities.py index 0cb419505..07ec08187 100644 --- a/tethys_quotas/utilities.py +++ b/tethys_quotas/utilities.py @@ -6,6 +6,7 @@ * Copyright: (c) Aquaveo 2018 ******************************************************************************** """ + import logging from django.conf import settings from django.core.exceptions import PermissionDenied diff --git a/tethys_sdk/__init__.py b/tethys_sdk/__init__.py index 569467060..eaa3077bc 100644 --- a/tethys_sdk/__init__.py +++ b/tethys_sdk/__init__.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # flake8: noqa # This module provides a centralized location for SDK imports. from tethys_portal import __version__ diff --git a/tethys_sdk/app_settings.py b/tethys_sdk/app_settings.py index cb8583539..8f3139121 100644 --- a/tethys_sdk/app_settings.py +++ b/tethys_sdk/app_settings.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # flake8: noqa # DO NOT ERASE from tethys_apps.models import ( diff --git a/tethys_sdk/base.py b/tethys_sdk/base.py index f9be7b8d4..475c80065 100644 --- a/tethys_sdk/base.py +++ b/tethys_sdk/base.py @@ -8,7 +8,11 @@ # flake8: noqa # DO NOT ERASE -from tethys_apps.base import TethysAppBase, TethysExtensionBase, TethysAsyncWebsocketConsumer +from tethys_apps.base import ( + TethysAppBase, + TethysExtensionBase, + TethysAsyncWebsocketConsumer, +) from tethys_apps.base.url_map import url_map_maker from tethys_apps.base.controller import TethysController from tethys_apps.base.bokeh_handler import with_request, with_workspaces diff --git a/tethys_sdk/compute.py b/tethys_sdk/compute.py index 327a0bf3a..f3e5ddfef 100644 --- a/tethys_sdk/compute.py +++ b/tethys_sdk/compute.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # flake8: noqa # DO NOT ERASE from tethys_compute.scheduler_manager import ( diff --git a/tethys_sdk/gizmos.py b/tethys_sdk/gizmos.py index 1ce3abd79..582e3154d 100644 --- a/tethys_sdk/gizmos.py +++ b/tethys_sdk/gizmos.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # flake8: noqa # DO NOT ERASE from tethys_gizmos.gizmo_options import * diff --git a/tethys_sdk/handoff.py b/tethys_sdk/handoff.py index dd4b0f39d..5cf6d0331 100644 --- a/tethys_sdk/handoff.py +++ b/tethys_sdk/handoff.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # flake8: noqa # DO NOT ERASE from tethys_apps.base.handoff import HandoffHandler diff --git a/tethys_sdk/jobs.py b/tethys_sdk/jobs.py index 42dd8c594..65c6c9a13 100644 --- a/tethys_sdk/jobs.py +++ b/tethys_sdk/jobs.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # flake8: noqa # DO NOT ERASE from tethys_compute.models import ( diff --git a/tethys_sdk/layouts.py b/tethys_sdk/layouts.py index 609e7abbc..7512c9173 100644 --- a/tethys_sdk/layouts.py +++ b/tethys_sdk/layouts.py @@ -6,6 +6,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # flake8: noqa # DO NOT ERASE from tethys_layouts.views.map_layout import MapLayout diff --git a/tethys_sdk/permissions.py b/tethys_sdk/permissions.py index 1db34e771..68552b59c 100644 --- a/tethys_sdk/permissions.py +++ b/tethys_sdk/permissions.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # flake8: noqa # DO NOT ERASE from tethys_apps.base import Permission, PermissionGroup, has_permission diff --git a/tethys_sdk/services.py b/tethys_sdk/services.py index f8dc80bba..77e0303f7 100644 --- a/tethys_sdk/services.py +++ b/tethys_sdk/services.py @@ -1 +1,21 @@ -""" ******************************************************************************** * Name: services.py * Author: Nathan Swain * Created On: 7 August 2015 * Copyright: (c) Brigham Young University 2015 * License: BSD 2-Clause ******************************************************************************** """ # flake8: noqa # DO NOT ERASE from tethys_services.utilities import ( list_dataset_engines, get_dataset_engine, list_spatial_dataset_engines, get_spatial_dataset_engine, list_wps_service_engines, get_wps_service_engine, ensure_oauth2, ) \ No newline at end of file +""" +******************************************************************************** +* Name: services.py +* Author: Nathan Swain +* Created On: 7 August 2015 +* Copyright: (c) Brigham Young University 2015 +* License: BSD 2-Clause +******************************************************************************** +""" + +# flake8: noqa +# DO NOT ERASE +from tethys_services.utilities import ( + list_dataset_engines, + get_dataset_engine, + list_spatial_dataset_engines, + get_spatial_dataset_engine, + list_wps_service_engines, + get_wps_service_engine, + ensure_oauth2, +) diff --git a/tethys_sdk/testing.py b/tethys_sdk/testing.py index 50769cc67..035fd0f5b 100644 --- a/tethys_sdk/testing.py +++ b/tethys_sdk/testing.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # flake8: noqa # DO NOT ERASE from tethys_apps.base.testing.testing import TethysTestCase diff --git a/tethys_sdk/workspaces.py b/tethys_sdk/workspaces.py index 01afab79e..3bf487de6 100644 --- a/tethys_sdk/workspaces.py +++ b/tethys_sdk/workspaces.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + # flake8: noqa # DO NOT ERASE from tethys_apps.base.workspace import ( diff --git a/tethys_services/__init__.py b/tethys_services/__init__.py index 65d0712c2..727d2dc3b 100644 --- a/tethys_services/__init__.py +++ b/tethys_services/__init__.py @@ -7,5 +7,6 @@ * License: BSD 2-Clause ******************************************************************************** """ + # Load the custom app config default_app_config = "tethys_services.apps.TethysServicesConfig" diff --git a/tethys_services/admin.py b/tethys_services/admin.py index 5dd26a984..b8d88d90a 100644 --- a/tethys_services/admin.py +++ b/tethys_services/admin.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.contrib import admin from django.utils.translation import gettext_lazy as _ from .models import ( diff --git a/tethys_services/apps.py b/tethys_services/apps.py index 9f4d12e79..e6e5dc54c 100644 --- a/tethys_services/apps.py +++ b/tethys_services/apps.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.apps import AppConfig diff --git a/tethys_services/backends/arcgis_portal.py b/tethys_services/backends/arcgis_portal.py index 8e63a0a09..a885d8f68 100644 --- a/tethys_services/backends/arcgis_portal.py +++ b/tethys_services/backends/arcgis_portal.py @@ -6,6 +6,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from social_core.backends.arcgis import ArcGISOAuth2 from django.conf import settings diff --git a/tethys_services/backends/hydroshare.py b/tethys_services/backends/hydroshare.py index 5c09f2cfe..5eb297f24 100644 --- a/tethys_services/backends/hydroshare.py +++ b/tethys_services/backends/hydroshare.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from datetime import datetime import time from social_core.backends.oauth import BaseOAuth2 diff --git a/tethys_services/backends/hydroshare_beta.py b/tethys_services/backends/hydroshare_beta.py index 8e76fbca8..f98c13b02 100644 --- a/tethys_services/backends/hydroshare_beta.py +++ b/tethys_services/backends/hydroshare_beta.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from .hydroshare import HydroShareOAuth2 diff --git a/tethys_services/backends/hydroshare_playground.py b/tethys_services/backends/hydroshare_playground.py index 88d05ae0c..0a85c8241 100644 --- a/tethys_services/backends/hydroshare_playground.py +++ b/tethys_services/backends/hydroshare_playground.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from .hydroshare import HydroShareOAuth2 diff --git a/tethys_services/models.py b/tethys_services/models.py index 47a69863c..d678ddc9c 100644 --- a/tethys_services/models.py +++ b/tethys_services/models.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.db import models from django.core.exceptions import ObjectDoesNotExist, ValidationError from urllib.error import HTTPError, URLError diff --git a/tethys_services/urls.py b/tethys_services/urls.py index 7fd18903a..c13e955ea 100644 --- a/tethys_services/urls.py +++ b/tethys_services/urls.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.urls import re_path, include from tethys_services import views as tethys_services_views diff --git a/tethys_services/utilities.py b/tethys_services/utilities.py index f9a686376..ee4e33627 100644 --- a/tethys_services/utilities.py +++ b/tethys_services/utilities.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + import logging from urllib.error import HTTPError, URLError from functools import wraps diff --git a/tethys_services/views.py b/tethys_services/views.py index 7d572cd69..b96f9647c 100644 --- a/tethys_services/views.py +++ b/tethys_services/views.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ + from django.shortcuts import render from tethys_apps.decorators import login_required From e2a20588ca8faa0e632857eb2633a9a23ada9183 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Fri, 23 Feb 2024 16:42:17 -0600 Subject: [PATCH 06/18] initial restructure of authenticated websocket consumer not uses the decorator for permissions added additional args for login_required and permissions_use_or --- tethys_apps/base/app_base.py | 45 ++++++++++++++-------------------- tethys_apps/base/controller.py | 16 +++++++++++- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/tethys_apps/base/app_base.py b/tethys_apps/base/app_base.py index 0e21b4592..1a3c73ca3 100644 --- a/tethys_apps/base/app_base.py +++ b/tethys_apps/base/app_base.py @@ -1854,6 +1854,8 @@ class TethysAsyncWebsocketConsumer(AsyncWebsocketConsumer): """ permissions = [] + permissions_use_or = False + login_required = True _authorized = None _perms = None @@ -1873,44 +1875,35 @@ def perms(self): @property async def authorized(self): if self._authorized is None: - self._authorized = True - for perm in self.perms: - if not await scoped_user_has_permission(self.scope, perm): - self._authorized = False + if self.login_required and not self.scope['user'].is_authenticated: + self._authorized = False + return self._authorized + + if self.permissions_use_or: + self._authorized = False + for perm in self.perms: + if await scoped_user_has_permission(self.scope, perm): + self._authorized = True + else: + self._authorized = True + for perm in self.perms: + if not await scoped_user_has_permission(self.scope, perm): + self._authorized = False return self._authorized async def on_authorized_connect(self): """Custom class method to run custom code when user connects to the websocket""" pass - async def on_connect(self): + async def on_unauthorized_connect(self): """Custom class method to run custom code when user connects to the websocket""" pass - async def on_disconnect(self, event): - """Custom class method to run custom code when user disconnects to the websocket""" - pass - - async def on_receive(self, event): - """Custom class method to run custom code when websocket receives a message""" - pass - async def connect(self): """Class method to handle when user connects to the websocket""" - await self.accept() - await self.on_connect() - if await self.authorized: + await self.accept() await self.on_authorized_connect() else: # User not authorized for websocket access - await self.close(code=4004) - - async def disconnect(self, event): - """Class method to handle when user disconnects from the websocket""" - await self.on_disconnect(event) - - async def receive(self, text_data): - """Class method to handle when websocket receives a message""" - if await self.authorized: - await self.on_receive(text_data) + await self.on_unauthorized_connect() diff --git a/tethys_apps/base/controller.py b/tethys_apps/base/controller.py index c31d1199b..5ea43a01d 100644 --- a/tethys_apps/base/controller.py +++ b/tethys_apps/base/controller.py @@ -19,7 +19,7 @@ from tethys_quotas.decorators import enforce_quota from tethys_services.utilities import ensure_oauth2 from . import url_map_maker -from .app_base import DEFAULT_CONTROLLER_MODULES +from .app_base import DEFAULT_CONTROLLER_MODULES, TethysAsyncWebsocketConsumer from .bokeh_handler import ( _get_bokeh_controller, @@ -58,6 +58,11 @@ def consumer( name: str = None, url: str = None, regex: Union[str, list, tuple] = None, + # login_required kwargs + login_required: bool = True, + # permission_required kwargs + permissions_required: Union[str, list, tuple] = None, + permissions_use_or: bool = False, ) -> Callable: """ Decorator to register a Consumer class as routed consumer endpoint @@ -100,6 +105,15 @@ def wrapped(function_or_class): regex=regex, ) + function_or_class.permissions = permissions_required + function_or_class.permissions_use_or = permissions_use_or + function_or_class.login_required = login_required + function_or_class._authorized = None + function_or_class._perms = None + function_or_class.perms = TethysAsyncWebsocketConsumer.perms + function_or_class.authorized = TethysAsyncWebsocketConsumer.authorized + function_or_class.connect = TethysAsyncWebsocketConsumer.connect + controller = function_or_class.as_asgi() _process_url_kwargs(controller, url_map_kwargs_list) return function_or_class From 30a0ec923ee78fc373da39266aa912a10047cdb2 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Mon, 26 Feb 2024 10:44:36 -0600 Subject: [PATCH 07/18] cleaned up code and moved functions --- tethys_apps/base/__init__.py | 1 - tethys_apps/base/app_base.py | 70 +-------------- tethys_apps/base/controller.py | 14 +-- tethys_apps/base/mixins.py | 160 +++++++++++++++++++++++++++++++++ tethys_apps/utilities.py | 49 ++++++++++ tethys_sdk/base.py | 1 - 6 files changed, 215 insertions(+), 80 deletions(-) diff --git a/tethys_apps/base/__init__.py b/tethys_apps/base/__init__.py index ce537bd37..f6becc4a2 100644 --- a/tethys_apps/base/__init__.py +++ b/tethys_apps/base/__init__.py @@ -12,7 +12,6 @@ from tethys_apps.base.app_base import ( # noqa: F401 TethysAppBase, TethysExtensionBase, - TethysAsyncWebsocketConsumer, ) from tethys_apps.base.bokeh_handler import with_request, with_workspaces # noqa: F401 from tethys_apps.base.url_map import url_map_maker # noqa: F401 diff --git a/tethys_apps/base/app_base.py b/tethys_apps/base/app_base.py index 1a3c73ca3..6af880ff8 100644 --- a/tethys_apps/base/app_base.py +++ b/tethys_apps/base/app_base.py @@ -14,7 +14,6 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.urls import re_path from django.utils.functional import classproperty -from channels.generic.websocket import AsyncWebsocketConsumer from .testing.environment import ( is_testing_environment, @@ -23,11 +22,10 @@ ) from .permissions import ( Permission as TethysPermission, - PermissionGroup, - scoped_user_has_permission, + PermissionGroup ) from .handoff import HandoffManager -from .mixins import TethysBaseMixin +from .mixins import TethysBaseMixin, TethysAsyncWebsocketConsumerMixin, TethysAsyncWebsocketConsumerMixin from .workspace import get_app_workspace, get_user_workspace from ..exceptions import TethysAppSettingDoesNotExist, TethysAppSettingNotAssigned @@ -1843,67 +1841,3 @@ def post_delete_app_workspace(cls): """ Override this method to post-process the app workspace after it is emptied """ - - -class TethysAsyncWebsocketConsumer(AsyncWebsocketConsumer): - """ - Base class used to create a Django channel websocket consumer for Tethys - - Attributes: - permissions (string, list, tuple): List of permissions required to connect and use the websocket. - """ - - permissions = [] - permissions_use_or = False - login_required = True - _authorized = None - _perms = None - - @property - def perms(self): - if self._perms is None: - if type(self.permissions) in [list, tuple]: - self._perms = self.permissions - elif isinstance(self.permissions, str): - self._perms = self.permissions.split(",") - else: - raise TypeError( - "permissions must be a list, tuple, or comma separated string" - ) - return self._perms - - @property - async def authorized(self): - if self._authorized is None: - if self.login_required and not self.scope['user'].is_authenticated: - self._authorized = False - return self._authorized - - if self.permissions_use_or: - self._authorized = False - for perm in self.perms: - if await scoped_user_has_permission(self.scope, perm): - self._authorized = True - else: - self._authorized = True - for perm in self.perms: - if not await scoped_user_has_permission(self.scope, perm): - self._authorized = False - return self._authorized - - async def on_authorized_connect(self): - """Custom class method to run custom code when user connects to the websocket""" - pass - - async def on_unauthorized_connect(self): - """Custom class method to run custom code when user connects to the websocket""" - pass - - async def connect(self): - """Class method to handle when user connects to the websocket""" - if await self.authorized: - await self.accept() - await self.on_authorized_connect() - else: - # User not authorized for websocket access - await self.on_unauthorized_connect() diff --git a/tethys_apps/base/controller.py b/tethys_apps/base/controller.py index 5ea43a01d..2e74f7cc7 100644 --- a/tethys_apps/base/controller.py +++ b/tethys_apps/base/controller.py @@ -19,7 +19,7 @@ from tethys_quotas.decorators import enforce_quota from tethys_services.utilities import ensure_oauth2 from . import url_map_maker -from .app_base import DEFAULT_CONTROLLER_MODULES, TethysAsyncWebsocketConsumer +from .app_base import DEFAULT_CONTROLLER_MODULES from .bokeh_handler import ( _get_bokeh_controller, @@ -31,7 +31,7 @@ user_workspace as user_workspace_decorator, ) from ..decorators import login_required as login_required_decorator, permission_required -from ..utilities import get_all_submodules +from ..utilities import get_all_submodules, update_decorated_websocket_consumer_class # imports for type hinting from typing import Union, Any @@ -105,14 +105,8 @@ def wrapped(function_or_class): regex=regex, ) - function_or_class.permissions = permissions_required - function_or_class.permissions_use_or = permissions_use_or - function_or_class.login_required = login_required - function_or_class._authorized = None - function_or_class._perms = None - function_or_class.perms = TethysAsyncWebsocketConsumer.perms - function_or_class.authorized = TethysAsyncWebsocketConsumer.authorized - function_or_class.connect = TethysAsyncWebsocketConsumer.connect + function_or_class = update_decorated_websocket_consumer_class(function_or_class, permissions_required, + permissions_use_or, login_required) controller = function_or_class.as_asgi() _process_url_kwargs(controller, url_map_kwargs_list) diff --git a/tethys_apps/base/mixins.py b/tethys_apps/base/mixins.py index 512e1d8f1..6f40f6457 100644 --- a/tethys_apps/base/mixins.py +++ b/tethys_apps/base/mixins.py @@ -1,3 +1,7 @@ + +from .permissions import scoped_user_has_permission + + class TethysBaseMixin: """ Provides methods and properties common to the TethysBase and model classes. @@ -16,3 +20,159 @@ def url_namespace(self): @property def index_url(self): return f"{self.url_namespace}:{self.index}" + + +class TethysAsyncWebsocketConsumerMixin: + """ + Provides methods and properties common to Tethys async websocket consumers. + """ + + permissions = [] + permissions_use_or = False + login_required = True + _authorized = None + _perms = None + + @property + def perms(self): + if self._perms is None: + if type(self.permissions) in [list, tuple]: + self._perms = self.permissions + elif isinstance(self.permissions, str): + self._perms = self.permissions.split(",") + else: + raise TypeError( + "permissions must be a list, tuple, or comma separated string" + ) + return self._perms + + @property + async def authorized(self): + if self._authorized is None: + if self.login_required and not self.scope['user'].is_authenticated: + self._authorized = False + return self._authorized + + if self.permissions_use_or: + self._authorized = False + for perm in self.perms: + if await scoped_user_has_permission(self.scope, perm): + self._authorized = True + else: + self._authorized = True + for perm in self.perms: + if not await scoped_user_has_permission(self.scope, perm): + self._authorized = False + return self._authorized + + async def authorized_connect(self): + """Custom class method to run custom code when an authorized user connects to the websocket""" + pass + + async def unauthorized_connect(self): + """Custom class method to run custom code when an unauthorized user connects to the websocket""" + pass + + async def authorized_disconnect(self, close_code): + """Custom class method to run custom code when an authorized user connects to the websocket""" + pass + + async def unauthorized_disconnect(self, close_code): + """Custom class method to run custom code when an unauthorized user connects to the websocket""" + pass + + async def connect(self): + """Class method to handle when user connects to the websocket""" + if await self.authorized: + await self.accept() + await self.authorized_connect() + else: + # User not authorized for websocket access + await self.unauthorized_connect() + + async def disconnect(self, close_code): + """Class method to handle when user disconnects to the websocket""" + if await self.authorized: + await self.authorized_disconnect(close_code) + else: + # User not authorized for websocket access + await self.unauthorized_disconnect(close_code) + + +class TethysWebsocketConsumerMixin: + """ + Provides methods and properties common to Tethys websocket consumers. + """ + + permissions = [] + permissions_use_or = False + login_required = True + _authorized = None + _perms = None + + @property + def perms(self): + if self._perms is None: + if self.permissions is None: + self._perms = [] + elif type(self.permissions) in [list, tuple]: + self._perms = self.permissions + elif isinstance(self.permissions, str): + self._perms = self.permissions.split(",") + else: + raise TypeError( + "permissions must be a list, tuple, or comma separated string" + ) + return self._perms + + @property + def authorized(self): + if self._authorized is None: + if self.login_required and not self.scope['user'].is_authenticated: + self._authorized = False + return self._authorized + + if self.permissions_use_or: + self._authorized = False + for perm in self.perms: + if scoped_user_has_permission(self.scope, perm): + self._authorized = True + else: + self._authorized = True + for perm in self.perms: + if not scoped_user_has_permission(self.scope, perm): + self._authorized = False + return self._authorized + + def authorized_connect(self): + """Custom class method to run custom code when an authorized user connects to the websocket""" + pass + + def unauthorized_connect(self): + """Custom class method to run custom code when an unauthorized user connects to the websocket""" + pass + + def authorized_disconnect(self, close_code): + """Custom class method to run custom code when an authorized user connects to the websocket""" + pass + + def unauthorized_disconnect(self, close_code): + """Custom class method to run custom code when an unauthorized user connects to the websocket""" + pass + + def connect(self): + """Class method to handle when user connects to the websocket""" + if self.authorized: + self.accept() + self.authorized_connect() + else: + # User not authorized for websocket access + self.unauthorized_connect() + + def disconnect(self, close_code): + """Class method to handle when user disconnects to the websocket""" + if self.authorized: + self.authorized_disconnect(close_code) + else: + # User not authorized for websocket access + self.unauthorized_disconnect(close_code) diff --git a/tethys_apps/utilities.py b/tethys_apps/utilities.py index f976716c1..448b6909c 100644 --- a/tethys_apps/utilities.py +++ b/tethys_apps/utilities.py @@ -12,6 +12,7 @@ import logging import os from pathlib import Path +import inspect import pkgutil import yaml @@ -21,6 +22,7 @@ from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.utils._os import safe_join +from tethys_apps.base.mixins import TethysAsyncWebsocketConsumerMixin, TethysWebsocketConsumerMixin from tethys_apps.exceptions import TethysAppSettingNotAssigned from .harvester import SingletonHarvester @@ -702,3 +704,50 @@ def sign_and_unsign_secret_string(signer, value, is_signing): else: secret_unsigned = signer.unsign_object(f"{value}") return secret_unsigned + + +def update_decorated_websocket_consumer_class(function_or_class, permissions_required, permissions_use_or, + login_required): + """Updates a given consumer class and adds the necessary properties and function for authorizing user access + depending on the other args given. + + Args: + function_or_class (class): class of the websocket consumer + permissions_required (str, list, tuple): the permissions required for user access + permissions_use_or (bool): Determines if all permissions need to be met or just one of them + login_required (bool): Determines if the user needs to be logged in to use + + Returns: + class: updated class with necessary properties and function for authorizing user access + """ + base_class_name = inspect.getmro(function_or_class)[1].__name__ + function_or_class_name = function_or_class.__name__ + if "async" in function_or_class_name.lower() or "async" in base_class_name.lower(): + consumer_mixin = TethysAsyncWebsocketConsumerMixin + else: + consumer_mixin = TethysWebsocketConsumerMixin + breakpoint() + function_or_class.permissions = permissions_required + function_or_class.permissions_use_or = permissions_use_or + function_or_class.login_required = login_required + function_or_class._authorized = None + function_or_class._perms = None + function_or_class.perms = consumer_mixin.perms + function_or_class.authorized = consumer_mixin.authorized + function_or_class.connect = consumer_mixin.connect + function_or_class.disconnect = consumer_mixin.disconnect + + if not getattr(function_or_class, "authorized_connect", None): + function_or_class.authorized_connect = consumer_mixin.authorized_connect + + if not getattr(function_or_class, "unauthorized_connect", None): + function_or_class.unauthorized_connect = consumer_mixin.unauthorized_connect + + if not getattr(function_or_class, "authorized_disconnect", None): + function_or_class.authorized_disconnect = consumer_mixin.authorized_disconnect + + if not getattr(function_or_class, "unauthorized_disconnect", None): + function_or_class.unauthorized_disconnect = consumer_mixin.unauthorized_disconnect + + return function_or_class + diff --git a/tethys_sdk/base.py b/tethys_sdk/base.py index 475c80065..367f64ec7 100644 --- a/tethys_sdk/base.py +++ b/tethys_sdk/base.py @@ -11,7 +11,6 @@ from tethys_apps.base import ( TethysAppBase, TethysExtensionBase, - TethysAsyncWebsocketConsumer, ) from tethys_apps.base.url_map import url_map_maker from tethys_apps.base.controller import TethysController From 9da61869d99b204602b619c636b1d48bfebaea91 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Mon, 26 Feb 2024 10:55:18 -0600 Subject: [PATCH 08/18] cleaned up manual copying of class methods --- tethys_apps/utilities.py | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/tethys_apps/utilities.py b/tethys_apps/utilities.py index 448b6909c..7e54b9c70 100644 --- a/tethys_apps/utilities.py +++ b/tethys_apps/utilities.py @@ -726,28 +726,10 @@ def update_decorated_websocket_consumer_class(function_or_class, permissions_req consumer_mixin = TethysAsyncWebsocketConsumerMixin else: consumer_mixin = TethysWebsocketConsumerMixin - breakpoint() - function_or_class.permissions = permissions_required - function_or_class.permissions_use_or = permissions_use_or - function_or_class.login_required = login_required - function_or_class._authorized = None - function_or_class._perms = None - function_or_class.perms = consumer_mixin.perms - function_or_class.authorized = consumer_mixin.authorized - function_or_class.connect = consumer_mixin.connect - function_or_class.disconnect = consumer_mixin.disconnect - - if not getattr(function_or_class, "authorized_connect", None): - function_or_class.authorized_connect = consumer_mixin.authorized_connect - - if not getattr(function_or_class, "unauthorized_connect", None): - function_or_class.unauthorized_connect = consumer_mixin.unauthorized_connect - - if not getattr(function_or_class, "authorized_disconnect", None): - function_or_class.authorized_disconnect = consumer_mixin.authorized_disconnect - - if not getattr(function_or_class, "unauthorized_disconnect", None): - function_or_class.unauthorized_disconnect = consumer_mixin.unauthorized_disconnect + + class_bases = list(function_or_class.__bases__) + class_bases.insert(0, consumer_mixin) + function_or_class.__bases__ = tuple(class_bases) return function_or_class From c7ea3bef1361baab4911fb247a117ca8407ce2a1 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Mon, 26 Feb 2024 10:56:00 -0600 Subject: [PATCH 09/18] more small code changes --- tethys_apps/utilities.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tethys_apps/utilities.py b/tethys_apps/utilities.py index 7e54b9c70..ab2bce1ae 100644 --- a/tethys_apps/utilities.py +++ b/tethys_apps/utilities.py @@ -720,9 +720,9 @@ def update_decorated_websocket_consumer_class(function_or_class, permissions_req Returns: class: updated class with necessary properties and function for authorizing user access """ - base_class_name = inspect.getmro(function_or_class)[1].__name__ - function_or_class_name = function_or_class.__name__ - if "async" in function_or_class_name.lower() or "async" in base_class_name.lower(): + base_class_name = inspect.getmro(function_or_class)[1].__name__.lower() + function_or_class_name = function_or_class.__name__.lower() + if "async" in function_or_class_name or "async" in base_class_name: consumer_mixin = TethysAsyncWebsocketConsumerMixin else: consumer_mixin = TethysWebsocketConsumerMixin From 9d1e4ef6171750a59ea86a5d780c6147fb32dbb4 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Mon, 26 Feb 2024 11:02:31 -0600 Subject: [PATCH 10/18] updated docs --- docs/tutorials/websockets.rst | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/tutorials/websockets.rst b/docs/tutorials/websockets.rst index 794bc72ce..7099e5fa5 100644 --- a/docs/tutorials/websockets.rst +++ b/docs/tutorials/websockets.rst @@ -37,18 +37,18 @@ a. Create a new file called ``consumers.py`` and add the following code: .. code-block:: python - from tethys_sdk.base import TethysAsyncWebsocketConsumer + from channels.generic.websocket import AsyncWebsocketConsumer from tethys_sdk.routing import consumer @consumer(name='dam_notification', url='dams/notifications') - class NotificationsConsumer(TethysAsyncWebsocketConsumer): + class NotificationsConsumer(AsyncWebsocketConsumer): permissions = [] - async def on_authorized_connect(self): + async def authorized_connect(self): print("-----------WebSocket Connected-----------") - async def on_disconnect(self, close_code): + async def authorized_disconnect(self, close_code): pass .. note:: @@ -94,14 +94,13 @@ a. Update the ``consumer class`` to look like this. ... @consumer(name='dam_notification', url='dams/notifications') - class NotificationsConsumer(TethysAsyncWebsocketConsumer): - permissions = [] + class NotificationsConsumer(AsyncWebsocketConsumer): - async def on_authorized_connect(self): + async def authorized_connect(self): await self.channel_layer.group_add("notifications", self.channel_name) print(f"Added {self.channel_name} channel to notifications") - async def on_disconnect(self, close_code): + async def authorized_disconnect(self, close_code): await self.channel_layer.group_discard("notifications", self.channel_name) print(f"Removed {self.channel_name} channel from notifications") From 18d1d40b97095b1aa6276cdef9ebffe53cf0c501 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Mon, 26 Feb 2024 11:03:19 -0600 Subject: [PATCH 11/18] removed old permissions property for the docs example --- docs/tutorials/websockets.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/tutorials/websockets.rst b/docs/tutorials/websockets.rst index 7099e5fa5..bcf6ecfd8 100644 --- a/docs/tutorials/websockets.rst +++ b/docs/tutorials/websockets.rst @@ -43,7 +43,6 @@ a. Create a new file called ``consumers.py`` and add the following code: @consumer(name='dam_notification', url='dams/notifications') class NotificationsConsumer(AsyncWebsocketConsumer): - permissions = [] async def authorized_connect(self): print("-----------WebSocket Connected-----------") From c1715875a553a58fe6acc8b9eeca6affec3dde13 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Mon, 26 Feb 2024 11:05:22 -0600 Subject: [PATCH 12/18] black formatted and linted code --- tethys_apps/base/app_base.py | 7 ++----- tethys_apps/base/controller.py | 5 +++-- tethys_apps/base/mixins.py | 9 ++++----- tethys_apps/utilities.py | 13 ++++++++----- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tethys_apps/base/app_base.py b/tethys_apps/base/app_base.py index 6af880ff8..0900053b6 100644 --- a/tethys_apps/base/app_base.py +++ b/tethys_apps/base/app_base.py @@ -20,12 +20,9 @@ get_test_db_name, TESTING_DB_FLAG, ) -from .permissions import ( - Permission as TethysPermission, - PermissionGroup -) +from .permissions import Permission as TethysPermission, PermissionGroup from .handoff import HandoffManager -from .mixins import TethysBaseMixin, TethysAsyncWebsocketConsumerMixin, TethysAsyncWebsocketConsumerMixin +from .mixins import TethysBaseMixin from .workspace import get_app_workspace, get_user_workspace from ..exceptions import TethysAppSettingDoesNotExist, TethysAppSettingNotAssigned diff --git a/tethys_apps/base/controller.py b/tethys_apps/base/controller.py index 2e74f7cc7..88105f8f3 100644 --- a/tethys_apps/base/controller.py +++ b/tethys_apps/base/controller.py @@ -105,8 +105,9 @@ def wrapped(function_or_class): regex=regex, ) - function_or_class = update_decorated_websocket_consumer_class(function_or_class, permissions_required, - permissions_use_or, login_required) + function_or_class = update_decorated_websocket_consumer_class( + function_or_class, permissions_required, permissions_use_or, login_required + ) controller = function_or_class.as_asgi() _process_url_kwargs(controller, url_map_kwargs_list) diff --git a/tethys_apps/base/mixins.py b/tethys_apps/base/mixins.py index 6f40f6457..d1e282760 100644 --- a/tethys_apps/base/mixins.py +++ b/tethys_apps/base/mixins.py @@ -1,4 +1,3 @@ - from .permissions import scoped_user_has_permission @@ -49,10 +48,10 @@ def perms(self): @property async def authorized(self): if self._authorized is None: - if self.login_required and not self.scope['user'].is_authenticated: + if self.login_required and not self.scope["user"].is_authenticated: self._authorized = False return self._authorized - + if self.permissions_use_or: self._authorized = False for perm in self.perms: @@ -128,10 +127,10 @@ def perms(self): @property def authorized(self): if self._authorized is None: - if self.login_required and not self.scope['user'].is_authenticated: + if self.login_required and not self.scope["user"].is_authenticated: self._authorized = False return self._authorized - + if self.permissions_use_or: self._authorized = False for perm in self.perms: diff --git a/tethys_apps/utilities.py b/tethys_apps/utilities.py index ab2bce1ae..2a0c868af 100644 --- a/tethys_apps/utilities.py +++ b/tethys_apps/utilities.py @@ -22,7 +22,10 @@ from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.utils._os import safe_join -from tethys_apps.base.mixins import TethysAsyncWebsocketConsumerMixin, TethysWebsocketConsumerMixin +from tethys_apps.base.mixins import ( + TethysAsyncWebsocketConsumerMixin, + TethysWebsocketConsumerMixin, +) from tethys_apps.exceptions import TethysAppSettingNotAssigned from .harvester import SingletonHarvester @@ -706,8 +709,9 @@ def sign_and_unsign_secret_string(signer, value, is_signing): return secret_unsigned -def update_decorated_websocket_consumer_class(function_or_class, permissions_required, permissions_use_or, - login_required): +def update_decorated_websocket_consumer_class( + function_or_class, permissions_required, permissions_use_or, login_required +): """Updates a given consumer class and adds the necessary properties and function for authorizing user access depending on the other args given. @@ -730,6 +734,5 @@ def update_decorated_websocket_consumer_class(function_or_class, permissions_req class_bases = list(function_or_class.__bases__) class_bases.insert(0, consumer_mixin) function_or_class.__bases__ = tuple(class_bases) - + return function_or_class - From 69300d909b870c09ce71ae6adba9f2f076da86f1 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Mon, 26 Feb 2024 15:18:51 -0600 Subject: [PATCH 13/18] updated tests --- .../test_base/test_app_base.py | 99 ------- .../test_tethys_apps/test_base/test_mixins.py | 256 ++++++++++++++++++ .../test_tethys_apps/test_utilities.py | 49 ++++ tethys_apps/base/mixins.py | 4 +- tethys_apps/utilities.py | 3 + 5 files changed, 311 insertions(+), 100 deletions(-) diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py b/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py index c93705ae1..a17745df7 100644 --- a/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py +++ b/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py @@ -1480,102 +1480,3 @@ def test_remove_from_db_2(self, mock_ta, mock_log): # Check tethys log error mock_log.error.assert_called() - - -class TestTethysAsyncWebsocketConsumer(unittest.IsolatedAsyncioTestCase): - def setUp(self): - self.consumer = tethys_app_base.TethysAsyncWebsocketConsumer() - self.consumer.permissions = ["test_permission"] - self.consumer.scope = {"user": UserFactory(), "path": "path/to/app"} - - def tearDown(self): - pass - - def test_perms_list(self): - self.assertTrue(self.consumer.perms == ["test_permission"]) - - def test_perms_str(self): - self.consumer.permissions = "test_permission,test_permission1" - self.assertTrue(self.consumer.perms == ["test_permission", "test_permission1"]) - - def test_perms_exception(self): - self.consumer.permissions = {"test": "test_permsision"} - with self.assertRaises(TypeError) as context: - self.consumer.perms - - self.assertTrue( - context.exception.args[0] - == "permissions must be a list, tuple, or comma separated string" - ) - - @mock.patch("tethys_apps.base.app_base.scoped_user_has_permission") - async def test_authorized(self, mock_suhp): - self.consumer.permissions = ["test_permission", "test_permission1"] - mock_suhp.side_effect = [True, True] - self.assertTrue(await self.consumer.authorized) - - @mock.patch("tethys_apps.base.app_base.scoped_user_has_permission") - async def test_authorized_not(self, mock_suhp): - self.consumer.permissions = ["test_permission", "test_permission1"] - mock_suhp.side_effect = [True, False] - self.assertFalse(await self.consumer.authorized) - - async def test_on_authorized_connect(self): - await self.consumer.on_authorized_connect() - - async def test_on_connect(self): - await self.consumer.on_connect() - - async def test_on_disconnect(self): - event = {} - await self.consumer.on_disconnect(event) - - async def test_on_receive(self): - event = {} - await self.consumer.on_receive(event) - - @mock.patch( - "tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_authorized_connect" - ) - @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_connect") - @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.accept") - async def test_connect( - self, mock_accept, mock_on_connect, mock_on_authorized_connect - ): - self.consumer._authorized = True - await self.consumer.connect() - mock_accept.assert_called_once() - mock_on_connect.assert_called_once() - mock_on_authorized_connect.assert_called_once() - - @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.close") - @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_connect") - @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.accept") - async def test_connect_not_authorized( - self, mock_accept, mock_on_connect, mock_close - ): - self.consumer._authorized = False - await self.consumer.connect() - mock_accept.assert_called_once() - mock_on_connect.assert_called_once() - mock_close.assert_called_with(code=4004) - - @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_disconnect") - async def test_disconnect(self, mock_on_disconnect): - event = "event" - await self.consumer.disconnect(event) - mock_on_disconnect.assert_called_with(event) - - @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_receive") - async def test_receive(self, mock_on_receive): - self.consumer._authorized = True - text_data = "text_data" - await self.consumer.receive(text_data) - mock_on_receive.assert_called_with(text_data) - - @mock.patch("tethys_apps.base.app_base.TethysAsyncWebsocketConsumer.on_receive") - async def test_receive_not_authorized(self, mock_on_receive): - self.consumer._authorized = False - text_data = "text_data" - await self.consumer.receive(text_data) - mock_on_receive.assert_not_called() diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_mixins.py b/tests/unit_tests/test_tethys_apps/test_base/test_mixins.py index 4bcc349a1..024d9fb5f 100644 --- a/tests/unit_tests/test_tethys_apps/test_base/test_mixins.py +++ b/tests/unit_tests/test_tethys_apps/test_base/test_mixins.py @@ -1,5 +1,7 @@ import unittest +from unittest import mock import tethys_apps.base.mixins as tethys_mixins +from ... import UserFactory class TestTethysBaseMixin(unittest.TestCase): @@ -13,3 +15,257 @@ def test_TethysBaseMixin(self): result = tethys_mixins.TethysBaseMixin() result.root_url = "test-url" self.assertEqual("test_url", result.url_namespace) + + +class TestTethysAsyncWebsocketConsumer(unittest.IsolatedAsyncioTestCase): + def setUp(self): + self.consumer = tethys_mixins.TethysAsyncWebsocketConsumerMixin() + self.consumer.accept = mock.AsyncMock() + self.consumer.permissions = ["test_permission"] + self.consumer.scope = {"user": UserFactory(), "path": "path/to/app"} + + def tearDown(self): + pass + + def test_perms_list(self): + self.assertTrue(self.consumer.perms == ["test_permission"]) + + def test_perms_none(self): + self.consumer.permissions = None + self.assertTrue(self.consumer.perms == []) + + def test_perms_str(self): + self.consumer.permissions = "test_permission,test_permission1" + self.assertTrue(self.consumer.perms == ["test_permission", "test_permission1"]) + + def test_perms_exception(self): + self.consumer.permissions = {"test": "test_permsision"} + with self.assertRaises(TypeError) as context: + self.consumer.perms + + self.assertTrue( + context.exception.args[0] + == "permissions must be a list, tuple, or comma separated string" + ) + + async def test_authorized_login_required_success(self): + self.consumer.permissions = [] + self.consumer.login_required = True + self.assertTrue(await self.consumer.authorized) + + async def test_authorized_login_required_failure(self): + self.consumer.permissions = [] + self.consumer.login_required = True + self.consumer.scope = { + "user": mock.MagicMock(is_authenticated=False), + "path": "path/to/app", + } + self.assertFalse(await self.consumer.authorized) + + @mock.patch("tethys_apps.base.mixins.scoped_user_has_permission") + async def test_authorized_permissions_and(self, mock_suhp): + self.consumer.permissions = ["test_permission", "test_permission1"] + mock_suhp.side_effect = [True, True] + self.assertTrue(await self.consumer.authorized) + + @mock.patch("tethys_apps.base.mixins.scoped_user_has_permission") + async def test_authorized_inadequate_permissions_and(self, mock_suhp): + self.consumer.permissions = ["test_permission", "test_permission1"] + mock_suhp.side_effect = [True, False] + self.assertFalse(await self.consumer.authorized) + + @mock.patch("tethys_apps.base.mixins.scoped_user_has_permission") + async def test_authorized_permissions_or(self, mock_suhp): + self.consumer.permissions = ["test_permission", "test_permission1"] + self.consumer.permissions_use_or = True + mock_suhp.side_effect = [True, False] + self.assertTrue(await self.consumer.authorized) + + @mock.patch("tethys_apps.base.mixins.scoped_user_has_permission") + async def test_authorized_inadequate_permissions_or(self, mock_suhp): + self.consumer.permissions = ["test_permission", "test_permission1"] + self.consumer.permissions_use_or = True + mock_suhp.side_effect = [False, False] + self.assertFalse(await self.consumer.authorized) + + async def test_authorized_connect(self): + await self.consumer.authorized_connect() + + async def test_unauthorized_connect(self): + await self.consumer.unauthorized_connect() + + async def test_authorized_disconnect(self): + event = {} + await self.consumer.authorized_disconnect(event) + + async def test_unauthorized_disconnect(self): + event = {} + await self.consumer.unauthorized_disconnect(event) + + @mock.patch( + "tethys_apps.base.mixins.TethysAsyncWebsocketConsumerMixin.authorized_connect" + ) + async def test_connect(self, mock_authorized_connect): + self.consumer._authorized = True + await self.consumer.connect() + self.consumer.accept.assert_called_once() + mock_authorized_connect.assert_called_once() + + @mock.patch( + "tethys_apps.base.mixins.TethysAsyncWebsocketConsumerMixin.unauthorized_connect" + ) + async def test_connect_not_authorized(self, mock_unauthorized_connect): + self.consumer._authorized = False + await self.consumer.connect() + self.consumer.accept.assert_not_called() + mock_unauthorized_connect.assert_called_once() + + @mock.patch( + "tethys_apps.base.mixins.TethysAsyncWebsocketConsumerMixin.authorized_disconnect" + ) + async def test_disconnect(self, mock_authorized_disconnect): + self.consumer._authorized = True + event = "event" + await self.consumer.disconnect(event) + mock_authorized_disconnect.assert_called_with(event) + + @mock.patch( + "tethys_apps.base.mixins.TethysAsyncWebsocketConsumerMixin.unauthorized_disconnect" + ) + async def test_disconnect_not_authorized(self, mock_unauthorized_disconnect): + self.consumer._authorized = False + event = "event" + await self.consumer.disconnect(event) + mock_unauthorized_disconnect.assert_called_once() + + +class TestTethysWebsocketConsumer(unittest.TestCase): + def setUp(self): + self.consumer = tethys_mixins.TethysWebsocketConsumerMixin() + self.consumer.accept = mock.MagicMock() + self.consumer.permissions = ["test_permission"] + self.consumer.scope = {"user": UserFactory(), "path": "path/to/app"} + + def tearDown(self): + pass + + def test_perms_list(self): + self.assertTrue(self.consumer.perms == ["test_permission"]) + + def test_perms_none(self): + self.consumer.permissions = None + self.assertTrue(self.consumer.perms == []) + + def test_perms_str(self): + self.consumer.permissions = "test_permission,test_permission1" + self.assertTrue(self.consumer.perms == ["test_permission", "test_permission1"]) + + def test_perms_exception(self): + self.consumer.permissions = {"test": "test_permsision"} + with self.assertRaises(TypeError) as context: + self.consumer.perms + + self.assertTrue( + context.exception.args[0] + == "permissions must be a list, tuple, or comma separated string" + ) + + def test_authorized_login_required_success(self): + self.consumer.permissions = [] + self.consumer.login_required = True + self.assertTrue(self.consumer.authorized) + + def test_authorized_login_required_failure(self): + self.consumer.permissions = [] + self.consumer.login_required = True + self.consumer.scope = { + "user": mock.MagicMock(is_authenticated=False), + "path": "path/to/app", + } + self.assertFalse(self.consumer.authorized) + + def test_authorized_permissions_and(self): + self.consumer.permissions = ["test_permission"] + with mock.patch( + "tethys_apps.base.mixins.scoped_user_has_permission", user_has_perms + ): + self.assertTrue(self.consumer.authorized) + + def test_authorized_inadequate_permissions_and(self): + self.consumer.permissions = ["test_permission", "test_permission1"] + with mock.patch( + "tethys_apps.base.mixins.scoped_user_has_permission", user_has_perms + ): + self.assertFalse(self.consumer.authorized) + + def test_authorized_permissions_or(self): + self.consumer.permissions = ["test_permission", "test_permission1"] + self.consumer.permissions_use_or = True + with mock.patch( + "tethys_apps.base.mixins.scoped_user_has_permission", user_has_perms + ): + self.assertTrue(self.consumer.authorized) + + def test_authorized_inadequate_permissions_or(self): + self.consumer.permissions = ["test_permission1"] + self.consumer.permissions_use_or = True + with mock.patch( + "tethys_apps.base.mixins.scoped_user_has_permission", user_has_perms + ): + self.assertFalse(self.consumer.authorized) + + def test_authorized_connect(self): + self.consumer.authorized_connect() + + def test_unauthorized_connect(self): + self.consumer.unauthorized_connect() + + def test_authorized_disconnect(self): + event = {} + self.consumer.authorized_disconnect(event) + + def test_unauthorized_disconnect(self): + event = {} + self.consumer.unauthorized_disconnect(event) + + @mock.patch( + "tethys_apps.base.mixins.TethysWebsocketConsumerMixin.authorized_connect" + ) + def test_connect(self, mock_authorized_connect): + self.consumer._authorized = True + self.consumer.connect() + self.consumer.accept.assert_called_once() + mock_authorized_connect.assert_called_once() + + @mock.patch( + "tethys_apps.base.mixins.TethysWebsocketConsumerMixin.unauthorized_connect" + ) + def test_connect_not_authorized(self, mock_unauthorized_connect): + self.consumer._authorized = False + self.consumer.connect() + self.consumer.accept.assert_not_called() + mock_unauthorized_connect.assert_called_once() + + @mock.patch( + "tethys_apps.base.mixins.TethysWebsocketConsumerMixin.authorized_disconnect" + ) + def test_disconnect(self, mock_authorized_disconnect): + self.consumer._authorized = True + event = "event" + self.consumer.disconnect(event) + mock_authorized_disconnect.assert_called_with(event) + + @mock.patch( + "tethys_apps.base.mixins.TethysWebsocketConsumerMixin.unauthorized_disconnect" + ) + def test_disconnect_not_authorized(self, mock_unauthorized_disconnect): + self.consumer._authorized = False + event = "event" + self.consumer.disconnect(event) + mock_unauthorized_disconnect.assert_called_once() + + +def user_has_perms(_, perm): + if perm == "test_permission": + return True + return False diff --git a/tests/unit_tests/test_tethys_apps/test_utilities.py b/tests/unit_tests/test_tethys_apps/test_utilities.py index 4fa33b160..9a5152b7a 100644 --- a/tests/unit_tests/test_tethys_apps/test_utilities.py +++ b/tests/unit_tests/test_tethys_apps/test_utilities.py @@ -4,6 +4,7 @@ from tethys_sdk.testing import TethysTestCase from tethys_apps import utilities from django.core.signing import Signer +from channels.generic.websocket import AsyncWebsocketConsumer, WebsocketConsumer class TethysAppsUtilitiesTests(unittest.TestCase): @@ -1025,3 +1026,51 @@ def test_secrets_signed_unsigned_value_with_secrets( custom_secret_setting.name(), secret_signed_mock, app_target_name, False ) self.assertEqual(unsigned_secret, mock_val) + + def test_update_decorated_websocket_consumer_class(self): + class TestConsumer(WebsocketConsumer): + def authorized_connect(self): + """Connects to the websocket consumer and adds a notifications group to the channel""" + return "authorized_connect_run" + + permissions_required = ["test_permission"] + permissions_use_or = True + login_required = False + updated_class = utilities.update_decorated_websocket_consumer_class( + TestConsumer, permissions_required, permissions_use_or, login_required + ) + + self.assertTrue(updated_class.permissions == permissions_required) + self.assertTrue(updated_class.permissions_use_or == permissions_use_or) + self.assertTrue(updated_class.login_required == login_required) + self.assertTrue( + updated_class().authorized_connect() == "authorized_connect_run" + ) + + +class TestAsyncUtilities(unittest.IsolatedAsyncioTestCase): + def set_up(self): + pass + + def tear_down(self): + pass + + async def test_update_decorated_websocket_consumer_class_async(self): + class TestConsumer(AsyncWebsocketConsumer): + async def authorized_connect(self): + """Connects to the websocket consumer and adds a notifications group to the channel""" + return "authorized_connect_run" + + permissions_required = ["test_permission"] + permissions_use_or = True + login_required = False + updated_class = utilities.update_decorated_websocket_consumer_class( + TestConsumer, permissions_required, permissions_use_or, login_required + ) + + self.assertTrue(updated_class.permissions == permissions_required) + self.assertTrue(updated_class.permissions_use_or == permissions_use_or) + self.assertTrue(updated_class.login_required == login_required) + self.assertTrue( + await updated_class().authorized_connect() == "authorized_connect_run" + ) diff --git a/tethys_apps/base/mixins.py b/tethys_apps/base/mixins.py index d1e282760..7e73f9337 100644 --- a/tethys_apps/base/mixins.py +++ b/tethys_apps/base/mixins.py @@ -35,7 +35,9 @@ class TethysAsyncWebsocketConsumerMixin: @property def perms(self): if self._perms is None: - if type(self.permissions) in [list, tuple]: + if self.permissions is None: + self._perms = [] + elif type(self.permissions) in [list, tuple]: self._perms = self.permissions elif isinstance(self.permissions, str): self._perms = self.permissions.split(",") diff --git a/tethys_apps/utilities.py b/tethys_apps/utilities.py index 2a0c868af..1dbb96934 100644 --- a/tethys_apps/utilities.py +++ b/tethys_apps/utilities.py @@ -734,5 +734,8 @@ def update_decorated_websocket_consumer_class( class_bases = list(function_or_class.__bases__) class_bases.insert(0, consumer_mixin) function_or_class.__bases__ = tuple(class_bases) + function_or_class.permissions = permissions_required + function_or_class.permissions_use_or = permissions_use_or + function_or_class.login_required = login_required return function_or_class From ad548bc6c7a94422dd562c7a382bbc358420c56f Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Mon, 26 Feb 2024 15:50:38 -0600 Subject: [PATCH 14/18] added another permission example to the consumer decorator --- tethys_apps/base/controller.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tethys_apps/base/controller.py b/tethys_apps/base/controller.py index 88105f8f3..45cd86457 100644 --- a/tethys_apps/base/controller.py +++ b/tethys_apps/base/controller.py @@ -94,6 +94,17 @@ class MyConsumer(AsyncWebsocketConsumer): ) class MyConsumer(AsyncWebsocketConsumer): pass + + ------------ + + @consumer( + name='custom_name', + url='customized/url', + permissions_required='permission', + login_required=True + ) + class MyConsumer(AsyncWebsocketConsumer): + pass """ # noqa: E501 def wrapped(function_or_class): From 17372b5eeb495fe9adc8622614044cac993801fd Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Tue, 27 Feb 2024 10:45:23 -0600 Subject: [PATCH 15/18] updated examples in the controller decorator docstring --- tethys_apps/base/controller.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tethys_apps/base/controller.py b/tethys_apps/base/controller.py index 45cd86457..74ae6cc03 100644 --- a/tethys_apps/base/controller.py +++ b/tethys_apps/base/controller.py @@ -93,7 +93,9 @@ class MyConsumer(AsyncWebsocketConsumer): url='customized/url', ) class MyConsumer(AsyncWebsocketConsumer): - pass + + def connect(): + pass ------------ @@ -104,7 +106,10 @@ class MyConsumer(AsyncWebsocketConsumer): login_required=True ) class MyConsumer(AsyncWebsocketConsumer): - pass + + def authorized_connect(): + pass + """ # noqa: E501 def wrapped(function_or_class): From a6112817c2a26069130662daf63c1a05d394e912 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Wed, 28 Feb 2024 11:46:01 -0600 Subject: [PATCH 16/18] mocked channels db for docs --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 921e125d5..8c4738a2d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,6 +40,7 @@ "bokeh.server.django.consumers", "bokeh.util.compiler", "channels", + "channels.db", "channels.consumer", "conda", "conda.cli", From 62756844d0df3f5bf139782abecc17cf85af7dad Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Tue, 12 Mar 2024 08:57:29 -0500 Subject: [PATCH 17/18] cleaned and simplified update_decorated_websocket_consumer_class code --- tethys_apps/utilities.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tethys_apps/utilities.py b/tethys_apps/utilities.py index 1dbb96934..85dc0ed95 100644 --- a/tethys_apps/utilities.py +++ b/tethys_apps/utilities.py @@ -21,6 +21,7 @@ from django.core import signing from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.utils._os import safe_join +from channels.consumer import SyncConsumer from tethys_apps.base.mixins import ( TethysAsyncWebsocketConsumerMixin, @@ -724,12 +725,10 @@ def update_decorated_websocket_consumer_class( Returns: class: updated class with necessary properties and function for authorizing user access """ - base_class_name = inspect.getmro(function_or_class)[1].__name__.lower() - function_or_class_name = function_or_class.__name__.lower() - if "async" in function_or_class_name or "async" in base_class_name: - consumer_mixin = TethysAsyncWebsocketConsumerMixin - else: + if issubclass(function_or_class, SyncConsumer): consumer_mixin = TethysWebsocketConsumerMixin + else: + consumer_mixin = TethysAsyncWebsocketConsumerMixin class_bases = list(function_or_class.__bases__) class_bases.insert(0, consumer_mixin) From e20223f31cc87b3b8ea3d6bef6a8142db2582007 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Tue, 12 Mar 2024 09:00:22 -0500 Subject: [PATCH 18/18] removed unused import --- tethys_apps/utilities.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tethys_apps/utilities.py b/tethys_apps/utilities.py index 85dc0ed95..f3c4f81ac 100644 --- a/tethys_apps/utilities.py +++ b/tethys_apps/utilities.py @@ -12,7 +12,6 @@ import logging import os from pathlib import Path -import inspect import pkgutil import yaml