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 493ed1a58f7f88963d397c4bb6cbe2217b3e829e Author: Marcus Christie <[email protected]> AuthorDate: Fri Apr 15 17:23:22 2022 -0400 AIRAVATA-3565 WIP: Ext User Profile UI with load/saving text and single_choice values --- .../api/static/django_airavata_api/js/index.js | 6 ++ .../js/models/ExtendedUserProfileField.js | 39 ++++++++ .../js/models/ExtendedUserProfileFieldChoice.js | 13 +++ .../js/models/ExtendedUserProfileFieldLink.js | 16 +++ .../js/models/ExtendedUserProfileValue.js | 18 ++++ .../django_airavata_api/js/service_config.js | 10 ++ django_airavata/apps/auth/serializers.py | 2 +- .../js/components/ExtendedUserProfileEditor.vue | 42 ++++++++ .../ExtendedUserProfileSingleChoiceFieldEditor.vue | 45 +++++++++ .../ExtendedUserProfileTextFieldEditor.vue | 31 ++++++ .../js/containers/UserProfileContainer.vue | 12 ++- .../static/django_airavata_auth/js/store/index.js | 2 + .../js/store/modules/extendedUserProfile.js | 108 +++++++++++++++++++++ django_airavata/apps/auth/urls.py | 4 +- 14 files changed, 344 insertions(+), 4 deletions(-) diff --git a/django_airavata/apps/api/static/django_airavata_api/js/index.js b/django_airavata/apps/api/static/django_airavata_api/js/index.js index ca08f1d7..e177e42f 100644 --- a/django_airavata/apps/api/static/django_airavata_api/js/index.js +++ b/django_airavata/apps/api/static/django_airavata_api/js/index.js @@ -120,6 +120,12 @@ const services = { ExperimentStoragePathService: ServiceFactory.service( "ExperimentStoragePaths" ), + ExtendedUserProfileFieldService: ServiceFactory.service( + "ExtendedUserProfileFields" + ), + ExtendedUserProfileValueService: ServiceFactory.service( + "ExtendedUserProfileValues" + ), FullExperimentService: ServiceFactory.service("FullExperiments"), GatewayResourceProfileService: ServiceFactory.service( "GatewayResourceProfile" diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileField.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileField.js new file mode 100644 index 00000000..8b719db1 --- /dev/null +++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileField.js @@ -0,0 +1,39 @@ +import BaseModel from "./BaseModel"; +import ExtendedUserProfileFieldChoice from "./ExtendedUserProfileFieldChoice"; +import ExtendedUserProfileFieldLink from "./ExtendedUserProfileFieldLink"; + +const FIELDS = [ + "id", + "name", + "help_text", + "order", + { + name: "created_date", + type: "date", + }, + { + name: "updated_date", + type: "date", + }, + "field_type", + { + name: "links", + list: true, + type: ExtendedUserProfileFieldLink, + }, + // For user_agreement type + "checkbox_label", + // For single_choice and multi_choice types + { + name: "choices", + list: true, + type: ExtendedUserProfileFieldChoice, + }, + "other", +]; + +export default class ExtendedUserProfileField extends BaseModel { + constructor(data = {}) { + super(FIELDS, data); + } +} diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldChoice.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldChoice.js new file mode 100644 index 00000000..edd0f8a0 --- /dev/null +++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldChoice.js @@ -0,0 +1,13 @@ +import BaseModel from "./BaseModel"; + +const FIELDS = [ + "id", + "display_text", + "order", +]; + +export default class ExtendedUserProfileFieldChoice extends BaseModel { + constructor(data = {}) { + super(FIELDS, data); + } +} diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldLink.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldLink.js new file mode 100644 index 00000000..5d52803a --- /dev/null +++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldLink.js @@ -0,0 +1,16 @@ +import BaseModel from "./BaseModel"; + +const FIELDS = [ + "id", + "label", + "url", + "order", + "display_link", + "display_inline", +]; + +export default class ExtendedUserProfileFieldLink extends BaseModel { + constructor(data = {}) { + super(FIELDS, data); + } +} 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 new file mode 100644 index 00000000..471faec7 --- /dev/null +++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js @@ -0,0 +1,18 @@ +import BaseModel from "./BaseModel"; + +// TODO: do we need this? +const FIELDS = [ + "id", + "value_type", + "ext_user_profile_field", + "text_value", + "choices", + "other_value", + "agreement_value", +]; + +export default class ExtendedUserProfileValue extends BaseModel { + constructor(data = {}) { + super(FIELDS, data); + } +} 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 152a3961..941ae12c 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 @@ -10,6 +10,7 @@ import ExperimentSearchFields from "./models/ExperimentSearchFields"; import ExperimentStatistics from "./models/ExperimentStatistics"; import ExperimentStoragePath from "./models/ExperimentStoragePath"; import ExperimentSummary from "./models/ExperimentSummary"; +import ExtendedUserProfileField from "./models/ExtendedUserProfileField"; import FullExperiment from "./models/FullExperiment"; import GatewayResourceProfile from "./models/GatewayResourceProfile"; import Group from "./models/Group"; @@ -237,6 +238,15 @@ export default { }, }, }, + ExtendedUserProfileFields: { + url: "/auth/extended-user-profile-fields", + viewSet: true, + modelClass: ExtendedUserProfileField, + }, + ExtendedUserProfileValues: { + url: "/auth/extended-user-profile-values", + viewSet: true, + }, FullExperiments: { url: "/api/full-experiments", viewSet: [ diff --git a/django_airavata/apps/auth/serializers.py b/django_airavata/apps/auth/serializers.py index 66dd54f6..61fc0239 100644 --- a/django_airavata/apps/auth/serializers.py +++ b/django_airavata/apps/auth/serializers.py @@ -222,7 +222,7 @@ class ExtendedUserProfileFieldSerializer(serializers.ModelSerializer): class ExtendedUserProfileValueSerializer(serializers.ModelSerializer): text_value = serializers.CharField(required=False, allow_blank=True) - choices = serializers.ListField(child=serializers.IntegerField(), required=False, allow_empty=False, min_length=1) + choices = serializers.ListField(child=serializers.IntegerField(), required=False) other_value = serializers.CharField(required=False, allow_blank=True) agreement_value = serializers.BooleanField(required=False) 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 new file mode 100644 index 00000000..3b1cfea1 --- /dev/null +++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileEditor.vue @@ -0,0 +1,42 @@ +<template> + <b-card> + <template v-for="extendedUserProfileField in extendedUserProfileFields"> + <component + :key="extendedUserProfileField.id" + :is="getEditor(extendedUserProfileField)" + :extended-user-profile-field="extendedUserProfileField" + /> + </template> + </b-card> +</template> + +<script> +import { mapGetters } from "vuex"; +import ExtendedUserProfileSingleChoiceFieldEditor from "./ExtendedUserProfileSingleChoiceFieldEditor.vue"; +import ExtendedUserProfileTextFieldEditor from "./ExtendedUserProfileTextFieldEditor.vue"; +export default { + computed: { + ...mapGetters("extendedUserProfile", ["extendedUserProfileFields"]), + }, + methods: { + getEditor(extendedUserProfileField) { + const fieldTypeEditors = { + text: ExtendedUserProfileTextFieldEditor, + single_choice: ExtendedUserProfileSingleChoiceFieldEditor, + }; + + if (extendedUserProfileField.field_type in fieldTypeEditors) { + return fieldTypeEditors[extendedUserProfileField.field_type]; + } else { + // eslint-disable-next-line no-console + console.error( + "Unexpected field_type", + extendedUserProfileField.field_type + ); + } + }, + }, +}; +</script> + +<style></style> 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 new file mode 100644 index 00000000..bf95019f --- /dev/null +++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileSingleChoiceFieldEditor.vue @@ -0,0 +1,45 @@ +<template> + <b-form-group + :label="extendedUserProfileField.name" + :description="extendedUserProfileField.help_text" + > + <b-form-select v-model="value" :options="options"></b-form-select> + </b-form-group> +</template> + +<script> +import { mapGetters, mapMutations } from "vuex"; +export default { + props: ["extendedUserProfileField"], + computed: { + ...mapGetters("extendedUserProfile", ["getSingleChoiceValue"]), + value: { + get() { + return this.getSingleChoiceValue(this.extendedUserProfileField.id); + }, + set(value) { + this.setSingleChoiceValue({ + value, + id: this.extendedUserProfileField.id, + }); + }, + }, + options() { + return this.extendedUserProfileField && + this.extendedUserProfileField.choices + ? this.extendedUserProfileField.choices.map((choice) => { + return { + value: choice.id, + text: choice.display_text, + }; + }) + : []; + }, + }, + methods: { + ...mapMutations("extendedUserProfile", ["setSingleChoiceValue"]), + }, +}; +</script> + +<style></style> 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 new file mode 100644 index 00000000..8f2b9989 --- /dev/null +++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileTextFieldEditor.vue @@ -0,0 +1,31 @@ +<template> + <b-form-group + :label="extendedUserProfileField.name" + :description="extendedUserProfileField.help_text" + > + <b-form-input v-model="value" /> + </b-form-group> +</template> + +<script> +import { mapGetters, mapMutations } from 'vuex'; +export default { + props: ["extendedUserProfileField"], + computed: { + ...mapGetters('extendedUserProfile', ['getTextValue']), + value: { + get() { + return this.getTextValue(this.extendedUserProfileField.id); + }, + set(value) { + this.setTextValue({value, id: this.extendedUserProfileField.id}) + } + } + }, + methods: { + ...mapMutations('extendedUserProfile', ['setTextValue']), + } +}; +</script> + +<style></style> diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue index 65499d58..4d44405f 100644 --- a/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue +++ b/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue @@ -21,6 +21,9 @@ @save="onSave" @resend-email-verification="handleResendEmailVerification" /> + <!-- TODO: include both forms in the same card --> + <!-- include extended-user-profile-editor if there are extendedUserProfileFields --> + <extended-user-profile-editor v-if="extendedUserProfileFields && extendedUserProfileFields.length > 0"/> <b-link v-if="user && user.complete" class="text-muted small" @@ -34,12 +37,15 @@ import UserProfileEditor from "../components/UserProfileEditor.vue"; import { notifications } from "django-airavata-common-ui"; import { mapActions, mapGetters } from "vuex"; +import ExtendedUserProfileEditor from '../components/ExtendedUserProfileEditor.vue'; export default { - components: { UserProfileEditor }, + components: { UserProfileEditor, ExtendedUserProfileEditor }, name: "user-profile-container", async created() { await this.loadCurrentUser(); + await this.loadExtendedUserProfileFields(); + await this.loadExtendedUserProfileValues(); const queryParams = new URLSearchParams(window.location.search); if (queryParams.has("code")) { @@ -60,6 +66,7 @@ export default { }, computed: { ...mapGetters("userProfile", ["user"]), + ...mapGetters("extendedUserProfile", ["extendedUserProfileFields"]), }, methods: { ...mapActions("userProfile", [ @@ -68,7 +75,10 @@ export default { "updateUser", "resendEmailVerification", ]), + ...mapActions("extendedUserProfile", ["loadExtendedUserProfileFields", "loadExtendedUserProfileValues", "saveExtendedUserProfileValues"]), async onSave() { + // TODO: only save if both standard and extended user profiles are valid + this.saveExtendedUserProfileValues(); if (this.$refs.userProfileEditor.valid) { await this.updateUser(); notifications.NotificationList.add( diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/store/index.js b/django_airavata/apps/auth/static/django_airavata_auth/js/store/index.js index bb13e8ac..8dd262a7 100644 --- a/django_airavata/apps/auth/static/django_airavata_auth/js/store/index.js +++ b/django_airavata/apps/auth/static/django_airavata_auth/js/store/index.js @@ -1,5 +1,6 @@ import Vuex from "vuex"; import userProfile from "./modules/userProfile"; +import extendedUserProfile from "./modules/extendedUserProfile"; const debug = process.env.NODE_ENV !== "production"; @@ -8,6 +9,7 @@ function createStore(Vue) { return new Vuex.Store({ modules: { userProfile, + extendedUserProfile, }, strict: debug, }); 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 e69de29b..85c085d7 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 @@ -0,0 +1,108 @@ +import { services } from "django-airavata-api"; + +const state = () => ({ + extendedUserProfileFields: null, + extendedUserProfileValues: [], +}); + +const getters = { + extendedUserProfileFields: (state) => state.extendedUserProfileFields, + extendedUserProfileValues: (state) => state.extendedUserProfileValues, + getTextValue: (state) => (id) => { + const value = state.extendedUserProfileValues.find( + (v) => v.ext_user_profile_field === id + ); + return value ? value.text_value : null; + }, + getSingleChoiceValue: (state) => (id) => { + const value = state.extendedUserProfileValues.find( + (v) => v.ext_user_profile_field === id + ); + if (value && value.choices && value.choices.length === 1) { + return value.choices[0]; + } else { + return null; + } + }, +}; + +const actions = { + async loadExtendedUserProfileFields({ commit }) { + const extendedUserProfileFields = await services.ExtendedUserProfileFieldService.list(); + commit("setExtendedUserProfileFields", { extendedUserProfileFields }); + }, + async loadExtendedUserProfileValues({ commit }) { + const extendedUserProfileValues = await services.ExtendedUserProfileValueService.list(); + commit("setExtendedUserProfileValues", { extendedUserProfileValues }); + }, + async saveExtendedUserProfileValues({ state, commit }) { + for (const value of state.extendedUserProfileValues) { + // Create or update each value + if (value.id) { + await services.ExtendedUserProfileValueService.update({ + lookup: value.id, + data: value, + }); + } else { + const extendedUserProfileValue = await services.ExtendedUserProfileValueService.create( + { data: value } + ); + commit("updateExtendedUserProfileValue", { extendedUserProfileValue }); + } + } + }, +}; + +const mutations = { + setExtendedUserProfileFields(state, { extendedUserProfileFields }) { + state.extendedUserProfileFields = extendedUserProfileFields; + }, + setExtendedUserProfileValues(state, { extendedUserProfileValues }) { + state.extendedUserProfileValues = extendedUserProfileValues; + }, + setTextValue(state, { value, id }) { + const profileValue = state.extendedUserProfileValues.find( + (v) => v.ext_user_profile_field === id + ); + if (profileValue) { + profileValue.text_value = value; + } else { + state.extendedUserProfileValues.push({ + value_type: "text", + ext_user_profile_field: id, + text_value: value, + }); + } + }, + setSingleChoiceValue(state, { value, id }) { + const profileValue = state.extendedUserProfileValues.find( + (v) => v.ext_user_profile_field === id + ); + if (profileValue) { + profileValue.choices = [value]; + profileValue.other_value = ""; + } else { + state.extendedUserProfileValues.push({ + value_type: "single_choice", + ext_user_profile_field: id, + choices: [value], + }); + } + }, + updateExperimentInputValue(state, { extendedUserProfileValue }) { + const index = state.extendedUserProfileValues.findIndex( + (v) => + v.ext_user_profile_field === + extendedUserProfileValue.ext_user_profile_field + ); + state.extendedUserProfileValues.splice(index, 1, extendedUserProfileValue); + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/django_airavata/apps/auth/urls.py b/django_airavata/apps/auth/urls.py index 8ca2f532..a7622718 100644 --- a/django_airavata/apps/auth/urls.py +++ b/django_airavata/apps/auth/urls.py @@ -7,8 +7,8 @@ from . import views router = routers.DefaultRouter() router.register(r'users', views.UserViewSet, basename='user') -router.register(r'extended-user-profile-fields', views.ExtendedUserProfileFieldViewset, basename='extend-user-profile-field') -router.register(r'extended-user-profile-values', views.ExtendedUserProfileValueViewset, basename='extend-user-profile-value') +router.register(r'extended-user-profile-fields', views.ExtendedUserProfileFieldViewset, basename='extended-user-profile-field') +router.register(r'extended-user-profile-values', views.ExtendedUserProfileValueViewset, basename='extended-user-profile-value') app_name = 'django_airavata_auth' urlpatterns = [ re_path(r'^', include(router.urls)),
