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