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


The following commit(s) were added to refs/heads/master by this push:
     new 59c861a  AIRAVATA-3179 switch to cilent side validation for group comp 
prefs
59c861a is described below

commit 59c861a0ba9752a0c2de97fc630bc6fa3253982d
Author: Marcus Christie <[email protected]>
AuthorDate: Fri Nov 1 17:06:18 2019 -0400

    AIRAVATA-3179 switch to cilent side validation for group comp prefs
---
 .../BatchQueueResourcePolicy.vue                   |  88 +++++++-
 .../ComputePreference.vue                          | 235 ++++++++++++++++-----
 django_airavata/apps/api/serializers.py            |   4 +-
 .../js/models/BatchQueueResourcePolicy.js          |  46 +++-
 .../js/models/GroupComputeResourcePreference.js    |  62 +++---
 5 files changed, 331 insertions(+), 104 deletions(-)

diff --git 
a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/BatchQueueResourcePolicy.vue
 
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/BatchQueueResourcePolicy.vue
index 9cf4164..1a3dc11 100644
--- 
a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/BatchQueueResourcePolicy.vue
+++ 
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/BatchQueueResourcePolicy.vue
@@ -1,23 +1,65 @@
 <template>
   <div class="row">
     <div class="col">
-      <b-form-group label="Maximum Allowed Nodes" 
label-for="max-allowed-nodes">
-        <b-form-input id="max-allowed-nodes" type="number" 
v-model="data.maxAllowedNodes" @input="policyUpdated" min="1" 
:max="batchQueue.maxNodes"
-          :formatter="numberFormatter" :placeholder="'Max Nodes: ' + 
batchQueue.maxNodes">
+      <b-form-group
+        label="Maximum Allowed Nodes"
+        label-for="max-allowed-nodes"
+        :invalid-feedback="validationFeedback.maxAllowedNodes.invalidFeedback"
+        :state="validationFeedback.maxAllowedNodes.state"
+      >
+        <b-form-input
+          id="max-allowed-nodes"
+          type="number"
+          v-model="data.maxAllowedNodes"
+          @input="policyUpdated"
+          min="1"
+          :max="batchQueue.maxNodes"
+          :formatter="numberFormatter"
+          :placeholder="'Max Nodes: ' + batchQueue.maxNodes"
+        :state="validationFeedback.maxAllowedNodes.state"
+        >
         </b-form-input>
       </b-form-group>
     </div>
     <div class="col">
-      <b-form-group label="Maximum Allowed Cores" 
label-for="max-allowed-cores">
-        <b-form-input id="max-allowed-cores" type="number" 
v-model="data.maxAllowedCores" @input="policyUpdated" min="1" 
:max="batchQueue.maxProcessors"
-          :formatter="numberFormatter" :placeholder="'Max Cores: ' + 
batchQueue.maxProcessors">
+      <b-form-group
+        label="Maximum Allowed Cores"
+        label-for="max-allowed-cores"
+        :invalid-feedback="validationFeedback.maxAllowedCores.invalidFeedback"
+        :state="validationFeedback.maxAllowedCores.state"
+      >
+        <b-form-input
+          id="max-allowed-cores"
+          type="number"
+          v-model="data.maxAllowedCores"
+          @input="policyUpdated"
+          min="1"
+          :max="batchQueue.maxProcessors"
+          :formatter="numberFormatter"
+          :placeholder="'Max Cores: ' + batchQueue.maxProcessors"
+        :state="validationFeedback.maxAllowedCores.state"
+        >
         </b-form-input>
       </b-form-group>
     </div>
     <div class="col">
-      <b-form-group label="Maximum Allowed Wall Time" 
label-for="max-allowed-walltime">
-        <b-form-input id="max-allowed-walltime" type="number" 
v-model="data.maxAllowedWalltime" @input="policyUpdated" min="1" 
:max="batchQueue.maxRunTime"
-          :formatter="numberFormatter" :placeholder="'Max Wall Time: ' + 
batchQueue.maxRunTime">
+      <b-form-group
+        label="Maximum Allowed Wall Time"
+        label-for="max-allowed-walltime"
+        
:invalid-feedback="validationFeedback.maxAllowedWalltime.invalidFeedback"
+        :state="validationFeedback.maxAllowedWalltime.state"
+      >
+        <b-form-input
+          id="max-allowed-walltime"
+          type="number"
+          v-model="data.maxAllowedWalltime"
+          @input="policyUpdated"
+          min="1"
+          :max="batchQueue.maxRunTime"
+          :formatter="numberFormatter"
+          :placeholder="'Max Wall Time: ' + batchQueue.maxRunTime"
+        :state="validationFeedback.maxAllowedWalltime.state"
+        >
         </b-form-input>
       </b-form-group>
     </div>
@@ -26,6 +68,7 @@
 
 <script>
 import { models } from "django-airavata-api";
+import { errors as uiErrors } from "django-airavata-common-ui";
 
 export default {
   name: "batch-queue-resource-policy",
@@ -39,6 +82,10 @@ export default {
       type: models.BatchQueue
     }
   },
+  created() {
+    this.$on("input", this.validate);
+    this.validate();
+  },
   data: function() {
     const localValue = this.value
       ? this.value.clone()
@@ -62,7 +109,28 @@ export default {
     },
     numberFormatter: function(value) {
       const num = parseInt(value);
-      return !isNaN(num) ? num : null;
+      return !isNaN(num) ? "" + num : value;
+    },
+    validate() {
+      if (this.valid) {
+        this.$emit('valid');
+      } else {
+        this.$emit('invalid');
+      }
+    }
+  },
+  computed: {
+    valid() {
+      return Object.keys(this.validation).length === 0;
+    },
+    validation() {
+      return this.data.validate(this.batchQueue);
+    },
+    validationFeedback() {
+      return uiErrors.ValidationErrors.createValidationFeedback(
+        this.data,
+        this.validation
+      );
     }
   }
 };
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 cf95963..794a552 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
@@ -3,8 +3,14 @@
     <div class="row">
       <div class="col">
         <h1 class="h4 mb-4">
-          <div v-if="localGroupResourceProfile" 
class="group-resource-profile-name text-muted text-uppercase">
-            <i class="fa fa-server" aria-hidden="true"></i> {{ 
localGroupResourceProfile.groupResourceProfileName }}</div>
+          <div
+            v-if="localGroupResourceProfile"
+            class="group-resource-profile-name text-muted text-uppercase"
+          >
+            <i
+              class="fa fa-server"
+              aria-hidden="true"
+            ></i> {{ localGroupResourceProfile.groupResourceProfileName 
}}</div>
           {{ computeResource.hostName }}
         </h1>
       </div>
@@ -13,16 +19,36 @@
       <div class="col">
         <div class="card">
           <div class="card-body">
-            <b-form-group label="Login Username" label-for="login-username">
-              <b-form-input id="login-username" type="text" 
v-model="data.loginUserName">
+            <b-form-group
+              label="Login Username"
+              label-for="login-username"
+              
:invalid-feedback="validationFeedback.loginUserName.invalidFeedback"
+              :state="validationFeedback.loginUserName.state"
+            >
+              <b-form-input
+                id="login-username"
+                type="text"
+                required
+                v-model="data.loginUserName"
+                :state="validationFeedback.loginUserName.state"
+                @input="validate"
+              >
               </b-form-input>
             </b-form-group>
-            <b-form-group label="SSH Credential" 
label-for="credential-store-token">
-              <ssh-credential-selector 
v-model="data.resourceSpecificCredentialStoreToken"
+            <b-form-group
+              label="SSH Credential"
+              label-for="credential-store-token"
+            >
+              <ssh-credential-selector
+                v-model="data.resourceSpecificCredentialStoreToken"
                 v-if="localGroupResourceProfile"
                 
:null-option-default-credential-token="localGroupResourceProfile.defaultCredentialStoreToken"
-                
:null-option-disabled="!localGroupResourceProfile.defaultCredentialStoreToken">
-                <template slot="null-option-label" 
slot-scope="nullOptionLabelScope">
+                
:null-option-disabled="!localGroupResourceProfile.defaultCredentialStoreToken"
+              >
+                <template
+                  slot="null-option-label"
+                  slot-scope="nullOptionLabelScope"
+                >
                   <span v-if="nullOptionLabelScope.defaultCredentialSummary">
                     Use the default SSH credential for {{ 
localGroupResourceProfile.groupResourceProfileName }} ({{
                     nullOptionLabelScope.defaultCredentialSummary.description 
}})
@@ -33,12 +59,31 @@
                 </template>
               </ssh-credential-selector>
             </b-form-group>
-            <b-form-group label="Allocation Project Number" 
label-for="allocation-number">
-              <b-form-input id="allocation-number" type="text" 
v-model="data.allocationProjectNumber">
+            <b-form-group
+              label="Allocation Project Number"
+              label-for="allocation-number"
+            >
+              <b-form-input
+                id="allocation-number"
+                type="text"
+                v-model="data.allocationProjectNumber"
+              >
               </b-form-input>
             </b-form-group>
-            <b-form-group label="Scratch Location" 
label-for="scratch-location" 
:invalid-feedback="validationFeedback.scratchLocation.invalidFeedback" 
:state="validationFeedback.scratchLocation.state">
-              <b-form-input id="scratch-location" type="text" required 
v-model="data.scratchLocation" 
:state="validationFeedback.scratchLocation.state">
+            <b-form-group
+              label="Scratch Location"
+              label-for="scratch-location"
+              
:invalid-feedback="validationFeedback.scratchLocation.invalidFeedback"
+              :state="validationFeedback.scratchLocation.state"
+            >
+              <b-form-input
+                id="scratch-location"
+                type="text"
+                required
+                v-model="data.scratchLocation"
+                :state="validationFeedback.scratchLocation.state"
+                @input="validate"
+              >
               </b-form-input>
             </b-form-group>
           </div>
@@ -50,15 +95,28 @@
         <div class="card">
           <div class="card-body">
             <h5 class="card-title">Policy</h5>
-            <b-form-group label="Allowed Queues" 
v-if="localComputeResourcePolicy">
-              <div v-for="batchQueue in computeResource.batchQueues" 
:key="batchQueue.queueName">
-                <b-form-checkbox 
:checked="localComputeResourcePolicy.allowedBatchQueues.includes(batchQueue.queueName)"
-                  @input="batchQueueChecked(batchQueue, $event)">
+            <b-form-group
+              label="Allowed Queues"
+              v-if="localComputeResourcePolicy"
+            >
+              <div
+                v-for="batchQueue in computeResource.batchQueues"
+                :key="batchQueue.queueName"
+              >
+                <b-form-checkbox
+                  
:checked="localComputeResourcePolicy.allowedBatchQueues.includes(batchQueue.queueName)"
+                  @input="batchQueueChecked(batchQueue, $event)"
+                >
                   {{ batchQueue.queueName }}
                 </b-form-checkbox>
-                <batch-queue-resource-policy 
v-if="localComputeResourcePolicy.allowedBatchQueues.includes(batchQueue.queueName)"
-                  :batch-queue="batchQueue" 
:value="localBatchQueueResourcePolicies.find(pol => pol.queuename === 
batchQueue.queueName)"
-                  @input="updatedBatchQueueResourcePolicy(batchQueue, $event)" 
/>
+                <batch-queue-resource-policy
+                  
v-if="localComputeResourcePolicy.allowedBatchQueues.includes(batchQueue.queueName)"
+                  :batch-queue="batchQueue"
+                  :value="localBatchQueueResourcePolicies.find(pol => 
pol.queuename === batchQueue.queueName)"
+                  @input="updatedBatchQueueResourcePolicy(batchQueue, $event)"
+                  @valid="recordValidBatchQueueResourcePolicy(batchQueue)"
+                  @invalid="recordInvalidBatchQueueResourcePolicy(batchQueue)"
+                />
               </div>
             </b-form-group>
           </div>
@@ -67,9 +125,21 @@
     </div>
     <div class="row">
       <div class="col d-flex justify-content-end">
-        <b-button variant="primary" @click="save">Save</b-button>
-        <b-button class="ml-2" variant="danger" 
@click="remove">Delete</b-button>
-        <b-button class="ml-2" variant="secondary" 
@click="cancel">Cancel</b-button>
+        <b-button
+          variant="primary"
+          @click="save"
+          :disabled="!valid"
+        >Save</b-button>
+        <b-button
+          class="ml-2"
+          variant="danger"
+          @click="remove"
+        >Delete</b-button>
+        <b-button
+          class="ml-2"
+          variant="secondary"
+          @click="cancel"
+        >Cancel</b-button>
       </div>
     </div>
   </div>
@@ -81,7 +151,11 @@ import BatchQueueResourcePolicy from 
"./BatchQueueResourcePolicy.vue";
 import SSHCredentialSelector from 
"../../credentials/SSHCredentialSelector.vue";
 
 import { models, services, errors } from "django-airavata-api";
-import { mixins, notifications, errors as uiErrors } from 
"django-airavata-common-ui";
+import {
+  mixins,
+  notifications,
+  errors as uiErrors
+} from "django-airavata-common-ui";
 
 export default {
   name: "compute-preference",
@@ -135,6 +209,7 @@ export default {
     } else if (!this.computeResourcePolicy) {
       this.createDefaultComputeResourcePolicy(computeResourcePromise);
     }
+    this.$on("input", this.validate);
   },
   data: function() {
     return {
@@ -156,15 +231,29 @@ export default {
         batchQueues: [],
         jobSubmissionInterfaces: []
       },
-      validationErrors: null
+      validationErrors: null,
+      invalidBatchQueueResourcePolicies: []
     };
   },
   computed: {
-
+    groupComputeResourceValidation() {
+      return this.data.validate();
+    },
     validationFeedback() {
       return uiErrors.ValidationErrors.createValidationFeedback(
         this.data,
-        this.validationErrors
+        this.groupComputeResourceValidation
+      );
+    },
+    valid() {
+      return (
+        this.allowedInvalidBatchQueueResourcePolicies.length === 0 &&
+        Object.keys(this.groupComputeResourceValidation).length === 0
+      );
+    },
+    allowedInvalidBatchQueueResourcePolicies() {
+      return this.invalidBatchQueueResourcePolicies.filter(queueName =>
+        this.localComputeResourcePolicy.allowedBatchQueues.includes(queueName)
       );
     }
   },
@@ -234,33 +323,44 @@ export default {
       );
       return this.saveOrUpdate(groupResourceProfile)
         .then(groupResourceProfile => {
-            // Navigate back to GroupResourceProfile with success message
-            this.$router.push({
-              name: "group_resource_preference",
-              params: {
-                value: groupResourceProfile,
-                id: groupResourceProfile.groupResourceProfileId
-              }
-            });
+          // Navigate back to GroupResourceProfile with success message
+          this.$router.push({
+            name: "group_resource_preference",
+            params: {
+              value: groupResourceProfile,
+              id: groupResourceProfile.groupResourceProfileId
+            }
+          });
         })
         .catch(error => {
-
-          if (errors.ErrorUtils.isValidationError(error) && 
'computePreferences' in error.details.response) {
-            const computePreferencesIndex = 
groupResourceProfile.computePreferences.findIndex(cp => cp.computeResourceId 
=== this.host_id);
-            this.validationErrors = 
error.details.response.computePreferences[computePreferencesIndex];
+          if (
+            errors.ErrorUtils.isValidationError(error) &&
+            "computePreferences" in error.details.response
+          ) {
+            const computePreferencesIndex = 
groupResourceProfile.computePreferences.findIndex(
+              cp => cp.computeResourceId === this.host_id
+            );
+            this.validationErrors =
+              error.details.response.computePreferences[
+                computePreferencesIndex
+              ];
           } else {
             this.validationErrors = null;
             notifications.NotificationList.addError(error);
           }
-        })
+        });
     },
     saveOrUpdate(groupResourceProfile) {
       if (this.id) {
-        return DjangoAiravataAPI.services.GroupResourceProfileService
-          .update({ data: groupResourceProfile, lookup: this.id }, { 
ignoreErrors: true })
+        return DjangoAiravataAPI.services.GroupResourceProfileService.update(
+          { data: groupResourceProfile, lookup: this.id },
+          { ignoreErrors: true }
+        );
       } else {
-        return DjangoAiravataAPI.services.GroupResourceProfileService
-          .create({ data: groupResourceProfile }, { ignoreErrors: true })
+        return DjangoAiravataAPI.services.GroupResourceProfileService.create(
+          { data: groupResourceProfile },
+          { ignoreErrors: true }
+        );
       }
     },
     remove: function() {
@@ -269,18 +369,19 @@ export default {
         this.host_id
       );
       if (removedChildren) {
-        DjangoAiravataAPI.services.GroupResourceProfileService
-          .update({ data: groupResourceProfile, lookup: this.id })
-          .then(groupResourceProfile => {
-            // Navigate back to GroupResourceProfile with success message
-            this.$router.push({
-              name: "group_resource_preference",
-              params: {
-                value: groupResourceProfile,
-                id: this.id
-              }
-            });
+        DjangoAiravataAPI.services.GroupResourceProfileService.update({
+          data: groupResourceProfile,
+          lookup: this.id
+        }).then(groupResourceProfile => {
+          // Navigate back to GroupResourceProfile with success message
+          this.$router.push({
+            name: "group_resource_preference",
+            params: {
+              value: groupResourceProfile,
+              id: this.id
+            }
           });
+        });
       } else {
         // Since nothing was removed, just handle this like a cancel
         this.cancel();
@@ -309,6 +410,32 @@ export default {
         );
         this.localComputeResourcePolicy = defaultComputeResourcePolicy;
       });
+    },
+    recordValidBatchQueueResourcePolicy(batchQueue) {
+      if (
+        this.invalidBatchQueueResourcePolicies.includes(batchQueue.queueName)
+      ) {
+        const index = this.invalidBatchQueueResourcePolicies.indexOf(
+          batchQueue.queueName
+        );
+        this.invalidBatchQueueResourcePolicies.splice(index, 1);
+      }
+      this.validate(); // propagate validation
+    },
+    recordInvalidBatchQueueResourcePolicy(batchQueue) {
+      if (
+        !this.invalidBatchQueueResourcePolicies.includes(batchQueue.queueName)
+      ) {
+        this.invalidBatchQueueResourcePolicies.push(batchQueue.queueName);
+      }
+      this.validate(); // propagate validation
+    },
+    validate() {
+      if (this.valid) {
+        this.$emit("valid");
+      } else {
+        this.$emit("invalid");
+      }
     }
   },
   beforeRouteEnter: function(to, from, next) {
diff --git a/django_airavata/apps/api/serializers.py 
b/django_airavata/apps/api/serializers.py
index 5929bb0..58b1470 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -569,9 +569,7 @@ class UserProfileSerializer(
 
 class GroupComputeResourcePreferenceSerializer(
         thrift_utils.create_serializer_class(GroupComputeResourcePreference)):
-
-    class Meta:
-        required = ('scratchLocation',)
+    pass
 
 
 class GroupResourceProfileSerializer(
diff --git 
a/django_airavata/apps/api/static/django_airavata_api/js/models/BatchQueueResourcePolicy.js
 
b/django_airavata/apps/api/static/django_airavata_api/js/models/BatchQueueResourcePolicy.js
index 9f4b0d2..5f95665 100644
--- 
a/django_airavata/apps/api/static/django_airavata_api/js/models/BatchQueueResourcePolicy.js
+++ 
b/django_airavata/apps/api/static/django_airavata_api/js/models/BatchQueueResourcePolicy.js
@@ -1,19 +1,43 @@
-import BaseModel from './BaseModel'
-
+import BaseModel from "./BaseModel";
 
 const FIELDS = [
-     'resourcePolicyId',
-     'computeResourceId',
-     'groupResourceProfileId',
-     'queuename',
-     'maxAllowedNodes',
-     'maxAllowedCores',
-     'maxAllowedWalltime',
+  "resourcePolicyId",
+  "computeResourceId",
+  "groupResourceProfileId",
+  "queuename",
+  "maxAllowedNodes",
+  "maxAllowedCores",
+  "maxAllowedWalltime"
 ];
 
 export default class BatchQueueResourcePolicy extends BaseModel {
+  constructor(data = {}) {
+    super(FIELDS, data);
+  }
 
-    constructor(data = {}) {
-        super(FIELDS, data);
+  validate(batchQueue) {
+    let validationResults = {};
+    if (this.maxAllowedNodes && this.maxAllowedNodes < 1) {
+      validationResults["maxAllowedNodes"] = "Must be at least 1.";
+    } else if (this.maxAllowedNodes > batchQueue.maxNodes) {
+      validationResults[
+        "maxAllowedNodes"
+      ] = `Must be at most ${batchQueue.maxNodes}.`;
+    }
+    if (this.maxAllowedCores && this.maxAllowedCores < 1) {
+      validationResults["maxAllowedCores"] = "Must be at least 1.";
+    } else if (this.maxAllowedCores > batchQueue.maxProcessors) {
+      validationResults[
+        "maxAllowedCores"
+      ] = `Must be at most ${batchQueue.maxProcessors}.`;
+    }
+    if (this.maxAllowedWalltime && this.maxAllowedWalltime < 1) {
+      validationResults["maxAllowedWalltime"] = "Must be at least 1.";
+    } else if (this.maxAllowedWalltime > batchQueue.maxRunTime) {
+      validationResults[
+        "maxAllowedWalltime"
+      ] = `Must be at most ${batchQueue.maxRunTime}.`;
     }
+    return validationResults;
+  }
 }
diff --git 
a/django_airavata/apps/api/static/django_airavata_api/js/models/GroupComputeResourcePreference.js
 
b/django_airavata/apps/api/static/django_airavata_api/js/models/GroupComputeResourcePreference.js
index 1dacbc5..facaa85 100644
--- 
a/django_airavata/apps/api/static/django_airavata_api/js/models/GroupComputeResourcePreference.js
+++ 
b/django_airavata/apps/api/static/django_airavata_api/js/models/GroupComputeResourcePreference.js
@@ -1,34 +1,44 @@
-import BaseModel from './BaseModel'
-
+import BaseModel from "./BaseModel";
 
 const FIELDS = [
-     'computeResourceId',
-     'groupResourceProfileId',
-     {
-         name: 'overridebyAiravata',
-         type: 'boolean',
-         default: true,
-     },
-     'loginUserName',
-     'preferredJobSubmissionProtocol',
-     'preferredDataMovementProtocol',
-     'preferredBatchQueue',
-     'scratchLocation',
-     'allocationProjectNumber',
-     'resourceSpecificCredentialStoreToken',
-     'usageReportingGatewayId',
-     'qualityOfService',
-     'reservation',
-     'reservationStartTime',
-     'reservationEndTime',
-     'sshAccountProvisioner',
-     'groupSSHAccountProvisionerConfigs',
-     'sshAccountProvisionerAdditionalInfo',
+  "computeResourceId",
+  "groupResourceProfileId",
+  {
+    name: "overridebyAiravata",
+    type: "boolean",
+    default: true
+  },
+  "loginUserName",
+  "preferredJobSubmissionProtocol",
+  "preferredDataMovementProtocol",
+  "preferredBatchQueue",
+  "scratchLocation",
+  "allocationProjectNumber",
+  "resourceSpecificCredentialStoreToken",
+  "usageReportingGatewayId",
+  "qualityOfService",
+  "reservation",
+  "reservationStartTime",
+  "reservationEndTime",
+  "sshAccountProvisioner",
+  "groupSSHAccountProvisionerConfigs",
+  "sshAccountProvisionerAdditionalInfo"
 ];
 
 export default class GroupComputeResourcePreference extends BaseModel {
+  constructor(data = {}) {
+    super(FIELDS, data);
+  }
 
-    constructor(data = {}) {
-        super(FIELDS, data);
+  validate() {
+    let validationResults = {};
+    if (this.isEmpty(this.loginUserName)) {
+      validationResults["loginUserName"] =
+        "Please provide a login username.";
+    }
+    if (this.isEmpty(this.scratchLocation)) {
+      validationResults["scratchLocation"] = "Please provide a scratch 
location.";
     }
+    return validationResults;
+  }
 }

Reply via email to