This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a commit to branch feat/generic-experiment-launcher
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git

commit 3b79918d2b063891e77c4ba75d3cac44d439c322
Author: yasithdev <[email protected]>
AuthorDate: Fri Apr 24 21:55:06 2026 -0400

    feat(launcher): /workspace/launch view + 301 redirects from old URLs
    
    Adds the SPA shell view at /workspace/launch and wires 301 redirects from
    the old /workspace/applications, /applications/<id>/create_experiment, and
    /applications/<id>/ URLs. Updates all internal reverse() calls and nav 
config
    to point to the new launch URL. Includes failing-first TDD test suite with
    signal-handler and Vite-loader patches to make tests self-contained.
---
 .../django_airavata/apps/admin/views.py            |  2 +-
 .../django_airavata/apps/auth/signals.py           |  4 +-
 .../django_airavata/apps/workspace/apps.py         |  4 +-
 .../django_airavata_workspace/launch.html          | 10 +++
 .../apps/workspace/tests/__init__.py               |  0
 .../apps/workspace/tests/test_launch_view.py       | 96 ++++++++++++++++++++++
 .../django_airavata/apps/workspace/urls.py         | 12 +--
 .../django_airavata/apps/workspace/views.py        |  7 ++
 airavata-django-portal/django_airavata/settings.py |  2 +-
 9 files changed, 126 insertions(+), 11 deletions(-)

diff --git a/airavata-django-portal/django_airavata/apps/admin/views.py 
b/airavata-django-portal/django_airavata/apps/admin/views.py
index 5468b7776..927d98b80 100644
--- a/airavata-django-portal/django_airavata/apps/admin/views.py
+++ b/airavata-django-portal/django_airavata/apps/admin/views.py
@@ -14,7 +14,7 @@ def app_catalog(request):
     # sub-routes (new, edit) still render the admin SPA editor.
     path = request.path.rstrip("/")
     if path == "/admin/applications":
-        return redirect(reverse("django_airavata_workspace:applications"))
+        return redirect(reverse("django_airavata_workspace:launch"))
     request.active_nav_item = "app_catalog"
     return render(request, "admin/admin_base.html")
 
diff --git a/airavata-django-portal/django_airavata/apps/auth/signals.py 
b/airavata-django-portal/django_airavata/apps/auth/signals.py
index 143a78a32..12f20f53f 100644
--- a/airavata-django-portal/django_airavata/apps/auth/signals.py
+++ b/airavata-django-portal/django_airavata/apps/auth/signals.py
@@ -26,8 +26,8 @@ def email_user_added_to_group(sender, user, groups, request, 
**kwargs):
             "last_name": user.lastName,
             "username": user.userId,
             "portal_title": settings.PORTAL_TITLE,
-            "dashboard_url": 
request.build_absolute_uri(reverse("django_airavata_workspace:applications")),
-            "experiments_url": 
request.build_absolute_uri(reverse("django_airavata_workspace:applications")),
+            "dashboard_url": 
request.build_absolute_uri(reverse("django_airavata_workspace:launch")),
+            "experiments_url": 
request.build_absolute_uri(reverse("django_airavata_workspace:launch")),
             "group_names": [g.name for g in groups],
         }
     )
diff --git a/airavata-django-portal/django_airavata/apps/workspace/apps.py 
b/airavata-django-portal/django_airavata/apps/workspace/apps.py
index ed13e11d3..15991acc8 100644
--- a/airavata-django-portal/django_airavata/apps/workspace/apps.py
+++ b/airavata-django-portal/django_airavata/apps/workspace/apps.py
@@ -21,8 +21,8 @@ class WorkspaceConfig(AiravataAppConfig):
         {
             "label": "Applications",
             "icon": "fa fa-cubes",
-            "url": "django_airavata_workspace:applications",
-            "active_prefixes": ["applications"],
+            "url": "django_airavata_workspace:launch",
+            "active_prefixes": ["applications", "launch"],
         },
         {
             "label": "Datasets",
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/templates/django_airavata_workspace/launch.html
 
b/airavata-django-portal/django_airavata/apps/workspace/templates/django_airavata_workspace/launch.html
new file mode 100644
index 000000000..5048ab712
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/templates/django_airavata_workspace/launch.html
@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+{% load django_vite %}
+
+{% block content %}
+  <div id="launch-app" data-feature-flag="{{ feature_flag|yesno:'on,off' 
}}"></div>
+{% endblock %}
+
+{% block scripts %}
+  {% vite_asset "js/entry-launch.ts" app="WORKSPACE" %}
+{% endblock %}
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/tests/__init__.py 
b/airavata-django-portal/django_airavata/apps/workspace/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/tests/test_launch_view.py
 
b/airavata-django-portal/django_airavata/apps/workspace/tests/test_launch_view.py
new file mode 100644
index 000000000..eb7065622
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/tests/test_launch_view.py
@@ -0,0 +1,96 @@
+from unittest.mock import patch
+
+from django.contrib.auth import get_user_model
+from django.contrib.auth.signals import user_logged_in
+from django.test import TestCase
+
+# Fake authz token returned by the patched get_authz_token so that
+# authz_token_middleware doesn't log out the test user.
+_FAKE_AUTHZ_TOKEN = {
+    "accessToken": "test.eyJ1c2VybmFtZSI6ImFsaWNlIn0.sig",
+    "gatewayID": "test-gateway",
+    "userName": "alice",
+}
+
+
+class LaunchViewTest(TestCase):
+    def setUp(self):
+        # Disconnect portal signal handlers that require request.authz_token,
+        # which is not present on plain Django test requests.
+        user_logged_in.disconnect(dispatch_uid="auth_initialize_user_profile")
+        
user_logged_in.disconnect(dispatch_uid="auth_project_auto_provisioning")
+
+        User = get_user_model()
+        self.user = User.objects.create_user(username="alice", password="pw")
+        self.client.force_login(self.user)
+
+        # authz_token_middleware calls get_authz_token and logs out the user if
+        # it returns None (because there's no real Keycloak session in tests).
+        # Patch it to return a fake token so the test user stays logged in.
+        self._authz_patcher = patch(
+            "django_airavata.apps.auth.middleware.utils.get_authz_token",
+            return_value=_FAKE_AUTHZ_TOKEN,
+        )
+        self._authz_patcher.start()
+
+        # {% vite_asset %} requires a built manifest entry for 
js/entry-launch.ts,
+        # which won't exist until Task 9 builds it. Patch the tag to return an
+        # empty string so template rendering doesn't error out in unit tests.
+        self._vite_patcher = patch(
+            
"django_vite.templatetags.django_vite.DjangoViteAssetLoader.instance",
+            return_value=_FakeViteLoader(),
+        )
+        self._vite_patcher.start()
+
+    def tearDown(self):
+        self._authz_patcher.stop()
+        self._vite_patcher.stop()
+
+        # Re-connect the signal handlers so other test suites are not affected.
+        from django_airavata.apps.auth.signals import initialize_user_profile, 
provision_user_projects
+
+        user_logged_in.connect(
+            initialize_user_profile,
+            dispatch_uid="auth_initialize_user_profile",
+        )
+        user_logged_in.connect(
+            provision_user_projects,
+            dispatch_uid="auth_project_auto_provisioning",
+        )
+
+    def test_launch_renders_for_authenticated_user(self):
+        resp = self.client.get("/workspace/launch")
+        self.assertEqual(resp.status_code, 200)
+        self.assertContains(resp, 'id="launch-app"')
+
+    def test_old_create_experiment_url_redirects_to_launch(self):
+        resp = 
self.client.get("/workspace/applications/anything/create_experiment", 
follow=False)
+        self.assertEqual(resp.status_code, 301)
+        self.assertEqual(resp["Location"], "/workspace/launch")
+
+    def test_old_applications_url_redirects_to_launch(self):
+        resp = self.client.get("/workspace/applications", follow=False)
+        self.assertEqual(resp.status_code, 301)
+        self.assertEqual(resp["Location"], "/workspace/launch")
+
+    def test_launch_requires_auth(self):
+        self.client.logout()
+        self._authz_patcher.stop()
+        self._authz_patcher = patch(
+            "django_airavata.apps.auth.middleware.utils.get_authz_token",
+            return_value=None,
+        )
+        self._authz_patcher.start()
+        resp = self.client.get("/workspace/launch", follow=False)
+        # Login redirect — exact path depends on AUTH_LOGIN_URL but we check 
redirect or 401
+        self.assertIn(resp.status_code, (302, 401))
+
+
+class _FakeViteLoader:
+    """Minimal stub for DjangoViteAssetLoader used in tests only."""
+
+    def generate_vite_asset(self, path, app=None, **kwargs):
+        return ""
+
+    def generate_vite_asset_url(self, path, app=None, **kwargs):
+        return ""
diff --git a/airavata-django-portal/django_airavata/apps/workspace/urls.py 
b/airavata-django-portal/django_airavata/apps/workspace/urls.py
index a4669f115..d493334b5 100644
--- a/airavata-django-portal/django_airavata/apps/workspace/urls.py
+++ b/airavata-django-portal/django_airavata/apps/workspace/urls.py
@@ -39,19 +39,21 @@ urlpatterns = [
     ),
     # Project edit
     re_path(r"^projects/(?P<project_id>[^/]+)/edit$", views.edit_project, 
name="edit_project"),
+    # Generic experiment launcher (Task 5)
+    re_path(r"^launch$", views.launch, name="launch"),
     # Applications (gateway-wide)
     re_path(r"^applications/new$", views.new_application, 
name="new_application"),
     re_path(
         r"^applications/(?P<app_module_id>[^/]+)/create_experiment$",
-        views.create_experiment,
-        name="create_experiment",
+        RedirectView.as_view(url="/workspace/launch", permanent=True),
+        name="create_experiment_redirect",
     ),
     re_path(
         r"^applications/(?P<app_module_id>[^/]+)/$",
-        views.edit_application,
-        name="edit_application",
+        RedirectView.as_view(url="/workspace/launch", permanent=True),
+        name="application_editor_redirect",
     ),
-    re_path(r"^applications$", views.applications, name="applications"),
+    re_path(r"^applications$", RedirectView.as_view(url="/workspace/launch", 
permanent=True), name="applications_redirect"),
     # Resources — storage
     re_path(
         r"^storage/(?P<storage_resource_id>[^/]+)/tree(?:/(?P<path>.*))?$",
diff --git a/airavata-django-portal/django_airavata/apps/workspace/views.py 
b/airavata-django-portal/django_airavata/apps/workspace/views.py
index a5e945602..34258b59c 100644
--- a/airavata-django-portal/django_airavata/apps/workspace/views.py
+++ b/airavata-django-portal/django_airavata/apps/workspace/views.py
@@ -451,6 +451,13 @@ def datasets_list(request):
     )
 
 
+@login_required
+def launch(request):
+    return render(request, "django_airavata_workspace/launch.html", {
+        "feature_flag": getattr(settings, "FEATURE_GENERIC_LAUNCHER", False),
+    })
+
+
 @login_required
 def experiments_list(request, project_id):
     request.active_nav_item = "projects"
diff --git a/airavata-django-portal/django_airavata/settings.py 
b/airavata-django-portal/django_airavata/settings.py
index 493b1875f..0a1b99268 100644
--- a/airavata-django-portal/django_airavata/settings.py
+++ b/airavata-django-portal/django_airavata/settings.py
@@ -253,7 +253,7 @@ WAGTAILIMAGES_JPEG_QUALITY = 100
 DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000
 
 LOGIN_URL = "django_airavata_auth:login"
-LOGIN_REDIRECT_URL = "django_airavata_workspace:applications"
+LOGIN_REDIRECT_URL = "django_airavata_workspace:launch"
 LOGOUT_REDIRECT_URL = "/"
 
 AUTHENTICATION_OPTIONS = {

Reply via email to