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 <machris...@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; } }