From c6ab0cac82940108f3bfac290e1cfb0aae0efb3f Mon Sep 17 00:00:00 2001 From: hhn <hhn@uio.no> Date: Thu, 21 Sep 2023 15:09:03 +0200 Subject: [PATCH] Add ability for sponsors to reject a guest's registration If a guest's suplied identification is considered invalid by a sponsor they now have the ablility to reject them. When a guest is rejected they will be set to unregistered and they will receive an email with a new registration link where they can correct their mistake. --- frontend/public/locales/en/common.json | 12 +++- frontend/public/locales/nb/common.json | 12 +++- frontend/public/locales/nn/common.json | 12 +++- .../src/components/identityLine/index.tsx | 60 ++++++++++++++-- frontend/src/interfaces/index.ts | 4 +- .../routes/sponsor/guest/guestInfo/index.tsx | 11 +++ .../sponsor/guest/guestRoleInfo/index.tsx | 1 + frontend/src/utils/index.ts | 1 + greg/models.py | 4 +- greg/utils.py | 8 ++- gregui/api/serializers/role.py | 7 +- gregui/api/urls.py | 8 ++- gregui/api/views/invitation.py | 68 ++++++++++++++++++- gregui/api/views/person.py | 1 + gregui/api/views/userinfo.py | 3 + gregui/mailutils/invite_guest.py | 15 +++- .../0005_alter_emailtemplate_template_key.py | 27 ++++++++ gregui/models.py | 2 + gregui/templates/guest_redo_registration.txt | 7 ++ gregui/tests/api/serializers/test_guest.py | 1 + gregui/tests/api/views/test_invite_guest.py | 37 ++++++++++ gregui/tests/api/views/test_userinfo.py | 4 +- gregui/tests/conftest.py | 59 ++++++++++++++-- 23 files changed, 341 insertions(+), 23 deletions(-) create mode 100644 gregui/migrations/0005_alter_emailtemplate_template_key.py create mode 100644 gregui/templates/guest_redo_registration.txt diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 3b2a19db..848dd6ae 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -160,7 +160,8 @@ "next": "Next", "save": "Save", "cancel": "Cancel", - "verify": "Verify identification", + "verify": "Verify", + "reject": "Reject", "backToFrontPage": "Go to front page", "cancelInvitation": "Cancel", "resendInvitation": "Resend invitation", @@ -207,6 +208,15 @@ "cancelInvitation": "Cancel invitation?", "cancelInvitationDescription": "Do you want to cancel the invitation?" }, + "rejectionDialog": { + "text": { + "uio": "Before verifying this identity, please make sure that you have seen ID-papers with a picture matching the supplied value. Approved papers are passport, Norwegian driver's license, and Norwegian national ID card.", + "uib": "Are you sure you want to reject this identity?", + "default": "Are you sure you want to reject this identity?", + "helperText": "If the value does not match what the guest has entered click \"YES\", and they will receive a new email to correct it." + }, + "rejectIdentityTitle": "Invalid value?" + }, "error": { "error": "Error", "changeRoleFailed": "Failed to change role information", diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json index 8e3821d9..52a879c0 100644 --- a/frontend/public/locales/nb/common.json +++ b/frontend/public/locales/nb/common.json @@ -160,7 +160,8 @@ "next": "Neste", "save": "Lagre", "cancel": "Avbryt", - "verify": "Godkjenn identifikasjon", + "verify": "Godkjenn", + "reject": "Avslå", "backToFrontPage": "Tilbake til forsiden", "resendInvitation": "Send ny invitasjon", "cancelInvitation": "Kanseller", @@ -207,6 +208,15 @@ "cancelInvitation": "Kanseller invitasjon?", "cancelInvitationDescription": "Vil du kansellere invitasjonen?" }, + "rejectionDialog": { + "text": { + "uio": "Vennligst sammenlign verdien du godkjenner mot ID-papirer med bilde, før du godkjenner. Godkjente papirer er pass, norsk førerkort og norsk nasjonalt ID-kort.", + "uib": "Er du sikker på at du vil avslå denne identiteten?", + "default": "Er du sikker på at du vil avslå denne identiteten?", + "helperText": "Om verdien ikke stemmer overens med det gjesten har lagt inn klikk \"JA\", og de vil da få en ny epost for å rette opp i dette." + }, + "rejectIdentityTitle": "Ugyldig verdi?" + }, "error": { "error": "Feil", "changeRoleFailed": "Kunne ikke endre rolleinformasjon.", diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json index ee585da5..4a996ae4 100644 --- a/frontend/public/locales/nn/common.json +++ b/frontend/public/locales/nn/common.json @@ -160,7 +160,8 @@ "next": "Neste", "save": "Lagre", "cancel": "Avbryt", - "verify": "Godkjenn identifikasjon", + "verify": "Godkjenn", + "reject": "Avslå", "backToFrontPage": "Tilbake til framsida", "resendInvitation": "Send ny invitasjon", "cancelInvitation": "Kanseller", @@ -207,6 +208,15 @@ "cancelInvitation": "Kanseller invitasjon?", "cancelInvitationDescription": "Vil du kansellere invitasjonen?" }, + "rejectionDialog": { + "text": { + "uio": "Vennligst samanlikn verdien mot ID-papirer med bilde, før du godkjenner. Godkjende papirer er pass, norsk førerkort og norsk nasjonalt ID-kort.", + "uib": "Er du sikker på at du vil avslå denne identiteten?", + "default": "Er du sikker på at du vil avslå denne identiteten?", + "helperText": "Om verdien ikkje stemmer overeins med det gjesten har lagt inn klikk \"JA\", og dei vil då få ein ny e-post for å retta opp i dette." + }, + "rejectIdentityTitle": "Ugyldig verdi?" + }, "error": { "error": "Feil", "changeRoleFailed": "Kunne ikkje endre rolleinformasjon", diff --git a/frontend/src/components/identityLine/index.tsx b/frontend/src/components/identityLine/index.tsx index ddf9d53a..6202d8a1 100644 --- a/frontend/src/components/identityLine/index.tsx +++ b/frontend/src/components/identityLine/index.tsx @@ -17,6 +17,7 @@ import { interface IdentityLineProps { text: string identity: Identity | null + isSponsorForGuest: boolean reloadGuest: () => void reloadGuests: () => void } @@ -24,20 +25,28 @@ interface IdentityLineProps { const IdentityLine = ({ text, identity, + isSponsorForGuest, reloadGuest, reloadGuests, }: IdentityLineProps) => { - // Make a line with a confirmation button if the identity has not been verified + // Make a line with a confirmation button and + // rejection button if the identity has not been verified if (identity == null) { return <></> } - const [confirmOpen, setConfirmOpen] = useState(false) + const [confirmOpen, setConfirmOpen] = useState<boolean>(false) + const [rejectOpen, setRejectOpen] = useState<boolean>(false) const [t] = useTranslation('common') const verifyIdentity = (id: string) => async () => { await fetch(`/api/ui/v1/identity/${id}`, submitJsonOpts('PATCH', {})) reloadGuest() reloadGuests() } + const rejectIdentity = (id: string) => async () => { + await fetch(`/api/ui/v1/invite/${id}/recreate`, submitJsonOpts('PATCH', {})) + reloadGuest() + reloadGuests() + } const [verifiedBoxAnchor, setVerifiedBoxAnchor] = React.useState<null | HTMLElement>(null) const verifiedBoxOpen = Boolean(verifiedBoxAnchor) @@ -87,7 +96,7 @@ const IdentityLine = ({ } return <></> } - const getDialogText = () => { + const getConfirmDialogText = () => { switch (appInst) { case 'uio': return ( @@ -103,6 +112,23 @@ const IdentityLine = ({ return t('confirmationDialog.text.default') } } + const getRejectDialogText = () => { + const transBuilder = (baseText: string) => ( + <Trans + i18nKey={`${t(baseText)}<br/><br/>${t( + 'rejectionDialog.text.helperText' + )}`} + /> + ) + switch (appInst) { + case 'uio': + return transBuilder('rejectionDialog.text.uio') + case 'uib': + return transBuilder('rejectionDialog.text.uib') + default: + return transBuilder('rejectionDialog.text.default') + } + } useEffect(() => { if (enableIgaCheck && confirmOpen && identityCheckText === null) { @@ -147,7 +173,33 @@ const IdentityLine = ({ onConfirm={verifyIdentity(identity.id)} > <IdentityCheckText /> - {getDialogText()} + {getConfirmDialogText()} + </ConfirmDialog> + <Button + aria-label={t('button.verify')} + sx={{ + alignSelf: { xs: 'auto', md: 'flex-end' }, + marginLeft: { xs: '0rem', md: '1rem' }, + marginTop: { xs: '0.3rem', md: '0rem' }, + }} + onClick={() => setRejectOpen(true)} + disabled={ + !identity || + (disableNinVerification && + identity.type === 'norwegian_national_id_number') || + !isSponsorForGuest + } + > + {t('button.reject')} + </Button> + <ConfirmDialog + title={t('rejectionDialog.rejectIdentityTitle')} + open={rejectOpen} + setOpen={setRejectOpen} + onConfirm={rejectIdentity(identity.id)} + > + <IdentityCheckText /> + {getRejectDialogText()} </ConfirmDialog> </> )} diff --git a/frontend/src/interfaces/index.ts b/frontend/src/interfaces/index.ts index e0fcf7a2..4a41825d 100644 --- a/frontend/src/interfaces/index.ts +++ b/frontend/src/interfaces/index.ts @@ -14,7 +14,7 @@ export type Guest = { roles: Role[] } -type VerifiedChoices = "manual" | "automatic" | "" +type VerifiedChoices = 'manual' | 'automatic' | '' export type Identity = { id: string @@ -64,6 +64,7 @@ export type Role = { contact_person_unit: string | null comments: string | null sponsor_name: string + sponsor_feide_id: string ou_id: number } @@ -79,6 +80,7 @@ export type FetchedRole = { contact_person_unit: string | null comments: string | null sponsor_name: string + sponsor_feide_id: string ou_id: number } diff --git a/frontend/src/routes/sponsor/guest/guestInfo/index.tsx b/frontend/src/routes/sponsor/guest/guestInfo/index.tsx index d2fb3726..c1fe4801 100644 --- a/frontend/src/routes/sponsor/guest/guestInfo/index.tsx +++ b/frontend/src/routes/sponsor/guest/guestInfo/index.tsx @@ -29,6 +29,7 @@ import { TableHeadCell, TableRow, } from 'components/table' +import { useUserContext } from 'contexts' import { Guest } from 'interfaces' import SponsorInfoButtons from 'routes/components/sponsorInfoButtons' import { isValidEmail, isValidMobilePhoneNumber, submitJsonOpts } from 'utils' @@ -121,6 +122,11 @@ export default function GuestInfo({ useState<ServerErrorReportData>() const [updateOKMsg, setUpdateOKMsg] = useState<string>('') const [showErrorMessage, setShowErrorMessage] = useState<Boolean>(false) + const { user } = useUserContext() + const sponsorFeideId = user.feide_id + const isSponsorForGuest = guest.roles + .map(({ sponsor_feide_id }) => sponsor_feide_id) + .includes(sponsorFeideId) const { control, @@ -499,18 +505,23 @@ export default function GuestInfo({ <IdentityLine text={t('input.nationalIdNumber')} identity={guest.fnr} + isSponsorForGuest={isSponsorForGuest} reloadGuest={reloadGuest} reloadGuests={reloadGuests} /> <IdentityLine text={t('input.passportNumber')} identity={guest.passport} + isSponsorForGuest={isSponsorForGuest} reloadGuest={reloadGuest} reloadGuests={reloadGuests} /> </TableBody> </Table> </TableContainer> + <Button type="submit" color="secondary" disabled={!isValid || !isDirty}> + {t('button.save')} + </Button> </Box> <Typography sx={{ marginBottom: '1rem' }} variant="h2"> {t('guestInfo.roleInfoHead')} diff --git a/frontend/src/routes/sponsor/guest/guestRoleInfo/index.tsx b/frontend/src/routes/sponsor/guest/guestRoleInfo/index.tsx index 8bfdbde9..7e9ecc1a 100644 --- a/frontend/src/routes/sponsor/guest/guestRoleInfo/index.tsx +++ b/frontend/src/routes/sponsor/guest/guestRoleInfo/index.tsx @@ -138,6 +138,7 @@ export default function GuestRoleInfo({ contact_person_unit: null, comments: null, sponsor_name: '', + sponsor_feide_id: '', ou_id: -1, }) // Prepare min and max date values diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index b525f654..27f917dd 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -278,6 +278,7 @@ export function parseRole(role: FetchedRole): Role { contact_person_unit: role.contact_person_unit, comments: role.comments, sponsor_name: role.sponsor_name, + sponsor_feide_id: role.sponsor_feide_id, ou_id: role.ou_id, } } diff --git a/greg/models.py b/greg/models.py index 70a3af83..75ac0e72 100644 --- a/greg/models.py +++ b/greg/models.py @@ -711,8 +711,8 @@ class InvitationLink(BaseModel): """ Link to an invitation. - Having the uuid of an InvitationLink should grant access to the view for posting - If the Invitation itself is deleted, all InvitationLinks are also be removed. + Having the uuid of an InvitationLink should grant access to the view for posting. + If the Invitation itself is deleted, all InvitationLinks are also removed. """ uuid = models.UUIDField(null=False, default=uuid.uuid4, blank=False) diff --git a/greg/utils.py b/greg/utils.py index ad312319..5820e4e0 100644 --- a/greg/utils.py +++ b/greg/utils.py @@ -195,13 +195,17 @@ def create_objects_for_invitation( return person -def queue_mail_to_invitee(person: Person, sponsor: Sponsor): +def queue_mail_to_invitee( + person: Person, + sponsor: Sponsor, + is_fresh_registration: bool = True, +): for invitationlink in InvitationLink.objects.filter( invitation__role__person_id=person.id, invitation__role__sponsor_id=sponsor.id, ): invitemailer = InviteGuest() - invitemailer.queue_mail(invitationlink) + invitemailer.queue_mail(invitationlink, is_fresh_registration) def role_invitation_date_validator( diff --git a/gregui/api/serializers/role.py b/gregui/api/serializers/role.py index c820bfe4..4728b115 100644 --- a/gregui/api/serializers/role.py +++ b/gregui/api/serializers/role.py @@ -95,6 +95,7 @@ class ExtendedRoleSerializer(serializers.ModelSerializer): ou_en = SerializerMethodField(source="orgunit") max_days = SerializerMethodField(source="type") sponsor_name = SerializerMethodField(source="sponsor") + sponsor_feide_id = SerializerMethodField(source="sponsor") ou_id = SerializerMethodField(source="orgunit") def get_name_nb(self, obj): @@ -115,6 +116,9 @@ class ExtendedRoleSerializer(serializers.ModelSerializer): def get_sponsor_name(self, obj): return f"{obj.sponsor.first_name} {obj.sponsor.last_name}" + def get_sponsor_feide_id(self, obj): + return obj.sponsor.feide_id + def get_ou_id(self, obj): return obj.orgunit.id @@ -132,6 +136,7 @@ class ExtendedRoleSerializer(serializers.ModelSerializer): "contact_person_unit", "comments", "sponsor_name", + "sponsor_feide_id", "ou_id", ] - read_only_fields = ["contact_person_unit", "sponsor_name"] + read_only_fields = ["contact_person_unit", "sponsor_name", "sponsor_feide_id"] diff --git a/gregui/api/urls.py b/gregui/api/urls.py index 409a7edb..d8916b57 100644 --- a/gregui/api/urls.py +++ b/gregui/api/urls.py @@ -8,9 +8,10 @@ from gregui.api.views.identity import ( from gregui.api.views.invitation import ( CheckInvitationView, - ResendInvitationView, InvitationView, InvitedGuestView, + RecreateInvitationView, + ResendInvitationView, ) from gregui.api.views.ou import OusViewSet from gregui.api.views.person import ( @@ -44,6 +45,11 @@ urlpatterns += [ ResendInvitationView.as_view(), name="invite-resend", ), + path( + "invite/<int:identity_id>/recreate", + RecreateInvitationView.as_view(), + name="invite-recreate", + ), path("invite/", InvitationView.as_view(), name="invitation"), path( "person/search/", diff --git a/gregui/api/views/invitation.py b/gregui/api/views/invitation.py index 6f540b95..9bc7e9a2 100644 --- a/gregui/api/views/invitation.py +++ b/gregui/api/views/invitation.py @@ -1,3 +1,4 @@ +import datetime from enum import Enum from typing import Optional, List import structlog @@ -18,7 +19,7 @@ from rest_framework.response import Response from rest_framework.throttling import AnonRateThrottle from rest_framework.views import APIView -from greg.models import Identity, InvitationLink, Person +from greg.models import Consent, Identity, Invitation, InvitationLink, Person from greg.permissions import IsSponsor from greg.utils import ( get_default_invitation_expire_date_from_now, @@ -499,3 +500,68 @@ class ResendInvitationView(UpdateModelMixin, APIView): invitemailer.queue_mail(invitation_link) return Response(status=status.HTTP_200_OK) + + +class RecreateInvitationView(UpdateModelMixin, APIView): + """ + Endpoint for rejecting a guest's identity data. + + This endpoint is to be used when a sponsor deems a guest's supplied identification data as invalid. + The guest will be set to unregistered, and they will receive an email with a new registration link. + """ + + authentication_classes = [BasicAuthentication, SessionAuthentication] + permission_classes = [IsSponsor] + + def patch(self, request, *args, **kwargs) -> Response: + sponsor_user = GregUserProfile.objects.get(user=request.user) + sponsor = sponsor_user.sponsor + if sponsor is None: + return Response(status=status.HTTP_403_FORBIDDEN) + + identity_id = kwargs["identity_id"] + try: + ident: Identity = Identity.objects.get(id=identity_id) + except Identity.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + guest: Person = ident.person + + # This endpoint should only be used on guests that has been + # registered with invalid data, they should therefore not be verified. + if not guest.is_registered or guest.is_verified: + return Response(status=status.HTTP_400_BAD_REQUEST) + + with transaction.atomic(): + # Set the guest as unregistered + guest.registration_completed_date = None + guest.save() + + # Delete the guest's private mobile to not get + # duplicate identity error on reregister + private_mobile = Identity.objects.filter( + type=Identity.IdentityType.PRIVATE_MOBILE_NUMBER, person__id=guest.id + ).first() + if private_mobile is not None: + private_mobile.delete() + + # Delete the guest's consents to + # not violate uniqueness constraint + guest_consents = Consent.objects.filter(person__id=guest.id) + for consent in guest_consents: + consent.delete() + + for invitation in Invitation.objects.filter( + role__person_id=guest.id, role__sponsor_id=sponsor.id + ): + InvitationLink.objects.create( + invitation=invitation, + expire=timezone.now() + datetime.timedelta(days=30), + ) + + queue_mail_to_invitee( + person=guest, + sponsor=sponsor, + is_fresh_registration=False, + ) + + return Response(status=status.HTTP_200_OK) diff --git a/gregui/api/views/person.py b/gregui/api/views/person.py index 5fd62ae6..92a78f30 100644 --- a/gregui/api/views/person.py +++ b/gregui/api/views/person.py @@ -242,6 +242,7 @@ class GuestInfoViewSet(mixins.ListModelMixin, GenericViewSet): """ user = GregUserProfile.objects.get(user=self.request.user) + assert user.sponsor is not None units = user.sponsor.get_allowed_units() return ( Person.objects.filter(roles__orgunit__in=list(units)) diff --git a/gregui/api/views/userinfo.py b/gregui/api/views/userinfo.py index 385a1709..b20f002f 100644 --- a/gregui/api/views/userinfo.py +++ b/gregui/api/views/userinfo.py @@ -79,6 +79,9 @@ class UserInfoView(APIView): if sponsor: content["sponsor_id"] = sponsor.id + content["first_name"] = sponsor.first_name + content["last_name"] = sponsor.last_name + content["feide_id"] = sponsor.feide_id if person: person_roles = [ diff --git a/gregui/mailutils/invite_guest.py b/gregui/mailutils/invite_guest.py index c8796420..e30d49ca 100644 --- a/gregui/mailutils/invite_guest.py +++ b/gregui/mailutils/invite_guest.py @@ -25,6 +25,11 @@ class InviteGuest: template_key=EmailTemplate.EmailType.GUEST_REGISTRATION ) + def get_redo_registration_template(self): + return EmailTemplate.objects.get( + template_key=EmailTemplate.EmailType.GUEST_REDO_REGISTRATION + ) + def get_notice_template(self): return EmailTemplate.objects.get( template_key=EmailTemplate.EmailType.INVALID_EMAIL @@ -47,7 +52,11 @@ class InviteGuest: recipient_list=[sponsor_mail], ) - def queue_mail(self, link: InvitationLink) -> Optional[str]: + def queue_mail( + self, + link: InvitationLink, + is_fresh_registration: bool = True, + ) -> Optional[str]: guest = link.invitation.role.person email_address = guest.private_email if not email_address: @@ -64,6 +73,7 @@ class InviteGuest: "sponsor_id": sponsor.id, "sponsor_name": f"{sponsor.first_name} {sponsor.last_name}", "token": link.uuid, + "is_fresh_registration": is_fresh_registration, }, ) @@ -74,9 +84,12 @@ def try_send_registration_mail( sponsor_id: int, sponsor_name: str, token: str, + is_fresh_registration: bool = True, ): ig = InviteGuest() template = ig.get_template() + if not is_fresh_registration: + template = ig.get_redo_registration_template() context = Context( { "institution": settings.INSTANCE_NAME, diff --git a/gregui/migrations/0005_alter_emailtemplate_template_key.py b/gregui/migrations/0005_alter_emailtemplate_template_key.py new file mode 100644 index 00000000..b57fc48c --- /dev/null +++ b/gregui/migrations/0005_alter_emailtemplate_template_key.py @@ -0,0 +1,27 @@ +# Generated by Django 4.1.4 on 2023-07-19 14:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("gregui", "0004_add_email_template_type"), + ] + + operations = [ + migrations.AlterField( + model_name="emailtemplate", + name="template_key", + field=models.CharField( + choices=[ + ("guest_registration", "Guest Registration"), + ("guest_redo_registration", "Guest Redo Registration"), + ("sponsor_confirmation", "Sponsor Confirmation"), + ("role_end_reminder", "Role End Reminder"), + ("invalid_email", "Invalid Email"), + ], + max_length=64, + unique=True, + ), + ), + ] diff --git a/gregui/models.py b/gregui/models.py index 256f7ba6..201f8710 100644 --- a/gregui/models.py +++ b/gregui/models.py @@ -35,6 +35,7 @@ class EmailTemplate(BaseModel): EmailType. GUEST_REGISTRATION is for informing a guest that they have been invited + GUEST_REDO_REGISTRATION is for informing a guest that the identification data is incorrect and need to be resubmitted SPONSOR_CONFIRMATION is for informing the sponsor they must verify the guest's information ROLE_END_REMINDER is used when reminding the sponsor if their ending roles in the near future INVALID_EMAIL is used to notify sponsors when email sending fails @@ -45,6 +46,7 @@ class EmailTemplate(BaseModel): """Types of Emails""" GUEST_REGISTRATION = "guest_registration" + GUEST_REDO_REGISTRATION = "guest_redo_registration" SPONSOR_CONFIRMATION = "sponsor_confirmation" ROLE_END_REMINDER = "role_end_reminder" INVALID_EMAIL = "invalid_email" diff --git a/gregui/templates/guest_redo_registration.txt b/gregui/templates/guest_redo_registration.txt new file mode 100644 index 00000000..9f6ad37c --- /dev/null +++ b/gregui/templates/guest_redo_registration.txt @@ -0,0 +1,7 @@ +Dette er en automatisk generert melding fra gjesteregistreringstjenesten. +Din registrering ved {{ institution }} har blitt avslått av {{ sponsor }} grunnet ugyldig identifisering. +For å rette opp i dette, vennligst følg denne lenken: {{ registration_link }} + +This message has been automatically generated by the guest registration system. +Your registration at {{ institution }} has been rejected by {{ sponsor }} due to invalid identification. +To amend this, please follow this link: {{ registration_link }} diff --git a/gregui/tests/api/serializers/test_guest.py b/gregui/tests/api/serializers/test_guest.py index 6798cc40..6763df8d 100644 --- a/gregui/tests/api/serializers/test_guest.py +++ b/gregui/tests/api/serializers/test_guest.py @@ -35,6 +35,7 @@ def test_serialize_guest(invited_person): "contact_person_unit": "", "comments": "", "sponsor_name": "Sponsor Bar", + "sponsor_feide_id": "foo@example.org", "ou_id": 1, }, ], diff --git a/gregui/tests/api/views/test_invite_guest.py b/gregui/tests/api/views/test_invite_guest.py index 06d28720..e8e188da 100644 --- a/gregui/tests/api/views/test_invite_guest.py +++ b/gregui/tests/api/views/test_invite_guest.py @@ -285,3 +285,40 @@ def test_invite_resend_person_multiple_expired_links_send_all( response = client.patch(url) assert response.status_code == status.HTTP_200_OK assert send_invite_mock_function.call_count == 2 + + +@pytest.mark.django_db +def test_invite_recreate( + client, + log_in, + user_sponsor, + person_registered: Person, + mocker, +): + send_invite_mock_function = mocker.patch.object(InviteGuest, "queue_mail") + log_in(user_sponsor) + + assert person_registered.is_registered is True + assert person_registered.is_verified is False + assert person_registered.private_mobile is not None + invitation_links = InvitationLink.objects.filter( + invitation__role__person_id=person_registered.id + ) + assert invitation_links.count() == 0 + + identity_id = person_registered.feide_id.id + url = reverse("gregui-v1:invite-recreate", kwargs={"identity_id": identity_id}) + response = client.patch(url) + person_registered = Person.objects.get(id=person_registered.id) + + assert person_registered.is_registered is False + assert person_registered.is_verified is False + assert person_registered.private_mobile is None + invitation_links = InvitationLink.objects.filter( + invitation__role__person_id=person_registered.id + ) + # person_registered had two invites from this sponsor, so two links should have been created + assert invitation_links.count() == 2 + # person_registered had two invites from this sponsor, so two emails should have been sent + assert send_invite_mock_function.call_count == 2 + assert response.status_code == status.HTTP_200_OK diff --git a/gregui/tests/api/views/test_userinfo.py b/gregui/tests/api/views/test_userinfo.py index e6212bc7..964c4ef8 100644 --- a/gregui/tests/api/views/test_userinfo.py +++ b/gregui/tests/api/views/test_userinfo.py @@ -63,7 +63,9 @@ def test_userinfo_sponsor_get(client, log_in, user_sponsor): assert response.status_code == status.HTTP_200_OK assert response.json() == { "auth_type": "oidc", - "feide_id": "", + "first_name": "Sponsor", + "last_name": "Bar", + "feide_id": "foo@example.org", "person_id": None, "roles": [], "consents": [], diff --git a/gregui/tests/conftest.py b/gregui/tests/conftest.py index d4b78773..f36419c9 100644 --- a/gregui/tests/conftest.py +++ b/gregui/tests/conftest.py @@ -382,14 +382,18 @@ def create_person() -> ( def create_person( first_name: str, last_name: str, - email: str = None, - phone_number: str = None, - nin: str = None, - feide_id: str = None, - date_of_birth: datetime.date = None, + email: Optional[str] = None, + phone_number: Optional[str] = None, + nin: Optional[str] = None, + feide_id: Optional[str] = None, + date_of_birth: Optional[datetime.date] = None, + registration_completed_date: Optional[datetime.date] = None, ) -> Person: person = Person.objects.create( - first_name=first_name, last_name=last_name, date_of_birth=date_of_birth + first_name=first_name, + last_name=last_name, + date_of_birth=date_of_birth, + registration_completed_date=registration_completed_date, ) if nin: @@ -451,6 +455,49 @@ def person_invited(create_person) -> Person: return Person.objects.get(id=person.id) +@pytest.fixture +def person_registered( + create_person, + create_role, + create_invitation, + sponsor_foo, + sponsor_bar, + unit_foo, + role_type_foo, +) -> Person: + """Invited person after registration.""" + person = create_person( + first_name="Foofoo", + last_name="Barbar", + email="foofoo@example.org", + feide_id="foofoo@example.org", + phone_number="41111111", + registration_completed_date=datetime.date(2023, 1, 1), + ) + role_1 = create_role( + person=person, + sponsor=sponsor_foo, + unit=unit_foo, + role_type=role_type_foo, + ) + role_2 = create_role( + person=person, + sponsor=sponsor_foo, + unit=unit_foo, + role_type=role_type_foo, + ) + role_3 = create_role( + person=person, + sponsor=sponsor_bar, + unit=unit_foo, + role_type=role_type_foo, + ) + create_invitation(role=role_1) + create_invitation(role=role_2) + create_invitation(role=role_3) + return Person.objects.get(id=person.id) + + @pytest.fixture def invited_person( create_role, -- GitLab