diff --git a/docs/tethys_portal/configuration.rst b/docs/tethys_portal/configuration.rst index db2685521..c0162604b 100644 --- a/docs/tethys_portal/configuration.rst +++ b/docs/tethys_portal/configuration.rst @@ -84,6 +84,8 @@ STATIC_ROOT the Django `STATIC_ROOT - - - {% endif %} + {% if request.user.is_staff %} +
+ +
+ {% endif %} {% endblock %} {% block exit_button_override %} -
- -
+ {% if not single_app_mode %} +
+ +
+ {% endif %} + {% endblock %} + {% block user_menu %} + {% if user.is_authenticated and user.is_active and single_app_mode %} + {% include "tethys_portal/user/user_header_menu.html" %} + {% endif %} {% endblock %} {% endblock %} diff --git a/tethys_apps/urls.py b/tethys_apps/urls.py index 442df2ef0..b062d7170 100644 --- a/tethys_apps/urls.py +++ b/tethys_apps/urls.py @@ -11,36 +11,69 @@ import logging from django.urls import include, re_path from channels.routing import URLRouter +from django.views.generic import RedirectView from tethys_apps.harvester import SingletonHarvester from tethys_apps.views import library, send_beta_feedback_email +from tethys_apps.utilities import get_configured_standalone_app from django.conf import settings tethys_log = logging.getLogger("tethys." + __name__) prefix_url = f"{settings.PREFIX_URL}" urlpatterns = [ - re_path(r"^$", library, name="app_library"), re_path( r"^send-beta-feedback/$", send_beta_feedback_email, name="send_beta_feedback" ), ] +if settings.MULTIPLE_APP_MODE: + urlpatterns.append(re_path(r"^$", library, name="app_library")) + url_namespaces = None +else: + standalone_app = get_configured_standalone_app() + urlpatterns.append( + re_path( + r"^apps/", + RedirectView.as_view(pattern_name="home"), + name="app_library", + ) + ) + url_namespaces = [standalone_app.url_namespace] # 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 = [] for namespace, urls in handler_url_patterns["http_handler_patterns"].items(): - root_pattern = r"^apps/{0}/".format(namespace.replace("_", "-")) + + if settings.MULTIPLE_APP_MODE: + root_pattern = f'apps/{namespace.replace("_", "-")}/' + else: + root_pattern = "" + if prefix_url is not None and prefix_url != "/": - root_pattern = rf"^{prefix_url}/apps/{0}/".format(namespace.replace("_", "-")) + root_pattern = f"{prefix_url}/{root_pattern}" + root_pattern = rf"^{root_pattern}" + http_handler_patterns.append(re_path(root_pattern, URLRouter(urls))) # Add app url patterns to urlpatterns, namespaced per app appropriately for namespace, urls in normal_url_patterns["app_url_patterns"].items(): - root_pattern = r"^{0}/".format(namespace.replace("_", "-")) + if settings.MULTIPLE_APP_MODE: + root_pattern = r"^{0}/".format(namespace.replace("_", "-")) + else: + root_pattern = "" + home_urls = [url for url in urls if url.name == "home"] + urlpatterns.append( + re_path( + r"", + include(home_urls[:1]), + name="home", + ), + ) + urlpatterns.append( re_path(root_pattern, include((urls, namespace), namespace=namespace)) ) @@ -49,7 +82,11 @@ ext_url_patterns = normal_url_patterns["ext_url_patterns"] extension_urls = [] for namespace, urls in ext_url_patterns.items(): - root_pattern = r"^{0}/".format(namespace.replace("_", "-")) + if settings.MULTIPLE_APP_MODE: + root_pattern = r"^{0}/".format(namespace.replace("_", "-")) + else: + root_pattern = r"^" + extension_urls.append( re_path(root_pattern, include((urls, namespace), namespace=namespace)) ) @@ -62,12 +99,16 @@ def prepare_websocket_urls(app_websocket_url_patterns): prepared_urls = [] for namespace, urls in app_websocket_url_patterns.items(): - root_url = namespace.replace("_", "-") + if settings.MULTIPLE_APP_MODE: + root_url = f'apps/{namespace.replace("_", "-")}/' + else: + root_url = "" + for u in urls: url_str = str(u.pattern).replace("^", "") - namespaced_url_str = f"^apps/{root_url}/{url_str}" + namespaced_url_str = rf"^{root_url}{url_str}" if prefix_url is not None and prefix_url != "/": - namespaced_url_str = f"^{prefix_url}/apps/{root_url}/{url_str}" + namespaced_url_str = rf"^{prefix_url}/{root_url}{url_str}" namespaced_url = re_path(namespaced_url_str, u.callback, name=u.name) prepared_urls.append(namespaced_url) diff --git a/tethys_apps/utilities.py b/tethys_apps/utilities.py index f3c4f81ac..479326cec 100644 --- a/tethys_apps/utilities.py +++ b/tethys_apps/utilities.py @@ -20,6 +20,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 channels.consumer import SyncConsumer from tethys_apps.base.mixins import ( @@ -131,36 +132,37 @@ def get_active_app(request=None, url=None, get_class=False): """ from tethys_apps.models import TethysApp - apps_root = "apps" - - if request is not None: - the_url = request.path - elif url is not None: - the_url = url - else: - return None - - url_parts = the_url.split("/") - 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) + 1 - app_root_url = url_parts[app_root_url_index] - - if app_root_url: - try: - # Get the app from the database - app = TethysApp.objects.get(root_url=app_root_url) - except ObjectDoesNotExist: - tethys_log.warning( - 'Could not locate app with root url "{0}".'.format(app_root_url) - ) - except MultipleObjectsReturned: - tethys_log.warning( - 'Multiple apps found with root url "{0}".'.format(app_root_url) - ) + if settings.MULTIPLE_APP_MODE: + if request is not None: + the_url = request.path + elif url is not None: + the_url = url + else: + return None + + url_parts = the_url.split("/") + app = None + apps_root = "apps" + 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 = url_parts[app_root_url_index] + + if app_root_url: + try: + # Get the app from the database + app = TethysApp.objects.get(root_url=app_root_url) + except ObjectDoesNotExist: + tethys_log.warning( + 'Could not locate app with root url "{0}".'.format(app_root_url) + ) + except MultipleObjectsReturned: + tethys_log.warning( + 'Multiple apps found with root url "{0}".'.format(app_root_url) + ) + else: + app = get_configured_standalone_app() if get_class: app = get_app_class(app) @@ -611,6 +613,24 @@ def get_installed_tethys_items(apps=False, extensions=False): return paths +def get_configured_standalone_app(): + """ + Returns a list apps installed in the tethysapp directory. + """ + from tethys_apps.models import TethysApp + + standalone_app = settings.STANDALONE_APP + + if standalone_app: + app = TethysApp.objects.get(package=standalone_app) + else: + app = TethysApp.objects.first() + if not app: + raise ObjectDoesNotExist("No Tethys Apps have been installed") + + return app + + def get_installed_tethys_apps(): """ Returns a list apps installed in the tethysapp directory. diff --git a/tethys_portal/context_processors.py b/tethys_portal/context_processors.py index a9bc03267..457a5dad9 100644 --- a/tethys_portal/context_processors.py +++ b/tethys_portal/context_processors.py @@ -10,6 +10,7 @@ from .optional_dependencies import has_module from django.conf import settings +from tethys_apps.utilities import get_configured_standalone_app def tethys_portal_context(request): @@ -18,6 +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"), @@ -26,7 +30,16 @@ def tethys_portal_context(request): "has_gravatar": has_module("django_gravatar"), "has_session_security": has_module("session_security"), "has_oauth2_provider": has_module("oauth2_provider"), + "single_app_mode": single_app_mode, + "single_app_name": single_app_name, "idp_backends": idps.keys(), } return context + + +def check_single_app_mode(): + if settings.MULTIPLE_APP_MODE: + return False, None + else: + return True, get_configured_standalone_app().name diff --git a/tethys_portal/settings.py b/tethys_portal/settings.py index ef7d94f93..84b95878e 100644 --- a/tethys_portal/settings.py +++ b/tethys_portal/settings.py @@ -88,6 +88,11 @@ REGISTER_CONTROLLER = TETHYS_PORTAL_CONFIG.pop("REGISTER_CONTROLLER", None) +MULTIPLE_APP_MODE = TETHYS_PORTAL_CONFIG.pop("MULTIPLE_APP_MODE", True) +STANDALONE_APP = TETHYS_PORTAL_CONFIG.pop("STANDALONE_APP", None) +if not MULTIPLE_APP_MODE: + 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/templates/header.html b/tethys_portal/templates/header.html index 449fbb5bf..6551fe131 100644 --- a/tethys_portal/templates/header.html +++ b/tethys_portal/templates/header.html @@ -30,45 +30,14 @@ {% url 'app_library' as app_library_url %} {% if ENABLE_OPEN_PORTAL or user.is_authenticated and user.is_active %} {% endif %} {% endblock %} {% 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 diff --git a/tethys_portal/urls.py b/tethys_portal/urls.py index 1feaa07c7..0b3dc8b62 100644 --- a/tethys_portal/urls.py +++ b/tethys_portal/urls.py @@ -180,36 +180,48 @@ # re_path(r'^500/$', tethys_portal_error.handler_500, name='error_500'), # ] -urlpatterns = [ - re_path(r"^$", tethys_portal_home.home, name="home"), - 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( - r"^handoff/(?P[\w-]+)/$", - tethys_apps_views.handoff_capabilities, - name="handoff_capabilities", - ), - re_path( - r"^handoff/(?P[\w-]+)/(?P[\w-]+)/$", - tethys_apps_views.handoff, - name="handoff", - ), - re_path( - r"^update-job-status/(?P[\w-]+)/$", - tethys_apps_views.update_job_status, - name="update_job_status", - ), - re_path( - r"^update-dask-job-status/(?P[\w-]+)/$", - tethys_apps_views.update_dask_job_status, - name="update_dask_job_status", - ), - re_path(r"^api/", include((api_urls, "api"), namespace="api")), -] +if settings.MULTIPLE_APP_MODE: + urlpatterns = [ + re_path(r"^$", tethys_portal_home.home, name="home"), + re_path(r"^apps/", include("tethys_apps.urls")), + ] +else: + urlpatterns = [ + re_path(r"^", 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"^extensions/", include(extension_urls)), + re_path(r"^developer/", include(developer_urls)), + re_path( + r"^handoff/(?P[\w-]+)/$", + tethys_apps_views.handoff_capabilities, + name="handoff_capabilities", + ), + re_path( + r"^handoff/(?P[\w-]+)/(?P[\w-]+)/$", + tethys_apps_views.handoff, + name="handoff", + ), + re_path( + r"^update-job-status/(?P[\w-]+)/$", + tethys_apps_views.update_job_status, + name="update_job_status", + ), + re_path( + r"^update-dask-job-status/(?P[\w-]+)/$", + tethys_apps_views.update_dask_job_status, + 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 5e2256781..e794581b3 100644 --- a/tethys_portal/views/home.py +++ b/tethys_portal/views/home.py @@ -22,8 +22,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( @@ -31,6 +29,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, }, )