From 3f9cd6bfad9d46e83ad1b370732d80793b5e2872 Mon Sep 17 00:00:00 2001 From: Tore Brede <Tore.Brede@uib.no> Date: Mon, 13 Dec 2021 20:11:08 +0100 Subject: [PATCH] GREG-154: Adding gender field --- frontend/public/locales/en/common.json | 5 +- frontend/public/locales/nb/common.json | 5 +- frontend/public/locales/nn/common.json | 7 +- frontend/src/contexts/featureContext.ts | 3 + frontend/src/providers/featureProvider.tsx | 12 +++- .../routes/guest/register/enteredGuestData.ts | 1 + .../routes/guest/register/guestDataForm.ts | 1 + frontend/src/routes/guest/register/index.tsx | 12 +++- .../routes/guest/register/steps/register.tsx | 47 ++++++++++++- gregui/api/serializers/guest.py | 14 ++++ gregui/api/views/invitation.py | 4 +- gregui/tests/api/views/test_invitation.py | 69 +++++++++++++++++++ 12 files changed, 170 insertions(+), 10 deletions(-) diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 61140646..90cf89f5 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -23,7 +23,10 @@ "passportNumber": "Passport number", "passportNationality": "Passport nationality", "countryCallingCode": "Country code", - "contactPersonUnit": "Contact at unit" + "contactPersonUnit": "Contact at unit", + "gender": "Gender", + "male": "Male", + "female": "Female" }, "sponsor": { "addRole": "Add role", diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json index b9f2d0a7..459ee2b9 100644 --- a/frontend/public/locales/nb/common.json +++ b/frontend/public/locales/nb/common.json @@ -23,7 +23,10 @@ "passportNumber": "Passnummer", "passportNationality": "Passnasjonalitet", "countryCallingCode": "Landkode", - "contactPersonUnit": "Kontakt ved avdeling" + "contactPersonUnit": "Kontakt ved avdeling", + "gender": "Kjønn", + "male": "Mann", + "female": "Kvinne" }, "sponsor": { "addRole": "Legg til rolle", diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json index 60fc0cc0..7e6fb203 100644 --- a/frontend/public/locales/nn/common.json +++ b/frontend/public/locales/nn/common.json @@ -24,10 +24,13 @@ "passportNumber": "Passnummer", "passportNationality": "Passnasjonalitet", "countryCallingCode": "Landkode", - "contactPersonUnit": "Kontakt ved avdeling" + "contactPersonUnit": "Kontakt ved avdeling", + "gender": "Kjønn", + "male": "Mann", + "female": "Kvinne" }, "sponsor": { - "addRole": "Legg til role", + "addRole": "Legg til rolle", "roleInfoText": "Her kan du endre på start- og sluttdato for gjesterollen eller avslutte perioden", "choose": "Velg", "details": "Detaljer", diff --git a/frontend/src/contexts/featureContext.ts b/frontend/src/contexts/featureContext.ts index 0fd76a4e..42130084 100644 --- a/frontend/src/contexts/featureContext.ts +++ b/frontend/src/contexts/featureContext.ts @@ -5,11 +5,14 @@ export interface IFeatureContext { displayContactAtUnit: boolean // Controls whether the optional field is shown in the register new guest wizard displayComment: boolean + // Controls whether the gender field is shown for guests + showGenderFieldForGuest: boolean } export const FeatureContext = createContext<IFeatureContext>({ displayContactAtUnit: true, displayComment: true, + showGenderFieldForGuest: true, }) export const useFeatureContext = () => useContext(FeatureContext) diff --git a/frontend/src/providers/featureProvider.tsx b/frontend/src/providers/featureProvider.tsx index 962e46aa..7ac6ee48 100644 --- a/frontend/src/providers/featureProvider.tsx +++ b/frontend/src/providers/featureProvider.tsx @@ -13,12 +13,20 @@ function FeatureProvider(props: FeatureProviderProps) { let features: IFeatureContext switch (appInst) { case 'uib': - features = { displayContactAtUnit: false, displayComment: false } + features = { + displayContactAtUnit: false, + displayComment: false, + showGenderFieldForGuest: true, + } break case 'uio': default: - features = { displayContactAtUnit: true, displayComment: true } + features = { + displayContactAtUnit: true, + displayComment: true, + showGenderFieldForGuest: false, + } break } diff --git a/frontend/src/routes/guest/register/enteredGuestData.ts b/frontend/src/routes/guest/register/enteredGuestData.ts index b9bb8e60..e18db96e 100644 --- a/frontend/src/routes/guest/register/enteredGuestData.ts +++ b/frontend/src/routes/guest/register/enteredGuestData.ts @@ -12,6 +12,7 @@ export type GuestRegisterData = { passportNumber: string passportNationality: string dateOfBirth: Date | null + gender: string | null } export type GuestConsentData = { diff --git a/frontend/src/routes/guest/register/guestDataForm.ts b/frontend/src/routes/guest/register/guestDataForm.ts index c1d35dcd..37b96c4f 100644 --- a/frontend/src/routes/guest/register/guestDataForm.ts +++ b/frontend/src/routes/guest/register/guestDataForm.ts @@ -15,6 +15,7 @@ export type GuestInviteInformation = { role_start: string role_end: string comment?: string + gender?: string feide_id?: string email?: string diff --git a/frontend/src/routes/guest/register/index.tsx b/frontend/src/routes/guest/register/index.tsx index c595bac7..7b148dc2 100644 --- a/frontend/src/routes/guest/register/index.tsx +++ b/frontend/src/routes/guest/register/index.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, useEffect, useRef, useState } from 'react' +import React, { Suspense, useContext, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useHistory } from 'react-router-dom' import { @@ -21,6 +21,7 @@ import AuthenticationMethod from './authenticationMethod' import GuestRegisterStep from './steps/register' import GuestConsentStep from './steps/consent' import GuestSuccessStep from './steps/success' +import { FeatureContext } from '../../../contexts' enum SubmitState { NotSubmitted, @@ -44,6 +45,7 @@ type InvitationData = { passport?: string feide_id?: string date_of_birth?: string + gender?: string } sponsor: { first_name: string @@ -69,6 +71,7 @@ type InvitationData = { export default function GuestRegister() { const { t } = useTranslation(['common']) const history = useHistory() + const { showGenderFieldForGuest } = useContext(FeatureContext) const guestRegisterRef = useRef<GuestRegisterCallableMethods>(null) const guestConsentRef = useRef<GuestRegisterCallableMethods>(null) @@ -133,6 +136,7 @@ export default function GuestRegister() { first_name: data.person.first_name ?? '', last_name: data.person.last_name ?? '', date_of_birth: dateOfBirth, + gender: data.person.gender ?? '', email: data.person.email ?? '', feide_id: data.person.feide_id ?? '', fnr: data.person.fnr ?? '', @@ -173,6 +177,7 @@ export default function GuestRegister() { nationalIdNumber: initialGuestData.fnr ?? '', passportNumber: initialGuestData.passport ?? '', passportNationality: initialGuestData.passportNationality ?? '', + gender: initialGuestData.gender ?? '', }) }, [initialGuestData]) @@ -234,6 +239,11 @@ export default function GuestRegister() { payload.person.consents = consentData.consents } + // Do not expect gender to be set if the field should not be shown + if (showGenderFieldForGuest && registerData.gender) { + payload.person.gender = registerData.gender + } + return payload } diff --git a/frontend/src/routes/guest/register/steps/register.tsx b/frontend/src/routes/guest/register/steps/register.tsx index 9cc70c71..13d15a31 100644 --- a/frontend/src/routes/guest/register/steps/register.tsx +++ b/frontend/src/routes/guest/register/steps/register.tsx @@ -11,6 +11,7 @@ import { SubmitHandler, Controller, useForm } from 'react-hook-form' import React, { forwardRef, Ref, + useContext, useEffect, useImperativeHandle, useState, @@ -30,9 +31,11 @@ import { GuestInviteInformation } from '../guestDataForm' import { GuestRegisterData } from '../enteredGuestData' import { GuestRegisterCallableMethods } from '../guestRegisterCallableMethods' import AuthenticationMethod from '../authenticationMethod' +import { FeatureContext } from '../../../../contexts' interface GuestRegisterProperties { nextHandler(registerData: GuestRegisterData): void + initialGuestData: GuestInviteInformation registerData: GuestRegisterData | null } @@ -46,6 +49,7 @@ const GuestRegisterStep = forwardRef( (props: GuestRegisterProperties, ref: Ref<GuestRegisterCallableMethods>) => { const { i18n, t } = useTranslation(['common']) const { nextHandler, initialGuestData, registerData } = props + const { showGenderFieldForGuest } = useContext(FeatureContext) // For select-components it seems to be easier to tie them to a state // and then handle the updating of the form using this, than to tie the @@ -56,6 +60,7 @@ const GuestRegisterStep = forwardRef( const [passportNationality, setPassportNationality] = useState< string | undefined >(undefined) + const [gender, setGender] = useState<string | undefined>(undefined) const [idErrorState, setIdErrorState] = useState<string>('') console.log('register step registerData', registerData) @@ -151,6 +156,16 @@ const GuestRegisterStep = forwardRef( setValue('mobilePhone', value.target.value) } + const handleGenderChange = (event: SelectChangeEvent) => { + if (event.target.value) { + setGender(event.target.value) + setValue('gender', event.target.value) + } else { + setGender(undefined) + setValue('gender', null) + } + } + const today = new Date() const minBirthDate = subYears(100)(today) const maxBirthDate = subYears(1)(today) @@ -186,7 +201,7 @@ const GuestRegisterStep = forwardRef( <Box sx={{ maxWidth: '30rem' }}> <form onSubmit={onSubmit}> <Stack spacing={2}> - {/* The name is only editable if it is it is not coming from some trusted source */} + {/* The name is only editable if it is not coming from some trusted source */} {initialGuestData.authentication_method !== AuthenticationMethod.Invite ? ( <> @@ -248,6 +263,34 @@ const GuestRegisterStep = forwardRef( </> )} + {showGenderFieldForGuest && ( + <Select + sx={{ + maxHeight: '2.5rem', + minWidth: '5rem', + marginRight: '0.5rem', + }} + labelId="gender-select" + id="gender-select-id" + displayEmpty + onChange={handleGenderChange} + value={gender} + renderValue={(selected: any) => { + if (!selected) { + return t('input.gender') + } + return t(`input.${selected}`) + }} + > + {/*Keep it simple and hardcode the gender values*/} + <MenuItem disabled value=""> + {t('input.gender')} + </MenuItem> + <MenuItem value="male">{t('input.male')}</MenuItem> + <MenuItem value="female">{t('input.female')}</MenuItem> + </Select> + )} + <Controller name="dateOfBirth" control={control} @@ -349,7 +392,7 @@ const GuestRegisterStep = forwardRef( return countryTuple }) .filter( - // A few country codes do no have a country name. Assuming + // A few country codes do not have a country name. Assuming // these are not needed and filtering them out to make the // list look nicer (countryTuple: [CountryCode, string]) => diff --git a/gregui/api/serializers/guest.py b/gregui/api/serializers/guest.py index 471f459d..5198622d 100644 --- a/gregui/api/serializers/guest.py +++ b/gregui/api/serializers/guest.py @@ -10,6 +10,7 @@ from gregui.validation import ( validate_norwegian_national_id_number, ) + # pylint: disable=W0223 class GuestConsentChoiceSerializer(serializers.Serializer): type = serializers.CharField(required=True) @@ -39,6 +40,7 @@ class GuestRegisterSerializer(serializers.ModelSerializer): ) passport = serializers.CharField(required=False) date_of_birth = serializers.DateField(required=False) + gender = serializers.CharField(required=False) consents = GuestConsentChoiceSerializer(required=False, many=True, write_only=True) def update(self, instance, validated_data): @@ -76,6 +78,9 @@ class GuestRegisterSerializer(serializers.ModelSerializer): if "date_of_birth" in validated_data: instance.date_of_birth = validated_data["date_of_birth"] + if "gender" in validated_data: + instance.gender = validated_data["gender"] + consents = validated_data.get("consents", {}) self._handle_consents(person=instance, consents=consents) @@ -118,12 +123,21 @@ class GuestRegisterSerializer(serializers.ModelSerializer): return date_of_birth + def validate_gender(self, gender): + # Looks like the gender choices are enforced by the person model on + # serialization, so need to check that the gender is valid here + if gender not in Person.GenderType: + raise serializers.ValidationError("Unexpected gender value") + + return gender + class Meta: model = Person fields = ( "id", "first_name", "last_name", + "gender", "email", "mobile_phone", "fnr", diff --git a/gregui/api/views/invitation.py b/gregui/api/views/invitation.py index 34b6d4fa..6ac668fe 100644 --- a/gregui/api/views/invitation.py +++ b/gregui/api/views/invitation.py @@ -181,8 +181,9 @@ class InvitedGuestView(GenericAPIView): "passport", "date_of_birth", "consents", + "gender", ] - fields_allowed_to_update_if_feide = ["mobile_phone", "consents"] + fields_allowed_to_update_if_feide = ["mobile_phone", "consents", "gender"] def get(self, request, *args, **kwargs): """ @@ -223,6 +224,7 @@ class InvitedGuestView(GenericAPIView): "fnr": person.fnr and person.fnr.value, "passport": person.passport and person.passport.value, "feide_id": person.feide_id and person.feide_id.value, + "gender": person.gender, }, "sponsor": { "first_name": sponsor.first_name, diff --git a/gregui/tests/api/views/test_invitation.py b/gregui/tests/api/views/test_invitation.py index 0fb8e456..4ea6868e 100644 --- a/gregui/tests/api/views/test_invitation.py +++ b/gregui/tests/api/views/test_invitation.py @@ -524,3 +524,72 @@ def test_post_invited_info_valid_dnumber(client, invited_person): assert response.status_code == status.HTTP_200_OK, response.data person.refresh_from_db() assert person.fnr.value == d_number + + +@pytest.mark.django_db +def test_gender_stored(client, invited_person_verified_nin): + _, invitation_link = invited_person_verified_nin + + session = client.session + session["invite_id"] = str(invitation_link.uuid) + session.save() + + date_of_birth = "1995-10-28" + url = reverse("gregui-v1:invited-info") + data = { + "person": { + "mobile_phone": "+4797543992", + "date_of_birth": date_of_birth, + "gender": "male", + } + } + response = client.post(url, data, format="json") + + assert response.status_code == status.HTTP_200_OK + + person = Person.objects.get() + assert person.gender == "male" + + +@pytest.mark.django_db +def test_gender_blank_allowed(client, invited_person_verified_nin): + _, invitation_link = invited_person_verified_nin + + session = client.session + session["invite_id"] = str(invitation_link.uuid) + session.save() + + date_of_birth = "1995-10-28" + url = reverse("gregui-v1:invited-info") + data = {"person": {"mobile_phone": "+4797543992", "date_of_birth": date_of_birth}} + response = client.post(url, data, format="json") + + assert response.status_code == status.HTTP_200_OK + + person = Person.objects.get() + assert person.gender is None + + +@pytest.mark.django_db +def test_invalid_gender_rejected(client, invited_person_verified_nin): + _, invitation_link = invited_person_verified_nin + + session = client.session + session["invite_id"] = str(invitation_link.uuid) + session.save() + + date_of_birth = "1995-10-28" + url = reverse("gregui-v1:invited-info") + data = { + "person": { + "mobile_phone": "+4797543992", + "date_of_birth": date_of_birth, + "gender": "abcdefg", + } + } + response = client.post(url, data, format="json") + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + person = Person.objects.get() + assert person.gender is None -- GitLab