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