diff --git a/mesads/api/views.py b/mesads/api/views.py index e2b6b5a..54cfc78 100644 --- a/mesads/api/views.py +++ b/mesads/api/views.py @@ -60,7 +60,6 @@ def get(self, request): geojson = shp.__geo_interface__ for feature in geojson["features"]: insee_code = feature["properties"]["code_insee"] - print(insee_code) feature["properties"]["ads_count"] = stats.get(insee_code, {}).get( "ads_count", 0 ) diff --git a/mesads/app/urls.py b/mesads/app/urls.py index e25bbd3..b2fbf7e 100644 --- a/mesads/app/urls.py +++ b/mesads/app/urls.py @@ -28,9 +28,19 @@ ), path( "registre_ads/admin_gestion", - ads_manager_administrator_required(views.ADSManagerAdminView.as_view()), + ads_manager_administrator_required(views.ADSManagerAdminIndexView.as_view()), name="app.ads-manager-admin.index", ), + path( + "registre_ads/admin_gestion/", + ads_manager_administrator_required(views.ADSManagerAdminDetailsView.as_view()), + name="app.ads-manager-admin.details", + ), + path( + "registre_ads/admin_gestion//changements", + ads_manager_administrator_required(views.ADSManagerAdminUpdatesView.as_view()), + name="app.ads-manager-admin.updates", + ), path( "registre_ads/gestion", login_required(views.ADSManagerRequestView.as_view()), diff --git a/mesads/app/views/__init__.py b/mesads/app/views/__init__.py index cb6174c..9bd3ad7 100644 --- a/mesads/app/views/__init__.py +++ b/mesads/app/views/__init__.py @@ -13,10 +13,15 @@ ads_manager_decree_view, ADSManagerAutocompleteView, ) -from .ads_manager_admin import PrefectureExportView, ADSManagerExportView # noqa: F401 +from .ads_manager_admin import ( # noqa: F401 + PrefectureExportView, + ADSManagerExportView, + ADSManagerAdminIndexView, + ADSManagerAdminDetailsView, + ADSManagerAdminUpdatesView, +) from .ads_manager_request import ( # noqa: F401 ADSManagerRequestView, - ADSManagerAdminView, ) from .dashboards import DashboardsView, DashboardsDetailView # noqa: F401 from .public import ( # noqa: F401 @@ -34,6 +39,9 @@ class ADSRegisterView(RedirectView): def get_redirect_url(self, *args, **kwargs): if self.request.user.is_staff: return reverse("app.dashboards.list") - if len(self.request.user.adsmanageradministrator_set.all()): - return reverse("app.ads-manager-admin.index") + ads_manager_administrators = self.request.user.adsmanageradministrator_set.all() + if len(ads_manager_administrators): + return reverse( + "app.ads-manager-admin.index", + ) return reverse("app.ads-manager.index") diff --git a/mesads/app/views/ads_manager_admin.py b/mesads/app/views/ads_manager_admin.py index ace36f5..1518498 100644 --- a/mesads/app/views/ads_manager_admin.py +++ b/mesads/app/views/ads_manager_admin.py @@ -1,14 +1,140 @@ -from django.shortcuts import get_object_or_404 +from django.conf import settings +from django.core.exceptions import SuspiciousOperation +from django.core.mail import send_mail +from django.db import connection +from django.shortcuts import get_object_or_404, redirect +from django.template.loader import render_to_string +from django.urls import reverse from django.utils.text import slugify -from django.views.generic import View +from django.views.generic import RedirectView, View, TemplateView + +from reversion.views import RevisionMixin from ..models import ( + ADS, + ADSManagerAdministrator, + ADSManagerRequest, ADSManager, ) from .export import ADSExporter +class ADSManagerAdminIndexView(RedirectView): + def get_redirect_url(self, *args, **kwargs): + administrators = ADSManagerAdministrator.objects.filter( + users__in=[self.request.user] + ) + if len(administrators): + return reverse( + "app.ads-manager-admin.details", + kwargs={"prefecture_id": administrators[0].prefecture.id}, + ) + return reverse("app.ads-manager.index") + + +class ADSManagerAdminDetailsView(RevisionMixin, TemplateView): + """This view is used by ADSManagerAdministrators to validate + ADSManagerRequests and list changes made by ADSManagers.""" + + template_name = "pages/ads_register/ads_manager_admin.html" + + def get_context_data(self, **kwargs): + """Populate context with the list of ADSManagerRequest current user can accept.""" + ctx = super().get_context_data(**kwargs) + + query = ( + ADSManagerRequest.objects.select_related( + "ads_manager", + "ads_manager__administrator", + "user", + ) + .prefetch_related( + "ads_manager__content_type", + "ads_manager__content_object", + ) + .filter(ads_manager__administrator=self.kwargs["ads_manager_administrator"]) + ) + + if self.request.GET.get("sort") == "name": + ctx["sort"] = "name" + ctx["ads_manager_requests"] = query.order_by( + "ads_manager__administrator", + "ads_manager__commune__libelle", + "ads_manager__epci__name", + "ads_manager__prefecture__libelle", + ) + else: + ctx["ads_manager_requests"] = query.order_by( + "ads_manager__administrator", + "-created_at", + ) + return ctx + + def post(self, request, **kwargs): + request_id = request.POST.get("request_id") + action = request.POST.get("action") + + if action not in ("accept", "deny"): + raise SuspiciousOperation("Invalid action") + + ads_manager_request = get_object_or_404(ADSManagerRequest, id=request_id) + + # Make sure current user can accept this request + get_object_or_404( + ADSManagerAdministrator, + users__in=[request.user], + adsmanager=ads_manager_request.ads_manager, + ) + + if action == "accept": + ads_manager_request.accepted = True + else: + ads_manager_request.accepted = False + ads_manager_request.save() + + # Send notification to user + email_subject = render_to_string( + "pages/email_ads_manager_request_result_subject.txt", + { + "ads_manager_request": ads_manager_request, + }, + request=request, + ).strip() + email_content = render_to_string( + "pages/email_ads_manager_request_result_content.txt", + { + "request": request, + "ads_manager_request": ads_manager_request, + }, + request=request, + ) + email_content_html = render_to_string( + "pages/email_ads_manager_request_result_content.mjml", + { + "request": request, + "ads_manager_request": ads_manager_request, + }, + request=request, + ) + send_mail( + email_subject, + email_content, + settings.MESADS_CONTACT_EMAIL, + [ads_manager_request.user.email], + fail_silently=True, + html_message=email_content_html, + ) + return redirect( + reverse( + "app.ads-manager-admin.details", + kwargs={ + "prefecture_id": ads_manager_request.ads_manager.administrator.prefecture.id + }, + ) + ) + + class ADSManagerExportView(View, ADSExporter): def get(self, request, manager_id): self.ads_manager = get_object_or_404(ADSManager, id=manager_id) @@ -73,3 +199,71 @@ def add_sheets(self, workbook): ), ) sheet.autofit() + + +class ADSManagerAdminUpdatesView(TemplateView): + template_name = "pages/ads_register/ads_manager_admin_updates.html" + + def get_updates(self, cursor): + # You might be wondering why we didn't implement pagination and why we + # limit to 100 results. + # Long story short, it's because we are using a raw query and pagination + # needs to be handled manually, and I've spent way too much time on this + # already. + # Alternatively we could use the django ORM instead of a raw query, but + # good luck with that. + cursor.execute( + """ + SELECT + ads.id AS id, + adsmanager.id, + CASE + WHEN COUNT(revision.id) = 0 THEN NULL + ELSE COALESCE(JSON_AGG(JSON_BUILD_OBJECT( + 'user_id', revision.user_id, + 'user_email', "user".email, + 'modification_date', revision.date_created + ) ORDER BY revision.date_created DESC), '[]'::json) + END as updates + FROM app_ads AS ads + LEFT JOIN app_adsmanager AS adsmanager + ON adsmanager.id = ads.ads_manager_id + LEFT JOIN app_adsmanageradministrator AS adsmanageradministrator + ON adsmanager.administrator_id = adsmanageradministrator.id + LEFT JOIN reversion_version AS version + ON (version.serialized_data::json -> 0 ->> 'model') = 'app.ads' + AND (version.serialized_data::json -> 0 ->> 'pk')::bigint = ads.id + LEFT JOIN reversion_revision AS revision + ON version.revision_id = revision.id + LEFT JOIN users_user AS "user" + ON "user".id = revision.user_id + WHERE + adsmanageradministrator.id = %s + GROUP BY ads.id, adsmanager.id + ORDER BY ads.last_update DESC + LIMIT 100 + """, + (self.kwargs["ads_manager_administrator"].id,), + ) + updates = cursor.fetchall() + + # Load objects + ads_objects = ADS.objects.filter(id__in=[row[0] for row in updates]) + ads_dict = {obj.id: obj for obj in ads_objects} + + ads_managers = ADSManager.objects.filter(id__in=[row[1] for row in updates]) + ads_managers_dict = {obj.id: obj for obj in ads_managers} + return [ + { + "ads": ads_dict[update[0]], + "ads_manager": ads_managers_dict[update[1]], + "history_entries": update[2], + } + for update in updates + ] + + def get_context_data(self, *args, **kwargs): + ctx = super().get_context_data(**kwargs) + with connection.cursor() as cursor: + ctx["updates"] = self.get_updates(cursor) + return ctx diff --git a/mesads/app/views/ads_manager_request.py b/mesads/app/views/ads_manager_request.py index 4850ba1..ac7d6d8 100644 --- a/mesads/app/views/ads_manager_request.py +++ b/mesads/app/views/ads_manager_request.py @@ -1,17 +1,12 @@ from django.conf import settings from django.contrib import messages -from django.core.exceptions import SuspiciousOperation from django.core.mail import send_mail from django.db.models import Count from django.db import transaction -from django.shortcuts import get_object_or_404, redirect from django.template.loader import render_to_string -from django.urls import reverse, reverse_lazy -from django.views.generic import TemplateView +from django.urls import reverse_lazy from django.views.generic.edit import FormView -from reversion.views import RevisionMixin - from ..forms import ( ADSManagerForm, ) @@ -21,96 +16,6 @@ ) -class ADSManagerAdminView(RevisionMixin, TemplateView): - """This view is used by ADSManagerAdministrators to validate ADSManagerRequests.""" - - template_name = "pages/ads_register/ads_manager_admin.html" - - def get_context_data(self, **kwargs): - """Populate context with the list of ADSManagerRequest current user can accept.""" - ctx = super().get_context_data(**kwargs) - query = ( - ADSManagerRequest.objects.select_related( - "ads_manager__administrator", - "ads_manager__administrator__prefecture", - "ads_manager__content_type", - "user", - ) - .prefetch_related("ads_manager__content_object") - .filter(ads_manager__administrator__users__in=[self.request.user]) - ) - if self.request.GET.get("sort") == "name": - ctx["sort"] = "name" - ctx["ads_manager_requests"] = query.order_by( - "ads_manager__administrator", - "ads_manager__commune__libelle", - "ads_manager__epci__name", - "ads_manager__prefecture__libelle", - ) - else: - ctx["ads_manager_requests"] = query.order_by( - "ads_manager__administrator", - "-created_at", - ) - return ctx - - def post(self, request): - request_id = request.POST.get("request_id") - action = request.POST.get("action") - - if action not in ("accept", "deny"): - raise SuspiciousOperation("Invalid action") - - ads_manager_request = get_object_or_404(ADSManagerRequest, id=request_id) - - # Make sure current user can accept this request - get_object_or_404( - ADSManagerAdministrator, - users__in=[request.user], - adsmanager=ads_manager_request.ads_manager, - ) - - if action == "accept": - ads_manager_request.accepted = True - else: - ads_manager_request.accepted = False - ads_manager_request.save() - - # Send notification to user - email_subject = render_to_string( - "pages/email_ads_manager_request_result_subject.txt", - { - "ads_manager_request": ads_manager_request, - }, - request=request, - ).strip() - email_content = render_to_string( - "pages/email_ads_manager_request_result_content.txt", - { - "request": request, - "ads_manager_request": ads_manager_request, - }, - request=request, - ) - email_content_html = render_to_string( - "pages/email_ads_manager_request_result_content.mjml", - { - "request": request, - "ads_manager_request": ads_manager_request, - }, - request=request, - ) - send_mail( - email_subject, - email_content, - settings.MESADS_CONTACT_EMAIL, - [ads_manager_request.user.email], - fail_silently=True, - html_message=email_content_html, - ) - return redirect(reverse("app.ads-manager-admin.index")) - - class ADSManagerRequestView(FormView): template_name = "pages/ads_register/ads_manager_request.html" form_class = ADSManagerForm diff --git a/mesads/app/views/test_ads_manager_admin.py b/mesads/app/views/test_ads_manager_admin.py index ac1a5cf..453ce37 100644 --- a/mesads/app/views/test_ads_manager_admin.py +++ b/mesads/app/views/test_ads_manager_admin.py @@ -1,11 +1,98 @@ from datetime import datetime -from ..models import ( - ADS, -) +from django.core import mail + +from ..models import ADS, ADSManager, ADSManagerRequest from ..unittest import ClientTestCase +class TestADSManagerAdminView(ClientTestCase): + def setUp(self): + super().setUp() + self.ads_manager_request = ADSManagerRequest.objects.create( + user=self.create_user().obj, + ads_manager=self.ads_manager_city35, + accepted=None, + ) + + def test_permissions(self): + for client_name, client, expected_status in ( + ("admin", self.admin_client, 200), + ("anonymous", self.anonymous_client, 302), + ("auth", self.auth_client, 404), + ("ads_manager 35", self.ads_manager_city35_client, 404), + ("ads_manager_admin 35", self.ads_manager_administrator_35_client, 200), + ): + with self.subTest(client_name=client_name, expected_status=expected_status): + resp = client.get( + f"/registre_ads/admin_gestion/{self.ads_manager_administrator_35.prefecture_id}" + ) + self.assertEqual(resp.status_code, expected_status) + + def test_invalid_action(self): + resp = self.ads_manager_administrator_35_client.post( + f"/registre_ads/admin_gestion/{self.ads_manager_administrator_35.prefecture_id}", + {"action": "xxx", "request_id": 1}, + ) + self.assertEqual(resp.status_code, 400) + + def test_invalid_request_id(self): + resp = self.ads_manager_administrator_35_client.post( + f"/registre_ads/admin_gestion/{self.ads_manager_administrator_35.prefecture_id}", + {"action": "accept", "request_id": 12342}, + ) + self.assertEqual(resp.status_code, 404) + + def test_accept(self): + self.assertEqual(len(mail.outbox), 0) + + resp = self.ads_manager_administrator_35_client.post( + f"/registre_ads/admin_gestion/{self.ads_manager_administrator_35.prefecture_id}", + {"action": "accept", "request_id": self.ads_manager_request.id}, + ) + self.assertEqual(resp.status_code, 302) + self.assertEqual( + resp.url, + f"/registre_ads/admin_gestion/{self.ads_manager_administrator_35.prefecture_id}", + ) + self.ads_manager_request.refresh_from_db() + self.assertTrue(self.ads_manager_request.accepted) + self.assertEqual(len(mail.outbox), 1) + + def test_deny(self): + self.assertEqual(len(mail.outbox), 0) + + resp = self.ads_manager_administrator_35_client.post( + f"/registre_ads/admin_gestion/{self.ads_manager_administrator_35.prefecture_id}", + {"action": "deny", "request_id": self.ads_manager_request.id}, + ) + self.assertEqual(resp.status_code, 302) + self.assertEqual( + resp.url, + f"/registre_ads/admin_gestion/{self.ads_manager_administrator_35.prefecture_id}", + ) + self.ads_manager_request.refresh_from_db() + self.assertFalse(self.ads_manager_request.accepted) + self.assertEqual(len(mail.outbox), 1) + + def test_sort(self): + for ads_manager in ADSManager.objects.all(): + ADSManagerRequest.objects.create( + user=self.create_user().obj, + ads_manager=ads_manager, + accepted=None, + ) + resp = self.ads_manager_administrator_35_client.get( + f"/registre_ads/admin_gestion/{self.ads_manager_administrator_35.prefecture_id}", + ) + self.assertEqual(resp.status_code, 200) + + resp = self.ads_manager_administrator_35_client.get( + f"/registre_ads/admin_gestion/{self.ads_manager_administrator_35.prefecture_id}?sort=name", + ) + self.assertEqual(resp.status_code, 200) + + class TestExportPrefecture(ClientTestCase): def test_permissions(self): for client_name, client, expected_status in ( diff --git a/mesads/app/views/test_ads_manager_request.py b/mesads/app/views/test_ads_manager_request.py index 789949c..a0e216f 100644 --- a/mesads/app/views/test_ads_manager_request.py +++ b/mesads/app/views/test_ads_manager_request.py @@ -4,90 +4,12 @@ from mesads.fradm.models import EPCI, Prefecture from ..models import ( - ADSManager, ADSManagerRequest, Notification, ) from ..unittest import ClientTestCase -class TestADSManagerAdminView(ClientTestCase): - def setUp(self): - super().setUp() - self.ads_manager_request = ADSManagerRequest.objects.create( - user=self.create_user().obj, - ads_manager=self.ads_manager_city35, - accepted=None, - ) - - def test_permissions(self): - for client_name, client, expected_status in ( - ("admin", self.admin_client, 200), - ("anonymous", self.anonymous_client, 302), - ("auth", self.auth_client, 404), - ("ads_manager 35", self.ads_manager_city35_client, 404), - ("ads_manager_admin 35", self.ads_manager_administrator_35_client, 200), - ): - with self.subTest(client_name=client_name, expected_status=expected_status): - resp = client.get("/registre_ads/admin_gestion") - self.assertEqual(resp.status_code, expected_status) - - def test_invalid_action(self): - resp = self.ads_manager_administrator_35_client.post( - "/registre_ads/admin_gestion", {"action": "xxx", "request_id": 1} - ) - self.assertEqual(resp.status_code, 400) - - def test_invalid_request_id(self): - resp = self.ads_manager_administrator_35_client.post( - "/registre_ads/admin_gestion", {"action": "accept", "request_id": 12342} - ) - self.assertEqual(resp.status_code, 404) - - def test_accept(self): - self.assertEqual(len(mail.outbox), 0) - - resp = self.ads_manager_administrator_35_client.post( - "/registre_ads/admin_gestion", - {"action": "accept", "request_id": self.ads_manager_request.id}, - ) - self.assertEqual(resp.status_code, 302) - self.assertEqual(resp.url, "/registre_ads/admin_gestion") - self.ads_manager_request.refresh_from_db() - self.assertTrue(self.ads_manager_request.accepted) - self.assertEqual(len(mail.outbox), 1) - - def test_deny(self): - self.assertEqual(len(mail.outbox), 0) - - resp = self.ads_manager_administrator_35_client.post( - "/registre_ads/admin_gestion", - {"action": "deny", "request_id": self.ads_manager_request.id}, - ) - self.assertEqual(resp.status_code, 302) - self.assertEqual(resp.url, "/registre_ads/admin_gestion") - self.ads_manager_request.refresh_from_db() - self.assertFalse(self.ads_manager_request.accepted) - self.assertEqual(len(mail.outbox), 1) - - def test_sort(self): - for ads_manager in ADSManager.objects.all(): - ADSManagerRequest.objects.create( - user=self.create_user().obj, - ads_manager=ads_manager, - accepted=None, - ) - resp = self.ads_manager_administrator_35_client.get( - "/registre_ads/admin_gestion", - ) - self.assertEqual(resp.status_code, 200) - - resp = self.ads_manager_administrator_35_client.get( - "/registre_ads/admin_gestion?sort=name", - ) - self.assertEqual(resp.status_code, 200) - - class TestADSManagerRequestView(ClientTestCase): def setUp(self): super().setUp() diff --git a/mesads/html_metadata.yml b/mesads/html_metadata.yml index b3b6ef3..4fffbf0 100644 --- a/mesads/html_metadata.yml +++ b/mesads/html_metadata.yml @@ -107,6 +107,9 @@ urls: description: "Découvrez en détail le déploiement et l'utilisation du registre ADS dans {{ object.prefecture.display_fulltext }}." app.ads-manager-admin.index: + missing: true # Redirect view + + app.ads-manager-admin.details: title: "Gestion et contrôle des accès des instructeurs ADS" description: "Gérez les demandes d'accès au registre ADS en acceptant ou refusant les instructeurs de votre département." diff --git a/mesads/templates/django/pages/email_ads_manager_request_administrator_content.mjml b/mesads/templates/django/pages/email_ads_manager_request_administrator_content.mjml index bd1575d..1a0b50a 100644 --- a/mesads/templates/django/pages/email_ads_manager_request_administrator_content.mjml +++ b/mesads/templates/django/pages/email_ads_manager_request_administrator_content.mjml @@ -16,7 +16,7 @@

- + Aller sur Mes ADS pour répondre à la demande diff --git a/mesads/templates/django/pages/email_ads_manager_request_administrator_content.txt b/mesads/templates/django/pages/email_ads_manager_request_administrator_content.txt index fcfba28..d8e00e6 100644 --- a/mesads/templates/django/pages/email_ads_manager_request_administrator_content.txt +++ b/mesads/templates/django/pages/email_ads_manager_request_administrator_content.txt @@ -5,7 +5,7 @@ L'utilisateur {{ user.email }} souhaite obtenir la permission de gérer les auto En tant qu'administrateur, vous avez le pouvoir d'accepter ou de refuser d'accéder à cette demande. -Rendez-vous sur MesADS sur {{ request.scheme }}://{{ request.get_host }}{% url 'app.ads-manager-admin.index' %} pour répondre à la demande. +Rendez-vous sur MesADS sur {{ request.scheme }}://{{ request.get_host }}{% url 'app.ads-manager-admin.details' prefecture_id=ads_manager.administrator.prefecture_id %} pour répondre à la demande. Attention ! Avant d'accepter une demande, assurez-vous que le demandeur travaille bien pour l'administration renseignée. N'hésitez pas à lui envoyer un email ou un appel téléphonique pour demander confirmation. {% endblock %} \ No newline at end of file diff --git a/mesads/templates/webpack/pages/ads_register/ads.html b/mesads/templates/webpack/pages/ads_register/ads.html index b3a5438..deded35 100644 --- a/mesads/templates/webpack/pages/ads_register/ads.html +++ b/mesads/templates/webpack/pages/ads_register/ads.html @@ -3,7 +3,7 @@ {% block main %}