From fb6bddab3074219b2ed0d5809b08396328713af8 Mon Sep 17 00:00:00 2001 From: ssorin Date: Fri, 29 Nov 2024 11:29:11 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(dashboard)=20Consent=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../apps/consent/consent_management.py | 37 +++ .../apps/consent/fixtures/consent.py | 1 - .../management/commands/seed_consent.py | 4 +- .../apps/consent/permissions_utils.py | 25 ++ src/dashboard/apps/consent/querysets.py | 83 ++++++ .../apps/consent/static/consent/js/app.js | 8 + .../templates/consent/consent-management.html | 9 - .../includes/_resume_awaiting_consents.html | 29 ++ .../includes/_resume_validated_consents.html | 17 ++ .../apps/consent/templates/consent/index.html | 13 +- .../consent/templates/consent/manage.html | 67 +++++ .../apps/consent/tests/test_querysets.py | 262 ++++++++++++++++++ src/dashboard/apps/consent/urls.py | 5 +- src/dashboard/apps/consent/views.py | 60 +++- src/dashboard/apps/core/admin.py | 19 +- .../0002_alter_deliverypoint_entity.py | 24 ++ src/dashboard/apps/core/models.py | 9 +- src/dashboard/dashboard/settings.py | 2 +- src/dashboard/templates/base.html | 1 + 19 files changed, 632 insertions(+), 43 deletions(-) create mode 100644 src/dashboard/apps/consent/consent_management.py create mode 100644 src/dashboard/apps/consent/permissions_utils.py create mode 100644 src/dashboard/apps/consent/querysets.py create mode 100644 src/dashboard/apps/consent/static/consent/js/app.js delete mode 100644 src/dashboard/apps/consent/templates/consent/consent-management.html create mode 100644 src/dashboard/apps/consent/templates/consent/includes/_resume_awaiting_consents.html create mode 100644 src/dashboard/apps/consent/templates/consent/includes/_resume_validated_consents.html create mode 100644 src/dashboard/apps/consent/templates/consent/manage.html create mode 100644 src/dashboard/apps/consent/tests/test_querysets.py create mode 100644 src/dashboard/apps/core/migrations/0002_alter_deliverypoint_entity.py diff --git a/src/dashboard/apps/consent/consent_management.py b/src/dashboard/apps/consent/consent_management.py new file mode 100644 index 00000000..5633b5c1 --- /dev/null +++ b/src/dashboard/apps/consent/consent_management.py @@ -0,0 +1,37 @@ +"""Dashboard consent permissions utils.""" + +from django.db.models import QuerySet, TextField +from django.db.models.functions import Cast + +from .models import Consent + + +def consent_validate(selected_ids: list): + """Updates the status of consent to VALIDATED for the given list of consent IDs. + + Returns: + int: The number of records that were successfully updated. + + """ + return Consent.objects.filter(id__in=selected_ids).update(status=Consent.VALIDATED) + + +def consent_awaiting(consents: QuerySet, selected_ids: list): + """Updates the status of consent to AWAITING for the given list of consent IDs. + + Parameters: + consents (QuerySet): A QuerySet of Consent objects to be processed. + selected_ids (list): A list of IDs for which the consent status should not be + updated. + + Returns: + int: The number of consents updated to 'AWAITING' status. + """ + base_ids = list( + consents.annotate(str_id=Cast("id", output_field=TextField())).values_list( + "str_id", flat=True + ) + ) + awaiting_ids = [item for item in base_ids if item not in selected_ids] + + return Consent.objects.filter(id__in=awaiting_ids).update(status=Consent.AWAITING) diff --git a/src/dashboard/apps/consent/fixtures/consent.py b/src/dashboard/apps/consent/fixtures/consent.py index 80597646..c25f632e 100644 --- a/src/dashboard/apps/consent/fixtures/consent.py +++ b/src/dashboard/apps/consent/fixtures/consent.py @@ -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) diff --git a/src/dashboard/apps/consent/management/commands/seed_consent.py b/src/dashboard/apps/consent/management/commands/seed_consent.py index fe00986d..2b14b394 100644 --- a/src/dashboard/apps/consent/management/commands/seed_consent.py +++ b/src/dashboard/apps/consent/management/commands/seed_consent.py @@ -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.")) diff --git a/src/dashboard/apps/consent/permissions_utils.py b/src/dashboard/apps/consent/permissions_utils.py new file mode 100644 index 00000000..a671b5e4 --- /dev/null +++ b/src/dashboard/apps/consent/permissions_utils.py @@ -0,0 +1,25 @@ +"""Dashboard consent permissions utils.""" + +from django.db.models import QuerySet + +from apps.core.models import Entity + + +def has_entity_perms(user, selected_entity: Entity) -> bool: + """Check if the user has permission to access the selected entity.""" + + def is_direct_user(entity: Entity) -> bool: + return Entity.objects.filter(slug=selected_entity.slug, users=user).exists() + + def is_proxy_user(entities: QuerySet) -> bool: + return any( + proxy.slug == selected_entity.slug + for entity in entities + for proxy in entity.proxy_for.all() + ) + + if is_direct_user(selected_entity): + return True + + user_entities = Entity.objects.filter(users=user) + return is_proxy_user(user_entities) diff --git a/src/dashboard/apps/consent/querysets.py b/src/dashboard/apps/consent/querysets.py new file mode 100644 index 00000000..8d670eec --- /dev/null +++ b/src/dashboard/apps/consent/querysets.py @@ -0,0 +1,83 @@ +"""Dashboard consent QuerySets.""" + +from django.core.exceptions import PermissionDenied +from django.db.models import Count + +from apps.core.models import Entity + +from .models import Consent +from .permissions_utils import has_entity_perms + + +def get_user_entities(user): + """Retrieve a list of entities and their proxies associated with the user.""" + entities = Entity.objects.filter(users=user) + related_entities = [] + + for entity in entities: + related_entities.append(entity) + related_entities.extend(list(entity.proxy_for.all())) + + return related_entities + + +def get_awaiting_consents(user, selected_entity=None): + """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 filter. 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 has_entity_perms(user, selected_entity): + queryset_filters["delivery_point__entity"] = selected_entity + else: + raise PermissionDenied() + else: + related_entities = get_user_entities(user) + if related_entities: + queryset_filters["delivery_point__entity__in"] = related_entities + else: + queryset_filters["delivery_point__entity__users"] = user + + return Consent.objects.filter( + **queryset_filters, status=Consent.AWAITING, delivery_point__is_active=True + ).order_by("delivery_point__entity", "start") + + +def count_consents_by_entity(user, status: str = Consent.VALIDATED): + """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 = get_user_entities(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, + ).annotate(dcount=Count("delivery_points")) + + +def count_validated_consents_by_entity(user): + """Counts the number of validated consents associated with a given entity.""" + return count_consents_by_entity(user, Consent.VALIDATED) + + +def count_awaiting_consents_by_entity(user): + """Counts the number of validated consents associated with a given entity.""" + return count_consents_by_entity(user, Consent.AWAITING) diff --git a/src/dashboard/apps/consent/static/consent/js/app.js b/src/dashboard/apps/consent/static/consent/js/app.js new file mode 100644 index 00000000..938eb111 --- /dev/null +++ b/src/dashboard/apps/consent/static/consent/js/app.js @@ -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); +}); diff --git a/src/dashboard/apps/consent/templates/consent/consent-management.html b/src/dashboard/apps/consent/templates/consent/consent-management.html deleted file mode 100644 index e267143f..00000000 --- a/src/dashboard/apps/consent/templates/consent/consent-management.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "home/base.html" %} - -{% load i18n %} - -{% block content %} -

- {% trans "Consents management" %} -

-{% endblock content %} diff --git a/src/dashboard/apps/consent/templates/consent/includes/_resume_awaiting_consents.html b/src/dashboard/apps/consent/templates/consent/includes/_resume_awaiting_consents.html new file mode 100644 index 00000000..7795712f --- /dev/null +++ b/src/dashboard/apps/consent/templates/consent/includes/_resume_awaiting_consents.html @@ -0,0 +1,29 @@ +{% load i18n %} + +{% if awaiting %} +

+ {% trans "Consents summary" %} +

+ +

+ + + {% trans "Validate content for all entities" %} + + +

+ + +{% endif %} \ No newline at end of file diff --git a/src/dashboard/apps/consent/templates/consent/includes/_resume_validated_consents.html b/src/dashboard/apps/consent/templates/consent/includes/_resume_validated_consents.html new file mode 100644 index 00000000..57f294f7 --- /dev/null +++ b/src/dashboard/apps/consent/templates/consent/includes/_resume_validated_consents.html @@ -0,0 +1,17 @@ +{% load i18n %} + +{% if validated %} +

{% trans "Validated entities" %}

+ + +{% endif %} \ No newline at end of file diff --git a/src/dashboard/apps/consent/templates/consent/index.html b/src/dashboard/apps/consent/templates/consent/index.html index ee02c35c..cc9ceb3e 100644 --- a/src/dashboard/apps/consent/templates/consent/index.html +++ b/src/dashboard/apps/consent/templates/consent/index.html @@ -3,13 +3,8 @@ {% load i18n %} {% block content %} -

- {% trans "Consents summary" %} -

-

- - {% trans "Manage consent" %} - -

-{% endblock content %} + {% include "consent/includes/_resume_awaiting_consents.html" %} + {% include "consent/includes/_resume_validated_consents.html" %} + +{% endblock content %} \ No newline at end of file diff --git a/src/dashboard/apps/consent/templates/consent/manage.html b/src/dashboard/apps/consent/templates/consent/manage.html new file mode 100644 index 00000000..841a7294 --- /dev/null +++ b/src/dashboard/apps/consent/templates/consent/manage.html @@ -0,0 +1,67 @@ +{% extends "home/base.html" %} + +{% load i18n static %} + +{% block content %} +

{% trans "Manage consents" %}

+ +{% if consents %} +
+ {% csrf_token %} +
+ + {% trans "Legend for all elements" %} + + +
+
+ + +
+
+ + {% for dp in consents %} + {% ifchanged dp.delivery_point.entity.name %} + {{ dp.delivery_point.entity.name }} + {% endifchanged %} + +
+
+ + +
+
+
+ {% endfor %} + +
+ {{ field.errors }} +
+
+ + +
+ + {% else %} +

{% trans "No consents to validate" %}

+ {% endif %} +{% endblock content %} + +{% block extra_dashboard_js %} + {% if consents %} + + {% endif %} +{% endblock extra_dashboard_js %} diff --git a/src/dashboard/apps/consent/tests/test_querysets.py b/src/dashboard/apps/consent/tests/test_querysets.py new file mode 100644 index 00000000..c69da6e1 --- /dev/null +++ b/src/dashboard/apps/consent/tests/test_querysets.py @@ -0,0 +1,262 @@ +"""Dashboard consent QuerySets tests.""" + +import pytest + +from apps.auth.factories import UserFactory +from apps.consent.factories import ConsentFactory +from apps.consent.models import Consent +from apps.consent.querysets import get_awaiting_consents +from apps.core.factories import DeliveryPointFactory, EntityFactory +from apps.core.models import DeliveryPoint + + +@pytest.mark.django_db +def test_consent_queryset_user_without_entity(): + """User without an associated entity. + + Test user without an associated entity doesn't have any consents awaiting approval. + """ + user = UserFactory() + user2 = UserFactory() + entity2 = EntityFactory(users=(user2,)) + + for i in range(1, 4): + DeliveryPointFactory(provider_assigned_id=f"entity2_{i}", entity=entity2) + + for delivery_point in DeliveryPoint.objects.all(): + ConsentFactory(delivery_point=delivery_point, created_by=user2) + + assert get_awaiting_consents(user).exists() is False + + +@pytest.mark.django_db +def test_consent_queryset_standard_user(): + """Test user with one entity and delivery points. + + Test that user with an associated entity and delivery points have correct + number of consents awaiting approval. + """ + user1 = UserFactory() + user2 = UserFactory() + entity1 = EntityFactory(users=(user1,), name="entity1") + entity2 = EntityFactory(users=(user2,)) + + for i in range(1, 4): + DeliveryPointFactory(provider_assigned_id=f"entity1_{i}", entity=entity1) + DeliveryPointFactory(provider_assigned_id=f"entity2_{i}", entity=entity2) + + for delivery_point in DeliveryPoint.objects.all(): + ConsentFactory(delivery_point=delivery_point, created_by=user1) + + delivery_point = DeliveryPointFactory( + provider_assigned_id="entity2_4", entity=entity2 + ) + ConsentFactory( + delivery_point=delivery_point, created_by=user1, status=Consent.VALIDATED + ) + + awaiting_consents = get_awaiting_consents(user1) + assert awaiting_consents.count() == 3 # noqa: PLR2004 + for consent in awaiting_consents: + assert consent.status == Consent.AWAITING + assert consent.delivery_point.entity.name == "entity1" + + +@pytest.mark.django_db +def test_consent_queryset_standard_user_without_consent(): + """User with one entity and delivery points, but without consents to validated. + + Test if a user with an associated entity and delivery points but no consent + to validate has no consent pending approval. + """ + user1 = UserFactory() + user2 = UserFactory() + entity1 = EntityFactory(users=(user1,), name="entity1") + entity2 = EntityFactory(users=(user2,)) + + for i in range(1, 4): + DeliveryPointFactory(provider_assigned_id=f"entity1_{i}", entity=entity1) + DeliveryPointFactory(provider_assigned_id=f"entity2_{i}", entity=entity2) + + for delivery_point in DeliveryPoint.objects.all(): + ConsentFactory( + delivery_point=delivery_point, created_by=user1, status=Consent.VALIDATED + ) + + awaiting_consents = get_awaiting_consents(user1) + assert awaiting_consents.exists() is False + + +@pytest.mark.django_db +def test_consent_queryset_multy_multi_user_by_entity(): + """Multy User with one entity and delivery points. + + Test that multi-user associated on the same entity and delivery points have correct + number of consents awaiting approval. + """ + user1 = UserFactory() + user2 = UserFactory() + entity1 = EntityFactory(users=(user1, user2), name="entity1") + + for i in range(1, 4): + DeliveryPointFactory(provider_assigned_id=f"entity1_{i}", entity=entity1) + + for delivery_point in DeliveryPoint.objects.all(): + ConsentFactory(delivery_point=delivery_point, created_by=user1) + + awaiting_consents = get_awaiting_consents(user1) + assert awaiting_consents.count() == 3 # noqa: PLR2004 + for consent in awaiting_consents: + assert consent.status == Consent.AWAITING + assert consent.delivery_point.entity.name == "entity1" + + awaiting_consents = get_awaiting_consents(user2) + assert awaiting_consents.count() == 3 # noqa: PLR2004 + for consent in awaiting_consents: + assert consent.status == Consent.AWAITING + assert consent.delivery_point.entity.name == "entity1" + + +@pytest.mark.django_db +def test_consent_queryset_multy_standard_user_without_consent(): + """Multy User with one entity and delivery points but without consents to validated. + + Test if multi-user associated on the same entity and delivery points but with + no consent to validate has no consent pending approval. + """ + user1 = UserFactory() + user2 = UserFactory() + entity1 = EntityFactory(users=(user1, user2), name="entity1") + + for i in range(1, 4): + DeliveryPointFactory(provider_assigned_id=f"entity1_{i}", entity=entity1) + + for delivery_point in DeliveryPoint.objects.all(): + ConsentFactory( + delivery_point=delivery_point, created_by=user1, status=Consent.VALIDATED + ) + awaiting_consents = get_awaiting_consents(user1) + assert awaiting_consents.exists() is False + + awaiting_consents = get_awaiting_consents(user2) + assert awaiting_consents.exists() is False + + +@pytest.mark.django_db +def test_consent_queryset_user_proxy_for(): + """Verify the queryset of consents for a proxy user. + + This test ensures that the consents awaiting for a user who is a proxy for multiple + entities are correctly identified. + It creates multiple users, entities, and delivery points, associating them + accordingly. + It then verifies that the consents created for these delivery points are fetched + correctly for a proxy user. + + 1. Create users and entities, with one entity being a proxy for others. + 2. Generate delivery points for each entity and associate consents with them. + 3. Assert the total count of delivery points and consents. + 4. Fetch awaiting consents for a proxy user and verify their count and 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" + ) + + 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) + + for delivery_point in DeliveryPoint.objects.all(): + ConsentFactory(delivery_point=delivery_point, created_by=user1) + + assert DeliveryPoint.objects.all().count() == 9 # noqa: PLR2004 + + awaiting_consents = get_awaiting_consents(user3) + + assert awaiting_consents.count() == 9 # noqa: PLR2004 + entities_name = [] + for consent in awaiting_consents: + assert consent.status == Consent.AWAITING + entities_name.append(consent.delivery_point.entity.name) + + assert len(set(entities_name)) == 3 # noqa: PLR2004 + assert set(entities_name) == {"entity1", "entity2", "entity3"} + + +@pytest.mark.django_db +def test_consent_queryset_user_proxy_for_without_consent(): + """Ensure a proxy user without consents does not retrieve any 'awaiting consents'. + + Consents are generated for each delivery point with 'user1' as the creator. + The main assertion verifies that there are no 'awaiting consents' for 'user3', + the user associated with the proxy entity. + """ + 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" + ) + + 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) + + for delivery_point in DeliveryPoint.objects.all(): + ConsentFactory( + delivery_point=delivery_point, created_by=user1, status=Consent.VALIDATED + ) + + assert DeliveryPoint.objects.all().count() == 9 # noqa: PLR2004 + + awaiting_consents = get_awaiting_consents(user3) + + assert awaiting_consents.count() == 0 + + +@pytest.mark.django_db +def test_consent_queryset_user_with_proxy(): + """Verify the queryset of consents for user with proxy on his entity. + + This test ensures that the consents awaiting for a user who has a proxy can + only access to his entity. + """ + 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" + ) + + 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) + + for delivery_point in DeliveryPoint.objects.all(): + ConsentFactory(delivery_point=delivery_point, created_by=user1) + + assert DeliveryPoint.objects.all().count() == 9 # noqa: PLR2004 + + awaiting_consents = get_awaiting_consents(user1) + + assert awaiting_consents.count() == 3 # noqa: PLR2004 + entities_name = [] + for consent in awaiting_consents: + assert consent.status == Consent.AWAITING + entities_name.append(consent.delivery_point.entity.name) + + assert len(set(entities_name)) == 1 + assert set(entities_name) != {"entity2", "entity3"} + assert set(entities_name) == {"entity1"} diff --git a/src/dashboard/apps/consent/urls.py b/src/dashboard/apps/consent/urls.py index d12e65f8..6f6a0616 100644 --- a/src/dashboard/apps/consent/urls.py +++ b/src/dashboard/apps/consent/urls.py @@ -2,11 +2,12 @@ from django.urls import path -from .views import IndexView, ManageView +from .views import IndexView, consent_form_view app_name = "consent" urlpatterns = [ path("", IndexView.as_view(), name="index"), - path("manage/", ManageView.as_view(), name="manage"), + path("manage/", consent_form_view, name="manage"), + path("manage/", consent_form_view, name="manage"), ] diff --git a/src/dashboard/apps/consent/views.py b/src/dashboard/apps/consent/views.py index 057599ca..09226665 100644 --- a/src/dashboard/apps/consent/views.py +++ b/src/dashboard/apps/consent/views.py @@ -1,9 +1,19 @@ """Dashboard consent app views.""" -from django.urls import reverse +from django.shortcuts import get_object_or_404, render +from django.urls import reverse_lazy as reverse from django.utils.translation import gettext_lazy as _ from django.views.generic import TemplateView +from apps.core.models import Entity + +from .consent_management import consent_awaiting, consent_validate +from .querysets import ( + count_awaiting_consents_by_entity, + count_validated_consents_by_entity, + get_awaiting_consents, +) + class IndexView(TemplateView): """Index view of the consent app.""" @@ -13,24 +23,46 @@ class IndexView(TemplateView): def get_context_data(self, **kwargs): """Add custom context to the view.""" context = super().get_context_data(**kwargs) + context["awaiting"] = count_awaiting_consents_by_entity(self.request.user) + context["validated"] = count_validated_consents_by_entity(self.request.user) + context["breadcrumb_data"] = { "current": _("Consent"), } return context -class ManageView(TemplateView): - """Consents management view.""" +def consent_form_view(request, slug=None): + """Manage consent forms. - template_name = "consent/consent-management.html" + This function performs the following actions: + - Retrieves the entity associated with the given slug, if provided. + - Fetches consents awaiting validation for the current user and the specified + entity. + - If a POST request is received, updates the consent status to either VALIDATED + or AWAITING based on user selections and existing data. + """ + template_name = "consent/manage.html" - def get_context_data(self, **kwargs): - """Add custom context to the view.""" - context = super().get_context_data(**kwargs) - context["breadcrumb_data"] = { - "links": [ - {"url": reverse("consent:index"), "title": _("Consent")}, - ], - "current": _("Manage Consents"), - } - return context + entity = None + if slug: + entity = get_object_or_404(Entity, slug=slug) + consents = get_awaiting_consents(request.user, entity) + + if request.POST: + selected_ids = request.POST.getlist("status") + consent_validate(selected_ids) + consent_awaiting(consents, selected_ids) + + breadcrumb_data = { + "links": [ + {"url": reverse("consent:index"), "title": _("Consent")}, + ], + "current": _("Manage Consents"), + } + + return render( + request=request, + template_name=template_name, + context={"consents": consents, "breadcrumb_data": breadcrumb_data}, + ) diff --git a/src/dashboard/apps/core/admin.py b/src/dashboard/apps/core/admin.py index 333db61b..cb96768d 100644 --- a/src/dashboard/apps/core/admin.py +++ b/src/dashboard/apps/core/admin.py @@ -1,6 +1,7 @@ """Dashboard core admin.""" from django.contrib import admin +from django.utils.translation import gettext_lazy as _ from .models import DeliveryPoint, Entity @@ -9,11 +10,25 @@ class EntityAdmin(admin.ModelAdmin): """Entity admin.""" - pass + list_display = ["name", "get_users_name", "get_proxies_for"] + filter_horizontal = ( + "users", + "proxy_for", + ) + + @admin.display(description=_("Users")) + def get_users_name(self, obj): + """Returns a comma-separated string of usernames for the given object.""" + return ", ".join(user.username for user in obj.users.all()) + + @admin.display(description=_("Proxies")) + def get_proxies_for(self, obj): + """Returns a comma-separated string of `proxy_for.name` for the given object.""" + return ", ".join(p.name for p in obj.proxy_for.all()) @admin.register(DeliveryPoint) class DeliveryPointAdmin(admin.ModelAdmin): """Delivery point admin.""" - pass + list_display = ["provider_assigned_id", "entity", "is_active"] diff --git a/src/dashboard/apps/core/migrations/0002_alter_deliverypoint_entity.py b/src/dashboard/apps/core/migrations/0002_alter_deliverypoint_entity.py new file mode 100644 index 00000000..0c38b747 --- /dev/null +++ b/src/dashboard/apps/core/migrations/0002_alter_deliverypoint_entity.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.3 on 2024-11-29 10:42 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("qcd_core", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="deliverypoint", + name="entity", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="delivery_points", + to="qcd_core.entity", + verbose_name="entity", + ), + ), + ] diff --git a/src/dashboard/apps/core/models.py b/src/dashboard/apps/core/models.py index 0605a098..e968e09e 100644 --- a/src/dashboard/apps/core/models.py +++ b/src/dashboard/apps/core/models.py @@ -48,7 +48,9 @@ class Entity(DashboardBase): slug = AutoSlugField(_("slug"), populate_from="name", unique=True) name = models.CharField(_("name"), max_length=64, unique=True) users = models.ManyToManyField(User, verbose_name=_("users")) - proxy_for = models.ManyToManyField("self", verbose_name=_("proxy for"), blank=True) + proxy_for = models.ManyToManyField( + "self", verbose_name=_("proxy for"), blank=True, symmetrical=False + ) class Meta: # noqa: D106 verbose_name = "entity" @@ -71,7 +73,10 @@ class DeliveryPoint(DashboardBase): provider_assigned_id = models.CharField(_("provider assigned id"), max_length=64) entity = models.ForeignKey( - Entity, on_delete=models.CASCADE, verbose_name=_("entity") + Entity, + on_delete=models.CASCADE, + related_name="delivery_points", + verbose_name=_("entity"), ) is_active = models.BooleanField(_("is active"), default=True) diff --git a/src/dashboard/dashboard/settings.py b/src/dashboard/dashboard/settings.py index 710a5794..dd4a0f54 100644 --- a/src/dashboard/dashboard/settings.py +++ b/src/dashboard/dashboard/settings.py @@ -74,7 +74,7 @@ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [ - "templates", + BASE_DIR / "templates", ], "APP_DIRS": True, "OPTIONS": { diff --git a/src/dashboard/templates/base.html b/src/dashboard/templates/base.html index 8f26b426..5928b359 100644 --- a/src/dashboard/templates/base.html +++ b/src/dashboard/templates/base.html @@ -11,6 +11,7 @@ {% block extra_js %} + {% block extra_dashboard_js %}{% endblock extra_dashboard_js %} {% endblock extra_js %} {# djlint:off #}