Skip to content
Snippets Groups Projects
Commit 89170913 authored by Jonas Braathen's avatar Jonas Braathen
Browse files

Fix handling of private email and mobile for the invitation endpoints

parent 5a551bf8
No related branches found
No related tags found
1 merge request!107Fix handling of private email and mobile for the invitation endpoints
Pipeline #96828 failed
import uuid import uuid
from datetime import date from datetime import date
from typing import Optional
from dirtyfields import DirtyFieldsMixin from dirtyfields import DirtyFieldsMixin
from django.conf import settings from django.conf import settings
...@@ -63,6 +64,18 @@ class Person(BaseModel): ...@@ -63,6 +64,18 @@ class Person(BaseModel):
self.last_name, self.last_name,
) )
@property
def private_email(self) -> Optional["Identity"]:
"""The user provided private email address."""
return self.identities.filter(type=Identity.IdentityType.PRIVATE_EMAIL).first()
@property
def private_mobile(self) -> Optional["Identity"]:
"""The user provided private mobile number."""
return self.identities.filter(
type=Identity.IdentityType.PRIVATE_MOBILE_NUMBER
).first()
@property @property
def is_registered(self) -> bool: def is_registered(self) -> bool:
""" """
...@@ -257,10 +270,19 @@ class Identity(BaseModel): ...@@ -257,10 +270,19 @@ class Identity(BaseModel):
) )
verified_at = models.DateTimeField(null=True) verified_at = models.DateTimeField(null=True)
def __str__(self):
return "{}(id={!r}, type={!r}, value={!r})".format(
self.__class__.__name__,
self.pk,
self.type,
self.value,
)
def __repr__(self): def __repr__(self):
return "{}(id={!r}, type={!r}, source={!r}, value={!r}, verified_by={!r}, verified_at={!r})".format( return "{}(id={!r}, person_id={!r}, type={!r}, source={!r}, value={!r}, verified_by={!r}, verified_at={!r})".format(
self.__class__.__name__, self.__class__.__name__,
self.pk, self.pk,
self.person_id,
self.type, self.type,
self.source, self.source,
self.value, self.value,
......
...@@ -5,5 +5,5 @@ import pytest ...@@ -5,5 +5,5 @@ import pytest
def test_identity_repr(person_foo_verified): def test_identity_repr(person_foo_verified):
assert ( assert (
repr(person_foo_verified) repr(person_foo_verified)
== "Identity(id=3, type='passport_number', source='Test', value='12345', verified_by=Sponsor(id=1, feide_id='guy@example.org', first_name='Sponsor', last_name='Guy'), verified_at=datetime.datetime(2021, 6, 15, 12, 34, 56, tzinfo=<UTC>))" == "Identity(id=3, person_id=1, type='passport_number', source='Test', value='12345', verified_by=Sponsor(id=1, feide_id='guy@example.org', first_name='Sponsor', last_name='Guy'), verified_at=datetime.datetime(2021, 6, 15, 12, 34, 56, tzinfo=<UTC>))"
) )
...@@ -5,25 +5,43 @@ from greg.models import Identity, Person ...@@ -5,25 +5,43 @@ from greg.models import Identity, Person
class GuestRegisterSerializer(serializers.ModelSerializer): class GuestRegisterSerializer(serializers.ModelSerializer):
first_name = serializers.CharField(required=True)
last_name = serializers.CharField(required=True)
email = serializers.CharField(required=True) email = serializers.CharField(required=True)
mobile_phone = serializers.CharField(required=True)
def create(self, validated_data): def update(self, instance, validated_data):
# TODO: this serializer is untested
email = validated_data.pop("email") email = validated_data.pop("email")
with transaction.atomic(): mobile_phone = validated_data.pop("mobile_phone")
person = super().create(**validated_data)
if not instance.private_email:
Identity.objects.create( Identity.objects.create(
person=person, person=instance,
type=Identity.IdentityType.PRIVATE_EMAIL, type=Identity.IdentityType.PRIVATE_EMAIL,
value=email, value=email,
) )
return person else:
instance.private_email.value = email
instance.private_email.save()
if not instance.private_mobile:
Identity.objects.create(
person=instance,
type=Identity.IdentityType.PRIVATE_MOBILE_NUMBER,
value=mobile_phone,
)
else:
instance.private_mobile.value = mobile_phone
instance.private_mobile.save()
# TODO: we only want to allow changing the name if we don't have one
# from a reliable source (Feide/KORR)
instance.first_name = validated_data["first_name"]
instance.last_name = validated_data["last_name"]
return instance
class Meta: class Meta:
model = Person model = Person
fields = ("id", "first_name", "last_name", "email") fields = ("id", "first_name", "last_name", "email", "mobile_phone")
read_only_fields = ("id",) read_only_fields = ("id",)
extra_kwargs = {
"first_name": {"required": True},
"last_name": {"required": True},
}
...@@ -51,6 +51,3 @@ class InviteGuestSerializer(serializers.ModelSerializer): ...@@ -51,6 +51,3 @@ class InviteGuestSerializer(serializers.ModelSerializer):
"uuid", "uuid",
) )
read_only_field = ("uuid",) read_only_field = ("uuid",)
foo = InviteGuestSerializer()
...@@ -8,7 +8,7 @@ from django.http.response import JsonResponse ...@@ -8,7 +8,7 @@ from django.http.response import JsonResponse
from django.utils import timezone from django.utils import timezone
from rest_framework import serializers, status from rest_framework import serializers, status
from rest_framework.authentication import SessionAuthentication, BasicAuthentication from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView, GenericAPIView
from rest_framework.parsers import JSONParser from rest_framework.parsers import JSONParser
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
...@@ -64,6 +64,8 @@ class CreateInvitationView(CreateAPIView): ...@@ -64,6 +64,8 @@ class CreateInvitationView(CreateAPIView):
data=request.data, context={"request": request} data=request.data, context={"request": request}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
# TODO: check that sponsor has access to OU
person = serializer.save() person = serializer.save()
invitationlink = InvitationLink.objects.filter( invitationlink = InvitationLink.objects.filter(
...@@ -99,7 +101,7 @@ class CheckInvitationView(APIView): ...@@ -99,7 +101,7 @@ class CheckInvitationView(APIView):
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
class InvitedGuestView(APIView): class InvitedGuestView(GenericAPIView):
authentication_classes = [SessionAuthentication, BasicAuthentication] authentication_classes = [SessionAuthentication, BasicAuthentication]
permission_classes = [AllowAny] permission_classes = [AllowAny]
parser_classes = [JSONParser] parser_classes = [JSONParser]
...@@ -140,8 +142,8 @@ class InvitedGuestView(APIView): ...@@ -140,8 +142,8 @@ class InvitedGuestView(APIView):
"person": { "person": {
"first_name": person.first_name, "first_name": person.first_name,
"last_name": person.last_name, "last_name": person.last_name,
"email": person.email, "email": person.private_email and person.private_email.value,
"mobile_phone": person.mobile_phone, "mobile_phone": person.private_mobile and person.private_mobile.value,
"fnr": fnr, "fnr": fnr,
"passport": passport, "passport": passport,
}, },
...@@ -172,22 +174,20 @@ class InvitedGuestView(APIView): ...@@ -172,22 +174,20 @@ class InvitedGuestView(APIView):
invite_id = request.session.get("invite_id") invite_id = request.session.get("invite_id")
data = request.data data = request.data
# Ensure the invitation link is valid and not expired
try:
invite_link = InvitationLink.objects.get(uuid=invite_id)
except (InvitationLink.DoesNotExist, exceptions.ValidationError):
return Response(status=status.HTTP_403_FORBIDDEN)
if invite_link.expire <= timezone.now():
return Response(status=status.HTTP_403_FORBIDDEN)
person = invite_link.invitation.role.person
with transaction.atomic(): with transaction.atomic():
# Ensure the invitation link is valid and not expired serializer = self.get_serializer(instance=person, data=request.data)
try: serializer.is_valid(raise_exception=True)
invite_link = InvitationLink.objects.get(uuid=invite_id) person = serializer.save()
except (InvitationLink.DoesNotExist, exceptions.ValidationError):
return Response(status=status.HTTP_403_FORBIDDEN)
if invite_link.expire <= timezone.now():
return Response(status=status.HTTP_403_FORBIDDEN)
# Get objects to update
person = invite_link.invitation.role.person
# Update with input from the guest
mobile = data.get("mobile_phone")
if mobile:
person.mobile_phone = data["mobile_phone"]
# Mark guest interaction done # Mark guest interaction done
person.registration_completed_date = timezone.now().date() person.registration_completed_date = timezone.now().date()
...@@ -197,4 +197,4 @@ class InvitedGuestView(APIView): ...@@ -197,4 +197,4 @@ class InvitedGuestView(APIView):
invite_link.expire = timezone.now() invite_link.expire = timezone.now()
invite_link.save() invite_link.save()
# TODO: Send an email to the sponsor? # TODO: Send an email to the sponsor?
return Response(status=status.HTTP_201_CREATED) return Response(status=status.HTTP_200_OK)
...@@ -4,7 +4,7 @@ from rest_framework import status ...@@ -4,7 +4,7 @@ from rest_framework import status
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.test import APIClient from rest_framework.test import APIClient
from greg.models import InvitationLink, Person from greg.models import Identity, InvitationLink, Person
@pytest.mark.django_db @pytest.mark.django_db
...@@ -43,7 +43,9 @@ def test_get_invited_info_no_session(client, invitation_link): ...@@ -43,7 +43,9 @@ def test_get_invited_info_no_session(client, invitation_link):
@pytest.mark.django_db @pytest.mark.django_db
def test_get_invited_info_session_okay(client, invitation_link): def test_get_invited_info_session_okay(
client, invitation_link, person_foo_data, sponsor_guy_data, role_type_foo, unit_foo
):
# get a session # get a session
client.get( client.get(
reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid}) reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
...@@ -52,20 +54,39 @@ def test_get_invited_info_session_okay(client, invitation_link): ...@@ -52,20 +54,39 @@ def test_get_invited_info_session_okay(client, invitation_link):
response = client.get(reverse("gregui-v1:invited-info")) response = client.get(reverse("gregui-v1:invited-info"))
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
data = response.json() data = response.json()
assert data.get("person") assert data.get("person") == dict(
assert data.get("sponsor") **person_foo_data,
assert data.get("role") email=None,
mobile_phone=None,
fnr=None,
passport=None,
)
assert data.get("sponsor") == dict(
first_name=sponsor_guy_data["first_name"],
last_name=sponsor_guy_data["last_name"],
)
assert data.get("role") == dict(
start=None,
end="2050-10-15",
comments="",
ou_name_en=unit_foo.name_en,
ou_name_nb=unit_foo.name_nb,
role_name_en=role_type_foo.name_en,
role_name_nb=role_type_foo.name_nb,
)
@pytest.mark.django_db @pytest.mark.django_db
def test_get_invited_info_expired_link(client, invitation_link): def test_get_invited_info_expired_link(
client, invitation_link, invitation_expired_date
):
# Get a session while link is valid # Get a session while link is valid
client.get( client.get(
reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid}) reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
) )
# Set expire link to expire long ago # Set expire link to expire long ago
invlink = InvitationLink.objects.get(uuid=invitation_link.uuid) invlink = InvitationLink.objects.get(uuid=invitation_link.uuid)
invlink.expire = "1970-01-01" invlink.expire = invitation_expired_date
invlink.save() invlink.save()
# Make a get request that should fail because invite expired after login, but # Make a get request that should fail because invite expired after login, but
# before get to userinfo # before get to userinfo
...@@ -74,45 +95,46 @@ def test_get_invited_info_expired_link(client, invitation_link): ...@@ -74,45 +95,46 @@ def test_get_invited_info_expired_link(client, invitation_link):
@pytest.mark.django_db @pytest.mark.django_db
def test_post_invited_info_ok_mobile_update(client: APIClient, invitation_link): def test_invited_guest_can_post_information(
client: APIClient, invitation_link, person_foo_data
):
# get a session # get a session
client.get( client.get(
reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid}) reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
) )
person = invitation_link.invitation.role.person
assert person.private_mobile is None
# post updated info to confirm from guest # post updated info to confirm from guest
new_phone = "12345678" new_email = "private@example.org"
post_data = {"mobile_phone": new_phone} new_phone = "+4712345678"
data = dict(email=new_email, mobile_phone=new_phone, **person_foo_data)
response = client.post( response = client.post(
reverse("gregui-v1:invited-info"), reverse("gregui-v1:invited-info"),
post_data, data,
format="json", format="json",
) )
assert response.status_code == status.HTTP_201_CREATED print(response.content)
# Check that the object was updated in the database assert response.status_code == status.HTTP_200_OK
person = Person.objects.get(id=invitation_link.invitation.role.person.id)
assert person.mobile_phone == new_phone
@pytest.mark.django_db # Check that the object was updated in the database
def test_post_invited_info_ok(client, invitation_link): assert Person.objects.count() == 1
# get a session assert person.private_email.value == new_email
client.get( assert person.private_mobile.value == new_phone
reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
)
# post updated info to confirm from guest
response = client.post(reverse("gregui-v1:invited-info"))
assert response.status_code == status.HTTP_201_CREATED
@pytest.mark.django_db @pytest.mark.django_db
def test_post_invited_info_expired_session(client, invitation_link): def test_post_invited_info_expired_session(
client, invitation_link, invitation_expired_date
):
# get a session # get a session
client.get( client.get(
reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid}) reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
) )
# Set expire link to expire long ago # Set expire link to expire long ago
invlink = InvitationLink.objects.get(uuid=invitation_link.uuid) invlink = InvitationLink.objects.get(uuid=invitation_link.uuid)
invlink.expire = "1970-01-01" invlink.expire = invitation_expired_date
invlink.save() invlink.save()
# post updated info to confirm from guest, should fail because of expired # post updated info to confirm from guest, should fail because of expired
# invitation link # invitation link
......
import datetime
import logging import logging
import pytest
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils.timezone import make_aware
from rest_framework.authtoken.admin import User from rest_framework.authtoken.admin import User
from rest_framework.test import APIClient from rest_framework.test import APIClient
import pytest
from greg.models import ( from greg.models import (
Invitation, Invitation,
...@@ -30,21 +34,28 @@ def client() -> APIClient: ...@@ -30,21 +34,28 @@ def client() -> APIClient:
@pytest.fixture @pytest.fixture
def unit_foo() -> OrganizationalUnit: def unit_foo() -> OrganizationalUnit:
ou = OrganizationalUnit.objects.create(orgreg_id="12345", name_en="foo_unit") ou = OrganizationalUnit.objects.create(
orgreg_id="12345", name_en="Foo EN", name_nb="Foo NB"
)
return OrganizationalUnit.objects.get(id=ou.id) return OrganizationalUnit.objects.get(id=ou.id)
@pytest.fixture @pytest.fixture
def role_type_foo() -> RoleType: def role_type_foo() -> RoleType:
rt = RoleType.objects.create(identifier="role_foo", name_en="Role Foo") rt = RoleType.objects.create(
identifier="role_foo", name_en="Role Foo EN", name_nb="Role Foo NB"
)
return RoleType.objects.get(id=rt.id) return RoleType.objects.get(id=rt.id)
@pytest.fixture @pytest.fixture
def sponsor_guy(unit_foo: OrganizationalUnit) -> Sponsor: def sponsor_guy_data() -> dict:
sponsor = Sponsor.objects.create( return dict(feide_id="guy@example.org", first_name="Sponsor", last_name="Guy")
feide_id="guy@example.org", first_name="Sponsor", last_name="Guy"
)
@pytest.fixture
def sponsor_guy(unit_foo: OrganizationalUnit, sponsor_guy_data) -> Sponsor:
sponsor = Sponsor.objects.create(**sponsor_guy_data)
sponsor.units.add(unit_foo, through_defaults={"hierarchical_access": False}) sponsor.units.add(unit_foo, through_defaults={"hierarchical_access": False})
return Sponsor.objects.get(id=sponsor.id) return Sponsor.objects.get(id=sponsor.id)
...@@ -67,8 +78,16 @@ def user_sponsor(sponsor_guy: Sponsor) -> User: ...@@ -67,8 +78,16 @@ def user_sponsor(sponsor_guy: Sponsor) -> User:
@pytest.fixture @pytest.fixture
def person() -> Person: def person_foo_data() -> dict:
pe = Person.objects.create() return dict(
first_name="Foo",
last_name="Bar",
)
@pytest.fixture
def person(person_foo_data) -> Person:
pe = Person.objects.create(**person_foo_data)
return Person.objects.get(id=pe.id) return Person.objects.get(id=pe.id)
...@@ -91,12 +110,26 @@ def invitation(role) -> Invitation: ...@@ -91,12 +110,26 @@ def invitation(role) -> Invitation:
@pytest.fixture @pytest.fixture
def invitation_link(invitation) -> InvitationLink: def invitation_valid_date() -> datetime.datetime:
il = InvitationLink.objects.create(invitation=invitation, expire="2060-10-15") return make_aware(datetime.datetime(2060, 10, 15))
@pytest.fixture
def invitation_expired_date() -> datetime.datetime:
return make_aware(datetime.datetime(1970, 1, 1))
@pytest.fixture
def invitation_link(invitation, invitation_valid_date) -> InvitationLink:
il = InvitationLink.objects.create(
invitation=invitation, expire=invitation_valid_date
)
return InvitationLink.objects.get(id=il.id) return InvitationLink.objects.get(id=il.id)
@pytest.fixture @pytest.fixture
def invitation_link_expired(invitation) -> InvitationLink: def invitation_link_expired(invitation, invitation_expired_date) -> InvitationLink:
il = InvitationLink.objects.create(invitation=invitation, expire="1970-01-01") il = InvitationLink.objects.create(
invitation=invitation, expire=invitation_expired_date
)
return InvitationLink.objects.get(id=il.id) return InvitationLink.objects.get(id=il.id)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment