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;
   }
 }

Reply via email to