From 42b12ba8680b4bcbd2daa0e233a1814e19152ffe Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Date: Sat, 15 Jul 2023 13:05:35 -0400 Subject: [PATCH 01/10] Extend code to allow serving the app from a subpath --- manager/manager/settings.py | 14 ++++++++++++-- manager/subscription/routing.py | 6 +++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/manager/manager/settings.py b/manager/manager/settings.py index f18b1fee..fa122472 100644 --- a/manager/manager/settings.py +++ b/manager/manager/settings.py @@ -17,6 +17,10 @@ # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ +# Define the URL subpath for this application when deployed: +FORCE_SCRIPT_NAME = os.environ.get("URL_SUBPATH") + +# Define the type of storage to use for media files: remote or local DEFAULT_FILE_STORAGE = ( "manager.utils.RemoteStorage" if os.environ.get("REMOTE_STORAGE") @@ -181,7 +185,11 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ -STATIC_URL = "/manager/static/" + +STATIC_URL = ( + f"{FORCE_SCRIPT_NAME}/manager/static/" if FORCE_SCRIPT_NAME else "/manager/static/" +) + """URL to access Django static files (`string`)""" STATIC_ROOT = os.path.join(BASE_DIR, "static") @@ -197,7 +205,9 @@ os.path.join(BASE_DIR, "static_files"), ] -MEDIA_URL = "/media/" +MEDIA_URL = ( + f"{FORCE_SCRIPT_NAME}/manager/media/" if FORCE_SCRIPT_NAME else "/manager/media/" +) """URL for media files access (`string`)""" if TESTING: diff --git a/manager/subscription/routing.py b/manager/subscription/routing.py index 3d734c81..d523f630 100644 --- a/manager/subscription/routing.py +++ b/manager/subscription/routing.py @@ -2,10 +2,14 @@ from django.conf.urls import url from subscription.auth import TokenAuthMiddleware from .consumers import SubscriptionConsumer +from django.conf import settings + websocket_urlpatterns = [ url( - r"^manager(.*?)/ws/subscription/?$", + rf"^{settings.FORCE_SCRIPT_NAME[1:]}/manager(.*?)/ws/subscription/?$" + if settings.FORCE_SCRIPT_NAME + else r"^manager(.*?)/ws/subscription/?$", TokenAuthMiddleware(SubscriptionConsumer.as_asgi()), ), ] From 136f93f70ce13d046617448069bd75fd74749c0b Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Date: Sat, 15 Jul 2023 13:06:02 -0400 Subject: [PATCH 02/10] Formatter fix --- manager/manager/urls.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/manager/manager/urls.py b/manager/manager/urls.py index 5dd143d8..6eb78307 100644 --- a/manager/manager/urls.py +++ b/manager/manager/urls.py @@ -22,7 +22,6 @@ from drf_yasg.views import get_schema_view from drf_yasg import openapi from rest_framework import permissions -from django.conf import settings schema_view = get_schema_view( openapi.Info( @@ -38,7 +37,8 @@ path("manager/admin/", admin.site.urls), path("manager/test/", TemplateView.as_view(template_name="test.html")), path( - "manager/login/", TemplateView.as_view(template_name="registration/login.html") + "manager/login/", + TemplateView.as_view(template_name="registration/login.html"), ), path("manager/api/", include("api.urls")), path("manager/ui_framework/", include("ui_framework.urls")), @@ -57,5 +57,8 @@ schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc", ), - path("manager/schema_validation/", TemplateView.as_view(template_name="test.html")), + path( + "manager/schema_validation/", + TemplateView.as_view(template_name="test.html"), + ), ] From 7f093518b19679243a956aa45302fa3783a59178 Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Date: Sat, 15 Jul 2023 13:07:09 -0400 Subject: [PATCH 03/10] Update remote TTS LOVE config file url --- manager/api/fixtures/initial_data_remote_tucson.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manager/api/fixtures/initial_data_remote_tucson.json b/manager/api/fixtures/initial_data_remote_tucson.json index 7f585d2c..6df1cf70 100644 --- a/manager/api/fixtures/initial_data_remote_tucson.json +++ b/manager/api/fixtures/initial_data_remote_tucson.json @@ -7,7 +7,7 @@ "update_timestamp": "2020-12-29T14:21:31.040Z", "user": 1, "file_name": "default.json", - "config_file": "http://love.tu.lsst.org/media/configs/default.json" + "config_file": "https://tucson-teststand.lsst.codes/love/manager/media/configs/default.json" } }, { From 4536934ccc1e9b7da7ec6eeead398422a137430a Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Date: Sat, 15 Jul 2023 13:07:37 -0400 Subject: [PATCH 04/10] Add new URL_SUBPATH env variable to the documentation --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e16fb86f..3ce6853e 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ All these variables are initialized with default variables defined in :code:`.en - `LOVE_SITE`: defines the site name of the LOVE system. This value is used to identify the LOVE system in the LOVE-manager. - `REMOTE_STORAGE`: defines if remote storage is used. If this variable is defined, then the LOVE-manager will connect to the LFA to upload files. If not defined, then the LOVE-manager will store the files locally. - `COMMANDING_PERMISSION_TYPE`: defines the type of permission to use for commanding. Currently two options are available: `user` and `location`. If `user` is used, then requests from users with `api.command.execute_command` permission are allowed. If `location` is used, then only requests from the configured location of control will be allowed. If not defined, then `user` will be used. +- `URL_SUBPATH`: defines the path where the LOVE-manager will be served. If not defined, then requests will be served from the root path `/`. Note: the application has its own routing system, so this variable must be thought as a prefix to the application's routes. # Local load for development From 376c79a88eda5bc46fd661acd47cd1ac6444d1ae Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Date: Sat, 15 Jul 2023 13:09:02 -0400 Subject: [PATCH 05/10] Fix LFA file reading method to support format --- manager/manager/utils.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/manager/manager/utils.py b/manager/manager/utils.py index ee6e3853..45277ea8 100644 --- a/manager/manager/utils.py +++ b/manager/manager/utils.py @@ -61,7 +61,13 @@ class RemoteStorage(Storage): PREFIX_THUMBNAIL = "thumbnails/" PREFIX_CONFIG = "configs/" - ALLOWED_FILE_TYPES = ["image/png", "image/jpeg", "image/jpg", "application/json"] + ALLOWED_FILE_TYPES = [ + "image/png", + "image/jpeg", + "image/jpg", + "application/json", + "binary/octet-stream", + ] def __init__(self, location=None): self.location = f"http://{os.environ.get('COMMANDER_HOSTNAME')}:{os.environ.get('COMMANDER_PORT')}/lfa" @@ -78,13 +84,21 @@ def _open(self, name, mode="rb"): # Validate name is a remote url self._validate_LFA_url(name) + + # Make request to remote server response = requests.get(name) if response.status_code != 200: raise FileNotFoundError(f"Error requesting file at: {name}.") + # Create temporary file to store the response tf = TemporaryFile() + # If request is for thumbnail (image file) - if response.headers.get("content-type") in RemoteStorage.ALLOWED_FILE_TYPES[:3]: + if ( + response.headers.get("content-type") in RemoteStorage.ALLOWED_FILE_TYPES[:3] + ) or response.headers.get("content-type") == RemoteStorage.ALLOWED_FILE_TYPES[ + 4 + ]: byte_encoded_response = response.content tf.write(byte_encoded_response) # Before sending the file, we need to reset the file pointer to the beginning @@ -92,7 +106,11 @@ def _open(self, name, mode="rb"): return tf # If request is for config files (json file) - if response.headers.get("content-type") == RemoteStorage.ALLOWED_FILE_TYPES[3]: + if ( + response.headers.get("content-type") == RemoteStorage.ALLOWED_FILE_TYPES[3] + or response.headers.get("content-type") + == RemoteStorage.ALLOWED_FILE_TYPES[4] + ): json_response = response.json() byte_encoded_response = json.dumps(json_response).encode("ascii") tf.write(byte_encoded_response) From 76b9a9c1e5a2b36fda76d8f0741b816d8d9d7999 Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Date: Mon, 17 Jul 2023 09:56:55 -0400 Subject: [PATCH 06/10] Update CHANGELOG --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a113ed6f..c1fca576 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,11 @@ Version History =============== +v5.14.0 +-------- + +* Extend LOVE manager routing system for subpath app serving ``_ + v5.13.0 -------- From e6101d3c382d253d5436c813ab19331936abe481 Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Date: Mon, 17 Jul 2023 10:48:43 -0400 Subject: [PATCH 07/10] Remove testing media routes --- manager/manager/settings.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/manager/manager/settings.py b/manager/manager/settings.py index fa122472..8b6ec7d6 100644 --- a/manager/manager/settings.py +++ b/manager/manager/settings.py @@ -209,13 +209,8 @@ f"{FORCE_SCRIPT_NAME}/manager/media/" if FORCE_SCRIPT_NAME else "/manager/media/" ) """URL for media files access (`string`)""" - -if TESTING: - MEDIA_BASE = os.path.join(BASE_DIR, "ui_framework", "tests") - MEDIA_ROOT = os.path.join(BASE_DIR, "ui_framework", "tests", "media") -else: - MEDIA_BASE = BASE_DIR - MEDIA_ROOT = os.path.join(BASE_DIR, "media") +MEDIA_BASE = BASE_DIR +MEDIA_ROOT = os.path.join(BASE_DIR, "media") # Channels ASGI_APPLICATION = "manager.routing.application" From 20aaf7f0c37ef6a9fde05a5c90a0aa441d0c632f Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Date: Mon, 17 Jul 2023 10:49:07 -0400 Subject: [PATCH 08/10] Adjust tests due to new media url --- manager/ui_framework/tests/tests_view_thumbnail.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/manager/ui_framework/tests/tests_view_thumbnail.py b/manager/ui_framework/tests/tests_view_thumbnail.py index 65fa3b9b..6fd6d8ba 100644 --- a/manager/ui_framework/tests/tests_view_thumbnail.py +++ b/manager/ui_framework/tests/tests_view_thumbnail.py @@ -12,7 +12,7 @@ from ui_framework.models import View from unittest import mock from api.models import Token -from manager import settings +from django.conf import settings @override_settings(DEBUG=True) @@ -139,7 +139,9 @@ def test_new_view(self): # - thumbnail url view = View.objects.get(name="view name") - self.assertEqual(view.thumbnail.url, "/media/thumbnails/view_1.png") + self.assertEqual( + view.thumbnail.url, f"{settings.MEDIA_URL}thumbnails/view_1.png" + ) # - expected response data expected_response = { @@ -152,7 +154,8 @@ def test_new_view(self): self.assertEqual(response.data, expected_response) # - stored file content - file_url = settings.MEDIA_BASE + view.thumbnail.url + thumbnail_url = view.thumbnail.url.replace("/manager/media/", "/") + file_url = settings.MEDIA_ROOT + thumbnail_url expected_url = mock_location + ".png" self.assertTrue( filecmp.cmp(file_url, expected_url), From fe7dc89a4f519dd2fc0e22ffb5567a6c6ce0f1bb Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Date: Mon, 17 Jul 2023 15:09:53 -0400 Subject: [PATCH 09/10] Commit changes to daphne too for production environment --- manager/runserver.sh | 6 +++++- manager/subscription/routing.py | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/manager/runserver.sh b/manager/runserver.sh index 72d6dd23..db5b17e5 100755 --- a/manager/runserver.sh +++ b/manager/runserver.sh @@ -36,4 +36,8 @@ fi python manage.py loaddata ui_framework/fixtures/initial_data_${love_site}.json echo -e "\nStarting server" -daphne -b 0.0.0.0 -p 8000 manager.asgi:application +if [ -z ${URL_SUBPATH} ]; then + daphne -b 0.0.0.0 -p 8000 manager.asgi:application +else + daphne --root-path=${URL_SUBPATH} -b 0.0.0.0 -p 8000 manager.asgi:application +fi diff --git a/manager/subscription/routing.py b/manager/subscription/routing.py index d523f630..75dce5d6 100644 --- a/manager/subscription/routing.py +++ b/manager/subscription/routing.py @@ -5,11 +5,11 @@ from django.conf import settings +URL_PREFIX = settings.FORCE_SCRIPT_NAME[1:] + "/" if settings.FORCE_SCRIPT_NAME else "" + websocket_urlpatterns = [ url( - rf"^{settings.FORCE_SCRIPT_NAME[1:]}/manager(.*?)/ws/subscription/?$" - if settings.FORCE_SCRIPT_NAME - else r"^manager(.*?)/ws/subscription/?$", + rf"^{URL_PREFIX}manager(.*?)/ws/subscription/?$", TokenAuthMiddleware(SubscriptionConsumer.as_asgi()), ), ] From 0abd7d011da76dfc27842b291a7d3ea6f69b8998 Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Date: Mon, 17 Jul 2023 16:39:26 -0400 Subject: [PATCH 10/10] Pair review fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ce6853e..60c793f1 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ All these variables are initialized with default variables defined in :code:`.en - `LOVE_SITE`: defines the site name of the LOVE system. This value is used to identify the LOVE system in the LOVE-manager. - `REMOTE_STORAGE`: defines if remote storage is used. If this variable is defined, then the LOVE-manager will connect to the LFA to upload files. If not defined, then the LOVE-manager will store the files locally. - `COMMANDING_PERMISSION_TYPE`: defines the type of permission to use for commanding. Currently two options are available: `user` and `location`. If `user` is used, then requests from users with `api.command.execute_command` permission are allowed. If `location` is used, then only requests from the configured location of control will be allowed. If not defined, then `user` will be used. -- `URL_SUBPATH`: defines the path where the LOVE-manager will be served. If not defined, then requests will be served from the root path `/`. Note: the application has its own routing system, so this variable must be thought as a prefix to the application's routes. +- `URL_SUBPATH`: defines the path where the LOVE-manager will be served. If not defined, then requests will be served from the root path `/`. Note: the application has its own routing system, so this variable must be thought of as a prefix to the application's routes. # Local load for development