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

commit b7cabc2b6dea2a5f8c2604f88bca60d86c0b8972
Author: Marcus Christie <[email protected]>
AuthorDate: Thu Aug 30 11:34:20 2018 -0400

    AIRAVATA-2876 Initial interface editor, loading interface for module
---
 .../applications/ApplicationEditorContainer.vue    | 208 ++++++++++------
 .../applications/ApplicationInterfaceEditor.vue    |  69 ++++-
 .../applications/ApplicationModuleEditor.vue       | 114 ++++-----
 .../src/store/applications/app_interfaces.js       |  46 ++++
 .../src/store/applications/app_modules.js          | 106 ++++----
 .../src/store/applications/index.js                |  10 +-
 .../django_airavata_admin/static/css/admin.css     |   4 +-
 django_airavata/apps/api/exceptions.py             |   9 +-
 django_airavata/apps/api/serializers.py            |  28 +--
 .../api/static/django_airavata_api/js/index.js     |   2 +-
 .../js/models/ApplicationInterfaceDefinition.js    |  62 +++--
 .../django_airavata_api/js/service_config.js       | 135 +++++-----
 .../django_airavata_api/js/utils/FetchUtils.js     | 277 +++++++++++----------
 django_airavata/apps/api/views.py                  |  14 +-
 django_airavata/static/common/js/index.js          |  24 +-
 .../common/js/notifications/NotificationList.js    |  35 ++-
 16 files changed, 666 insertions(+), 477 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 642cb59..b6213c6 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
@@ -1,101 +1,143 @@
 <template>
-    <div>
-        <div class="row">
-            <div class="col">
-                <h1 class="h4 mb-4">
-                    {{ title }}
-                </h1>
-            </div>
-        </div>
-        <div class="row">
-            <div class="col">
-                <b-nav tabs>
-                    <b-nav-item exact-active-class="active" exact :to="{name: 
id ? 'application_module' : 'new_application_module', params: {id: 
id}}">Details</b-nav-item>
-                    <b-nav-item exact-active-class="active" exact :to="{name: 
'application_interface', params: {id: id}}" 
:disabled="!id">Interface</b-nav-item>
-                    <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="module" v-model="module" 
@save="saveModule" @cancel="cancelModule"/>
-                <router-view name="interface"/>
-                <router-view name="deployments"/>
-                <router-view name="deployment"/>
-            </div>
-        </div>
+  <div>
+    <div class="row">
+      <div class="col">
+        <h1 class="h4 mb-4">
+          {{ title }}
+        </h1>
+      </div>
     </div>
+    <div class="row">
+      <div class="col">
+        <b-nav tabs>
+          <b-nav-item exact-active-class="active" exact :to="{name: id ? 
'application_module' : 'new_application_module', params: {id: 
id}}">Details</b-nav-item>
+          <b-nav-item exact-active-class="active" exact :to="{name: 
'application_interface', params: {id: id}}" 
:disabled="!id">Interface</b-nav-item>
+          <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="module" v-model="module" 
@save="saveModule" @cancel="cancelModule" />
+        <router-view name="interface" v-if="appInterface" 
v-model="appInterface" @save="saveInterface" @cancel="cancelInterface" />
+        <router-view name="deployments" />
+        <router-view name="deployment" />
+      </div>
+    </div>
+  </div>
 </template>
 
 <script>
-import {mapActions, mapState} from 'vuex'
-import {models} from 'django-airavata-api'
+import { mapActions, mapState } from "vuex";
+import { models } from "django-airavata-api";
+import { notifications } from "django-airavata-common-ui";
 
 export default {
-    name: 'application-editor-container',
-    props: {
-        id: String
+  name: "application-editor-container",
+  props: {
+    id: String
+  },
+  data: function() {
+    return {
+      module: null,
+      appInterface: null,
+      deployments: null,
+      deployment: null
+    };
+  },
+  computed: {
+    title: function() {
+      if (this.id) {
+        return this.module && this.module.appModuleName
+          ? this.module.appModuleName
+          : "";
+      } else {
+        return "Create a New Application";
+      }
     },
-    data: function() {
-        return {
-            module: null,
-            interface: null,
-            deployments: null,
-            deployment: null,
-        }
+    ...mapState("applications/modules", ["currentModule"]),
+    ...mapState("applications/interfaces", ["currentInterface"])
+  },
+  created() {
+    this.initialize();
+  },
+  methods: {
+    ...mapActions("applications/modules", [
+      "loadApplicationModule",
+      "createApplicationModule",
+      "updateApplicationModule"
+    ]),
+    ...mapActions("applications/interfaces", [
+      "loadApplicationInterface",
+      "createApplicationInterface",
+      "updateApplicationInterface"
+    ]),
+    initialize() {
+      // TODO: move this to applications store?
+      if (this.currentModule && this.currentModule.appModuleId === this.id) {
+        this.module = this.currentModule.clone();
+        this.loadApplicationInterface(this.id).catch(error => {
+          notifications.NotificationList.addError(error);
+        });
+      } else if (this.id) {
+        this.loadApplicationModule(this.id);
+        this.loadApplicationInterface(this.id).catch(error => {
+          notifications.NotificationList.addError(error);
+        });
+      } else {
+        this.module = new models.ApplicationModule();
+      }
     },
-    computed: {
-        title: function() {
-            if (this.id) {
+    saveModule() {
+      if (this.id) {
+        this.updateApplicationModule(this.module).then(() => {
+          this.$router.push({ path: "/applications" });
+        });
+      } else {
+        this.createApplicationModule(this.module).then(appModule => {
+          this.$router.push({
+            name: "application_module",
+            params: { id: appModule.appModuleId }
+          });
+        });
+      }
+    },
+    saveInterface() {
+      // Copy name and description from module
+      this.appInterface.applicationName = this.module.appModuleName;
+      this.appInterface.applicationDescription = 
this.module.appModuleDescription;
 
-                return this.module && this.module.appModuleName ? 
this.module.appModuleName : "";
-            } else {
-                return "Create a New Application";
-            }
-        },
-        ...mapState('applications/modules', [
-            'currentModule',
-        ]),
+      this.updateApplicationModule(this.module).then(appModule => {
+        if (this.appInterface.applicationInterfaceId) {
+          return this.updateApplicationInterface(this.appInterface).then(() => 
{
+            this.$router.push({ path: "/applications" });
+          });
+        } else {
+          this.appInterface.applicationModules = [this.id];
+          return this.createApplicationInterface(this.appInterface).then(() => 
{
+            this.$router.push({ path: "/applications" });
+          });
+        }
+      });
     },
-    created () {
+    cancelModule() {
+      this.$router.push({ path: "/applications" });
+    },
+    cancelInterface() {
+      this.$router.push({ path: "/applications" });
+    }
+  },
+  watch: {
+    $route: function(to, from) {
+      if (to.params.id !== from.params.id) {
         this.initialize();
+      }
     },
-    methods: {
-        ...mapActions('applications/modules', [
-            'loadApplicationModule',
-            'createApplicationModule',
-            'updateApplicationModule',
-        ]),
-        initialize() {
-            if (this.currentModule && this.currentModule.appModuleId === 
this.id) {
-                this.module = this.currentModule.clone();
-            } else if (this.id) {
-                this.loadApplicationModule(this.id);
-            } else {
-                this.module = new models.ApplicationModule();
-            }
-        },
-        saveModule() {
-            if (this.id) {
-                this.updateApplicationModule(this.module)
-                    .then(() => {
-                        this.$router.push({path: '/applications'});
-                    });
-            } else {
-                this.createApplicationModule(this.module)
-                    .then(appModule => {
-                        this.$router.push({name: 'application_module', params: 
{id: appModule.appModuleId}});
-                    });
-            }
-        },
-        cancelModule() {
-            this.$router.push({path: '/applications'});
-        },
+    currentModule: function(newModule) {
+      // Clone the module from the store so we can modify it locally
+      this.module = newModule.clone();
     },
-    watch: {
-        '$route': 'initialize',
-        'currentModule': function(newModule) {
-            // Clone the module from the store so we can modify it locally
-            this.module = newModule.clone();
-        }
+    currentInterface: function(newInterface) {
+      this.appInterface = newInterface.clone();
     }
-}
+  }
+};
 </script>
 
 <style>
diff --git 
a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue
 
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue
index 1137fad..1618c0c 100644
--- 
a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue
+++ 
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue
@@ -1,10 +1,73 @@
 <template>
-    <div>Application Interface Editor</div>
+  <div>
+    <div class="row">
+      <div class="col">
+        <h1 class="h4 mb-4">
+          Application Interface
+        </h1>
+      </div>
+    </div>
+    <div class="row">
+      <div class="col">
+        <b-form-group label="Enable Archiving Working Directory" 
label-for="archive-directory">
+          <b-form-radio-group id="archive-directory"
+            v-model="appInterface.archiveWorkingDirectory"
+            :options="archiveWorkingDirectoryOptions"
+            @input="emitChanged">
+          </b-form-radio-group>
+        </b-form-group>
+      </div>
+    </div>
+    <div class="row">
+      <div class="col">
+        <b-button variant="primary" @click="save">
+            Save
+        </b-button>
+        <b-button variant="secondary" @click="cancel">
+            Cancel
+        </b-button>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script>
+import { models } from "django-airavata-api";
+
 export default {
-    name: 'application-interface-editor'
-}
+  name: "application-interface-editor",
+  props: {
+    value: {
+      type: models.ApplicationInterfaceDefinition,
+      required: true
+    }
+  },
+  data: function() {
+    return {
+      appInterface: this.value.clone()
+    };
+  },
+  computed: {
+    archiveWorkingDirectoryOptions() {
+      return [{ text: "True", value: true }, { text: "False", value: false }];
+    }
+  },
+  methods: {
+    emitChanged() {
+      this.$emit("input", this.appInterface);
+    },
+    save() {
+      this.$emit("save");
+    },
+    cancel() {
+      this.$emit("cancel");
+    }
+  },
+  watch: {
+    value: function(newValue) {
+      this.appInterface = newValue.clone();
+    }
+  }
+};
 </script>
 
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 0ce2952..092fdc9 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
@@ -1,72 +1,66 @@
 <template>
-    <div>
-        <div class="row">
-            <div class="col">
-                <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="appModule.appModuleName" required
-                    @input="emitChanged"></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="appModule.appModuleVersion"
-                    @input="emitChanged"></b-form-input>
-                </b-form-group>
-                <b-form-group label="Application Description" 
label-for="application-description">
-                    <b-form-textarea id="application-description"
-                        v-model="appModule.appModuleDescription"
-                        :rows="3"></b-form-textarea>
-                </b-form-group>
-            </div>
-        </div>
-        <div class="row">
-            <div class="col">
-                <b-button variant="primary" @click="save">
-                    Save
-                </b-button>
-                <b-button variant="secondary" @click="cancel">
-                    Cancel
-                </b-button>
-            </div>
-        </div>
+  <div>
+    <div class="row">
+      <div class="col">
+        <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="appModule.appModuleName" required @input="emitChanged"></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="appModule.appModuleVersion" @input="emitChanged"></b-form-input>
+        </b-form-group>
+        <b-form-group label="Application Description" 
label-for="application-description">
+          <b-form-textarea id="application-description" 
v-model="appModule.appModuleDescription" :rows="3" 
@input="emitChanged"></b-form-textarea>
+        </b-form-group>
+      </div>
     </div>
+    <div class="row">
+      <div class="col">
+        <b-button variant="primary" @click="save">
+          Save
+        </b-button>
+        <b-button variant="secondary" @click="cancel">
+          Cancel
+        </b-button>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script>
-import {models} from 'django-airavata-api'
+import { models } from "django-airavata-api";
 
 export default {
-    name: 'application-module-editor',
-    props: {
-        value: {
-            type: models.ApplicationModule,
-            required: true,
-        }
-    },
-    data: function() {
-        return {
-            appModule: this.value.clone(),
-        }
+  name: "application-module-editor",
+  props: {
+    value: {
+      type: models.ApplicationModule,
+      required: true
+    }
+  },
+  data: function() {
+    return {
+      appModule: this.value.clone()
+    };
+  },
+  methods: {
+    emitChanged() {
+      this.$emit("input", this.appModule);
     },
-    methods: {
-        emitChanged() {
-            this.$emit('input', this.appModule);
-        },
-        save() {
-            this.$emit('save');
-        },
-        cancel() {
-            this.$emit('cancel');
-        }
+    save() {
+      this.$emit("save");
     },
-    watch: {
-        value: function(newValue) {
-            this.appModule = newValue.clone();
-        }
+    cancel() {
+      this.$emit("cancel");
+    }
+  },
+  watch: {
+    value: function(newValue) {
+      this.appModule = newValue.clone();
     }
-}
+  }
+};
 </script>
 
diff --git 
a/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_interfaces.js
 
b/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_interfaces.js
new file mode 100644
index 0000000..e1c9f9e
--- /dev/null
+++ 
b/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_interfaces.js
@@ -0,0 +1,46 @@
+import { services, models } from 'django-airavata-api'
+
+export default {
+  namespaced: true,
+  state: {
+    currentInterface: null,
+  },
+  mutations: {
+    setCurrentInterface(state, currentInterface) {
+      state.currentInterface = currentInterface;
+    }
+  },
+  actions: {
+    loadApplicationInterface({ commit }, appModuleId) {
+      // Otherwise fetch from the backend
+      return services.ApplicationModuleService.getApplicationInterface({ 
lookup: appModuleId }, { ignoreErrors: true })
+        .then(appInterface => {
+          commit('setCurrentInterface', appInterface);
+          return appInterface;
+        })
+        .catch(error => {
+          if (error.details.status === 404) {
+            // If there is no interface, just create a new instance
+            commit('setCurrentInterface', new 
models.ApplicationInterfaceDefinition());
+            return Promise.resolve(null);
+          } else {
+            throw error;
+          }
+        });
+    },
+    createApplicationInterface({ commit }, appInterface) {
+      return services.ApplicationInterfaceService.create({ data: appInterface 
})
+        .then(appInterface => {
+          commit('setCurrentInterface', appInterface);
+          return appInterface;
+        });
+    },
+    updateApplicationInterface({ commit }, appInterface) {
+      return services.ApplicationInterfaceService.update({ lookup: 
appInterface.applicationInterfaceId, data: appInterface })
+        .then(appInterface => {
+          commit('setCurrentInterface', appInterface);
+          return appInterface;
+        })
+    }
+  }
+}
diff --git 
a/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_modules.js
 
b/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_modules.js
index 3b92e94..2ad6131 100644
--- 
a/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_modules.js
+++ 
b/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_modules.js
@@ -1,61 +1,61 @@
 import { services } from 'django-airavata-api'
 
 export default {
-    namespaced: true,
-    state: {
-        modules: null,
-        currentModule: null,
+  namespaced: true,
+  state: {
+    modules: null,
+    currentModule: null,
+  },
+  mutations: {
+    setModules(state, modules) {
+      state.modules = modules;
     },
-    mutations: {
-        setModules(state, modules) {
-            state.modules = modules;
-        },
-        setModule(state, module) {
-            state.currentModule = module;
-        }
+    setModule(state, module) {
+      state.currentModule = module;
+    }
+  },
+  actions: {
+    loadApplicationModules({ commit }) {
+      return services.ApplicationModuleService.list()
+        .then(appModules => {
+          commit('setModules', appModules);
+          return appModules;
+        });
     },
-    actions: {
-        loadApplicationModules({ commit }) {
-            return services.ApplicationModuleService.list()
-                .then(appModules => {
-                    commit('setModules', appModules);
-                    return appModules;
-                });
-        },
-        loadApplicationModule({ commit, state }, appModuleId) {
-            if (state.modules) {
-                const appModule = state.modules.find(appModule => 
appModule.appModuleId === appModuleId);
-                if (appModule) {
-                    commit('setModule', appModule);
-                    return Promise.resolve(appModule);
-                }
-            }
-            // Otherwise fetch from the backend
-            return services.ApplicationModuleService.retrieve({lookup: 
appModuleId})
-                .then(appModule => {
-                    commit('setModule', appModule);
-                    return appModule;
-                });
-        },
-        createApplicationModule({ commit, state }, appModule) {
-            return services.ApplicationModuleService.create({data: appModule})
-                .then(appModule => {
-                    commit('setModule', appModule);
-                    const appModulesCopy = state.modules.slice();
-                    appModulesCopy.push(appModule);
-                    commit('setModules', appModulesCopy);
-                    return appModule;
-                });
-        },
-        updateApplicationModule({ commit, state }, appModule) {
-            return services.ApplicationModuleService.update({lookup: 
appModule.appModuleId, data: appModule})
-                .then(appModule => {
-                    commit('setModule', appModule);
-                    const appModules = state.modules.filter(mod => 
mod.appModuleId !== appModule.appModuleId)
-                    appModules.push(appModule);
-                    commit('setModules', appModules);
-                    return appModule;
-                })
+    loadApplicationModule({ commit, state }, appModuleId) {
+      if (state.modules) {
+        const appModule = state.modules.find(appModule => 
appModule.appModuleId === appModuleId);
+        if (appModule) {
+          commit('setModule', appModule);
+          return Promise.resolve(appModule);
         }
+      }
+      // Otherwise fetch from the backend
+      return services.ApplicationModuleService.retrieve({ lookup: appModuleId 
})
+        .then(appModule => {
+          commit('setModule', appModule);
+          return appModule;
+        });
+    },
+    createApplicationModule({ commit, state }, appModule) {
+      return services.ApplicationModuleService.create({ data: appModule })
+        .then(appModule => {
+          commit('setModule', appModule);
+          const appModulesCopy = state.modules.slice();
+          appModulesCopy.push(appModule);
+          commit('setModules', appModulesCopy);
+          return appModule;
+        });
+    },
+    updateApplicationModule({ commit, state }, appModule) {
+      return services.ApplicationModuleService.update({ lookup: 
appModule.appModuleId, data: appModule })
+        .then(appModule => {
+          commit('setModule', appModule);
+          const appModules = state.modules.filter(mod => mod.appModuleId !== 
appModule.appModuleId)
+          appModules.push(appModule);
+          commit('setModules', appModules);
+          return appModule;
+        })
     }
+  }
 }
diff --git 
a/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/index.js
 
b/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/index.js
index 2a83e50..b83d783 100644
--- 
a/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/index.js
+++ 
b/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/index.js
@@ -1,8 +1,10 @@
+import interfaces from './app_interfaces'
 import modules from './app_modules'
 
 export default {
-    namespaced: true,
-    modules: {
-        modules
-    }
+  namespaced: true,
+  modules: {
+    interfaces,
+    modules,
+  }
 }
diff --git 
a/django_airavata/apps/admin/static/django_airavata_admin/static/css/admin.css 
b/django_airavata/apps/admin/static/django_airavata_admin/static/css/admin.css
index 3b633b7..8ad58ac 100644
--- 
a/django_airavata/apps/admin/static/django_airavata_admin/static/css/admin.css
+++ 
b/django_airavata/apps/admin/static/django_airavata_admin/static/css/admin.css
@@ -9,7 +9,7 @@
   margin-bottom: 10px;
 }
 
-.new_app input[type="text"], input[type="number"], textarea, select {
+/* .new_app input[type="text"], input[type="number"], textarea, select {
   width: 100%;
   border-radius: 2px;
   border: solid #dddddd 1px;
@@ -19,7 +19,7 @@
 
 button, input[type="button"] {
   outline: none;
-}
+} */
 
 .main_section {
   width: 100%;
diff --git a/django_airavata/apps/api/exceptions.py 
b/django_airavata/apps/api/exceptions.py
index 632d97c..e643f1c 100644
--- a/django_airavata/apps/api/exceptions.py
+++ b/django_airavata/apps/api/exceptions.py
@@ -12,6 +12,7 @@ log = logging.getLogger(__name__)
 
 
 def custom_exception_handler(exc, context):
+    log.debug("API exception", exc_info=exc)
     # Call REST framework's default exception handler first,
     # to get the standard error response.
     response = exception_handler(exc, context)
@@ -24,10 +25,16 @@ def custom_exception_handler(exc, context):
             {'detail': str(exc)},
             status=status.HTTP_500_INTERNAL_SERVER_ERROR)
 
+    # Generic handler
+    if response is None:
+        return Response(
+            {'detail': str(exc)},
+            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))
 
-    log.debug("API exception", exc_info=exc)
     return response
diff --git a/django_airavata/apps/api/serializers.py 
b/django_airavata/apps/api/serializers.py
index 41182e5..21b5cf7 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -235,12 +235,6 @@ class ApplicationModuleSerializer(
     class Meta:
         required = ('appModuleName',)
 
-    def create(self, validated_data):
-        return ApplicationModule(**validated_data)
-
-    def update(self, instance, validated_data):
-        return self.create(validated_data)
-
 
 class InputDataObjectTypeSerializer(serializers.Serializer):
     name = serializers.CharField(required=False)
@@ -293,23 +287,13 @@ class CustomSerializer(serializers.Serializer):
         return params
 
 
-class ApplicationInterfaceDescriptionSerializer(CustomSerializer):
-    url = 
FullyEncodedHyperlinkedIdentityField(view_name='django_airavata_api:application-interface-detail',
 lookup_field='applicationInterfaceId', lookup_url_kwarg='app_interface_id')
-    applicationInterfaceId = serializers.CharField(read_only=True)
-    applicationName = serializers.CharField(required=False)
-    applicationDescription = serializers.CharField(required=False)
-    archiveWorkingDirectory = serializers.BooleanField(required=False)
-    hasOptionalFileInputs = serializers.BooleanField(required=False)
-    applicationOutputs = 
serializers.ListSerializer(child=OutputDataObjectTypeSerializer())
-    applicationInputs = 
serializers.ListSerializer(child=InputDataObjectTypeSerializer())
-    applicationModules = 
serializers.ListSerializer(child=serializers.CharField())
+class ApplicationInterfaceDescriptionSerializer(
+        thrift_utils.create_serializer_class(ApplicationInterfaceDescription)):
 
-    def create(self, validated_data):
-        params=self.process_list_fields(validated_data)
-        return ApplicationInterfaceDescription(**params)
-
-    def update(self, instance, validated_data):
-        raise Exception("Not implemented")
+    url = FullyEncodedHyperlinkedIdentityField(
+        view_name='django_airavata_api:application-interface-detail',
+        lookup_field='applicationInterfaceId',
+        lookup_url_kwarg='app_interface_id')
 
 
 class CommandObjectSerializer(CustomSerializer):
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 d82fe04..324cdd9 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
@@ -76,7 +76,7 @@ exports.models = {
 
 exports.services = {
     ApplicationDeploymentService,
-    ApplicationInterfaceService,
+    ApplicationInterfaceService: 
ServiceFactory.service("ApplicationInterfaces"),
     ApplicationModuleService: ServiceFactory.service("ApplicationModules"),
     CredentialSummaryService: ServiceFactory.service("CredentialSummaries"),
     ExperimentService,
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 0383219..0d98c9c 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
@@ -4,36 +4,44 @@ import OutputDataObjectType from './OutputDataObjectType'
 
 
 const FIELDS = [
-    'applicationInterfaceId',
-    'applicationName',
-    'applicationDescription',
-    {
-        name: 'applicationModules',
-        type: 'string',
-        list: true,
-    },
-    {
-        name: 'applicationInputs',
-        type: InputDataObjectType,
-        list: true,
-    },
-    {
-        name: 'applicationOutputs',
-        type: OutputDataObjectType,
-        list: true,
-    },
-    'archiveWorkingDirectory',
-    'hasOptionalFileInputs',
+  'applicationInterfaceId',
+  'applicationName',
+  'applicationDescription',
+  {
+    name: 'applicationModules',
+    type: 'string',
+    list: true,
+  },
+  {
+    name: 'applicationInputs',
+    type: InputDataObjectType,
+    list: true,
+  },
+  {
+    name: 'applicationOutputs',
+    type: OutputDataObjectType,
+    list: true,
+  },
+  {
+    name: 'archiveWorkingDirectory',
+    type: 'boolean',
+    default: false,
+  },
+  {
+    name: 'hasOptionalFileInputs',
+    type: 'boolean',
+    default: false,
+  },
 ];
 
 export default class ApplicationInterfaceDefinition extends BaseModel {
 
-    constructor(data = {}) {
-        super(FIELDS, data);
-    }
+  constructor(data = {}) {
+    super(FIELDS, data);
+  }
 
-    getOrderedApplicationInputs() {
-        // Use slice() to make a copy and sort that copy
-        return this.applicationInputs ? 
this.applicationInputs.slice().sort((a, b) => a.inputOrder - b.inputOrder) : [];
-    }
+  getOrderedApplicationInputs() {
+    // Use slice() to make a copy and sort that copy
+    return this.applicationInputs ? this.applicationInputs.slice().sort((a, b) 
=> a.inputOrder - b.inputOrder) : [];
+  }
 }
diff --git 
a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js 
b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
index 363fa6c..a3536a3 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
@@ -5,6 +5,7 @@ import Group from './models/Group'
 import GroupResourceProfile from './models/GroupResourceProfile'
 import SharedEntity from './models/SharedEntity'
 import UserProfile from './models/UserProfile'
+import ApplicationInterfaceDefinition from 
'./models/ApplicationInterfaceDefinition';
 
 const post = "post";
 const get = "get";
@@ -39,61 +40,81 @@ pagination:true # whether to treat the response as a 
paginated response
  */
 
 export default {
-    "ApplicationDeployments": {
-        url: "/api/application-deployments",
-        viewSet: true,
-        queryParams: ['appModuleId', 'groupResourceProfileId'],
-        modelClass: ApplicationDeploymentDescription,
-    },
-    "ApplicationModules": {
-        url: "/api/applications",
-        viewSet: true,
-        modelClass: ApplicationModule,
-    },
-    "ComputeResources": {
-        url: "/api/compute-resources",
-        viewSet: [{
-            name: "list"
-        }, {
-            name: "names",
-            url: "/api/compute-resources/all_names/",
-            requestType: 'get',
-        }, {
-            name: "namesList",
-            url: "/api/compute-resources/all_names_list/",
-            requestType: 'get',
-        }],
-        modelClass: ApplicationDeploymentDescription,
-    },
-    "CredentialSummaries": {
-        url: "/api/credential-summaries/",
-        viewSet: [{
-            name: "list"
-        }],
-        modelClass: CredentialSummary,
-    },
-    "GroupResourceProfiles": {
-        url: "/api/group-resource-profiles/",
-        viewSet: true,
-        modelClass: GroupResourceProfile,
-    },
-    "Groups": {
-        url: "/api/groups",
-        viewSet: true,
-        pagination: true,
-        queryParams: ['limit', 'offset'],
-        modelClass: Group,
-    },
-    "SharedEntities": {
-        url: "/api/shared-entities",
-        viewSet: true,
-        modelClass: SharedEntity,
-    },
-    "UserProfiles": {
-        url: "/api/user-profiles",
-        viewSet: [{
-            name: "list"
-        }],
-        modelClass: UserProfile,
-    },
+  "ApplicationDeployments": {
+    url: "/api/application-deployments",
+    viewSet: true,
+    queryParams: ['appModuleId', 'groupResourceProfileId'],
+    modelClass: ApplicationDeploymentDescription,
+  },
+  "ApplicationInterfaces": {
+    url: "/api/application-interfaces",
+    viewSet: true,
+    modelClass: ApplicationInterfaceDefinition,
+  },
+  "ApplicationModules": {
+    url: "/api/applications",
+    viewSet: [{
+      name: "list"
+    }, {
+      name: "create"
+    }, {
+      name: "retrieve"
+    }, {
+      name: "update"
+    }, {
+      name: "delete"
+    }, {
+      name: "getApplicationInterface",
+      url: "/api/applications/<lookup>/application_interface/",
+      requestType: 'get',
+      modelClass: ApplicationInterfaceDefinition
+    }],
+    modelClass: ApplicationModule,
+  },
+  "ComputeResources": {
+    url: "/api/compute-resources",
+    viewSet: [{
+      name: "list"
+    }, {
+      name: "names",
+      url: "/api/compute-resources/all_names/",
+      requestType: 'get',
+    }, {
+      name: "namesList",
+      url: "/api/compute-resources/all_names_list/",
+      requestType: 'get',
+    }],
+    modelClass: ApplicationDeploymentDescription,
+  },
+  "CredentialSummaries": {
+    url: "/api/credential-summaries/",
+    viewSet: [{
+      name: "list"
+    }],
+    modelClass: CredentialSummary,
+  },
+  "GroupResourceProfiles": {
+    url: "/api/group-resource-profiles/",
+    viewSet: true,
+    modelClass: GroupResourceProfile,
+  },
+  "Groups": {
+    url: "/api/groups",
+    viewSet: true,
+    pagination: true,
+    queryParams: ['limit', 'offset'],
+    modelClass: Group,
+  },
+  "SharedEntities": {
+    url: "/api/shared-entities",
+    viewSet: true,
+    modelClass: SharedEntity,
+  },
+  "UserProfiles": {
+    url: "/api/user-profiles",
+    viewSet: [{
+      name: "list"
+    }],
+    modelClass: UserProfile,
+  },
 }
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 f16d382..c6a796c 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
@@ -2,161 +2,164 @@ 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("&");
-    }
-    if (queryParams && queryParams !== "") {
-        return url + "?" + queryParams;
-    } else {
-        return url;
-    }
+  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) {
-    let spinner = document.getElementById("airavata-spinner");
-    spinner.style.display = display;
+  let spinner = document.getElementById("airavata-spinner");
+  spinner.style.display = display;
 }
 
 const incrementCount = function () {
-    count++;
-    if (count == 1) {
-        setSpinnerDisplay("block");
-    }
+  count++;
+  if (count == 1) {
+    setSpinnerDisplay("block");
+  }
 }
 const decrementCount = function () {
-    if (count > 0) {
-        count--;
-        if (count == 0) {
-            setSpinnerDisplay("none");
-        }
+  if (count > 0) {
+    count--;
+    if (count == 0) {
+      setSpinnerDisplay("none");
     }
+  }
 }
 
 export default {
-    enableSpinner: function () {
+  enableSpinner: function () {
+
+  },
+  disableSpinner: 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") {
+    var csrfToken = this.getCSRFToken();
+    var headers = new Headers({
+      "Content-Type": contentType,
+      "Accept": accept,
+    });
+    if (csrfToken != null) {
+      headers.set("X-CSRFToken", csrfToken)
+    }
+    return headers;
+  },
+  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),
+      headers: headers,
+      credentials: "same-origin",
+      ignoreErrors
+    });
+  },
+  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),
+      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("&")
+    }
+    if (queryParams) {
+      url = url + "?" + queryParams
+    }
+    var headers = this.createHeaders(mediaType);
+    return this.processFetch(url, {
+      method: 'get',
+      headers: headers,
+      credentials: "same-origin",
+      ignoreErrors
+    });
+  },
+  delete: function (url, { ignoreErrors = false } = {}) {
+    var headers = this.createHeaders();
+    return this.processFetch(url, {
+      method: 'delete',
+      headers: headers,
+      credentials: "same-origin",
+      ignoreErrors
+    });
+  },
+  processFetch: function (url, { method = 'get', headers, credentials = 
'same-origin', body, ignoreErrors = false }) {
 
-    },
-    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];
+    const fetchConfig = {
+      method,
+      headers,
+      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 null;
-        }
-    },
-    createHeaders: function (contentType = "application/json", accept = 
"application/json") {
-        var csrfToken = this.getCSRFToken();
-        var headers = new Headers({
-            "Content-Type": contentType,
-            "Accept": accept,
-        });
-        if (csrfToken != null) {
-            headers.set("X-CSRFToken", csrfToken)
-        }
-        return headers;
-    },
-    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),
-            headers: headers,
-            credentials: "same-origin",
-            ignoreErrors
-        });
-    },
-    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),
-            headers: headers,
-            credentials: "same-origin"
-        });
-    },
-    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
+          return Promise.resolve(response.json())
         }
-        var headers = this.createHeaders(mediaType);
-        return this.processFetch(url, {
-            method: 'get',
-            headers: headers,
-            credentials: "same-origin"
-        });
-    },
-    delete: function (url, { ignoreErrors = false } = {}) {
-        var headers = this.createHeaders();
-        return this.processFetch(url, {
-            method: 'delete',
-            headers: headers,
-            credentials: "same-origin"
+      } 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 })
+          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;
         });
-    },
-    processFetch: function(url, {method = 'get', headers, credentials = 
'same-origin', body, ignoreErrors = false}) {
+      }
+    }, (error) => {
+      decrementCount();
+      error.details = this.createErrorDetails({ url, body });
+      throw error;
+    }).catch(error => {
 
-        const fetchConfig = {
-            method,
-            headers,
-            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})
-                    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;
+      if (!ignoreErrors) {
+        UnhandledErrorDispatcher.reportError({
+          message: error.message,
+          error: error,
+          details: error.details,
         })
-    },
-    createErrorDetails: function({url, body, status = null, responseBody = 
null}={}) {
-        return {
-            url,
-            body,
-            status,
-            response: responseBody
-        }
+      }
+      throw error;
+    })
+  },
+  createErrorDetails: function ({ url, body, status = null, responseBody = 
null } = {}) {
+    return {
+      url,
+      body,
+      status,
+      response: responseBody
     }
+  }
 }
diff --git a/django_airavata/apps/api/views.py 
b/django_airavata/apps/api/views.py
index 2edcae0..63966bc 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -289,13 +289,15 @@ class ApplicationModuleViewSet(APIBackedViewSet):
             return Response(serializer.data)
         elif len(app_interfaces) > 1:
             log.error(
-                "More than one application interface found for module {}: 
{}".format(app_module_id, app_interfaces))
-            return Response(
-                {'error': 'More than one application interface found for 
module id {}'.format(app_module_id)},
-                status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+                "More than one application interface found for module {}: {}"
+                .format(app_module_id, app_interfaces))
+            raise Exception(
+                'More than one application interface found for module {}'
+                .format(app_module_id)
+            )
         else:
-            return Response({'error': 'No application interface found for 
module id {}'.format(app_module_id)},
-                            status=status.HTTP_404_NOT_FOUND)
+            raise Http404("No application interface found for module id {}"
+                          .format(app_module_id))
 
     @detail_route()
     def application_deployments(self, request, app_module_id):
diff --git a/django_airavata/static/common/js/index.js 
b/django_airavata/static/common/js/index.js
index 30b0672..fca2e7e 100644
--- a/django_airavata/static/common/js/index.js
+++ b/django_airavata/static/common/js/index.js
@@ -9,23 +9,31 @@ import GlobalErrorHandler from './errors/GlobalErrorHandler'
 
 import ListLayout from './layouts/ListLayout.vue'
 
+import Notification from './notifications/Notification'
+import NotificationList from './notifications/NotificationList'
+
 import * as utils from './utils'
 
 exports.components = {
-    Pager,
-    ApplicationCard,
-    Autocomplete,
-    AutocompleteTextInput,
-    NotificationsDisplay,
-    ShareButton,
+  Pager,
+  ApplicationCard,
+  Autocomplete,
+  AutocompleteTextInput,
+  NotificationsDisplay,
+  ShareButton,
 }
 
 exports.errors = {
-    GlobalErrorHandler,
+  GlobalErrorHandler,
 }
 
 exports.layouts = {
-    ListLayout,
+  ListLayout,
+}
+
+exports.notifications = {
+  Notification,
+  NotificationList,
 }
 
 exports.utils = utils;
diff --git a/django_airavata/static/common/js/notifications/NotificationList.js 
b/django_airavata/static/common/js/notifications/NotificationList.js
index 86c7b41..de8b6f8 100644
--- a/django_airavata/static/common/js/notifications/NotificationList.js
+++ b/django_airavata/static/common/js/notifications/NotificationList.js
@@ -1,22 +1,31 @@
+import Notification from './Notification'
 
 class NotificationList {
 
-    constructor() {
-        this.notifications = [];
-    }
+  constructor() {
+    this.notifications = [];
+  }
 
-    add(notification) {
-        this.notifications.push(notification);
-    }
+  add(notification) {
+    this.notifications.push(notification);
+  }
 
-    remove(notification) {
-        const i = this.notifications.indexOf(notification);
-        this.notifications.splice(i, 1);
-    }
+  // Convenience method for adding an error
+  addError(error) {
+    this.notifications.push(new Notification({
+      type: "ERROR",
+      message: error.message
+    }))
+  }
 
-    get list() {
-        return this.notifications;
-    }
+  remove(notification) {
+    const i = this.notifications.indexOf(notification);
+    this.notifications.splice(i, 1);
+  }
+
+  get list() {
+    return this.notifications;
+  }
 }
 
 export default new NotificationList();

Reply via email to