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>