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 %}