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 2022/05/10 21:30:43 UTC

[airavata-django-portal] 01/04: AIRAVATA-3565 Add validation to extended user profile editor

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch AIRAVATA-3562
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 6d761044053c67f0000b00a5aedda025585f8d61
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue May 10 16:12:44 2022 -0400

    AIRAVATA-3565 Add validation to extended user profile editor
---
 .../js/components/ExtendedUserProfileEditor.vue    |  6 +++
 .../ExtendedUserProfileMultiChoiceFieldEditor.vue  | 39 +++++++++++++++++
 .../ExtendedUserProfileSingleChoiceFieldEditor.vue | 50 +++++++++++++++++++++-
 .../ExtendedUserProfileTextFieldEditor.vue         | 21 ++++++++-
 ...ExtendedUserProfileUserAgreementFieldEditor.vue | 30 ++++++++++++-
 .../js/store/modules/extendedUserProfile.js        |  2 +-
 .../static/common/js/errors/vuelidateHelpers.js    | 14 ++++++
 7 files changed, 158 insertions(+), 4 deletions(-)

diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileEditor.vue
index b224fba2..1f6bcdf8 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileEditor.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileEditor.vue
@@ -2,6 +2,7 @@
   <b-card>
     <template v-for="extendedUserProfileField in extendedUserProfileFields">
       <component
+        ref="extendedUserProfileFieldComponents"
         :key="extendedUserProfileField.id"
         :is="getEditor(extendedUserProfileField)"
         :extended-user-profile-field="extendedUserProfileField"
@@ -19,6 +20,11 @@ import ExtendedUserProfileUserAgreementFieldEditor from "./ExtendedUserProfileUs
 export default {
   computed: {
     ...mapGetters("extendedUserProfile", ["extendedUserProfileFields"]),
+    valid() {
+      return this.$refs.extendedUserProfileFieldComponents.every(
+        (c) => c.valid
+      );
+    },
   },
   methods: {
     getEditor(extendedUserProfileField) {
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileMultiChoiceFieldEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileMultiChoiceFieldEditor.vue
index db45cbb9..8edc55b6 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileMultiChoiceFieldEditor.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileMultiChoiceFieldEditor.vue
@@ -5,25 +5,39 @@
       :options="options"
       stacked
       @change="onChange"
+      :state="validateStateErrorOnly($v.value)"
     >
       <b-form-checkbox :value="otherOptionValue"
         >Other (please specify)</b-form-checkbox
       >
+
+      <b-form-invalid-feedback :state="validateState($v.value)"
+        >This field is required.</b-form-invalid-feedback
+      >
     </b-form-checkbox-group>
     <b-form-input
       class="mt-2"
       v-if="showOther"
       v-model="other"
       placeholder="Please specify"
+      :state="validateState($v.other)"
+      @input="onInput"
     />
+    <b-form-invalid-feedback :state="validateState($v.other)"
+      >Please specify a value for 'Other'.</b-form-invalid-feedback
+    >
   </extended-user-profile-field-editor>
 </template>
 
 <script>
 import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { required } from "vuelidate/lib/validators";
+import { errors } from "django-airavata-common-ui";
 import ExtendedUserProfileFieldEditor from "./ExtendedUserProfileFieldEditor.vue";
 const OTHER_OPTION = new Object(); // sentinel value
 export default {
+  mixins: [validationMixin],
   components: { ExtendedUserProfileFieldEditor },
   props: ["extendedUserProfileField"],
   data() {
@@ -52,6 +66,7 @@ export default {
           value: values,
           id: this.extendedUserProfileField.id,
         });
+        this.$v.value.$touch();
       },
     },
     other: {
@@ -63,6 +78,7 @@ export default {
           value,
           id: this.extendedUserProfileField.id,
         });
+        this.$v.other.$touch();
       },
     },
     showOther() {
@@ -82,6 +98,21 @@ export default {
     otherOptionValue() {
       return OTHER_OPTION;
     },
+    valid() {
+      return !this.$v.$invalid;
+    },
+  },
+  validations() {
+    const validations = {
+      value: {
+        required,
+      },
+      other: {},
+    };
+    if (this.showOther) {
+      validations.other = { required };
+    }
+    return validations;
   },
   methods: {
     ...mapMutations("extendedUserProfile", [
@@ -94,6 +125,14 @@ export default {
         this.other = "";
       }
     },
+    onInput() {
+      // Handle case where initially there is an other value. If the user
+      // deletes the other value, then we still want to keep the other text box
+      // until the user unchecks the other option.
+      this.otherOptionSelected = true;
+    },
+    validateState: errors.vuelidateHelpers.validateState,
+    validateStateErrorOnly: errors.vuelidateHelpers.validateStateErrorOnly,
   },
 };
 </script>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileSingleChoiceFieldEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileSingleChoiceFieldEditor.vue
index 524fee70..0324b753 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileSingleChoiceFieldEditor.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileSingleChoiceFieldEditor.vue
@@ -1,25 +1,48 @@
 <template>
   <extended-user-profile-field-editor v-bind="$props">
-    <b-form-select v-model="value" :options="options" @change="onChange">
+    <b-form-select
+      v-model="value"
+      :options="options"
+      @change="onChange"
+      :state="validateStateErrorOnly($v.value)"
+    >
+      <template #first>
+        <b-form-select-option :value="null" disabled
+          >-- Please select an option --</b-form-select-option
+        >
+      </template>
+
       <b-form-select-option :value="otherOptionValue"
         >Other (please specify)</b-form-select-option
       >
     </b-form-select>
+    <b-form-invalid-feedback :state="validateState($v.value)"
+      >This field is required.</b-form-invalid-feedback
+    >
     <b-form-input
       class="mt-2"
       v-if="showOther"
       v-model="other"
       placeholder="Please specify"
+      :state="validateState($v.other)"
+      @input="onInput"
     />
+    <b-form-invalid-feedback :state="validateState($v.other)"
+      >Please specify a value for 'Other'.</b-form-invalid-feedback
+    >
   </extended-user-profile-field-editor>
 </template>
 
 <script>
 import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { required } from "vuelidate/lib/validators";
+import { errors } from "django-airavata-common-ui";
 import ExtendedUserProfileFieldEditor from "./ExtendedUserProfileFieldEditor.vue";
 const OTHER_OPTION = new Object(); // sentinel value
 
 export default {
+  mixins: [validationMixin],
   components: { ExtendedUserProfileFieldEditor },
   props: ["extendedUserProfileField"],
   data() {
@@ -46,6 +69,7 @@ export default {
             value,
             id: this.extendedUserProfileField.id,
           });
+          this.$v.value.$touch();
         }
       },
     },
@@ -58,6 +82,7 @@ export default {
           value,
           id: this.extendedUserProfileField.id,
         });
+        this.$v.other.$touch();
       },
     },
     showOther() {
@@ -78,6 +103,21 @@ export default {
     otherOptionValue() {
       return OTHER_OPTION;
     },
+    valid() {
+      return !this.$v.$invalid;
+    },
+  },
+  validations() {
+    const validations = {
+      value: {},
+      other: {},
+    };
+    if (this.showOther) {
+      validations.other = { required };
+    } else {
+      validations.value = { required };
+    }
+    return validations;
   },
   methods: {
     ...mapMutations("extendedUserProfile", [
@@ -87,6 +127,14 @@ export default {
     onChange(value) {
       this.otherOptionSelected = value === this.otherOptionValue;
     },
+    onInput() {
+      // Handle case where initially there is an other value. If the user
+      // deletes the other value, then we still want to keep the other text box
+      // until the user unchecks the other option.
+      this.otherOptionSelected = true;
+    },
+    validateState: errors.vuelidateHelpers.validateState,
+    validateStateErrorOnly: errors.vuelidateHelpers.validateStateErrorOnly,
   },
 };
 </script>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileTextFieldEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileTextFieldEditor.vue
index c9a96488..284ac65d 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileTextFieldEditor.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileTextFieldEditor.vue
@@ -1,13 +1,20 @@
 <template>
   <extended-user-profile-field-editor v-bind="$props">
-    <b-form-input v-model="value" />
+    <b-form-input v-model="value" :state="validateState($v.value)" />
+    <b-form-invalid-feedback :state="validateState($v.value)"
+      >This field is required.</b-form-invalid-feedback
+    >
   </extended-user-profile-field-editor>
 </template>
 
 <script>
 import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { required } from "vuelidate/lib/validators";
+import { errors } from "django-airavata-common-ui";
 import ExtendedUserProfileFieldEditor from "./ExtendedUserProfileFieldEditor.vue";
 export default {
+  mixins: [validationMixin],
   components: { ExtendedUserProfileFieldEditor },
   props: ["extendedUserProfileField"],
   computed: {
@@ -18,11 +25,23 @@ export default {
       },
       set(value) {
         this.setTextValue({ value, id: this.extendedUserProfileField.id });
+        this.$v.$touch();
       },
     },
+    valid() {
+      return !this.$v.$invalid;
+    },
+  },
+  validations() {
+    return {
+      value: {
+        required,
+      },
+    };
   },
   methods: {
     ...mapMutations("extendedUserProfile", ["setTextValue"]),
+    validateState: errors.vuelidateHelpers.validateState,
   },
 };
 </script>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileUserAgreementFieldEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileUserAgreementFieldEditor.vue
index f21965be..9d1c6ffa 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileUserAgreementFieldEditor.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileUserAgreementFieldEditor.vue
@@ -1,15 +1,29 @@
 <template>
   <extended-user-profile-field-editor v-bind="$props">
-    <b-form-checkbox v-model="value" :unchecked-value="false">
+    <b-form-checkbox
+      v-model="value"
+      :unchecked-value="false"
+      :value="true"
+      :state="validateStateErrorOnly($v.value)"
+    >
       {{ extendedUserProfileField.checkbox_label }}
     </b-form-checkbox>
+    <b-form-invalid-feedback :state="validateState($v.value)"
+      >This field is required.</b-form-invalid-feedback
+    >
   </extended-user-profile-field-editor>
 </template>
 
 <script>
 import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { errors } from "django-airavata-common-ui";
 import ExtendedUserProfileFieldEditor from "./ExtendedUserProfileFieldEditor.vue";
+
+const mustBeTrue = (value) => value === true;
+
 export default {
+  mixins: [validationMixin],
   components: { ExtendedUserProfileFieldEditor },
   props: ["extendedUserProfileField"],
   computed: {
@@ -23,11 +37,25 @@ export default {
           value,
           id: this.extendedUserProfileField.id,
         });
+        this.$v.value.$touch();
       },
     },
+    valid() {
+      return !this.$v.$invalid;
+    },
+  },
+  validations() {
+    const validations = {
+      value: {
+        mustBeTrue,
+      },
+    };
+    return validations;
   },
   methods: {
     ...mapMutations("extendedUserProfile", ["setUserAgreementValue"]),
+    validateState: errors.vuelidateHelpers.validateState,
+    validateStateErrorOnly: errors.vuelidateHelpers.validateStateErrorOnly,
   },
 };
 </script>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/extendedUserProfile.js b/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/extendedUserProfile.js
index d2163890..6e264490 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/extendedUserProfile.js
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/extendedUserProfile.js
@@ -50,7 +50,7 @@ const getters = {
     const value = state.extendedUserProfileValues.find(
       (v) => v.ext_user_profile_field === id
     );
-    return value && value.agreement_value;
+    return value ? value.agreement_value : false;
   },
 };
 
diff --git a/django_airavata/static/common/js/errors/vuelidateHelpers.js b/django_airavata/static/common/js/errors/vuelidateHelpers.js
index 3f431834..1332e77d 100644
--- a/django_airavata/static/common/js/errors/vuelidateHelpers.js
+++ b/django_airavata/static/common/js/errors/vuelidateHelpers.js
@@ -2,3 +2,17 @@ export function validateState(validation) {
   const { $dirty, $error } = validation;
   return $dirty ? !$error : null;
 }
+
+/**
+ * Return false if there is a validation error, null otherwise.
+ *
+ * This is just like validateState except it doesn't return true when valid
+ * which is useful if you only want to show invalid feedback.
+ *
+ * @param {*} validation
+ * @returns
+ */
+export function validateStateErrorOnly(validation) {
+  const { $dirty, $error } = validation;
+  return $dirty && $error ? false : null;
+}