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 2020/03/25 20:07:14 UTC
[airavata-django-portal] 02/09: AIRAVATA-3216
ComputeResourceReservation validations
This is an automated email from the ASF dual-hosted git repository.
machristie pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git
commit ac818b43e18ece9491f39cae5d57d0473067fde1
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue Mar 10 11:52:34 2020 -0400
AIRAVATA-3216 ComputeResourceReservation validations
---
.../ComputePreference.vue | 8 +-
.../ComputeResourceReservationEditor.vue | 44 ++--
.../ComputeResourceReservationList.vue | 65 +++++-
.../django_airavata_api/js/models/BaseModel.js | 229 ++++++++++++---------
.../js/models/ComputeResourceReservation.js | 11 +-
5 files changed, 229 insertions(+), 128 deletions(-)
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputePreference.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputePreference.vue
index 0ab37a6..74294d8 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputePreference.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputePreference.vue
@@ -154,6 +154,8 @@
@added="addReservation"
@deleted="deleteReservation"
@updated="updateReservation"
+ @valid="reservationsInvalid = false"
+ @invalid="reservationsInvalid = true"
/>
</div>
</div>
@@ -264,7 +266,8 @@ export default {
jobSubmissionInterfaces: []
},
validationErrors: null,
- invalidBatchQueueResourcePolicies: []
+ invalidBatchQueueResourcePolicies: [],
+ reservationsInvalid: false
};
},
computed: {
@@ -280,7 +283,8 @@ export default {
valid() {
return (
this.allowedInvalidBatchQueueResourcePolicies.length === 0 &&
- Object.keys(this.groupComputeResourceValidation).length === 0
+ Object.keys(this.groupComputeResourceValidation).length === 0 &&
+ !this.reservationsInvalid
);
},
allowedInvalidBatchQueueResourcePolicies() {
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputeResourceReservationEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputeResourceReservationEditor.vue
index 0d18ba7..b10d269 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputeResourceReservationEditor.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputeResourceReservationEditor.vue
@@ -1,5 +1,5 @@
<template>
- <b-form @input="valuesChanged">
+ <b-form>
<b-form-group
label="Reservation name"
label-for="reservation-name"
@@ -14,7 +14,12 @@
:state="nameValidationState"
/>
</b-form-group>
- <b-form-group label="Start Time" label-for="start-time">
+ <b-form-group
+ label="Start Time"
+ label-for="start-time"
+ :invalid-feedback="getValidationFeedback('startTime')"
+ :state="getValidationState('startTime')"
+ >
<datetime
id="start-time"
type="datetime"
@@ -37,12 +42,20 @@
@input="data.startTime = stringToDate($event)"
></datetime>
</b-form-group>
- <b-form-group label="End Time" label-for="end-time">
+ <b-form-group
+ label="End Time"
+ label-for="end-time"
+ :invalid-feedback="getValidationFeedback('endTime')"
+ :state="getValidationState('endTime')"
+ >
<datetime
id="end-time"
type="datetime"
:value="endTimeAsString"
- input-class="form-control"
+ :input-class="{
+ 'form-control': true,
+ 'is-invalid': getValidationState('endTime')
+ }"
:format="{
year: 'numeric',
month: '2-digit',
@@ -60,15 +73,17 @@
@input="data.endTime = stringToDate($event)"
></datetime>
</b-form-group>
- <b-form-group label="Queues" label-for="queues"
+ <b-form-group
+ label="Queues"
+ label-for="queues"
:invalid-feedback="getValidationFeedback('queueNames')"
:state="getValidationState('queueNames')"
>
<b-form-checkbox-group
- id="queues"
- v-model="data.queueNames"
- :options="queueNameOptions"
- :state="getValidationState('queueNames')"
+ id="queues"
+ v-model="data.queueNames"
+ :options="queueNameOptions"
+ :state="getValidationState('queueNames')"
/>
</b-form-group>
</b-form>
@@ -96,6 +111,9 @@ export default {
nameInputBegins: false
};
},
+ created() {
+ this.$on("input", this.valuesChanged);
+ },
computed: {
startTimeAsString() {
return this.data.startTime.toISOString();
@@ -104,13 +122,13 @@ export default {
return this.data.endTime.toISOString();
},
nameValidationFeedback() {
- return this.getValidationFeedback('reservationName');
+ return this.getValidationFeedback("reservationName");
},
nameValidationState() {
if (this.nameInputBegins === false) {
return null;
}
- return this.getValidationState('reservationName');
+ return this.getValidationState("reservationName");
},
queueNameOptions() {
return this.queues.slice().sort();
@@ -129,9 +147,9 @@ export default {
valuesChanged() {
const validationResults = this.data.validate();
if (Object.keys(validationResults).length === 0) {
- this.$emit('valid');
+ this.$emit("valid");
} else {
- this.$emit('invalid');
+ this.$emit("invalid");
}
}
}
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputeResourceReservationList.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputeResourceReservationList.vue
index 2b2671b..2415291 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputeResourceReservationList.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputeResourceReservationList.vue
@@ -11,8 +11,14 @@
<compute-resource-reservation-editor
v-model="newReservation"
:queues="queues"
- @valid="newReservationValid = true"
- @invalid="newReservationValid = false"
+ @valid="
+ newReservationValid = true;
+ validate();
+ "
+ @invalid="
+ newReservationValid = false;
+ validate();
+ "
/>
<div class="row">
<div class="col">
@@ -48,6 +54,7 @@
v-if="!readonly"
class="action-link"
@click="toggleDetails(data)"
+ :disabled="isReservationInvalid(data.item.key)"
>
Edit
<i class="fa fa-edit" aria-hidden="true"></i>
@@ -67,8 +74,15 @@
:value="row.item"
@input="updatedReservation"
:queues="queues"
+ @valid="removeInvalidReservation(row.item.key)"
+ @invalid="recordInvalidReservation(row.item.key)"
/>
- <b-button size="sm" @click="toggleDetails(row)">Close</b-button>
+ <b-button
+ size="sm"
+ @click="toggleDetails(row)"
+ :disabled="isReservationInvalid(row.item.key)"
+ >Close</b-button
+ >
</b-card>
</template>
</b-table>
@@ -108,7 +122,8 @@ export default {
showingDetails: {},
showNewItemEditor: false,
newReservation: null,
- newReservationValid: false
+ newReservationValid: false,
+ invalidReservations: [] // list of ComputeResourceReservation.key
};
},
computed: {
@@ -153,22 +168,31 @@ export default {
},
isSaveDisabled() {
return !this.newReservationValid;
+ },
+ valid() {
+ return (
+ (!this.showNewItemEditor || this.newReservationValid) &&
+ this.invalidReservations.length === 0
+ );
}
},
created() {},
methods: {
updatedReservation(newValue) {
- const reservationIndex = this.reservations.findIndex(r => r.key === newValue.key);
+ const reservationIndex = this.reservations.findIndex(
+ r => r.key === newValue.key
+ );
this.$emit("updated", newValue, reservationIndex);
},
toggleDetails(row) {
row.toggleDetails();
- this.showingDetails[row.item.key] = !this.showingDetails[
- row.item.key
- ];
+ this.showingDetails[row.item.key] = !this.showingDetails[row.item.key];
},
deleteReservation(reservation) {
- const reservationIndex = this.reservations.findIndex(r => r.key === reservation.key);
+ const reservationIndex = this.reservations.findIndex(
+ r => r.key === reservation.key
+ );
+ this.removeInvalidReservation(reservation.key);
this.$emit("deleted", reservationIndex);
},
addNewReservation() {
@@ -183,6 +207,29 @@ export default {
},
cancelNewReservation() {
this.showNewItemEditor = false;
+ },
+ recordInvalidReservation(reservationKey) {
+ if (this.invalidReservations.indexOf(reservationKey) < 0) {
+ this.invalidReservations.push(reservationKey);
+ }
+ this.validate();
+ },
+ removeInvalidReservation(reservationKey) {
+ const index = this.invalidReservations.indexOf(reservationKey);
+ if (index >= 0) {
+ this.invalidReservations.splice(index, 1);
+ }
+ this.validate();
+ },
+ isReservationInvalid(reservationKey) {
+ return this.invalidReservations.indexOf(reservationKey) >= 0;
+ },
+ validate() {
+ if (this.valid) {
+ this.$emit("valid");
+ } else {
+ this.$emit("invalid");
+ }
}
}
};
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/BaseModel.js b/django_airavata/apps/api/static/django_airavata_api/js/models/BaseModel.js
index f922b5f..de985a4 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/BaseModel.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/BaseModel.js
@@ -1,118 +1,145 @@
-import BaseEnum from './BaseEnum'
+import BaseEnum from "./BaseEnum";
export default class BaseModel {
-
- /**
- * Create and optionally populate fields of a model instance.
- * - fields: an Array of field definitions. Each field definition can either
- * be just the name of the field as a string, or an object with the
- * following properties:
- * - name (required)
- * - type (required: one of 'string', 'boolean', 'number', 'date', or a class reference)
- * - list (optional, boolean)
- * - default (optional, the default value to be used, if not specified then null is used)
- * - data: a data object, typically a deserialized JSON response
- */
- constructor(fields, data={}){
- fields.forEach(fieldDefinition => {
- if (typeof fieldDefinition === 'string') {
- this[fieldDefinition] = this.convertSimpleField(data[fieldDefinition], null);
- } else { // fieldDefinition must be an object
- let fieldName = fieldDefinition.name;
- let fieldType = fieldDefinition.type;
- let fieldIsList = typeof fieldDefinition.list !== 'undefined' ? fieldDefinition.list : false;
- let fieldDefault = typeof fieldDefinition.default !== 'undefined' ? this.getDefaultValue(fieldDefinition.default) : null;
- let fieldValue = data[fieldName];
- if (fieldIsList) {
- this[fieldName] = fieldValue ? fieldValue.map(item => this.convertField(fieldType, item, fieldDefault)) : fieldDefault;
- } else {
- this[fieldName] = this.convertField(fieldType, fieldValue, fieldDefault);
- }
- }
- });
- }
-
- convertField(fieldType, fieldValue, fieldDefault) {
- if (fieldValue === null || typeof fieldValue === 'undefined') {
- return fieldDefault;
- } else if (fieldType === 'string' || fieldType === 'boolean' || fieldType === 'number') {
- return this.convertSimpleField(fieldValue, fieldDefault);
- } else if (fieldType === 'date') {
- return this.convertDateField(fieldValue, fieldDefault);
- } else if (typeof fieldType === 'function') {
- // Assume that it is another BaseModel class
- return this.convertModelField(fieldType, fieldValue, fieldDefault);
+ /**
+ * Create and optionally populate fields of a model instance.
+ * - fields: an Array of field definitions. Each field definition can either
+ * be just the name of the field as a string, or an object with the
+ * following properties:
+ * - name (required)
+ * - type (required: one of 'string', 'boolean', 'number', 'date', or a class reference)
+ * - list (optional, boolean)
+ * - default (optional, the default value to be used, if not specified then null is used)
+ * - data: a data object, typically a deserialized JSON response
+ */
+ constructor(fields, data = {}) {
+ fields.forEach(fieldDefinition => {
+ if (typeof fieldDefinition === "string") {
+ this[fieldDefinition] = this.convertSimpleField(
+ data[fieldDefinition],
+ null
+ );
+ } else {
+ // fieldDefinition must be an object
+ let fieldName = fieldDefinition.name;
+ let fieldType = fieldDefinition.type;
+ let fieldIsList =
+ typeof fieldDefinition.list !== "undefined"
+ ? fieldDefinition.list
+ : false;
+ let fieldDefault =
+ typeof fieldDefinition.default !== "undefined"
+ ? this.getDefaultValue(fieldDefinition.default)
+ : null;
+ let fieldValue = data[fieldName];
+ if (fieldIsList) {
+ this[fieldName] = fieldValue
+ ? fieldValue.map(item =>
+ this.convertField(fieldType, item, fieldDefault)
+ )
+ : fieldDefault;
+ } else {
+ this[fieldName] = this.convertField(
+ fieldType,
+ fieldValue,
+ fieldDefault
+ );
}
- }
+ }
+ });
+ }
- convertSimpleField(fieldValue, fieldDefault) {
- return typeof fieldValue !== 'undefined' ? fieldValue : fieldDefault;
+ convertField(fieldType, fieldValue, fieldDefault) {
+ if (fieldValue === null || typeof fieldValue === "undefined") {
+ return fieldDefault;
+ } else if (
+ fieldType === "string" ||
+ fieldType === "boolean" ||
+ fieldType === "number"
+ ) {
+ return this.convertSimpleField(fieldValue, fieldDefault);
+ } else if (fieldType === "date") {
+ return this.convertDateField(fieldValue, fieldDefault);
+ } else if (typeof fieldType === "function") {
+ // Assume that it is another BaseModel class
+ return this.convertModelField(fieldType, fieldValue, fieldDefault);
}
+ }
- convertDateField(fieldValue, fieldDefault) {
- return typeof fieldValue !== 'undefined' ? new Date(fieldValue) : fieldDefault;
- }
+ convertSimpleField(fieldValue, fieldDefault) {
+ return typeof fieldValue !== "undefined" ? fieldValue : fieldDefault;
+ }
- convertModelField(modelClass, fieldValue, fieldDefault) {
- if (typeof fieldValue !== 'undefined') {
- if (modelClass.prototype instanceof BaseEnum) {
- // When cloning the fieldValue is an enum instance
- if (fieldValue instanceof BaseEnum){
- return fieldValue;
- }
- let enumValue = null;
- if (typeof fieldValue === 'string') {
- // convert by name if type is string
- enumValue = modelClass.byName(fieldValue);
- } else {
- // Otherwise it is an integer that we need to convert to enum
- enumValue = modelClass.byValue(fieldValue);
- }
- if (!enumValue) {
- // enum wasn't found, construct an enum instance from the value
- return new BaseEnum(`Unknown value: ${fieldValue}`, fieldValue);
- } else {
- return enumValue;
- }
- } else if (fieldValue instanceof modelClass) {
- // No conversion necessary, just return the fieldValue
- return fieldValue;
- } else {
- return new modelClass(fieldValue);
- }
- }
- return fieldDefault;
- }
+ convertDateField(fieldValue, fieldDefault) {
+ return typeof fieldValue !== "undefined"
+ ? new Date(fieldValue)
+ : fieldDefault;
+ }
- getDefaultValue(fieldDefault) {
- if (typeof fieldDefault === 'function') {
- return fieldDefault();
+ convertModelField(modelClass, fieldValue, fieldDefault) {
+ if (typeof fieldValue !== "undefined") {
+ if (modelClass.prototype instanceof BaseEnum) {
+ // When cloning the fieldValue is an enum instance
+ if (fieldValue instanceof BaseEnum) {
+ return fieldValue;
+ }
+ let enumValue = null;
+ if (typeof fieldValue === "string") {
+ // convert by name if type is string
+ enumValue = modelClass.byName(fieldValue);
+ } else {
+ // Otherwise it is an integer that we need to convert to enum
+ enumValue = modelClass.byValue(fieldValue);
+ }
+ if (!enumValue) {
+ // enum wasn't found, construct an enum instance from the value
+ return new BaseEnum(`Unknown value: ${fieldValue}`, fieldValue);
} else {
- return fieldDefault;
+ return enumValue;
}
+ } else if (fieldValue instanceof modelClass) {
+ // No conversion necessary, just return the fieldValue
+ return fieldValue;
+ } else {
+ return new modelClass(fieldValue);
+ }
}
+ return fieldDefault;
+ }
- static defaultNewInstance(classRef) {
- return () => new classRef();
+ getDefaultValue(fieldDefault) {
+ if (typeof fieldDefault === "function") {
+ return fieldDefault();
+ } else {
+ return fieldDefault;
}
+ }
- /**
- * Override to provide validation. If there are validation errors this
- * method should return a dictionary where keys are property names and
- * values are an array of error messages.
- */
- validate() {
- return null;
- }
+ static defaultNewInstance(classRef) {
+ return () => new classRef();
+ }
- isEmpty(value) {
- return value === null || (typeof value === 'string' && value.trim() === '');
- }
+ /**
+ * Override to provide validation. If there are validation errors this
+ * method should return a dictionary where keys are property names and
+ * values are an array of error messages.
+ */
+ validate() {
+ return null;
+ }
- /**
- * Return a fully deep cloned instance of this instance.
- */
- clone() {
- return new this.constructor(this);
- }
+ isEmpty(value) {
+ return (
+ value === null ||
+ (typeof value === "string" && value.trim() === "") ||
+ (value instanceof Array && value.length === 0)
+ );
+ }
+
+ /**
+ * Return a fully deep cloned instance of this instance.
+ */
+ clone() {
+ return new this.constructor(this);
+ }
}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ComputeResourceReservation.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ComputeResourceReservation.js
index 9a437b4..c4a8921 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/ComputeResourceReservation.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ComputeResourceReservation.js
@@ -12,13 +12,12 @@ const FIELDS = [
{
name: "startTime",
type: Date,
- default: () => new Date(),
-
+ default: () => new Date()
},
{
name: "endTime",
type: Date,
- default: () => new Date(),
+ default: () => new Date()
}
];
@@ -36,6 +35,12 @@ export default class ComputeResourceReservation extends BaseModel {
validationResults["reservationName"] =
"Please provide the name of this reservation.";
}
+ if (this.startTime > this.endTime) {
+ validationResults["endTime"] = "End time must be later than start time.";
+ }
+ if (this.isEmpty(this.queueNames)) {
+ validationResults["queueNames"] = "Please select at least one queue.";
+ }
return validationResults;
}
}