diff --git a/greg/api/filters.py b/greg/api/filters.py index 307fe3202146880b9c943e485356ac6e95558476..14ef355eb0bc7935dc090f5cf189055e2df78765 100644 --- a/greg/api/filters.py +++ b/greg/api/filters.py @@ -1,6 +1,6 @@ from django_filters import rest_framework as filters -from greg.models import Person, PersonRole +from greg.models import Person, PersonRole, PersonIdentity class PersonRoleFilter(filters.FilterSet): @@ -19,3 +19,9 @@ class PersonFilter(filters.FilterSet): class Meta: model = Person fields = ["first_name", "last_name", "verified"] + + +class PersonIdentityFilter(filters.FilterSet): + class Meta: + model = PersonIdentity + fields = ["type", "verified_by_id"] diff --git a/greg/api/serializers/person.py b/greg/api/serializers/person.py index 77eec13afec9c235552e84b503b085ee80c44a77..f4e5f2b29452ff926c536ebcd13c313aaa510551 100644 --- a/greg/api/serializers/person.py +++ b/greg/api/serializers/person.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from greg.models import Person, PersonRole, Role +from greg.models import Person, PersonRole, Role, PersonIdentity class PersonSerializer(serializers.ModelSerializer): @@ -31,3 +31,9 @@ class PersonRoleSerializer(serializers.ModelSerializer): "updated", "role", ] + + +class PersonIdentitySerializer(serializers.ModelSerializer): + class Meta: + model = PersonIdentity + fields = "__all__" diff --git a/greg/api/urls.py b/greg/api/urls.py index 69743c28704421b8128bbe25b19f66d3e7412a42..a589fe46911f3cc8193e20e7516b49d1d96d2598 100644 --- a/greg/api/urls.py +++ b/greg/api/urls.py @@ -13,6 +13,7 @@ from greg.api.views.organizational_unit import OrganizationalUnitViewSet from greg.api.views.person import ( PersonRoleViewSet, PersonViewSet, + PersonIdentityViewSet, ) from greg.api.views.role import RoleViewSet from greg.api.views.health import Health @@ -43,7 +44,19 @@ urlpatterns += [ ), re_path( r"^persons/(?P<person_id>[0-9]+)/roles/(?P<id>[0-9]+)/$", - PersonRoleViewSet.as_view({"get": "retrieve"}), + PersonRoleViewSet.as_view( + {"get": "retrieve", "patch": "partial_update", "delete": "destroy"} + ), name="person_role-detail", ), + re_path( + r"^persons/(?P<person_id>[0-9]+)/identities/$", + PersonIdentityViewSet.as_view({"get": "list", "post": "create"}), + name="person_identity-list", + ), + re_path( + r"^persons/(?P<person_id>[0-9]+)/identities/(?P<id>[0-9]+)$", + PersonIdentityViewSet.as_view({"delete": "destroy", "patch": "partial_update"}), + name="person_identity-detail", + ), ] diff --git a/greg/api/views/person.py b/greg/api/views/person.py index 3566e930e53ba46bc3f6682d0b59eddbc7e30632..9918256b5f05aba0416213c34d3245d08728f21a 100644 --- a/greg/api/views/person.py +++ b/greg/api/views/person.py @@ -1,12 +1,17 @@ from django.core.exceptions import ValidationError from django_filters import rest_framework as filters from drf_spectacular.utils import extend_schema, OpenApiParameter -from rest_framework import viewsets +from rest_framework import viewsets, status +from rest_framework.response import Response -from greg.api.filters import PersonFilter, PersonRoleFilter +from greg.api.filters import PersonFilter, PersonRoleFilter, PersonIdentityFilter from greg.api.pagination import PrimaryKeyCursorPagination -from greg.api.serializers.person import PersonSerializer, PersonRoleSerializer -from greg.models import Person, PersonRole +from greg.api.serializers.person import ( + PersonSerializer, + PersonRoleSerializer, + PersonIdentitySerializer, +) +from greg.models import Person, PersonRole, PersonIdentity class PersonViewSet(viewsets.ModelViewSet): @@ -42,6 +47,7 @@ class PersonRoleViewSet(viewsets.ModelViewSet): pagination_class = PrimaryKeyCursorPagination filter_backends = (filters.DjangoFilterBackend,) filterset_class = PersonRoleFilter + lookup_field = "id" def get_queryset(self): qs = self.queryset @@ -62,3 +68,45 @@ class PersonRoleViewSet(viewsets.ModelViewSet): raise ValidationError("No person id") serializer.save(person_id=person_id) + + +class PersonIdentityViewSet(viewsets.ModelViewSet): + """ + Person identity API + """ + + queryset = PersonIdentity.objects.all().order_by("id") + serializer_class = PersonIdentitySerializer + pagination_class = PrimaryKeyCursorPagination + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = PersonIdentityFilter + # This is set so that the id parameter in the path of the URL is used for looking up objects + lookup_url_kwarg = "id" + + def get_queryset(self): + qs = self.queryset + if not self.kwargs: + return qs.none() + person_id = self.kwargs["person_id"] + qs = qs.filter(person_id=person_id) + return qs + + def create(self, request, *args, **kwargs): + # Want to get the person id which is part of the API path and then + # include this with the data used to create the identity for the person + person_id = self.kwargs["person_id"] + + if person_id is None: + # Should not happen, the person ID is part of the API path + raise ValidationError("No person id") + + input_data = request.data.copy() + input_data["person"] = person_id + + serializer = self.get_serializer(data=input_data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response( + serializer.data, status=status.HTTP_201_CREATED, headers=headers + ) diff --git a/greg/migrations/0001_initial.py b/greg/migrations/0001_initial.py index 62250ae108abb1d187c24eb74c4fba07ee65b464..75cc9c477c8b3a9b1946f9e1e67a0a5513f98d79 100644 --- a/greg/migrations/0001_initial.py +++ b/greg/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.5 on 2021-08-05 13:10 +# Generated by Django 3.2.5 on 2021-08-04 11:07 import datetime import dirtyfields.dirtyfields @@ -166,7 +166,7 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True)), ('updated', models.DateTimeField(auto_now=True)), - ('type', models.CharField(choices=[('PASSPORT_NUMBER', 'Passport Number'), ('FEIDE_ID', 'Feide Id')], max_length=15)), + ('type', models.CharField(choices=[('ID_PORTEN', 'Id Porten'), ('FEIDE_ID', 'Feide Id'), ('PASSPORT', 'Passport'), ('DRIVERS_LICENSE', 'Drivers License'), ('NATIONAL_ID_CARD', 'National Id Card'), ('OTHER', 'Other')], max_length=16)), ('source', models.CharField(max_length=256)), ('value', models.CharField(max_length=256)), ('verified', models.CharField(blank=True, choices=[('AUTOMATIC', 'Automatic'), ('MANUAL', 'Manual')], max_length=9)), @@ -185,7 +185,7 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True)), ('updated', models.DateTimeField(auto_now=True)), - ('consent_given_at', models.DateField()), + ('consent_given_at', models.DateField(null=True)), ('consent', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='link_person_consent', to='greg.consent')), ('person', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='link_person_consent', to='greg.person')), ], diff --git a/greg/models.py b/greg/models.py index 46d4a4883581d4c6184d4cdd684605fe9cbfe229..a0db3da1a55c194ed4d5b04f52af2c7ed3c37239 100644 --- a/greg/models.py +++ b/greg/models.py @@ -131,10 +131,14 @@ class Notification(BaseModel): class PersonIdentity(BaseModel): - # TODO: Add more types class IdentityType(models.TextChoices): - PASSPORT_NUMBER = "PASSPORT_NUMBER" + ID_PORTEN = "ID_PORTEN" FEIDE_ID = "FEIDE_ID" + PASSPORT = "PASSPORT" + DRIVERS_LICENSE = "DRIVERS_LICENSE" + NATIONAL_ID_CARD = "NATIONAL_ID_CARD" + # Sponsor writes what is used in the value column + OTHER = "OTHER" class Verified(models.TextChoices): AUTOMATIC = "AUTOMATIC" @@ -143,7 +147,7 @@ class PersonIdentity(BaseModel): person = models.ForeignKey( "Person", on_delete=models.PROTECT, related_name="person" ) - type = models.CharField(max_length=15, choices=IdentityType.choices) + type = models.CharField(max_length=16, choices=IdentityType.choices) source = models.CharField(max_length=256) value = models.CharField(max_length=256) verified = models.CharField(max_length=9, choices=Verified.choices, blank=True) @@ -202,7 +206,8 @@ class PersonConsent(BaseModel): consent = models.ForeignKey( "Consent", on_delete=models.PROTECT, related_name="link_person_consent" ) - consent_given_at = models.DateField() + # If the date is blank it means the person has not given consent yet + consent_given_at = models.DateField(null=True) class Meta: constraints = [ diff --git a/greg/signals.py b/greg/signals.py index 3a3a52726fde5a9651633d7c0e658f3e79c0dfe7..e1d0f60c7abab9bd97d0c988a5e6e46ea6d4037d 100644 --- a/greg/signals.py +++ b/greg/signals.py @@ -1,5 +1,6 @@ import time import logging +from typing import Dict from django.db import models from django.dispatch import receiver @@ -9,6 +10,9 @@ from greg.models import ( PersonRole, Role, Notification, + PersonIdentity, + PersonConsent, + Consent, ) logger = logging.getLogger(__name__) @@ -17,6 +21,8 @@ SUPPORTED_MODELS = ( Person, PersonRole, Role, + PersonIdentity, + PersonConsent, ) @@ -30,6 +36,10 @@ def disconnect_notification_signals(*args, **kwargs): models.signals.pre_save.disconnect(dispatch_uid="add_changed_fields_callback") models.signals.post_save.disconnect(dispatch_uid="save_notification_callback") models.signals.post_delete.disconnect(dispatch_uid="delete_notification_callback") + models.signals.m2m_changed.connect( + receiver=m2m_changed_notification_callback, + dispatch_uid="m2m_changed_notification_callback", + ) @receiver(models.signals.post_migrate) @@ -70,7 +80,7 @@ def add_changed_fields_callback(sender, instance, raw, *args, **kwargs): Makes note of any dirty (changed) fields before they are saved, stuffing them in the instance for use in any post-save callbacks. """ - if not isinstance(instance, (Person, PersonRole)): + if not isinstance(instance, SUPPORTED_MODELS): return changed = instance.is_dirty() if not changed: @@ -84,11 +94,8 @@ def add_changed_fields_callback(sender, instance, raw, *args, **kwargs): def save_notification_callback(sender, instance, created, *args, **kwargs): if not isinstance(instance, SUPPORTED_MODELS): return - meta = {} + meta = _create_metadata(instance) operation = "add" if created else "update" - if isinstance(instance, PersonRole): - meta["person_id"] = instance.person.id - meta["role_id"] = instance.role.id _store_notification( identifier=instance.id, object_type=instance._meta.object_name, @@ -101,13 +108,78 @@ def save_notification_callback(sender, instance, created, *args, **kwargs): def delete_notification_callback(sender, instance, *args, **kwargs): if not isinstance(instance, SUPPORTED_MODELS): return - meta = {} - if isinstance(instance, PersonRole): - meta["person_id"] = instance.person.id - meta["role_id"] = instance.role.id + meta = _create_metadata(instance) _store_notification( identifier=instance.id, object_type=instance._meta.object_name, operation="delete", **meta ) + + +@receiver(models.signals.m2m_changed, dispatch_uid="m2m_changed_notification_callback") +def m2m_changed_notification_callback( + sender, instance, action, *args, model=None, pk_set=None, **kwargs +): + if action not in ("post_add", "post_remove"): + return + if sender not in (PersonConsent, PersonRole, PersonIdentity): + return + + operation = "add" if action == "post_add" else "delete" + instance_type = type(instance) + + if sender is PersonConsent: + person_consents = [] + if instance_type is Person and model is Consent: + person_consents = PersonConsent.objects.filter( + person_id=instance.id, consent_id__in=pk_set + ) + elif instance_type is Consent and model is Person: + person_consents = PersonConsent.objects.filter( + consent_id=instance.id, person_id__in=pk_set + ) + + for pc in person_consents: + meta = _create_metadata(pc) + _store_notification( + identifier=pc.id, + object_type=PersonConsent._meta.object_name, + operation=operation, + **meta + ) + elif sender is PersonRole: + person_roles = [] + if instance_type is Person and model is Role: + person_roles = PersonRole.objects.filter( + person_id=instance.id, role_id__in=pk_set + ) + elif instance_type is Role and model is Person: + person_roles = PersonRole.objects.filter( + role_id=instance.id, person_id__in=pk_set + ) + + for pr in person_roles: + meta = _create_metadata(pr) + _store_notification( + identifier=pr.id, + object_type=PersonRole._meta.object_name, + operation=operation, + **meta + ) + + +def _create_metadata(instance) -> Dict: + meta = {} + + if isinstance(instance, PersonRole): + meta["person_id"] = instance.person.id + meta["role_id"] = instance.role.id + if isinstance(instance, PersonIdentity): + meta["person_id"] = instance.person.id + meta["identity_id"] = instance.id + if isinstance(instance, PersonConsent): + meta["person_id"] = instance.person.id + meta["consent_id"] = instance.consent.id + + return meta diff --git a/greg/tests/api/test_person.py b/greg/tests/api/test_person.py index f79d9d26a2475b542cc22dda4c9dbb7ed7104ce4..a81e383fd2f1c75cd3d35d899195a2ead12be997 100644 --- a/greg/tests/api/test_person.py +++ b/greg/tests/api/test_person.py @@ -1,10 +1,11 @@ -import pytest +from typing import Dict +import pytest from rest_framework import status from rest_framework.reverse import reverse from rest_framework.status import HTTP_200_OK -from greg.models import Person +from greg.models import Person, PersonIdentity, Sponsor, Role, OrganizationalUnit @pytest.fixture @@ -29,6 +30,81 @@ def person_bar() -> Person: ) +@pytest.fixture +def sponsor_guy() -> Sponsor: + return Sponsor.objects.create(feide_id="guy@example.org") + + +@pytest.fixture +def person_foo_verified(person_foo, sponsor_guy) -> PersonIdentity: + return PersonIdentity.objects.create( + person=person_foo, + type=PersonIdentity.IdentityType.PASSPORT, + source="Test", + value="12345", + verified=PersonIdentity.Verified.MANUAL, + verified_by=sponsor_guy, + verified_when="2021-06-15", + ) + + +@pytest.fixture +def person_foo_not_verified(person_foo) -> PersonIdentity: + return PersonIdentity.objects.create( + person=person_foo, + type=PersonIdentity.IdentityType.DRIVERS_LICENSE, + source="Test", + value="12345", + ) + + +@pytest.fixture +def role_visiting_professor() -> Role: + return Role.objects.create( + type="visiting_professor", + name_nb="Gjesteprofessor", + name_en="Visiting professor", + description_nb="Gjesteprofessor", + description_en="Visiting professor", + default_duration_days=180, + ) + + +@pytest.fixture +def unit_human_resources() -> OrganizationalUnit: + return OrganizationalUnit.objects.create( + orgreg_id="org_unit_1", name_nb="Personal", name_en="Human Resources" + ) + + +@pytest.fixture() +def role_test_guest() -> Role: + return Role.objects.create(type="Test Guest") + + +@pytest.fixture() +def sponsor_bar() -> Sponsor: + return Sponsor.objects.create(feide_id="bar") + + +@pytest.fixture +def unit_foo() -> OrganizationalUnit: + return OrganizationalUnit.objects.create(orgreg_id="12345", name_en="foo_unit") + + +@pytest.fixture +def role_data_guest( + role_test_guest: Role, sponsor_bar: Sponsor, unit_foo: OrganizationalUnit +) -> Dict: + return { + "role": "Test Guest", + "start_date": "2021-06-10", + "end_date": "2021-08-10", + "registered_by": sponsor_bar.id, + "unit": unit_foo.id, + } + + @pytest.mark.django_db def test_get_person(client, person_foo): resp = client.get(reverse("person-detail", kwargs={"id": person_foo.id})) @@ -48,29 +124,33 @@ def test_persons(client, person_foo, person_bar): @pytest.mark.django_db -def test_persons_verified_filter_include(client, setup_db_test_data): +def test_persons_verified_filter_include( + client, person_bar, person_foo, person_foo_verified +): url = reverse("person-list") response = client.get(url, {"verified": "true"}) results = response.json()["results"] assert len(results) == 1 - # The following person will have a verified identity set up for him - # in the test data - assert results[0]["first_name"] == "Christopher" - assert results[0]["last_name"] == "Flores" + assert results[0]["first_name"] == "Foo" + assert results[0]["last_name"] == "Foo" @pytest.mark.django_db -def test_persons_verified_filter_exclude(client, setup_db_test_data): +def test_persons_verified_filter_exclude( + client, person_bar, person_foo, person_foo_verified +): url = reverse("person-list") response = client.get(url, {"verified": "false"}) results = response.json()["results"] - names = [(result["first_name"], result["last_name"]) for result in results] - assert len(results) == 9 - assert ("Christopher", "Flores") not in names + assert len(results) == 1 + assert results[0]["first_name"] == "Bar" + assert results[0]["last_name"] == "Bar" @pytest.mark.django_db -def test_add_role(client, person_foo): +def test_add_role( + client, person_foo, role_visiting_professor, sponsor_guy, unit_human_resources +): url = reverse("person_role-list", kwargs={"person_id": person_foo.id}) roles_for_person = client.get(url).json()["results"] @@ -78,7 +158,7 @@ def test_add_role(client, person_foo): assert len(roles_for_person) == 0 role_data = { - "role": "Visiting Professor", + "role": "visiting_professor", "start_date": "2021-06-10", "end_date": "2021-08-10", "registered_by": "1", @@ -94,3 +174,191 @@ def test_add_role(client, person_foo): # Check that the role shows up when listing roles for the person now assert len(roles_for_person) == 1 assert roles_for_person[0]["id"] == response_data["id"] + + +@pytest.mark.django_db +def test_update_role(client, person_foo, role_data_guest): + url = reverse("person_role-list", kwargs={"person_id": person_foo.id}) + response = client.post(url, role_data_guest) + response_data = response.json() + + assert response_data["start_date"] == "2021-06-10" + + # Update the date and check that the change is registered + role_id = response.json()["id"] + updated_role = role_data_guest.copy() + updated_role["start_date"] = "2021-06-15" + + url_detail = reverse( + "person_role-detail", kwargs={"person_id": person_foo.id, "id": role_id} + ) + client.patch(url_detail, updated_role) + + updated_role_data = client.get(url) + updated_data = updated_role_data.json()["results"][0] + + assert updated_data["id"] == role_id + assert updated_data["start_date"] == "2021-06-15" + + +@pytest.mark.django_db +def test_delete_role(client, person_foo, role_data_guest): + url = reverse("person_role-list", kwargs={"person_id": person_foo.id}) + role_id = client.post(url, role_data_guest).json()["id"] + roles_for_person = client.get(url).json()["results"] + + assert len(roles_for_person) == 1 + + url_detail = reverse( + "person_role-detail", kwargs={"person_id": person_foo.id, "id": role_id} + ) + client.delete(url_detail) + + assert len(client.get(url).json()["results"]) == 0 + + +@pytest.mark.django_db +def test_identity_list( + client, person_foo, person_foo_verified, person_foo_not_verified +): + response = client.get( + reverse("person-list"), + data={"first_name": person_foo.first_name, "last_name": person_foo.last_name}, + ) + person_id = response.json()["results"][0]["id"] + response = client.get( + reverse("person_identity-list", kwargs={"person_id": person_id}) + ) + data = response.json()["results"] + assert len(data) == 2 + + +@pytest.mark.django_db +def test_identity_add(client, person_foo): + response = client.get( + reverse("person_identity-list", kwargs={"person_id": person_foo.id}) + ) + results = response.json()["results"] + assert len(results) == 0 + + data = { + "type": PersonIdentity.IdentityType.FEIDE_ID, + "source": "Test source", + "value": "12345", + } + client.post( + reverse("person_identity-list", kwargs={"person_id": person_foo.id}), data=data + ) + + response = client.get( + reverse("person_identity-list", kwargs={"person_id": person_foo.id}) + ) + results = response.json()["results"] + assert len(results) == 1 + + +@pytest.mark.django_db +def test_identity_delete(client, person_foo): + response = client.get( + reverse("person_identity-list", kwargs={"person_id": person_foo.id}) + ) + results = response.json()["results"] + assert len(results) == 0 + + data = { + "type": PersonIdentity.IdentityType.FEIDE_ID, + "source": "Test source", + "value": "12345", + } + post_response = client.post( + reverse("person_identity-list", kwargs={"person_id": person_foo.id}), data=data + ) + identity_id = post_response.json()["id"] + + # Create two identities for the user + data = { + "type": PersonIdentity.IdentityType.PASSPORT, + "source": "Test", + "value": "1234413241235", + } + post_response2 = client.post( + reverse("person_identity-list", kwargs={"person_id": person_foo.id}), data=data + ) + identity_id2 = post_response2.json()["id"] + + response = client.get( + reverse("person_identity-list", kwargs={"person_id": person_foo.id}) + ) + results = response.json()["results"] + assert len(results) == 2 + + # Delete the first identity created + client.delete( + reverse( + "person_identity-detail", + kwargs={"person_id": person_foo.id, "id": identity_id}, + ) + ) + + # Check that the other identity is still there + response = client.get( + reverse("person_identity-list", kwargs={"person_id": person_foo.id}) + ) + results = response.json()["results"] + + assert len(results) == 1 + assert results[0]["id"] == identity_id2 + + +@pytest.mark.django_db +def test_identity_update(client, person_foo): + response = client.get( + reverse("person_identity-list", kwargs={"person_id": person_foo.id}) + ) + results = response.json()["results"] + assert len(results) == 0 + + data = { + "type": PersonIdentity.IdentityType.FEIDE_ID, + "source": "Test source", + "value": "12345", + } + client.post( + reverse("person_identity-list", kwargs={"person_id": person_foo.id}), data=data + ) + + response = client.get( + reverse("person_identity-list", kwargs={"person_id": person_foo.id}) + ) + results = response.json()["results"] + assert len(results) == 1 + + identity_id = results[0]["id"] + + assert results[0]["type"] == data["type"] + assert results[0]["source"] == data["source"] + assert results[0]["value"] == data["value"] + + data_updated = { + "type": PersonIdentity.IdentityType.PASSPORT, + "source": "Test", + "value": "10000", + } + patch_response = client.patch( + reverse( + "person_identity-detail", + kwargs={"person_id": person_foo.id, "id": identity_id}, + ), + data=data_updated, + ) + assert patch_response.status_code == status.HTTP_200_OK + + response = client.get( + reverse("person_identity-list", kwargs={"person_id": person_foo.id}) + ) + results = response.json()["results"] + assert len(results) == 1 + + assert results[0]["type"] == data_updated["type"] + assert results[0]["source"] == data_updated["source"] + assert results[0]["value"] == data_updated["value"] diff --git a/greg/tests/conftest.py b/greg/tests/conftest.py index bbdf24d3973f2d452e7e45e22ae1a81e2bcda7b5..9280ae7c911ecba5bece2b7cfc414d49e183cb69 100644 --- a/greg/tests/conftest.py +++ b/greg/tests/conftest.py @@ -9,22 +9,9 @@ from django.contrib.auth import get_user_model # see https://github.com/joke2k/faker/issues/753 import pytest -from greg.tests.populate_database import DatabasePopulation - logging.getLogger("faker").setLevel(logging.ERROR) -# The database is populated once when scope is session. -# If the scope is changed to function some additional -# logic is needed to make sure the old data is cleaned -# before the seeding is run again -@pytest.fixture(scope="session") -def setup_db_test_data(django_db_setup, django_db_blocker): - with django_db_blocker.unblock(): - database_seeder = DatabasePopulation() - database_seeder.populate_database() - - @pytest.fixture def client() -> APIClient: user, _ = get_user_model().objects.get_or_create(username="test") diff --git a/greg/tests/models/test_consent.py b/greg/tests/models/test_consent.py index ad800bd596686e43520556a1acb2ec22aa2a76c6..8f7711e08ff37e9e20dc7f8ce9eacca45f51ce34 100644 --- a/greg/tests/models/test_consent.py +++ b/greg/tests/models/test_consent.py @@ -1,8 +1,11 @@ +import datetime + import pytest from greg.models import ( Person, Consent, + PersonConsent, ) @@ -17,9 +20,9 @@ def person() -> Person: ) -@pytest.mark.django_db -def test_add_consent_to_person(person): - consent = Consent.objects.create( +@pytest.fixture() +def consent() -> Consent: + return Consent.objects.create( type="it_guidelines", consent_name_en="IT Guidelines", consent_name_nb="IT Regelverk", @@ -28,4 +31,25 @@ def test_add_consent_to_person(person): consent_link_en="https://example.org/it_guidelines", user_allowed_to_change=False, ) - person.consents.add(consent, through_defaults={"consent_given_at": "2021-06-20"}) + + +@pytest.mark.django_db +def test_add_consent_to_person(person: Person, consent: Consent): + consent_given_date = "2021-06-20" + person.consents.add(consent, through_defaults={"consent_given_at": consent_given_date}) # type: ignore + person_consent_links = PersonConsent.objects.filter(person_id=person.id) + + assert len(person_consent_links) == 1 + assert person_consent_links[0].person_id == person.id + assert person_consent_links[0].consent_id == consent.id + assert person_consent_links[0].consent_given_at == datetime.date(2021, 6, 20) + + +@pytest.mark.django_db +def test_add_not_acknowledged_consent_to_person(person: Person, consent: Consent): + person.consents.add(consent) + person_consent_links = PersonConsent.objects.filter(person_id=person.id) + assert len(person_consent_links) == 1 + assert person_consent_links[0].person_id == person.id + assert person_consent_links[0].consent_id == consent.id + assert person_consent_links[0].consent_given_at is None diff --git a/greg/tests/test_notifications.py b/greg/tests/test_notifications.py new file mode 100644 index 0000000000000000000000000000000000000000..8172a2be5cf1668a39a8f848bd62e420f349730f --- /dev/null +++ b/greg/tests/test_notifications.py @@ -0,0 +1,235 @@ +import pytest + +from greg.models import ( + Person, + Notification, + Consent, + Role, + OrganizationalUnit, + Sponsor, + PersonConsent, + PersonIdentity, +) + + +@pytest.fixture +def person() -> Person: + return Person.objects.create( + first_name="Test", + last_name="Tester", + date_of_birth="2000-01-27", + email="test@example.org", + mobile_phone="123456789", + ) + + +@pytest.fixture +def role_foo() -> Role: + return Role.objects.create(type="role_foo", name_en="Role Foo") + + +@pytest.fixture +def org_unit_bar() -> OrganizationalUnit: + return OrganizationalUnit.objects.create(orgreg_id="bar_unit") + + +@pytest.fixture +def sponsor() -> Sponsor: + return Sponsor.objects.create(feide_id="sponsor_id") + + +@pytest.fixture +def consent() -> Consent: + return Consent.objects.create( + type="it_guidelines", + consent_name_en="IT Guidelines", + consent_name_nb="IT Regelverk", + consent_description_en="IT Guidelines description", + consent_description_nb="IT Regelverk beskrivelse", + consent_link_en="https://example.org/it_guidelines", + user_allowed_to_change=False, + ) + + +@pytest.fixture +def person_identity(person: Person) -> PersonIdentity: + return PersonIdentity.objects.create( + person=person, + type=PersonIdentity.IdentityType.PASSPORT_NUMBER, + source="Test", + value="12345678901", + ) + + +@pytest.mark.django_db +def test_person_add_notification(person: Person): + notifications = Notification.objects.filter(object_type="Person") + assert len(notifications) == 1 + assert notifications[0].operation == "add" + assert notifications[0].identifier == person.id + + +@pytest.mark.django_db +def test_person_update_notification(person: Person): + person.first_name = "New first name" + person.save() + notifications = Notification.objects.filter(object_type="Person") + assert len(notifications) == 2 + assert notifications[1].operation == "update" + assert notifications[1].identifier == person.id + + +@pytest.mark.django_db +def test_person_delete_notification(person: Person): + person_id = person.id + person.delete() + notifications = Notification.objects.filter(object_type="Person") + assert len(notifications) == 2 + assert notifications[1].operation == "delete" + assert notifications[1].identifier == person_id + + +@pytest.mark.django_db +def test_role_add_notification( + person: Person, role_foo: Role, org_unit_bar: OrganizationalUnit, sponsor: Sponsor +): + person.roles.add( # type: ignore + role_foo, + through_defaults={ + "start_date": "2021-05-06", + "end_date": "2021-10-20", + "unit": org_unit_bar, + "registered_by": sponsor, + }, + ) + notifications = Notification.objects.filter(object_type="PersonRole") + assert len(notifications) == 1 + assert notifications[0].operation == "add" + meta_data = notifications[0].meta + assert meta_data["person_id"] == person.id + assert meta_data["role_id"] == role_foo.id + + +@pytest.mark.django_db +def test_role_update_notification( + person: Person, role_foo: Role, org_unit_bar: OrganizationalUnit, sponsor: Sponsor +): + person.roles.add( # type: ignore + role_foo, + through_defaults={ + "start_date": "2021-05-06", + "end_date": "2021-10-20", + "unit": org_unit_bar, + "registered_by": sponsor, + }, + ) + + assert len(person.person_roles.all()) == 1 + person_role = person.person_roles.all()[0] + person_role.end_date = "2021-10-21" + person_role.save() + notifications = Notification.objects.filter(object_type="PersonRole") + assert len(notifications) == 2 + assert notifications[1].operation == "update" + meta_data = notifications[1].meta + assert meta_data["person_id"] == person.id + assert meta_data["role_id"] == role_foo.id + + +@pytest.mark.django_db +def test_role_delete_notification( + person: Person, role_foo: Role, org_unit_bar: OrganizationalUnit, sponsor: Sponsor +): + person.roles.add( # type: ignore + role_foo, + through_defaults={ + "start_date": "2021-05-06", + "end_date": "2021-10-20", + "unit": org_unit_bar, + "registered_by": sponsor, + }, + ) + + assert len(person.person_roles.all()) == 1 + person_role = person.person_roles.all()[0] + person_role.delete() + notifications = Notification.objects.filter(object_type="PersonRole") + assert len(notifications) == 2 + assert notifications[1].operation == "delete" + meta_data = notifications[1].meta + assert meta_data["person_id"] == person.id + assert meta_data["role_id"] == role_foo.id + + +@pytest.mark.django_db +def test_consent_add_notification(person: Person, consent: Consent): + person.consents.add(consent, through_defaults={"consent_given_at": "2021-06-20"}) # type: ignore + notifications = Notification.objects.filter(object_type="PersonConsent") + assert len(notifications) == 1 + assert notifications[0].identifier == person.id + meta_data = notifications[0].meta + assert meta_data["person_id"] == person.id + assert meta_data["consent_id"] == consent.id + + +@pytest.mark.django_db +def test_consent_update_notification(person: Person, consent: Consent): + person.consents.add(consent, through_defaults={"consent_given_at": "2021-06-20"}) # type: ignore + person_consents = PersonConsent.objects.filter(person=person, consent=consent) + person_consents[0].consent_given_at = "2021-06-21" + person_consents[0].save() + + notifications = Notification.objects.filter(object_type="PersonConsent") + assert len(notifications) == 2 + assert notifications[0].identifier == person.id + meta_data = notifications[0].meta + assert meta_data["person_id"] == person.id + assert meta_data["consent_id"] == consent.id + + +@pytest.mark.django_db +def test_consent_delete_notification(person: Person, consent: Consent): + person.consents.add(consent, through_defaults={"consent_given_at": "2021-06-20"}) # type: ignore + person_consents = PersonConsent.objects.filter(person=person, consent=consent) + person_consents[0].delete() + notifications = Notification.objects.filter(object_type="PersonConsent") + + assert len(notifications) == 2 + assert notifications[1].identifier == person.id + assert notifications[1].operation == "delete" + meta_data = notifications[0].meta + assert meta_data["person_id"] == person.id + assert meta_data["consent_id"] == consent.id + + +@pytest.mark.django_db +def test_person_identity_add_notification( + person: Person, person_identity: PersonIdentity, sponsor: Sponsor +): + notifications = Notification.objects.filter(object_type="PersonIdentity") + assert len(notifications) == 1 + assert notifications[0].identifier == person.id + assert notifications[0].operation == "add" + + meta_data = notifications[0].meta + assert meta_data["person_id"] == person.id + assert meta_data["identity_id"] == person_identity.id + + +@pytest.mark.django_db +def test_person_identity_update_notification( + person: Person, person_identity: PersonIdentity, sponsor: Sponsor +): + person_identity.verified = PersonIdentity.Verified.MANUAL + person_identity.verified_by = sponsor + person_identity.verified_when = "2021-08-02" + person_identity.save() + + notifications = Notification.objects.filter(object_type="PersonIdentity") + # One notification for adding person identity and one for updating it + assert len(notifications) == 2 + assert notifications[1].operation == "update" + + meta_data = notifications[1].meta + assert meta_data["person_id"] == person.id + assert meta_data["identity_id"] == person_identity.id