airavata-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From machris...@apache.org
Subject [airavata-django-portal] branch master updated: AIRAVATA-2876 Display validation errors when saving app module
Date Mon, 24 Sep 2018 15:34:16 GMT
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git


The following commit(s) were added to refs/heads/master by this push:
     new f8d5c01  AIRAVATA-2876 Display validation errors when saving app module
f8d5c01 is described below

commit f8d5c01c1f2e6ac00dbc6b133cac48bd01aab11d
Author: Marcus Christie <machrist@iu.edu>
AuthorDate: Mon Sep 24 11:34:02 2018 -0400

    AIRAVATA-2876 Display validation errors when saving app module
---
 .../applications/ApplicationEditorContainer.vue    |  49 +++--
 .../applications/ApplicationModuleEditor.vue       |  18 +-
 django_airavata/apps/api/exceptions.py             |   5 -
 django_airavata/apps/api/serializers.py            |   4 +-
 .../django_airavata_api/js/errors/ErrorUtils.js    |  10 +
 .../api/static/django_airavata_api/js/index.js     | 114 +++++------
 .../js/models/ApplicationInterfaceDefinition.js    |  47 +++--
 .../django_airavata_api/js/utils/FetchUtils.js     | 217 ++++++++++++++-------
 .../static/common/js/errors/ValidationErrors.js    |  27 +++
 django_airavata/static/common/js/index.js          |   4 +-
 10 files changed, 313 insertions(+), 182 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue
index 583ea8d..d8c1426 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue
@@ -19,7 +19,7 @@
           <b-nav-item active-class="active" :to="{name: 'application_deployments', params:
{id: id}}" :disabled="!id">Deployments</b-nav-item>
         </b-nav>
         <router-view name="module" v-if="appModule" v-model="appModule" @input="appModuleIsDirty
= true" :readonly="!appModule.userHasWriteAccess"
-        />
+          :validation-errors="appModuleValidationErrors" />
         <router-view name="interface" v-if="appInterface" v-model="appInterface" @input="appInterfaceIsDirty
= true" :readonly="!appInterface.userHasWriteAccess"
         />
         <router-view name="deployments" v-if="appDeployments" :deployments="appDeployments"
@new="createNewDeployment" @delete="deleteApplicationDeployment"
@@ -72,7 +72,8 @@ export default {
       appModuleIsDirty: false,
       appInterfaceIsDirty: false,
       dirtyAppDeploymentComputeHostIds: [],
-      dirtyAppDeploymentSharedEntityComputeHostIds: []
+      dirtyAppDeploymentSharedEntityComputeHostIds: [],
+      appModuleValidationErrors: null
     };
   },
   computed: {
@@ -142,28 +143,40 @@ export default {
       });
     },
     createApplicationModule(appModule) {
-      return services.ApplicationModuleService.create({ data: appModule }).then(
-        appModule => {
-          this.appModuleIsDirty = false;
-          this.appModule = appModule;
-          return appModule;
-        }
+      return services.ApplicationModuleService.create(
+        { data: appModule },
+        { ignoreErrors: true }
       );
     },
     updateApplicationModule(appModule) {
-      return services.ApplicationModuleService.update({
-        lookup: appModule.appModuleId,
-        data: appModule
-      }).then(appModule => {
-        this.appModuleIsDirty = false;
-        this.appModule = appModule;
-        return appModule;
-      });
+      return services.ApplicationModuleService.update(
+        {
+          lookup: appModule.appModuleId,
+          data: appModule
+        },
+        { ignoreErrors: true }
+      );
     },
     saveApplicationModule(appModule) {
-      return this.id
+      return (this.id
         ? this.updateApplicationModule(appModule)
-        : this.createApplicationModule(appModule);
+        : this.createApplicationModule(appModule)
+      )
+        .then(appModule => {
+          this.appModuleValidationErrors = null;
+          this.appModuleIsDirty = false;
+          this.appModule = appModule;
+          return appModule;
+        })
+        .catch(error => {
+          if (errors.ErrorUtils.isValidationError(error)) {
+            this.appModuleValidationErrors = error.details.response;
+          } else {
+            this.appModuleValidationErrors = null;
+            notifications.NotificationList.addError(error);
+          }
+          return Promise.reject(error);
+        });
     },
     deleteApplicationModule(appModule) {
       const deleteModule = this.id
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationModuleEditor.vue
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationModuleEditor.vue
index 8964c71..2ed5628 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationModuleEditor.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationModuleEditor.vue
@@ -5,8 +5,9 @@
         <h1 class="h4 mb-4">
           Application Details
         </h1>
-        <b-form-group label="Application Name" label-for="application-name">
-          <b-form-input id="application-name" type="text" v-model="data.appModuleName"
required :disabled="readonly"></b-form-input>
+        <b-form-group label="Application Name" label-for="application-name" :invalid-feedback="validationFeedback.appModuleName.invalidFeedback"
+          :state="validationFeedback.appModuleName.state">
+          <b-form-input id="application-name" type="text" v-model="data.appModuleName"
required :disabled="readonly" :state="validationFeedback.appModuleName.state"></b-form-input>
         </b-form-group>
         <b-form-group label="Application Version" label-for="application-version">
           <b-form-input id="application-version" type="text" v-model="data.appModuleVersion"
:disabled="readonly"></b-form-input>
@@ -21,7 +22,7 @@
 
 <script>
 import { models } from "django-airavata-api";
-import { components } from "django-airavata-common-ui";
+import { components, errors } from "django-airavata-common-ui";
 import vmodel_mixin from "../commons/vmodel_mixin";
 
 export default {
@@ -34,11 +35,22 @@ export default {
     readonly: {
       type: Boolean,
       default: false
+    },
+    validationErrors: {
+      type: Object
     }
   },
   components: {
     "delete-button": components.DeleteButton
   },
+  computed: {
+    validationFeedback() {
+      return errors.ValidationErrors.createValidationFeedback(
+        this.data,
+        this.validationErrors
+      );
+    }
+  },
   methods: {
     save() {
       this.$emit("save");
diff --git a/django_airavata/apps/api/exceptions.py b/django_airavata/apps/api/exceptions.py
index e643f1c..4c217e7 100644
--- a/django_airavata/apps/api/exceptions.py
+++ b/django_airavata/apps/api/exceptions.py
@@ -32,9 +32,4 @@ def custom_exception_handler(exc, context):
             status=status.HTTP_500_INTERNAL_SERVER_ERROR
         )
 
-    if isinstance(exc, serializers.ValidationError):
-        # Create a default error message for the validation error
-        response.data['detail'] = "ValidationError: {}".format(
-            json.dumps(response.data))
-
     return response
diff --git a/django_airavata/apps/api/serializers.py b/django_airavata/apps/api/serializers.py
index ea014c3..c052a40 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -262,7 +262,9 @@ class ApplicationModuleSerializer(
 
 class InputDataObjectTypeSerializer(
         thrift_utils.create_serializer_class(InputDataObjectType)):
-    pass
+
+    class Meta:
+        required = ('name',)
 
 
 class ApplicationInterfaceDescriptionSerializer(
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/errors/ErrorUtils.js b/django_airavata/apps/api/static/django_airavata_api/js/errors/ErrorUtils.js
new file mode 100644
index 0000000..c7d8f4a
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/errors/ErrorUtils.js
@@ -0,0 +1,10 @@
+export default {
+  isValidationError(error) {
+    return (
+      error.details &&
+      error.details.status === 400 &&
+      error.details.response &&
+      !("detail" in error.details.response)
+    );
+  }
+};
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/index.js b/django_airavata/apps/api/static/django_airavata_api/js/index.js
index 700b4bd..7a04bee 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/index.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/index.js
@@ -1,58 +1,60 @@
-import UnhandledError from './errors/UnhandledError'
-import UnhandledErrorDispatcher from './errors/UnhandledErrorDispatcher'
-import UnhandledErrorDisplayList from './errors/UnhandledErrorDisplayList'
+import ErrorUtils from "./errors/ErrorUtils";
+import UnhandledError from "./errors/UnhandledError";
+import UnhandledErrorDispatcher from "./errors/UnhandledErrorDispatcher";
+import UnhandledErrorDisplayList from "./errors/UnhandledErrorDisplayList";
 
-import ApplicationDeploymentDescription from './models/ApplicationDeploymentDescription'
-import ApplicationInterfaceDefinition from './models/ApplicationInterfaceDefinition'
-import ApplicationModule from './models/ApplicationModule'
-import BaseModel from './models/BaseModel'
-import BatchQueue from './models/BatchQueue'
-import BatchQueueResourcePolicy from './models/BatchQueueResourcePolicy'
-import CommandObject from './models/CommandObject'
-import ComputeResourcePolicy from './models/ComputeResourcePolicy'
-import DataType from './models/DataType'
-import Experiment from './models/Experiment'
-import ExperimentState from './models/ExperimentState'
-import FullExperiment from './models/FullExperiment'
-import Group from './models/Group'
-import GroupComputeResourcePreference from './models/GroupComputeResourcePreference'
-import GroupPermission from './models/GroupPermission'
-import GroupResourceProfile from './models/GroupResourceProfile'
-import InputDataObjectType from './models/InputDataObjectType'
-import OutputDataObjectType from './models/OutputDataObjectType'
-import ParallelismType from './models/ParallelismType'
-import Project from './models/Project'
-import ResourcePermissionType from './models/ResourcePermissionType'
-import SetEnvPaths from './models/SetEnvPaths'
-import SharedEntity from './models/SharedEntity'
-import SummaryType from './models/SummaryType'
-import UserPermission from './models/UserPermission'
+import ApplicationDeploymentDescription from "./models/ApplicationDeploymentDescription";
+import ApplicationInterfaceDefinition from "./models/ApplicationInterfaceDefinition";
+import ApplicationModule from "./models/ApplicationModule";
+import BaseModel from "./models/BaseModel";
+import BatchQueue from "./models/BatchQueue";
+import BatchQueueResourcePolicy from "./models/BatchQueueResourcePolicy";
+import CommandObject from "./models/CommandObject";
+import ComputeResourcePolicy from "./models/ComputeResourcePolicy";
+import DataType from "./models/DataType";
+import Experiment from "./models/Experiment";
+import ExperimentState from "./models/ExperimentState";
+import FullExperiment from "./models/FullExperiment";
+import Group from "./models/Group";
+import GroupComputeResourcePreference from "./models/GroupComputeResourcePreference";
+import GroupPermission from "./models/GroupPermission";
+import GroupResourceProfile from "./models/GroupResourceProfile";
+import InputDataObjectType from "./models/InputDataObjectType";
+import OutputDataObjectType from "./models/OutputDataObjectType";
+import ParallelismType from "./models/ParallelismType";
+import Project from "./models/Project";
+import ResourcePermissionType from "./models/ResourcePermissionType";
+import SetEnvPaths from "./models/SetEnvPaths";
+import SharedEntity from "./models/SharedEntity";
+import SummaryType from "./models/SummaryType";
+import UserPermission from "./models/UserPermission";
 
-import ExperimentService from './services/ExperimentService'
-import ExperimentSearchService from './services/ExperimentSearchService'
-import FullExperimentService from './services/FullExperimentService'
-import ProjectService from './services/ProjectService'
-import GroupService from './services/GroupService'
-import UserProfileService from './services/UserProfileService'
-import CloudJobSubmissionService from './services/CloudJobSubmissionService'
-import GlobusJobSubmissionService from './services/GlobusJobSubmissionService'
-import LocaJobSubmissionService from './services/LocaJobSubmissionService'
-import SshJobSubmissionService from './services/SshJobSubmissionService'
-import UnicoreJobSubmissionService from './services/UnicoreJobSubmissionService'
-import SCPDataMovementService from './services/SCPDataMovementService'
-import GridFTPDataMovementService from './services/GridFTPDataMovementService'
-import UnicoreDataMovementService from './services/UnicoreDataMovementService'
-import ServiceFactory from './services/ServiceFactory'
+import ExperimentService from "./services/ExperimentService";
+import ExperimentSearchService from "./services/ExperimentSearchService";
+import FullExperimentService from "./services/FullExperimentService";
+import ProjectService from "./services/ProjectService";
+import GroupService from "./services/GroupService";
+import UserProfileService from "./services/UserProfileService";
+import CloudJobSubmissionService from "./services/CloudJobSubmissionService";
+import GlobusJobSubmissionService from "./services/GlobusJobSubmissionService";
+import LocaJobSubmissionService from "./services/LocaJobSubmissionService";
+import SshJobSubmissionService from "./services/SshJobSubmissionService";
+import UnicoreJobSubmissionService from "./services/UnicoreJobSubmissionService";
+import SCPDataMovementService from "./services/SCPDataMovementService";
+import GridFTPDataMovementService from "./services/GridFTPDataMovementService";
+import UnicoreDataMovementService from "./services/UnicoreDataMovementService";
+import ServiceFactory from "./services/ServiceFactory";
 
-import FetchUtils from './utils/FetchUtils'
-import PaginationIterator from './utils/PaginationIterator'
-import StringUtils from './utils/StringUtils'
+import FetchUtils from "./utils/FetchUtils";
+import PaginationIterator from "./utils/PaginationIterator";
+import StringUtils from "./utils/StringUtils";
 
 exports.errors = {
+  ErrorUtils,
   UnhandledError,
   UnhandledErrorDispatcher,
-  UnhandledErrorDisplayList,
-}
+  UnhandledErrorDisplayList
+};
 
 exports.models = {
   ApplicationDeploymentDescription,
@@ -79,11 +81,13 @@ exports.models = {
   SetEnvPaths,
   SharedEntity,
   SummaryType,
-  UserPermission,
-}
+  UserPermission
+};
 
 exports.services = {
-  ApplicationDeploymentService: ServiceFactory.service("ApplicationDeployments"),
+  ApplicationDeploymentService: ServiceFactory.service(
+    "ApplicationDeployments"
+  ),
   ApplicationInterfaceService: ServiceFactory.service("ApplicationInterfaces"),
   ApplicationModuleService: ServiceFactory.service("ApplicationModules"),
   CloudJobSubmissionService,
@@ -104,11 +108,11 @@ exports.services = {
   SshJobSubmissionService,
   UnicoreDataMovementService,
   UnicoreJobSubmissionService,
-  UserProfileService,
-}
+  UserProfileService
+};
 
 exports.utils = {
   FetchUtils,
   PaginationIterator,
-  StringUtils,
-}
+  StringUtils
+};
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js
b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js
index fe46c61..9ca07f0 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js
@@ -1,46 +1,45 @@
-import BaseModel from './BaseModel'
-import InputDataObjectType from './InputDataObjectType'
-import OutputDataObjectType from './OutputDataObjectType'
-import DataType from './DataType';
-
+import BaseModel from "./BaseModel";
+import InputDataObjectType from "./InputDataObjectType";
+import OutputDataObjectType from "./OutputDataObjectType";
+import DataType from "./DataType";
 
 const FIELDS = [
-  'applicationInterfaceId',
-  'applicationName',
-  'applicationDescription',
+  "applicationInterfaceId",
+  "applicationName",
+  "applicationDescription",
   {
-    name: 'applicationModules',
-    type: 'string',
-    list: true,
+    name: "applicationModules",
+    type: "string",
+    list: true
   },
   // When saving/updating, the order of the inputs in the applicationInputs
   // array determines the 'inputOrder' that will be applied to each input on the
   // backend. Updating 'inputOrder' will have no effect.
   {
-    name: 'applicationInputs',
+    name: "applicationInputs",
     type: InputDataObjectType,
     list: true,
+    default: BaseModel.defaultNewInstance(Array)
   },
   {
-    name: 'applicationOutputs',
+    name: "applicationOutputs",
     type: OutputDataObjectType,
-    list: true,
+    list: true
   },
   {
-    name: 'archiveWorkingDirectory',
-    type: 'boolean',
-    default: false,
+    name: "archiveWorkingDirectory",
+    type: "boolean",
+    default: false
   },
   {
-    name: 'hasOptionalFileInputs',
-    type: 'boolean',
-    default: false,
+    name: "hasOptionalFileInputs",
+    type: "boolean",
+    default: false
   },
-  'userHasWriteAccess',
+  "userHasWriteAccess"
 ];
 
 export default class ApplicationInterfaceDefinition extends BaseModel {
-
   constructor(data = {}) {
     super(FIELDS, data);
   }
@@ -49,12 +48,12 @@ export default class ApplicationInterfaceDefinition extends BaseModel
{
     const stdout = new OutputDataObjectType({
       name: "Standard-Out",
       type: DataType.STDOUT,
-      isRequired: true,
+      isRequired: true
     });
     const stderr = new OutputDataObjectType({
       name: "Standard-Error",
       type: DataType.STDERR,
-      isRequired: true,
+      isRequired: true
     });
     if (!this.applicationOutputs) {
       this.applicationOutputs = [];
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js b/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js
index c6a796c..391682d 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js
@@ -1,165 +1,232 @@
 import UnhandledErrorDispatcher from "../errors/UnhandledErrorDispatcher";
 
 var count = 0;
-const parseQueryParams = function (url, queryParams = "") {
-  if (queryParams && typeof (queryParams) != "string") {
-    queryParams = Object.keys(queryParams).map(key => encodeURIComponent(key) + "=" +
encodeURIComponent(queryParams[key])).join("&");
+const parseQueryParams = function(url, queryParams = "") {
+  if (queryParams && typeof queryParams != "string") {
+    queryParams = Object.keys(queryParams)
+      .map(
+        key =>
+          encodeURIComponent(key) + "=" + encodeURIComponent(queryParams[key])
+      )
+      .join("&");
   }
   if (queryParams && queryParams !== "") {
     return url + "?" + queryParams;
   } else {
     return url;
   }
-}
+};
 
-const setSpinnerDisplay = function (display) {
+const setSpinnerDisplay = function(display) {
   let spinner = document.getElementById("airavata-spinner");
   spinner.style.display = display;
-}
+};
 
-const incrementCount = function () {
+const incrementCount = function() {
   count++;
   if (count == 1) {
     setSpinnerDisplay("block");
   }
-}
-const decrementCount = function () {
+};
+const decrementCount = function() {
   if (count > 0) {
     count--;
     if (count == 0) {
       setSpinnerDisplay("none");
     }
   }
-}
+};
 
 export default {
-  enableSpinner: function () {
-
-  },
-  disableSpinner: function () {
-
-  },
-  getCSRFToken: function () {
-    var csrfToken = document.cookie.split(';').map(val => val.trim()).filter(val =>
val.startsWith("csrftoken" + '=')).map(val => val.split("=")[1]);
+  enableSpinner: function() {},
+  disableSpinner: function() {},
+  getCSRFToken: function() {
+    var csrfToken = document.cookie
+      .split(";")
+      .map(val => val.trim())
+      .filter(val => val.startsWith("csrftoken" + "="))
+      .map(val => val.split("=")[1]);
     if (csrfToken) {
       return csrfToken[0];
     } else {
       return null;
     }
   },
-  createHeaders: function (contentType = "application/json", accept = "application/json")
{
+  createHeaders: function(
+    contentType = "application/json",
+    accept = "application/json"
+  ) {
     var csrfToken = this.getCSRFToken();
     var headers = new Headers({
       "Content-Type": contentType,
-      "Accept": accept,
+      Accept: accept
     });
     if (csrfToken != null) {
-      headers.set("X-CSRFToken", csrfToken)
+      headers.set("X-CSRFToken", csrfToken);
     }
     return headers;
   },
-  post: function (url, body, queryParams = "", { mediaType = "application/json", ignoreErrors
= false } = {}) {
-    var headers = this.createHeaders(mediaType)
+  post: function(
+    url,
+    body,
+    queryParams = "",
+    { mediaType = "application/json", ignoreErrors = false } = {}
+  ) {
+    var headers = this.createHeaders(mediaType);
     // Browsers automatically handle content type for FormData request bodies
     if (body instanceof FormData) {
       headers.delete("Content-Type");
     }
     url = parseQueryParams(url, queryParams);
     return this.processFetch(url, {
-      method: 'post',
-      body: (body instanceof FormData || typeof body === 'string') ? body : JSON.stringify(body),
+      method: "post",
+      body:
+        body instanceof FormData || typeof body === "string"
+          ? body
+          : JSON.stringify(body),
       headers: headers,
       credentials: "same-origin",
       ignoreErrors
     });
   },
-  put: function (url, body, { mediaType = "application/json", ignoreErrors = false } = {})
{
+  put: function(
+    url,
+    body,
+    { mediaType = "application/json", ignoreErrors = false } = {}
+  ) {
     var headers = this.createHeaders(mediaType);
     return this.processFetch(url, {
-      method: 'put',
-      body: (body instanceof FormData || typeof body === 'string') ? body : JSON.stringify(body),
+      method: "put",
+      body:
+        body instanceof FormData || typeof body === "string"
+          ? body
+          : JSON.stringify(body),
       headers: headers,
       credentials: "same-origin",
       ignoreErrors
     });
   },
-  get: function (url, queryParams = "", { mediaType = "application/json", ignoreErrors =
false } = {}) {
-    if (queryParams && typeof (queryParams) != "string") {
-      queryParams = Object.keys(queryParams).map(key => encodeURIComponent(key) + "="
+ encodeURIComponent(queryParams[key])).join("&")
+  get: function(
+    url,
+    queryParams = "",
+    { mediaType = "application/json", ignoreErrors = false } = {}
+  ) {
+    if (queryParams && typeof queryParams != "string") {
+      queryParams = Object.keys(queryParams)
+        .map(
+          key =>
+            encodeURIComponent(key) + "=" + encodeURIComponent(queryParams[key])
+        )
+        .join("&");
     }
     if (queryParams) {
-      url = url + "?" + queryParams
+      url = url + "?" + queryParams;
     }
     var headers = this.createHeaders(mediaType);
     return this.processFetch(url, {
-      method: 'get',
+      method: "get",
       headers: headers,
       credentials: "same-origin",
       ignoreErrors
     });
   },
-  delete: function (url, { ignoreErrors = false } = {}) {
+  delete: function(url, { ignoreErrors = false } = {}) {
     var headers = this.createHeaders();
     return this.processFetch(url, {
-      method: 'delete',
+      method: "delete",
       headers: headers,
       credentials: "same-origin",
       ignoreErrors
     });
   },
-  processFetch: function (url, { method = 'get', headers, credentials = 'same-origin', body,
ignoreErrors = false }) {
-
+  processFetch: function(
+    url,
+    {
+      method = "get",
+      headers,
+      credentials = "same-origin",
+      body,
+      ignoreErrors = false
+    }
+  ) {
     const fetchConfig = {
       method,
       headers,
-      credentials,
+      credentials
     };
     if (body) {
       fetchConfig.body = body;
     }
     incrementCount();
-    return fetch(url, fetchConfig).then((response) => {
-      decrementCount();
-      if (response.ok) {
-        // No response body
-        if (response.status === 204) {
-          return Promise.resolve();
-        } else {
-          return Promise.resolve(response.json())
-        }
-      } else {
-        return response.json().then(json => {
-          const error = new Error(json.detail ? json.detail : response.statusText);
-          error.details = this.createErrorDetails({ url, body, status: response.status, responseBody:
json })
+    return fetch(url, fetchConfig)
+      .then(
+        response => {
+          decrementCount();
+          if (response.ok) {
+            // No response body
+            if (response.status === 204) {
+              return Promise.resolve();
+            } else {
+              return Promise.resolve(response.json());
+            }
+          } else {
+            return response.json().then(
+              json => {
+                // if json doesn't have detail key, stringify body
+                let errorMessage = json.detail;
+                if (!("detail" in json)) {
+                  errorMessage = "Error: " + JSON.stringify(json);
+                }
+                const error = new Error(errorMessage);
+                error.details = this.createErrorDetails({
+                  url,
+                  body,
+                  status: response.status,
+                  responseBody: json
+                });
+                throw error;
+              },
+              e => {
+                // In case JSON parsing fails
+                const error = new Error(response.statusText);
+                error.details = this.createErrorDetails({
+                  url,
+                  body,
+                  status: response.status
+                });
+                throw error;
+              }
+            );
+          }
+        },
+        error => {
+          decrementCount();
+          error.details = this.createErrorDetails({ url, body });
           throw error;
-        }, e => { // In case JSON parsing fails
-          const error = new Error(response.statusText);
-          error.details = this.createErrorDetails({ url, body, status: response.status })
-          throw error;
-        });
-      }
-    }, (error) => {
-      decrementCount();
-      error.details = this.createErrorDetails({ url, body });
-      throw error;
-    }).catch(error => {
-
-      if (!ignoreErrors) {
-        UnhandledErrorDispatcher.reportError({
-          message: error.message,
-          error: error,
-          details: error.details,
-        })
-      }
-      throw error;
-    })
+        }
+      )
+      .catch(error => {
+        if (!ignoreErrors) {
+          UnhandledErrorDispatcher.reportError({
+            message: error.message,
+            error: error,
+            details: error.details
+          });
+        }
+        throw error;
+      });
   },
-  createErrorDetails: function ({ url, body, status = null, responseBody = null } = {}) {
+  createErrorDetails: function({
+    url,
+    body,
+    status = null,
+    responseBody = null
+  } = {}) {
     return {
       url,
       body,
       status,
       response: responseBody
-    }
+    };
   }
-}
+};
diff --git a/django_airavata/static/common/js/errors/ValidationErrors.js b/django_airavata/static/common/js/errors/ValidationErrors.js
new file mode 100644
index 0000000..bf2e7b0
--- /dev/null
+++ b/django_airavata/static/common/js/errors/ValidationErrors.js
@@ -0,0 +1,27 @@
+export default {
+  createValidationFeedback(data, validationErrors) {
+    const validationFeedback = {};
+    if (!data) {
+      return validationFeedback;
+    }
+    for (const fieldName in data) {
+      if (data.hasOwnProperty(fieldName)) {
+        const errorMessages = validationErrors
+          ? validationErrors[fieldName]
+          : null;
+        if (errorMessages) {
+          validationFeedback[fieldName] = {
+            invalidFeedback: errorMessages,
+            state: "invalid"
+          };
+        } else {
+          validationFeedback[fieldName] = {
+            invalidFeedback: null,
+            state: null
+          };
+        }
+      }
+    }
+    return validationFeedback;
+  }
+};
diff --git a/django_airavata/static/common/js/index.js b/django_airavata/static/common/js/index.js
index c39dbd6..8b7ce4b 100644
--- a/django_airavata/static/common/js/index.js
+++ b/django_airavata/static/common/js/index.js
@@ -10,6 +10,7 @@ import ShareButton from "./components/ShareButton.vue";
 import UnsavedChangesGuard from "./components/UnsavedChangesGuard.vue";
 
 import GlobalErrorHandler from "./errors/GlobalErrorHandler";
+import ValidationErrors from "./errors/ValidationErrors";
 
 import ListLayout from "./layouts/ListLayout.vue";
 
@@ -32,7 +33,8 @@ exports.components = {
 };
 
 exports.errors = {
-  GlobalErrorHandler
+  GlobalErrorHandler,
+  ValidationErrors
 };
 
 exports.layouts = {


Mime
View raw message