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

Merge branch 'GREG-7_schedule_notifications' into 'master'

GREG-7: Simple task running functionality. Adding ask for checking roles about to expire

See merge request !18
parents 0e8fddb5 2e76c52b
No related branches found
No related tags found
1 merge request!18GREG-7: Simple task running functionality. Adding ask for checking roles about to expire
Pipeline #89065 passed
import logging
import signal
import sys
from threading import Event
import daemon
import lockfile
from daemon import pidfile
from django.conf import settings
from django.core.management.base import BaseCommand
from greg.schedule import ExpiringRolesNotification
logging.config.dictConfig(settings.LOGGING)
logger = logging.getLogger()
def exception_handler(ex_cls, ex, tb):
logger.critical("Uncaught exception:", exc_info=(ex_cls, ex, tb))
sys.excepthook = exception_handler
class Command(BaseCommand):
"""
This command starts a basic task runner. All tasks it is supposed to
run are given explicitly in code
"""
help = "Start schedule task runner"
def add_arguments(self, parser):
parser.add_argument(
"--detach", action="store_true", help="Run detached as a dæmon"
)
parser.add_argument(
"--use-pidfile", action="store_true", help="Use a PID lockfile"
)
def handle(self, *args, **options):
lock_context = None
if options.get("use_pidfile"):
lock_context = pidfile.TimeoutPIDLockFile(
settings.SCHEDULE_TASKS["daemon"]["pid_file"]
)
try:
exit_event = Event()
def exit_wrapper(*args):
exit_event.set()
with daemon.DaemonContext(
pidfile=lock_context,
files_preserve=[x.stream.fileno() for x in logger.handlers],
stderr=sys.stderr,
stdout=sys.stdout,
signal_map={signal.SIGINT: exit_wrapper, signal.SIGTERM: exit_wrapper},
detach_process=options.get("detach"),
):
logger.info(
"Running %s", "detached" if options.get("detach") else "attached"
)
# For now there is just one task, but the idea is that more can
# be added
expiring_role_notification_task = ExpiringRolesNotification()
while not exit_event.is_set():
expiring_role_notification_task.run()
# The single task is set up for far only needs to run once
# a day, but running every 6 hours just in case a run fails,
# it is up the task it self to figure out if needs to do
# something every time it is called
exit_event.wait(60 * 60 * 6)
except lockfile.AlreadyLocked as e:
logger.warning("Can't start daemon: %s", e)
sys.exit(1)
finally:
logger.info("Stopped")
import os
import signal
from django.conf import settings
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Stop schedule tasks"
def handle(self, *args, **options):
try:
with open(settings.SCHEDULE_TASKS["daemon"]["pid_file"], "r") as f:
pid = int(f.read().strip())
os.kill(pid, signal.SIGINT)
except FileNotFoundError:
pass
# Generated by Django 3.2.5 on 2021-08-04 11:07
# Generated by Django 3.2.5 on 2021-08-06 10:47
import datetime
import dirtyfields.dirtyfields
......@@ -104,6 +104,14 @@ class Migration(migrations.Migration):
},
bases=(dirtyfields.dirtyfields.DirtyFieldsMixin, models.Model),
),
migrations.CreateModel(
name='ScheduleTask',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=32)),
('last_completed', models.DateTimeField(null=True)),
],
),
migrations.CreateModel(
name='Sponsor',
fields=[
......
......@@ -299,3 +299,12 @@ class SponsorOrganizationalUnit(BaseModel):
self.organizational_unit,
self.hierarchical_access,
)
class ScheduleTask(models.Model):
"""
Stores information about a task
"""
name = models.CharField(max_length=32)
last_completed = models.DateTimeField(null=True)
import time
from datetime import date, datetime, timedelta
from typing import Optional
from abc import ABC, abstractmethod
from django.utils import timezone
from greg.models import PersonRole, Notification, ScheduleTask
class BaseSchedule(ABC):
"""
Provides common methods for tasks
"""
task_object: ScheduleTask
def __init__(self, name: str):
self.task_object = ScheduleTask.objects.get(name=name)
def run(self):
self._run_internal()
self.task_object.last_completed = timezone.now()
self.task_object.save()
@abstractmethod
def _run_internal(self):
pass
def get_last_run(self) -> Optional[datetime]:
self.task_object.refresh_from_db()
return self.task_object.last_completed
def _store_notification(
self, identifier, object_type, operation, **kwargs
) -> Notification:
return Notification.objects.create(
identifier=identifier,
object_type=object_type,
operation=operation,
issued_at=int(time.time()),
meta=kwargs,
)
class ExpiringRolesNotification(BaseSchedule):
"""
This task does a simple check for roles that will expire in 30 days
and creates entries in the notification table for them.
There should be an entry in the ScheduleTask-table with name role_expiration
before this task is run.
Some assumptions that are made:
- The task will be run every day, it does
not keep track of which roles it has or has not notified, it only
looks at which roles will expire in exactly 30 days
- If there are roles created that expire in less than 30 days, no
notification is necessary
"""
EXPIRATION_THRESHOLD_DAYS = 30
def __init__(self):
# Will raise exception if there is not exactly one result
super().__init__("role_expiration")
def _run_internal(self):
last_run = self.get_last_run()
# Only run once per day
if last_run is None or last_run.date() != date.today():
check_date = datetime.today() + timedelta(
days=self.EXPIRATION_THRESHOLD_DAYS
)
self.__get_roles_about_to_expire(check_date)
def __get_roles_about_to_expire(self, end_date: date):
roles_about_to_expire = PersonRole.objects.filter(end_date=end_date)
for person_role in roles_about_to_expire:
meta = {"person_id": person_role.person.id, "role_id": person_role.role.id}
self._store_notification(
identifier=person_role.id,
object_type="PersonRole",
operation="expire_reminder",
**meta
)
from datetime import datetime, timedelta
import pytest
from django.db.models import Q
from django.utils import timezone
from greg.models import (
ScheduleTask,
Role,
Person,
OrganizationalUnit,
PersonRole,
Notification,
Sponsor,
)
from greg.schedule import ExpiringRolesNotification
@pytest.fixture
def role_task():
ScheduleTask.objects.create(
name="role_expiration", last_completed=timezone.now() - timedelta(days=1)
)
@pytest.fixture
def role_bar() -> Role:
return Role.objects.create(type="role_bar", name_en="Role Bar")
@pytest.fixture
def person(role_bar: Role) -> 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 organizational_unit() -> OrganizationalUnit:
return OrganizationalUnit.objects.create(orgreg_id="12345", name_en="Test unit")
@pytest.fixture
def sponsor() -> Sponsor:
return Sponsor.objects.create(feide_id="sponsor@example.org")
@pytest.fixture
def person_role(
person: Person,
role_bar: Role,
organizational_unit: OrganizationalUnit,
sponsor: Sponsor,
) -> PersonRole:
return PersonRole.objects.create(
person=person,
role=role_bar,
unit=organizational_unit,
start_date="2020-03-05",
end_date=datetime.today() + timedelta(days=30),
contact_person_unit="Contact Person",
available_in_search=True,
registered_by=sponsor,
)
@pytest.mark.django_db
def test_role_picked_up(role_task: ScheduleTask, person_role: PersonRole):
role_notification = ExpiringRolesNotification()
assert len(Notification.objects.filter(~Q(operation="add"))) == 0
role_notification.run()
notification = Notification.objects.get(operation="expire_reminder")
assert notification.identifier == person_role.id
role_notification.run()
notifications = Notification.objects.filter(operation="expire_reminder")
assert len(notifications) == 1
@pytest.mark.django_db
def test_no_notification_for_role_not_about_to_expire(
role_task: ScheduleTask, person_role: PersonRole
):
person_role.end_date = datetime.today() + timedelta(days=31)
person_role.save()
role_notification = ExpiringRolesNotification()
assert len(Notification.objects.filter(operation="expire_reminder")) == 0
# Role should not be picked up since it expires in 31 days
role_notification.run()
notifications = Notification.objects.filter(operation="expire_reminder")
assert len(notifications) == 0
......@@ -55,7 +55,7 @@ def consent() -> Consent:
def person_identity(person: Person) -> PersonIdentity:
return PersonIdentity.objects.create(
person=person,
type=PersonIdentity.IdentityType.PASSPORT_NUMBER,
type=PersonIdentity.IdentityType.PASSPORT,
source="Test",
value="12345678901",
)
......
......@@ -206,6 +206,10 @@ NOTIFICATION_PUBLISHER = {
"daemon": {"pid_file": "/tmp/greg_notification_publisher.lock", "poll_interval": 1},
}
SCHEDULE_TASKS = {
"daemon": {"pid_file": "/tmp/schedule_tasks.lock"},
}
INSTANCE_NAME = "local"
INTERNAL_RK_PREFIX = "no.{instance}.greg".format(instance=INSTANCE_NAME)
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