- {% if form.errors %}
+ {% if form.errors %}
+
+ {% endif %}
{% include "consent/includes/_manage_consents.html" %}
{% include "consent/includes/_manage_company_informations.html" %}
diff --git a/src/dashboard/apps/consent/tests/test_views.py b/src/dashboard/apps/consent/tests/test_views.py
index 41dbe678..076acab2 100644
--- a/src/dashboard/apps/consent/tests/test_views.py
+++ b/src/dashboard/apps/consent/tests/test_views.py
@@ -1,7 +1,9 @@
"""Dashboard consent views tests."""
+import datetime
import uuid
from http import HTTPStatus
+from unittest.mock import MagicMock
import pytest
from django.urls import reverse
@@ -13,6 +15,16 @@
from apps.consent.views import ConsentFormView
from apps.core.factories import DeliveryPointFactory, EntityFactory
+FORM_CLEANED_DATA = {
+ "is_authoritative_signatory": True,
+ "allows_measurements": True,
+ "allows_daily_index_readings": True,
+ "allows_max_daily_power": True,
+ "allows_load_curve": True,
+ "allows_technical_contractual_data": True,
+ "signed_at": "2025-03-01",
+}
+
@pytest.mark.django_db
def test_bulk_update_consent_status_without_ids(rf):
@@ -30,14 +42,16 @@ def test_bulk_update_consent_status_without_ids(rf):
assert all(c == AWAITING for c in Consent.objects.values_list("status", flat=True))
# bulk update to VALIDATED of… nothing, and check 0 record have been updated.
- assert view._bulk_update_consent([], VALIDATED) == 0
+ mock_form = MagicMock()
+ mock_form.cleaned_data = FORM_CLEANED_DATA
+ assert view._bulk_update_consent([], VALIDATED, mock_form) == 0
# and checks that the data has not changed after the update.
assert all(c == AWAITING for c in Consent.objects.values_list("status", flat=True))
@pytest.mark.django_db
-def test_bulk_update_consent_status(rf):
+def test_bulk_update_consent_status(rf, settings):
"""Test all consents are correctly updated."""
user = UserFactory()
@@ -56,19 +70,51 @@ def test_bulk_update_consent_status(rf):
# check data before update
assert all(c == AWAITING for c in Consent.objects.values_list("status", flat=True))
+ mock_form = MagicMock()
+ mock_form.cleaned_data = FORM_CLEANED_DATA
+
# bulk update to VALIDATED, and check all records have been updated.
- assert view._bulk_update_consent(ids, VALIDATED) == size
+ assert view._bulk_update_consent(ids, VALIDATED, mock_form) == size
# and checks that the data has changed to VALIDATED after the update.
assert all(c == VALIDATED for c in Consent.objects.values_list("status", flat=True))
+ for c in Consent.objects.all():
+ assert c.signed_at == datetime.datetime(
+ 2025, 3, 1, 0, 0, tzinfo=datetime.timezone.utc
+ )
+ assert c.signature_location == settings.CONSENT_SIGNATURE_LOCATION
+
+ # check company data are present
+ assert c.company is not None
+ assert c.company["name"] == entity.name
+ assert c.company["company_type"] == entity.company_type
+ assert c.company["siret"] == entity.siret
+
+ # check company representative data are present
+ assert c.company is not None
+ assert c.company_representative["email"] == user.email
+
+ # check control authority data are presents
+ assert c.control_authority is not None
+ assert c.control_authority["name"] == settings.CONSENT_CONTROL_AUTHORITY["name"]
+ assert (
+ c.control_authority["email"] == settings.CONSENT_CONTROL_AUTHORITY["email"]
+ )
+
+ # check authorizations (boolean fields)
+ assert c.is_authoritative_signatory is True
+ assert c.allows_measurements is True
+ assert c.allows_max_daily_power is True
+ assert c.allows_load_curve is True
+ assert c.allows_technical_contractual_data is True
- # bulk update from VALIDATED to AWAITING: no data must be updated
- assert view._bulk_update_consent(ids, AWAITING) == 0
+ # bulk update from VALIDATED to AWAITING: no data must be updated.
+ assert view._bulk_update_consent(ids, AWAITING, mock_form) == 0
# and checks that the status has not changed after the update.
assert all(c == VALIDATED for c in Consent.objects.values_list("status", flat=True))
- # bulk update from VALIDATED to REVOKED: no data must be updated
- assert view._bulk_update_consent(ids, REVOKED) == 0
+ # bulk update from VALIDATED to REVOKED: no data must be updated.
+ assert view._bulk_update_consent(ids, REVOKED, mock_form) == 0
# and checks that the status has not changed after the update.
assert all(c == VALIDATED for c in Consent.objects.values_list("status", flat=True))
@@ -98,7 +144,9 @@ def test_bulk_update_consent_status_with_fake_id(rf):
# bulk update to VALIDATED,
# and check all records have been updated except the fake id.
- assert view._bulk_update_consent(ids, VALIDATED) == size
+ mock_form = MagicMock()
+ mock_form.cleaned_data = FORM_CLEANED_DATA
+ assert view._bulk_update_consent(ids, VALIDATED, mock_form) == size
# and checks that the data has changed to VALIDATED after the update.
assert all(c == VALIDATED for c in Consent.objects.values_list("status", flat=True))
@@ -136,7 +184,9 @@ def test_bulk_update_consent_without_user_perms(rf):
# bulk update to VALIDATED,
# and check all records have been updated except the wrong ID.
- assert view._bulk_update_consent(ids, VALIDATED) == size
+ mock_form = MagicMock()
+ mock_form.cleaned_data = FORM_CLEANED_DATA
+ assert view._bulk_update_consent(ids, VALIDATED, mock_form) == size
# and checks that the data has changed to VALIDATED after the update.
assert all(
diff --git a/src/dashboard/apps/consent/views.py b/src/dashboard/apps/consent/views.py
index d8718bd7..da79311d 100644
--- a/src/dashboard/apps/consent/views.py
+++ b/src/dashboard/apps/consent/views.py
@@ -1,9 +1,11 @@
"""Dashboard consent app views."""
+import sentry_sdk
from django.contrib import messages
-from django.core.exceptions import PermissionDenied
+from django.core.exceptions import PermissionDenied, ValidationError
from django.db.models import Q
-from django.shortcuts import get_object_or_404, redirect
+from django.http import HttpResponse
+from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy as reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
@@ -18,6 +20,11 @@
from .mixins import BreadcrumbContextMixin
from .models import Consent
from .settings import CONSENT_CONTROL_AUTHORITY, CONSENT_SIGNATURE_LOCATION
+from .validators import (
+ validate_company_schema,
+ validate_control_authority_schema,
+ validate_representative_schema,
+)
class IndexView(BreadcrumbContextMixin, TemplateView):
@@ -33,11 +40,102 @@ def get_context_data(self, **kwargs): # noqa: D102
return context
+def _get_validate_company_data(consent: dict) -> dict:
+ """Get the validate company data."""
+ company_data = {
+ "name": consent["delivery_point__entity__name"],
+ "company_type": consent["delivery_point__entity__company_type"],
+ "legal_form": consent["delivery_point__entity__legal_form"],
+ "trade_name": consent["delivery_point__entity__trade_name"],
+ "siret": consent["delivery_point__entity__siret"],
+ "naf": consent["delivery_point__entity__naf"],
+ "address": {
+ "line_1": consent["delivery_point__entity__address_1"],
+ "line_2": consent["delivery_point__entity__address_2"],
+ "zip_code": consent["delivery_point__entity__address_zip_code"],
+ "city": consent["delivery_point__entity__address_city"],
+ },
+ }
+
+ validate_company_schema(company_data)
+
+ return company_data
+
+
+def _get_validate_control_authority():
+ """Get the validate control authority."""
+ control_authority = {
+ "name": CONSENT_CONTROL_AUTHORITY.get("name"),
+ "represented_by": CONSENT_CONTROL_AUTHORITY.get("represented_by"),
+ "email": CONSENT_CONTROL_AUTHORITY.get("email"),
+ "address": {
+ "line_1": CONSENT_CONTROL_AUTHORITY.get("line_1"),
+ "line_2": CONSENT_CONTROL_AUTHORITY.get("line_2"),
+ "zip_code": CONSENT_CONTROL_AUTHORITY.get("zip_code"),
+ "city": CONSENT_CONTROL_AUTHORITY.get("city"),
+ },
+ }
+ validate_control_authority_schema(control_authority)
+
+ return control_authority
+
+
+def _get_validate_company_representative(user):
+ """Get the validate company representative."""
+ company_representative = {
+ "firstname": user.first_name,
+ "lastname": user.last_name,
+ "email": user.email,
+ "phone": None,
+ }
+
+ validate_representative_schema(company_representative)
+
+ return company_representative
+
+
class ConsentFormView(BreadcrumbContextMixin, FormView):
"""Updates the status of consents."""
+ ERROR_MESSAGE = _(
+ "An error occurred while validating the form. "
+ "Our team has been notified and will get back to you shortly."
+ )
+
+ CONSENT_FIELDS_FOR_UPDATE = [
+ "status",
+ "created_by",
+ "updated_at",
+ "company",
+ "control_authority",
+ "company_representative",
+ "is_authoritative_signatory",
+ "allows_measurements",
+ "allows_daily_index_readings",
+ "allows_max_daily_power",
+ "allows_load_curve",
+ "allows_technical_contractual_data",
+ "signed_at",
+ "signature_location",
+ ]
+
+ CONSENT_VALUES_FIELDS = [
+ "id",
+ "delivery_point__entity__name",
+ "delivery_point__entity__company_type",
+ "delivery_point__entity__legal_form",
+ "delivery_point__entity__trade_name",
+ "delivery_point__entity__siret",
+ "delivery_point__entity__naf",
+ "delivery_point__entity__address_1",
+ "delivery_point__entity__address_2",
+ "delivery_point__entity__address_zip_code",
+ "delivery_point__entity__address_city",
+ ]
+
template_name = "consent/manage.html"
form_class = ConsentForm
+ success_url = reverse("consent:index")
breadcrumb_links = [
{"url": reverse("consent:index"), "title": _("Consent")},
@@ -54,13 +152,17 @@ def form_valid(self, form):
"""
selected_ids: list[str] = self.request.POST.getlist("status")
- self._bulk_update_consent(selected_ids, VALIDATED) # type: ignore
- awaiting_ids = self._get_awaiting_ids(selected_ids)
- self._bulk_update_consent(awaiting_ids, AWAITING) # type: ignore
+ try:
+ self._bulk_update_consent(selected_ids, VALIDATED, form) # type: ignore
+ awaiting_ids = self._get_awaiting_ids(selected_ids)
+ self._bulk_update_consent(awaiting_ids, AWAITING, form) # type: ignore
+ except ValidationError as e:
+ sentry_sdk.capture_exception(e)
+ form.add_error(None, self.ERROR_MESSAGE)
+ return self.form_invalid(form)
messages.success(self.request, _("Consents updated."))
-
- return redirect(reverse("consent:index"))
+ return super().form_valid(form)
def get_context_data(self, **kwargs):
"""Add the user's entities to the context.
@@ -88,22 +190,56 @@ def _get_entities(self) -> list:
else:
return list(user.get_entities())
- def _bulk_update_consent(self, ids: list[str], status: str) -> int:
+ def _bulk_update_consent(
+ self, ids: list[str], status: str, form: ConsentForm
+ ) -> HttpResponse | int:
"""Bulk update of the consent status for a given status and list of entities.
- Only `AWAITING` consents can be updated by users.
+ This method updates the consent statuses, and their related information:
+ - company information,
+ - control authority information,
+ - company representative information,
+ - form data,
+ - date of signature,
+ - signature location,
+
+ Note: Only `AWAITING` consents can be updated by users.
"""
- return (
- Consent.objects.filter(id__in=ids, status=AWAITING)
+ # validate and get control authority and company representative data
+ control_authority_data = _get_validate_control_authority()
+ company_representative_data = _get_validate_company_representative(
+ self.request.user
+ )
+
+ # retrieve consents to update, and get related company data for each consent.
+ consents = (
+ Consent.objects.filter(
+ id__in=ids,
+ status=AWAITING,
+ )
.filter(
Q(delivery_point__entity__users=self.request.user)
| Q(delivery_point__entity__proxies__users=self.request.user)
)
- .update(
- status=status,
- created_by=self.request.user,
- updated_at=timezone.now(),
+ .values(*self.CONSENT_VALUES_FIELDS)
+ )
+
+ # build consent object with validated fields to update
+ update_objects = [
+ self._build_consent_object(
+ consent,
+ status,
+ form.cleaned_data,
+ control_authority_data,
+ company_representative_data,
)
+ for consent in consents
+ ]
+
+ # and finally, bulk update the consents
+ return Consent.objects.bulk_update(
+ update_objects,
+ fields=self.CONSENT_FIELDS_FOR_UPDATE,
)
def _get_awaiting_ids(self, validated_ids: list[str]) -> list[str]:
@@ -117,3 +253,43 @@ def _get_awaiting_ids(self, validated_ids: list[str]) -> list[str]:
for c in e.get_consents()
if str(c.id) not in validated_ids
]
+
+ def _build_consent_object(
+ self,
+ consent: dict,
+ status: str,
+ form_values: dict,
+ control_authority: dict,
+ company_representative: dict,
+ ) -> Consent:
+ """Builds and returns a `Consent` object with validated data.
+
+ Parameters:
+ - consent (dict): Consent data, including the identifier, and the related
+ entity information.
+ - status (str): Consent status (e.g., "AWAITING", "VALIDATED").
+ - form_values (dict): Validated data from the consent form.
+ - control_authority (dict): Details about the control authority.
+ - company_representative (dict): Information about the company representative.
+ """
+ company_data = _get_validate_company_data(consent)
+
+ return Consent(
+ id=consent["id"],
+ status=status,
+ created_by=self.request.user, # type: ignore[misc]
+ updated_at=timezone.now(),
+ company=company_data,
+ control_authority=control_authority,
+ company_representative=company_representative,
+ is_authoritative_signatory=form_values["is_authoritative_signatory"],
+ allows_measurements=form_values["allows_measurements"],
+ allows_daily_index_readings=form_values["allows_daily_index_readings"],
+ allows_max_daily_power=form_values["allows_max_daily_power"],
+ allows_load_curve=form_values["allows_load_curve"],
+ allows_technical_contractual_data=form_values[
+ "allows_technical_contractual_data"
+ ],
+ signed_at=form_values["signed_at"],
+ signature_location=CONSENT_SIGNATURE_LOCATION,
+ )