diff --git a/mesads/app/admin/__init__.py b/mesads/app/admin/__init__.py index f4ba4635..3fe53b36 100644 --- a/mesads/app/admin/__init__.py +++ b/mesads/app/admin/__init__.py @@ -6,6 +6,7 @@ from .ads_manager_administrator import * # noqa from .ads_manager_request import * # noqa from .ads_update_file import * # noqa +from .demande_gestion_prefecture import * # noqa from .inscription_liste_attente import * # noqa from .notifications import * # noqa diff --git a/mesads/app/admin/demande_gestion_prefecture.py b/mesads/app/admin/demande_gestion_prefecture.py new file mode 100644 index 00000000..1291fca4 --- /dev/null +++ b/mesads/app/admin/demande_gestion_prefecture.py @@ -0,0 +1,100 @@ +from django.conf import settings +from django.contrib import admin, messages +from django.contrib.staticfiles import finders +from django.core.mail import EmailMultiAlternatives +from django.db.models import F, Q +from django.db.models.functions import Collate +from django.http import HttpResponseRedirect +from django.template.loader import render_to_string +from django.urls import reverse + +from mesads.app.models import DemandeGestionPrefecture + + +@admin.register(DemandeGestionPrefecture) +class DemandeGestionPrefectureAdmin(admin.ModelAdmin): + list_display = ("user", "administrator", "created_at") + + search_fields = ("user__email",) + + change_form_template = "admin/app/demande_gestion_prefecture/change_form.html" + + def response_change(self, request, obj): + # Gestion du bouton "Valider" + if "_valider" in request.POST: + self.validation_demande(obj, request) + self.message_user(request, "Demande validée.", level=messages.SUCCESS) + + changelist_url = reverse( + f"admin:{obj._meta.app_label}_{obj._meta.model_name}_changelist" + ) + obj.delete() + return HttpResponseRedirect(changelist_url) + + return super().response_change(request, obj) + + def validation_demande(self, obj, request): + obj.administrator.users.add(obj.user) + email_subject = render_to_string( + "demande_gestion_prefecture/email_demande_gestion_prefecture_result_subject.txt", + { + "demande": obj, + }, + request=request, + ).strip() + email_content = render_to_string( + "demande_gestion_prefecture/email_demande_gestion_prefecture_result_content.txt", + { + "request": request, + "demande": obj, + "email_contact": settings.MESADS_CONTACT_EMAIL, + }, + request=request, + ) + email_content_html = render_to_string( + "demande_gestion_prefecture/email_demande_gestion_prefecture_result_content.mjml", + { + "request": request, + "demande": obj, + "email_contact": settings.MESADS_CONTACT_EMAIL, + }, + request=request, + ) + + email = EmailMultiAlternatives( + subject=email_subject, + body=email_content, + from_email=settings.MESADS_CONTACT_EMAIL, + to=[obj.user.email], + ) + email.attach_alternative(email_content_html, "text/html") + + file_path = finders.find("Guide d'utilisation.pdf") + + with open(file_path, "rb") as f: + email.attach( + "Guide d'utilisation.pdf", + f.read(), + "application/pdf", + ) + + email.send(fail_silently=True) + + def get_search_results(self, request, queryset, search_term): + """The field Users.email uses a non-deterministic collation, which makes + it impossible to perform a LIKE query on it. + + By overriding this method, we can specify the collation to use for the search. + """ + use_distinct = True + queryset = queryset.annotate(collated_email=Collate(F("user__email"), "C")) + queryset = queryset.filter( + Q( + collated_email__icontains=search_term, + ) + | Q(administrator__prefecture__libelle=search_term) + ) + return ( + queryset, + use_distinct, + ) diff --git a/mesads/app/forms.py b/mesads/app/forms.py index 38fea79d..bf52383f 100644 --- a/mesads/app/forms.py +++ b/mesads/app/forms.py @@ -451,3 +451,10 @@ class ListesAttentePubliquesSearchForm(forms.Form): def is_filled(self): return self.cleaned_data.get("departement") or self.cleaned_data.get("commune") + + +class DemandeGestionPrefectureForm(forms.Form): + departement = forms.ModelChoiceField( + queryset=Prefecture.objects.exclude(numero="999"), + label="Département", + ) diff --git a/mesads/app/migrations/0110_demandegestionprefecture.py b/mesads/app/migrations/0110_demandegestionprefecture.py new file mode 100644 index 00000000..0341e17f --- /dev/null +++ b/mesads/app/migrations/0110_demandegestionprefecture.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.10 on 2026-02-18 15:09 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0109_merge_20260112_1826'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='DemandeGestionPrefecture', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('administrator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='demandes_gestion_prefecture', to='app.adsmanageradministrator')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='demandes_gestion_prefecture', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Demande pour devenir gestionnaire de préfecture', + 'verbose_name_plural': 'Demandes pour devenir gestionnaire de préfecture', + 'unique_together': {('user', 'administrator')}, + }, + ), + ] diff --git a/mesads/app/models.py b/mesads/app/models.py index 2b233322..842a1604 100644 --- a/mesads/app/models.py +++ b/mesads/app/models.py @@ -379,6 +379,36 @@ def ordered_adsmanager_set(self): ) +@reversion.register +class DemandeGestionPrefecture(models.Model): + """Requête utilisateur pour devenir gestionnaire d'une préfecture. + Elle doit être accepté par un membre de l'équipe de MesADS. + """ + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + blank=False, + related_name="demandes_gestion_prefecture", + ) + administrator = models.ForeignKey( + ADSManagerAdministrator, + on_delete=models.CASCADE, + blank=False, + related_name="demandes_gestion_prefecture", + ) + + created_at = models.DateTimeField(auto_now_add=True, null=False) + + class Meta: + unique_together = (("user", "administrator"),) + verbose_name = "Demande pour devenir gestionnaire de préfecture" + verbose_name_plural = "Demandes pour devenir gestionnaire de préfecture" + + def __str__(self): + return f"Requete de {self.user} pour être gestionnaire de {self.administrator}" + + def validate_siret(value): if not value: return diff --git a/mesads/app/tests/views/test_ads_manager_admin.py b/mesads/app/tests/views/test_ads_manager_admin.py index f1af63b7..4dede044 100644 --- a/mesads/app/tests/views/test_ads_manager_admin.py +++ b/mesads/app/tests/views/test_ads_manager_admin.py @@ -1,10 +1,16 @@ from datetime import date, datetime from http import HTTPStatus +from django.conf import settings from django.core import mail from django.urls import reverse -from mesads.app.models import ADS, ADSManager, ADSManagerRequest +from mesads.app.models import ( + ADS, + ADSManager, + ADSManagerRequest, + DemandeGestionPrefecture, +) from mesads.fradm.models import Prefecture from mesads.unittest import ClientTestCase from mesads.vehicules_relais.models import Proprietaire, Vehicule @@ -403,3 +409,56 @@ def test_get_context(self): self.ads_manager_administrator_35, ) self.assertEqual(response.context["vehicule"], self.vehicule) + + +class TestDemandeGestionPrefecture(ClientTestCase): + def setUp(self): + super().setUp() + self.client, self.user = self.create_client() + self.administrator = self.ads_manager_administrator_35 + self.prefecture = self.ads_manager_administrator_35.prefecture + + def test_get_page_demande_gestion_prefecture(self): + response = self.client.get( + reverse("app.ads-manager-admin.demande_gestion_prefecture") + ) + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertTemplateUsed( + response, "pages/ads_register/demande_gestion_prefecture.html" + ) + + def test_post_page_demande_gestion_prefecture(self): + assert ( + DemandeGestionPrefecture.objects.filter( + user=self.user, administrator=self.administrator + ).count() + == 0 + ) + response = self.client.post( + reverse("app.ads-manager-admin.demande_gestion_prefecture"), + {"departement": self.prefecture.id}, + ) + self.assertRedirects( + response, + expected_url=reverse( + "app.homepage", + ), + status_code=HTTPStatus.FOUND, + target_status_code=HTTPStatus.OK, + fetch_redirect_response=True, + ) + assert ( + DemandeGestionPrefecture.objects.filter( + user=self.user, administrator=self.administrator + ).count() + == 1 + ) + + self.assertEqual(len(mail.outbox), 1) + + email = mail.outbox[0] + + self.assertEqual( + email.subject, f"Mes ADS : Demande d'accès préfecture de {self.user.email}" + ) + self.assertEqual(email.to, [settings.MESADS_CONTACT_EMAIL]) diff --git a/mesads/app/urls.py b/mesads/app/urls.py index af354428..b6d379ec 100644 --- a/mesads/app/urls.py +++ b/mesads/app/urls.py @@ -50,6 +50,11 @@ name="app.exports.prefecture", # A GARDER ), + path( + "registre_ads/demande_gestion_prefecture/", + login_required(views.DemandeGestionPrefectureView.as_view()), + name="app.ads-manager-admin.demande_gestion_prefecture", + ), ] url_gestionnaire = [ diff --git a/mesads/app/views/__init__.py b/mesads/app/views/__init__.py index 9c77782e..4b12e3f1 100644 --- a/mesads/app/views/__init__.py +++ b/mesads/app/views/__init__.py @@ -17,6 +17,7 @@ ADSManagerAdminRequestsView, ADSManagerAdminUpdatesView, ADSManagerExportView, + DemandeGestionPrefectureView, PrefectureExportView, RepertoireVehiculeRelaisView, VehiculeView, diff --git a/mesads/app/views/ads_manager_admin.py b/mesads/app/views/ads_manager_admin.py index 564a1494..afa33714 100644 --- a/mesads/app/views/ads_manager_admin.py +++ b/mesads/app/views/ads_manager_admin.py @@ -25,10 +25,10 @@ from django.template.loader import render_to_string from django.urls import reverse from django.utils.text import slugify -from django.views.generic import ListView, TemplateView, View +from django.views.generic import FormView, ListView, TemplateView, View from reversion.views import RevisionMixin -from mesads.app.forms import SearchVehiculeForm +from mesads.app.forms import DemandeGestionPrefectureForm, SearchVehiculeForm from mesads.fradm.models import EPCI, Aeroport, Commune, Prefecture from mesads.utils_psql import SplitPart from mesads.vehicules_relais.models import Vehicule @@ -39,6 +39,7 @@ ADSManagerAdministrator, ADSManagerRequest, ADSUpdateLog, + DemandeGestionPrefecture, ) from ..services import ( get_ads_data_for_excel_export, @@ -543,3 +544,62 @@ def get_context_data(self, **kwargs): "ads_manager_administrator" ) return context + + +class DemandeGestionPrefectureView(FormView): + form_class = DemandeGestionPrefectureForm + template_name = "pages/ads_register/demande_gestion_prefecture.html" + + def get_success_url(self): + return reverse("app.homepage") + + def form_valid(self, form): + administrator = ADSManagerAdministrator.objects.filter( + prefecture=form.data.get("departement") + ).first() + + if administrator: + demande, _ = DemandeGestionPrefecture.objects.get_or_create( + user=self.request.user, administrator=administrator + ) + messages.success( + self.request, + "Votre demande a bien été transmise à notre équipe", + ) + self.envoi_email_notification(demande) + # send maiil + + return super().form_valid(form) + + def envoi_email_notification(self, demande): + email_subject = render_to_string( + "demande_gestion_prefecture/email_demande_gestion_prefecture_subject.txt", + { + "demande": demande, + }, + request=self.request, + ).strip() + email_content = render_to_string( + "demande_gestion_prefecture/email_demande_gestion_prefecture_content.txt", + { + "request": self.request, + "demande": demande, + }, + request=self.request, + ) + email_content_html = render_to_string( + "demande_gestion_prefecture/email_demande_gestion_prefecture_content.mjml", + { + "request": self.request, + "demande": demande, + }, + request=self.request, + ) + send_mail( + email_subject, + email_content, + settings.MESADS_CONTACT_EMAIL, + [settings.MESADS_CONTACT_EMAIL], + fail_silently=True, + html_message=email_content_html, + ) diff --git a/mesads/html_metadata.yml b/mesads/html_metadata.yml index c62f6e36..136aa2f8 100644 --- a/mesads/html_metadata.yml +++ b/mesads/html_metadata.yml @@ -118,6 +118,10 @@ urls: 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." + app.ads-manager-admin.demande_gestion_prefecture: + title: "MesADS - Demande de gestion préfecture" + description: "MesADS - Demandez l'accès à l'espace préfecture de votre département." + app.ads-manager-admin.gestionnaires: title: "MesADS - Administrations de votre préfecture" description: "MesADS - Administrations de votre préfecture" diff --git a/mesads/templates/django/admin/app/demande_gestion_prefecture/change_form.html b/mesads/templates/django/admin/app/demande_gestion_prefecture/change_form.html new file mode 100644 index 00000000..4bf57811 --- /dev/null +++ b/mesads/templates/django/admin/app/demande_gestion_prefecture/change_form.html @@ -0,0 +1,20 @@ +{% extends "admin/change_form.html" %} +{% load i18n admin_urls %} + +{% block submit_buttons_bottom %} + {% if change %} +
+ L'utilisateur {{ demande.user.email }} souhaite obtenir la permission de gérer {{ demande.administrator.prefecture.display_fulltext|safe }}. +
++ En tant qu'administrateur, vous avez le pouvoir d'accepter ou de refuser d'accéder à cette demande en cliquant sur le bouton ci-dessous. +
++ 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. +
++ Bonjour, +
++ Nous vous confirmons que votre accès "Préfecture" a été validé. Vous trouverez ci-joint le guide d'utilisation dédié aux comptes administrateurs, dans lequel vous retrouverez la liste exhaustive des fonctionnalités qui vous sont proposées. +
++ Nous restons à votre disposition pour tout renseignement complémentaire, +
++ Bien cordialement, +
++ L'équipe MesADS +
+Demander un accès à l'équipe MesADS.
+
+