diff --git a/frontend/src/routes/guest/register/enteredGuestData.ts b/frontend/src/routes/guest/register/enteredGuestData.ts index cf5c8662c000dd2a24738d9bd0ba25162bd3fce1..ce62a7212a0d0f28b6bd0520d8e95bd7b2fe5db9 100644 --- a/frontend/src/routes/guest/register/enteredGuestData.ts +++ b/frontend/src/routes/guest/register/enteredGuestData.ts @@ -11,4 +11,5 @@ export type EnteredGuestData = { nationalIdNumber?: string passportNumber?: string passportNationality?: string + dateOfBirth?: Date } diff --git a/frontend/src/routes/guest/register/guestDataForm.ts b/frontend/src/routes/guest/register/guestDataForm.ts index 09e7da4288e247fdf95e43dce5e164bd20956daa..0bc7a59b5f7ce0c41604a862e54364374e2607de 100644 --- a/frontend/src/routes/guest/register/guestDataForm.ts +++ b/frontend/src/routes/guest/register/guestDataForm.ts @@ -18,18 +18,17 @@ export type GuestInviteInformation = { feide_id?: string - // These fields are in the form, but it is not expected that - // they are set, with the exception of e-mail, when the guest - // first follows the invite link email?: string + // These fields are in the form, but it is not expected that + // they are set, when the guest first follows the invite link mobile_phone_country_code?: string mobile_phone?: string - fnr?: string passport?: string passportNationality?: string countryForCallingCode?: string + dateOfBirth?: Date authentication_method: AuthenticationMethod } diff --git a/frontend/src/routes/guest/register/registerPage.tsx b/frontend/src/routes/guest/register/registerPage.tsx index d73e0bc23d71896b87f5e6845cf46c656390200c..5de260df569e765f4252372f8146e6b2ca486ac1 100644 --- a/frontend/src/routes/guest/register/registerPage.tsx +++ b/frontend/src/routes/guest/register/registerPage.tsx @@ -23,6 +23,8 @@ import { getCountryCallingCode, } from 'libphonenumber-js' import { getAlpha2Codes, getName } from 'i18n-iso-countries' +import { DatePicker } from '@mui/lab' +import { subYears } from 'date-fns/fp' import { GuestInviteInformation } from './guestDataForm' import { EnteredGuestData } from './enteredGuestData' import { GuestRegisterCallableMethods } from './guestRegisterCallableMethods' @@ -123,6 +125,10 @@ const GuestRegisterStep = forwardRef( setValue('mobilePhone', value.target.value) } + const today = new Date() + const minBirthDate = subYears(100)(today) + const maxBirthDate = subYears(1)(today) + useEffect(() => { setCountryCode(guestData.countryForCallingCode) setValue( @@ -140,6 +146,7 @@ const GuestRegisterStep = forwardRef( function doSubmit() { return onSubmit() } + register('mobilePhoneCountry') useImperativeHandle(ref, () => ({ doSubmit })) @@ -215,6 +222,25 @@ const GuestRegisterStep = forwardRef( </> )} + <Controller + name="dateOfBirth" + control={control} + render={({ field }) => ( + <DatePicker + mask="____-__-__" + label={t('input.dateOfBirth')} + value={field.value} + minDate={minBirthDate} + maxDate={maxBirthDate} + inputFormat="yyyy-MM-dd" + onChange={(value) => { + field.onChange(value) + }} + renderInput={(params) => <TextField {...params} />} + /> + )} + /> + <TextField id="email" label={t('input.email')} diff --git a/gregui/api/serializers/guest.py b/gregui/api/serializers/guest.py index a7fe9a16071d8e90f4298cdb91fc3d5292755302..866dd8298a94b90df29dbb0536a20072358381fa 100644 --- a/gregui/api/serializers/guest.py +++ b/gregui/api/serializers/guest.py @@ -1,9 +1,11 @@ +import datetime + from rest_framework import serializers from greg.models import Identity, Person from gregui.validation import ( validate_phone_number, - validate_norwegian_national_id_number, + validate_norwegian_national_id_number ) @@ -20,6 +22,7 @@ class GuestRegisterSerializer(serializers.ModelSerializer): required=False, validators=[validate_norwegian_national_id_number] ) passport = serializers.CharField(required=False) + date_of_birth = serializers.DateField(required=False) def update(self, instance, validated_data): if "email" in validated_data: @@ -53,8 +56,18 @@ class GuestRegisterSerializer(serializers.ModelSerializer): if "last_name" in validated_data: instance.last_name = validated_data["last_name"] + if "date_of_birth" in validated_data: + instance.date_of_birth = validated_data["date_of_birth"] + return instance + def validate_date_of_birth(self, value): + today = datetime.date.today() + + # Check that the date of birth is between the interval starting about 100 years ago and last year + if not (today - datetime.timedelta(weeks=100 * 52) < value < today - datetime.timedelta(weeks=52)): + raise serializers.ValidationError("Invalid date of birth") + class Meta: model = Person fields = ( @@ -65,12 +78,13 @@ class GuestRegisterSerializer(serializers.ModelSerializer): "mobile_phone", "fnr", "passport", + "date_of_birth" ) read_only_fields = ("id",) def create_identity_or_update( - identity_type: Identity.IdentityType, value: str, person: Person + identity_type: Identity.IdentityType, value: str, person: Person ): existing_identity = person.identities.filter(type=identity_type).first() if not existing_identity: diff --git a/gregui/api/views/invitation.py b/gregui/api/views/invitation.py index aa2dd0698a66b70d5cd73020ae3f8e19ccf0fdff..7082894f706cd5186efc07e6d45976793c4ff609 100644 --- a/gregui/api/views/invitation.py +++ b/gregui/api/views/invitation.py @@ -72,8 +72,8 @@ class InvitationView(CreateAPIView, DestroyAPIView): person = serializer.save() for invitationlink in InvitationLink.objects.filter( - invitation__role__person_id=person.id, - invitation__role__sponsor_id=sponsor_user.sponsor_id, + invitation__role__person_id=person.id, + invitation__role__sponsor_id=sponsor_user.sponsor_id, ): send_invite_mail(invitationlink) @@ -150,6 +150,7 @@ class InvitedGuestView(GenericAPIView): "fnr", "mobile_phone", "passport", + "date_of_birth" ] fields_allowed_to_update_if_feide = ["mobile_phone"] @@ -242,10 +243,10 @@ class InvitedGuestView(GenericAPIView): # If there is a Feide ID registered with the guest, assume that the name is also coming from there feide_id = self._get_identity_or_none(person, Identity.IdentityType.FEIDE_ID) if not self._only_allowed_fields_in_request( - data, - self.fields_allowed_to_update_if_invite - if feide_id is None - else self.fields_allowed_to_update_if_feide, + data, + self.fields_allowed_to_update_if_invite + if feide_id is None + else self.fields_allowed_to_update_if_feide, ): return Response(status=status.HTTP_400_BAD_REQUEST) @@ -295,7 +296,7 @@ class InvitedGuestView(GenericAPIView): @staticmethod def _get_identity_or_none( - person: Person, identity_type: Identity.IdentityType + person: Person, identity_type: Identity.IdentityType ) -> Optional[str]: try: return person.identities.get(type=identity_type).value diff --git a/gregui/tests/api/views/test_invitation.py b/gregui/tests/api/views/test_invitation.py index a94f4daf9d98afefbb22e9429ec2e2dd3ddc5f8b..3bf201aa70f28fe1f907cf3ae9dae5e65b3f2aed 100644 --- a/gregui/tests/api/views/test_invitation.py +++ b/gregui/tests/api/views/test_invitation.py @@ -56,7 +56,7 @@ def test_get_invited_info_no_session(client, invitation_link): @pytest.mark.django_db def test_get_invited_info_session_okay( - client, invited_person, sponsor_foo_data, role_type_foo, unit_foo + client, invited_person, sponsor_foo_data, role_type_foo, unit_foo ): person, invitation_link = invited_person # get a session @@ -91,7 +91,7 @@ def test_get_invited_info_session_okay( @pytest.mark.django_db def test_get_invited_info_expired_link( - client, invitation_link, invitation_expired_date + client, invitation_link, invitation_expired_date ): # Get a session while link is valid client.get(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid}) @@ -134,7 +134,7 @@ def test_invited_guest_can_post_information(client: APIClient, invited_person): @pytest.mark.django_db def test_post_invited_info_expired_session( - client, invitation_link, invitation_expired_date + client, invitation_link, invitation_expired_date ): # get a session client.post(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid}) @@ -246,10 +246,10 @@ def test_register_passport(client, invited_person): session.save() assert ( - Identity.objects.filter( - person__id=person.id, type=Identity.IdentityType.PASSPORT_NUMBER - ).count() - == 0 + Identity.objects.filter( + person__id=person.id, type=Identity.IdentityType.PASSPORT_NUMBER + ).count() + == 0 ) response = client.post(url, data, format="json") @@ -266,15 +266,15 @@ def test_register_passport(client, invited_person): @pytest.mark.django_db def test_name_update_not_allowed_if_feide_identity_is_present( - client: APIClient, - person_foo, - sponsor_foo, - unit_foo, - role_type_foo, - create_role, - create_invitation, - create_invitation_link, - invitation_valid_date, + client: APIClient, + person_foo, + sponsor_foo, + unit_foo, + role_type_foo, + create_role, + create_invitation, + create_invitation_link, + invitation_valid_date, ): # This person has a Feide ID, so the name is assumed to come from there as well # and the guest should not be allowed to update it @@ -310,15 +310,15 @@ def test_name_update_not_allowed_if_feide_identity_is_present( @pytest.mark.django_db def test_name_update_allowed_if_feide_identity_is_not_present( - client: APIClient, - create_person, - sponsor_foo, - unit_foo, - role_type_foo, - create_role, - create_invitation, - create_invitation_link, - invitation_valid_date, + client: APIClient, + create_person, + sponsor_foo, + unit_foo, + role_type_foo, + create_role, + create_invitation, + create_invitation_link, + invitation_valid_date, ): # This person does not have a Feide ID, so he will be viewed as someone whose name was entered manually # by the sponsor, and in it that case the guest can update it @@ -390,3 +390,40 @@ def test_post_info_fail_unknown_field(client, invited_person_verified_nin): # Verify rejection assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +def test_date_of_birth_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}} + response = client.post(url, data, format="json") + + assert response.status_code == status.HTTP_200_OK + + person = Person.objects.get() + assert person.date_of_birth == datetime.datetime.strptime(date_of_birth, "%Y-%m-%d").date() + +@pytest.mark.django_db +def test_invalid_date_of_birth_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 = "1900-03-16" + 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_400_BAD_REQUEST + + person = Person.objects.get() + assert person.date_of_birth is None