From 0706be63b650e1f7f894eeebe2a2e32ae69be690 Mon Sep 17 00:00:00 2001 From: Tore Brede <Tore.Brede@uib.no> Date: Tue, 9 Nov 2021 12:28:59 +0100 Subject: [PATCH] GREG-94: Changing resend logic --- greg/utils.py | 6 ++ gregui/api/views/invitation.py | 52 +++++++++++------ gregui/mailutils.py | 6 +- gregui/tests/api/test_invite_guest.py | 84 +++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 21 deletions(-) diff --git a/greg/utils.py b/greg/utils.py index 8408087a..94018220 100644 --- a/greg/utils.py +++ b/greg/utils.py @@ -1,5 +1,7 @@ import re +import datetime from datetime import date +from django.utils import timezone def camel_to_snake(s: str) -> str: @@ -90,3 +92,7 @@ def _compute_checksum(input_digits: str) -> bool: k2 = 0 return k1 < 10 and k2 < 10 and k1 == d[9] and k2 == d[10] + + +def get_default_invitation_expire_date_from_now(): + return timezone.now() + datetime.timedelta(days=30) diff --git a/gregui/api/views/invitation.py b/gregui/api/views/invitation.py index 231c7d8a..cb043f06 100644 --- a/gregui/api/views/invitation.py +++ b/gregui/api/views/invitation.py @@ -1,6 +1,4 @@ -import datetime import logging -import uuid from enum import Enum from typing import Optional @@ -8,19 +6,18 @@ from django.core import exceptions from django.db import transaction from django.http.response import JsonResponse from django.utils import timezone -from django.utils.timezone import now from rest_framework import status from rest_framework.authentication import SessionAuthentication, BasicAuthentication -from rest_framework.generics import CreateAPIView, GenericAPIView -from rest_framework.mixins import UpdateModelMixin from rest_framework.generics import CreateAPIView, GenericAPIView, DestroyAPIView +from rest_framework.mixins import UpdateModelMixin from rest_framework.parsers import JSONParser from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView -from greg.models import Identity, InvitationLink, Person, Invitation +from greg.models import Identity, InvitationLink, Person from greg.permissions import IsSponsor +from greg.utils import get_default_invitation_expire_date_from_now from gregui.api.serializers.guest import GuestRegisterSerializer from gregui.api.serializers.invitation import InviteGuestSerializer from gregui.mailutils import send_invite_mail @@ -312,18 +309,37 @@ class ResendInvitationView(UpdateModelMixin, APIView): # No invitation, not expected that the endpoint should be called in this case return Response(status=status.HTTP_400_BAD_REQUEST) - if invitation_links.count() > 1: - logger.warning(f"Person with ID {person_id} has multiple invitation links") - - for link in invitation_links: - link.uuid = uuid.uuid4() - link.expire = timezone.now() + datetime.timedelta(days=30) - link.save() - # invitationlink = InvitationLink.objects.create( - # invitation=link.invitation, - # expire=timezone.now() + datetime.timedelta(days=30), - # ) + non_expired_links = invitation_links.filter(expire__gt=timezone.now()) + + if non_expired_links.count() > 0: + if non_expired_links.count() > 1: + # Do not expect this to happen + logger.warning( + f"Person with ID {person_id} has multiple invitation links" + ) + + # Just resend all and do not create a new one + for link in non_expired_links: + send_invite_mail(link) + else: + # All the invitation links have expired, create a new one + invitations_to_resend = set( + [invitation_link.invitation for invitation_link in invitation_links] + ) - send_invite_mail(link) + if len(invitations_to_resend) > 1: + # Do not expected that a person has several open invitations, it could happen + # if he has been invited by different sponsor at the same time, but that + # could be an indication that there has been a mixup + logger.warning( + f"Multiple invitations exist for person with ID {person_id}" + ) + + for invitation in invitations_to_resend: + invitation_link = InvitationLink.objects.create( + invitation=invitation, + expire=get_default_invitation_expire_date_from_now(), + ) + send_invite_mail(invitation_link) return Response(status=status.HTTP_200_OK) diff --git a/gregui/mailutils.py b/gregui/mailutils.py index b2d78e1d..435224cf 100644 --- a/gregui/mailutils.py +++ b/gregui/mailutils.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) def prepare_arguments( - template: EmailTemplate, context: dict[str, str], mail_to: str + template: EmailTemplate, context: dict[str, str], mail_to: str ) -> dict[str, Union[str, list[str]]]: """Combine input to a dict ready for use as arguments ti django's send_mail""" return { @@ -25,7 +25,7 @@ def prepare_arguments( def registration_template( - institution: str, sponsor: str, mail_to: str + institution: str, sponsor: str, mail_to: str ) -> dict[str, Union[str, list[str]]]: """ Prepare email for registration @@ -74,7 +74,7 @@ def send_invite_mail(link: InvitationLink) -> Optional[str]: email_address = link.invitation.role.person.private_email if not email_address: logger.warning( - f"No e-mail address found for invitation link with ID: {link.id}" + "No e-mail address found for invitation link with ID: {%s}" % link.id ) return None diff --git a/gregui/tests/api/test_invite_guest.py b/gregui/tests/api/test_invite_guest.py index 616a9346..1b8db41c 100644 --- a/gregui/tests/api/test_invite_guest.py +++ b/gregui/tests/api/test_invite_guest.py @@ -1,5 +1,7 @@ import datetime + import pytest +from django.utils import timezone from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APIRequestFactory, force_authenticate @@ -69,3 +71,85 @@ def test_invite_cancel( assert Role.objects.filter(id=role.id).count() == 0 assert Invitation.objects.filter(id=invitation.id).count() == 0 assert InvitationLink.objects.filter(invitation__id=invitation.id).count() == 0 + + +@pytest.mark.django_db +def test_invite_resend_existing_invite_active( + client, + log_in, + user_sponsor, + person, + invitation, + invitation_link, + invitation_link_expired, + mocker, +): + send_invite_mock_function = mocker.patch( + "gregui.api.views.invitation.send_invite_mail" + ) + log_in(user_sponsor) + + invitation_links_for_person = InvitationLink.objects.filter( + invitation__role__person_id=person.id + ) + assert invitation_links_for_person.count() == 2 + + original_expire_date = invitation_link_expired.expire + original_date = invitation_link.expire + url = reverse("gregui-v1:invite-resend", kwargs={"person_id": person.id}) + response = client.patch(url) + assert response.status_code == status.HTTP_200_OK + + # One e-mail should have been sent with an invite + assert send_invite_mock_function.call_count == 1 + + # Just check that the expire dates have not changed + invitation_link_expired.refresh_from_db() + assert original_expire_date == invitation_link_expired.expire + + invitation_link.refresh_from_db() + assert original_date == invitation_link.expire + + # The number of invitations should not have changed + invitation_links_for_person = InvitationLink.objects.filter( + invitation__role__person_id=person.id + ) + assert invitation_links_for_person.count() == 2 + + +@pytest.mark.django_db +def test_invite_resend_existing_invite_not_active( + client, log_in, user_sponsor, person, invitation_link_expired, mocker +): + send_invite_mock_function = mocker.patch( + "gregui.api.views.invitation.send_invite_mail", return_value=10 + ) + log_in(user_sponsor) + + invitation_links_for_person = InvitationLink.objects.filter( + invitation__role__person_id=person.id + ) + assert invitation_links_for_person.count() == 1 + + original_expire_date = invitation_link_expired.expire + url = reverse("gregui-v1:invite-resend", kwargs={"person_id": person.id}) + response = client.patch(url) + assert response.status_code == status.HTTP_200_OK + + # One e-mail should have been sent with an invite + assert send_invite_mock_function.call_count == 1 + + # Just check that the expire dates have not changed + invitation_link_expired.refresh_from_db() + assert original_expire_date == invitation_link_expired.expire + + # There should now be two invitations + invitation_links_for_person = InvitationLink.objects.filter( + invitation__role__person_id=person.id + ) + assert invitation_links_for_person.count() == 2 + + # If there is no active link the following line will raise an exception + InvitationLink.objects.get( + invitation__role__person_id=person.id, expire__gt=timezone.now() + ) -- GitLab