From 54675fe3233a385130700f805adf443698def11d Mon Sep 17 00:00:00 2001
From: Jonas Braathen <jonas.braathen@usit.uio.no>
Date: Fri, 4 Mar 2022 09:09:30 +0100
Subject: [PATCH] Add an invitation status field to the guest serializer for
 use in the UI.

Move serializers only used by gregui.
---
 greg/api/serializers/identity.py           |   6 --
 greg/api/serializers/person.py             |  56 +----------
 greg/api/serializers/role.py               |  43 +-------
 gregui/api/serializers/guest.py            | 108 ++++++++++++++++++---
 gregui/api/serializers/identity.py         |   6 ++
 gregui/api/serializers/role.py             |  47 +++++++++
 gregui/api/views/person.py                 |   7 +-
 gregui/tests/api/serializers/test_guest.py |  58 +++++++++++
 8 files changed, 210 insertions(+), 121 deletions(-)
 create mode 100644 gregui/tests/api/serializers/test_guest.py

diff --git a/greg/api/serializers/identity.py b/greg/api/serializers/identity.py
index bd0ce0ba..337f5e54 100644
--- a/greg/api/serializers/identity.py
+++ b/greg/api/serializers/identity.py
@@ -16,9 +16,3 @@ class IdentitySerializer(serializers.ModelSerializer):
         if self.is_duplicate(attrs["type"], attrs["value"]):
             raise ValidationError("Identity already exists")
         return attrs
-
-
-class SpecialIdentitySerializer(serializers.ModelSerializer):
-    class Meta:
-        model = Identity
-        fields = ["id", "value", "type", "verified_at"]
diff --git a/greg/api/serializers/person.py b/greg/api/serializers/person.py
index a92a9d57..5c0a749d 100644
--- a/greg/api/serializers/person.py
+++ b/greg/api/serializers/person.py
@@ -1,9 +1,8 @@
 from rest_framework import serializers
-from rest_framework.fields import BooleanField, CharField, SerializerMethodField
 
 from greg.api.serializers.consent import ConsentSerializerBrief
-from greg.api.serializers.identity import IdentitySerializer, SpecialIdentitySerializer
-from greg.api.serializers.role import RoleSerializer, SpecialRoleSerializer
+from greg.api.serializers.identity import IdentitySerializer
+from greg.api.serializers.role import RoleSerializer
 from greg.models import Person
 
 
@@ -25,54 +24,3 @@ class PersonSerializer(serializers.ModelSerializer):
             "roles",
             "consents",
         ]
-
-
-class SpecialPersonSerializer(serializers.ModelSerializer):
-    """
-    Serializer for the person endpoint
-
-    Can be used to change or add an email to the person
-
-    """
-
-    pid = CharField(source="id", read_only=True)
-    first = CharField(source="first_name", read_only=True)
-    last = CharField(source="last_name", read_only=True)
-    email = SerializerMethodField(source="private_email")
-    mobile = SerializerMethodField(source="private_mobile", read_only=True)
-    fnr = SpecialIdentitySerializer(read_only=True)
-    passport = SpecialIdentitySerializer(read_only=True)
-    feide_id = SerializerMethodField(source="feide_id", read_only=True)
-    active = SerializerMethodField(source="active", read_only=True)
-    registered = BooleanField(source="is_registered", read_only=True)
-    verified = BooleanField(source="is_verified", read_only=True)
-    roles = SpecialRoleSerializer(many=True, read_only=True)
-
-    def get_email(self, obj):
-        return obj.private_email and obj.private_email.value
-
-    def get_mobile(self, obj):
-        return obj.private_mobile and obj.private_mobile.value
-
-    def get_active(self, obj):
-        return obj.is_registered and obj.is_verified
-
-    def get_feide_id(self, obj):
-        return obj.feide_id and obj.feide_id.value
-
-    class Meta:
-        model = Person
-        fields = [
-            "pid",
-            "first",
-            "last",
-            "mobile",
-            "fnr",
-            "email",
-            "passport",
-            "feide_id",
-            "active",
-            "registered",
-            "verified",
-            "roles",
-        ]
diff --git a/greg/api/serializers/role.py b/greg/api/serializers/role.py
index acdb7b4a..1ef704c4 100644
--- a/greg/api/serializers/role.py
+++ b/greg/api/serializers/role.py
@@ -1,5 +1,5 @@
 from rest_framework import serializers
-from rest_framework.fields import IntegerField, SerializerMethodField
+from rest_framework.fields import IntegerField
 
 from greg.api.serializers.organizational_unit import OrganizationalUnitSerializer
 from greg.models import Role, RoleType
@@ -42,44 +42,3 @@ class RoleWriteSerializer(RoleSerializer):
     """
 
     orgunit = IntegerField(source="orgunit_id")  # type: ignore
-
-
-class SpecialRoleSerializer(serializers.ModelSerializer):
-    name_nb = SerializerMethodField(source="type")
-    name_en = SerializerMethodField(source="type")
-    ou_nb = SerializerMethodField(source="orgunit")
-    ou_en = SerializerMethodField(source="orgunit")
-    max_days = SerializerMethodField(source="type")
-
-    def get_name_nb(self, obj):
-        return obj.type.name_nb
-
-    def get_name_en(self, obj):
-        return obj.type.name_en
-
-    def get_ou_nb(self, obj):
-        return obj.orgunit.name_nb
-
-    def get_ou_en(self, obj):
-        return obj.orgunit.name_en
-
-    def get_max_days(self, obj):
-        return obj.type.max_days
-
-    class Meta:
-        model = Role
-        fields = [
-            "id",
-            "name_nb",
-            "name_en",
-            "ou_nb",
-            "ou_en",
-            "start_date",
-            "end_date",
-            "max_days",
-            "contact_person_unit",
-            "comments",
-        ]
-        read_only_fields = [
-            "contact_person_unit",
-        ]
diff --git a/gregui/api/serializers/guest.py b/gregui/api/serializers/guest.py
index 0cfe4b4c..33607815 100644
--- a/gregui/api/serializers/guest.py
+++ b/gregui/api/serializers/guest.py
@@ -1,19 +1,50 @@
 import datetime
 
 from django.conf import settings
-from django.utils.timezone import now
+from django.utils import timezone
 from rest_framework import serializers
 from rest_framework.exceptions import ValidationError
+from rest_framework.fields import BooleanField, CharField, SerializerMethodField
 
+<<<<<<< HEAD
 from greg.models import Consent, ConsentChoice, ConsentType, Identity, Person
 from greg.utils import is_identity_duplicate
 from gregui.api.serializers.IdentityDuplicateError import IdentityDuplicateError
+=======
+
+from greg.models import (
+    Consent,
+    ConsentChoice,
+    ConsentType,
+    Identity,
+    Person,
+    InvitationLink,
+)
+from gregui.api.serializers.identity import PartialIdentitySerializer
+from gregui.api.serializers.role import ExtendedRoleSerializer
+>>>>>>> 5be5116 (Add an invitation status field to the guest serializer for use in the UI.)
 from gregui.validation import (
     validate_phone_number,
     validate_norwegian_national_id_number,
 )
 
 
+def create_identity_or_update(
+    identity_type: Identity.IdentityType, value: str, person: Person
+):
+    existing_identity = person.identities.filter(type=identity_type).first()
+    if not existing_identity:
+        Identity.objects.create(
+            person=person,
+            type=identity_type,
+            source=settings.DEFAULT_IDENTITY_SOURCE,
+            value=value,
+        )
+    else:
+        existing_identity.value = value
+        existing_identity.save()
+
+
 # pylint: disable=W0223
 class GuestConsentChoiceSerializer(serializers.Serializer):
     type = serializers.CharField(required=True)
@@ -107,7 +138,7 @@ class GuestRegisterSerializer(serializers.ModelSerializer):
                 type=consent_type,
                 person=person,
                 choice=choice,
-                defaults={"consent_given_at": now()},
+                defaults={"consent_given_at": timezone.now()},
             )
             if not created and consent_instance.choice != choice:
                 consent_instance.choice = choice
@@ -194,17 +225,64 @@ class GuestRegisterSerializer(serializers.ModelSerializer):
         read_only_fields = ("id",)
 
 
-def create_identity_or_update(
-    identity_type: Identity.IdentityType, value: str, person: Person
-):
-    existing_identity = person.identities.filter(type=identity_type).first()
-    if not existing_identity:
-        Identity.objects.create(
-            person=person,
-            type=identity_type,
-            source=settings.DEFAULT_IDENTITY_SOURCE,
-            value=value,
+class GuestSerializer(serializers.ModelSerializer):
+    """
+    Serializer used for presenting guests to sponsors.
+    """
+
+    pid = CharField(source="id", read_only=True)
+    first = CharField(source="first_name", read_only=True)
+    last = CharField(source="last_name", read_only=True)
+    email = SerializerMethodField(source="private_email")
+    mobile = SerializerMethodField(source="private_mobile", read_only=True)
+    fnr = PartialIdentitySerializer(read_only=True)
+    passport = PartialIdentitySerializer(read_only=True)
+    feide_id = SerializerMethodField(source="feide_id", read_only=True)
+    active = SerializerMethodField(source="active", read_only=True)
+    registered = BooleanField(source="is_registered", read_only=True)
+    verified = BooleanField(source="is_verified", read_only=True)
+    invitation_status = SerializerMethodField(
+        source="get_invitation_status", read_only=True
+    )
+    roles = ExtendedRoleSerializer(many=True, read_only=True)
+
+    def get_email(self, obj):
+        return obj.private_email and obj.private_email.value
+
+    def get_mobile(self, obj):
+        return obj.private_mobile and obj.private_mobile.value
+
+    def get_active(self, obj):
+        return obj.is_registered and obj.is_verified
+
+    def get_feide_id(self, obj):
+        return obj.feide_id and obj.feide_id.value
+
+    def get_invitation_status(self, obj):
+        invitation_links = InvitationLink.objects.filter(
+            invitation__role__person__id=obj.id
         )
-    else:
-        existing_identity.value = value
-        existing_identity.save()
+        non_expired_links = invitation_links.filter(expire__gt=timezone.now())
+        if non_expired_links.count():
+            return "active"
+        if invitation_links.count():
+            return "expired"
+        return "none"
+
+    class Meta:
+        model = Person
+        fields = [
+            "pid",
+            "first",
+            "last",
+            "mobile",
+            "fnr",
+            "email",
+            "passport",
+            "feide_id",
+            "active",
+            "registered",
+            "verified",
+            "invitation_status",
+            "roles",
+        ]
diff --git a/gregui/api/serializers/identity.py b/gregui/api/serializers/identity.py
index 6395cf12..44412cbe 100644
--- a/gregui/api/serializers/identity.py
+++ b/gregui/api/serializers/identity.py
@@ -61,3 +61,9 @@ class IdentitySerializer(serializers.ModelSerializer):
         attrs["verified_by"] = self._get_sponsor()
         attrs["verified_at"] = timezone.now()
         return attrs
+
+
+class PartialIdentitySerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Identity
+        fields = ["id", "value", "type", "verified_at"]
diff --git a/gregui/api/serializers/role.py b/gregui/api/serializers/role.py
index b092f8b8..40b2e809 100644
--- a/gregui/api/serializers/role.py
+++ b/gregui/api/serializers/role.py
@@ -1,5 +1,6 @@
 import datetime
 from rest_framework import serializers
+from rest_framework.fields import SerializerMethodField
 from rest_framework.exceptions import ValidationError
 from rest_framework.validators import UniqueTogetherValidator
 
@@ -89,3 +90,49 @@ class InviteRoleSerializerUi(RoleSerializerUi):
             "comments",
             "available_in_search",
         ]
+
+
+class ExtendedRoleSerializer(serializers.ModelSerializer):
+    """
+    A role serializer with additional human readable names for the
+    role type and associated organizational unit.
+    """
+
+    name_nb = SerializerMethodField(source="type")
+    name_en = SerializerMethodField(source="type")
+    ou_nb = SerializerMethodField(source="orgunit")
+    ou_en = SerializerMethodField(source="orgunit")
+    max_days = SerializerMethodField(source="type")
+
+    def get_name_nb(self, obj):
+        return obj.type.name_nb
+
+    def get_name_en(self, obj):
+        return obj.type.name_en
+
+    def get_ou_nb(self, obj):
+        return obj.orgunit.name_nb
+
+    def get_ou_en(self, obj):
+        return obj.orgunit.name_en
+
+    def get_max_days(self, obj):
+        return obj.type.max_days
+
+    class Meta:
+        model = Role
+        fields = [
+            "id",
+            "name_nb",
+            "name_en",
+            "ou_nb",
+            "ou_en",
+            "start_date",
+            "end_date",
+            "max_days",
+            "contact_person_unit",
+            "comments",
+        ]
+        read_only_fields = [
+            "contact_person_unit",
+        ]
diff --git a/gregui/api/views/person.py b/gregui/api/views/person.py
index 06ea9d3b..32f027bf 100644
--- a/gregui/api/views/person.py
+++ b/gregui/api/views/person.py
@@ -6,13 +6,12 @@ from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
 from rest_framework.viewsets import GenericViewSet
 
-from greg.api.serializers.person import SpecialPersonSerializer
 from greg.models import Identity, Person
 from greg.permissions import IsSponsor
 from greg.utils import is_identity_duplicate
 from gregui import validation
 from gregui.api.serializers.IdentityDuplicateError import IdentityDuplicateError
-from gregui.api.serializers.guest import create_identity_or_update
+from gregui.api.serializers.guest import GuestSerializer, create_identity_or_update
 from gregui.models import GregUserProfile
 
 
@@ -30,7 +29,7 @@ class PersonViewSet(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, GenericV
     permission_classes = [IsAuthenticated, IsSponsor]
     queryset = Person.objects.all()
     http_methods = ["get", "patch"]
-    serializer_class = SpecialPersonSerializer
+    serializer_class = GuestSerializer
 
     def update(self, request, *args, **kwargs):
         """
@@ -169,7 +168,7 @@ class GuestInfoViewSet(mixins.ListModelMixin, GenericViewSet):
 
     authentication_classes = [SessionAuthentication, BasicAuthentication]
     permission_classes = [IsAuthenticated, IsSponsor]
-    serializer_class = SpecialPersonSerializer
+    serializer_class = GuestSerializer
 
     def get_queryset(self):
         """
diff --git a/gregui/tests/api/serializers/test_guest.py b/gregui/tests/api/serializers/test_guest.py
new file mode 100644
index 00000000..82176be4
--- /dev/null
+++ b/gregui/tests/api/serializers/test_guest.py
@@ -0,0 +1,58 @@
+from datetime import timedelta
+
+import pytest
+from django.conf import settings
+from django.utils import timezone
+
+from gregui.api.serializers.guest import GuestSerializer
+
+
+@pytest.mark.django_db
+def test_serialize_guest(invited_person):
+    person, _ = invited_person
+    assert GuestSerializer().to_representation(person) == {
+        "active": False,
+        "email": "foo@example.org",
+        "feide_id": None,
+        "first": "Foo",
+        "fnr": None,
+        "invitation_status": "active",
+        "last": "Bar",
+        "mobile": None,
+        "passport": None,
+        "pid": "1",
+        "registered": False,
+        "roles": [
+            {
+                "id": 1,
+                "name_nb": "Role Foo NB",
+                "name_en": "Role Foo EN",
+                "ou_nb": "Foo NB",
+                "ou_en": "Foo EN",
+                "start_date": None,
+                "end_date": "2050-10-15",
+                "max_days": 365,
+                "contact_person_unit": "",
+                "comments": "",
+            },
+        ],
+        "verified": False,
+    }
+
+
+@pytest.mark.django_db
+def test_invitation_status(invited_person):
+    s = GuestSerializer()
+    person, invitation = invited_person
+
+    # there's an active invitation link
+    assert s.to_representation(person).get("invitation_status") == "active"
+
+    # the invitation link has expired
+    invitation.expire = timezone.now() - timedelta(days=settings.INVITATION_DURATION)
+    invitation.save()
+    assert s.to_representation(person).get("invitation_status") == "expired"
+
+    # there are no invitation links
+    invitation.delete()
+    assert s.to_representation(person).get("invitation_status") == "none"
-- 
GitLab