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/06/07 21:17:49 UTC

[airavata-django-portal] branch AIRAVATA-3562 updated (75ccc89c -> 09582cf3)

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

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


    from 75ccc89c AIRAVATA-3564 Add button to get to extended user profile editor
     new e0aecfa9 AIRAVATA-3566 Display text and user agreement ext. user profile values in Manage Users
     new f3675511 AIRAVATA-3566 Single and multichoice display of ext. user profile values in Manage Users
     new 09582cf3 AIRAVATA-3566 Refactor code, pushing logic into model

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../components/users/ExtendedUserProfilePanel.vue  |  82 +++++++
 .../src/components/users/UserDetailsContainer.vue  |   3 +
 .../src/store/modules/extendedUserProfile.js       |  11 +
 .../js/models/ExtendedUserProfileValue.js          |   3 +-
 .../django_airavata_api/js/service_config.js       |   3 +
 django_airavata/apps/auth/models.py                |  57 +++++
 django_airavata/apps/auth/serializers.py           |   4 +-
 django_airavata/apps/auth/tests/test_models.py     | 240 +++++++++++++++++++++
 django_airavata/apps/auth/views.py                 |   6 +-
 9 files changed, 403 insertions(+), 6 deletions(-)
 create mode 100644 django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue
 create mode 100644 django_airavata/apps/auth/tests/test_models.py


[airavata-django-portal] 02/03: AIRAVATA-3566 Single and multichoice display of ext. user profile values in Manage Users

Posted by ma...@apache.org.
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 f36755118ff5f45e6de80e26403b972ed2f33048
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue Jun 7 14:51:36 2022 -0400

    AIRAVATA-3566 Single and multichoice display of ext. user profile values in Manage Users
---
 .../components/users/ExtendedUserProfilePanel.vue  | 49 +++++++++++++++++++++-
 1 file changed, 47 insertions(+), 2 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue
index 6d739273..4b4b52a6 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue
@@ -4,12 +4,19 @@
       <template #cell(value)="{ value, item }">
         <i v-if="item.valid" class="fas fa-check text-success"></i>
         <i v-if="!item.valid" class="fas fa-times text-danger"></i>
-        <template v-if="item.type === 'text'">
+        <template v-if="item.type === 'text' || item.type === 'single_choice'">
           {{ value }}
         </template>
         <template v-else-if="item.type === 'user_agreement'">
           <b-checkbox class="ml-2 d-inline" :checked="value" disabled />
         </template>
+        <template v-else-if="item.type === 'multi_choice'">
+          <ul>
+            <li v-for="result in item.value" :key="result">
+              {{ result }}
+            </li>
+          </ul>
+        </template>
       </template>
     </b-table>
   </b-card>
@@ -75,10 +82,48 @@ export default {
       if (value && value.value_type === "user_agreement") {
         return { value: value.user_agreement, valid: value.valid };
       }
+      if (value && value.value_type === "single_choice") {
+        if (value.other_value) {
+          return { value: `Other: ${value.other_value}`, valid: value.valid };
+        } else if (value.choices && value.choices.length === 1) {
+          const displayText = this.getChoiceDisplayText({
+            field,
+            choiceId: value.choices[0],
+          });
+          if (displayText) {
+            return { value: displayText, valid: value.valid };
+          }
+        }
+        return { value: null, valid: value.valid };
+      }
+      if (value && value.value_type === "multi_choice") {
+        const results = [];
+        if (value.choices) {
+          for (const choiceId of value.choices) {
+            const displayText = this.getChoiceDisplayText({ field, choiceId });
+            if (displayText) {
+              results.push(displayText);
+            }
+          }
+        }
+        if (value.other_value) {
+          results.push(`Other: ${value.other_value}`);
+        }
+        return { value: results, valid: value.valid };
+      }
       return { value: null, valid: !field.required };
     },
+    getChoiceDisplayText({ field, choiceId }) {
+      const choice = field.choices.find((c) => c.id === choiceId);
+      return choice ? choice.display_text : null;
+    },
   },
 };
 </script>
 
-<style></style>
+<style scoped>
+ul {
+  display: inline-block;
+  padding-left: 20px;
+}
+</style>


[airavata-django-portal] 01/03: AIRAVATA-3566 Display text and user agreement ext. user profile values in Manage Users

Posted by ma...@apache.org.
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 e0aecfa968bd1592ad094e9f1e3ea9c036958b0d
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue Jun 7 14:23:28 2022 -0400

    AIRAVATA-3566 Display text and user agreement ext. user profile values in Manage Users
---
 .../components/users/ExtendedUserProfilePanel.vue  | 84 ++++++++++++++++++++++
 .../src/components/users/UserDetailsContainer.vue  |  3 +
 .../src/store/modules/extendedUserProfile.js       | 11 +++
 .../js/models/ExtendedUserProfileValue.js          |  1 +
 .../django_airavata_api/js/service_config.js       |  3 +
 django_airavata/apps/auth/serializers.py           | 21 +++++-
 django_airavata/apps/auth/views.py                 |  6 +-
 7 files changed, 125 insertions(+), 4 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue
new file mode 100644
index 00000000..6d739273
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue
@@ -0,0 +1,84 @@
+<template>
+  <b-card header="Extended User Profile">
+    <b-table :items="items" :fields="fields" small borderless>
+      <template #cell(value)="{ value, item }">
+        <i v-if="item.valid" class="fas fa-check text-success"></i>
+        <i v-if="!item.valid" class="fas fa-times text-danger"></i>
+        <template v-if="item.type === 'text'">
+          {{ value }}
+        </template>
+        <template v-else-if="item.type === 'user_agreement'">
+          <b-checkbox class="ml-2 d-inline" :checked="value" disabled />
+        </template>
+      </template>
+    </b-table>
+  </b-card>
+</template>
+
+<script>
+import { models } from "django-airavata-api";
+import { mapActions, mapGetters } from "vuex";
+export default {
+  props: {
+    iamUserProfile: {
+      type: models.IAMUserProfile,
+      required: true,
+    },
+  },
+  created() {
+    this.loadExtendedUserProfileFields();
+    this.loadExtendedUserProfileValues({
+      username: this.iamUserProfile.userId,
+    });
+  },
+  computed: {
+    ...mapGetters("extendedUserProfile", [
+      "extendedUserProfileFields",
+      "extendedUserProfileValues",
+    ]),
+    fields() {
+      return ["name", "value"];
+    },
+    items() {
+      if (this.extendedUserProfileFields && this.extendedUserProfileValues) {
+        const items = [];
+        for (const field of this.extendedUserProfileFields) {
+          const { value, valid } = this.getValue({ field });
+          items.push({
+            name: field.name,
+            value,
+            valid,
+            type: field.field_type,
+          });
+        }
+        return items;
+      } else {
+        return [];
+      }
+    },
+  },
+  methods: {
+    ...mapActions("extendedUserProfile", [
+      "loadExtendedUserProfileFields",
+      "loadExtendedUserProfileValues",
+    ]),
+    getValue({ field }) {
+      if (!this.extendedUserProfileValues) {
+        return null;
+      }
+      const value = this.extendedUserProfileValues.find(
+        (v) => v.ext_user_profile_field === field.id
+      );
+      if (value && value.value_type === "text") {
+        return { value: value.text_value, valid: value.valid };
+      }
+      if (value && value.value_type === "user_agreement") {
+        return { value: value.user_agreement, valid: value.valid };
+      }
+      return { value: null, valid: !field.required };
+    },
+  },
+};
+</script>
+
+<style></style>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
index d735160c..476892b0 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
@@ -25,6 +25,7 @@
         @save="groupsUpdated"
       />
       <user-profile-panel :iamUserProfile="iamUserProfile" />
+      <extended-user-profile-panel :iamUserProfile="iamUserProfile" />
       <external-idp-user-info-panel
         v-if="hasExternalIDPUserInfo"
         :externalIDPUserInfo="localIAMUserProfile.externalIDPUserInfo"
@@ -79,6 +80,7 @@ import ChangeUsernamePanel from "./ChangeUsernamePanel.vue";
 import EditGroupsPanel from "./EditGroupsPanel.vue";
 import ExternalIDPUserInfoPanel from "./ExternalIDPUserInfoPanel.vue";
 import UserProfilePanel from "./UserProfilePanel.vue";
+import ExtendedUserProfilePanel from "./ExtendedUserProfilePanel.vue";
 
 export default {
   name: "user-details-container",
@@ -101,6 +103,7 @@ export default {
     EditGroupsPanel,
     "external-idp-user-info-panel": ExternalIDPUserInfoPanel,
     UserProfilePanel,
+    ExtendedUserProfilePanel,
   },
   data() {
     return {
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/store/modules/extendedUserProfile.js b/django_airavata/apps/admin/static/django_airavata_admin/src/store/modules/extendedUserProfile.js
index 9ca0b917..a058bee1 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/store/modules/extendedUserProfile.js
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/store/modules/extendedUserProfile.js
@@ -2,11 +2,13 @@ import { models, services } from "django-airavata-api";
 
 const state = () => ({
   extendedUserProfileFields: null,
+  extendedUserProfileValues: null,
   deletedExtendedUserProfileFields: [],
 });
 
 const getters = {
   extendedUserProfileFields: (state) => state.extendedUserProfileFields,
+  extendedUserProfileValues: (state) => state.extendedUserProfileValues,
 };
 
 const actions = {
@@ -14,6 +16,12 @@ const actions = {
     const extendedUserProfileFields = await services.ExtendedUserProfileFieldService.list();
     commit("setExtendedUserProfileFields", { extendedUserProfileFields });
   },
+  async loadExtendedUserProfileValues({ commit }, { username }) {
+    const extendedUserProfileValues = await services.ExtendedUserProfileValueService.list(
+      { username }
+    );
+    commit("setExtendedUserProfileValues", { extendedUserProfileValues });
+  },
   async saveExtendedUserProfileFields({ commit, dispatch, state }) {
     let order = 1;
     for (const field of state.extendedUserProfileFields) {
@@ -80,6 +88,9 @@ const mutations = {
   setExtendedUserProfileFields(state, { extendedUserProfileFields }) {
     state.extendedUserProfileFields = extendedUserProfileFields;
   },
+  setExtendedUserProfileValues(state, { extendedUserProfileValues }) {
+    state.extendedUserProfileValues = extendedUserProfileValues;
+  },
   setName(state, { value, field }) {
     setFieldProp(state, field, "name", value);
   },
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js
index 471faec7..2a1d56c9 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js
@@ -9,6 +9,7 @@ const FIELDS = [
   "choices",
   "other_value",
   "agreement_value",
+  "valid",
 ];
 
 export default class ExtendedUserProfileValue extends BaseModel {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
index 941ae12c..bef3bfe0 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
@@ -11,6 +11,7 @@ import ExperimentStatistics from "./models/ExperimentStatistics";
 import ExperimentStoragePath from "./models/ExperimentStoragePath";
 import ExperimentSummary from "./models/ExperimentSummary";
 import ExtendedUserProfileField from "./models/ExtendedUserProfileField";
+import ExtendedUserProfileValue from "./models/ExtendedUserProfileValue";
 import FullExperiment from "./models/FullExperiment";
 import GatewayResourceProfile from "./models/GatewayResourceProfile";
 import Group from "./models/Group";
@@ -246,6 +247,8 @@ export default {
   ExtendedUserProfileValues: {
     url: "/auth/extended-user-profile-values",
     viewSet: true,
+    modelClass: ExtendedUserProfileValue,
+    queryParams: ["username"],
   },
   FullExperiments: {
     url: "/api/full-experiments",
diff --git a/django_airavata/apps/auth/serializers.py b/django_airavata/apps/auth/serializers.py
index 148e93d1..fe538aa7 100644
--- a/django_airavata/apps/auth/serializers.py
+++ b/django_airavata/apps/auth/serializers.py
@@ -230,11 +230,12 @@ class ExtendedUserProfileValueSerializer(serializers.ModelSerializer):
     choices = serializers.ListField(child=serializers.IntegerField(), required=False)
     other_value = serializers.CharField(required=False, allow_blank=True)
     agreement_value = serializers.BooleanField(required=False)
+    valid = serializers.SerializerMethodField()
 
     class Meta:
         model = models.ExtendedUserProfileValue
         fields = ['id', 'value_type', 'ext_user_profile_field', 'text_value',
-                  'choices', 'other_value', 'agreement_value']
+                  'choices', 'other_value', 'agreement_value', 'valid']
         read_only_fields = ['value_type']
 
     def to_representation(self, instance):
@@ -352,3 +353,21 @@ class ExtendedUserProfileValueSerializer(serializers.ModelSerializer):
                 if not ext_user_profile_field.multi_choice.choices.filter(id=choice, deleted=False).exists():
                     raise serializers.ValidationError({'choices': 'Invalid choice.'})
         return attrs
+
+    # TODO: add label and display_value to serializer?
+    def get_valid(self, value: models.ExtendedUserProfileValue):
+        # TODO: move these to the model
+        if value.ext_user_profile_field.required:
+            if value.ext_user_profile_field.field_type == 'text':
+                return value.text.text_value and len(value.text.text_value.strip()) > 0
+            if value.ext_user_profile_field.field_type == 'single_choice':
+                return value.single_choice.choice is not None or (
+                    value.single_choice.other_value and
+                    len(value.single_choice.other_value.strip()) > 0)
+            if value.ext_user_profile_field.field_type == 'multi_choice':
+                return len(value.multi_choice.choices) > 0 or (
+                    value.multi_choice.other_value and
+                    len(value.multi_choice.other_value.strip()) > 0)
+            if value.ext_user_profile_field.field_type == 'user_agreement':
+                return value.user_agreement.agreement_value is True
+        return True
diff --git a/django_airavata/apps/auth/views.py b/django_airavata/apps/auth/views.py
index 6948901d..95520b42 100644
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@ -743,9 +743,9 @@ class ExtendedUserProfileValueViewset(mixins.CreateModelMixin,
         user = self.request.user
         if self.request.is_gateway_admin:
             queryset = models.ExtendedUserProfileValue.objects.all()
-            user = self.request.query_params.get('user')
-            if user is not None:
-                queryset = queryset.filter(user_profile__user_id=user)
+            username = self.request.query_params.get('username')
+            if username is not None:
+                queryset = queryset.filter(user_profile__user__username=username)
         else:
             queryset = user.user_profile.extended_profile.all()
         return queryset


[airavata-django-portal] 03/03: AIRAVATA-3566 Refactor code, pushing logic into model

Posted by ma...@apache.org.
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 09582cf39fef01ac96dcd4d3c300ce78bd01d796
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue Jun 7 17:17:36 2022 -0400

    AIRAVATA-3566 Refactor code, pushing logic into model
---
 .../components/users/ExtendedUserProfilePanel.vue  |  69 +-----
 .../js/models/ExtendedUserProfileValue.js          |   2 +-
 django_airavata/apps/auth/models.py                |  57 +++++
 django_airavata/apps/auth/serializers.py           |  23 +-
 django_airavata/apps/auth/tests/test_models.py     | 240 +++++++++++++++++++++
 5 files changed, 311 insertions(+), 80 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue
index 4b4b52a6..c3e376c1 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue
@@ -2,21 +2,17 @@
   <b-card header="Extended User Profile">
     <b-table :items="items" :fields="fields" small borderless>
       <template #cell(value)="{ value, item }">
-        <i v-if="item.valid" class="fas fa-check text-success"></i>
+        <!-- only show a valid checkmark when there is a user provided value -->
+        <i v-if="value && item.valid" class="fas fa-check text-success"></i>
         <i v-if="!item.valid" class="fas fa-times text-danger"></i>
-        <template v-if="item.type === 'text' || item.type === 'single_choice'">
-          {{ value }}
-        </template>
-        <template v-else-if="item.type === 'user_agreement'">
-          <b-checkbox class="ml-2 d-inline" :checked="value" disabled />
-        </template>
-        <template v-else-if="item.type === 'multi_choice'">
+        <template v-if="Array.isArray(value)">
           <ul>
-            <li v-for="result in item.value" :key="result">
+            <li v-for="result in value" :key="result">
               {{ result }}
             </li>
           </ul>
         </template>
+        <template v-else> {{ value }} </template>
       </template>
     </b-table>
   </b-card>
@@ -50,12 +46,12 @@ export default {
       if (this.extendedUserProfileFields && this.extendedUserProfileValues) {
         const items = [];
         for (const field of this.extendedUserProfileFields) {
-          const { value, valid } = this.getValue({ field });
+          const value = this.getValue(field);
           items.push({
             name: field.name,
-            value,
-            valid,
-            type: field.field_type,
+            value: value ? value.value_display : null,
+            // if no value, consider it invalid only if it is required
+            valid: value ? value.valid : !field.required,
           });
         }
         return items;
@@ -69,53 +65,10 @@ export default {
       "loadExtendedUserProfileFields",
       "loadExtendedUserProfileValues",
     ]),
-    getValue({ field }) {
-      if (!this.extendedUserProfileValues) {
-        return null;
-      }
-      const value = this.extendedUserProfileValues.find(
+    getValue(field) {
+      return this.extendedUserProfileValues.find(
         (v) => v.ext_user_profile_field === field.id
       );
-      if (value && value.value_type === "text") {
-        return { value: value.text_value, valid: value.valid };
-      }
-      if (value && value.value_type === "user_agreement") {
-        return { value: value.user_agreement, valid: value.valid };
-      }
-      if (value && value.value_type === "single_choice") {
-        if (value.other_value) {
-          return { value: `Other: ${value.other_value}`, valid: value.valid };
-        } else if (value.choices && value.choices.length === 1) {
-          const displayText = this.getChoiceDisplayText({
-            field,
-            choiceId: value.choices[0],
-          });
-          if (displayText) {
-            return { value: displayText, valid: value.valid };
-          }
-        }
-        return { value: null, valid: value.valid };
-      }
-      if (value && value.value_type === "multi_choice") {
-        const results = [];
-        if (value.choices) {
-          for (const choiceId of value.choices) {
-            const displayText = this.getChoiceDisplayText({ field, choiceId });
-            if (displayText) {
-              results.push(displayText);
-            }
-          }
-        }
-        if (value.other_value) {
-          results.push(`Other: ${value.other_value}`);
-        }
-        return { value: results, valid: value.valid };
-      }
-      return { value: null, valid: !field.required };
-    },
-    getChoiceDisplayText({ field, choiceId }) {
-      const choice = field.choices.find((c) => c.id === choiceId);
-      return choice ? choice.display_text : null;
     },
   },
 };
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js
index 2a1d56c9..bb417a6d 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js
@@ -1,6 +1,5 @@
 import BaseModel from "./BaseModel";
 
-// TODO: do we need this?
 const FIELDS = [
   "id",
   "value_type",
@@ -10,6 +9,7 @@ const FIELDS = [
   "other_value",
   "agreement_value",
   "valid",
+  "value_display",
 ];
 
 export default class ExtendedUserProfileValue extends BaseModel {
diff --git a/django_airavata/apps/auth/models.py b/django_airavata/apps/auth/models.py
index 8d7cbb41..3bae9e25 100644
--- a/django_airavata/apps/auth/models.py
+++ b/django_airavata/apps/auth/models.py
@@ -265,6 +265,63 @@ class ExtendedUserProfileValue(models.Model):
         else:
             raise Exception("Could not determine value_type")
 
+    @property
+    def value_display(self):
+        if self.value_type == 'text':
+            return self.text.text_value
+        elif self.value_type == 'single_choice':
+            if self.single_choice.choice:
+                try:
+                    choice = self.ext_user_profile_field.single_choice.choices.get(id=self.single_choice.choice)
+                    return choice.display_text
+                except ExtendedUserProfileSingleChoiceFieldChoice.DoesNotExist:
+                    return None
+            elif self.single_choice.other_value:
+                return f"Other: {self.single_choice.other_value}"
+        elif self.value_type == 'multi_choice':
+            result = []
+            if self.multi_choice.choices:
+                mc_field = self.ext_user_profile_field.multi_choice
+                for choice_value in self.multi_choice.choices.all():
+                    try:
+                        choice = mc_field.choices.get(id=choice_value.value)
+                        result.append(choice.display_text)
+                    except ExtendedUserProfileMultiChoiceFieldChoice.DoesNotExist:
+                        continue
+            if self.multi_choice.other_value:
+                result.append(f"Other: {self.multi_choice.other_value}")
+            return result
+        elif self.value_type == 'user_agreement':
+            if self.user_agreement.agreement_value:
+                return "Yes"
+            else:
+                return "No"
+        return None
+
+    @property
+    def valid(self):
+        if self.ext_user_profile_field.required:
+            if self.value_type == 'text':
+                return self.text.text_value and len(self.text.text_value.strip()) > 0
+            if self.value_type == 'single_choice':
+                choice_exists = (self.single_choice.choice and
+                                 self.ext_user_profile_field.single_choice.choices
+                                 .filter(id=self.single_choice.choice).exists())
+                has_other = (self.ext_user_profile_field.single_choice.other and
+                             self.single_choice.other_value and
+                             len(self.single_choice.other_value.strip()) > 0)
+                return choice_exists or has_other
+            if self.value_type == 'multi_choice':
+                choice_ids = list(map(lambda c: c.value, self.multi_choice.choices.all()))
+                choice_exists = self.ext_user_profile_field.multi_choice.choices.filter(id__in=choice_ids).exists()
+                has_other = (self.ext_user_profile_field.multi_choice.other and
+                             self.multi_choice.other_value and
+                             len(self.multi_choice.other_value.strip()) > 0)
+                return choice_exists or has_other
+            if self.value_type == 'user_agreement':
+                return self.user_agreement.agreement_value is True
+        return True
+
 
 class ExtendedUserProfileTextValue(ExtendedUserProfileValue):
     value_ptr = models.OneToOneField(ExtendedUserProfileValue,
diff --git a/django_airavata/apps/auth/serializers.py b/django_airavata/apps/auth/serializers.py
index fe538aa7..dbafa415 100644
--- a/django_airavata/apps/auth/serializers.py
+++ b/django_airavata/apps/auth/serializers.py
@@ -230,13 +230,12 @@ class ExtendedUserProfileValueSerializer(serializers.ModelSerializer):
     choices = serializers.ListField(child=serializers.IntegerField(), required=False)
     other_value = serializers.CharField(required=False, allow_blank=True)
     agreement_value = serializers.BooleanField(required=False)
-    valid = serializers.SerializerMethodField()
 
     class Meta:
         model = models.ExtendedUserProfileValue
         fields = ['id', 'value_type', 'ext_user_profile_field', 'text_value',
-                  'choices', 'other_value', 'agreement_value', 'valid']
-        read_only_fields = ['value_type']
+                  'choices', 'other_value', 'agreement_value', 'valid', 'value_display']
+        read_only_fields = ['value_type', 'value_display']
 
     def to_representation(self, instance):
         result = super().to_representation(instance)
@@ -353,21 +352,3 @@ class ExtendedUserProfileValueSerializer(serializers.ModelSerializer):
                 if not ext_user_profile_field.multi_choice.choices.filter(id=choice, deleted=False).exists():
                     raise serializers.ValidationError({'choices': 'Invalid choice.'})
         return attrs
-
-    # TODO: add label and display_value to serializer?
-    def get_valid(self, value: models.ExtendedUserProfileValue):
-        # TODO: move these to the model
-        if value.ext_user_profile_field.required:
-            if value.ext_user_profile_field.field_type == 'text':
-                return value.text.text_value and len(value.text.text_value.strip()) > 0
-            if value.ext_user_profile_field.field_type == 'single_choice':
-                return value.single_choice.choice is not None or (
-                    value.single_choice.other_value and
-                    len(value.single_choice.other_value.strip()) > 0)
-            if value.ext_user_profile_field.field_type == 'multi_choice':
-                return len(value.multi_choice.choices) > 0 or (
-                    value.multi_choice.other_value and
-                    len(value.multi_choice.other_value.strip()) > 0)
-            if value.ext_user_profile_field.field_type == 'user_agreement':
-                return value.user_agreement.agreement_value is True
-        return True
diff --git a/django_airavata/apps/auth/tests/test_models.py b/django_airavata/apps/auth/tests/test_models.py
new file mode 100644
index 00000000..73b18331
--- /dev/null
+++ b/django_airavata/apps/auth/tests/test_models.py
@@ -0,0 +1,240 @@
+from django.contrib.auth import get_user_model
+from django.test import TestCase
+
+from django_airavata.apps.auth import models
+
+
+class ExtendedUserProfileValueTestCase(TestCase):
+
+    def setUp(self) -> None:
+        User = get_user_model()
+        user = User.objects.create_user("testuser")
+        self.user_profile = models.UserProfile.objects.create(user=user)
+
+    def test_value_display_of_text_value(self):
+        field = models.ExtendedUserProfileTextField.objects.create(
+            name="test", order=1)
+        value = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            text_value="Some random answer.")
+        self.assertEqual(value.value_display, value.text_value)
+
+    def test_value_display_of_single_choice_with_choice(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        choice_two = field.choices.get(display_text="Choice #2")
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            choice=choice_two.id)
+        self.assertEqual(value.value_display, choice_two.display_text)
+
+    def test_value_display_of_single_choice_with_non_existent_choice(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            choice=-1)
+        self.assertEqual(value.value_display, None)
+
+    def test_value_display_of_single_choice_with_other(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            other_value="Write-in value")
+        self.assertEqual(value.value_display, "Other: Write-in value")
+
+    def test_value_display_of_multi_choice_with_choices(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1)
+        choice_one = field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        choice_three = field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile)
+        value.choices.create(value=choice_one.id)
+        value.choices.create(value=choice_three.id)
+        self.assertListEqual(value.value_display, [choice_one.display_text, choice_three.display_text])
+
+    def test_value_display_of_multi_choice_with_other(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            other_value="Some write-in value.")
+        self.assertListEqual(value.value_display, ["Other: Some write-in value."])
+
+    def test_value_display_of_multi_choice_with_choices_and_other(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1)
+        choice_one = field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        choice_three = field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            other_value="Some write-in value.")
+        value.choices.create(value=choice_one.id)
+        value.choices.create(value=choice_three.id)
+        self.assertListEqual(value.value_display, [choice_one.display_text, choice_three.display_text, "Other: Some write-in value."])
+
+    def test_value_display_of_multi_choice_with_non_existent_choices(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1)
+        choice_one = field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        choice_three = field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile)
+        value.choices.create(value=choice_one.id)
+        value.choices.create(value=choice_three.id)
+        value.choices.create(value=-1)
+        self.assertListEqual(value.value_display, [choice_one.display_text, choice_three.display_text])
+
+    def test_value_display_of_user_agreement_with_no(self):
+        field = models.ExtendedUserProfileAgreementField.objects.create(
+            name="test", order=1)
+        value = models.ExtendedUserProfileAgreementValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            agreement_value=False)
+        self.assertEqual(value.value_display, "No")
+
+    def test_value_display_of_user_agreement_with_yes(self):
+        field = models.ExtendedUserProfileAgreementField.objects.create(
+            name="test", order=1)
+        value = models.ExtendedUserProfileAgreementValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            agreement_value=True)
+        self.assertEqual(value.value_display, "Yes")
+
+    def test_valid_of_text(self):
+        field = models.ExtendedUserProfileTextField.objects.create(
+            name="test", order=1, required=True)
+        value = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            text_value="Some value")
+        self.assertTrue(value.valid)
+
+    def test_valid_of_text_empty(self):
+        field = models.ExtendedUserProfileTextField.objects.create(
+            name="test", order=1, required=True)
+        value = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            text_value="")
+        self.assertFalse(value.valid)
+
+    def test_valid_of_text_empty_no_required(self):
+        field = models.ExtendedUserProfileTextField.objects.create(
+            name="test", order=1, required=False)
+        value = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            text_value="")
+        self.assertTrue(value.valid)
+
+    def test_valid_of_single_choice_none(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1, required=True)
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile)
+        self.assertFalse(value.valid)
+
+    def test_valid_of_single_choice_with_choice(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1, required=True)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        choice_two = field.choices.get(display_text="Choice #2")
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            choice=choice_two.id)
+        self.assertTrue(value.valid)
+
+    def test_valid_of_single_choice_with_non_existent_choice(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1, required=True)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            choice=-1)
+        self.assertFalse(value.valid)
+
+    def test_valid_of_single_choice_with_other(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1, required=True, other=True)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            other_value="Some write-in value.")
+        self.assertTrue(value.valid)
+
+    def test_valid_of_single_choice_with_other_but_not_allowed(self):
+        # Configure field so that 'Other' isn't an option
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1, required=True, other=False)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            other_value="Some write-in value.")
+        self.assertFalse(value.valid)
+
+    def test_valid_of_multi_choice_with_none(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1, required=True)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile)
+        self.assertFalse(value.valid)
+
+    def test_valid_of_multi_choice_with_some(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1, required=True)
+        choice_one = field.choices.create(display_text="Choice #1", order=1)
+        choice_two = field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile)
+        value.choices.create(value=choice_one.id)
+        value.choices.create(value=choice_two.id)
+        self.assertTrue(value.valid)
+
+    def test_valid_of_multi_choice_with_non_existent_choice(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1, required=True)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile)
+        value.choices.create(value=-1)
+        self.assertFalse(value.valid)
+
+    def test_valid_of_multi_choice_with_other(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1, required=True, other=True)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            other_value="Some write-in value.")
+        self.assertTrue(value.valid)