Skip to content

Commit

Permalink
👔(dashboard) update the consent status update logic and add validatio…
Browse files Browse the repository at this point in the history
…n checks

- Implemented more stringent validation checks during the status change from `VALIDATED` to `REVOKED`. This update ensures that only the necessary fields can be adjusted during this transition.
- add validation on the allowed fields
- add validation when consent status is updated from validated status
- add documentation on business logic of consent in the readme.md file
- add custom consent exception
- update test
- update changelog
  • Loading branch information
ssorin committed Jan 16, 2025
1 parent 0409835 commit f33a9e4
Show file tree
Hide file tree
Showing 5 changed files with 469 additions and 66 deletions.
4 changes: 3 additions & 1 deletion src/dashboard/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ and this project adheres to
- 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 consent has the status `VALIDATED`
- block the deletion of consent if it has the status `VALIDATED`
- Allow consent updates if status changes from `VALIDATED` to `REVOKED`
and updated fields are allowed to be updated
- block the deletion of consent if it has the status `VALIDATED` or `REVOKED`
- block consent updates (via the consent form) if the consent status is not `AWAITING`
- integration of custom 403, 404 and 500 pages
- sentry integration
Expand Down
14 changes: 14 additions & 0 deletions src/dashboard/apps/consent/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Dashboard consent exceptions."""

from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _


class ConsentWorkflowError(ValidationError):
"""Exception for consent workflow validation errors."""

DEFAULT_MESSAGE = _("Consent workflow error.")

def __init__(self, custom_message=None):
"""Initialize the exception with an optional custom message."""
super().__init__(custom_message or self.DEFAULT_MESSAGE)
92 changes: 76 additions & 16 deletions src/dashboard/apps/consent/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +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, VALIDATED
from .exceptions import ConsentWorkflowError
from .managers import ConsentManager
from .utils import consent_end_date

Expand All @@ -30,8 +30,6 @@ 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"
)
Expand Down Expand Up @@ -80,28 +78,90 @@ def clean(self):
ValidationError
If the Consent object's status is `VALIDATED`.
"""
if self._is_validated_and_modified():
raise ValidationError(message=self.VALIDATION_ERROR_MESSAGE)
if self._is_update_allowed():
return

def save(self, *args, **kwargs):
"""Saves with custom logic.
If the consent status is `REVOKED`, `revoked_at` is updated to the current time.
- Validates and restricts updates to the consent based on its status and
modified fields.
- Updates `revoked_at` with the current date if the consent status is `REVOKED`.
"""
if self._is_validated_and_modified():
raise ValidationError(message=self.VALIDATION_ERROR_MESSAGE)
if not self._is_update_allowed():
return

if self.status == REVOKED:
self.revoked_at = timezone.now()

return super(Consent, self).save(*args, **kwargs)

def delete(self, *args, **kwargs):
"""Restrict the deletion of a consent if its status is `VALIDATED`."""
if self._loaded_values.get("status") == VALIDATED:
raise ValidationError(message=self.VALIDATION_ERROR_MESSAGE)
super().delete(*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
"""Restrict the deletion of a consent.
Consents cannot be deleted if status is `VALIDATED` or `REVOKED`.
"""
if self._is_deletion_allowed():
super().delete(*args, **kwargs)

def _is_update_allowed(self) -> bool:
"""Check if consent can be updated.
Workflow according to consent status:
- AWAITING:
- can be updated without restriction
- VALIDATED
- if the status is updated to something other than REVOKED, an exception is
raised,
- if the status is updated to REVOKED, we check the updated fields are
allowed to be updated.
- REVOKED
- can be updated without restriction
todo: add restriction: REVOKED consent cannot be modified
"""
ALLOWED_UPDATE_FIELDS = {"status", "revoked_at", "updated_at"}

if self._state.adding:
return True

loaded_status = self._loaded_values.get("status") # type: ignore[attr-defined]
updated_status = self.status

if loaded_status == VALIDATED:
if updated_status != REVOKED:
raise ConsentWorkflowError(
_('Validated consent can only be changed to the status "revoked".')
)

# Update the consent status from VALIDATED to REVOKED
# we check the updated fields are allowed to be updated.
updated_fields = {
field
for field, loaded_value in self._loaded_values.items() # type: ignore[attr-defined]
if getattr(self, field) != loaded_value
}

if not updated_fields.issubset(ALLOWED_UPDATE_FIELDS):
raise ConsentWorkflowError(
_(
'Only the authorized fields ("status", "revoked_at" and '
'"updated_at") can be modified.'
)
)

return True

def _is_deletion_allowed(self) -> bool:
"""Check if a consent can be deleted.
Consent cannot be deleted if his loaded status is `VALIDATED` or `REVOKED`.
"""
if self._loaded_values.get("status") == VALIDATED: # type: ignore[attr-defined]
raise ConsentWorkflowError(_("Validated consent cannot be deleted."))

elif self._loaded_values.get("status") == REVOKED: # type: ignore[attr-defined]
raise ConsentWorkflowError(_("Revoked consent cannot be deleted."))

return True
Loading

0 comments on commit f33a9e4

Please sign in to comment.