diff --git a/src/dashboard/CHANGELOG.md b/src/dashboard/CHANGELOG.md index 6b070c3c..4862bf48 100644 --- a/src/dashboard/CHANGELOG.md +++ b/src/dashboard/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to - add consent form to manage consents of one or many entities - add admin integration for Entity, DeliveryPoint and Consent - add mass admin action (make revoked and make awaiting) for consents +- block the updates of all new data if a consentement has the status `VALIDATED` - 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 diff --git a/src/dashboard/apps/consent/models.py b/src/dashboard/apps/consent/models.py index 61544ad2..e98a6283 100644 --- a/src/dashboard/apps/consent/models.py +++ b/src/dashboard/apps/consent/models.py @@ -1,12 +1,13 @@ """Dashboard consent app models.""" +from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from apps.core.abstract_models import DashboardBase -from . import AWAITING, CONSENT_STATUS_CHOICE, REVOKED +from . import AWAITING, CONSENT_STATUS_CHOICE, REVOKED, VALIDATED from .managers import ConsentManager from .utils import consent_end_date @@ -29,6 +30,8 @@ class Consent(DashboardBase): - revoked_at (DateTimeField): recording the revoked date of the consent, if any. """ + VALIDATION_ERROR_MESSAGE = _("Validated consent cannot be modified once defined.") + delivery_point = models.ForeignKey( "qcd_core.DeliveryPoint", on_delete=models.CASCADE, related_name="consents" ) @@ -57,12 +60,45 @@ class Meta: # noqa: D106 def __str__(self): # noqa: D105 return f"{self.delivery_point} - {self.updated_at}: {self.status}" + @classmethod + def from_db(cls, db, field_names, values): + """Store the original values when an instance is loaded from the database.""" + instance = super().from_db(db, field_names, values) + instance._loaded_values = dict(zip(field_names, values, strict=False)) + return instance + + def clean(self): + """Custom validation logic. + + Validates and restricts updates to the Consent object if its status is set + to `VALIDATED`. This ensures that validated consents cannot be modified + after their status are defined to `VALIDATED` (We prevent this update + for contractual reasons). + + Raises: + ------ + ValidationError + If the Consent object's status is `VALIDATED`. + """ + if self._is_validated_and_modified(): + raise ValidationError( + message=self.VALIDATION_ERROR_MESSAGE + ) + def save(self, *args, **kwargs): """Saves with custom logic. If the consent status is `REVOKED`, `revoked_at` is updated to the current time. """ + if self._is_validated_and_modified(): + raise ValidationError( + message=self.VALIDATION_ERROR_MESSAGE + ) if self.status == REVOKED: self.revoked_at = timezone.now() return super(Consent, self).save(*args, **kwargs) + + def _is_validated_and_modified(self): + """Checks if the validated 'Consent' object is trying to be modified.""" + return not self._state.adding and self._loaded_values.get("status") == VALIDATED diff --git a/src/dashboard/apps/consent/tests/test_models.py b/src/dashboard/apps/consent/tests/test_models.py index 48cfd8e6..62869ec3 100644 --- a/src/dashboard/apps/consent/tests/test_models.py +++ b/src/dashboard/apps/consent/tests/test_models.py @@ -3,6 +3,7 @@ import datetime import pytest +from django.core.exceptions import ValidationError from django.db.models import signals from apps.consent import AWAITING, REVOKED, VALIDATED @@ -100,27 +101,56 @@ def test_create_consent_with_custom_period_date(): @pytest.mark.django_db def test_update_consent_status(): - """Tests updating a consent status.""" + """Tests updating a consent status. + + Test that consents can no longer be modified once their status is passed to + `VALIDATED` (raise ValidationError). + """ from apps.consent.models import Consent # create one `delivery_point` and consequently one `consent` + assert Consent.objects.count() == 0 delivery_point = DeliveryPointFactory() + assert Consent.objects.count() == 1 # get the created consent consent = Consent.objects.get(delivery_point=delivery_point) consent_updated_at = consent.updated_at + assert consent.status == AWAITING + assert consent.revoked_at is None - # update status to VALIDATED - consent.status = VALIDATED + # update status to REVOKED + consent.status = REVOKED consent.save() - assert consent.status == VALIDATED + assert consent.status == REVOKED assert consent.updated_at > consent_updated_at + assert consent.revoked_at is not None + new_updated_at = consent.updated_at + # refresh the state in memory + consent = Consent.objects.get(delivery_point=delivery_point) + + # Update the consent to AWAITING + consent.status = AWAITING + consent.revoked_at = None + consent.save() + assert consent.status == AWAITING + assert consent.updated_at > new_updated_at assert consent.revoked_at is None new_updated_at = consent.updated_at + # refresh the state in memory + consent = Consent.objects.get(delivery_point=delivery_point) - # update status to REVOKED - consent.status = REVOKED + # update status to VALIDATED + consent.status = VALIDATED + consent.revoked_at = None consent.save() - assert consent.status == REVOKED + assert consent.status == VALIDATED assert consent.updated_at > new_updated_at - assert consent.revoked_at is not None + assert consent.revoked_at is None + # refresh the state in memory + consent = Consent.objects.get(delivery_point=delivery_point) + + # The consent status is `VALIDATED`, so it cannot be changed anymore. + with pytest.raises(ValidationError): + consent.status = AWAITING + consent.save()