Skip to content
Snippets Groups Projects
Commit d9cf3249 authored by Tore.Brede's avatar Tore.Brede
Browse files

Merge branch 'GREG-20/2' into 'master'

Heuristic for telling if guest is registered and verified

See merge request !26
parents c8c3847a 22ebe900
No related branches found
No related tags found
1 merge request!26Heuristic for telling if guest is registered and verified
Pipeline #90775 passed
from django_filters import rest_framework as filters from django_filters.rest_framework import (
BaseInFilter,
BooleanFilter,
FilterSet,
)
from greg.models import Person, PersonRole, PersonIdentity from greg.models import (
Person,
PersonRole,
PersonIdentity,
)
class PersonRoleFilter(filters.FilterSet): class PersonRoleFilter(FilterSet):
type = filters.BaseInFilter(field_name="role__type", lookup_expr="in") type = BaseInFilter(field_name="role__type", lookup_expr="in")
class Meta: class Meta:
model = PersonRole model = PersonRole
fields = ["type"] fields = ["type"]
class PersonFilter(filters.FilterSet): class PersonFilter(FilterSet):
verified = filters.BooleanFilter( verified = BooleanFilter(
field_name="person__verified_by_id", lookup_expr="isnull", exclude=True field_name="identities__verified_by_id", lookup_expr="isnull", exclude=True
) )
class Meta: class Meta:
...@@ -21,7 +29,7 @@ class PersonFilter(filters.FilterSet): ...@@ -21,7 +29,7 @@ class PersonFilter(filters.FilterSet):
fields = ["first_name", "last_name", "verified"] fields = ["first_name", "last_name", "verified"]
class PersonIdentityFilter(filters.FilterSet): class PersonIdentityFilter(FilterSet):
class Meta: class Meta:
model = PersonIdentity model = PersonIdentity
fields = ["type", "verified_by_id"] fields = ["type", "verified_by_id"]
...@@ -36,7 +36,7 @@ class PersonViewSet(viewsets.ModelViewSet): ...@@ -36,7 +36,7 @@ class PersonViewSet(viewsets.ModelViewSet):
] ]
) )
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
return super().list(request) return super().list(request, *args, **kwargs)
class PersonRoleViewSet(viewsets.ModelViewSet): class PersonRoleViewSet(viewsets.ModelViewSet):
......
from datetime import date from datetime import (
date,
datetime,
)
from dirtyfields import DirtyFieldsMixin from dirtyfields import DirtyFieldsMixin
from django.db import models from django.db import models
...@@ -57,6 +60,65 @@ class Person(BaseModel): ...@@ -57,6 +60,65 @@ class Person(BaseModel):
self.last_name, self.last_name,
) )
@property
def is_registered(self) -> bool:
"""
A registered guest is a person who has completed
the registration process.
The registration process requires that the guest has verified
their email address via a token link, filled in the required
personal information along with providing at least one
verification method, and accepted the institution's mandatory
consent forms.
The registered guest may or may not already be verified,
depending on the verification method. However, before a
guest is cleared for account creation at the institution's
IGA, the guest must be both registered (``is_registered``)
and verified (``is_verified``).
"""
# registration_completed_date is set only after accepting consents
return (
self.registration_completed_date is not None
and self.registration_completed_date <= datetime.now()
)
@property
def is_verified(self) -> bool:
"""
A verified guest is a person whom has had their personal
identity verified.
Due to the diversity of guests at a university institution,
there are many ways for guests to identify themselves.
These include Feide ID, passport number, driver's license,
national ID card, or another manual (human) verification.
Some of these methods are implicitly trusted (Feide ID) because
the guest is likely a visitor from another academic institution
who has already been pre-verified. Others are manul, such
as the sponsor vouching for having checked the guest's
personal details against his or her passport.
The verified guest may or may not have completed
the registration process which implies that it is only
the combination of being registered (``is_registered``)
and being verified (``is_verified``) that qualifies for being cleared
for account creation in the IGA.
Note that we do not distinguish between the quality,
authenticity, or trust level of the guest's associated identities.
"""
# the requirement is minimum one personal identity
return (
self.identities.filter(
verified_when__isnull=False,
verified_when__lte=datetime.now(),
).count()
>= 1
)
class Role(BaseModel): class Role(BaseModel):
"""A role variant.""" """A role variant."""
...@@ -155,7 +217,7 @@ class PersonIdentity(BaseModel): ...@@ -155,7 +217,7 @@ class PersonIdentity(BaseModel):
MANUAL = "MANUAL" MANUAL = "MANUAL"
person = models.ForeignKey( person = models.ForeignKey(
"Person", on_delete=models.CASCADE, related_name="person" "Person", on_delete=models.CASCADE, related_name="identities"
) )
type = models.CharField(max_length=18, choices=IdentityType.choices) type = models.CharField(max_length=18, choices=IdentityType.choices)
source = models.CharField(max_length=256) source = models.CharField(max_length=256)
...@@ -167,15 +229,14 @@ class PersonIdentity(BaseModel): ...@@ -167,15 +229,14 @@ class PersonIdentity(BaseModel):
verified_when = models.DateField(null=True) verified_when = models.DateField(null=True)
def __repr__(self): def __repr__(self):
return ( return "{}(id={!r}, type={!r}, source={!r}, value={!r}, verified_by={!r}, verified_when={!r})".format(
"{}(id={!r}, type={!r}, source={!r}, value={!r}, verified_by={!r})".format( self.__class__.__name__,
self.__class__.__name__, self.pk,
self.pk, self.type,
self.type, self.source,
self.source, self.value,
self.value, self.verified_by,
self.verified_by, self.verified_when,
)
) )
......
from datetime import (
datetime,
timedelta,
)
from functools import partial from functools import partial
import pytest import pytest
...@@ -5,6 +9,7 @@ import pytest ...@@ -5,6 +9,7 @@ import pytest
from greg.models import ( from greg.models import (
OrganizationalUnit, OrganizationalUnit,
Person, Person,
PersonIdentity,
PersonRole, PersonRole,
Role, Role,
Sponsor, Sponsor,
...@@ -30,7 +35,7 @@ def role_bar() -> Role: ...@@ -30,7 +35,7 @@ def role_bar() -> Role:
@pytest.fixture @pytest.fixture
def person(role_foo, role_bar) -> Person: def person(role_foo: Role, role_bar: Role) -> Person:
person = Person.objects.create( person = Person.objects.create(
first_name="Test", first_name="Test",
last_name="Tester", last_name="Tester",
...@@ -56,9 +61,117 @@ def person(role_foo, role_bar) -> Person: ...@@ -56,9 +61,117 @@ def person(role_foo, role_bar) -> Person:
return person return person
@pytest.fixture
def a_year_ago() -> datetime:
return datetime.now() - timedelta(days=365)
@pytest.fixture
def a_year_into_future() -> datetime:
return datetime.now() + timedelta(days=365)
@pytest.fixture
def feide_id(a_year_ago: datetime) -> PersonIdentity:
return PersonIdentity(
type=PersonIdentity.IdentityType.FEIDE_ID,
verified_when=a_year_ago,
)
@pytest.fixture
def passport(a_year_ago: datetime) -> PersonIdentity:
return PersonIdentity(
type=PersonIdentity.IdentityType.PASSPORT,
verified_when=a_year_ago,
)
@pytest.fixture
def unverified_passport() -> PersonIdentity:
return PersonIdentity(type=PersonIdentity.IdentityType.PASSPORT)
@pytest.fixture
def future_identity(a_year_into_future: datetime) -> PersonIdentity:
return PersonIdentity(
type=PersonIdentity.IdentityType.NATIONAL_ID_CARD,
verified_when=a_year_into_future,
)
@pytest.fixture
def feide_verified(person: Person, feide_id: PersonIdentity) -> Person:
person.identities.add(feide_id, bulk=False)
return person
@pytest.mark.django_db @pytest.mark.django_db
def test_add_multiple_roles_to_person(person, role_foo, role_bar): def test_add_multiple_roles_to_person(person, role_foo, role_bar):
person_roles = person.roles.all() person_roles = person.roles.all()
assert len(person_roles) == 2 assert len(person_roles) == 2
assert role_foo in person_roles assert role_foo in person_roles
assert role_bar in person_roles assert role_bar in person_roles
@pytest.mark.django_db
def test_identities(person: Person, feide_id: PersonIdentity, passport: PersonIdentity):
person.identities.add(feide_id, bulk=False)
assert list(person.identities.all()) == [feide_id]
person.identities.add(passport, bulk=False)
assert list(person.identities.all()) == [feide_id, passport]
@pytest.mark.django_db
def test_is_registered_incomplete(person):
assert person.registration_completed_date is None
assert not person.is_registered
@pytest.mark.django_db
def test_is_registered_completed_in_past(person, a_year_ago):
person.registration_completed_date = a_year_ago
assert person.is_registered
@pytest.mark.django_db
def test_is_registered_completed_in_future(person, a_year_into_future):
person.registration_completed_date = a_year_into_future
assert not person.is_registered
@pytest.mark.django_db
@pytest.mark.parametrize("identity_type", PersonIdentity.IdentityType)
def test_is_verified(identity_type, a_year_ago, person):
identity = PersonIdentity(type=identity_type, verified_when=a_year_ago)
person.identities.add(identity, bulk=False)
assert person.is_verified
@pytest.mark.django_db
def test_is_verified_multiple_identities(person, feide_id, passport):
person.identities.add(feide_id, passport, bulk=False)
assert person.is_verified
@pytest.mark.django_db
def is_verified_when_identity_is_unverified(person, unverified_passport):
person.identities.add(unverified_passport, bulk=False)
assert not person.is_verified
@pytest.mark.django_db
def is_verified_mixed_verified_and_unverified_identities(
person,
feide_id,
unverified_passport,
future_identity,
):
person.identities.add(feide_id, unverified_passport, future_identity, bulk=False)
assert person.is_verified
@pytest.mark.django_db
def is_verified_in_future(person, future_identity):
person.identities.add(future_identity, bulk=False)
assert not person.is_verified
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