Skip to content
Snippets Groups Projects
Commit 3f9cd6bf authored by Tore.Brede's avatar Tore.Brede
Browse files

GREG-154: Adding gender field

parent ac18be18
No related branches found
No related tags found
1 merge request!205GREG-154: Add gender field
Pipeline #104172 failed
Showing with 170 additions and 10 deletions
...@@ -23,7 +23,10 @@ ...@@ -23,7 +23,10 @@
"passportNumber": "Passport number", "passportNumber": "Passport number",
"passportNationality": "Passport nationality", "passportNationality": "Passport nationality",
"countryCallingCode": "Country code", "countryCallingCode": "Country code",
"contactPersonUnit": "Contact at unit" "contactPersonUnit": "Contact at unit",
"gender": "Gender",
"male": "Male",
"female": "Female"
}, },
"sponsor": { "sponsor": {
"addRole": "Add role", "addRole": "Add role",
......
...@@ -23,7 +23,10 @@ ...@@ -23,7 +23,10 @@
"passportNumber": "Passnummer", "passportNumber": "Passnummer",
"passportNationality": "Passnasjonalitet", "passportNationality": "Passnasjonalitet",
"countryCallingCode": "Landkode", "countryCallingCode": "Landkode",
"contactPersonUnit": "Kontakt ved avdeling" "contactPersonUnit": "Kontakt ved avdeling",
"gender": "Kjønn",
"male": "Mann",
"female": "Kvinne"
}, },
"sponsor": { "sponsor": {
"addRole": "Legg til rolle", "addRole": "Legg til rolle",
......
...@@ -24,10 +24,13 @@ ...@@ -24,10 +24,13 @@
"passportNumber": "Passnummer", "passportNumber": "Passnummer",
"passportNationality": "Passnasjonalitet", "passportNationality": "Passnasjonalitet",
"countryCallingCode": "Landkode", "countryCallingCode": "Landkode",
"contactPersonUnit": "Kontakt ved avdeling" "contactPersonUnit": "Kontakt ved avdeling",
"gender": "Kjønn",
"male": "Mann",
"female": "Kvinne"
}, },
"sponsor": { "sponsor": {
"addRole": "Legg til role", "addRole": "Legg til rolle",
"roleInfoText": "Her kan du endre på start- og sluttdato for gjesterollen eller avslutte perioden", "roleInfoText": "Her kan du endre på start- og sluttdato for gjesterollen eller avslutte perioden",
"choose": "Velg", "choose": "Velg",
"details": "Detaljer", "details": "Detaljer",
......
...@@ -5,11 +5,14 @@ export interface IFeatureContext { ...@@ -5,11 +5,14 @@ export interface IFeatureContext {
displayContactAtUnit: boolean displayContactAtUnit: boolean
// Controls whether the optional field is shown in the register new guest wizard // Controls whether the optional field is shown in the register new guest wizard
displayComment: boolean displayComment: boolean
// Controls whether the gender field is shown for guests
showGenderFieldForGuest: boolean
} }
export const FeatureContext = createContext<IFeatureContext>({ export const FeatureContext = createContext<IFeatureContext>({
displayContactAtUnit: true, displayContactAtUnit: true,
displayComment: true, displayComment: true,
showGenderFieldForGuest: true,
}) })
export const useFeatureContext = () => useContext(FeatureContext) export const useFeatureContext = () => useContext(FeatureContext)
...@@ -13,12 +13,20 @@ function FeatureProvider(props: FeatureProviderProps) { ...@@ -13,12 +13,20 @@ function FeatureProvider(props: FeatureProviderProps) {
let features: IFeatureContext let features: IFeatureContext
switch (appInst) { switch (appInst) {
case 'uib': case 'uib':
features = { displayContactAtUnit: false, displayComment: false } features = {
displayContactAtUnit: false,
displayComment: false,
showGenderFieldForGuest: true,
}
break break
case 'uio': case 'uio':
default: default:
features = { displayContactAtUnit: true, displayComment: true } features = {
displayContactAtUnit: true,
displayComment: true,
showGenderFieldForGuest: false,
}
break break
} }
......
...@@ -12,6 +12,7 @@ export type GuestRegisterData = { ...@@ -12,6 +12,7 @@ export type GuestRegisterData = {
passportNumber: string passportNumber: string
passportNationality: string passportNationality: string
dateOfBirth: Date | null dateOfBirth: Date | null
gender: string | null
} }
export type GuestConsentData = { export type GuestConsentData = {
......
...@@ -15,6 +15,7 @@ export type GuestInviteInformation = { ...@@ -15,6 +15,7 @@ export type GuestInviteInformation = {
role_start: string role_start: string
role_end: string role_end: string
comment?: string comment?: string
gender?: string
feide_id?: string feide_id?: string
email?: string email?: string
......
import React, { Suspense, useEffect, useRef, useState } from 'react' import React, { Suspense, useContext, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useHistory } from 'react-router-dom' import { useHistory } from 'react-router-dom'
import { import {
...@@ -21,6 +21,7 @@ import AuthenticationMethod from './authenticationMethod' ...@@ -21,6 +21,7 @@ import AuthenticationMethod from './authenticationMethod'
import GuestRegisterStep from './steps/register' import GuestRegisterStep from './steps/register'
import GuestConsentStep from './steps/consent' import GuestConsentStep from './steps/consent'
import GuestSuccessStep from './steps/success' import GuestSuccessStep from './steps/success'
import { FeatureContext } from '../../../contexts'
enum SubmitState { enum SubmitState {
NotSubmitted, NotSubmitted,
...@@ -44,6 +45,7 @@ type InvitationData = { ...@@ -44,6 +45,7 @@ type InvitationData = {
passport?: string passport?: string
feide_id?: string feide_id?: string
date_of_birth?: string date_of_birth?: string
gender?: string
} }
sponsor: { sponsor: {
first_name: string first_name: string
...@@ -69,6 +71,7 @@ type InvitationData = { ...@@ -69,6 +71,7 @@ type InvitationData = {
export default function GuestRegister() { export default function GuestRegister() {
const { t } = useTranslation(['common']) const { t } = useTranslation(['common'])
const history = useHistory() const history = useHistory()
const { showGenderFieldForGuest } = useContext(FeatureContext)
const guestRegisterRef = useRef<GuestRegisterCallableMethods>(null) const guestRegisterRef = useRef<GuestRegisterCallableMethods>(null)
const guestConsentRef = useRef<GuestRegisterCallableMethods>(null) const guestConsentRef = useRef<GuestRegisterCallableMethods>(null)
...@@ -133,6 +136,7 @@ export default function GuestRegister() { ...@@ -133,6 +136,7 @@ export default function GuestRegister() {
first_name: data.person.first_name ?? '', first_name: data.person.first_name ?? '',
last_name: data.person.last_name ?? '', last_name: data.person.last_name ?? '',
date_of_birth: dateOfBirth, date_of_birth: dateOfBirth,
gender: data.person.gender ?? '',
email: data.person.email ?? '', email: data.person.email ?? '',
feide_id: data.person.feide_id ?? '', feide_id: data.person.feide_id ?? '',
fnr: data.person.fnr ?? '', fnr: data.person.fnr ?? '',
...@@ -173,6 +177,7 @@ export default function GuestRegister() { ...@@ -173,6 +177,7 @@ export default function GuestRegister() {
nationalIdNumber: initialGuestData.fnr ?? '', nationalIdNumber: initialGuestData.fnr ?? '',
passportNumber: initialGuestData.passport ?? '', passportNumber: initialGuestData.passport ?? '',
passportNationality: initialGuestData.passportNationality ?? '', passportNationality: initialGuestData.passportNationality ?? '',
gender: initialGuestData.gender ?? '',
}) })
}, [initialGuestData]) }, [initialGuestData])
...@@ -234,6 +239,11 @@ export default function GuestRegister() { ...@@ -234,6 +239,11 @@ export default function GuestRegister() {
payload.person.consents = consentData.consents 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 return payload
} }
......
...@@ -11,6 +11,7 @@ import { SubmitHandler, Controller, useForm } from 'react-hook-form' ...@@ -11,6 +11,7 @@ import { SubmitHandler, Controller, useForm } from 'react-hook-form'
import React, { import React, {
forwardRef, forwardRef,
Ref, Ref,
useContext,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useState, useState,
...@@ -30,9 +31,11 @@ import { GuestInviteInformation } from '../guestDataForm' ...@@ -30,9 +31,11 @@ import { GuestInviteInformation } from '../guestDataForm'
import { GuestRegisterData } from '../enteredGuestData' import { GuestRegisterData } from '../enteredGuestData'
import { GuestRegisterCallableMethods } from '../guestRegisterCallableMethods' import { GuestRegisterCallableMethods } from '../guestRegisterCallableMethods'
import AuthenticationMethod from '../authenticationMethod' import AuthenticationMethod from '../authenticationMethod'
import { FeatureContext } from '../../../../contexts'
interface GuestRegisterProperties { interface GuestRegisterProperties {
nextHandler(registerData: GuestRegisterData): void nextHandler(registerData: GuestRegisterData): void
initialGuestData: GuestInviteInformation initialGuestData: GuestInviteInformation
registerData: GuestRegisterData | null registerData: GuestRegisterData | null
} }
...@@ -46,6 +49,7 @@ const GuestRegisterStep = forwardRef( ...@@ -46,6 +49,7 @@ const GuestRegisterStep = forwardRef(
(props: GuestRegisterProperties, ref: Ref<GuestRegisterCallableMethods>) => { (props: GuestRegisterProperties, ref: Ref<GuestRegisterCallableMethods>) => {
const { i18n, t } = useTranslation(['common']) const { i18n, t } = useTranslation(['common'])
const { nextHandler, initialGuestData, registerData } = props const { nextHandler, initialGuestData, registerData } = props
const { showGenderFieldForGuest } = useContext(FeatureContext)
// For select-components it seems to be easier to tie them to a state // 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 // and then handle the updating of the form using this, than to tie the
...@@ -56,6 +60,7 @@ const GuestRegisterStep = forwardRef( ...@@ -56,6 +60,7 @@ const GuestRegisterStep = forwardRef(
const [passportNationality, setPassportNationality] = useState< const [passportNationality, setPassportNationality] = useState<
string | undefined string | undefined
>(undefined) >(undefined)
const [gender, setGender] = useState<string | undefined>(undefined)
const [idErrorState, setIdErrorState] = useState<string>('') const [idErrorState, setIdErrorState] = useState<string>('')
console.log('register step registerData', registerData) console.log('register step registerData', registerData)
...@@ -151,6 +156,16 @@ const GuestRegisterStep = forwardRef( ...@@ -151,6 +156,16 @@ const GuestRegisterStep = forwardRef(
setValue('mobilePhone', value.target.value) 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 today = new Date()
const minBirthDate = subYears(100)(today) const minBirthDate = subYears(100)(today)
const maxBirthDate = subYears(1)(today) const maxBirthDate = subYears(1)(today)
...@@ -186,7 +201,7 @@ const GuestRegisterStep = forwardRef( ...@@ -186,7 +201,7 @@ const GuestRegisterStep = forwardRef(
<Box sx={{ maxWidth: '30rem' }}> <Box sx={{ maxWidth: '30rem' }}>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<Stack spacing={2}> <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 !== {initialGuestData.authentication_method !==
AuthenticationMethod.Invite ? ( AuthenticationMethod.Invite ? (
<> <>
...@@ -248,6 +263,34 @@ const GuestRegisterStep = forwardRef( ...@@ -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 <Controller
name="dateOfBirth" name="dateOfBirth"
control={control} control={control}
...@@ -349,7 +392,7 @@ const GuestRegisterStep = forwardRef( ...@@ -349,7 +392,7 @@ const GuestRegisterStep = forwardRef(
return countryTuple return countryTuple
}) })
.filter( .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 // these are not needed and filtering them out to make the
// list look nicer // list look nicer
(countryTuple: [CountryCode, string]) => (countryTuple: [CountryCode, string]) =>
......
...@@ -10,6 +10,7 @@ from gregui.validation import ( ...@@ -10,6 +10,7 @@ from gregui.validation import (
validate_norwegian_national_id_number, validate_norwegian_national_id_number,
) )
# pylint: disable=W0223 # pylint: disable=W0223
class GuestConsentChoiceSerializer(serializers.Serializer): class GuestConsentChoiceSerializer(serializers.Serializer):
type = serializers.CharField(required=True) type = serializers.CharField(required=True)
...@@ -39,6 +40,7 @@ class GuestRegisterSerializer(serializers.ModelSerializer): ...@@ -39,6 +40,7 @@ class GuestRegisterSerializer(serializers.ModelSerializer):
) )
passport = serializers.CharField(required=False) passport = serializers.CharField(required=False)
date_of_birth = serializers.DateField(required=False) date_of_birth = serializers.DateField(required=False)
gender = serializers.CharField(required=False)
consents = GuestConsentChoiceSerializer(required=False, many=True, write_only=True) consents = GuestConsentChoiceSerializer(required=False, many=True, write_only=True)
def update(self, instance, validated_data): def update(self, instance, validated_data):
...@@ -76,6 +78,9 @@ class GuestRegisterSerializer(serializers.ModelSerializer): ...@@ -76,6 +78,9 @@ class GuestRegisterSerializer(serializers.ModelSerializer):
if "date_of_birth" in validated_data: if "date_of_birth" in validated_data:
instance.date_of_birth = validated_data["date_of_birth"] instance.date_of_birth = validated_data["date_of_birth"]
if "gender" in validated_data:
instance.gender = validated_data["gender"]
consents = validated_data.get("consents", {}) consents = validated_data.get("consents", {})
self._handle_consents(person=instance, consents=consents) self._handle_consents(person=instance, consents=consents)
...@@ -118,12 +123,21 @@ class GuestRegisterSerializer(serializers.ModelSerializer): ...@@ -118,12 +123,21 @@ class GuestRegisterSerializer(serializers.ModelSerializer):
return date_of_birth 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: class Meta:
model = Person model = Person
fields = ( fields = (
"id", "id",
"first_name", "first_name",
"last_name", "last_name",
"gender",
"email", "email",
"mobile_phone", "mobile_phone",
"fnr", "fnr",
......
...@@ -181,8 +181,9 @@ class InvitedGuestView(GenericAPIView): ...@@ -181,8 +181,9 @@ class InvitedGuestView(GenericAPIView):
"passport", "passport",
"date_of_birth", "date_of_birth",
"consents", "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): def get(self, request, *args, **kwargs):
""" """
...@@ -223,6 +224,7 @@ class InvitedGuestView(GenericAPIView): ...@@ -223,6 +224,7 @@ class InvitedGuestView(GenericAPIView):
"fnr": person.fnr and person.fnr.value, "fnr": person.fnr and person.fnr.value,
"passport": person.passport and person.passport.value, "passport": person.passport and person.passport.value,
"feide_id": person.feide_id and person.feide_id.value, "feide_id": person.feide_id and person.feide_id.value,
"gender": person.gender,
}, },
"sponsor": { "sponsor": {
"first_name": sponsor.first_name, "first_name": sponsor.first_name,
......
...@@ -524,3 +524,72 @@ def test_post_invited_info_valid_dnumber(client, invited_person): ...@@ -524,3 +524,72 @@ def test_post_invited_info_valid_dnumber(client, invited_person):
assert response.status_code == status.HTTP_200_OK, response.data assert response.status_code == status.HTTP_200_OK, response.data
person.refresh_from_db() person.refresh_from_db()
assert person.fnr.value == d_number 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment