From 4038ab0ca87fe3c48c1c7639f463b23d5ae19c73 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Wed, 6 Mar 2024 16:30:58 -0600 Subject: [PATCH 01/23] initial implementation working for stand alone apps home and app_library now redirected to the configured app admin capabilities still working need to figure out a way to get the admin dropdown menu in the stand alone app and clean up header buttons --- tethys_apps/harvester.py | 18 ++++++++++++------ tethys_apps/urls.py | 11 ++++++++--- tethys_apps/utilities.py | 7 ++++++- tethys_portal/settings.py | 4 ++++ tethys_portal/urls.py | 26 +++++++++++++++++++++----- tethys_portal/views/home.py | 4 +--- 6 files changed, 52 insertions(+), 18 deletions(-) diff --git a/tethys_apps/harvester.py b/tethys_apps/harvester.py index cc0ec3830..e5315fa25 100644 --- a/tethys_apps/harvester.py +++ b/tethys_apps/harvester.py @@ -83,18 +83,21 @@ def harvest_apps(self): except Exception: """DO NOTHING""" - def get_url_patterns(self): + def get_url_patterns(self, url_namespaces=None): """ Generate the url pattern lists for each app and namespace them accordingly. """ app_url_patterns = dict() ext_url_patterns = dict() ws_url_patterns = dict() + apps = self.apps + if url_namespaces: + apps = [app for app in apps if app.url_namespace in url_namespaces] - for app in self.apps: + for app in apps: app_url_patterns.update(app.url_patterns["http"]) - for app in self.apps: + for app in apps: ws_url_patterns.update(app.url_patterns["websocket"]) for extension in self.extensions: @@ -108,17 +111,20 @@ def get_url_patterns(self): return url_patterns - def get_handler_patterns(self): + def get_handler_patterns(self, url_namespaces=None): """ Generate the url handler pattern lists for each app and namespace them accordingly. """ http_handler_patterns = dict() ws_handler_patterns = dict() + apps = self.apps + if url_namespaces: + apps = [app for app in apps if app.url_namespace in url_namespaces] - for app in self.apps: + for app in apps: http_handler_patterns.update(app.handler_patterns["http"]) - for app in self.apps: + for app in apps: ws_handler_patterns.update(app.handler_patterns["websocket"]) handler_patterns = { diff --git a/tethys_apps/urls.py b/tethys_apps/urls.py index 64512a808..b86b3f2ff 100644 --- a/tethys_apps/urls.py +++ b/tethys_apps/urls.py @@ -18,16 +18,21 @@ prefix_url = f"{settings.PREFIX_URL}" urlpatterns = [ - re_path(r"^$", library, name="app_library"), re_path( r"^send-beta-feedback/$", send_beta_feedback_email, name="send_beta_feedback" ), ] +if settings.STANDALONE_APP_CONTROLLER: + standalone_app_namespace, _ = settings.STANDALONE_APP_CONTROLLER.split(":") + url_namespaces = [standalone_app_namespace] +else: + urlpatterns.append(re_path(r"^$", library, name="app_library")) + url_namespaces = None # Append the app urls urlpatterns harvester = SingletonHarvester() -normal_url_patterns = harvester.get_url_patterns() -handler_url_patterns = harvester.get_handler_patterns() +normal_url_patterns = harvester.get_url_patterns(url_namespaces=url_namespaces) +handler_url_patterns = harvester.get_handler_patterns(url_namespaces=url_namespaces) # configure handler HTTP routes http_handler_patterns = [] diff --git a/tethys_apps/utilities.py b/tethys_apps/utilities.py index dc3c14c27..2ce77b199 100644 --- a/tethys_apps/utilities.py +++ b/tethys_apps/utilities.py @@ -19,6 +19,7 @@ from django.core import signing from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.utils._os import safe_join +from django.conf import settings from tethys_apps.exceptions import TethysAppSettingNotAssigned from .harvester import SingletonHarvester @@ -126,6 +127,10 @@ def get_active_app(request=None, url=None, get_class=False): from tethys_apps.models import TethysApp apps_root = "apps" + app_root_index_add = 1 + if settings.STANDALONE_APP_CONTROLLER: + app_root_index_add = 0 + apps_root = settings.STANDALONE_APP_CONTROLLER.split(":")[0].replace("_", "-") if request is not None: the_url = request.path @@ -140,7 +145,7 @@ def get_active_app(request=None, url=None, get_class=False): # Find the app key if apps_root in url_parts: # The app root_url is the path item following (+1) the apps_root item - app_root_url_index = url_parts.index(apps_root) + 1 + app_root_url_index = url_parts.index(apps_root) + app_root_index_add app_root_url = url_parts[app_root_url_index] if app_root_url: diff --git a/tethys_portal/settings.py b/tethys_portal/settings.py index af3b78e47..0a3bee0bc 100644 --- a/tethys_portal/settings.py +++ b/tethys_portal/settings.py @@ -90,6 +90,10 @@ REGISTER_CONTROLLER = TETHYS_PORTAL_CONFIG.pop("REGISTER_CONTROLLER", None) +STANDALONE_APP_CONTROLLER = TETHYS_PORTAL_CONFIG.pop("STANDALONE_APP_CONTROLLER", None) +if STANDALONE_APP_CONTROLLER: + BYPASS_TETHYS_HOME_PAGE = True + ADDITIONAL_URLPATTERNS = TETHYS_PORTAL_CONFIG.pop("ADDITIONAL_URLPATTERNS", []) SESSION_CONFIG = portal_config_settings.pop("SESSION_CONFIG", {}) diff --git a/tethys_portal/urls.py b/tethys_portal/urls.py index 6beea748b..72d9eda2c 100644 --- a/tethys_portal/urls.py +++ b/tethys_portal/urls.py @@ -14,14 +14,16 @@ from django.urls import reverse_lazy, include, re_path from django.shortcuts import redirect from django.views.decorators.cache import never_cache +from django.views.generic import RedirectView from django.contrib import admin from django.contrib.auth.views import ( PasswordResetDoneView, PasswordResetConfirmView, PasswordResetCompleteView, ) +from django.conf import settings -from tethys_apps.urls import extension_urls +from tethys_apps.urls import extension_urls, urlpatterns as tethys_app_urlpatterns from tethys_portal.views import ( accounts as tethys_portal_accounts, @@ -179,12 +181,26 @@ # re_path(r'^500/$', tethys_portal_error.handler_500, name='error_500'), # ] -urlpatterns = [ - re_path(r"^$", tethys_portal_home.home, name="home"), +if settings.STANDALONE_APP_CONTROLLER: + standalone_app_namespace, _ = settings.STANDALONE_APP_CONTROLLER.split(":") + standalone_app_namespace_hyphen = standalone_app_namespace.replace("_", "-") + standalone_app_urlpatterns = tethys_app_urlpatterns[1] + urlpatterns = [ + re_path(r"^{0}/".format(standalone_app_namespace_hyphen), include((standalone_app_urlpatterns.url_patterns, standalone_app_namespace), namespace=standalone_app_namespace)), + re_path(r"^$", RedirectView.as_view(url=f"/{standalone_app_namespace_hyphen}/"), name="home"), + re_path(r"^$", RedirectView.as_view(url=f"/{standalone_app_namespace_hyphen}/"), name="app_library") + ] +else: + urlpatterns = [ + re_path(r"^$", include("tethys_apps.urls"), name="home"), + re_path(r"^apps/", include("tethys_apps.urls")) + ] + + +urlpatterns.extend([ re_path(r"^admin/", admin_urls), re_path(r"^accounts/", include((account_urls, "accounts"), namespace="accounts")), re_path(r"^user/", include((user_urls, "user"), namespace="user")), - re_path(r"^apps/", include("tethys_apps.urls")), re_path(r"^extensions/", include(extension_urls)), re_path(r"^developer/", include(developer_urls)), re_path( @@ -208,7 +224,7 @@ name="update_dask_job_status", ), re_path(r"^api/", include((api_urls, "api"), namespace="api")), -] +]) if has_module(psa_views): oauth2_urls = [ diff --git a/tethys_portal/views/home.py b/tethys_portal/views/home.py index 89669b78d..9cf8ffc9c 100644 --- a/tethys_portal/views/home.py +++ b/tethys_portal/views/home.py @@ -21,8 +21,6 @@ def home(request): ): return redirect("app_library") - ENABLE_OPEN_PORTAL = getattr(settings, "ENABLE_OPEN_PORTAL", False) - template = get_custom_template("Home Page Template", "tethys_portal/home.html") return render( @@ -30,6 +28,6 @@ def home(request): template, { "ENABLE_OPEN_SIGNUP": settings.ENABLE_OPEN_SIGNUP, - "ENABLE_OPEN_PORTAL": ENABLE_OPEN_PORTAL, + "ENABLE_OPEN_PORTAL": settings.ENABLE_OPEN_PORTAL, }, ) From 1e4023ba53cbdceec859e447e3f5b9e573925add Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Thu, 7 Mar 2024 17:33:22 -0600 Subject: [PATCH 02/23] new site setting for standalone_app_mode adder user menu button to app base header when in standalone app mode --- .../templates/tethys_apps/app_base.html | 75 +++++++++++++++++-- tethys_cli/site_commands.py | 5 ++ tethys_config/init.py | 4 + 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/tethys_apps/templates/tethys_apps/app_base.html b/tethys_apps/templates/tethys_apps/app_base.html index 7b6c1d76f..cf59d9fc4 100644 --- a/tethys_apps/templates/tethys_apps/app_base.html +++ b/tethys_apps/templates/tethys_apps/app_base.html @@ -112,6 +112,25 @@ background: var(--app-primary-color, '#7ec1f7'); } + #app-header .btn-user-profile { + background-color: rgba(255, 255, 255, 0.08); + border: none; + color: {{ site_globals.primary_text_color }}; + } + + #app-header .btn-user-profile:hover { + background-color: rgba(255, 255, 255, 0.30); + } + + #app-header .user-menu-button .dropdown-menu .dropdown-item { + color:#3f3f3f; + } + + #app-header .user-menu-button .dropdown-menu .dropdown-item:active { + background-color: {{ site_globals.primary_color }}; + color: #dddddd; + } + #app-navigation .nav li a { color: var(--app-primary-color, '#7ec1f7'); } @@ -192,16 +211,56 @@ {% endif %} {% endblock %} {% block settings_button_override %} - {% if request.user.is_staff %} -
- -
- {% endif %} + {% if request.user.is_staff %} +
+ +
+ {% endif %} {% endblock %} {% block exit_button_override %} -
- -
+ {% if site_globals.standalone_app_mode == "false" %} +
+ +
+ {% endif %} + {% endblock %} + {% block user_menu %} + {% if user.is_authenticated and user.is_active and site_globals.standalone_app_mode == "true" %} + + {% else %} + Log In + {% endif %} {% endblock %} {% endblock %} diff --git a/tethys_cli/site_commands.py b/tethys_cli/site_commands.py index 794aa665a..bdbd2768f 100644 --- a/tethys_cli/site_commands.py +++ b/tethys_cli/site_commands.py @@ -28,6 +28,11 @@ def add_site_parser(subparsers): help="Title of the web page that appears in browser tabs and bookmarks of the site. " 'Default is "Tethys Portal".', ) + site_parser.add_argument( + "--standalone-app-mode", + dest="standalone_app_mode", + help="Boolean determining if the portal will act as a standalone app or host for multiple apps", + ) site_parser.add_argument( "--favicon", dest="favicon", diff --git a/tethys_config/init.py b/tethys_config/init.py index a7f25138f..9e80843cb 100644 --- a/tethys_config/init.py +++ b/tethys_config/init.py @@ -104,6 +104,10 @@ def setting_defaults(category): name="Site Title", content="Tethys Portal", date_modified=now ) + category.setting_set.create( + name="Standalone App Mode", content="false", date_modified=now + ) + category.setting_set.create( name="Favicon", content="/tethys_portal/images/default_favicon.png", From ba5d1cc9194f5c230489f942aea4fb36480bde89 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Fri, 8 Mar 2024 12:25:09 -0600 Subject: [PATCH 03/23] updated setting name to STANDALONE_APP and removed the necessity to add the controller name since that can be controlled by the application itself add some logic to the site commands, if a standalone app is given, then update site settings accordingly fixed some old url patterns for non standalone_app_mode that had been messed up updated header.html to not show App Library button if standalone app is enabled --- tethys_apps/urls.py | 5 ++--- tethys_apps/utilities.py | 15 +++++++++------ tethys_cli/site_commands.py | 18 +++++++++++++++++- tethys_portal/settings.py | 4 ++-- tethys_portal/templates/header.html | 2 +- tethys_portal/urls.py | 13 ++++++------- 6 files changed, 37 insertions(+), 20 deletions(-) diff --git a/tethys_apps/urls.py b/tethys_apps/urls.py index b86b3f2ff..d58d9796d 100644 --- a/tethys_apps/urls.py +++ b/tethys_apps/urls.py @@ -22,9 +22,8 @@ r"^send-beta-feedback/$", send_beta_feedback_email, name="send_beta_feedback" ), ] -if settings.STANDALONE_APP_CONTROLLER: - standalone_app_namespace, _ = settings.STANDALONE_APP_CONTROLLER.split(":") - url_namespaces = [standalone_app_namespace] +if settings.STANDALONE_APP: + url_namespaces = [settings.STANDALONE_APP] else: urlpatterns.append(re_path(r"^$", library, name="app_library")) url_namespaces = None diff --git a/tethys_apps/utilities.py b/tethys_apps/utilities.py index 2ce77b199..ffacdfd65 100644 --- a/tethys_apps/utilities.py +++ b/tethys_apps/utilities.py @@ -128,9 +128,9 @@ def get_active_app(request=None, url=None, get_class=False): apps_root = "apps" app_root_index_add = 1 - if settings.STANDALONE_APP_CONTROLLER: + if settings.STANDALONE_APP: app_root_index_add = 0 - apps_root = settings.STANDALONE_APP_CONTROLLER.split(":")[0].replace("_", "-") + apps_root = settings.STANDALONE_APP.replace("_", "-") if request is not None: the_url = request.path @@ -143,10 +143,13 @@ def get_active_app(request=None, url=None, get_class=False): app = None # Find the app key - if apps_root in url_parts: - # The app root_url is the path item following (+1) the apps_root item - app_root_url_index = url_parts.index(apps_root) + app_root_index_add - app_root_url = url_parts[app_root_url_index] + if apps_root in url_parts or (the_url == "/" and settings.STANDALONE_APP): + if settings.STANDALONE_APP: + app_root_url = apps_root + else: + # The app root_url is the path item following (+1) the apps_root item + app_root_url_index = url_parts.index(apps_root) + app_root_index_add + app_root_url = url_parts[app_root_url_index] if app_root_url: try: diff --git a/tethys_cli/site_commands.py b/tethys_cli/site_commands.py index bdbd2768f..cb6678c07 100644 --- a/tethys_cli/site_commands.py +++ b/tethys_cli/site_commands.py @@ -308,7 +308,10 @@ def gen_site_content(args): portal_yaml = Path(get_tethys_home_dir()) / "portal_config.yml" if portal_yaml.exists(): with portal_yaml.open() as f: - site_settings = yaml.safe_load(f).get("site_settings", {}) + portal_config_json = yaml.safe_load(f) + portal_config_json = update_site_settings_if_standalone_mode(portal_config_json) + site_settings = portal_config_json.get("site_settings", {}) + for category in SITE_SETTING_CATEGORIES: category_settings = site_settings.pop(category, {}) update_site_settings_content( @@ -371,6 +374,19 @@ def update_site_settings_content(settings_dict, warn_if_setting_not_found=False) obj.update(content=content, date_modified=timezone.now()) +def update_site_settings_if_standalone_mode(portal_config_json): + tethys_portal_config = portal_config_json.get('settings', {}).get('TETHYS_PORTAL_CONFIG', {}) + standalone_app = tethys_portal_config.get('STANDALONE_APP') + + if standalone_app: + standalone_app_title = standalone_app.replace("_", " ").replace("-", " ").title() + site_settings = portal_config_json.get("site_settings", {}) + site_settings["GENERAL_SETTINGS"]['STANDALONE_APP_MODE'] = "true" + site_settings["GENERAL_SETTINGS"]['BRAND_TEXT'] = standalone_app_title + + return portal_config_json + + def uncodify(code_name): setting_name = code_name.replace("_", " ").title() setting_name = setting_name.replace("Css", "CSS").replace("To ", "to ") diff --git a/tethys_portal/settings.py b/tethys_portal/settings.py index 0a3bee0bc..6b22a3276 100644 --- a/tethys_portal/settings.py +++ b/tethys_portal/settings.py @@ -90,8 +90,8 @@ REGISTER_CONTROLLER = TETHYS_PORTAL_CONFIG.pop("REGISTER_CONTROLLER", None) -STANDALONE_APP_CONTROLLER = TETHYS_PORTAL_CONFIG.pop("STANDALONE_APP_CONTROLLER", None) -if STANDALONE_APP_CONTROLLER: +STANDALONE_APP = TETHYS_PORTAL_CONFIG.pop("STANDALONE_APP", None) +if STANDALONE_APP: BYPASS_TETHYS_HOME_PAGE = True ADDITIONAL_URLPATTERNS = TETHYS_PORTAL_CONFIG.pop("ADDITIONAL_URLPATTERNS", []) diff --git a/tethys_portal/templates/header.html b/tethys_portal/templates/header.html index 449fbb5bf..f89ecfeec 100644 --- a/tethys_portal/templates/header.html +++ b/tethys_portal/templates/header.html @@ -28,7 +28,7 @@ {% block user_menu %} {% if user.is_authenticated and user.is_active %} - + {% include "tethys_portal/user/user_header_menu.html" %} {% else %} Log In {% endif %} diff --git a/tethys_portal/templates/tethys_portal/user/user_header_menu.html b/tethys_portal/templates/tethys_portal/user/user_header_menu.html new file mode 100644 index 000000000..d9eb04a54 --- /dev/null +++ b/tethys_portal/templates/tethys_portal/user/user_header_menu.html @@ -0,0 +1,32 @@ + \ No newline at end of file From 1825cc4629bb6d369e4caaa8c2f055f9053b8367 Mon Sep 17 00:00:00 2001 From: Corey Krewson Date: Mon, 1 Apr 2024 15:01:29 -0500 Subject: [PATCH 23/23] black formatted and linted --- tethys_portal/context_processors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tethys_portal/context_processors.py b/tethys_portal/context_processors.py index 144fc7e95..457a5dad9 100644 --- a/tethys_portal/context_processors.py +++ b/tethys_portal/context_processors.py @@ -19,9 +19,9 @@ def tethys_portal_context(request): if hasattr(settings, "SOCIAL_AUTH_SAML_ENABLED_IDPS") else {} ) - + single_app_mode, single_app_name = check_single_app_mode() - + context = { "has_analytical": has_module("analytical"), "has_recaptcha": has_module("snowpenguin.django.recaptcha2"),