Skip to content

Commit

Permalink
✨(dashboard) Consent management
Browse files Browse the repository at this point in the history
- add Consent management views and template
- add JavaScript functionality to select/uncheck all checkboxes in consent forms
- enhance the admin interface
- add test
- add migration to proxy_for symmetrical=False
  • Loading branch information
ssorin committed Dec 3, 2024
1 parent 91bcfc7 commit 61f1355
Show file tree
Hide file tree
Showing 18 changed files with 872 additions and 48 deletions.
1 change: 1 addition & 0 deletions src/dashboard/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to
- introduce new custom user model
- add consent app with Consent model
- add core app with Entity and DeliveryPoint models
- add consent form to manage consents of one or many entities

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

1 change: 0 additions & 1 deletion src/dashboard/apps/consent/fixtures/consent.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ def seed_consent():
entity3 = EntityFactory(users=(user3,), proxy_for=(entity1, entity2))
entity4 = EntityFactory(users=(user5,))


# create delivery points
for i in range(1, 4):
DeliveryPointFactory(provider_assigned_id=f"entity1_{i}", entity=entity1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,4 @@ def handle(self, *args, **kwargs):
"""Executes the command for creating development consent fixtures."""
self.stdout.write(self.style.NOTICE("Seeding database with consents..."))
seed_consent()
self.stdout.write(
self.style.SUCCESS("Done.")
)
self.stdout.write(self.style.SUCCESS("Done."))
132 changes: 131 additions & 1 deletion src/dashboard/apps/consent/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""Dashboard consent app models."""

from django.core.exceptions import PermissionDenied
from django.db import models
from django.db.models import Count, QuerySet, TextField
from django.db.models.functions import Cast
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from apps.auth.models import DashboardUser as User
from apps.core.models import DashboardBase, DeliveryPoint
from apps.core.models import DashboardBase, DeliveryPoint, Entity


class Consent(DashboardBase):
Expand Down Expand Up @@ -59,3 +62,130 @@ def save(self, *args, **kwargs):
if self.status == self.REVOKED:
self.revoked_at = timezone.now()
return super(Consent, self).save(*args, **kwargs)

@classmethod
def get_awaiting(cls, user: User, selected_entity=None) -> QuerySet:
"""Retrieves all awaiting consents or consents for a selected entity for a user.
Parameters:
- user (User): The user for whom the consents should be retrieved.
- selected_entity (Entity, optional): An optional entity qs. If provided,
consents will be filtered by this entity.
Returns:
QuerySet: A queryset of Consent objects that match the filter criteria, ordered
by entity and start.
"""
queryset_filters = {}
if selected_entity:
if selected_entity.user_has_perms(user):
queryset_filters["delivery_point__entity"] = selected_entity
else:
raise PermissionDenied()
else:
related_entities = Entity.get_by_user(user)
if related_entities:
queryset_filters["delivery_point__entity__in"] = related_entities
else:
queryset_filters["delivery_point__entity__users"] = user

return cls.objects.filter(
**queryset_filters,
status=cls.AWAITING,
delivery_point__is_active=True,
start__lte=timezone.now(),
end__gte=timezone.now(),
).order_by("delivery_point__entity", "start")

@staticmethod
def count_by_entity(user: User, status: str) -> QuerySet:
"""Counts and returns the number of consents for a given user for each entity.
Parameters:
- user (User): The user for whom to count consents.
- status (str): The status of the consents to be counted. Defaults VALIDATED.
"""
queryset_filters = {}
related_entities = Entity.get_by_user(user)
if related_entities:
queryset_filters["delivery_points__entity__in"] = related_entities
else:
queryset_filters["delivery_points__entity__users"] = user

return Entity.objects.filter(
**queryset_filters,
delivery_points__is_active=True,
delivery_points__consent__status=status,
delivery_points__consent__start__lte=timezone.now(),
delivery_points__consent__end__gte=timezone.now(),
).annotate(dcount=Count("delivery_points"))

@classmethod
def count_validated_by_entity(cls, user: User) -> QuerySet:
"""Counts the number of validated consents associated with a given entity."""
return cls.count_by_entity(user, cls.VALIDATED)

@classmethod
def count_awaiting_by_entity(cls, user: User) -> QuerySet:
"""Counts the number of validated consents associated with a given entity."""
return cls.count_by_entity(user, cls.AWAITING)

@classmethod
def _bulk_update_consent_status(
cls,
status: str,
consent_ids: list[str],
user: User,
) -> int:
"""Update the consent status to VALIDATED for the given list of consent IDs."""
return cls.objects.filter(id__in=consent_ids).update(
status=status,
created_by=user,
updated_at=timezone.now(),
)

@classmethod
def set_consents_status_by_ids(
cls,
selected_ids: list[str],
user: User,
consents_qs: QuerySet,
) -> tuple[int, int]:
"""Updates user consents to VALIDATED/AWAITING by given IDs.
This method updates the consents for a given user by processing and filtering
the selected consent IDs and calculating the non-selected consent IDs.
- Selected ids are update to VALIDATED
- Non-selected ids are update to AWAITING
Parameters:
selected_ids (list[str]): A list of consent IDs selected by the user.
user (User): The user for whom the consents are being updated.
consents_qs (QuerySet): A queryset containing the consent objects to be
processed.
"""
nb_validated = cls._bulk_update_consent_status(
cls.VALIDATED, selected_ids, user
)

base_ids = cls._extract_ids_from_queryset(consents_qs)
awaiting_ids = cls._get_non_selected_ids(base_ids, selected_ids)
nb_awaiting = cls._bulk_update_consent_status(cls.AWAITING, awaiting_ids, user)

return nb_validated, nb_awaiting

@staticmethod
def _extract_ids_from_queryset(consents_qs: QuerySet) -> list[str]:
"""Extracts string representations of IDs from a queryset."""
return list(
consents_qs.annotate(
str_id=Cast("id", output_field=TextField())
).values_list("str_id", flat=True)
)

@staticmethod
def _get_non_selected_ids(
base_ids: list[str], selected_ids: list[str]
) -> list[str]:
"""Retrieve ids from the base_ids that are not present in the selected_ids."""
return [item for item in base_ids if item not in selected_ids]
8 changes: 8 additions & 0 deletions src/dashboard/apps/consent/static/consent/js/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* check/uncheck all checkbox in consent form
*/
document.getElementById("toggle-all")
.addEventListener("change", function() {
const checkboxes = document.getElementsByName("status");
checkboxes.forEach(checkbox => checkbox.checked = this.checked);
});

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{% load i18n %}

{% if awaiting %}
<h2>
{% trans "Consents summary" %}
</h2>

<p>
<a class="fr-link fr-icon-arrow-right-line fr-link--icon-right" href="{% url "consent:manage" %}">
<strong>
{% trans "Validate content for all entities" %}
</strong>
</a>
</p>

<ul>
{% for entity in awaiting %}
<li>
<strong>{{ entity.name }}</strong>
<br />
<a class="fr-link fr-icon-arrow-right-line fr-link--icon-right" href="{% url "consent:manage" entity.slug %}">
{% blocktranslate with entity_count=entity.dcount pluralize=entity.dcount|pluralize %}
{{ entity_count }} consent{{ pluralize }} to validate for this entity
{% endblocktranslate %}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% load i18n %}

{% if validated %}
<h2>{% trans "Validated entities" %}</h2>

<ul>
{% for entity in validated %}
<li>
<strong>{{ entity.name }}</strong>
<br />
{% blocktranslate with entity_count=entity.dcount pluralize=entity.dcount|pluralize %}
{{ entity_count }} consent{{ pluralize }} validated
{% endblocktranslate %}
</li>
{% endfor %}
</ul>
{% endif %}
13 changes: 4 additions & 9 deletions src/dashboard/apps/consent/templates/consent/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,8 @@
{% load i18n %}

{% block content %}
<h2>
{% trans "Consents summary" %}
</h2>

<p>
<a class="fr-link fr-icon-arrow-right-line fr-link--icon-right" href="{% url "consent:manage" %}">
{% trans "Manage consent" %}
</a>
</p>
{% endblock content %}
{% include "consent/includes/_resume_awaiting_consents.html" %}
{% include "consent/includes/_resume_validated_consents.html" %}

{% endblock content %}
67 changes: 67 additions & 0 deletions src/dashboard/apps/consent/templates/consent/manage.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{% extends "home/base.html" %}

{% load i18n static %}

{% block content %}
<h2>{% trans "Manage consents" %}</h2>

{% if consents %}
<form action="" method="post">
{% csrf_token %}
<fieldset class="fr-fieldset" id="checkboxes" aria-labelledby="checkboxes-legend checkboxes-messages">

<legend class="fr-fieldset__legend--regular fr-fieldset__legend"
id="checkboxes-legend"> {% trans "Legend for all elements" %}
</legend>

<div class="fr-fieldset__element">
<div class="fr-checkbox-group">
<input type="checkbox" id="toggle-all" name="toggle-all"
aria-describedby="toggle-all-1-messages"
data-fr-js-checkbox-input="true"
data-fr-js-checkbox-actionee="true">
<label class="fr-label" for="toggle-all">{% trans "Toggle All" %}</label>
</div>
</div>

{% for dp in consents %}
{% ifchanged dp.delivery_point.entity.name %}
<strong>{{ dp.delivery_point.entity.name }}</strong>
{% endifchanged %}

<div class="fr-fieldset__element">
<div class="fr-checkbox-group">
<input type="checkbox"
name="status"
value="{{ dp.id }}"
id="{{ dp.id }}"
{% if dp.status == "VALIDATED" %} checked{% endif %}
aria-describedby="{{ dp.id }}-messages"
data-fr-js-checkbox-input="true"
data-fr-js-checkbox-actionee="true"
>
<label class="fr-label" for="{{ dp.id }}">{{ dp.delivery_point.provider_assigned_id }}</label>
<div class="fr-messages-group" id="{{ dp.id }}-messages" aria-live="assertive"></div>
</div>
</div>
{% endfor %}

<div class="fr-messages-group" id="checkboxes-messages" aria-live="assertive">
{{ field.errors }}
</div>
</fieldset>

<button class="fr-btn" type="submit" name="submit"> {% trans "submit" %} </button>
</form>

{% else %}
<p>{% trans "No consents to validate" %}</p>
{% endif %}
{% endblock content %}

{% block extra_dashboard_js %}
{% if consents %}
<script src="{% static 'consent/js/app.js' %}">
</script>
{% endif %}
{% endblock extra_dashboard_js %}
Loading

0 comments on commit 61f1355

Please sign in to comment.