Skip to content

Commit

Permalink
✨(dashboard) add the global consent
Browse files Browse the repository at this point in the history
Replaced function-based `consent_form_view` with `ConsentFormView` class-based view, leveraging Django forms for better structure and maintainability. Introduced a `ConsentForm` for managing consent input and added a `BreadcrumbContextMixin` to streamline breadcrumb handling. Updated templates and URLs accordingly for this refactor.
Refactored consent views by splitting and improving helper functions for better clarity and reusability. Introduced comprehensive tests for `_bulk_update_consent`, ensuring proper validation of consent status updates with varied scenarios. This enhances maintainability and test coverage.
  • Loading branch information
ssorin committed Dec 16, 2024
1 parent 1c1ad0b commit 99c89c0
Show file tree
Hide file tree
Showing 9 changed files with 362 additions and 132 deletions.
10 changes: 5 additions & 5 deletions src/dashboard/apps/consent/fixtures/consent.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ def seed_consent():
entity4 = EntityFactory(users=(user5,))

# create delivery points
for i in range(1, 4):
DeliveryPointFactory(provider_assigned_id=f"entity1_{i}", entity=entity1)
DeliveryPointFactory(provider_assigned_id=f"entity2_{i}", entity=entity2)
DeliveryPointFactory(provider_assigned_id=f"entity3_{i}", entity=entity3)
DeliveryPointFactory(provider_assigned_id=f"entity4_{i}", entity=entity4)
for _i 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():
Expand Down
26 changes: 26 additions & 0 deletions src/dashboard/apps/consent/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Dashboard consent app forms."""

from django import forms
from django.forms.widgets import CheckboxInput
from django.utils.translation import gettext_lazy as _


class ConsentCheckboxInput(CheckboxInput):
"""Custom CheckboxInput widget for rendering a checkbox input field."""

template_name = "consent/forms/widgets/checkbox.html"


class ConsentForm(forms.Form):
"""Save user consent through a checkbox field."""

consent_agreed = forms.BooleanField(
required=True,
initial=False,
widget=ConsentCheckboxInput(
attrs={
"label": _("I agree to give my consent"),
"help_text": _("Please confirm your consent by checking this box."),
},
),
)
37 changes: 37 additions & 0 deletions src/dashboard/apps/consent/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Dashboard consent app mixins."""

from django.views.generic.base import ContextMixin
from django_stubs_ext import StrOrPromise


class BreadcrumbContextMixin(ContextMixin):
"""Mixin to simplify usage of the `dsfr_breadcrumb` in class based views.
Add the breadcrumb elements in the view context for the dsfr breadcrumb:
https://numerique-gouv.github.io/django-dsfr/components/breadcrumb/.
```python
breadcrumb_links = [{"url": "first-url", "title": "First title"}, {...}],
breadcrumb_current: "Current page title",
breadcrumb_root_dir: "the root directory, if the site is not installed at the
root of the domain"
}
```
"""

breadcrumb_links: list[dict[StrOrPromise, StrOrPromise]] | None = None
breadcrumb_current: StrOrPromise | None = None
breadcrumb_root_dir: StrOrPromise | None = None

def get_context_data(self, **kwargs) -> dict:
"""Add breadcrumb context to the view."""
context = super().get_context_data(**kwargs)

context["breadcrumb_data"] = {
"links": self.breadcrumb_links,
"current": self.breadcrumb_current,
}
if self.breadcrumb_root_dir:
context["breadcrumb_data"]["root_dir"] = self.breadcrumb_root_dir

return context
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% include "django/forms/widgets/input.html" %}

<label class="fr-label"
{% if widget.attrs.id %}for="{{ widget.attrs.id }}"{% endif %}>
{{ widget.attrs.label }}
<span class="fr-hint-text">{{ widget.attrs.help_text }}</span>
</label>
116 changes: 59 additions & 57 deletions src/dashboard/apps/consent/templates/consent/manage.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,85 +3,87 @@
{% load i18n static %}

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

{% if entities %}
<form action="" method="post">
{% csrf_token %}
<h2>{% trans "Manage consents" %}</h2>

{% if entities %}
<form action="" method="post">
{% csrf_token %}

<div class="fr-messages-group" id="error-messages" aria-live="assertive">
{% if form.errors %}
<div class="{% if form.consent_agreed.errors %}fr-pl-3v{% endif %} fr-mb-6v">
<p class="fr-message fr-message--error" id="message-error">
{% trans "The form contains errors" %}
</p>
</div>
{% endif %}
</div>

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

<div class="consent-wrapper fr-py-6v fr-mb-6v">
<div class="consent-wrapper__inner">
{% for entity in entities %}
<fieldset class="fr-fieldset"
id="checkboxes"
aria-labelledby="checkboxes-legend checkboxes-messages">
<legend class="fr-fieldset__legend--regular fr-fieldset__legend"
id="checkboxes-legend">
{{ entity.name }}
</legend>

{% for consent in entity.get_consents %}
<div class="fr-fieldset__element">
<div class="fr-checkbox-group">
<input type="checkbox"
name="status"
value="{{ consent.id }}"
id="{{ consent.id }}"
{% if consent.status == 'VALIDATED' %} checked{% endif %}
aria-describedby="{{ consent.id }}-messages"
data-fr-js-checkbox-input="true"
data-fr-js-checkbox-actionee="true" />
<label class="fr-label" for="{{ consent.id }}">
{{ consent.delivery_point.provider_assigned_id }}
</label>
<div class="fr-messages-group"
id="{{ consent.id }}-messages"
aria-live="assertive">
</div>
</div>
</div>
{% endfor %}
<fieldset class="fr-fieldset" id="checkboxes" aria-labelledby="checkboxes-legend checkboxes-messages">
<legend class="fr-fieldset__legend--regular fr-fieldset__legend" id="checkboxes-legend">{{ entity.name }}</legend>

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

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

{# checkbox to apply the consent globally #}
<div class="{% if form.consent_agreed.errors %}fr-pl-3v{% endif %} fr-mb-6v">
<div class="fr-checkbox-group {% if form.consent_agreed.errors %}fr-checkbox-group--error{% endif %}">
{{ form.consent_agreed }}
<div class="fr-messages-group" id="{{ consent.id }}-messages" aria-live="assertive">
{% if form.consent_agreed.errors %}
{% for error in form.consent_agreed.errors %}
<p class="fr-message fr-message--error" id="checkboxes-error-message-error">
{{ error }}
</p>
{% endfor %}
{% endif %}
</div>
</div>
</div>

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

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

{% block dashboard_extra_js %}
{% if entities %}
<script src="{% static 'apps/consent/js/app.js' %}"></script>
<script src="{% static 'apps/consent/js/app.js' %}"></script>
{% endif %}
{% endblock dashboard_extra_js %}
146 changes: 146 additions & 0 deletions src/dashboard/apps/consent/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""Dashboard consent views tests."""

import pytest
from django.urls import reverse

from apps.auth.factories import UserFactory
from apps.consent import AWAITING, VALIDATED
from apps.consent.factories import ConsentFactory
from apps.consent.models import Consent
from apps.consent.views import ConsentFormView
from apps.core.factories import DeliveryPointFactory, EntityFactory
from apps.core.models import DeliveryPoint


@pytest.mark.django_db
def test_bulk_update_consent_status_without_ids(rf):
"""Test that no status is updated if no id is passed."""
request = rf.get(reverse("consent:manage"))
request.user = UserFactory()

view = ConsentFormView()
view.setup(request)

ConsentFactory.create_batch(4)

ids = []
assert view._bulk_update_consent(ids, VALIDATED) == 0
for consent in Consent.objects.all():
assert consent.status == AWAITING


@pytest.mark.django_db
def test_bulk_update_consent_status(rf):
"""Test that all consents are correctly updated."""
user1 = UserFactory()

request = rf.get(reverse("consent:manage"))
request.user = user1

view = ConsentFormView()
view.setup(request)
ids = []
entity1 = EntityFactory(users=(user1,), name="entity1")
for i in range(1, 4):
DeliveryPointFactory(provider_assigned_id=f"entity1_{i}", entity=entity1)

# create awaiting consent for each delivery points
ids = []
for delivery_point in DeliveryPoint.objects.all():
consent = ConsentFactory(delivery_point=delivery_point, created_by=user1)
ids.append(consent.id)

assert view._bulk_update_consent(ids, VALIDATED) == 3 # noqa: PLR2004
for consent in Consent.objects.all():
assert consent.status == VALIDATED


@pytest.mark.django_db
def test_bulk_update_consent_status_with_fake_id(rf):
"""Test update with wrong ID in list of ids to update."""
user1 = UserFactory()

request = rf.get(reverse("consent:manage"))
request.user = user1

view = ConsentFormView()
view.setup(request)
ids = ["fa62cf1d-c510-498a-b428-fdf72fa35651"]
entity1 = EntityFactory(users=(user1,), name="entity1")
for i in range(1, 4):
DeliveryPointFactory(provider_assigned_id=f"entity1_{i}", entity=entity1)

# create awaiting consent for each delivery points
ids = []
for delivery_point in DeliveryPoint.objects.all():
consent = ConsentFactory(delivery_point=delivery_point, created_by=user1)
ids.append(consent.id)

assert view._bulk_update_consent(ids, VALIDATED) == 3 # noqa: PLR2004
for consent in Consent.objects.all():
assert consent.status == VALIDATED


@pytest.mark.django_db
def test_bulk_update_consent_without_user_perms(rf):
"""Test the update of consents for which the user does not have the rights."""
user1 = UserFactory()

request = rf.get(reverse("consent:manage"))
request.user = user1

view = ConsentFormView()
view.setup(request)

entity1 = EntityFactory(users=(user1,), name="entity1")
for i in range(1, 4):
DeliveryPointFactory(provider_assigned_id=f"entity1_{i}", entity=entity1)

# create wrong consent
user2 = UserFactory()
entity2 = EntityFactory(users=(user2,), name="entity2")
wrong_id = DeliveryPointFactory(provider_assigned_id="entity2_1234", entity=entity2)

# create awaiting consent for each delivery points
ids = []
for delivery_point in DeliveryPoint.objects.all():
consent = ConsentFactory(delivery_point=delivery_point, created_by=user1)
ids.append(consent.id)

assert len(ids) == 4 # noqa: PLR2004
wrong_consent = Consent.objects.get(delivery_point=wrong_id)
assert wrong_consent.id in ids
assert view._bulk_update_consent(ids, VALIDATED) == 3 # noqa: PLR2004
for consent in Consent.objects.filter(delivery_point__entity=entity1):
assert consent.status == VALIDATED
for consent in Consent.objects.filter(delivery_point__entity=entity2):
assert consent.status == AWAITING


@pytest.mark.django_db
def test_get_awaiting_ids(rf):
"""Test getting of awaiting ids inferred from validated consents."""
user1 = UserFactory()

request = rf.get(reverse("consent:manage"))
request.user = user1

view = ConsentFormView()
view.setup(request)

entity1 = EntityFactory(users=(user1,), name="entity1")
for i in range(1, 4):
DeliveryPointFactory(provider_assigned_id=f"entity1_{i}", entity=entity1)

# create awaiting consent for each delivery points
ids = []
for delivery_point in DeliveryPoint.objects.all():
consent = ConsentFactory(delivery_point=delivery_point, created_by=user1)
ids.append(str(consent.id))

# removes one `id` from the list `ids`,
# this is the one we must find with _get_awaiting_ids()
id_not_include = ids.pop()
awaiting_ids = view._get_awaiting_ids(validated_ids=ids)
assert len(awaiting_ids) == 1
assert id_not_include in awaiting_ids
6 changes: 3 additions & 3 deletions src/dashboard/apps/consent/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

from django.urls import path

from .views import IndexView, consent_form_view
from .views import ConsentFormView, IndexView

app_name = "consent"

urlpatterns = [
path("", IndexView.as_view(), name="index"),
path("manage/", consent_form_view, name="manage"),
path("manage/<slug:slug>", consent_form_view, name="manage"),
path("manage/", ConsentFormView.as_view(), name="manage"),
path("manage/<slug:slug>", ConsentFormView.as_view(), name="manage"),
]
Loading

0 comments on commit 99c89c0

Please sign in to comment.