Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

👔(dashboard) update consent status logic when update from validated to revoked #341

Merged
merged 1 commit into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/dashboard/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ and this project adheres to
- add admin integration for Entity, DeliveryPoint and Consent
- add mass admin action (make revoked) 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 selected consent fields update if status changes from `VALIDATED` to `REVOKED`
- 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
jmaupetit marked this conversation as resolved.
Show resolved Hide resolved
"""
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(
_(
f"Only the authorized fields "
f"({', '.join(sorted(ALLOWED_UPDATE_FIELDS))}) 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
Loading