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) Consent management #245

Merged
merged 1 commit into from
Dec 10, 2024
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
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

11 changes: 10 additions & 1 deletion src/dashboard/apps/auth/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Dashboard auth models."""

from django.contrib.auth.models import AbstractUser
from django.db.models import Q, QuerySet

from apps.core.models import Entity


class DashboardUser(AbstractUser):
Expand All @@ -11,4 +14,10 @@ class DashboardUser(AbstractUser):
AbstractUser model in Django.
"""

pass
def get_entities(self) -> QuerySet[Entity]:
"""Get a list of entities, and their proxies associated."""
return Entity.objects.filter(Q(users=self) | Q(proxies__users=self)).distinct()

def can_validate_entity(self, entity: Entity) -> bool:
"""Determines if the provided entity can be validated."""
return entity in self.get_entities()
53 changes: 53 additions & 0 deletions src/dashboard/apps/auth/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Dashboard auth models tests."""

import pytest
from pytest_django.asserts import assertQuerySetEqual

from apps.auth.factories import AdminUserFactory, UserFactory
from apps.core.factories import EntityFactory


@pytest.mark.django_db
Expand All @@ -25,3 +27,54 @@ def test_create_superuser():
assert admin_user.is_active is True
assert admin_user.is_staff is True
assert admin_user.is_superuser is True


@pytest.mark.django_db
def test_get_entities():
"""Test that user retrieve his entities."""
user1 = UserFactory()
user2 = UserFactory()
user3 = UserFactory()
entity1 = EntityFactory(users=(user1,), name="entity1")
entity2 = EntityFactory(users=(user2,), name="entity2")
entity3 = EntityFactory(
users=(user3,), proxy_for=(entity1, entity2), name="entity3"
)

assertQuerySetEqual(user1.get_entities(), [entity1])
assertQuerySetEqual(user2.get_entities(), [entity2])

user1_entities = user3.get_entities()
user1_expected_entities = [entity1, entity2, entity3]
assertQuerySetEqual(user1_entities, user1_expected_entities)


@pytest.mark.django_db
def test_can_validate_entity():
"""Test if user can validate an entity."""
user1 = UserFactory()
user2 = UserFactory()
user3 = UserFactory()
user4 = UserFactory()

entity1 = EntityFactory(users=(user1,), name="entity1")
entity2 = EntityFactory(users=(user2,), name="entity2")
entity3 = EntityFactory(
users=(user3,), proxy_for=(entity1, entity2), name="entity3"
)
# multiple entities including one with proxy_for
entity4 = EntityFactory(users=(user4,), name="entity4")
entity5 = EntityFactory(users=(user4,), proxy_for=(entity2,), name="entity5")

assert user1.can_validate_entity(entity1) is True
assert user1.can_validate_entity(entity2) is False
assert user1.can_validate_entity(entity3) is False

assert user3.can_validate_entity(entity1) is True
assert user3.can_validate_entity(entity2) is True
assert user3.can_validate_entity(entity3) is True
assert user3.can_validate_entity(entity4) is False

assert user4.can_validate_entity(entity4) is True
assert user4.can_validate_entity(entity5) is True
assert user4.can_validate_entity(entity2) is True
16 changes: 16 additions & 0 deletions src/dashboard/apps/consent/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
"""Dashboard consent app."""

from typing import Literal

from django.utils.translation import gettext_lazy as _

AWAITING: str = "AWAITING"
VALIDATED: str = "VALIDATED"
REVOKED: str = "REVOKED"
CONSENT_STATUS_CHOICE = [
(AWAITING, _("Awaiting")),
(VALIDATED, _("Validated")),
(REVOKED, _("Revoked")),
]
jmaupetit marked this conversation as resolved.
Show resolved Hide resolved

# typing
StatusChoices = Literal["AWAITING", "VALIDATED", "REVOKED"]
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."))
20 changes: 20 additions & 0 deletions src/dashboard/apps/consent/managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Dashboard consent app managers."""

from django.db import models
from django.utils import timezone


class ConsentManager(models.Manager):
"""Custom consent manager."""

def get_queryset(self):
"""Return consents with active delivery point, for the current period."""
return (
super()
.get_queryset()
.filter(
delivery_point__is_active=True,
start__lte=timezone.now(),
end__gte=timezone.now(),
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 5.1.3 on 2024-12-06 14:53

import django.db.models.deletion
import django.db.models.manager
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("qcd_consent", "0001_initial"),
(
"qcd_core",
"0003_alter_deliverypoint_managers_alter_entity_proxy_for_and_more",
),
]

operations = [
migrations.AlterModelManagers(
name="consent",
managers=[
("active_objects", django.db.models.manager.Manager()),
],
),
migrations.AlterField(
model_name="consent",
name="delivery_point",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="consents",
to="qcd_core.deliverypoint",
),
),
]
29 changes: 15 additions & 14 deletions src/dashboard/apps/consent/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
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.abstract_models import DashboardBase

from . import AWAITING, CONSENT_STATUS_CHOICE, REVOKED
from .managers import ConsentManager


class Consent(DashboardBase):
Expand All @@ -26,18 +28,14 @@ class Consent(DashboardBase):
- revoked_at (DateTimeField): recording the revoked date of the consent, if any.
"""

AWAITING = "AWAITING"
VALIDATED = "VALIDATED"
REVOKED = "REVOKED"
CONSENT_STATUS_CHOICE = [
(AWAITING, _("Awaiting")),
(VALIDATED, _("Validated")),
(REVOKED, _("Revoked")),
]

delivery_point = models.ForeignKey(DeliveryPoint, on_delete=models.CASCADE)
delivery_point = models.ForeignKey(
"qcd_core.DeliveryPoint", on_delete=models.CASCADE, related_name="consents"
)
created_by = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, verbose_name=_("created by")
"qcd_auth.DashboardUser",
on_delete=models.SET_NULL,
null=True,
verbose_name=_("created by"),
)
status = models.CharField(
_("status"), max_length=20, choices=CONSENT_STATUS_CHOICE, default=AWAITING
Expand All @@ -48,6 +46,9 @@ class Consent(DashboardBase):
end = models.DateTimeField(_("end date"))
revoked_at = models.DateTimeField(_("revoked at"), null=True, blank=True)

active_objects = ConsentManager()
objects = models.Manager()

class Meta: # noqa: D106
ordering = ["delivery_point"]

Expand All @@ -56,6 +57,6 @@ def __str__(self): # noqa: D105

def save(self, *args, **kwargs):
"""Update the revoked_at timestamps if the consent is revoked."""
if self.status == self.REVOKED:
if self.status == REVOKED:
self.revoked_at = timezone.now()
return super(Consent, self).save(*args, **kwargs)
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);
});
4 changes: 2 additions & 2 deletions src/dashboard/apps/consent/templates/consent/base.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "base.html" %}

{% block content %}
{% endblock content %}
{% block dashboard_content %}
{% endblock dashboard_content %}

This file was deleted.

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

<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 entities %}

<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.count_awaiting_consents pluralize=entity.count_awaiting_consents|pluralize %}
{{ entity_count }} consent{{ pluralize }} to validate for this entity
{% endblocktranslate %}
</a>
</li>
{% endfor %}
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% load i18n %}

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

<ul>
{% for entity in entities %}
<li>
<strong>{{ entity.name }}</strong>
<br />
{% blocktranslate with entity_count=entity.count_validated_consents pluralize=entity.count_validated_consents|pluralize %}
{{ entity_count }} consent{{ pluralize }} validated
{% endblocktranslate %}
</li>
{% endfor %}
</ul>
17 changes: 5 additions & 12 deletions src/dashboard/apps/consent/templates/consent/index.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
{% extends "home/base.html" %}
{% extends "consent/base.html" %}

{% 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 %}
{% block dashboard_content %}
{% include "consent/includes/_resume_awaiting_consents.html" %}
{% include "consent/includes/_resume_validated_consents.html" %}
{% endblock dashboard_content %}
Loading
Loading