You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2018/12/27 19:54:04 UTC

[airavata-django-portal] 01/03: AIRAVATA-2761 Initial implementation of input dependencies

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 7b4cb0f5c17066a96a7ad945c288a903b3abf583
Author: Marcus Christie <ma...@iu.edu>
AuthorDate: Thu Dec 20 12:48:38 2018 -0500

    AIRAVATA-2761 Initial implementation of input dependencies
---
 .../django_airavata_api/js/models/Experiment.js    |  23 ++-
 .../js/models/InputDataObjectType.js               | 157 +++++++++++++++------
 .../js/models/dependencies/DependencyEvaluator.js  |  94 ++++++++++++
 .../js/input-editors/InputEditorMixin.js           |  18 +--
 .../js/components/experiment/ExperimentEditor.vue  |   8 +-
 .../input-editors/InputEditorContainer.vue         |   7 +-
 .../experiment/input-editors/StringInputEditor.vue |   3 +-
 7 files changed, 247 insertions(+), 63 deletions(-)

diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js b/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js
index 8c1bd30..2adac92 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js
@@ -42,7 +42,8 @@ const FIELDS = [
   {
     name: "experimentInputs",
     type: InputDataObjectType,
-    list: true
+    list: true,
+    default: BaseModel.defaultNewInstance(Array)
   },
   {
     name: "experimentOutputs",
@@ -75,6 +76,7 @@ const FIELDS = [
 export default class Experiment extends BaseModel {
   constructor(data = {}) {
     super(FIELDS, data);
+    this.evaluateInputDependencies();
   }
 
   validate() {
@@ -125,7 +127,8 @@ export default class Experiment extends BaseModel {
 
   get isEditable() {
     return (
-      (!this.latestStatus || this.latestStatus.state === ExperimentState.CREATED) &&
+      (!this.latestStatus ||
+        this.latestStatus.state === ExperimentState.CREATED) &&
       this.userHasWriteAccess
     );
   }
@@ -135,6 +138,22 @@ export default class Experiment extends BaseModel {
     this.experimentInputs = applicationInterface.applicationInputs.map(input =>
       input.clone()
     );
+    this.evaluateInputDependencies();
     this.experimentOutputs = applicationInterface.applicationOutputs.slice();
   }
+
+  evaluateInputDependencies() {
+    const inputValues = this._collectInputValues(this.experimentInputs);
+    for (const input of this.experimentInputs) {
+      input.evaluateDependencies(inputValues);
+    }
+  }
+
+  _collectInputValues() {
+    const result = {};
+    this.experimentInputs.forEach(inp => {
+      result[inp.name] = inp.value;
+    });
+    return result;
+  }
 }
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/InputDataObjectType.js b/django_airavata/apps/api/static/django_airavata_api/js/models/InputDataObjectType.js
index 07ce6ef..f3b9867 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/InputDataObjectType.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/InputDataObjectType.js
@@ -1,53 +1,54 @@
-
-import BaseModel from './BaseModel';
-import DataType from './DataType';
-import ValidatorFactory from './validators/ValidatorFactory';
-import uuidv4 from 'uuid/v4';
+import BaseModel from "./BaseModel";
+import DataType from "./DataType";
+import DependencyEvaluator from "./dependencies/DependencyEvaluator";
+import uuidv4 from "uuid/v4";
+import ValidatorFactory from "./validators/ValidatorFactory";
 
 const FIELDS = [
-  'name',
-  'value',
+  "name",
+  "value",
   {
-    name: 'type',
+    name: "type",
     type: DataType,
-    default: DataType.STRING,
+    default: DataType.STRING
   },
-  'applicationArgument',
+  "applicationArgument",
   {
-    name: 'standardInput',
-    type: 'boolean',
-    default: false,
+    name: "standardInput",
+    type: "boolean",
+    default: false
   },
-  'userFriendlyDescription',
-  'metaData',
-  'inputOrder',
+  "userFriendlyDescription",
+  "metaData",
+  "inputOrder",
   {
-    name: 'isRequired',
-    type: 'boolean',
-    default: false,
+    name: "isRequired",
+    type: "boolean",
+    default: false
   },
   {
-    name: 'requiredToAddedToCommandLine',
-    type: 'boolean',
-    default: false,
+    name: "requiredToAddedToCommandLine",
+    type: "boolean",
+    default: false
   },
   {
-    name: 'dataStaged',
-    type: 'boolean',
-    default: false,
+    name: "dataStaged",
+    type: "boolean",
+    default: false
   },
-  'storageResourceId',
+  "storageResourceId",
   {
-    name: 'isReadOnly',
-    type: 'boolean',
-    default: false,
-  },
+    name: "isReadOnly",
+    type: "boolean",
+    default: false
+  }
 ];
 
 export default class InputDataObjectType extends BaseModel {
   constructor(data = {}) {
     super(FIELDS, data);
     this._key = data.key ? data.key : uuidv4();
+    this.show = true;
   }
 
   get key() {
@@ -68,8 +69,12 @@ export default class InputDataObjectType extends BaseModel {
    */
   get editorUIComponentId() {
     const metadata = this._getMetadata();
-    if (metadata && 'editor' in metadata && 'ui-component-id' in metadata['editor']) {
-      return metadata['editor']['ui-component-id'];
+    if (
+      metadata &&
+      "editor" in metadata &&
+      "ui-component-id" in metadata["editor"]
+    ) {
+      return metadata["editor"]["ui-component-id"];
     } else {
       return null;
     }
@@ -92,8 +97,8 @@ export default class InputDataObjectType extends BaseModel {
    */
   get editorConfig() {
     const metadata = this._getMetadata();
-    if (metadata && 'editor' in metadata && 'config' in metadata['editor']) {
-      return metadata['editor']['config'];
+    if (metadata && "editor" in metadata && "config" in metadata["editor"]) {
+      return metadata["editor"]["config"];
     } else {
       return {};
     }
@@ -123,38 +128,106 @@ export default class InputDataObjectType extends BaseModel {
    */
   get editorValidations() {
     const metadata = this._getMetadata();
-    if (metadata && 'editor' in metadata && 'validations' in metadata['editor']) {
-      return metadata['editor']['validations'];
+    if (
+      metadata &&
+      "editor" in metadata &&
+      "validations" in metadata["editor"]
+    ) {
+      return metadata["editor"]["validations"];
     } else {
       return [];
     }
   }
 
+  /**
+   * Get the dependencies for the editor component. Returns empty object if
+   * there are no dependencies. See evaluateDependencies for a list of
+   * available kinds of dependencies.
+   *
+   * The expected JSON schema for the editor validations is the following:
+   * {
+   *   "editor": {
+   *     "dependencies": {
+   *       "show": {
+   *         "AND": [              // Boolean operator ("AND", "OR")
+   *           "INPUT_1": {        // Name of other application input
+   *             "type": "equals", // Name of comparison type
+   *             "value": "1"      // Value to compare with
+   *           },
+   *           "NOT": {            // "NOT" is given a single input comparison or "AND" or "OR" expression
+   *             "INPUT_2": {
+   *               ...
+   *             }
+   *           }
+   *           ... additional boolean expressions ("AND", "OR", "NOT")
+   *           ... additional application input comparisons
+   *         ]
+   *       }
+   *     }
+   *   }
+   * }
+   */
+  get editorDependencies() {
+    const metadata = this._getMetadata();
+    if (
+      metadata &&
+      "editor" in metadata &&
+      "dependencies" in metadata["editor"]
+    ) {
+      return metadata["editor"]["dependencies"];
+    } else {
+      return {};
+    }
+  }
+
   _getMetadata() {
     // metaData could really be anything, here we expect it to be an object
     // so safely check if it is first
-    if (this.metaData && typeof this.metaData === 'object') {
+    if (this.metaData && typeof this.metaData === "object") {
       return this.metaData;
     } else {
       return null;
     }
   }
 
-  validate(experiment, value = undefined) {
-    let inputValue = typeof value != 'undefined' ? value : this.value;
+  validate(value = undefined) {
+    let inputValue = typeof value != "undefined" ? value : this.value;
     let results = {};
+    // Skip running validations when the input isn't shown
+    if (!this.show) {
+      return results;
+    }
     let valueErrorMessages = [];
     if (this.isRequired && this.isEmpty(inputValue)) {
-      valueErrorMessages.push('This field is required.');
+      valueErrorMessages.push("This field is required.");
     }
     // Run through any validations if configured
     if (this.editorValidations.length > 0) {
       const validatorFactory = new ValidatorFactory();
-      valueErrorMessages = valueErrorMessages.concat(validatorFactory.validate(this.editorValidations, inputValue));
+      valueErrorMessages = valueErrorMessages.concat(
+        validatorFactory.validate(this.editorValidations, inputValue)
+      );
     }
     if (valueErrorMessages.length > 0) {
-      results['value'] = valueErrorMessages;
+      results["value"] = valueErrorMessages;
     }
     return results;
   }
+
+  /**
+   * Evaluate dependencies on the values of other application inputs.
+   */
+  evaluateDependencies(inputValues) {
+    if (Object.keys(this.editorDependencies).length > 0) {
+      const dependencyEvaluator = new DependencyEvaluator();
+      const dependencyEvaluations = dependencyEvaluator.evaluateDependencies(
+        inputValues,
+        this.editorDependencies
+      );
+      // TODO: if show changes to false, set value to null? Save off current value to restore when shown again?
+      if ("show" in dependencyEvaluations) {
+        this.show = dependencyEvaluations["show"];
+      }
+    }
+  }
 }
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/dependencies/DependencyEvaluator.js b/django_airavata/apps/api/static/django_airavata_api/js/models/dependencies/DependencyEvaluator.js
new file mode 100644
index 0000000..0a4f04c
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/dependencies/DependencyEvaluator.js
@@ -0,0 +1,94 @@
+
+// TODO: rename: BooleanExpressionEvaluator? There's really nothing related to "app input dependencies" in here.
+export default class DependencyEvaluator {
+
+  /**
+   * Evaluate all dependency expressions and return an object with the same
+   * keys as dependenciesConfig but with the result of each expression.
+   *
+   * @param inputValues
+   * @param dependenciesConfig
+   */
+  evaluateDependencies(inputValues, dependenciesConfig) {
+
+    const result = {};
+    for (const k of Object.keys(dependenciesConfig)) {
+      result[k] = this._evaluateExpression(dependenciesConfig[k], inputValues);
+    }
+    return result;
+  }
+
+  /**
+   * Evaluates boolean expression for given context and returns boolean result.
+   */
+  _evaluateExpression(expression, context) {
+    console.log("expression: " + JSON.stringify(expression));
+    console.log("context: " + JSON.stringify(context));
+    const keys = Object.keys(expression);
+    if (keys.length > 1) {
+      throw new Error("Expression contains more than one key: " + JSON.stringify(expression));
+    }
+    if (keys.length < 1) {
+      throw new Error("Expression does not contain a key: " + JSON.stringify(expression));
+    }
+
+    const key = keys[0];
+    const value = expression[key];
+    if (key === "AND") {
+      if (value instanceof Array) {
+        const evaluations = value.map(exp => this._evaluateExpression(exp, context));
+        return evaluations.reduce((acc, curr) => acc && curr);
+      } else if (typeof value === 'object') {
+        return this._evaluateExpression(exp, context);
+      } else {
+        throw new Error("Unrecognized operand value for AND: " + JSON.stringify(value));
+      }
+    } else if (key === "OR") {
+      if (value instanceof Array) {
+        const evaluations = value.map(exp => this._evaluateExpression(exp, context));
+        return evaluations.reduce((acc, curr) => acc || curr);
+      } else if (typeof value === 'object') {
+        return this._evaluateExpression(exp, context);
+      } else {
+        throw new Error("Unrecognized operand value for OR: " + JSON.stringify(value));
+      }
+    } else if (key === "NOT") {
+      if (typeof value === 'object') {
+        return !this._evaluateExpression(value, context);
+      } else {
+        throw new Error("Unrecognized operand value for NOT: " + JSON.stringify(value));
+      }
+    }
+
+    if (typeof value === 'object') {
+      if (!(key in context)) {
+        throw new Error("Missing context value for expression " + JSON.stringify(expression) + " in context " + JSON.stringify(context));
+      }
+      const contextValue = context[key];
+      return this._evaluateExpressionType(contextValue, value);
+    }
+  }
+
+  // TODO: rename: _evaluateExpressionDefinition
+  // TODO: rename: _evaluateComparison
+  _evaluateExpressionType(value, expressionType) {
+    const type = expressionType["type"];
+    if (!type) {
+      throw new Error("Expression definition is missing 'type' property: " + JSON.stringify(expressionType));
+    }
+    if (type === "equals") {
+      return value === this._getExpressionTypeValue(expressionType);
+    }
+    throw new Error("Unrecognized expression type " + JSON.stringify(expressionType));
+  }
+
+  // TODO: rename: _getExpressionDefinitionValue(expressionDefiniton)
+  // TODO: rename: _getComparisonTarget
+  _getExpressionTypeValue(expressionType) {
+
+    if (!("value" in expressionType)) {
+      throw new Error("Missing required 'value' property in expression definition: " + JSON.stringify(expressionType));
+    }
+    return expressionType["value"];
+  }
+}
diff --git a/django_airavata/apps/workspace/django-airavata-workspace-plugin-api/js/input-editors/InputEditorMixin.js b/django_airavata/apps/workspace/django-airavata-workspace-plugin-api/js/input-editors/InputEditorMixin.js
index 446b46e..8d8ebfb 100644
--- a/django_airavata/apps/workspace/django-airavata-workspace-plugin-api/js/input-editors/InputEditorMixin.js
+++ b/django_airavata/apps/workspace/django-airavata-workspace-plugin-api/js/input-editors/InputEditorMixin.js
@@ -1,16 +1,12 @@
 // InputEditorMixin: mixin for experiment InputEditors, provides basic v-model
 // and validation functionality and defines the basic props interface
-// (experimentInput and experiment).
+// (experimentInput and id).
 import {models} from 'django-airavata-api'
 export default {
     props: {
         value: {
             required: true,
         },
-        experiment: {
-            type: models.Experiment,
-            required: true,
-        },
         experimentInput: {
             type: models.InputDataObjectType,
             required: true,
@@ -28,7 +24,7 @@ export default {
     },
     computed: {
         validationResults: function() {
-            return this.experimentInput.validate(this.experiment, this.data);
+            return this.experimentInput.validate(this.data);
         },
         validationMessages: function() {
             return 'value' in this.validationResults ? this.validationResults['value'] : [];
@@ -47,7 +43,6 @@ export default {
         valueChanged: function() {
             this.inputHasBegun = true;
             this.$emit('input', this.data);
-            this.checkValidation();
         },
         checkValidation: function() {
             if (this.valid) {
@@ -57,7 +52,12 @@ export default {
             }
         }
     },
-    mounted: function() {
+    created: function() {
         this.checkValidation();
+    },
+    watch: {
+        valid() {
+          this.checkValidation();
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
index 95661ba..f184d81 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
@@ -75,12 +75,13 @@
               </h2>
               <input-editor-container
                 v-for="experimentInput in localExperiment.experimentInputs"
-                :experiment="localExperiment"
                 :experiment-input="experimentInput"
                 v-model="experimentInput.value"
+                v-show="experimentInput.show"
                 :key="experimentInput.name"
                 @invalid="recordInvalidInputEditorValue(experimentInput.name)"
                 @valid="recordValidInputEditorValue(experimentInput.name)"
+                @input="inputValueChanged"
               />
             </div>
           </div>
@@ -260,7 +261,10 @@ export default {
         const index = this.invalidInputs.indexOf(experimentInputName);
         this.invalidInputs.splice(index, 1);
       }
-    }
+    },
+    inputValueChanged: function() {
+      this.localExperiment.evaluateInputDependencies();
+    },
   },
   watch: {
     experiment: function(newValue) {
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/InputEditorContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/InputEditorContainer.vue
index 8f36b1d..4fbb0d0 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/InputEditorContainer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/InputEditorContainer.vue
@@ -3,7 +3,6 @@
         :state="validationState" :feedback-messages="validationFeedback">
         <component :is="inputEditorComponentName"
             :id="inputEditorComponentId"
-            :experiment="experiment"
             :experiment-input="experimentInput"
             v-model="data"
             @invalid="recordInvalidInputEditorValue"
@@ -27,10 +26,6 @@ export default {
         value: {
             required: true,
         },
-        experiment: {
-            type: models.Experiment,
-            required: true,
-        },
         experimentInput: {
             type: models.InputDataObjectType,
             required: true,
@@ -95,4 +90,4 @@ export default {
         },
     }
 }
-</script>
\ No newline at end of file
+</script>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/StringInputEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/StringInputEditor.vue
index 7162152..04ab99d 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/StringInputEditor.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/StringInputEditor.vue
@@ -14,8 +14,7 @@ export default {
     props: {
         value: {
             type: String,
-            required: true,
         },
     },
 }
-</script>
\ No newline at end of file
+</script>