Skip to content
Snippets Groups Projects
Verified Commit 2db65f96 authored by Andreas Ellewsen's avatar Andreas Ellewsen
Browse files

Add future notifications for roles

The post_save signal now checks if the instance is a Role, and queues a
django q task to produce a Notification on the start and end dates for
the Role. The future task checks if the date has changed after it was
queued and only produces a Notification if today's date matches the date
for the task.

Resolves: GREG-65
parent 334cb21e
No related branches found
No related tags found
1 merge request!137Greg 65 notification on start
Pipeline #99767 passed
......@@ -5,6 +5,7 @@ from greg.models import (
OuIdentifier,
Invitation,
InvitationLink,
Notification,
Person,
Role,
RoleType,
......@@ -133,6 +134,18 @@ class InvitationLinkAdmin(VersionAdmin):
readonly_fields = ("uuid",)
class NotificationAdmin(VersionAdmin):
list_display = (
"id",
"object_type",
"identifier",
"operation",
"created",
"updated",
"issued_at",
)
admin.site.register(Person, PersonAdmin)
admin.site.register(Role, RoleAdmin)
admin.site.register(RoleType, RoleTypeAdmin)
......@@ -145,3 +158,4 @@ admin.site.register(SponsorOrganizationalUnit, SponsorOrganizationalUnitAdmin)
admin.site.register(Invitation, InvitationAdmin)
admin.site.register(InvitationLink, InvitationLinkAdmin)
admin.site.register(OuIdentifier, IdentifierAdmin)
admin.site.register(Notification, NotificationAdmin)
import datetime
import time
import logging
from typing import Dict, Union
from django.db import models
from django.dispatch import receiver
from django_q.models import Schedule
from greg.models import (
Person,
Role,
......@@ -14,6 +15,7 @@ from greg.models import (
Consent,
ConsentType,
)
from greg.utils import date_to_datetime_midnight
logger = logging.getLogger(__name__)
......@@ -74,6 +76,37 @@ def _store_notification(identifier, object_type, operation, **kwargs):
)
def _queue_role_notification(role_id: int, created: str, attrname: str):
"""Create a notification if the date in attrname of role is today"""
try:
instance = Role.objects.get(id=role_id)
except Role.DoesNotExist:
# Role was deleted, ignore it
return
if getattr(instance, attrname) != datetime.date.today():
# Date was changed after this task was queued, ignore it, the new one will take
# care of it
return
meta = _create_metadata(instance)
operation = "add" if created else "update"
_store_notification(
identifier=instance.id,
object_type=instance._meta.object_name,
operation=operation,
**meta,
)
def _queue_role_start_notification(role_id: int, created: str):
"""Create a notification for the role if start_date is today"""
_queue_role_notification(role_id, created, "start_date")
def _queue_role_end_notification(role_id: int, created: str):
"""Create a notification for the role if end_date is today"""
_queue_role_notification(role_id, created, "end_date")
@receiver(models.signals.pre_save, dispatch_uid="add_changed_fields_callback")
def add_changed_fields_callback(sender, instance, raw, *args, **kwargs):
"""
......@@ -94,13 +127,28 @@ 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
# Queue future notifications on start and end date for roles
if isinstance(instance, Role):
if instance.start_date:
Schedule.objects.create(
func="greg.signals._queue_role_start_notification",
args=f"{instance.id},True",
next_run=date_to_datetime_midnight(instance.start_date),
schedule_type=Schedule.ONCE,
)
Schedule.objects.create(
func="greg.signals._queue_role_end_notification",
args=f"{instance.id},True",
next_run=date_to_datetime_midnight(instance.end_date),
schedule_type=Schedule.ONCE,
)
meta = _create_metadata(instance)
operation = "add" if created else "update"
_store_notification(
identifier=instance.id,
object_type=instance._meta.object_name,
operation=operation,
**meta
**meta,
)
......@@ -113,7 +161,7 @@ def delete_notification_callback(sender, instance, *args, **kwargs):
identifier=instance.id,
object_type=instance._meta.object_name,
operation="delete",
**meta
**meta,
)
......@@ -146,7 +194,7 @@ def m2m_changed_notification_callback(
identifier=pc.id,
object_type=Consent._meta.object_name,
operation=operation,
**meta
**meta,
)
elif sender is Role:
roles = []
......@@ -161,7 +209,7 @@ def m2m_changed_notification_callback(
identifier=pr.id,
object_type=Role._meta.object_name,
operation=operation,
**meta
**meta,
)
......
......@@ -15,22 +15,25 @@ from greg.models import (
@pytest.fixture
def org_unit_bar() -> OrganizationalUnit:
return OrganizationalUnit.objects.create()
org = OrganizationalUnit.objects.create()
return OrganizationalUnit.objects.get(pk=org.id)
@pytest.fixture
def sponsor() -> Sponsor:
return Sponsor.objects.create(feide_id="sponsor_id")
sp = Sponsor.objects.create(feide_id="sponsor_id")
return Sponsor.objects.get(pk=sp.id)
@pytest.fixture
def identity(person: Person) -> Identity:
return Identity.objects.create(
ident = Identity.objects.create(
person=person,
type=Identity.IdentityType.PASSPORT_NUMBER,
source="Test",
value="12345678901",
)
return Identity.objects.get(pk=ident.id)
@pytest.mark.django_db
......
import datetime
import pytest
from greg.models import Notification
from greg.models import Notification, Role
from greg.signals import _queue_role_start_notification, _queue_role_end_notification
@pytest.fixture
def role_today(person, role_type_test_guest, sponsor_guy, unit_foo):
"""A test role with end and start date today."""
role = Role.objects.create(
person=person,
type=role_type_test_guest,
sponsor=sponsor_guy,
orgunit=unit_foo,
start_date=datetime.date.today(),
end_date=datetime.date.today(),
)
return Role.objects.get(pk=role.id)
@pytest.mark.django_db
......@@ -17,3 +33,37 @@ def test_delete_signal_ou(unit_foo):
assert Notification.objects.count() == 0
unit_foo.delete()
assert Notification.objects.count() == 0
@pytest.mark.django_db
def test_queue_role_start_notification(role_today):
"""Check that a notification is produced if the role starts today"""
assert Notification.objects.all().count() == 3
_queue_role_start_notification(role_today.id, True)
assert Notification.objects.all().count() == 4
@pytest.mark.django_db
def test_queue_role_end_notification(role_today):
"""Check that a notification is produced if the role ends today"""
assert Notification.objects.all().count() == 3
_queue_role_end_notification(role_today.id, True)
assert Notification.objects.all().count() == 4
@pytest.mark.django_db
def test_queue_role_end_notification_wrong_date(role_today):
"""Check that a notification is produced if the role ends today"""
role_today.end_date = datetime.date.today() - datetime.timedelta(days=2)
role_today.save()
assert Notification.objects.all().count() == 4
_queue_role_end_notification(role_today.id, True)
assert Notification.objects.all().count() == 4
@pytest.mark.django_db
def test_queue_role_end_notification_role_deleted():
"""Check that a notification is not produced if the role was deleted"""
assert Notification.objects.all().count() == 0
_queue_role_end_notification(10, True)
assert Notification.objects.all().count() == 0
import re
from datetime import date
import typing
from datetime import date, datetime
from django.utils import timezone
def camel_to_snake(s: str) -> str:
......@@ -90,3 +93,12 @@ def _compute_checksum(input_digits: str) -> bool:
k2 = 0
return k1 < 10 and k2 < 10 and k1 == d[9] and k2 == d[10]
def date_to_datetime_midnight(in_date: typing.Union[date, str]) -> datetime:
"""Convert a date or str object to a datetime object with timezone utc"""
start_date = (
datetime.strptime(in_date, "%Y-%M-%d") if isinstance(in_date, str) else in_date
)
start = datetime.combine(start_date, datetime.min.time(), tzinfo=timezone.utc)
return start
......@@ -20,6 +20,9 @@ ignore_missing_imports = True
[mypy-django_extensions.db.fields]
ignore_missing_imports = True
[mypy-django_q.*]
ignore_missing_imports = True
[mypy-django_filters.*]
ignore_missing_imports = True
......
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