Skip to content

Commit

Permalink
✨(dashboard) add signals on delivery point creation
Browse files Browse the repository at this point in the history
Implemented a signal to automatically create a Consent object when a new DeliveryPoint is saved.
Added corresponding tests to ensure functionality and updated related Pipfile dependencies.
Centralized consent end date logic in a new app settings file (CONSENT_END_DATE) and updated all related signals, factories, and tests to use it.
Updated documentation and code comments to reflect these changes.
  • Loading branch information
ssorin committed Dec 20, 2024
1 parent 33325cf commit 86eb493
Show file tree
Hide file tree
Showing 16 changed files with 319 additions and 143 deletions.
3 changes: 3 additions & 0 deletions src/dashboard/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ and this project adheres to
- add consent form to manage consents of one or many entities
- integration of custom 403, 404 and 500 pages
- sentry integration
- added a signal on the creation of a delivery point. This signal allows the creation
of the consent corresponding to the delivery point


[unreleased]: https://github.com/MTES-MCT/qualicharge/compare/main...bootstrap-dashboard-project

4 changes: 4 additions & 0 deletions src/dashboard/apps/consent/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ class ConsentConfig(AppConfig):
name = "apps.consent"
label = "qcd_consent"
verbose_name = _("Consent")

def ready(self):
"""Register signals."""
from .signals import handle_new_delivery_point # noqa: F401
6 changes: 3 additions & 3 deletions src/dashboard/apps/consent/factories.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Dashboard consent factories."""

from datetime import timedelta

import factory
from django.utils import timezone
from factory import fuzzy
Expand All @@ -10,6 +8,8 @@
from apps.core.factories import DeliveryPointFactory

from .models import Consent
from .settings import CONSENT_NUMBER_DAYS_END_DATE
from .utils import calculate_consent_end_date


class ConsentFactory(factory.django.DjangoModelFactory):
Expand All @@ -18,7 +18,7 @@ class ConsentFactory(factory.django.DjangoModelFactory):
delivery_point = factory.SubFactory(DeliveryPointFactory)
created_by = factory.SubFactory(UserFactory)
start = fuzzy.FuzzyDateTime(timezone.now())
end = factory.LazyAttribute(lambda o: o.start + timedelta(days=90))
end = calculate_consent_end_date(CONSENT_NUMBER_DAYS_END_DATE)

class Meta: # noqa: D106
model = Consent
29 changes: 20 additions & 9 deletions src/dashboard/apps/consent/fixtures/consent.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
"""Dashboard consent dev fixture."""

from datetime import timedelta

from django.utils import timezone

from apps.auth.factories import UserFactory
from apps.consent import VALIDATED
from apps.consent.factories import ConsentFactory
from apps.core.factories import DeliveryPointFactory, EntityFactory
from apps.core.models import DeliveryPoint
Expand Down Expand Up @@ -29,12 +34,18 @@ def seed_consent():
entity4 = EntityFactory(users=(user5,))

# create delivery points
for _ in range(1, 4):
DeliveryPointFactory(entity=entity1)
DeliveryPointFactory(entity=entity2)
DeliveryPointFactory(entity=entity3)
DeliveryPointFactory(entity=entity4)

# create awaiting consents
for delivery_point in DeliveryPoint.objects.all():
ConsentFactory(delivery_point=delivery_point, created_by=user1)
size = 4
DeliveryPointFactory.create_batch(size, entity=entity1)
DeliveryPointFactory.create_batch(size, entity=entity2)
DeliveryPointFactory.create_batch(size, entity=entity3)
DeliveryPointFactory.create_batch(size, entity=entity4)

# create past consents with validated status
for dl in DeliveryPoint.objects.filter(entity=entity1):
ConsentFactory(
delivery_point=dl,
created_by=user1,
status=VALIDATED,
start=timezone.now() - timedelta(days=300),
end=timezone.now() - timedelta(days=270),
)
23 changes: 21 additions & 2 deletions src/dashboard/apps/consent/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

from . import AWAITING, CONSENT_STATUS_CHOICE, REVOKED
from .managers import ConsentManager
from .settings import CONSENT_NUMBER_DAYS_END_DATE
from .utils import calculate_consent_end_date


class Consent(DashboardBase):
Expand Down Expand Up @@ -46,8 +48,8 @@ class Consent(DashboardBase):
end = models.DateTimeField(_("end date"))
revoked_at = models.DateTimeField(_("revoked at"), null=True, blank=True)

active_objects = ConsentManager()
objects = models.Manager()
active_objects = ConsentManager()

class Meta: # noqa: D106
ordering = ["delivery_point"]
Expand All @@ -56,7 +58,24 @@ def __str__(self): # noqa: D105
return f"{self.delivery_point} - {self.updated_at}: {self.status}"

def save(self, *args, **kwargs):
"""Update the revoked_at timestamps if the consent is revoked."""
"""Saves with custom logic applied before persistency.
- If the consent status is revoked, `revoked_at` is updated to the current time.
- When creating a new consent instance, if the `start` and `end` dates are not
provided, they are set to default values: the `start` date is set to the current
date, and the `end` date is set to a computed date based on
`CONSENT_NUMBER_DAYS_END_DATE`.
"""
# Update the `revoked_at` timestamps if the consent is revoked.
if self.status == REVOKED:
self.revoked_at = timezone.now()

# if the current instance is newly created,
# set default values for `start` and end `dates` if they are not provided.
if self._state.adding:
self.start = self.start or timezone.now()
self.end = self.end or calculate_consent_end_date(
CONSENT_NUMBER_DAYS_END_DATE
)

return super(Consent, self).save(*args, **kwargs)
13 changes: 13 additions & 0 deletions src/dashboard/apps/consent/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Dashboard consent app settings."""

from django.conf import settings

# CONSENT_NUMBER_DAYS_END_DATE allows to calculate the end date of a consent period by
# adding a number of days to the current date.
# If the value is None, the end date of the period will correspond to the last day of
# the current year
# More details on the calculation in the function: `utils.calculate_consent_end_date()`
# ie:
# CONSENT_NUMBER_DAYS_END_DATE = 90 will return the current date + 90 days.
# CONSENT_NUMBER_DAYS_END_DATE = None will return 2024-12-31 23:59:59.
CONSENT_NUMBER_DAYS_END_DATE = getattr(settings, "CONSENT_NUMBER_DAYS_END_DATE", None)
18 changes: 18 additions & 0 deletions src/dashboard/apps/consent/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Dashboard consent app signals."""

from django.db.models.signals import post_save
from django.dispatch import receiver

from apps.core.models import DeliveryPoint

from .models import Consent


@receiver(post_save, sender=DeliveryPoint, dispatch_uid="handle_new_delivery_point")
def handle_new_delivery_point(sender, instance, created, **kwargs):
"""Signal triggered after a new DeliveryPoint is saved.
Create a new Consent object for the delivery point.
"""
if created:
Consent.objects.create(delivery_point=instance)
86 changes: 62 additions & 24 deletions src/dashboard/apps/consent/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,92 @@
"""Dashboard consent models tests."""

from datetime import timedelta

import pytest
from django.utils import formats
from django.db.models import signals

from apps.auth.factories import UserFactory
from apps.consent import AWAITING, REVOKED, VALIDATED
from apps.consent.factories import ConsentFactory
from apps.consent.models import Consent
from apps.consent.signals import handle_new_delivery_point
from apps.consent.utils import calculate_consent_end_date
from apps.core.factories import DeliveryPointFactory
from apps.core.models import DeliveryPoint


@pytest.mark.django_db
def test_create_consent():
"""Tests the creation of a consent."""
user1 = UserFactory()
assert Consent.objects.count() == 0

# create one `delivery_point` and consequently one `consent`
delivery_point = DeliveryPointFactory()

consent = ConsentFactory(
delivery_point=delivery_point,
created_by=user1,
)
assert Consent.objects.count() == 1

# get the created consent
consent = Consent.objects.get(delivery_point=delivery_point)

assert consent.delivery_point == delivery_point
assert consent.created_by == user1
assert consent.created_by == delivery_point.entity.users.first()
assert consent.status == AWAITING
assert consent.revoked_at is None
assert consent.start is not None
assert consent.end is not None

# test consent.end is 90 days later than the consent.start
end_date = consent.start + timedelta(days=90)
consent_start = formats.date_format(end_date, "Y/m/d")
consent_end = formats.date_format(consent.end, "Y/m/d")
assert consent_start == consent_end
# test consent.end is the last day of the year
expected_end_date = calculate_consent_end_date().replace(microsecond=0)
assert consent.end.replace(microsecond=0) == expected_end_date

# test created_at and updated_at have been updated.
assert consent.created_at is not None
assert consent.updated_at is not None


@pytest.mark.django_db
def test_update_consent_status():
"""Tests updating a consent status."""
user1 = UserFactory()
delivery_point = DeliveryPointFactory()
def test_create_consent_with_custom_period_date():
"""Tests the creation of a consent with a custom period date (`start` / `end`)."""
expected_start_date = "2024-12-20"
expected_end_date = "2024-12-22"

consent = ConsentFactory(
delivery_point=delivery_point,
created_by=user1,
# Create one consent with custom start / end date
# we have to disconnect the signals that allow the creation of a consent after the
# creation of a delivery point:
# `ConsentFactory` creates a new `delivery_point` which itself creates a new
# `consent`.
assert Consent.objects.count() == 0
signals.post_save.disconnect(
receiver=handle_new_delivery_point,
sender=DeliveryPoint,
dispatch_uid="handle_new_delivery_point",
)

new_updated_at = consent.updated_at
consent = ConsentFactory(start=expected_start_date, end=expected_end_date)

signals.post_save.connect(
receiver=handle_new_delivery_point,
sender=DeliveryPoint,
dispatch_uid="handle_new_delivery_point",
)
assert Consent.objects.count() == 1

assert consent.start == expected_start_date
assert consent.end == expected_end_date


@pytest.mark.django_db
def test_update_consent_status_and_period_date():
"""Tests updating a consent status."""
# create one `delivery_point` and consequently one `consent`
delivery_point = DeliveryPointFactory()

# get the created consent
consent = Consent.objects.get(delivery_point=delivery_point)
consent_updated_at = consent.updated_at

# update status to VALIDATED
consent.status = VALIDATED
consent.save()
assert consent.status == VALIDATED
assert consent.updated_at > new_updated_at
assert consent.updated_at > consent_updated_at
assert consent.revoked_at is None
new_updated_at = consent.updated_at

Expand All @@ -67,3 +96,12 @@ def test_update_consent_status():
assert consent.status == REVOKED
assert consent.updated_at > new_updated_at
assert consent.revoked_at is not None

# update start and end date
expected_start_date = "2024-12-20"
expected_end_date = "2024-12-22"
consent.start = expected_start_date
consent.end = expected_end_date
consent.save()
assert consent.start == expected_start_date
assert consent.end == expected_end_date
24 changes: 24 additions & 0 deletions src/dashboard/apps/consent/tests/test_signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Dashboard consent signals tests."""

import pytest

from apps.consent.models import Consent
from apps.consent.utils import calculate_consent_end_date
from apps.core.factories import DeliveryPointFactory


@pytest.mark.django_db
def test_signal_triggered_on_delivery_point_creation():
"""Tests if the signal is triggered when creating a DeliveryPoint."""
assert Consent.objects.count() == 0

# Create new delivery point
delivery_point = DeliveryPointFactory()

# check if consent was created with signals after delivery_point creation.
assert Consent.objects.count() == 1

consent = Consent.objects.first()
assert consent.delivery_point == delivery_point
expected_date = calculate_consent_end_date()
assert consent.end.replace(microsecond=0) == expected_date.replace(microsecond=0)
33 changes: 33 additions & 0 deletions src/dashboard/apps/consent/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Dashboard consent utils tests."""

from datetime import datetime, timedelta, timezone

import pytest

from apps.consent.utils import calculate_consent_end_date


@pytest.mark.django_db
def test_calculate_consent_end_date_with_days():
"""Test `calculate_consent_end_date` function when days argument is provided."""
days = 10
end_date = calculate_consent_end_date(days=days).replace(microsecond=0)
expected_date = datetime.now().replace(tzinfo=timezone.utc) + timedelta(days=days)
assert end_date == expected_date.replace(microsecond=0)


@pytest.mark.django_db
def test_calculate_consent_end_date_without_days():
"""Test `calculate_consent_end_date` function when days argument is not provided."""
end_date = calculate_consent_end_date().replace(microsecond=0)
current_year = datetime.now().year
expected_date = datetime(
year=current_year,
month=12,
day=31,
hour=23,
minute=59,
second=59,
tzinfo=timezone.utc,
)
assert end_date == expected_date.replace(microsecond=0)
Loading

0 comments on commit 86eb493

Please sign in to comment.