diff --git a/mesads/app/views.py b/mesads/app/views.py deleted file mode 100644 index e75a324..0000000 --- a/mesads/app/views.py +++ /dev/null @@ -1,1388 +0,0 @@ -from datetime import date, datetime, timedelta -import collections -import json - -from docxtpl import DocxTemplate - -from django.conf import settings -from django.contrib import messages -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.staticfiles import finders -from django.core.exceptions import SuspiciousOperation -from django.core.mail import send_mail -from django.db import IntegrityError, transaction -from django.db.models import Count, Q, Sum, Value -from django.db.models.functions import Coalesce, Replace, TruncMonth -from django.http import HttpResponse, HttpResponseRedirect -from django.shortcuts import get_object_or_404, redirect, render -from django.template.defaultfilters import date as date_template_filter -from django.template.loader import render_to_string -from django.urls import reverse, reverse_lazy -from django.utils import timezone -from django.utils.text import slugify -from django.views.generic import RedirectView, TemplateView, UpdateView, View -from django.views.generic.detail import DetailView -from django.views.generic.edit import CreateView, DeleteView, FormView, ProcessFormView -from django.views.generic.list import ListView - -import xlsxwriter - -from formtools.wizard.views import CookieWizardView - -from reversion.views import RevisionMixin - -from mesads.fradm.models import EPCI - -from .forms import ( - ADSDecreeForm1, - ADSDecreeForm2, - ADSDecreeForm3, - ADSDecreeForm4, - ADSForm, - ADSLegalFileFormSet, - ADSManagerDecreeFormSet, - ADSManagerEditForm, - ADSManagerForm, - ADSSearchForm, - ADSUserFormSet, -) -from .models import ( - ADS, - ADSLegalFile, - ADSManager, - ADSManagerAdministrator, - ADSManagerRequest, - ADSUser, -) -from .reversion_diff import ModelHistory - - -class HTTP500View(TemplateView): - """The default HTTP/500 handler can't access to context processors and does - not have access to the variable MESADS_CONTACT_EMAIL. - """ - - template_name = "500.html" - - def dispatch(self, request, *args, **kwargs): - """The base class TemplateView only accepts GET requests. By overriding - dispatch, we return the error page for any other method.""" - return super().get(request, *args, **kwargs) - - -class HomepageView(TemplateView): - template_name = "pages/homepage.html" - - -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") - return reverse("app.ads-manager.index") - - -class ADSManagerAdminView(RevisionMixin, TemplateView): - 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 - success_url = reverse_lazy("app.ads-manager.index") - - def get_context_data(self, **kwargs): - """Expose the list of ADSManagerAdministrators for which current user - is configured. - - It is also accessible through user.adsmanageradministrator_set.all, but - we need to prefetch ads_managers__content_object to reduce the number - of SQL queries generated. - """ - ctx = super().get_context_data(**kwargs) - ctx["user_ads_manager_requests"] = ( - ADSManagerRequest.objects.filter(user=self.request.user) - .annotate(ads_count=Count("ads_manager__ads")) - .all() - ) - - ctx["ads_managers_administrators"] = ( - ADSManagerAdministrator.objects.select_related("prefecture") - .filter(users=self.request.user) - .all() - ) - return ctx - - def form_valid(self, form): - _, created = ADSManagerRequest.objects.get_or_create( - user=self.request.user, - ads_manager=form.cleaned_data["ads_manager"], - ) - - # Request already exists - if not created: - messages.warning( - self.request, - self.get_message_for_existing_request(form.cleaned_data["ads_manager"]), - ) - # Send notifications to administrators. - else: - messages.success( - self.request, - self.get_message_for_new_request(form.cleaned_data["ads_manager"]), - ) - email_subject = render_to_string( - "pages/email_ads_manager_request_administrator_subject.txt", - { - "user": self.request.user, - }, - request=self.request, - ).strip() - email_content = render_to_string( - "pages/email_ads_manager_request_administrator_content.txt", - { - "request": self.request, - "ads_manager": form.cleaned_data["ads_manager"], - "user": self.request.user, - }, - request=self.request, - ) - email_content_html = render_to_string( - "pages/email_ads_manager_request_administrator_content.mjml", - { - "request": self.request, - "ads_manager": form.cleaned_data["ads_manager"], - "user": self.request.user, - }, - request=self.request, - ) - - if form.cleaned_data["ads_manager"].administrator: - for administrator_user in form.cleaned_data[ - "ads_manager" - ].administrator.users.all(): - send_mail( - email_subject, - email_content, - settings.MESADS_CONTACT_EMAIL, - [administrator_user], - fail_silently=True, - html_message=email_content_html, - ) - - return super().form_valid(form) - - def get_message_for_existing_request(self, ads_manager): - if not ads_manager.administrator: - return """ - Vous avez déjà effectué une demande pour gérer les ADS de %(administration)s, et notre équipe va y répondre dans les plus brefs délais.

- - Si vous n'avez eu aucun retour depuis plusieurs jours, n'hésitez pas à contacter notre équipe par email à %(email)s ou via notre module de tchat. - """ % { - "email": settings.MESADS_CONTACT_EMAIL, - "administration": ads_manager.content_object.display_fulltext(), - } - return """ - Vous avez déjà effectué une demande pour gérer les ADS de %(administration)s. Cette demande a été envoyée à %(prefecture)s qui devrait y répondre rapidement.

- - Si vous n'avez eu aucun retour depuis plusieurs jours, n'hésitez pas à nous signaler le problème par email à %(email)s ou via notre module de tchat. -

- Nous pourrons alors valider votre demande manuellement. - """ % { - "administration": ads_manager.content_object.display_fulltext(), - "prefecture": ads_manager.administrator.prefecture.display_fulltext(), - "email": settings.MESADS_CONTACT_EMAIL, - } - - def get_message_for_new_request(self, ads_manager): - # Request for EPCI or prefectures - if not ads_manager.administrator: - return """ - Votre demande vient d’être envoyée à notre équipe. Vous recevrez une confirmation de validation de votre - accès par mail.

- - En cas de difficulté ou si vous n’obtenez pas de validation de votre demande vous pouvez contacter par email à - %(email)s ou via notre module de tchat.

- - Vous pouvez également demander un accès pour la gestion des ADS d’une autre collectivité. - """ % { - "email": settings.MESADS_CONTACT_EMAIL - } - - return """ - Votre demande vient d’être envoyée à %(prefecture)s. Vous recevrez une confirmation de validation de votre - accès par mail.

- - En cas de difficulté ou si vous n’obtenez pas de validation de votre demande vous pouvez - contacter par email à %(email)s ou via notre module de tchat.

- - Vous pouvez également demander un accès pour la gestion des ADS d’une autre collectivité. - """ % { - "prefecture": ads_manager.administrator.prefecture.display_fulltext(), - "email": settings.MESADS_CONTACT_EMAIL, - } - - -class ADSManagerView(ListView, ProcessFormView): - template_name = "pages/ads_register/ads_manager.html" - model = ADS - paginate_by = 50 - - def get(self, request, *args, **kwargs): - self.search_form = ADSSearchForm(request.GET) - return ListView.get(self, request, *args, **kwargs) - - def get_ads_manager(self): - return ADSManager.objects.get(id=self.kwargs["manager_id"]) - - def get_form(self): - if self.request.method == "POST": - return ADSManagerEditForm( - instance=self.get_ads_manager(), data=self.request.POST - ) - return ADSManagerEditForm(instance=self.get_ads_manager()) - - def form_valid(self, form): - form.save() - return redirect("app.ads-manager.detail", manager_id=self.kwargs["manager_id"]) - - def form_invalid(self, form): - return self.get(self.request, *self.args, **self.kwargs) - - def get_queryset(self): - qs = super().get_queryset() - qs = qs.filter(ads_manager__id=self.kwargs["manager_id"]) - - if self.search_form.is_valid(): - if self.search_form.cleaned_data["accepted_cpam"] is not None: - qs = qs.filter( - accepted_cpam=self.search_form.cleaned_data["accepted_cpam"] - ) - - q = self.search_form.cleaned_data["q"] - if q: - qs = qs.annotate( - clean_immatriculation_plate=Replace( - "immatriculation_plate", Value("-"), Value("") - ) - ) - - qs = qs.filter( - Q(owner_siret__icontains=q) - | Q(adsuser__name__icontains=q) - | Q(adsuser__siret__icontains=q) - | Q(owner_name__icontains=q) - | Q(clean_immatriculation_plate__icontains=q) - | Q(epci_commune__libelle__icontains=q) - | Q(number__icontains=q) - ) - - # Add ordering on the number. CAST is necessary in the case the ADS number is not an integer. - qs_ordered = qs.extra( - select={ - "ads_number_as_int": "CAST(substring(number FROM '^[0-9]+') AS NUMERIC)" - } - ) - - # First, order by number if it is an integer, then by string. - return qs_ordered.annotate(c=Count("id")).order_by( - "ads_number_as_int", "number" - ) - - def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) - ctx["search_form"] = self.search_form - - # search_defined is a boolean, set to True of any of the search form - # parameter is defined. - ctx["search_defined"] = any( - (v is not None and v != "" for v in self.search_form.cleaned_data.values()) - ) - - ctx["edit_form"] = self.get_form() - ctx["ads_manager"] = ctx["edit_form"].instance - return ctx - - -class ADSView(RevisionMixin, UpdateView): - template_name = "pages/ads_register/ads.html" - form_class = ADSForm - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - - # If manager_id is the primary key of an EPCI, initialize ADSForm with - # the parameter "epci" which is required to setup autocompletion for the - # field ADS.epci_commune. This field is not displayed if the manager is - # a Prefecture or a Commune. - ads_manager = get_object_or_404(ADSManager, id=self.kwargs["manager_id"]) - if ads_manager.content_type.model_class() is EPCI: - kwargs["epci"] = ads_manager.content_object - - return kwargs - - def get_success_url(self): - return reverse( - "app.ads.detail", - kwargs={ - "manager_id": self.kwargs["manager_id"], - "ads_id": self.kwargs["ads_id"], - }, - ) - - def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) - ctx["ads_manager"] = ADSManager.objects.get(id=self.kwargs["manager_id"]) - ctx["ads_users_formset"] = self.ads_users_formset - ctx["ads_legal_files_formset"] = self.ads_legal_files_formset - return ctx - - def get_object(self, queryset=None): - ads = get_object_or_404(ADS, id=self.kwargs["ads_id"]) - - if self.request.POST and self.request.POST.get( - ADSUserFormSet().management_form["TOTAL_FORMS"].html_name - ): - self.ads_users_formset = ADSUserFormSet(self.request.POST, instance=ads) - else: - self.ads_users_formset = ADSUserFormSet(instance=ads) - # Always display at least a form - if not ads.adsuser_set.count(): - self.ads_users_formset.extra = 1 - - if self.request.POST and self.request.POST.get( - ADSLegalFileFormSet().management_form["TOTAL_FORMS"].html_name - ): - self.ads_legal_files_formset = ADSLegalFileFormSet( - self.request.POST, self.request.FILES, instance=ads - ) - else: - self.ads_legal_files_formset = ADSLegalFileFormSet(instance=ads) - - return ads - - def form_invalid(self, form): - messages.error( - self.request, - "Le formulaire contient des erreurs. Veuillez les corriger avant de soumettre à nouveau.", - ) - return super().form_invalid(form) - - @transaction.atomic - def form_valid(self, form): - html_name_ads_users_formset = self.ads_users_formset.management_form[ - "TOTAL_FORMS" - ].html_name - if ( - self.request.POST.get(html_name_ads_users_formset) is not None - and not self.ads_users_formset.is_valid() - ): - return self.form_invalid(form) - - html_name_ads_legal_files_formset = ( - self.ads_legal_files_formset.management_form["TOTAL_FORMS"].html_name - ) - if ( - self.request.POST.get(html_name_ads_legal_files_formset) is not None - and not self.ads_legal_files_formset.is_valid() - ): - return self.form_invalid(form) - - self.object = form.save(check=False) - self.ads_users_formset.instance = self.object - self.ads_legal_files_formset.instance = self.object - - if not self.request.POST.get(html_name_ads_users_formset): - ADSUser.objects.filter(ads=self.object).delete() - else: - try: - with transaction.atomic(): - self.ads_users_formset.save() - except IntegrityError: - errmsg = [ - c - for c in ADSUser._meta.constraints - if c.name == "only_one_titulaire_exploitant" - ][0].violation_error_message - self.ads_users_formset.non_form_errors().append(errmsg) - resp = self.form_invalid(form) - # Revert the transaction: we don't want to save the ADS if we can't save the users. - transaction.set_rollback(True) - return resp - - if not self.request.POST.get(html_name_ads_legal_files_formset): - ADSLegalFile.objects.filter(ads=self.object).delete() - else: - self.ads_legal_files_formset.instance = self.object - self.ads_legal_files_formset.save() - - self.object.run_checks() - - messages.success(self.request, "Les modifications ont été enregistrées.") - return HttpResponseRedirect(self.get_success_url()) - - -def ads_manager_decree_view(request, manager_id): - """Decree limiting the number of ADS for an ADSManager.""" - ads_manager = get_object_or_404(ADSManager, id=manager_id) - - if request.method == "POST": - formset = ADSManagerDecreeFormSet( - request.POST, request.FILES, instance=ads_manager - ) - if formset.is_valid(): - formset.save() - messages.success(request, "Les modifications ont été enregistrées.") - return redirect("app.ads-manager.decree.detail", manager_id=manager_id) - else: - formset = ADSManagerDecreeFormSet(instance=ads_manager) - - return render( - request, - "pages/ads_register/ads_manager_decree.html", - context={ - "ads_manager": ads_manager, - "formset": formset, - }, - ) - - -class ADSDeleteView(DeleteView): - template_name = "pages/ads_register/ads_confirm_delete.html" - model = ADS - pk_url_kwarg = "ads_id" - - def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) - ctx["ads_manager"] = ADSManager.objects.get(id=self.kwargs["manager_id"]) - return ctx - - def get_success_url(self): - return reverse( - "app.ads-manager.detail", - kwargs={ - "manager_id": self.kwargs["manager_id"], - }, - ) - - -class ADSCreateView(ADSView, CreateView): - def dispatch(self, request, manager_id): - """If the ADSManager has the flag no_ads_declared to True, it is - impossible to create ADS for it.""" - get_object_or_404(ADSManager, id=manager_id, no_ads_declared=False) - - html_name_ads_users_formset = ( - ADSUserFormSet().management_form["TOTAL_FORMS"].html_name - ) - if self.request.POST.get(html_name_ads_users_formset): - self.ads_users_formset = ADSUserFormSet(self.request.POST) - else: - self.ads_users_formset = ADSUserFormSet() - self.ads_users_formset.extra = 1 - - html_name_ads_legal_files_formset = ( - ADSLegalFileFormSet().management_form["TOTAL_FORMS"].html_name - ) - if self.request.POST.get(html_name_ads_legal_files_formset): - self.ads_legal_files_formset = ADSLegalFileFormSet( - self.request.POST, self.request.FILES - ) - else: - self.ads_legal_files_formset = ADSLegalFileFormSet() - return super().dispatch(request, manager_id) - - def get_object(self, queryset=None): - return None - - def get_success_url(self): - return reverse( - "app.ads.detail", - kwargs={"manager_id": self.kwargs["manager_id"], "ads_id": self.object.id}, - ) - - def form_valid(self, form): - ads_manager = ADSManager.objects.get(id=self.kwargs["manager_id"]) - form.instance.ads_manager = ads_manager - - # CreateView doesn't call validate_constraints(). The try/catch below - # attemps to save the object. If IntegrityError is returned from - # database, we return a custom error message for "number". - try: - with transaction.atomic(): - return super().form_valid(form) - except IntegrityError: - form.add_error("number", ADS.UNIQUE_ERROR_MSG) - return super().form_invalid(form) - - -class ADSExporter: - """Generic class to export a list of ADS in an Excel file.""" - - def get_filename(self): - raise NotImplementedError - - def get_queryset(self): - return ( - ADS.objects.select_related( - "ads_manager__administrator__prefecture", - ) - .prefetch_related( - "ads_manager__content_object", - ) - .annotate( - ads_users_status=ArrayAgg("adsuser__status"), - ads_users_names=ArrayAgg("adsuser__name"), - ads_users_sirets=ArrayAgg("adsuser__siret"), - ads_users_licenses=ArrayAgg("adsuser__license_number"), - ) - ) - - def display_bool(self, value): - if value is None: - return "" - return "oui" if value else "non" - - def display_date(self, value): - if not value: - return "" - return value.strftime("%d/%m/%Y") - - def generate(self): - response = HttpResponse( - content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - headers={ - "Content-Disposition": f'attachment; filename="{self.get_filename()}"' - }, - ) - workbook = xlsxwriter.Workbook(response) - self.add_sheets(workbook) - workbook.close() - return response - - def add_sheets(self, workbook): - """Override this method to add more sheets to the workbook.""" - self.ads_list_sheet(workbook) - - def ads_list_sheet(self, workbook): - bold_format = workbook.add_format({"bold": True}) - sheet = workbook.add_worksheet("ADS enregistrées") - headers = ( - "Type d'administration", - "Administration", - "Numéro de l'ADS", - "ADS actuellement exploitée ?", - "Date de création de l'ADS", - "Date du dernier renouvellement de l'ADS", - "Date d'attribution de l'ADS au titulaire actuel", - "Véhicule conventionné CPAM ?", - "Plaque d'immatriculation du véhicule", - "Le véhicule est-il un véhicule électrique/hybride ?", - "Véhicule compatible PMR ?", - "Titulaire de l'ADS", - "SIRET du titulaire de l'ADS", - "Téléphone fixe du titulaire de l'ADS", - "Téléphone mobile du titulaire de l'ADS", - "Email du titulaire de l'ADS", - ) - # If one of the ADS in the list has, let's say, 4 drivers, driver_headers - # will be appended 4 times to headers. - driver_headers = ( - "Statut du %s conducteur", - "Nom du %s conducteur", - "SIRET du %s conducteur", - "Numéro de la carte professionnelle du %s conducteur", - ) - # Counts the maximum number of drivers in the list of ADS.. - max_drivers = 0 - - # Applying bold format to headers - sheet.set_row(0, None, bold_format) - - for idx, ads in enumerate(self.get_queryset()): - # Append driver headers to headers if the current ADS has more drivers - # than the previous ones. - while max_drivers < len(ads.ads_users_status): - for h in driver_headers: - headers += ( - h % ("1er" if max_drivers == 0 else "%se" % (max_drivers + 1)), - ) - max_drivers += 1 - - info = ( - ads.ads_manager.content_object.type_name(), - ads.ads_manager.content_object.text(), - ads.number, - self.display_bool(ads.ads_in_use), - self.display_date(ads.ads_creation_date), - self.display_date(ads.ads_renew_date), - self.display_date(ads.attribution_date), - self.display_bool(ads.accepted_cpam), - ads.immatriculation_plate, - self.display_bool(ads.eco_vehicle), - self.display_bool(ads.vehicle_compatible_pmr), - ads.owner_name, - ads.owner_siret, - ads.owner_phone, - ads.owner_mobile, - ads.owner_email, - ) - for nth, status in enumerate(ads.ads_users_status): - # ads_users_status, ads_users_names, ads_users_sirets and - # ads_users_licenses have the same length. - info += ( - dict(ADSUser.status.field.choices).get( - ads.ads_users_status[nth], "" - ), - ads.ads_users_names[nth], - ads.ads_users_sirets[nth], - ads.ads_users_licenses[nth], - ) - sheet.write_row(idx + 1, 0, info) - - # Write headers, now that we know the maximum number of drivers. - sheet.write_row(0, 0, headers) - sheet.autofit() - - -class ADSManagerExportView(View, ADSExporter): - def get(self, request, manager_id): - self.ads_manager = get_object_or_404(ADSManager, id=manager_id) - return self.generate() - - def get_filename(self): - administration = self.ads_manager.content_object.display_text() - return slugify(f"ADS {administration}") + ".xlsx" - - def get_queryset(self): - qs = super().get_queryset() - return qs.filter(ads_manager=self.ads_manager) - - -class PrefectureExportView(View, ADSExporter): - def get(self, request, ads_manager_administrator): - self.ads_manager_administrator = ads_manager_administrator - return self.generate() - - def get_filename(self): - return f"ADS_prefecture_{self.ads_manager_administrator.prefecture.numero}.xlsx" - - def get_queryset(self): - qs = super().get_queryset() - return qs.filter(ads_manager__administrator=self.ads_manager_administrator) - - def add_sheets(self, workbook): - super().add_sheets(workbook) - sheet = workbook.add_worksheet("Gestionnaires ADS") - sheet.write_row( - 0, - 0, - ( - "Nom de l'administration", - "Nombre d'ADS", - "Statut de la gestion des ADS", - ), - ) - # Applying bold format to headers - bold_format = workbook.add_format({"bold": True}) - sheet.set_row(0, None, bold_format) - - for idx, ads_manager in enumerate( - self.ads_manager_administrator.adsmanager_set.all() - ): - status = "" - if ads_manager.no_ads_declared: - status = "L'administration a déclaré ne gérer aucune ADS" - elif ads_manager.epci_delegate: - status = ( - "La gestion des ADS est déléguée à %s" - % ads_manager.epci_delegate.display_fulltext() - ) - - sheet.write_row( - idx + 1, - 0, - ( - ads_manager.content_object.display_text(), - ads_manager.ads_set.count() or "", - status, - ), - ) - sheet.autofit() - - -class DashboardsView(TemplateView): - template_name = "pages/ads_register/dashboards_list.html" - - def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) - ctx["stats"], ctx["stats_total"] = self.get_stats() - return ctx - - def get_stats(self): - """This function returns a tuple of two values: - - * stats: a list of dictionaries containing the following keys: - - obj: the ADSManagerAdministrator instance - - ads: a dictionary where keys represent the period (now, 3 months - ago, 6 months ago, 12 months ago), and values are the number of ADS - for this ADSManagerAdministrator - - users: a dictionary where keys represent the period (now, 3 months - ago, 6 months ago, 12 months ago), and values are the number of - accounts who can create ADS for this ADSManagerAdministrator - - >>> [ - ... obj: - ... 'ads': { - ... 'now': - ... '3_months': - ... '6_months': - ... '12_months': - ... }, - ... 'users': { - ... 'now': - ... '3_months': - ... '6_months': - ... '12_months': - ... } - ... ] - - * stats_total: a dictionary containing the keys 'ads' and 'users', and - the values are dictionaries where keys represent the period, and - values are the total number of ADS and users. - - >>> { - ... 'ads': { - ... 'now': , - ... '3_months': , - ... '6_months': , - ... '12_months': , - ... }, - ... 'users': { ... } - ... } - """ - now = timezone.now() - - stats = collections.defaultdict(lambda: {"obj": None, "ads": {}, "users": {}}) - - stats_total = { - "ads": {}, - "users": {}, - } - - ads_query_now = ( - ADSManagerAdministrator.objects.select_related("prefecture") - .annotate(ads_count=Count("adsmanager__ads")) - .filter(ads_count__gt=0) - ) - - # All ADSManagerAdministrator, with the count of ADS with at least one of the contact fields filled. - ads_with_info_query_now = ( - ADSManagerAdministrator.objects.select_related("prefecture") - .annotate( - ads_count=Count( - "adsmanager__ads", - filter=~Q(adsmanager__ads__owner_email="") - | ~Q(adsmanager__ads__owner_mobile="") - | ~Q(adsmanager__ads__owner_phone=""), - ) - ) - .filter(ads_count__gt=0) - ) - - ads_query_3_months = ( - ADSManagerAdministrator.objects.select_related("prefecture") - .filter(adsmanager__ads__creation_date__lte=now - timedelta(weeks=4 * 3)) - .annotate(ads_count=Count("adsmanager__ads")) - ) - - ads_query_6_months = ( - ADSManagerAdministrator.objects.select_related("prefecture") - .filter(adsmanager__ads__creation_date__lte=now - timedelta(weeks=4 * 6)) - .annotate(ads_count=Count("adsmanager__ads")) - ) - - ads_query_12_months = ( - ADSManagerAdministrator.objects.select_related("prefecture") - .filter(adsmanager__ads__creation_date__lte=now - timedelta(weeks=4 * 12)) - .annotate(ads_count=Count("adsmanager__ads")) - ) - - for label, query in ( - ("now", ads_query_now), - ("with_info_now", ads_with_info_query_now), - ("3_months", ads_query_3_months), - ("6_months", ads_query_6_months), - ("12_months", ads_query_12_months), - ): - for row in query: - stats[row.prefecture.id]["obj"] = row - stats[row.prefecture.id]["ads"][label] = row.ads_count - - stats_total["ads"][label] = query.aggregate( - total=Coalesce(Sum("ads_count"), 0) - )["total"] - - users_query_now = ( - ADSManagerAdministrator.objects.select_related("prefecture") - .filter(adsmanager__adsmanagerrequest__accepted=True) - .annotate(users_count=Count("id")) - ) - - users_query_3_months = ( - ADSManagerAdministrator.objects.select_related("prefecture") - .filter( - adsmanager__adsmanagerrequest__accepted=True, - adsmanager__adsmanagerrequest__created_at__lte=now - - timedelta(weeks=4 * 3), - ) - .annotate(users_count=Count("id")) - ) - - users_query_6_months = ( - ADSManagerAdministrator.objects.select_related("prefecture") - .filter( - adsmanager__adsmanagerrequest__accepted=True, - adsmanager__adsmanagerrequest__created_at__lte=now - - timedelta(weeks=4 * 6), - ) - .annotate(users_count=Count("id")) - ) - - users_query_12_months = ( - ADSManagerAdministrator.objects.select_related("prefecture") - .filter( - adsmanager__adsmanagerrequest__accepted=True, - adsmanager__adsmanagerrequest__created_at__lte=now - - timedelta(weeks=4 * 12), - ) - .annotate(users_count=Count("id")) - ) - - for label, query in ( - ("now", users_query_now), - ("3_months", users_query_3_months), - ("6_months", users_query_6_months), - ("12_months", users_query_12_months), - ): - for row in query.all(): - stats[row.prefecture.id]["obj"] = row - stats[row.prefecture.id]["users"][label] = row.users_count - - stats_total["users"][label] = query.aggregate( - total=Coalesce(Sum("users_count"), 0) - )["total"] - - return ( - # Transform dict to an ordered list - sorted(list(stats.values()), key=lambda stat: stat["obj"].id), - stats_total, - ) - - -class DashboardsDetailView(DetailView): - template_name = "pages/ads_register/dashboards_detail.html" - model = ADSManagerAdministrator - pk_url_kwarg = "ads_manager_administrator_id" - - def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) - ctx["stats"] = self.get_stats() - return ctx - - def get_stats(self): - stats = {} - - stats = collections.defaultdict(lambda: {"obj": None, "ads": {}, "users": {}}) - - now = timezone.now() - - ads_query_now = ( - ADSManager.objects.prefetch_related("content_type", "content_object") - .filter(administrator=self.object) - .annotate(ads_count=Count("ads")) - .filter(ads_count__gt=0) - ) - - # All ADSManager, with the count of ADS with at least one of the contact fields filled. - ads_with_info_query_now = ( - ADSManager.objects.prefetch_related("content_type", "content_object") - .filter(administrator=self.object) - .annotate( - ads_count=Count( - "ads", - filter=~Q(ads__owner_email="") - | ~Q(ads__owner_mobile="") - | ~Q(ads__owner_phone=""), - ) - ) - .filter(ads_count__gt=0) - ) - - ads_query_3_months = ( - ADSManager.objects.prefetch_related("content_type", "content_object") - .filter( - administrator=self.object, - ads__creation_date__lte=now - timedelta(weeks=4 * 3), - ) - .annotate(ads_count=Count("ads")) - ) - - ads_query_6_months = ( - ADSManager.objects.prefetch_related("content_type", "content_object") - .filter( - administrator=self.object, - ads__creation_date__lte=now - timedelta(weeks=4 * 6), - ) - .annotate(ads_count=Count("ads")) - ) - - ads_query_12_months = ( - ADSManager.objects.prefetch_related("content_type", "content_object") - .filter( - administrator=self.object, - ads__creation_date__lte=now - timedelta(weeks=4 * 12), - ) - .annotate(ads_count=Count("ads")) - ) - - for label, query in ( - ("now", ads_query_now), - ("with_info_now", ads_with_info_query_now), - ("3_months", ads_query_3_months), - ("6_months", ads_query_6_months), - ("12_months", ads_query_12_months), - ): - for row in query: - stats[row.id]["obj"] = row - stats[row.id]["ads"][label] = row.ads_count - - users_query_now = ( - ADSManager.objects.prefetch_related("content_type", "content_object") - .filter(administrator=self.object, adsmanagerrequest__accepted=True) - .annotate(users_count=Count("id")) - ) - - users_query_3_months = ( - ADSManager.objects.prefetch_related("content_type", "content_object") - .filter( - administrator=self.object, - adsmanagerrequest__accepted=True, - adsmanagerrequest__created_at__lte=now - timedelta(weeks=4 * 3), - ) - .annotate(users_count=Count("id")) - ) - - users_query_6_months = ( - ADSManager.objects.prefetch_related("content_type", "content_object") - .filter( - administrator=self.object, - adsmanagerrequest__accepted=True, - adsmanagerrequest__created_at__lte=now - timedelta(weeks=4 * 6), - ) - .annotate(users_count=Count("id")) - ) - - users_query_12_months = ( - ADSManager.objects.prefetch_related("content_type", "content_object") - .filter( - administrator=self.object, - adsmanagerrequest__accepted=True, - adsmanagerrequest__created_at__lte=now - timedelta(weeks=4 * 12), - ) - .annotate(users_count=Count("id")) - ) - - for label, query in ( - ("now", users_query_now), - ("3_months", users_query_3_months), - ("6_months", users_query_6_months), - ("12_months", users_query_12_months), - ): - for row in query.all(): - stats[row.id]["obj"] = row - stats[row.id]["users"][label] = row.users_count - - return sorted(list(stats.values()), key=lambda stat: stat["obj"].id) - - -class FAQView(TemplateView): - template_name = "pages/faq.html" - - -class CustomCookieWizardView(CookieWizardView): - def get_prefix(self, request, *args, **kwargs): - """By default, WizardView uses the class name as a prefix. If the user - opens several tabs at the same time, the storage is shared and weird - behavior can happen. - - For example: - - * tab 1: from the page to create a decree for an ADS, go to second step - * tab 2: from the page to create a decree for another ADS, go to second step - * tab 2: refresh the page to go back to first step - * tab 1: go to third step - - If the prefix is shared, an error will be raised because the form data - have been deleted when the user refreshed the page in tab 2. - - We append the URL parameters to the prefix to avoid this issue most of - the time. It is not perfect, but it is better than nothing. If the two - tabs edit the same object, the prefix will be the same and the issue - will still happen. - """ - prefix = super().get_prefix(request, *args, **kwargs) - suffix = "_".join(str(kwargs[key]) for key in sorted(kwargs.keys())) - return f"{prefix}_{suffix}" - - def render_next_step(self, form, **kwargs): - """The base class implementation of render_next_step has a bug, with the following scenario. - - Imagine a wizard with 3 steps. - - 1. The user is at step 1, selects a field from a list, then goes to step 2 - 2. The step 2 renders a select field with choices computed from the data - of step 1. The user selects a value, and goes to step 3. - 3. The user goes back to step 2, then goes back to step 1. - 4. Finally, the user selects another value than the first time, and goes - to step 2. - - Since the choices of the select field are computed from the data of step - 1, the choice previously select and stored refers to an invalid choice. - - To fix this issue, we delete the stored data of the next step before - going to it. - """ - if self.steps.next in self.storage.data[self.storage.step_data_key]: - del self.storage.data[self.storage.step_data_key][self.steps.next] - return super().render_next_step(form, **kwargs) - - def render_done(self, form, **kwargs): - """The custom method render_done is called at the final step of the - wizard. The base class resets the storage, which prevents to edit the - form to regenerate the decree. We override the method to prevent the - storage from being reset.""" - storage_reset = self.storage.reset - self.storage.reset = lambda: None - resp = super().render_done(form, **kwargs) - self.storage.reset = storage_reset - return resp - - -class ADSHistoryView(DetailView): - template_name = "pages/ads_register/ads_history.html" - model = ADS - pk_url_kwarg = "ads_id" - - def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) - ctx["history"] = ModelHistory( - self.object, - ignore_fields=[ - ADS._meta.get_field("ads_manager"), - ADS._meta.get_field("creation_date"), - ADS._meta.get_field("last_update"), - ADSLegalFile._meta.get_field("ads"), - ADSLegalFile._meta.get_field("creation_date"), - ADSUser._meta.get_field("ads"), - ], - ) - return ctx - - -class ADSDecreeView(CustomCookieWizardView): - """Decree for ADS creation.""" - - template_name = "pages/ads_register/ads_decree.html" - form_list = ( - ADSDecreeForm1, - ADSDecreeForm2, - ADSDecreeForm3, - ADSDecreeForm4, - ) - - def get_form_kwargs(self, step=None): - """Instantiate ADSDecreeForm1 with the value of the previous form, to - set the correct choices of the select field.""" - ret = super().get_form_kwargs(step=step) - if step in ("1", "2"): - return {"is_old_ads": self.get_cleaned_data_for_step("0").get("is_old_ads")} - return ret - - def get_form_initial(self, step): - """Set fields defaults.""" - ret = super().get_form_initial(step) - ads = self.get_ads() - - if step == "0": - ret.update( - { - "is_old_ads": ads.ads_creation_date - and ads.ads_creation_date <= date(2014, 10, 1), - } - ) - elif step == "2": - ads_user = ads.adsuser_set.first() - - now = datetime.now() - try: - today_in_5_years = now.replace(year=now.year + 5) - except ValueError: # 29th February - today_in_5_years = now + timedelta(days=365 * 5) - - ret.update( - { - "decree_creation_date": now.strftime("%Y-%m-%d"), - "decree_commune": ads.ads_manager.content_object.libelle, - "ads_owner": ads.owner_name, - # By default, we only display the first ADSUser. If there - # are more, user can edit the .docx generated manually. - "tenant_ads_user": ads_user.name if ads_user else "", - # New ADS have a validity of 5 years - "ads_end_date": today_in_5_years.strftime("%Y-%m-%d"), - "ads_number": ads.number, - "immatriculation_plate": ads.immatriculation_plate, - } - ) - return ret - - def get_ads(self): - return get_object_or_404( - ADS, id=self.kwargs["ads_id"], ads_manager_id=self.kwargs["manager_id"] - ) - - def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) - ctx["ads"] = self.get_ads() - return ctx - - def done(self, form_list, **kwargs): - path = finders.find("template-arrete-municipal.docx") - decree = DocxTemplate(path) - - cleaned_data = self.get_all_cleaned_data() - - # DocxTemplate uses jinja2 to render the template. To render dates, we - # could use {{ date.strftime(...)}} but the month would be in English. - # Use the django date template filter to use correct format. - cleaned_data.update( - { - k + "_str": date_template_filter(v, "d F Y") - for k, v in cleaned_data.items() - if isinstance(v, date) - } - ) - - # Prefix the commune name with "la commune d'" or "la commune de " - decree_commune = cleaned_data["decree_commune"] - cleaned_data["decree_commune_fulltext"] = ( - "d'%s" % decree_commune - if decree_commune[:1] in ("aeiouy") - else "de %s" % decree_commune - ) - - decree.render(cleaned_data) - - response = HttpResponse( - content_type="application/vnd.openxmlformats", - headers={"Content-Disposition": 'attachment; filename="decret.docx"'}, - ) - - decree.save(response) - return response - - -class StatsView(TemplateView): - template_name = "pages/stats.html" - - def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) - - ctx["ads_count"] = ADS.objects.count() - - ads_count_by_month = ( - ADS.objects.annotate(month=TruncMonth("creation_date")) - .values("month") - .annotate(count=Count("id")) - .order_by("month") - ) - ctx["ads_count_by_month"] = json.dumps( - dict( - ((row["month"].isoformat(), row["count"]) for row in ads_count_by_month) - ) - ) - - ctx["ads_manager_requests_count"] = ADSManagerRequest.objects.filter( - accepted=True - ).count() - - ads_manager_requests_by_month = ( - ADSManagerRequest.objects.filter(accepted=True) - .annotate(month=TruncMonth("created_at")) - .values("month") - .annotate(count=Count("id")) - .order_by("month") - ) - ctx["ads_manager_requests_by_month"] = json.dumps( - dict( - ( - (row["month"].isoformat(), row["count"]) - for row in ads_manager_requests_by_month - ) - ) - ) - - ctx["ads_managers_count"] = ( - ADSManager.objects.annotate(ads_count=Count("ads")) - .filter(ads_count__gt=0) - .count() - ) - - return ctx - - -class ReglementationView(TemplateView): - template_name = "pages/reglementation.html" - entries = [ - { - "title": "Principes généraux", - "articles": [ - { - "title": "Le rôle des collectivités", - "template": "pages/reglementation/principes_generaux/role_collectivites.html", - }, - { - "title": "Qu'est-ce qu'une ADS ?", - "template": "pages/reglementation/principes_generaux/qu_est_ce_qu_une_ads.html", - }, - { - "title": "Qui délivre les ADS ?", - "template": "pages/reglementation/principes_generaux/qui_delivre_ads.html", - }, - ], - }, - { - "title": "Délivrance d'une ADS", - "articles": [ - { - "menu_title": "Arrêté délimitant le nombre d'ADS", - "title": "Étape 1 : l'arrêté délimitant le nombre d'ADS", - "template": "pages/reglementation/delivrance_ads/arrete_delimitant_ads.html", - }, - { - "menu_title": "Attribution de l'ADS", - "title": "Étape 2 : l'attribution de l'ADS", - "template": "pages/reglementation/delivrance_ads/attribution_ads.html", - }, - { - "menu_title": "L'arrêté municipal", - "title": "Étape 3 : la notification de l'arrêté", - "template": "pages/reglementation/delivrance_ads/notification_arrete.html", - }, - { - "menu_title": "Retrait d'une ADS", - "title": "Étape 4 : le retrait d'une ADS", - "template": "pages/reglementation/delivrance_ads/retrait_ads.html", - }, - ], - }, - { - "title": "Registre des taxis relais", - "articles": [ - { - "title": "Qu'est-ce qu'un taxi relais ?", - "template": "pages/reglementation/relais/definition.html", - }, - ], - }, - ] - - def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) - ctx["entries"] = self.entries - return ctx diff --git a/mesads/app/views/__init__.py b/mesads/app/views/__init__.py new file mode 100644 index 0000000..f1e08ca --- /dev/null +++ b/mesads/app/views/__init__.py @@ -0,0 +1,35 @@ +from django.urls import reverse +from django.views.generic import RedirectView + +from .ads import ( # noqa: F401 + ADSDecreeView, + ADSHistoryView, + ADSCreateView, + ADSDeleteView, + ADSView, +) +from .ads_manager import ADSManagerView, ads_manager_decree_view # noqa: F401 +from .ads_manager_admin import PrefectureExportView, ADSManagerExportView # noqa: F401 +from .ads_manager_request import ( # noqa: F401 + ADSManagerRequestView, + ADSManagerAdminView, +) +from .dashboards import DashboardsView, DashboardsDetailView # noqa: F401 +from .public import ( # noqa: F401 + FAQView, + StatsView, + ReglementationView, + HTTP500View, + HomepageView, +) + + +class ADSRegisterView(RedirectView): + """Redirect to the appropriate dashboard depending on the user's role.""" + + 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") + return reverse("app.ads-manager.index") diff --git a/mesads/app/views/ads.py b/mesads/app/views/ads.py new file mode 100644 index 0000000..904e6e3 --- /dev/null +++ b/mesads/app/views/ads.py @@ -0,0 +1,406 @@ +from datetime import date, datetime, timedelta + +from docxtpl import DocxTemplate + +from django.contrib import messages +from django.contrib.staticfiles import finders +from django.db import IntegrityError, transaction +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.template.defaultfilters import date as date_template_filter +from django.urls import reverse +from django.views.generic import UpdateView +from django.views.generic.detail import DetailView +from django.views.generic.edit import CreateView, DeleteView + +from formtools.wizard.views import CookieWizardView + +from reversion.views import RevisionMixin + +from mesads.fradm.models import EPCI + +from ..forms import ( + ADSDecreeForm1, + ADSDecreeForm2, + ADSDecreeForm3, + ADSDecreeForm4, + ADSForm, + ADSLegalFileFormSet, + ADSUserFormSet, +) +from ..models import ( + ADS, + ADSLegalFile, + ADSManager, + ADSUser, +) +from ..reversion_diff import ModelHistory + + +class ADSView(RevisionMixin, UpdateView): + template_name = "pages/ads_register/ads.html" + form_class = ADSForm + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + + # If manager_id is the primary key of an EPCI, initialize ADSForm with + # the parameter "epci" which is required to setup autocompletion for the + # field ADS.epci_commune. This field is not displayed if the manager is + # a Prefecture or a Commune. + ads_manager = get_object_or_404(ADSManager, id=self.kwargs["manager_id"]) + if ads_manager.content_type.model_class() is EPCI: + kwargs["epci"] = ads_manager.content_object + + return kwargs + + def get_success_url(self): + return reverse( + "app.ads.detail", + kwargs={ + "manager_id": self.kwargs["manager_id"], + "ads_id": self.kwargs["ads_id"], + }, + ) + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx["ads_manager"] = ADSManager.objects.get(id=self.kwargs["manager_id"]) + ctx["ads_users_formset"] = self.ads_users_formset + ctx["ads_legal_files_formset"] = self.ads_legal_files_formset + return ctx + + def get_object(self, queryset=None): + ads = get_object_or_404(ADS, id=self.kwargs["ads_id"]) + + if self.request.POST and self.request.POST.get( + ADSUserFormSet().management_form["TOTAL_FORMS"].html_name + ): + self.ads_users_formset = ADSUserFormSet(self.request.POST, instance=ads) + else: + self.ads_users_formset = ADSUserFormSet(instance=ads) + # Always display at least a form + if not ads.adsuser_set.count(): + self.ads_users_formset.extra = 1 + + if self.request.POST and self.request.POST.get( + ADSLegalFileFormSet().management_form["TOTAL_FORMS"].html_name + ): + self.ads_legal_files_formset = ADSLegalFileFormSet( + self.request.POST, self.request.FILES, instance=ads + ) + else: + self.ads_legal_files_formset = ADSLegalFileFormSet(instance=ads) + + return ads + + def form_invalid(self, form): + messages.error( + self.request, + "Le formulaire contient des erreurs. Veuillez les corriger avant de soumettre à nouveau.", + ) + return super().form_invalid(form) + + @transaction.atomic + def form_valid(self, form): + html_name_ads_users_formset = self.ads_users_formset.management_form[ + "TOTAL_FORMS" + ].html_name + if ( + self.request.POST.get(html_name_ads_users_formset) is not None + and not self.ads_users_formset.is_valid() + ): + return self.form_invalid(form) + + html_name_ads_legal_files_formset = ( + self.ads_legal_files_formset.management_form["TOTAL_FORMS"].html_name + ) + if ( + self.request.POST.get(html_name_ads_legal_files_formset) is not None + and not self.ads_legal_files_formset.is_valid() + ): + return self.form_invalid(form) + + self.object = form.save(check=False) + self.ads_users_formset.instance = self.object + self.ads_legal_files_formset.instance = self.object + + if not self.request.POST.get(html_name_ads_users_formset): + ADSUser.objects.filter(ads=self.object).delete() + else: + try: + with transaction.atomic(): + self.ads_users_formset.save() + except IntegrityError: + errmsg = [ + c + for c in ADSUser._meta.constraints + if c.name == "only_one_titulaire_exploitant" + ][0].violation_error_message + self.ads_users_formset.non_form_errors().append(errmsg) + resp = self.form_invalid(form) + # Revert the transaction: we don't want to save the ADS if we can't save the users. + transaction.set_rollback(True) + return resp + + if not self.request.POST.get(html_name_ads_legal_files_formset): + ADSLegalFile.objects.filter(ads=self.object).delete() + else: + self.ads_legal_files_formset.instance = self.object + self.ads_legal_files_formset.save() + + self.object.run_checks() + + messages.success(self.request, "Les modifications ont été enregistrées.") + return HttpResponseRedirect(self.get_success_url()) + + +class ADSDeleteView(DeleteView): + template_name = "pages/ads_register/ads_confirm_delete.html" + model = ADS + pk_url_kwarg = "ads_id" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx["ads_manager"] = ADSManager.objects.get(id=self.kwargs["manager_id"]) + return ctx + + def get_success_url(self): + return reverse( + "app.ads-manager.detail", + kwargs={ + "manager_id": self.kwargs["manager_id"], + }, + ) + + +class ADSCreateView(ADSView, CreateView): + def dispatch(self, request, manager_id): + """If the ADSManager has the flag no_ads_declared to True, it is + impossible to create ADS for it.""" + get_object_or_404(ADSManager, id=manager_id, no_ads_declared=False) + + html_name_ads_users_formset = ( + ADSUserFormSet().management_form["TOTAL_FORMS"].html_name + ) + if self.request.POST.get(html_name_ads_users_formset): + self.ads_users_formset = ADSUserFormSet(self.request.POST) + else: + self.ads_users_formset = ADSUserFormSet() + self.ads_users_formset.extra = 1 + + html_name_ads_legal_files_formset = ( + ADSLegalFileFormSet().management_form["TOTAL_FORMS"].html_name + ) + if self.request.POST.get(html_name_ads_legal_files_formset): + self.ads_legal_files_formset = ADSLegalFileFormSet( + self.request.POST, self.request.FILES + ) + else: + self.ads_legal_files_formset = ADSLegalFileFormSet() + return super().dispatch(request, manager_id) + + def get_object(self, queryset=None): + return None + + def get_success_url(self): + return reverse( + "app.ads.detail", + kwargs={"manager_id": self.kwargs["manager_id"], "ads_id": self.object.id}, + ) + + def form_valid(self, form): + ads_manager = ADSManager.objects.get(id=self.kwargs["manager_id"]) + form.instance.ads_manager = ads_manager + + # CreateView doesn't call validate_constraints(). The try/catch below + # attemps to save the object. If IntegrityError is returned from + # database, we return a custom error message for "number". + try: + with transaction.atomic(): + return super().form_valid(form) + except IntegrityError: + form.add_error("number", ADS.UNIQUE_ERROR_MSG) + return super().form_invalid(form) + + +class CustomCookieWizardView(CookieWizardView): + def get_prefix(self, request, *args, **kwargs): + """By default, WizardView uses the class name as a prefix. If the user + opens several tabs at the same time, the storage is shared and weird + behavior can happen. + + For example: + + * tab 1: from the page to create a decree for an ADS, go to second step + * tab 2: from the page to create a decree for another ADS, go to second step + * tab 2: refresh the page to go back to first step + * tab 1: go to third step + + If the prefix is shared, an error will be raised because the form data + have been deleted when the user refreshed the page in tab 2. + + We append the URL parameters to the prefix to avoid this issue most of + the time. It is not perfect, but it is better than nothing. If the two + tabs edit the same object, the prefix will be the same and the issue + will still happen. + """ + prefix = super().get_prefix(request, *args, **kwargs) + suffix = "_".join(str(kwargs[key]) for key in sorted(kwargs.keys())) + return f"{prefix}_{suffix}" + + def render_next_step(self, form, **kwargs): + """The base class implementation of render_next_step has a bug, with the following scenario. + + Imagine a wizard with 3 steps. + + 1. The user is at step 1, selects a field from a list, then goes to step 2 + 2. The step 2 renders a select field with choices computed from the data + of step 1. The user selects a value, and goes to step 3. + 3. The user goes back to step 2, then goes back to step 1. + 4. Finally, the user selects another value than the first time, and goes + to step 2. + + Since the choices of the select field are computed from the data of step + 1, the choice previously select and stored refers to an invalid choice. + + To fix this issue, we delete the stored data of the next step before + going to it. + """ + if self.steps.next in self.storage.data[self.storage.step_data_key]: + del self.storage.data[self.storage.step_data_key][self.steps.next] + return super().render_next_step(form, **kwargs) + + def render_done(self, form, **kwargs): + """The custom method render_done is called at the final step of the + wizard. The base class resets the storage, which prevents to edit the + form to regenerate the decree. We override the method to prevent the + storage from being reset.""" + storage_reset = self.storage.reset + self.storage.reset = lambda: None + resp = super().render_done(form, **kwargs) + self.storage.reset = storage_reset + return resp + + +class ADSDecreeView(CustomCookieWizardView): + """Decree for ADS creation.""" + + template_name = "pages/ads_register/ads_decree.html" + form_list = ( + ADSDecreeForm1, + ADSDecreeForm2, + ADSDecreeForm3, + ADSDecreeForm4, + ) + + def get_form_kwargs(self, step=None): + """Instantiate ADSDecreeForm1 with the value of the previous form, to + set the correct choices of the select field.""" + ret = super().get_form_kwargs(step=step) + if step in ("1", "2"): + return {"is_old_ads": self.get_cleaned_data_for_step("0").get("is_old_ads")} + return ret + + def get_form_initial(self, step): + """Set fields defaults.""" + ret = super().get_form_initial(step) + ads = self.get_ads() + + if step == "0": + ret.update( + { + "is_old_ads": ads.ads_creation_date + and ads.ads_creation_date <= date(2014, 10, 1), + } + ) + elif step == "2": + ads_user = ads.adsuser_set.first() + + now = datetime.now() + try: + today_in_5_years = now.replace(year=now.year + 5) + except ValueError: # 29th February + today_in_5_years = now + timedelta(days=365 * 5) + + ret.update( + { + "decree_creation_date": now.strftime("%Y-%m-%d"), + "decree_commune": ads.ads_manager.content_object.libelle, + "ads_owner": ads.owner_name, + # By default, we only display the first ADSUser. If there + # are more, user can edit the .docx generated manually. + "tenant_ads_user": ads_user.name if ads_user else "", + # New ADS have a validity of 5 years + "ads_end_date": today_in_5_years.strftime("%Y-%m-%d"), + "ads_number": ads.number, + "immatriculation_plate": ads.immatriculation_plate, + } + ) + return ret + + def get_ads(self): + return get_object_or_404( + ADS, id=self.kwargs["ads_id"], ads_manager_id=self.kwargs["manager_id"] + ) + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx["ads"] = self.get_ads() + return ctx + + def done(self, form_list, **kwargs): + path = finders.find("template-arrete-municipal.docx") + decree = DocxTemplate(path) + + cleaned_data = self.get_all_cleaned_data() + + # DocxTemplate uses jinja2 to render the template. To render dates, we + # could use {{ date.strftime(...)}} but the month would be in English. + # Use the django date template filter to use correct format. + cleaned_data.update( + { + k + "_str": date_template_filter(v, "d F Y") + for k, v in cleaned_data.items() + if isinstance(v, date) + } + ) + + # Prefix the commune name with "la commune d'" or "la commune de " + decree_commune = cleaned_data["decree_commune"] + cleaned_data["decree_commune_fulltext"] = ( + "d'%s" % decree_commune + if decree_commune[:1] in ("aeiouy") + else "de %s" % decree_commune + ) + + decree.render(cleaned_data) + + response = HttpResponse( + content_type="application/vnd.openxmlformats", + headers={"Content-Disposition": 'attachment; filename="decret.docx"'}, + ) + + decree.save(response) + return response + + +class ADSHistoryView(DetailView): + template_name = "pages/ads_register/ads_history.html" + model = ADS + pk_url_kwarg = "ads_id" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx["history"] = ModelHistory( + self.object, + ignore_fields=[ + ADS._meta.get_field("ads_manager"), + ADS._meta.get_field("creation_date"), + ADS._meta.get_field("last_update"), + ADSLegalFile._meta.get_field("ads"), + ADSLegalFile._meta.get_field("creation_date"), + ADSUser._meta.get_field("ads"), + ], + ) + return ctx diff --git a/mesads/app/views/ads_manager.py b/mesads/app/views/ads_manager.py new file mode 100644 index 0000000..eb01fe8 --- /dev/null +++ b/mesads/app/views/ads_manager.py @@ -0,0 +1,122 @@ +from django.contrib import messages +from django.db.models import Count, Q, Value +from django.db.models.functions import Replace +from django.shortcuts import get_object_or_404, redirect, render +from django.views.generic.edit import ProcessFormView +from django.views.generic.list import ListView + +from ..forms import ( + ADSManagerDecreeFormSet, + ADSManagerEditForm, + ADSSearchForm, +) +from ..models import ( + ADS, + ADSManager, +) + + +class ADSManagerView(ListView, ProcessFormView): + template_name = "pages/ads_register/ads_manager.html" + model = ADS + paginate_by = 50 + + def get(self, request, *args, **kwargs): + self.search_form = ADSSearchForm(request.GET) + return ListView.get(self, request, *args, **kwargs) + + def get_ads_manager(self): + return ADSManager.objects.get(id=self.kwargs["manager_id"]) + + def get_form(self): + if self.request.method == "POST": + return ADSManagerEditForm( + instance=self.get_ads_manager(), data=self.request.POST + ) + return ADSManagerEditForm(instance=self.get_ads_manager()) + + def form_valid(self, form): + form.save() + return redirect("app.ads-manager.detail", manager_id=self.kwargs["manager_id"]) + + def form_invalid(self, form): + return self.get(self.request, *self.args, **self.kwargs) + + def get_queryset(self): + qs = super().get_queryset() + qs = qs.filter(ads_manager__id=self.kwargs["manager_id"]) + + if self.search_form.is_valid(): + if self.search_form.cleaned_data["accepted_cpam"] is not None: + qs = qs.filter( + accepted_cpam=self.search_form.cleaned_data["accepted_cpam"] + ) + + q = self.search_form.cleaned_data["q"] + if q: + qs = qs.annotate( + clean_immatriculation_plate=Replace( + "immatriculation_plate", Value("-"), Value("") + ) + ) + + qs = qs.filter( + Q(owner_siret__icontains=q) + | Q(adsuser__name__icontains=q) + | Q(adsuser__siret__icontains=q) + | Q(owner_name__icontains=q) + | Q(clean_immatriculation_plate__icontains=q) + | Q(epci_commune__libelle__icontains=q) + | Q(number__icontains=q) + ) + + # Add ordering on the number. CAST is necessary in the case the ADS number is not an integer. + qs_ordered = qs.extra( + select={ + "ads_number_as_int": "CAST(substring(number FROM '^[0-9]+') AS NUMERIC)" + } + ) + + # First, order by number if it is an integer, then by string. + return qs_ordered.annotate(c=Count("id")).order_by( + "ads_number_as_int", "number" + ) + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx["search_form"] = self.search_form + + # search_defined is a boolean, set to True of any of the search form + # parameter is defined. + ctx["search_defined"] = any( + (v is not None and v != "" for v in self.search_form.cleaned_data.values()) + ) + + ctx["edit_form"] = self.get_form() + ctx["ads_manager"] = ctx["edit_form"].instance + return ctx + + +def ads_manager_decree_view(request, manager_id): + """Decree limiting the number of ADS for an ADSManager.""" + ads_manager = get_object_or_404(ADSManager, id=manager_id) + + if request.method == "POST": + formset = ADSManagerDecreeFormSet( + request.POST, request.FILES, instance=ads_manager + ) + if formset.is_valid(): + formset.save() + messages.success(request, "Les modifications ont été enregistrées.") + return redirect("app.ads-manager.decree.detail", manager_id=manager_id) + else: + formset = ADSManagerDecreeFormSet(instance=ads_manager) + + return render( + request, + "pages/ads_register/ads_manager_decree.html", + context={ + "ads_manager": ads_manager, + "formset": formset, + }, + ) diff --git a/mesads/app/views/ads_manager_admin.py b/mesads/app/views/ads_manager_admin.py new file mode 100644 index 0000000..427cc37 --- /dev/null +++ b/mesads/app/views/ads_manager_admin.py @@ -0,0 +1,75 @@ +from django.shortcuts import get_object_or_404 +from django.utils.text import slugify +from django.views.generic import View + +from ..models import ( + ADSManager, +) + +from .export import ADSExporter + + +class ADSManagerExportView(View, ADSExporter): + def get(self, request, manager_id): + self.ads_manager = get_object_or_404(ADSManager, id=manager_id) + return self.generate() + + def get_filename(self): + administration = self.ads_manager.content_object.display_text() + return slugify(f"ADS {administration}") + ".xlsx" + + def get_queryset(self): + qs = super().get_queryset() + return qs.filter(ads_manager=self.ads_manager) + + +class PrefectureExportView(View, ADSExporter): + def get(self, request, ads_manager_administrator): + self.ads_manager_administrator = ads_manager_administrator + return self.generate() + + def get_filename(self): + return f"ADS_prefecture_{self.ads_manager_administrator.prefecture.numero}.xlsx" + + def get_queryset(self): + qs = super().get_queryset() + return qs.filter(ads_manager__administrator=self.ads_manager_administrator) + + def add_sheets(self, workbook): + super().add_sheets(workbook) + sheet = workbook.add_worksheet("Gestionnaires ADS") + sheet.write_row( + 0, + 0, + ( + "Nom de l'administration", + "Nombre d'ADS", + "Statut de la gestion des ADS", + ), + ) + # Applying bold format to headers + bold_format = workbook.add_format({"bold": True}) + sheet.set_row(0, None, bold_format) + + for idx, ads_manager in enumerate( + self.ads_manager_administrator.adsmanager_set.all() + ): + status = "" + if ads_manager.no_ads_declared: + status = "L'administration a déclaré ne gérer aucune ADS" + elif ads_manager.epci_delegate: + status = ( + "La gestion des ADS est déléguée à %s" + % ads_manager.epci_delegate.display_fulltext() + ) + + sheet.write_row( + idx + 1, + 0, + ( + ads_manager.content_object.display_text(), + ads_manager.ads_set.count() or "", + status, + ), + ) + sheet.autofit() diff --git a/mesads/app/views/ads_manager_request.py b/mesads/app/views/ads_manager_request.py new file mode 100644 index 0000000..8823207 --- /dev/null +++ b/mesads/app/views/ads_manager_request.py @@ -0,0 +1,247 @@ +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.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.views.generic.edit import FormView + +from reversion.views import RevisionMixin + +from ..forms import ( + ADSManagerForm, +) +from ..models import ( + ADSManagerAdministrator, + ADSManagerRequest, +) + + +class ADSManagerAdminView(RevisionMixin, TemplateView): + """This view is used by ADSManagerAdministartors 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 + success_url = reverse_lazy("app.ads-manager.index") + + def get_context_data(self, **kwargs): + """Expose the list of ADSManagerAdministrators for which current user + is configured. + + It is also accessible through user.adsmanageradministrator_set.all, but + we need to prefetch ads_managers__content_object to reduce the number + of SQL queries generated. + """ + ctx = super().get_context_data(**kwargs) + ctx["user_ads_manager_requests"] = ( + ADSManagerRequest.objects.filter(user=self.request.user) + .annotate(ads_count=Count("ads_manager__ads")) + .all() + ) + + ctx["ads_managers_administrators"] = ( + ADSManagerAdministrator.objects.select_related("prefecture") + .filter(users=self.request.user) + .all() + ) + return ctx + + def form_valid(self, form): + _, created = ADSManagerRequest.objects.get_or_create( + user=self.request.user, + ads_manager=form.cleaned_data["ads_manager"], + ) + + # Request already exists + if not created: + messages.warning( + self.request, + self.get_message_for_existing_request(form.cleaned_data["ads_manager"]), + ) + # Send notifications to administrators. + else: + messages.success( + self.request, + self.get_message_for_new_request(form.cleaned_data["ads_manager"]), + ) + email_subject = render_to_string( + "pages/email_ads_manager_request_administrator_subject.txt", + { + "user": self.request.user, + }, + request=self.request, + ).strip() + email_content = render_to_string( + "pages/email_ads_manager_request_administrator_content.txt", + { + "request": self.request, + "ads_manager": form.cleaned_data["ads_manager"], + "user": self.request.user, + }, + request=self.request, + ) + email_content_html = render_to_string( + "pages/email_ads_manager_request_administrator_content.mjml", + { + "request": self.request, + "ads_manager": form.cleaned_data["ads_manager"], + "user": self.request.user, + }, + request=self.request, + ) + + if form.cleaned_data["ads_manager"].administrator: + for administrator_user in form.cleaned_data[ + "ads_manager" + ].administrator.users.all(): + send_mail( + email_subject, + email_content, + settings.MESADS_CONTACT_EMAIL, + [administrator_user], + fail_silently=True, + html_message=email_content_html, + ) + + return super().form_valid(form) + + def get_message_for_existing_request(self, ads_manager): + if not ads_manager.administrator: + return """ + Vous avez déjà effectué une demande pour gérer les ADS de %(administration)s, et notre équipe va y répondre dans les plus brefs délais.

+ + Si vous n'avez eu aucun retour depuis plusieurs jours, n'hésitez pas à contacter notre équipe par email à %(email)s ou via notre module de tchat. + """ % { + "email": settings.MESADS_CONTACT_EMAIL, + "administration": ads_manager.content_object.display_fulltext(), + } + return """ + Vous avez déjà effectué une demande pour gérer les ADS de %(administration)s. Cette demande a été envoyée à %(prefecture)s qui devrait y répondre rapidement.

+ + Si vous n'avez eu aucun retour depuis plusieurs jours, n'hésitez pas à nous signaler le problème par email à %(email)s ou via notre module de tchat. +

+ Nous pourrons alors valider votre demande manuellement. + """ % { + "administration": ads_manager.content_object.display_fulltext(), + "prefecture": ads_manager.administrator.prefecture.display_fulltext(), + "email": settings.MESADS_CONTACT_EMAIL, + } + + def get_message_for_new_request(self, ads_manager): + # Request for EPCI or prefectures + if not ads_manager.administrator: + return """ + Votre demande vient d’être envoyée à notre équipe. Vous recevrez une confirmation de validation de votre + accès par mail.

+ + En cas de difficulté ou si vous n’obtenez pas de validation de votre demande vous pouvez contacter par email à + %(email)s ou via notre module de tchat.

+ + Vous pouvez également demander un accès pour la gestion des ADS d’une autre collectivité. + """ % { + "email": settings.MESADS_CONTACT_EMAIL + } + + return """ + Votre demande vient d’être envoyée à %(prefecture)s. Vous recevrez une confirmation de validation de votre + accès par mail.

+ + En cas de difficulté ou si vous n’obtenez pas de validation de votre demande vous pouvez + contacter par email à %(email)s ou via notre module de tchat.

+ + Vous pouvez également demander un accès pour la gestion des ADS d’une autre collectivité. + """ % { + "prefecture": ads_manager.administrator.prefecture.display_fulltext(), + "email": settings.MESADS_CONTACT_EMAIL, + } diff --git a/mesads/app/views/dashboards.py b/mesads/app/views/dashboards.py new file mode 100644 index 0000000..e5f1ddc --- /dev/null +++ b/mesads/app/views/dashboards.py @@ -0,0 +1,308 @@ +from datetime import timedelta +import collections + +from django.db.models import Count, Q, Sum +from django.db.models.functions import Coalesce +from django.utils import timezone +from django.views.generic import TemplateView +from django.views.generic.detail import DetailView + +from ..models import ( + ADSManager, + ADSManagerAdministrator, +) + + +class DashboardsView(TemplateView): + template_name = "pages/ads_register/dashboards_list.html" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx["stats"], ctx["stats_total"] = self.get_stats() + return ctx + + def get_stats(self): + """This function returns a tuple of two values: + + * stats: a list of dictionaries containing the following keys: + - obj: the ADSManagerAdministrator instance + - ads: a dictionary where keys represent the period (now, 3 months + ago, 6 months ago, 12 months ago), and values are the number of ADS + for this ADSManagerAdministrator + - users: a dictionary where keys represent the period (now, 3 months + ago, 6 months ago, 12 months ago), and values are the number of + accounts who can create ADS for this ADSManagerAdministrator + + >>> [ + ... obj: + ... 'ads': { + ... 'now': + ... '3_months': + ... '6_months': + ... '12_months': + ... }, + ... 'users': { + ... 'now': + ... '3_months': + ... '6_months': + ... '12_months': + ... } + ... ] + + * stats_total: a dictionary containing the keys 'ads' and 'users', and + the values are dictionaries where keys represent the period, and + values are the total number of ADS and users. + + >>> { + ... 'ads': { + ... 'now': , + ... '3_months': , + ... '6_months': , + ... '12_months': , + ... }, + ... 'users': { ... } + ... } + """ + now = timezone.now() + + stats = collections.defaultdict(lambda: {"obj": None, "ads": {}, "users": {}}) + + stats_total = { + "ads": {}, + "users": {}, + } + + ads_query_now = ( + ADSManagerAdministrator.objects.select_related("prefecture") + .annotate(ads_count=Count("adsmanager__ads")) + .filter(ads_count__gt=0) + ) + + # All ADSManagerAdministrator, with the count of ADS with at least one of the contact fields filled. + ads_with_info_query_now = ( + ADSManagerAdministrator.objects.select_related("prefecture") + .annotate( + ads_count=Count( + "adsmanager__ads", + filter=~Q(adsmanager__ads__owner_email="") + | ~Q(adsmanager__ads__owner_mobile="") + | ~Q(adsmanager__ads__owner_phone=""), + ) + ) + .filter(ads_count__gt=0) + ) + + ads_query_3_months = ( + ADSManagerAdministrator.objects.select_related("prefecture") + .filter(adsmanager__ads__creation_date__lte=now - timedelta(weeks=4 * 3)) + .annotate(ads_count=Count("adsmanager__ads")) + ) + + ads_query_6_months = ( + ADSManagerAdministrator.objects.select_related("prefecture") + .filter(adsmanager__ads__creation_date__lte=now - timedelta(weeks=4 * 6)) + .annotate(ads_count=Count("adsmanager__ads")) + ) + + ads_query_12_months = ( + ADSManagerAdministrator.objects.select_related("prefecture") + .filter(adsmanager__ads__creation_date__lte=now - timedelta(weeks=4 * 12)) + .annotate(ads_count=Count("adsmanager__ads")) + ) + + for label, query in ( + ("now", ads_query_now), + ("with_info_now", ads_with_info_query_now), + ("3_months", ads_query_3_months), + ("6_months", ads_query_6_months), + ("12_months", ads_query_12_months), + ): + for row in query: + stats[row.prefecture.id]["obj"] = row + stats[row.prefecture.id]["ads"][label] = row.ads_count + + stats_total["ads"][label] = query.aggregate( + total=Coalesce(Sum("ads_count"), 0) + )["total"] + + users_query_now = ( + ADSManagerAdministrator.objects.select_related("prefecture") + .filter(adsmanager__adsmanagerrequest__accepted=True) + .annotate(users_count=Count("id")) + ) + + users_query_3_months = ( + ADSManagerAdministrator.objects.select_related("prefecture") + .filter( + adsmanager__adsmanagerrequest__accepted=True, + adsmanager__adsmanagerrequest__created_at__lte=now + - timedelta(weeks=4 * 3), + ) + .annotate(users_count=Count("id")) + ) + + users_query_6_months = ( + ADSManagerAdministrator.objects.select_related("prefecture") + .filter( + adsmanager__adsmanagerrequest__accepted=True, + adsmanager__adsmanagerrequest__created_at__lte=now + - timedelta(weeks=4 * 6), + ) + .annotate(users_count=Count("id")) + ) + + users_query_12_months = ( + ADSManagerAdministrator.objects.select_related("prefecture") + .filter( + adsmanager__adsmanagerrequest__accepted=True, + adsmanager__adsmanagerrequest__created_at__lte=now + - timedelta(weeks=4 * 12), + ) + .annotate(users_count=Count("id")) + ) + + for label, query in ( + ("now", users_query_now), + ("3_months", users_query_3_months), + ("6_months", users_query_6_months), + ("12_months", users_query_12_months), + ): + for row in query.all(): + stats[row.prefecture.id]["obj"] = row + stats[row.prefecture.id]["users"][label] = row.users_count + + stats_total["users"][label] = query.aggregate( + total=Coalesce(Sum("users_count"), 0) + )["total"] + + return ( + # Transform dict to an ordered list + sorted(list(stats.values()), key=lambda stat: stat["obj"].id), + stats_total, + ) + + +class DashboardsDetailView(DetailView): + template_name = "pages/ads_register/dashboards_detail.html" + model = ADSManagerAdministrator + pk_url_kwarg = "ads_manager_administrator_id" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx["stats"] = self.get_stats() + return ctx + + def get_stats(self): + stats = {} + + stats = collections.defaultdict(lambda: {"obj": None, "ads": {}, "users": {}}) + + now = timezone.now() + + ads_query_now = ( + ADSManager.objects.prefetch_related("content_type", "content_object") + .filter(administrator=self.object) + .annotate(ads_count=Count("ads")) + .filter(ads_count__gt=0) + ) + + # All ADSManager, with the count of ADS with at least one of the contact fields filled. + ads_with_info_query_now = ( + ADSManager.objects.prefetch_related("content_type", "content_object") + .filter(administrator=self.object) + .annotate( + ads_count=Count( + "ads", + filter=~Q(ads__owner_email="") + | ~Q(ads__owner_mobile="") + | ~Q(ads__owner_phone=""), + ) + ) + .filter(ads_count__gt=0) + ) + + ads_query_3_months = ( + ADSManager.objects.prefetch_related("content_type", "content_object") + .filter( + administrator=self.object, + ads__creation_date__lte=now - timedelta(weeks=4 * 3), + ) + .annotate(ads_count=Count("ads")) + ) + + ads_query_6_months = ( + ADSManager.objects.prefetch_related("content_type", "content_object") + .filter( + administrator=self.object, + ads__creation_date__lte=now - timedelta(weeks=4 * 6), + ) + .annotate(ads_count=Count("ads")) + ) + + ads_query_12_months = ( + ADSManager.objects.prefetch_related("content_type", "content_object") + .filter( + administrator=self.object, + ads__creation_date__lte=now - timedelta(weeks=4 * 12), + ) + .annotate(ads_count=Count("ads")) + ) + + for label, query in ( + ("now", ads_query_now), + ("with_info_now", ads_with_info_query_now), + ("3_months", ads_query_3_months), + ("6_months", ads_query_6_months), + ("12_months", ads_query_12_months), + ): + for row in query: + stats[row.id]["obj"] = row + stats[row.id]["ads"][label] = row.ads_count + + users_query_now = ( + ADSManager.objects.prefetch_related("content_type", "content_object") + .filter(administrator=self.object, adsmanagerrequest__accepted=True) + .annotate(users_count=Count("id")) + ) + + users_query_3_months = ( + ADSManager.objects.prefetch_related("content_type", "content_object") + .filter( + administrator=self.object, + adsmanagerrequest__accepted=True, + adsmanagerrequest__created_at__lte=now - timedelta(weeks=4 * 3), + ) + .annotate(users_count=Count("id")) + ) + + users_query_6_months = ( + ADSManager.objects.prefetch_related("content_type", "content_object") + .filter( + administrator=self.object, + adsmanagerrequest__accepted=True, + adsmanagerrequest__created_at__lte=now - timedelta(weeks=4 * 6), + ) + .annotate(users_count=Count("id")) + ) + + users_query_12_months = ( + ADSManager.objects.prefetch_related("content_type", "content_object") + .filter( + administrator=self.object, + adsmanagerrequest__accepted=True, + adsmanagerrequest__created_at__lte=now - timedelta(weeks=4 * 12), + ) + .annotate(users_count=Count("id")) + ) + + for label, query in ( + ("now", users_query_now), + ("3_months", users_query_3_months), + ("6_months", users_query_6_months), + ("12_months", users_query_12_months), + ): + for row in query.all(): + stats[row.id]["obj"] = row + stats[row.id]["users"][label] = row.users_count + + return sorted(list(stats.values()), key=lambda stat: stat["obj"].id) diff --git a/mesads/app/views/export.py b/mesads/app/views/export.py new file mode 100644 index 0000000..8d17d87 --- /dev/null +++ b/mesads/app/views/export.py @@ -0,0 +1,138 @@ +from django.contrib.postgres.aggregates import ArrayAgg +from django.http import HttpResponse + +import xlsxwriter + +from ..models import ( + ADS, + ADSUser, +) + + +class ADSExporter: + """Generic class to export a list of ADS in an Excel file.""" + + def get_filename(self): + raise NotImplementedError + + def get_queryset(self): + return ( + ADS.objects.select_related( + "ads_manager__administrator__prefecture", + ) + .prefetch_related( + "ads_manager__content_object", + ) + .annotate( + ads_users_status=ArrayAgg("adsuser__status"), + ads_users_names=ArrayAgg("adsuser__name"), + ads_users_sirets=ArrayAgg("adsuser__siret"), + ads_users_licenses=ArrayAgg("adsuser__license_number"), + ) + ) + + def display_bool(self, value): + if value is None: + return "" + return "oui" if value else "non" + + def display_date(self, value): + if not value: + return "" + return value.strftime("%d/%m/%Y") + + def generate(self): + response = HttpResponse( + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers={ + "Content-Disposition": f'attachment; filename="{self.get_filename()}"' + }, + ) + workbook = xlsxwriter.Workbook(response) + self.add_sheets(workbook) + workbook.close() + return response + + def add_sheets(self, workbook): + """Override this method to add more sheets to the workbook.""" + self.ads_list_sheet(workbook) + + def ads_list_sheet(self, workbook): + bold_format = workbook.add_format({"bold": True}) + sheet = workbook.add_worksheet("ADS enregistrées") + headers = ( + "Type d'administration", + "Administration", + "Numéro de l'ADS", + "ADS actuellement exploitée ?", + "Date de création de l'ADS", + "Date du dernier renouvellement de l'ADS", + "Date d'attribution de l'ADS au titulaire actuel", + "Véhicule conventionné CPAM ?", + "Plaque d'immatriculation du véhicule", + "Le véhicule est-il un véhicule électrique/hybride ?", + "Véhicule compatible PMR ?", + "Titulaire de l'ADS", + "SIRET du titulaire de l'ADS", + "Téléphone fixe du titulaire de l'ADS", + "Téléphone mobile du titulaire de l'ADS", + "Email du titulaire de l'ADS", + ) + # If one of the ADS in the list has, let's say, 4 drivers, driver_headers + # will be appended 4 times to headers. + driver_headers = ( + "Statut du %s conducteur", + "Nom du %s conducteur", + "SIRET du %s conducteur", + "Numéro de la carte professionnelle du %s conducteur", + ) + # Counts the maximum number of drivers in the list of ADS.. + max_drivers = 0 + + # Applying bold format to headers + sheet.set_row(0, None, bold_format) + + for idx, ads in enumerate(self.get_queryset()): + # Append driver headers to headers if the current ADS has more drivers + # than the previous ones. + while max_drivers < len(ads.ads_users_status): + for h in driver_headers: + headers += ( + h % ("1er" if max_drivers == 0 else "%se" % (max_drivers + 1)), + ) + max_drivers += 1 + + info = ( + ads.ads_manager.content_object.type_name(), + ads.ads_manager.content_object.text(), + ads.number, + self.display_bool(ads.ads_in_use), + self.display_date(ads.ads_creation_date), + self.display_date(ads.ads_renew_date), + self.display_date(ads.attribution_date), + self.display_bool(ads.accepted_cpam), + ads.immatriculation_plate, + self.display_bool(ads.eco_vehicle), + self.display_bool(ads.vehicle_compatible_pmr), + ads.owner_name, + ads.owner_siret, + ads.owner_phone, + ads.owner_mobile, + ads.owner_email, + ) + for nth, status in enumerate(ads.ads_users_status): + # ads_users_status, ads_users_names, ads_users_sirets and + # ads_users_licenses have the same length. + info += ( + dict(ADSUser.status.field.choices).get( + ads.ads_users_status[nth], "" + ), + ads.ads_users_names[nth], + ads.ads_users_sirets[nth], + ads.ads_users_licenses[nth], + ) + sheet.write_row(idx + 1, 0, info) + + # Write headers, now that we know the maximum number of drivers. + sheet.write_row(0, 0, headers) + sheet.autofit() diff --git a/mesads/app/views/public.py b/mesads/app/views/public.py new file mode 100644 index 0000000..4ef01d4 --- /dev/null +++ b/mesads/app/views/public.py @@ -0,0 +1,143 @@ +import json + +from django.db.models import Count +from django.db.models.functions import TruncMonth +from django.views.generic import TemplateView + +from ..models import ( + ADS, + ADSManager, + ADSManagerRequest, +) + + +class HTTP500View(TemplateView): + """The default HTTP/500 handler can't access to context processors and does + not have access to the variable MESADS_CONTACT_EMAIL. + """ + + template_name = "500.html" + + def dispatch(self, request, *args, **kwargs): + """The base class TemplateView only accepts GET requests. By overriding + dispatch, we return the error page for any other method.""" + return super().get(request, *args, **kwargs) + + +class HomepageView(TemplateView): + template_name = "pages/homepage.html" + + +class FAQView(TemplateView): + template_name = "pages/faq.html" + + +class StatsView(TemplateView): + template_name = "pages/stats.html" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + ctx["ads_count"] = ADS.objects.count() + + ads_count_by_month = ( + ADS.objects.annotate(month=TruncMonth("creation_date")) + .values("month") + .annotate(count=Count("id")) + .order_by("month") + ) + ctx["ads_count_by_month"] = json.dumps( + dict( + ((row["month"].isoformat(), row["count"]) for row in ads_count_by_month) + ) + ) + + ctx["ads_manager_requests_count"] = ADSManagerRequest.objects.filter( + accepted=True + ).count() + + ads_manager_requests_by_month = ( + ADSManagerRequest.objects.filter(accepted=True) + .annotate(month=TruncMonth("created_at")) + .values("month") + .annotate(count=Count("id")) + .order_by("month") + ) + ctx["ads_manager_requests_by_month"] = json.dumps( + dict( + ( + (row["month"].isoformat(), row["count"]) + for row in ads_manager_requests_by_month + ) + ) + ) + + ctx["ads_managers_count"] = ( + ADSManager.objects.annotate(ads_count=Count("ads")) + .filter(ads_count__gt=0) + .count() + ) + + return ctx + + +class ReglementationView(TemplateView): + template_name = "pages/reglementation.html" + entries = [ + { + "title": "Principes généraux", + "articles": [ + { + "title": "Le rôle des collectivités", + "template": "pages/reglementation/principes_generaux/role_collectivites.html", + }, + { + "title": "Qu'est-ce qu'une ADS ?", + "template": "pages/reglementation/principes_generaux/qu_est_ce_qu_une_ads.html", + }, + { + "title": "Qui délivre les ADS ?", + "template": "pages/reglementation/principes_generaux/qui_delivre_ads.html", + }, + ], + }, + { + "title": "Délivrance d'une ADS", + "articles": [ + { + "menu_title": "Arrêté délimitant le nombre d'ADS", + "title": "Étape 1 : l'arrêté délimitant le nombre d'ADS", + "template": "pages/reglementation/delivrance_ads/arrete_delimitant_ads.html", + }, + { + "menu_title": "Attribution de l'ADS", + "title": "Étape 2 : l'attribution de l'ADS", + "template": "pages/reglementation/delivrance_ads/attribution_ads.html", + }, + { + "menu_title": "L'arrêté municipal", + "title": "Étape 3 : la notification de l'arrêté", + "template": "pages/reglementation/delivrance_ads/notification_arrete.html", + }, + { + "menu_title": "Retrait d'une ADS", + "title": "Étape 4 : le retrait d'une ADS", + "template": "pages/reglementation/delivrance_ads/retrait_ads.html", + }, + ], + }, + { + "title": "Registre des taxis relais", + "articles": [ + { + "title": "Qu'est-ce qu'un taxi relais ?", + "template": "pages/reglementation/relais/definition.html", + }, + ], + }, + ] + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx["entries"] = self.entries + return ctx diff --git a/mesads/app/test_views.py b/mesads/app/views/test_ads.py similarity index 54% rename from mesads/app/test_views.py rename to mesads/app/views/test_ads.py index 812645e..d920006 100644 --- a/mesads/app/test_views.py +++ b/mesads/app/views/test_ads.py @@ -1,456 +1,20 @@ -from datetime import datetime, timedelta import re -from django.contrib import messages -from django.contrib.contenttypes.models import ContentType -from django.core import mail from django.core.exceptions import ValidationError from django.core.files.uploadedfile import SimpleUploadedFile from django.db.models import Q -from django.test import RequestFactory -from django.utils import timezone from freezegun import freeze_time -from mesads.app.views import HTTP500View -from mesads.fradm.models import Commune, EPCI, Prefecture +from mesads.fradm.models import Commune, EPCI -from .models import ( +from ..models import ( ADS, ADSLegalFile, ADSManager, - ADSManagerRequest, ADSUser, ) -from .unittest import ClientTestCase -from .views import DashboardsView, DashboardsDetailView - - -class TestHTTP500View(ClientTestCase): - def test_500(self): - request = RequestFactory().get("/500") - response = HTTP500View.as_view()(request) - self.assertEqual(response.status_code, 200) - - # POST requests should be allowed - request = RequestFactory().post("/500") - response = HTTP500View.as_view()(request) - self.assertEqual(response.status_code, 200) - - -class TestHomepageView(ClientTestCase): - def test_200(self): - resp = self.anonymous_client.get("/") - self.assertEqual(resp.status_code, 200) - - -class TestProfileADSManagerAdministratorView(ClientTestCase): - def test_200(self): - resp = self.anonymous_client.get("/prefecture") - self.assertEqual(resp.status_code, 200) - - -class TestProfileADSManagerView(ClientTestCase): - def test_200(self): - resp = self.anonymous_client.get("/gestionnaire_ads") - self.assertEqual(resp.status_code, 200) - - -class TestProfileDriverView(ClientTestCase): - def test_200(self): - resp = self.anonymous_client.get("/chauffeur") - self.assertEqual(resp.status_code, 200) - - -class TestADSRegisterView(ClientTestCase): - def test_redirection(self): - for client_name, client, expected_status, redirect_url in ( - ( - "anonymous", - self.anonymous_client, - 302, - "/auth/login/?next=/registre_ads/", - ), - ("auth", self.auth_client, 302, "/registre_ads/gestion"), - ( - "ads_manager 35", - self.ads_manager_city35_client, - 302, - "/registre_ads/gestion", - ), - ( - "ads_manager_admin 35", - self.ads_manager_administrator_35_client, - 302, - "/registre_ads/admin_gestion", - ), - ("admin", self.admin_client, 302, "/registre_ads/dashboards"), - ): - with self.subTest( - client_name=client_name, - expected_status=expected_status, - redirect_url=redirect_url, - ): - resp = client.get("/registre_ads/") - self.assertEqual(resp.status_code, expected_status) - self.assertEqual(resp.url, redirect_url) - - -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() - self.initial_ads_managers_count = ADSManagerRequest.objects.count() - - def test_permissions(self): - for client_name, client, expected_status in ( - ("anonymous", self.anonymous_client, 302), - ("auth", self.auth_client, 200), - ("ads_manager 35", self.ads_manager_city35_client, 200), - ("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/gestion") - self.assertEqual(resp.status_code, expected_status) - - def test_create_request_invalid_id(self): - """Provide the id of a non-existing object.""" - resp = self.auth_client.post("/registre_ads/gestion", {"commune": 9999}) - self.assertEqual(len(resp.context["form"].errors["__all__"]), 1) - - def test_create_request_commune(self): - resp = self.auth_client.post( - "/registre_ads/gestion", {"commune": self.commune_melesse.id} - ) - self.assertEqual(resp.status_code, 302) - self.assertEqual( - ADSManagerRequest.objects.count(), self.initial_ads_managers_count + 1 - ) - - # Make sure django message is in the next request - resp = self.auth_client.get("/registre_ads/gestion") - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.context["messages"]), 1) - self.assertEqual(list(resp.context["messages"])[0].level, messages.SUCCESS) - - # If there is a ADSManagerAdministrator related to the commune, an email is sent for each member. - # The base class ClientTestCase configures Melesse to be managed by the ADSManagerAdministrator entry of - # l'Ille-et-Vilaine. - self.assertEqual(len(mail.outbox), 1) - - # - # If we send the same request, a warning message is displayed and no email is sent. - # - resp = self.auth_client.post( - "/registre_ads/gestion", {"commune": self.commune_melesse.id} - ) - self.assertEqual(resp.status_code, 302) - self.assertEqual( - ADSManagerRequest.objects.count(), self.initial_ads_managers_count + 1 - ) - - # Check warning message - resp = self.auth_client.get("/registre_ads/gestion") - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.context["messages"]), 1) - self.assertEqual(list(resp.context["messages"])[0].level, messages.WARNING) - # No new email - self.assertEqual(len(mail.outbox), 1) - - def test_create_request_epci(self): - epci = EPCI.objects.first() - resp = self.auth_client.post("/registre_ads/gestion", {"epci": epci.id}) - self.assertEqual(resp.status_code, 302) - self.assertEqual( - ADSManagerRequest.objects.count(), self.initial_ads_managers_count + 1 - ) - - # Make sure django message is in the next request - resp = self.auth_client.get("/registre_ads/gestion") - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.context["messages"]), 1) - self.assertEqual(list(resp.context["messages"])[0].level, messages.SUCCESS) - - # - # If we send the same request, no object is created and a warning message is displayed. - # - resp = self.auth_client.post("/registre_ads/gestion", {"epci": epci.id}) - self.assertEqual(resp.status_code, 302) - self.assertEqual( - ADSManagerRequest.objects.count(), self.initial_ads_managers_count + 1 - ) - - resp = self.auth_client.get("/registre_ads/gestion") - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.context["messages"]), 1) - self.assertEqual(list(resp.context["messages"])[0].level, messages.WARNING) - - def test_create_request_prefecture(self): - prefecture = Prefecture.objects.first() - resp = self.auth_client.post( - "/registre_ads/gestion", {"prefecture": prefecture.id} - ) - self.assertEqual(resp.status_code, 302) - self.assertEqual( - ADSManagerRequest.objects.count(), self.initial_ads_managers_count + 1 - ) - - # Make sure django message is in the next request - resp = self.auth_client.get("/registre_ads/gestion") - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.context["messages"]), 1) - self.assertEqual(list(resp.context["messages"])[0].level, messages.SUCCESS) - - # - # If we send the same request, no object is created and a warning message is displayed. - # - resp = self.auth_client.post( - "/registre_ads/gestion", {"prefecture": prefecture.id} - ) - self.assertEqual(resp.status_code, 302) - self.assertEqual( - ADSManagerRequest.objects.count(), self.initial_ads_managers_count + 1 - ) - - # Make sure django message is in the next request - resp = self.auth_client.get("/registre_ads/gestion") - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.context["messages"]), 1) - self.assertEqual(list(resp.context["messages"])[0].level, messages.WARNING) - - -class TestADSManagerView(ClientTestCase): - def test_permissions(self): - for client_name, client, expected_status in ( - ("anonymous", self.anonymous_client, 302), - ("auth", self.auth_client, 404), - ("ads_manager 35", self.ads_manager_city35_client, 200), - ("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/gestion/{self.ads_manager_city35.id}/" - ) - self.assertEqual(resp.status_code, expected_status) - - def test_get_404(self): - resp = self.ads_manager_city35_client.get("/registre_ads/gestion/99999/") - self.assertEqual(resp.status_code, 404) - - def test_get(self): - resp = self.ads_manager_city35_client.get( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/" - ) - self.assertEqual(self.ads_manager_city35, resp.context["ads_manager"]) - - prefecture = Prefecture.objects.filter(numero="35").get() - ads_manager = ADSManager.objects.filter( - content_type=ContentType.objects.get_for_model(prefecture), - object_id=prefecture.id, - ).get() - - resp = self.ads_manager_administrator_35_client.get( - f"/registre_ads/gestion/{ads_manager.id}/" - ) - self.assertEqual( - self.ads_manager_administrator_35.prefecture, - resp.context["ads_manager"].content_object, - ) - - def test_filters(self): - """Test filtering""" - # ADS 1 - ads1 = ADS.objects.create( - number="FILTER1", - ads_manager=self.ads_manager_city35, - immatriculation_plate="imm4tri-cul4tion", - accepted_cpam=True, - ads_in_use=True, - ) - # ADS 2 - ads2 = ADS.objects.create( - number="FILTER2", - ads_manager=self.ads_manager_city35, - owner_name="Bob Dylan", - accepted_cpam=False, - ads_in_use=True, - ) - # ADS 3 - ads3 = ADS.objects.create( - number="FILTER3", - ads_manager=self.ads_manager_city35, - owner_siret="12312312312312", - ads_in_use=True, - ) - ADSUser.objects.create(ads=ads3, name="Henri super", siret="11111111111111") - # ADS 4 - ads4 = ADS.objects.create( - number="FILTER4", ads_manager=self.ads_manager_city35, ads_in_use=True - ) - ADSUser.objects.create( - ads=ads4, name="Matthieu pas super", siret="22222222222222" - ) - - # Immatriculatin plate, returns first ADS - resp = self.ads_manager_city35_client.get( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/?q=imm4tricul4tion" - ) - self.assertEqual(list(resp.context["ads_list"].all()), [ads1]) - - # Owner firstname/lastname, returns second ADS - resp = self.ads_manager_city35_client.get( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/?q=bob dyla" - ) - self.assertEqual(list(resp.context["ads_list"].all()), [ads2]) - - # Owner SIRET, return third ADS - resp = self.ads_manager_city35_client.get( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/?q=123123123" - ) - self.assertEqual(list(resp.context["ads_list"].all()), [ads3]) - - # User SIRET, return ADS 4 - resp = self.ads_manager_city35_client.get( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/?q=22222222222222" - ) - self.assertEqual(list(resp.context["ads_list"].all()), [ads4]) - - # User name, return ADS 3 - resp = self.ads_manager_city35_client.get( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/?q=Henri SUPER" - ) - self.assertEqual(list(resp.context["ads_list"].all()), [ads3]) - - # CPAM accepted true, return ads 1 - resp = self.ads_manager_city35_client.get( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/?accepted_cpam=True" - ) - self.assertEqual(list(resp.context["ads_list"].all()), [ads1]) - - # CPAM accepted false, return ads 2 - resp = self.ads_manager_city35_client.get( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/?accepted_cpam=False" - ) - self.assertEqual(list(resp.context["ads_list"].all()), [ads2]) - - # CPAM accepted any, and no filters, return all - resp = self.ads_manager_city35_client.get( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/?q=&accepted_cpam=" - ) - self.assertEqual(list(resp.context["ads_list"].all()), [ads1, ads2, ads3, ads4]) - - def test_post_ok(self): - # Set the flag "no_ads_declared" for an administration that has no ADS - self.assertFalse(self.ads_manager_city35.no_ads_declared) - resp = self.ads_manager_city35_client.post( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/", - { - "no_ads_declared": "on", - }, - ) - self.assertEqual(resp.status_code, 302) - self.ads_manager_city35.refresh_from_db() - self.assertTrue(self.ads_manager_city35.no_ads_declared) - - # Remove the flag - resp = self.ads_manager_city35_client.post( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/", - ) - self.assertEqual(resp.status_code, 302) - self.ads_manager_city35.refresh_from_db() - self.assertFalse(self.ads_manager_city35.no_ads_declared) - - def test_post_error(self): - # Set the flag "no_ads_declared" for an administration which has ADS registered is impossible - self.assertFalse(self.ads_manager_city35.no_ads_declared) - ADS.objects.create( - number="12346", ads_manager=self.ads_manager_city35, ads_in_use=True - ) - resp = self.ads_manager_city35_client.post( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/", - { - "no_ads_declared": "on", - }, - ) - self.assertEqual(resp.status_code, 200) - self.ads_manager_city35.refresh_from_db() - self.assertFalse(self.ads_manager_city35.no_ads_declared) +from ..unittest import ClientTestCase class TestADSView(ClientTestCase): @@ -1173,406 +737,6 @@ def test_create_with_legal_files(self): self.assertEqual(legal_files[1].file.read(), b"Second file") -class TestExportPrefecture(ClientTestCase): - 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/prefectures/{self.ads_manager_administrator_35.prefecture.id}/export" - ) - self.assertEqual(resp.status_code, expected_status) - - def test_get_404(self): - resp = self.ads_manager_administrator_35_client.get( - "/registre_ads/prefectures/9999/export" - ) - self.assertEqual(resp.status_code, 404) - - def test_export(self): - ADS.objects.create( - number="1", - ads_manager=self.ads_manager_city35, - accepted_cpam=True, - ads_in_use=True, - ) - ADS.objects.create( - number="2", - ads_manager=self.ads_manager_city35, - ads_in_use=True, - ) - ADS.objects.create( - number="3", - ads_manager=self.ads_manager_city35, - ads_creation_date=datetime.now().date(), - ads_in_use=True, - ) - - resp = self.ads_manager_administrator_35_client.get( - f"/registre_ads/prefectures/{self.ads_manager_administrator_35.prefecture.id}/export" - ) - self.assertEqual(resp.status_code, 200) - self.assertEqual( - resp.headers["Content-Type"], - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) - - def test_export_no_ads_declared(self): - """Similar to test_export, but with no_ads_declared for the ADSManager.""" - self.ads_manager_city35.no_ads_declared = True - self.ads_manager_city35.save() - - ADS.objects.create( - number="1", - ads_manager=self.ads_manager_city35, - accepted_cpam=True, - ads_in_use=True, - ) - resp = self.ads_manager_administrator_35_client.get( - f"/registre_ads/prefectures/{self.ads_manager_administrator_35.prefecture.id}/export" - ) - self.assertEqual(resp.status_code, 200) - self.assertEqual( - resp.headers["Content-Type"], - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) - - def test_export_epci_delegate(self): - """Similar to test_export, but with epci_delegate for the ADSManager.""" - self.ads_manager_city35.epci_delegate = self.fixtures_epci[0] - self.ads_manager_city35.save() - - ADS.objects.create( - number="1", - ads_manager=self.ads_manager_city35, - accepted_cpam=True, - ads_in_use=True, - ) - resp = self.ads_manager_administrator_35_client.get( - f"/registre_ads/prefectures/{self.ads_manager_administrator_35.prefecture.id}/export" - ) - self.assertEqual(resp.status_code, 200) - self.assertEqual( - resp.headers["Content-Type"], - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) - - -class TestExportADSManager(ClientTestCase): - 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, 200), - ("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/gestion/{self.ads_manager_city35.id}/export" - ) - self.assertEqual(resp.status_code, expected_status) - - def test_export(self): - ADS.objects.create( - number="1", - ads_manager=self.ads_manager_city35, - accepted_cpam=True, - ads_in_use=True, - ) - ADS.objects.create( - number="2", - ads_manager=self.ads_manager_city35, - ads_in_use=True, - ) - ADS.objects.create( - number="3", - ads_manager=self.ads_manager_city35, - ads_creation_date=datetime.now().date(), - ads_in_use=True, - ) - - resp = self.ads_manager_city35_client.get( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/export" - ) - self.assertEqual(resp.status_code, 200) - self.assertEqual( - resp.headers["Content-Type"], - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) - - -class TestDashboardsViews(ClientTestCase): - """Test DashboardsView and DashboardsDetailView""" - - def setUp(self): - super().setUp() - request = RequestFactory().get("/dashboards") - self.dashboards_view = DashboardsView() - self.dashboards_view.setup(request) - - request = RequestFactory().get( - f"/registre_ads/dashboards/{self.ads_manager_administrator_35.id}" - ) - self.dashboards_detail_view = DashboardsDetailView( - object=self.ads_manager_administrator_35 - ) - self.dashboards_detail_view.setup(request) - - def test_permissions(self): - for client_name, client, expected_status in ( - ("anonymous", self.anonymous_client, 302), - ("auth", self.auth_client, 302), - ("ads_manager 35", self.ads_manager_city35_client, 302), - ("ads_manager_admin 35", self.ads_manager_administrator_35_client, 302), - ("admin", self.admin_client, 200), - ): - with self.subTest(client_name=client_name, expected_status=expected_status): - resp = client.get("/registre_ads/dashboards") - self.assertEqual(resp.status_code, expected_status) - - resp = client.get( - f"/registre_ads/dashboards/{self.ads_manager_administrator_35.id}/" - ) - self.assertEqual(resp.status_code, expected_status) - - def test_stats_default(self): - # The base class ClientTestCase creates ads_manager_administrator for - # departement 35, and configures an ADSManager for the city fo Melesse. - stats = [ - { - "obj": self.ads_manager_administrator_35, - "ads": {}, - "users": { - "now": 1, - }, - } - ] - stats_total = { - "ads": { - "now": 0, - "with_info_now": 0, - "3_months": 0, - "6_months": 0, - "12_months": 0, - }, - "users": { - "now": 1, - "3_months": 0, - "6_months": 0, - "12_months": 0, - }, - } - self.assertEqual((stats, stats_total), self.dashboards_view.get_stats()) - - self.assertEqual( - [ - { - "obj": self.ads_manager_city35, - "ads": {}, - "users": { - "now": 1, - }, - } - ], - self.dashboards_detail_view.get_stats(), - ) - - def test_stats_for_several_ads(self): - # Create several ADS for the city of Melesse - now = timezone.now() - for idx, creation_date in enumerate( - [ - now - timedelta(days=365 * 2), # 2 years old ADS - now - timedelta(days=300), # > 6 && < 12 months old - now - timedelta(days=120), # > 3 && < 6 months old - now - timedelta(days=1), # yesterday - ] - ): - ads = ADS.objects.create( - number=str(idx), ads_manager=self.ads_manager_city35, ads_in_use=True - ) - ads.creation_date = creation_date - ads.save() - - stats = [ - { - "obj": self.ads_manager_administrator_35, - "ads": { - "now": 4, - "3_months": 3, - "6_months": 2, - "12_months": 1, - }, - "users": { - "now": 1, - }, - } - ] - stats_total = { - "ads": { - "now": 4, - "with_info_now": 0, - "3_months": 3, - "6_months": 2, - "12_months": 1, - }, - "users": { - "now": 1, - "3_months": 0, - "6_months": 0, - "12_months": 0, - }, - } - - self.assertEqual((stats, stats_total), self.dashboards_view.get_stats()) - - self.assertEqual( - [ - { - "obj": self.ads_manager_city35, - "ads": { - "now": 4, - "3_months": 3, - "6_months": 2, - "12_months": 1, - }, - "users": { - "now": 1, - }, - } - ], - self.dashboards_detail_view.get_stats(), - ) - - def test_stats_for_several_ads_managers(self): - now = timezone.now() - # Give administration permissions for several users to Melesse. - for creation_date in [ - now - timedelta(days=365 * 2), # 2 years old ADS - now - timedelta(days=300), # > 6 && < 12 months old - now - timedelta(days=120), # > 3 && < 6 months old - now - timedelta(days=1), # yesterday - ]: - user = self.create_user().obj - ads_manager_request = ADSManagerRequest.objects.create( - user=user, - ads_manager=self.ads_manager_city35, - accepted=True, - ) - ads_manager_request.created_at = creation_date - ads_manager_request.save() - - stats = [ - { - "obj": self.ads_manager_administrator_35, - "ads": {}, - "users": { - "now": 5, - "3_months": 3, - "6_months": 2, - "12_months": 1, - }, - } - ] - stats_total = { - "ads": { - "now": 0, - "with_info_now": 0, - "3_months": 0, - "6_months": 0, - "12_months": 0, - }, - "users": { - "now": 5, - "3_months": 3, - "6_months": 2, - "12_months": 1, - }, - } - - self.assertEqual((stats, stats_total), self.dashboards_view.get_stats()) - - self.assertEqual( - [ - { - "obj": self.ads_manager_city35, - "ads": {}, - "users": { - "now": 5, - "3_months": 3, - "6_months": 2, - "12_months": 1, - }, - } - ], - self.dashboards_detail_view.get_stats(), - ) - - -class TestFAQView(ClientTestCase): - def test_get(self): - resp = self.client.get("/faq") - self.assertEqual(resp.status_code, 200) - - -class TestADSManagerDecreeView(ClientTestCase): - def test_permissions(self): - for client_name, client, expected_status in ( - ("anonymous", self.anonymous_client, 302), - ("auth", self.auth_client, 404), - ("ads_manager 35", self.ads_manager_city35_client, 200), - ("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/gestion/{self.ads_manager_city35.id}/arrete" - ) - self.assertEqual(resp.status_code, expected_status) - - def test_get_404(self): - resp = self.ads_manager_city35_client.get("/registre_ads/gestion/99999/arrete") - self.assertEqual(resp.status_code, 404) - - def test_get(self): - resp = self.ads_manager_city35_client.get( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/arrete" - ) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.context["ads_manager"], self.ads_manager_city35) - - def test_post(self): - file1 = SimpleUploadedFile( - name="myfile.pdf", content=b"First file", content_type="application/pdf" - ) - file2 = SimpleUploadedFile( - name="myfile2.pdf", content=b"Second file", content_type="application/pdf" - ) - - resp = self.ads_manager_city35_client.post( - f"/registre_ads/gestion/{self.ads_manager_city35.id}/arrete", - { - "adsmanagerdecree_set-TOTAL_FORMS": 5, - "adsmanagerdecree_set-INITIAL_FORMS": 0, - "adsmanagerdecree_set-MIN_NUM_FORMS": 0, - "adsmanagerdecree_set-MAX_NUM_FORMS": 5, - "adsmanagerdecree_set-0-file": file1, - "adsmanagerdecree_set-1-file": file2, - }, - ) - self.assertEqual(resp.status_code, 302) - - ads_manager_decrees = self.ads_manager_city35.adsmanagerdecree_set.all() - self.assertEqual(len(ads_manager_decrees), 2) - self.assertEqual(ads_manager_decrees[0].file.read(), b"First file") - self.assertEqual(ads_manager_decrees[1].file.read(), b"Second file") - - class TestADSDecreeView(ClientTestCase): def setUp(self): super().setUp() @@ -1764,15 +928,3 @@ def test_get(self): f"/registre_ads/gestion/{self.ads_manager_city35.id}/ads/{self.ads.id}/history" ) self.assertEqual(resp.status_code, 200) - - -class TestStatsView(ClientTestCase): - def test_get(self): - resp = self.anonymous_client.get("/chiffres-cles") - self.assertEqual(resp.status_code, 200) - - -class TestReglementationView(ClientTestCase): - def test_get(self): - resp = self.anonymous_client.get("/reglementation") - self.assertEqual(resp.status_code, 200) diff --git a/mesads/app/views/test_ads_manager.py b/mesads/app/views/test_ads_manager.py new file mode 100644 index 0000000..141c279 --- /dev/null +++ b/mesads/app/views/test_ads_manager.py @@ -0,0 +1,267 @@ +from datetime import datetime + +from django.contrib.contenttypes.models import ContentType +from django.core.files.uploadedfile import SimpleUploadedFile + +from mesads.fradm.models import Prefecture + +from ..models import ( + ADS, + ADSManager, + ADSUser, +) +from ..unittest import ClientTestCase + + +class TestADSManagerView(ClientTestCase): + def test_permissions(self): + for client_name, client, expected_status in ( + ("anonymous", self.anonymous_client, 302), + ("auth", self.auth_client, 404), + ("ads_manager 35", self.ads_manager_city35_client, 200), + ("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/gestion/{self.ads_manager_city35.id}/" + ) + self.assertEqual(resp.status_code, expected_status) + + def test_get_404(self): + resp = self.ads_manager_city35_client.get("/registre_ads/gestion/99999/") + self.assertEqual(resp.status_code, 404) + + def test_get(self): + resp = self.ads_manager_city35_client.get( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/" + ) + self.assertEqual(self.ads_manager_city35, resp.context["ads_manager"]) + + prefecture = Prefecture.objects.filter(numero="35").get() + ads_manager = ADSManager.objects.filter( + content_type=ContentType.objects.get_for_model(prefecture), + object_id=prefecture.id, + ).get() + + resp = self.ads_manager_administrator_35_client.get( + f"/registre_ads/gestion/{ads_manager.id}/" + ) + self.assertEqual( + self.ads_manager_administrator_35.prefecture, + resp.context["ads_manager"].content_object, + ) + + def test_filters(self): + """Test filtering""" + # ADS 1 + ads1 = ADS.objects.create( + number="FILTER1", + ads_manager=self.ads_manager_city35, + immatriculation_plate="imm4tri-cul4tion", + accepted_cpam=True, + ads_in_use=True, + ) + # ADS 2 + ads2 = ADS.objects.create( + number="FILTER2", + ads_manager=self.ads_manager_city35, + owner_name="Bob Dylan", + accepted_cpam=False, + ads_in_use=True, + ) + # ADS 3 + ads3 = ADS.objects.create( + number="FILTER3", + ads_manager=self.ads_manager_city35, + owner_siret="12312312312312", + ads_in_use=True, + ) + ADSUser.objects.create(ads=ads3, name="Henri super", siret="11111111111111") + # ADS 4 + ads4 = ADS.objects.create( + number="FILTER4", ads_manager=self.ads_manager_city35, ads_in_use=True + ) + ADSUser.objects.create( + ads=ads4, name="Matthieu pas super", siret="22222222222222" + ) + + # Immatriculatin plate, returns first ADS + resp = self.ads_manager_city35_client.get( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/?q=imm4tricul4tion" + ) + self.assertEqual(list(resp.context["ads_list"].all()), [ads1]) + + # Owner firstname/lastname, returns second ADS + resp = self.ads_manager_city35_client.get( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/?q=bob dyla" + ) + self.assertEqual(list(resp.context["ads_list"].all()), [ads2]) + + # Owner SIRET, return third ADS + resp = self.ads_manager_city35_client.get( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/?q=123123123" + ) + self.assertEqual(list(resp.context["ads_list"].all()), [ads3]) + + # User SIRET, return ADS 4 + resp = self.ads_manager_city35_client.get( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/?q=22222222222222" + ) + self.assertEqual(list(resp.context["ads_list"].all()), [ads4]) + + # User name, return ADS 3 + resp = self.ads_manager_city35_client.get( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/?q=Henri SUPER" + ) + self.assertEqual(list(resp.context["ads_list"].all()), [ads3]) + + # CPAM accepted true, return ads 1 + resp = self.ads_manager_city35_client.get( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/?accepted_cpam=True" + ) + self.assertEqual(list(resp.context["ads_list"].all()), [ads1]) + + # CPAM accepted false, return ads 2 + resp = self.ads_manager_city35_client.get( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/?accepted_cpam=False" + ) + self.assertEqual(list(resp.context["ads_list"].all()), [ads2]) + + # CPAM accepted any, and no filters, return all + resp = self.ads_manager_city35_client.get( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/?q=&accepted_cpam=" + ) + self.assertEqual(list(resp.context["ads_list"].all()), [ads1, ads2, ads3, ads4]) + + def test_post_ok(self): + # Set the flag "no_ads_declared" for an administration that has no ADS + self.assertFalse(self.ads_manager_city35.no_ads_declared) + resp = self.ads_manager_city35_client.post( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/", + { + "no_ads_declared": "on", + }, + ) + self.assertEqual(resp.status_code, 302) + self.ads_manager_city35.refresh_from_db() + self.assertTrue(self.ads_manager_city35.no_ads_declared) + + # Remove the flag + resp = self.ads_manager_city35_client.post( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/", + ) + self.assertEqual(resp.status_code, 302) + self.ads_manager_city35.refresh_from_db() + self.assertFalse(self.ads_manager_city35.no_ads_declared) + + def test_post_error(self): + # Set the flag "no_ads_declared" for an administration which has ADS registered is impossible + self.assertFalse(self.ads_manager_city35.no_ads_declared) + ADS.objects.create( + number="12346", ads_manager=self.ads_manager_city35, ads_in_use=True + ) + resp = self.ads_manager_city35_client.post( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/", + { + "no_ads_declared": "on", + }, + ) + self.assertEqual(resp.status_code, 200) + self.ads_manager_city35.refresh_from_db() + self.assertFalse(self.ads_manager_city35.no_ads_declared) + + +class TestExportADSManager(ClientTestCase): + 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, 200), + ("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/gestion/{self.ads_manager_city35.id}/export" + ) + self.assertEqual(resp.status_code, expected_status) + + def test_export(self): + ADS.objects.create( + number="1", + ads_manager=self.ads_manager_city35, + accepted_cpam=True, + ads_in_use=True, + ) + ADS.objects.create( + number="2", + ads_manager=self.ads_manager_city35, + ads_in_use=True, + ) + ADS.objects.create( + number="3", + ads_manager=self.ads_manager_city35, + ads_creation_date=datetime.now().date(), + ads_in_use=True, + ) + + resp = self.ads_manager_city35_client.get( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/export" + ) + self.assertEqual(resp.status_code, 200) + self.assertEqual( + resp.headers["Content-Type"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) + + +class TestADSManagerDecreeView(ClientTestCase): + def test_permissions(self): + for client_name, client, expected_status in ( + ("anonymous", self.anonymous_client, 302), + ("auth", self.auth_client, 404), + ("ads_manager 35", self.ads_manager_city35_client, 200), + ("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/gestion/{self.ads_manager_city35.id}/arrete" + ) + self.assertEqual(resp.status_code, expected_status) + + def test_get_404(self): + resp = self.ads_manager_city35_client.get("/registre_ads/gestion/99999/arrete") + self.assertEqual(resp.status_code, 404) + + def test_get(self): + resp = self.ads_manager_city35_client.get( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/arrete" + ) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.context["ads_manager"], self.ads_manager_city35) + + def test_post(self): + file1 = SimpleUploadedFile( + name="myfile.pdf", content=b"First file", content_type="application/pdf" + ) + file2 = SimpleUploadedFile( + name="myfile2.pdf", content=b"Second file", content_type="application/pdf" + ) + + resp = self.ads_manager_city35_client.post( + f"/registre_ads/gestion/{self.ads_manager_city35.id}/arrete", + { + "adsmanagerdecree_set-TOTAL_FORMS": 5, + "adsmanagerdecree_set-INITIAL_FORMS": 0, + "adsmanagerdecree_set-MIN_NUM_FORMS": 0, + "adsmanagerdecree_set-MAX_NUM_FORMS": 5, + "adsmanagerdecree_set-0-file": file1, + "adsmanagerdecree_set-1-file": file2, + }, + ) + self.assertEqual(resp.status_code, 302) + + ads_manager_decrees = self.ads_manager_city35.adsmanagerdecree_set.all() + self.assertEqual(len(ads_manager_decrees), 2) + self.assertEqual(ads_manager_decrees[0].file.read(), b"First file") + self.assertEqual(ads_manager_decrees[1].file.read(), b"Second file") diff --git a/mesads/app/views/test_ads_manager_admin.py b/mesads/app/views/test_ads_manager_admin.py new file mode 100644 index 0000000..ac1a5cf --- /dev/null +++ b/mesads/app/views/test_ads_manager_admin.py @@ -0,0 +1,96 @@ +from datetime import datetime + +from ..models import ( + ADS, +) +from ..unittest import ClientTestCase + + +class TestExportPrefecture(ClientTestCase): + 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/prefectures/{self.ads_manager_administrator_35.prefecture.id}/export" + ) + self.assertEqual(resp.status_code, expected_status) + + def test_get_404(self): + resp = self.ads_manager_administrator_35_client.get( + "/registre_ads/prefectures/9999/export" + ) + self.assertEqual(resp.status_code, 404) + + def test_export(self): + ADS.objects.create( + number="1", + ads_manager=self.ads_manager_city35, + accepted_cpam=True, + ads_in_use=True, + ) + ADS.objects.create( + number="2", + ads_manager=self.ads_manager_city35, + ads_in_use=True, + ) + ADS.objects.create( + number="3", + ads_manager=self.ads_manager_city35, + ads_creation_date=datetime.now().date(), + ads_in_use=True, + ) + + resp = self.ads_manager_administrator_35_client.get( + f"/registre_ads/prefectures/{self.ads_manager_administrator_35.prefecture.id}/export" + ) + self.assertEqual(resp.status_code, 200) + self.assertEqual( + resp.headers["Content-Type"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) + + def test_export_no_ads_declared(self): + """Similar to test_export, but with no_ads_declared for the ADSManager.""" + self.ads_manager_city35.no_ads_declared = True + self.ads_manager_city35.save() + + ADS.objects.create( + number="1", + ads_manager=self.ads_manager_city35, + accepted_cpam=True, + ads_in_use=True, + ) + resp = self.ads_manager_administrator_35_client.get( + f"/registre_ads/prefectures/{self.ads_manager_administrator_35.prefecture.id}/export" + ) + self.assertEqual(resp.status_code, 200) + self.assertEqual( + resp.headers["Content-Type"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) + + def test_export_epci_delegate(self): + """Similar to test_export, but with epci_delegate for the ADSManager.""" + self.ads_manager_city35.epci_delegate = self.fixtures_epci[0] + self.ads_manager_city35.save() + + ADS.objects.create( + number="1", + ads_manager=self.ads_manager_city35, + accepted_cpam=True, + ads_in_use=True, + ) + resp = self.ads_manager_administrator_35_client.get( + f"/registre_ads/prefectures/{self.ads_manager_administrator_35.prefecture.id}/export" + ) + self.assertEqual(resp.status_code, 200) + self.assertEqual( + resp.headers["Content-Type"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) diff --git a/mesads/app/views/test_ads_manager_request.py b/mesads/app/views/test_ads_manager_request.py new file mode 100644 index 0000000..77aab1f --- /dev/null +++ b/mesads/app/views/test_ads_manager_request.py @@ -0,0 +1,209 @@ +from django.contrib import messages +from django.core import mail + +from mesads.fradm.models import EPCI, Prefecture + +from ..models import ( + 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("/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() + self.initial_ads_managers_count = ADSManagerRequest.objects.count() + + def test_permissions(self): + for client_name, client, expected_status in ( + ("anonymous", self.anonymous_client, 302), + ("auth", self.auth_client, 200), + ("ads_manager 35", self.ads_manager_city35_client, 200), + ("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/gestion") + self.assertEqual(resp.status_code, expected_status) + + def test_create_request_invalid_id(self): + """Provide the id of a non-existing object.""" + resp = self.auth_client.post("/registre_ads/gestion", {"commune": 9999}) + self.assertEqual(len(resp.context["form"].errors["__all__"]), 1) + + def test_create_request_commune(self): + resp = self.auth_client.post( + "/registre_ads/gestion", {"commune": self.commune_melesse.id} + ) + self.assertEqual(resp.status_code, 302) + self.assertEqual( + ADSManagerRequest.objects.count(), self.initial_ads_managers_count + 1 + ) + + # Make sure django message is in the next request + resp = self.auth_client.get("/registre_ads/gestion") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.context["messages"]), 1) + self.assertEqual(list(resp.context["messages"])[0].level, messages.SUCCESS) + + # If there is a ADSManagerAdministrator related to the commune, an email is sent for each member. + # The base class ClientTestCase configures Melesse to be managed by the ADSManagerAdministrator entry of + # l'Ille-et-Vilaine. + self.assertEqual(len(mail.outbox), 1) + + # + # If we send the same request, a warning message is displayed and no email is sent. + # + resp = self.auth_client.post( + "/registre_ads/gestion", {"commune": self.commune_melesse.id} + ) + self.assertEqual(resp.status_code, 302) + self.assertEqual( + ADSManagerRequest.objects.count(), self.initial_ads_managers_count + 1 + ) + + # Check warning message + resp = self.auth_client.get("/registre_ads/gestion") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.context["messages"]), 1) + self.assertEqual(list(resp.context["messages"])[0].level, messages.WARNING) + # No new email + self.assertEqual(len(mail.outbox), 1) + + def test_create_request_epci(self): + epci = EPCI.objects.first() + resp = self.auth_client.post("/registre_ads/gestion", {"epci": epci.id}) + self.assertEqual(resp.status_code, 302) + self.assertEqual( + ADSManagerRequest.objects.count(), self.initial_ads_managers_count + 1 + ) + + # Make sure django message is in the next request + resp = self.auth_client.get("/registre_ads/gestion") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.context["messages"]), 1) + self.assertEqual(list(resp.context["messages"])[0].level, messages.SUCCESS) + + # + # If we send the same request, no object is created and a warning message is displayed. + # + resp = self.auth_client.post("/registre_ads/gestion", {"epci": epci.id}) + self.assertEqual(resp.status_code, 302) + self.assertEqual( + ADSManagerRequest.objects.count(), self.initial_ads_managers_count + 1 + ) + + resp = self.auth_client.get("/registre_ads/gestion") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.context["messages"]), 1) + self.assertEqual(list(resp.context["messages"])[0].level, messages.WARNING) + + def test_create_request_prefecture(self): + prefecture = Prefecture.objects.first() + resp = self.auth_client.post( + "/registre_ads/gestion", {"prefecture": prefecture.id} + ) + self.assertEqual(resp.status_code, 302) + self.assertEqual( + ADSManagerRequest.objects.count(), self.initial_ads_managers_count + 1 + ) + + # Make sure django message is in the next request + resp = self.auth_client.get("/registre_ads/gestion") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.context["messages"]), 1) + self.assertEqual(list(resp.context["messages"])[0].level, messages.SUCCESS) + + # + # If we send the same request, no object is created and a warning message is displayed. + # + resp = self.auth_client.post( + "/registre_ads/gestion", {"prefecture": prefecture.id} + ) + self.assertEqual(resp.status_code, 302) + self.assertEqual( + ADSManagerRequest.objects.count(), self.initial_ads_managers_count + 1 + ) + + # Make sure django message is in the next request + resp = self.auth_client.get("/registre_ads/gestion") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.context["messages"]), 1) + self.assertEqual(list(resp.context["messages"])[0].level, messages.WARNING) diff --git a/mesads/app/views/test_dashboards.py b/mesads/app/views/test_dashboards.py new file mode 100644 index 0000000..292db48 --- /dev/null +++ b/mesads/app/views/test_dashboards.py @@ -0,0 +1,231 @@ +from datetime import timedelta + +from django.test import RequestFactory +from django.utils import timezone + +from ..models import ( + ADS, + ADSManagerRequest, +) +from ..unittest import ClientTestCase +from ..views import DashboardsView, DashboardsDetailView + + +class TestDashboardsViews(ClientTestCase): + """Test DashboardsView and DashboardsDetailView""" + + def setUp(self): + super().setUp() + request = RequestFactory().get("/dashboards") + self.dashboards_view = DashboardsView() + self.dashboards_view.setup(request) + + request = RequestFactory().get( + f"/registre_ads/dashboards/{self.ads_manager_administrator_35.id}" + ) + self.dashboards_detail_view = DashboardsDetailView( + object=self.ads_manager_administrator_35 + ) + self.dashboards_detail_view.setup(request) + + def test_permissions(self): + for client_name, client, expected_status in ( + ("anonymous", self.anonymous_client, 302), + ("auth", self.auth_client, 302), + ("ads_manager 35", self.ads_manager_city35_client, 302), + ("ads_manager_admin 35", self.ads_manager_administrator_35_client, 302), + ("admin", self.admin_client, 200), + ): + with self.subTest(client_name=client_name, expected_status=expected_status): + resp = client.get("/registre_ads/dashboards") + self.assertEqual(resp.status_code, expected_status) + + resp = client.get( + f"/registre_ads/dashboards/{self.ads_manager_administrator_35.id}/" + ) + self.assertEqual(resp.status_code, expected_status) + + def test_stats_default(self): + # The base class ClientTestCase creates ads_manager_administrator for + # departement 35, and configures an ADSManager for the city fo Melesse. + stats = [ + { + "obj": self.ads_manager_administrator_35, + "ads": {}, + "users": { + "now": 1, + }, + } + ] + stats_total = { + "ads": { + "now": 0, + "with_info_now": 0, + "3_months": 0, + "6_months": 0, + "12_months": 0, + }, + "users": { + "now": 1, + "3_months": 0, + "6_months": 0, + "12_months": 0, + }, + } + self.assertEqual((stats, stats_total), self.dashboards_view.get_stats()) + + self.assertEqual( + [ + { + "obj": self.ads_manager_city35, + "ads": {}, + "users": { + "now": 1, + }, + } + ], + self.dashboards_detail_view.get_stats(), + ) + + def test_stats_for_several_ads(self): + # Create several ADS for the city of Melesse + now = timezone.now() + for idx, creation_date in enumerate( + [ + now - timedelta(days=365 * 2), # 2 years old ADS + now - timedelta(days=300), # > 6 && < 12 months old + now - timedelta(days=120), # > 3 && < 6 months old + now - timedelta(days=1), # yesterday + ] + ): + ads = ADS.objects.create( + number=str(idx), ads_manager=self.ads_manager_city35, ads_in_use=True + ) + ads.creation_date = creation_date + ads.save() + + stats = [ + { + "obj": self.ads_manager_administrator_35, + "ads": { + "now": 4, + "3_months": 3, + "6_months": 2, + "12_months": 1, + }, + "users": { + "now": 1, + }, + } + ] + stats_total = { + "ads": { + "now": 4, + "with_info_now": 0, + "3_months": 3, + "6_months": 2, + "12_months": 1, + }, + "users": { + "now": 1, + "3_months": 0, + "6_months": 0, + "12_months": 0, + }, + } + + self.assertEqual((stats, stats_total), self.dashboards_view.get_stats()) + + self.assertEqual( + [ + { + "obj": self.ads_manager_city35, + "ads": { + "now": 4, + "3_months": 3, + "6_months": 2, + "12_months": 1, + }, + "users": { + "now": 1, + }, + } + ], + self.dashboards_detail_view.get_stats(), + ) + + def test_stats_for_several_ads_managers(self): + now = timezone.now() + # Give administration permissions for several users to Melesse. + for creation_date in [ + now - timedelta(days=365 * 2), # 2 years old ADS + now - timedelta(days=300), # > 6 && < 12 months old + now - timedelta(days=120), # > 3 && < 6 months old + now - timedelta(days=1), # yesterday + ]: + user = self.create_user().obj + ads_manager_request = ADSManagerRequest.objects.create( + user=user, + ads_manager=self.ads_manager_city35, + accepted=True, + ) + ads_manager_request.created_at = creation_date + ads_manager_request.save() + + stats = [ + { + "obj": self.ads_manager_administrator_35, + "ads": {}, + "users": { + "now": 5, + "3_months": 3, + "6_months": 2, + "12_months": 1, + }, + } + ] + stats_total = { + "ads": { + "now": 0, + "with_info_now": 0, + "3_months": 0, + "6_months": 0, + "12_months": 0, + }, + "users": { + "now": 5, + "3_months": 3, + "6_months": 2, + "12_months": 1, + }, + } + + self.assertEqual((stats, stats_total), self.dashboards_view.get_stats()) + + self.assertEqual( + [ + { + "obj": self.ads_manager_city35, + "ads": {}, + "users": { + "now": 5, + "3_months": 3, + "6_months": 2, + "12_months": 1, + }, + } + ], + self.dashboards_detail_view.get_stats(), + ) + + +class TestStatsView(ClientTestCase): + def test_get(self): + resp = self.anonymous_client.get("/chiffres-cles") + self.assertEqual(resp.status_code, 200) + + +class TestReglementationView(ClientTestCase): + def test_get(self): + resp = self.anonymous_client.get("/reglementation") + self.assertEqual(resp.status_code, 200) diff --git a/mesads/app/views/test_public.py b/mesads/app/views/test_public.py new file mode 100644 index 0000000..c2e0a65 --- /dev/null +++ b/mesads/app/views/test_public.py @@ -0,0 +1,81 @@ +from django.test import RequestFactory + +from mesads.app.views import HTTP500View + +from ..unittest import ClientTestCase + + +class TestHTTP500View(ClientTestCase): + def test_500(self): + request = RequestFactory().get("/500") + response = HTTP500View.as_view()(request) + self.assertEqual(response.status_code, 200) + + # POST requests should be allowed + request = RequestFactory().post("/500") + response = HTTP500View.as_view()(request) + self.assertEqual(response.status_code, 200) + + +class TestHomepageView(ClientTestCase): + def test_200(self): + resp = self.anonymous_client.get("/") + self.assertEqual(resp.status_code, 200) + + +class TestProfileADSManagerAdministratorView(ClientTestCase): + def test_200(self): + resp = self.anonymous_client.get("/prefecture") + self.assertEqual(resp.status_code, 200) + + +class TestProfileADSManagerView(ClientTestCase): + def test_200(self): + resp = self.anonymous_client.get("/gestionnaire_ads") + self.assertEqual(resp.status_code, 200) + + +class TestProfileDriverView(ClientTestCase): + def test_200(self): + resp = self.anonymous_client.get("/chauffeur") + self.assertEqual(resp.status_code, 200) + + +class TestADSRegisterView(ClientTestCase): + def test_redirection(self): + for client_name, client, expected_status, redirect_url in ( + ( + "anonymous", + self.anonymous_client, + 302, + "/auth/login/?next=/registre_ads/", + ), + ("auth", self.auth_client, 302, "/registre_ads/gestion"), + ( + "ads_manager 35", + self.ads_manager_city35_client, + 302, + "/registre_ads/gestion", + ), + ( + "ads_manager_admin 35", + self.ads_manager_administrator_35_client, + 302, + "/registre_ads/admin_gestion", + ), + ("admin", self.admin_client, 302, "/registre_ads/dashboards"), + ): + with self.subTest( + client_name=client_name, + expected_status=expected_status, + redirect_url=redirect_url, + ): + resp = client.get("/registre_ads/") + self.assertEqual(resp.status_code, expected_status) + self.assertEqual(resp.url, redirect_url) + + +class TestFAQView(ClientTestCase): + def test_get(self): + resp = self.client.get("/faq") + self.assertEqual(resp.status_code, 200)