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

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


The following commit(s) were added to refs/heads/main by this push:
     new da271f5fb feat(portal): align thin proto-direct portal with the 
SSH/SFTP resource model (#214)
da271f5fb is described below

commit da271f5fb2cdbdab82d9f2e4780bfe7aef26b344
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Fri Jun 12 13:44:56 2026 -0400

    feat(portal): align thin proto-direct portal with the SSH/SFTP resource 
model (#214)
    
    - Update JS models (BaseEnum, FullExperiment, 
GroupComputeResourcePreference, Job)
      and Vue workspace components for the proto-direct SDK + snake_case fields
    - API view/output adjustments (output_views, view_utils, views, dataparsers)
    - auth build-stats.mjs + package.json; settings.py and base template tweaks
---
 .../django_airavata/apps/api/output_views.py       |  4 +--
 .../django_airavata_api/js/models/BaseEnum.js      | 31 +++++++++++-----
 .../js/models/FullExperiment.js                    |  9 ++++-
 .../js/models/GroupComputeResourcePreference.js    |  6 ++--
 .../static/django_airavata_api/js/models/Job.js    |  8 ++++-
 .../django_airavata/apps/api/view_utils.py         | 42 ++++++++++++++++++----
 .../django_airavata/apps/api/views.py              | 17 ++++-----
 .../django_airavata/apps/auth/build-stats.mjs      | 15 ++++++++
 .../django_airavata/apps/auth/package.json         |  4 +--
 .../django_airavata/apps/dataparsers/urls.py       |  1 +
 .../django_airavata/apps/dataparsers/views.py      | 10 ++++++
 .../ComputationalResourceSchedulingEditor.vue      |  6 ++--
 .../js/components/experiment/ExperimentEditor.vue  | 12 +++----
 .../js/containers/ExperimentListContainer.vue      | 16 ++++-----
 .../js/store/modules/view-experiment.js            |  3 +-
 .../django_airavata/apps/workspace/views.py        | 26 +++++++-------
 airavata-django-portal/django_airavata/settings.py |  4 +++
 .../common/js/components/ExperimentStatusBadge.vue | 10 +++++-
 .../django_airavata/templates/base.html            |  2 ++
 19 files changed, 162 insertions(+), 64 deletions(-)

diff --git a/airavata-django-portal/django_airavata/apps/api/output_views.py 
b/airavata-django-portal/django_airavata/apps/api/output_views.py
index f26768800..af605b93e 100644
--- a/airavata-django-portal/django_airavata/apps/api/output_views.py
+++ b/airavata-django-portal/django_airavata/apps/api/output_views.py
@@ -112,7 +112,7 @@ def _get_output_view_provider(output_view_provider_id):
 
 def _get_output_view_providers(experiment_output, application_interface):
     output_view_providers = []
-    logger.debug(f"experiment_output={experiment_output}")
+    logger.debug("Resolving output view providers for output %s", 
experiment_output.name)
     if experiment_output.meta_data:
         try:
             output_metadata = json.loads(experiment_output.meta_data)
@@ -141,7 +141,7 @@ def 
_get_application_output_view_providers(application_interface, output_name):
         o for o in application_interface.application_outputs if o.name == 
output_name
     ]
     if len(app_output) == 1:
-        logger.debug(f"{output_name}: {app_output}")
+        logger.debug("Found application output definition for %s", output_name)
         app_output = app_output[0]
     else:
         return []
diff --git 
a/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/BaseEnum.js
 
b/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/BaseEnum.js
index 4838911ba..ad8bf5690 100644
--- 
a/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/BaseEnum.js
+++ 
b/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/BaseEnum.js
@@ -7,7 +7,10 @@ export default class BaseEnum {
     Object.freeze(this);
   }
   toJSON() {
-    return this.writeName ? this.name : this.value;
+    // Serialize the proto member NAME on the wire, symmetric with the read
+    // contract (the portal renders enums to NAMES via MessageToDict). The SDK
+    // resolves the NAME back to the proto int — the proto enum is the 
type-truth.
+    return this.name;
   }
   // Wire enum values are full proto member names 
("EXPERIMENT_STATE_EXECUTING");
   // byName also accepts the stripped short alias ("EXECUTING") so 
component-level
@@ -22,25 +25,35 @@ export default class BaseEnum {
     return this.values.find((x) => x.value === value);
   }
 
-  // The 0-sentinel is the only member that always carries the full
-  // SCREAMING_SNAKE(className) prefix; non-zero members carry it 
inconsistently
-  // (ExperimentState.EXPERIMENT_STATE_CREATED, but DataType.STRING). Strip the
-  // prefix when present so `ExperimentState.EXECUTING` resolves either way.
+  // Strip the enum's SCREAMING_SNAKE prefix when present so
+  // `ExperimentState.EXECUTING` resolves alongside the full
+  // `ExperimentState.EXPERIMENT_STATE_EXECUTING`. The prefix is derived from 
the
+  // 0-sentinel member name (see init) — NOT from this.name (the class name),
+  // because minified production builds mangle class names, which previously 
left
+  // every short alias unregistered (e.g. `ExperimentState.CREATED === 
undefined`).
   static shortAlias(name) {
-    const prefix = this.name.replace(/([a-z0-9])([A-Z])/g, 
"$1_$2").toUpperCase() + "_";
-    return name.startsWith(prefix) ? name.slice(prefix.length) : name;
+    const prefix = this._prefix || "";
+    return prefix && name.startsWith(prefix) ? name.slice(prefix.length) : 
name;
   }
 
   // names: full proto member names in proto order. Each value is registered
   // under its full name AND aliased to its stripped short name on the class, 
so
-  // both byName(fullName)/this[fullName] and this[shortName] resolve.
+  // both byName(fullName)/this[fullName] and this[shortName] resolve. The 
prefix
+  // is taken from the 0-sentinel (always carries the full prefix, e.g.
+  // "EXPERIMENT_STATE_UNKNOWN") minus its trailing segment — a string literal
+  // that survives minification, unlike the class name.
   static init(names, writeName = false) {
     const enums = names.map((name, index) => new this(name, index, writeName));
     Object.freeze(enums);
     Object.defineProperty(this, "values", { get: () => enums });
+    const sentinel = names[0] || "";
+    const lastUnderscore = sentinel.lastIndexOf("_");
+    const prefix = lastUnderscore >= 0 ? sentinel.slice(0, lastUnderscore + 1) 
: "";
+    Object.defineProperty(this, "_prefix", { value: prefix, configurable: true 
});
     this.values.forEach((v) => {
       this[v.name] = v;
-      this[this.shortAlias(v.name)] = v;
+      const short = this.shortAlias(v.name);
+      if (short !== v.name) this[short] = v;
     });
   }
 }
diff --git 
a/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/FullExperiment.js
 
b/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/FullExperiment.js
index 34fff3c48..cbd4677ea 100644
--- 
a/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/FullExperiment.js
+++ 
b/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/FullExperiment.js
@@ -73,6 +73,13 @@ export default class FullExperiment extends BaseModel {
   }
 
   get experimentStatusName() {
-    return this.experimentStatus ? this.experimentStatus.state.name : null;
+    if (!this.experimentStatus) {
+      return null;
+    }
+    // Wire carries the full proto member name (EXPERIMENT_STATE_CREATED); 
strip
+    // the enum prefix for a human-readable label. Display formatting is the
+    // portal's call — the SDK leaves enums in their proto type.
+    const state = this.experimentStatus.state;
+    return state.constructor.shortAlias(state.name);
   }
 }
diff --git 
a/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/GroupComputeResourcePreference.js
 
b/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/GroupComputeResourcePreference.js
index 0975be131..51a606c7c 100644
--- 
a/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/GroupComputeResourcePreference.js
+++ 
b/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/GroupComputeResourcePreference.js
@@ -48,9 +48,9 @@ export default class GroupComputeResourcePreference extends 
BaseModel {
 
   toJSON() {
     const json = { ...this };
-    if (this.resource_type && this.resource_type.value !== undefined) {
-      json.resource_type = this.resource_type.value;
-    } else if (this.resource_type && this.resource_type.name) {
+    // Emit the proto member NAME (e.g. "SLURM"); the SDK resolves it to the
+    // proto int. Proto enum = type-truth.
+    if (this.resource_type && this.resource_type.name) {
       json.resource_type = this.resource_type.name;
     }
 
diff --git 
a/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/Job.js
 
b/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/Job.js
index f5c5c8870..e6dce6c47 100644
--- 
a/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/Job.js
+++ 
b/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api/js/models/Job.js
@@ -38,7 +38,13 @@ export default class Job extends BaseModel {
   }
 
   get jobStatusStateName() {
-    return this.latestJobStatus ? this.latestJobStatus.job_state.name : null;
+    if (!this.latestJobStatus) {
+      return null;
+    }
+    // Strip the JOB_STATE_ proto prefix for display; the wire carries the full
+    // proto member name.
+    const state = this.latestJobStatus.job_state;
+    return state.constructor.shortAlias(state.name);
   }
 
   get jobStatusTimeOfStateChange() {
diff --git a/airavata-django-portal/django_airavata/apps/api/view_utils.py 
b/airavata-django-portal/django_airavata/apps/api/view_utils.py
index 87a82c8f7..5d25457e6 100644
--- a/airavata-django-portal/django_airavata/apps/api/view_utils.py
+++ b/airavata-django-portal/django_airavata/apps/api/view_utils.py
@@ -362,20 +362,48 @@ class ReadOnly(web.permissions.BasePermission):
         return request.method in web.SAFE_METHODS
 
 
+def _storage_root_relative(path):
+    """Reduce a storage path to its form relative to the storage filesystem 
root so
+    it can be compared uniformly against the configured shared-directory names.
+
+    The server now resolves paths against the storage root, so the same logical
+    location may arrive as a bare-relative ("Proj/Exp"), home ("~/Proj/Exp") or
+    absolute ("/storage/Proj/Exp") path. Previously an absolute path was 
treated as
+    "not shared" outright, which silently bypassed the admin-only write gate on
+    gateway-shared directories. Normalize all three forms here instead.
+    """
+    if not path:
+        return ""
+    if path == "~":
+        return ""
+    if path.startswith("~/"):
+        return path[2:]
+    if os.path.isabs(path):
+        root = getattr(settings, "GATEWAY_DATA_STORAGE_ROOT", None)
+        if root:
+            root = root.rstrip("/") + "/"
+            if path.startswith(root):
+                return path[len(root):]
+        return path.lstrip("/")
+    return path
+
+
 def is_shared_dir(path):
     shared_dirs: dict = getattr(settings, "GATEWAY_DATA_SHARED_DIRECTORIES", 
{})
-    return any(Path(n) == Path(path) for n in shared_dirs)
+    rel = _storage_root_relative(path)
+    return any(Path(_storage_root_relative(n)) == Path(rel) for n in 
shared_dirs)
 
 
 def is_shared_path(path):
     shared_dirs: dict = getattr(settings, "GATEWAY_DATA_SHARED_DIRECTORIES", 
{})
-    # FIXME: path returned when creating a new directory in user storage is an
-    # absolute path. Assume that when an absolute path is given that it was for
-    # a newly created directory and so it is not a shared path
-    if os.path.isabs(path):
+    rel = _storage_root_relative(path)
+    if not rel:
         return False
-    # check if path starts with a shared directory
-    return any(os.path.commonpath((n, path)) == n for n in shared_dirs)
+    # check if path starts with a shared directory (compared root-relative)
+    return any(
+        os.path.commonpath((_storage_root_relative(n), rel)) == 
_storage_root_relative(n)
+        for n in shared_dirs
+    )
 
 
 class BaseSharedDirPermission(web.permissions.BasePermission):
diff --git a/airavata-django-portal/django_airavata/apps/api/views.py 
b/airavata-django-portal/django_airavata/apps/api/views.py
index f0c1d9bc6..7a93444d3 100644
--- a/airavata-django-portal/django_airavata/apps/api/views.py
+++ b/airavata-django-portal/django_airavata/apps/api/views.py
@@ -1227,6 +1227,7 @@ def tus_upload_finish(request):
             }
         )
     except Exception as e:
+        log.error("Failed to finish tus upload", exc_info=True, 
extra={"request": request})
         return exceptions.generic_json_exception_response(e, status=400)
 
 
@@ -1445,10 +1446,6 @@ class SharedEntityViewSet(
 
     lookup_field = "entity_id"
 
-    # Legacy ``ResourcePermissionType`` integers → member NAME (write bodies 
may
-    # still carry the historical int ``permission_type``).
-    _PERMISSION_INT_TO_NAME = {0: "WRITE", 1: "READ", 2: "OWNER", 3: 
"MANAGE_SHARING"}
-
     @staticmethod
     def _sdk():
         from airavata_sdk.helpers import sharing_resources
@@ -1489,12 +1486,16 @@ class SharedEntityViewSet(
     def _normalize_permission(cls, value):
         """Coerce a body ``permission_type`` to the member NAME string.
 
-        Accepts either the legacy ``ResourcePermissionType`` integer or the new
-        member NAME, so the write path is stable across the 
camelCase→snake_case
-        cutover.
+        Accepts either a legacy ``ResourcePermissionType`` integer (resolved 
via
+        the proto enum's own ``Name()``, so it tracks the proto numbering) or 
the
+        new member NAME, so the write path is stable across the cutover.
         """
         if isinstance(value, int):
-            return cls._PERMISSION_INT_TO_NAME[value]
+            from 
airavata_sdk.generated.org.apache.airavata.model.group.group_manager_pb2 import 
(
+                ResourcePermissionType,
+            )
+
+            return ResourcePermissionType.Name(value)
         return value
 
     @classmethod
diff --git a/airavata-django-portal/django_airavata/apps/auth/build-stats.mjs 
b/airavata-django-portal/django_airavata/apps/auth/build-stats.mjs
new file mode 100644
index 000000000..2f116dbac
--- /dev/null
+++ b/airavata-django-portal/django_airavata/apps/auth/build-stats.mjs
@@ -0,0 +1,15 @@
+// The auth app has no page bundles — login/account self-service is hosted by 
Keycloak.
+// But the 'AUTH' django-webpack-loader config still expects a stats file to 
exist, and
+// `vite build` errors on an empty `input: {}` ("You must supply options.input 
to rollup").
+// So emit an empty-but-valid webpack-stats.json directly instead of running 
Vite.
+import { mkdirSync, writeFileSync } from "node:fs";
+
+const publicPath = "/static/django_airavata_auth/dist/";
+const outDir = "static/django_airavata_auth/dist";
+
+mkdirSync(outDir, { recursive: true });
+writeFileSync(
+  `${outDir}/webpack-stats.json`,
+  JSON.stringify({ status: "done", publicPath, chunks: {} }, null, 2) + "\n",
+);
+console.log("auth: emitted empty webpack-stats.json (no page bundles — 
Keycloak-hosted)");
diff --git a/airavata-django-portal/django_airavata/apps/auth/package.json 
b/airavata-django-portal/django_airavata/apps/auth/package.json
index d8b46d99d..ee577ffb0 100644
--- a/airavata-django-portal/django_airavata/apps/auth/package.json
+++ b/airavata-django-portal/django_airavata/apps/auth/package.json
@@ -5,8 +5,8 @@
   "author": "Apache Airavata <[email protected]>",
   "private": true,
   "scripts": {
-    "build": "vite build",
-    "watch": "vite build --watch",
+    "build": "node build-stats.mjs",
+    "watch": "node build-stats.mjs",
     "lint": "eslint ./static/django_airavata_auth/js",
     "format": "prettier --write ."
   },
diff --git a/airavata-django-portal/django_airavata/apps/dataparsers/urls.py 
b/airavata-django-portal/django_airavata/apps/dataparsers/urls.py
index 0c0d8dc3f..ff6a8f25a 100644
--- a/airavata-django-portal/django_airavata/apps/dataparsers/urls.py
+++ b/airavata-django-portal/django_airavata/apps/dataparsers/urls.py
@@ -5,6 +5,7 @@ from . import views
 app_name = "django_airavata_dataparsers"
 urlpatterns = [
     re_path(r"^$", views.home, name="home"),
+    re_path(r"^create/$", views.create_parser, name="create_parser"),
     re_path(
         r"^parsers/(?P<parser_id>[^/]+)/$", views.parser_details, 
name="parser_details"
     ),
diff --git a/airavata-django-portal/django_airavata/apps/dataparsers/views.py 
b/airavata-django-portal/django_airavata/apps/dataparsers/views.py
index 9065555f5..5908e7f75 100644
--- a/airavata-django-portal/django_airavata/apps/dataparsers/views.py
+++ b/airavata-django-portal/django_airavata/apps/dataparsers/views.py
@@ -25,3 +25,13 @@ def edit_parser(request, parser_id):
         "django_airavata_dataparsers/edit-parser.html",
         {"parser_id": parser_id, "bundle_name": "parser-edit"},
     )
+
+
+def create_parser(request):
+    # Same editor bundle as edit, but with no parser_id — the Vue 
ParserEditContainer
+    # treats an empty data-parser-id as "new parser" (it only fetches when an 
id is set).
+    return render(
+        request,
+        "django_airavata_dataparsers/edit-parser.html",
+        {"parser_id": "", "bundle_name": "parser-edit"},
+    )
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ComputationalResourceSchedulingEditor.vue
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ComputationalResourceSchedulingEditor.vue
index 6fafb8aa6..f4d545cd9 100644
--- 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ComputationalResourceSchedulingEditor.vue
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ComputationalResourceSchedulingEditor.vue
@@ -5,8 +5,8 @@
         <b-form-group
           label="Compute Resource"
           label-for="compute-resource"
-          :feedback="getValidationFeedback("resource_host_id")"
-          :state="getValidationState("resource_host_id")"
+          :feedback="getValidationFeedback('resource_host_id')"
+          :state="getValidationState('resource_host_id')"
         >
           <b-form-select
             id="compute-resource"
@@ -14,7 +14,7 @@
             :options="computeResourceOptions"
             required
             @change="computeResourceChanged"
-            :state="getValidationState("resource_host_id")"
+            :state="getValidationState('resource_host_id')"
             :disabled="
               !computeResourceOptions || computeResourceOptions.length === 0
             "
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
index a32008fbb..db2d40c2c 100644
--- 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
@@ -31,8 +31,8 @@
           <b-form-group
             label="Experiment Name"
             label-for="experiment-name"
-            :feedback="getValidationFeedback("experiment_name")"
-            :state="getValidationState("experiment_name")"
+            :feedback="getValidationFeedback('experiment_name')"
+            :state="getValidationState('experiment_name')"
           >
             <b-form-input
               id="experiment-name"
@@ -40,7 +40,7 @@
               v-model="localExperiment.experiment_name"
               required
               placeholder="Experiment name"
-              :state="getValidationState("experiment_name")"
+              :state="getValidationState('experiment_name')"
             ></b-form-input>
           </b-form-group>
           <experiment-description-editor
@@ -53,14 +53,14 @@
           <b-form-group
             label="Project"
             label-for="project"
-            :feedback="getValidationFeedback("project_id")"
-            :state="getValidationState("project_id")"
+            :feedback="getValidationFeedback('project_id')"
+            :state="getValidationState('project_id')"
           >
             <b-form-select
               id="project"
               v-model="localExperiment.project_id"
               required
-              :state="getValidationState("project_id")"
+              :state="getValidationState('project_id')"
             >
               <template slot="first">
                 <option :value="null" disabled>Select a Project</option>
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/ExperimentListContainer.vue
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/ExperimentListContainer.vue
index 6d8481cdb..2bd0304b9 100644
--- 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/ExperimentListContainer.vue
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/ExperimentListContainer.vue
@@ -61,14 +61,14 @@
                   </option>
                 </template>
                 <option value="ALL">ALL</option>
-                <option value="CREATED">Created</option>
-                <option value="VALIDATED">Validated</option>
-                <option value="SCHEDULED">Scheduled</option>
-                <option value="LAUNCHED">Launched</option>
-                <option value="EXECUTING">Executing</option>
-                <option value="CANCELED">Canceled</option>
-                <option value="COMPLETED">Completed</option>
-                <option value="FAILED">Failed</option>
+                <option value="EXPERIMENT_STATE_CREATED">Created</option>
+                <option value="EXPERIMENT_STATE_VALIDATED">Validated</option>
+                <option value="EXPERIMENT_STATE_SCHEDULED">Scheduled</option>
+                <option value="EXPERIMENT_STATE_LAUNCHED">Launched</option>
+                <option value="EXPERIMENT_STATE_EXECUTING">Executing</option>
+                <option value="EXPERIMENT_STATE_CANCELED">Canceled</option>
+                <option value="EXPERIMENT_STATE_COMPLETED">Completed</option>
+                <option value="EXPERIMENT_STATE_FAILED">Failed</option>
               </b-form-select>
               <b-input-group-append>
                 <b-button @click="resetSearch">Reset</b-button>
diff --git 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/store/modules/view-experiment.js
 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/store/modules/view-experiment.js
index 080e4365e..c83bd908d 100644
--- 
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/store/modules/view-experiment.js
+++ 
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/store/modules/view-experiment.js
@@ -120,7 +120,8 @@ export const actions = {
       });
       dispatch("setLaunching", { launching: true });
     } catch (error) {
-      // TODO: handle launch error
+      // Surface launch failures to the user instead of silently swallowing 
them.
+      errors.UnhandledErrorDispatcher.reportUnhandledError(error);
     }
   },
   async cancel({ dispatch, getters }) {
diff --git a/airavata-django-portal/django_airavata/apps/workspace/views.py 
b/airavata-django-portal/django_airavata/apps/workspace/views.py
index 144831378..35a787b02 100644
--- a/airavata-django-portal/django_airavata/apps/workspace/views.py
+++ b/airavata-django-portal/django_airavata/apps/workspace/views.py
@@ -15,6 +15,9 @@ from django_airavata.apps.api.views import (
     ProjectViewSet,
 )
 from django_airavata.apps.auth.decorators import login_required
+from 
airavata_sdk.generated.org.apache.airavata.model.application.io.application_io_pb2
 import (
+    DataType,
+)
 
 logger = logging.getLogger(__name__)
 
@@ -96,15 +99,14 @@ def create_experiment(request, app_module_id):
             )
         )
     user_input_values = {}
-    # The serialized application-input `type` is the DataType member NAME; the
-    # historical Thrift DataType integers are kept here so the comparisons 
below
-    # behave exactly as before (a string never equals an int, as was already 
the
-    # case with the Thrift IntEnum members).
-    DataType_URI = 3
-    DataType_STRING = 0
-    for app_input in app_interface.data["applicationInputs"]:
-        if app_input["type"] == DataType_URI and app_input["name"] in 
request.GET:
-            user_file_value = request.GET[app_input["name"]]
+    # `application_interface` returns a proto-direct WithAccess envelope, so 
read
+    # the ApplicationInterfaceDescription proto and its inputs directly (rather
+    # than subscripting a serialized dict). app_input.type stays the proto 
DataType
+    # enum; compare against its named members.
+    application_interface = app_interface.data.message
+    for app_input in application_interface.application_inputs:
+        if app_input.type == DataType.URI and app_input.name in request.GET:
+            user_file_value = request.GET[app_input.name]
             try:
                 user_file_url = urlparse(user_file_value)
                 if user_file_url.scheme == "airavata-dp":
@@ -117,7 +119,7 @@ def create_experiment(request, app_module_id):
                         if file_path and request.airavata.storage.file_exists(
                             file_path
                         ):
-                            user_input_values[app_input["name"]] = dp_uri
+                            user_input_values[app_input.name] = dp_uri
                     except Exception:
                         logger.exception(
                             f"Failed checking data product uri: {dp_uri}",
@@ -128,8 +130,8 @@ def create_experiment(request, app_module_id):
                     f"Invalid user file value: {user_file_value}",
                     extra={"request": request},
                 )
-        elif app_input["type"] == DataType_STRING and app_input["name"] in 
request.GET:
-            name = app_input["name"]
+        elif app_input.type == DataType.STRING and app_input.name in 
request.GET:
+            name = app_input.name
             user_input_values[name] = request.GET[name]
     context = {
         "bundle_name": "create-experiment",
diff --git a/airavata-django-portal/django_airavata/settings.py 
b/airavata-django-portal/django_airavata/settings.py
index a09176db7..9d9b5f990 100644
--- a/airavata-django-portal/django_airavata/settings.py
+++ b/airavata-django-portal/django_airavata/settings.py
@@ -106,6 +106,7 @@ TEMPLATES = [
             "context_processors": [
                 "django.template.context_processors.debug",
                 "django.template.context_processors.request",
+                "django.template.context_processors.csrf",
                 "django_airavata.context_processors.user",
                 "django.contrib.messages.context_processors.messages",
                 "django_airavata.context_processors.airavata_app_registry",
@@ -437,6 +438,9 @@ LOGGING = {
         "django_airavata": {
             "handlers": ["console", "console_debug", "mail_admins"],
             "level": "DEBUG",
+            # Don't also bubble up to the root logger, whose handlers 
(console/console_debug)
+            # would otherwise re-emit every django_airavata record a second 
time.
+            "propagate": False,
         },
         "root": {"handlers": ["console", "console_debug"], "level": "WARNING"},
     },
diff --git 
a/airavata-django-portal/django_airavata/static/common/js/components/ExperimentStatusBadge.vue
 
b/airavata-django-portal/django_airavata/static/common/js/components/ExperimentStatusBadge.vue
index 70d180290..87ae8a89e 100644
--- 
a/airavata-django-portal/django_airavata/static/common/js/components/ExperimentStatusBadge.vue
+++ 
b/airavata-django-portal/django_airavata/static/common/js/components/ExperimentStatusBadge.vue
@@ -1,5 +1,5 @@
 <template>
-  <b-badge :variant="badgeVariant">{{ statusName }}</b-badge>
+  <b-badge :variant="badgeVariant">{{ label }}</b-badge>
 </template>
 
 <script>
@@ -17,6 +17,14 @@ export default {
     experimentState: function () {
       return models.ExperimentState.byName(this.statusName);
     },
+    // statusName arrives as the full proto member name 
(EXPERIMENT_STATE_CREATED);
+    // render the prefix-stripped short alias. Falls back to the raw value if 
the
+    // name doesn't resolve to a known state.
+    label: function () {
+      return this.experimentState
+        ? 
this.experimentState.constructor.shortAlias(this.experimentState.name)
+        : this.statusName;
+    },
     badgeVariant: function () {
       if (this.experimentState.isProgressing) {
         return "secondary";
diff --git a/airavata-django-portal/django_airavata/templates/base.html 
b/airavata-django-portal/django_airavata/templates/base.html
index 6b1e6e7ae..a60f10a42 100644
--- a/airavata-django-portal/django_airavata/templates/base.html
+++ b/airavata-django-portal/django_airavata/templates/base.html
@@ -145,6 +145,8 @@
 </style>
 
 <body>
+  {# Sets the csrftoken cookie on every page so the SPA's FetchUtils can send 
X-CSRFToken on POST/PUT/DELETE. #}
+  {% csrf_token %}
   <header class=c-header>
     {% block header %}
     {% portal_logo %}

Reply via email to