From 98cec2d25a807f8a0f98901956f391759b9fd8bf Mon Sep 17 00:00:00 2001 From: "Sofian A. Thibaut" Date: Wed, 3 Jul 2024 17:23:21 +0200 Subject: [PATCH 01/17] Create download page --- home/templates/home/download.html | 34 +++++++++++++++++++++++++++++++ home/urls.py | 1 + home/views.py | 8 ++++++++ 3 files changed, 43 insertions(+) create mode 100644 home/templates/home/download.html diff --git a/home/templates/home/download.html b/home/templates/home/download.html new file mode 100644 index 000000000..f65843261 --- /dev/null +++ b/home/templates/home/download.html @@ -0,0 +1,34 @@ +{% extends "index.html" %} + +{% load static %} +{% load sri %} + +{% block pagetitle %}Accueil{% endblock pagetitle %} + +{% block headers %} +{% sri_static "home/css/home.css" %} +{% endblock headers %} + +{% block body_class %}home{% endblock body_class %} + + +{% block breadcrumbs %} +{% endblock breadcrumbs %} + +{% block content %} +
+
+

Téléchargements

+

Trames de rapport triennal local des communes sous RNU

+
+
+{% include "home/partials/newsletter_form.html" %} +{% endblock content %} + +{% block tagging %} + +{% endblock tagging %} diff --git a/home/urls.py b/home/urls.py index bbde82466..ac311cec4 100644 --- a/home/urls.py +++ b/home/urls.py @@ -8,6 +8,7 @@ urlpatterns = [ path("", views.HomeView.as_view(), name="home"), path("rapport-local", views.HomeRapportLocalView.as_view(), name="home_rapport_local"), + path("telechargements", views.DownloadView.as_view(), name="download"), path("mentions-legales", views.LegalNoticeView.as_view(), name="cgv"), path("confidentialité", views.PrivacyView.as_view(), name="privacy"), path("test", views.TestView.as_view(), name="test"), diff --git a/home/views.py b/home/views.py index e4fec7aad..b3037c28d 100644 --- a/home/views.py +++ b/home/views.py @@ -35,6 +35,14 @@ def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) +class DownloadView(BreadCrumbMixin, TemplateView): + template_name = "home/download.html" + + def get_context_data(self, **kwargs): + kwargs["form"] = NewsletterForm() + return super().get_context_data(**kwargs) + + class HomeRapportLocalView(BreadCrumbMixin, TemplateView): template_name = "home/home_rapport_local.html" From 2edde0314300cbd0df6ae1276879d65e1c848066 Mon Sep 17 00:00:00 2001 From: "Sofian A. Thibaut" Date: Wed, 3 Jul 2024 17:50:57 +0200 Subject: [PATCH 02/17] Add alert to home pages --- home/templates/home/home.html | 3 ++- home/templates/home/home_rapport_local.html | 11 ++++++++++- home/urls.py | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/home/templates/home/home.html b/home/templates/home/home.html index 57498ddba..f0f341748 100644 --- a/home/templates/home/home.html +++ b/home/templates/home/home.html @@ -29,7 +29,8 @@

Mon Diagnostic Artificialisation vous aide à analyser et ma

Nouveau

- Exportez une trame pour votre rapport triennal local de suivi de l'artificialisation des sols conformément à l'article L. 2231-1 du code général des collectivités territoriales + Exportez une trame de votre rapport triennal local de suivi de l'artificialisation des sols conformément à l'article L. 2231-1 du code général des collectivités territoriales.
+ Pour les DDT, ces trames sont disponibles en téléchargement par paquets pour les communes sous RNU.

diff --git a/home/templates/home/home_rapport_local.html b/home/templates/home/home_rapport_local.html index 0e709450f..5139bcd63 100644 --- a/home/templates/home/home_rapport_local.html +++ b/home/templates/home/home_rapport_local.html @@ -23,8 +23,17 @@

Préparer le rapport triennal local de suivi de l’artificialisation des sols avec Mon Diag Artif

-

Notre équipe travaille en partenariat avec la DGALN à la production automatique d'une trame pré-remplie du rapport triennal local de suivi de l’artificialisation des sols de votre territoire.

Nouveau

+

Notre équipe travaille en partenariat avec la DGALN à la production automatique d'une trame pré-remplie du rapport triennal local de suivi de l’artificialisation des sols de votre territoire.

+
+
+
+

+ Pour les DDT, ces trames sont disponibles en téléchargement par paquets pour les communes sous RNU. +

+
+
+
{% include "project/partials/search.html" %}
diff --git a/home/urls.py b/home/urls.py index ac311cec4..fec6bc307 100644 --- a/home/urls.py +++ b/home/urls.py @@ -8,7 +8,7 @@ urlpatterns = [ path("", views.HomeView.as_view(), name="home"), path("rapport-local", views.HomeRapportLocalView.as_view(), name="home_rapport_local"), - path("telechargements", views.DownloadView.as_view(), name="download"), + path("telechargements", views.DownloadView.as_view(), name="downloads"), path("mentions-legales", views.LegalNoticeView.as_view(), name="cgv"), path("confidentialité", views.PrivacyView.as_view(), name="privacy"), path("test", views.TestView.as_view(), name="test"), From 3b4d602487846b4a3d34b932a71fe82b8dcad9a2 Mon Sep 17 00:00:00 2001 From: "Sofian A. Thibaut" Date: Thu, 4 Jul 2024 15:15:10 +0200 Subject: [PATCH 03/17] Display departments list --- home/templates/home/download.html | 13 +++++++++++-- home/views.py | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/home/templates/home/download.html b/home/templates/home/download.html index f65843261..6d0bc36a9 100644 --- a/home/templates/home/download.html +++ b/home/templates/home/download.html @@ -3,7 +3,7 @@ {% load static %} {% load sri %} -{% block pagetitle %}Accueil{% endblock pagetitle %} +{% block pagetitle %}Téléchargements{% endblock pagetitle %} {% block headers %} {% sri_static "home/css/home.css" %} @@ -19,7 +19,16 @@

Téléchargements

-

Trames de rapport triennal local des communes sous RNU

+

Trames de rapport triennal local des communes sous RNU

+

Mon Diagnostic Artificialisation met à disposition des DDT les trames de rapport triennal local par paquets de leurs communes sous RNU

+
    + {% for departement in departements %} +
  • +
    {{ departement.source_id }} {{ departement.name }}
    + +
  • + {% endfor %} +
{% include "home/partials/newsletter_form.html" %} diff --git a/home/views.py b/home/views.py index b3037c28d..4e68e1602 100644 --- a/home/views.py +++ b/home/views.py @@ -13,6 +13,7 @@ from brevo.connectors import Brevo from project.models import Request +from public_data.models import Departement from users.models import User from utils.functions import get_url_with_domain from utils.htmx import HtmxRedirectMixin, StandAloneMixin @@ -39,7 +40,10 @@ class DownloadView(BreadCrumbMixin, TemplateView): template_name = "home/download.html" def get_context_data(self, **kwargs): - kwargs["form"] = NewsletterForm() + kwargs |= { + "form": NewsletterForm(), + "departements": Departement.objects.all().order_by("source_id"), + } return super().get_context_data(**kwargs) From 7b871eb346fa10c69b5d5be09682b311b27fa0f6 Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Mon, 1 Jul 2024 18:10:49 +0200 Subject: [PATCH 04/17] feat(rnu): add command to generate diagnostics for rnu communes --- project/models/create.py | 58 +++++++++++++++++ project/tasks/project.py | 65 +++++++++++++++++++ .../commands/create_rnu_diagnostics.py | 48 ++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 public_data/management/commands/create_rnu_diagnostics.py diff --git a/project/models/create.py b/project/models/create.py index 6a248ecd4..ab984d5ff 100644 --- a/project/models/create.py +++ b/project/models/create.py @@ -5,6 +5,10 @@ from project import tasks from project.models import Project from project.models.enums import ProjectChangeReason +from project.models.request import Request, RequestedDocumentChoices +from public_data.infra.planning_competency.PlanningCompetencyServiceSudocuh import ( + PlanningCompetencyServiceSudocuh, +) from public_data.models import AdminRef, Land from users.models import User @@ -150,3 +154,57 @@ def update_ocsge(project: Project): project._change_reason = ProjectChangeReason.NEW_OCSGE_HAS_BEEN_DELIVERED project.save() + + +@celery.shared_task +def create_request_rnu_package_one_off(project_id: int) -> None: + project = Project.objects.get(pk=project_id) + user: User = project.user + request = Request.objects.create( + project=project, + first_name=user.first_name, + last_name=user.last_name, + function=user.function, + organism=user.organism, + email=user.email, + user=user, + requested_document=RequestedDocumentChoices.RAPPORT_LOCAL, + du_en_cours=PlanningCompetencyServiceSudocuh.planning_document_in_revision(project.land), + competence_urba=False, + ) + return request.id + + +def trigger_async_tasks_rnu_pakage_one_off(project: Project, public_key: str | None = None) -> None: + from metabase.tasks import async_create_stat_for_project + from project import tasks as t + + if not public_key: + public_key = project.get_public_key() + + tasks_list = [] + + if not project.async_add_city_done: + tasks_list.append(t.add_city.si(project.id, public_key)) + + if not project.async_set_combined_emprise_done: + tasks_list.append(t.set_combined_emprise.si(project.id)) + + if not project.async_find_first_and_last_ocsge_done: + tasks_list.append(t.find_first_and_last_ocsge.si(project.id)) + + if not project.async_ocsge_coverage_status_done: + tasks_list.append(t.calculate_project_ocsge_status.si(project.id)) + + if not project.async_add_comparison_lands_done: + tasks_list.append(t.add_comparison_lands.si(project.id)) + + return celery.chain( + *[ + *tasks_list, + map_tasks.si(project.id), + ], + async_create_stat_for_project.si(project.id, do_location=True), + create_request_rnu_package_one_off.si(project.id), + t.generate_word_diagnostic_rnu_package_one_off.si(project.id), + ).apply_async() diff --git a/project/tasks/project.py b/project/tasks/project.py index 15f63a99b..3f83a365f 100644 --- a/project/tasks/project.py +++ b/project/tasks/project.py @@ -284,6 +284,71 @@ class WordAlreadySentException(Exception): pass +@shared_task(bind=True, max_retries=6, queue="long") +def generate_word_diagnostic_rnu_package_one_off(self, project_id) -> int: + from diagnostic_word.renderers import ( + ConsoReportRenderer, + FullReportRenderer, + LocalReportRenderer, + ) + from highcharts.charts import RateLimitExceededException + + project = Project.objects.get(id=int(project_id)) + request = Request.objects.filter(project=project).first() + request_id = request.id + + logger.info(f"Start generate word for request_id={request_id}") + try: + req = Request.objects.select_related("project").get(id=int(request_id)) + + if not req.project: + raise Project.DoesNotExist("Project does not exist") + elif not req.project.async_complete: + msg = "Not all async tasks are completed, retry later" + raise WaitAsyncTaskException(msg) + elif req.sent_file: + raise WordAlreadySentException("Word already sent") + + logger.info("Start generating word") + + logger.info("Requested document: %s", req.requested_document) + + renderer_class = { + RequestedDocumentChoices.RAPPORT_COMPLET: FullReportRenderer, + RequestedDocumentChoices.RAPPORT_LOCAL: LocalReportRenderer, + RequestedDocumentChoices.RAPPORT_CONSO: ConsoReportRenderer, + }[req.requested_document] + + with renderer_class(request=req) as renderer: + context = renderer.get_context_data() + buffer = renderer.render_to_docx(context=context) + filename = renderer.get_file_name() + req.sent_file.save(filename, buffer, save=True) + logger.info("Word created and saved") + return request_id + + except ( + RateLimitExceededException, + Project.DoesNotExist, + WaitAsyncTaskException, + WordAlreadySentException, + ) as exc: + req.record_exception(exc) + logger.error("Error while generating word: %s", exc) + logger.exception(exc) + self.retry(exc=exc, countdown=5 ** (self.request.retries + 1)) + except Request.DoesNotExist as exc: + logger.error("Request doesn't not exist, no retry.") + logger.exception(exc) + except Exception as exc: + logger.error("Unknow exception, please investigate.") + req.record_exception(exc) + logger.exception(exc) + self.retry(exc=exc, countdown=900) + finally: + logger.info("End generate word for request=%d", request_id) + + @shared_task(bind=True, max_retries=6, queue="long") def generate_word_diagnostic(self, request_id) -> int: from diagnostic_word.renderers import ( diff --git a/public_data/management/commands/create_rnu_diagnostics.py b/public_data/management/commands/create_rnu_diagnostics.py new file mode 100644 index 000000000..ccc3be0c9 --- /dev/null +++ b/public_data/management/commands/create_rnu_diagnostics.py @@ -0,0 +1,48 @@ +import logging +from datetime import datetime + +from django.core.management.base import BaseCommand +from django.utils import timezone + +from project.models import Project +from project.models.create import trigger_async_tasks_rnu_pakage_one_off +from public_data.models import Commune, Land +from public_data.models.sudocuh import DocumentUrbanismeChoices, Sudocuh +from users.models import User + +logger = logging.getLogger("management.commands") + + +class Command(BaseCommand): + help = "create_rnu_diagnostics" + + def handle(self, *args, **options): + mondiagartif_user, _ = User.objects.get_or_create( + email="rnu.package@mondiagartif.beta.gouv.fr", + first_name="Alexis", + last_name="Athlani", + organism=User.ORGANISMS.DDT, + function="Développeur", + defaults={"email_checked": datetime.now()}, + ) + + for commune in Commune.objects.filter( + insee__in=[Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU).values("code_insee")] + ): + land = Land(public_key=f"COMM_{commune.pk}") + project = Project.objects.create( + name=f"Diagnostic de {land.name}", + is_public=True, + analyse_start_date="2011", + analyse_end_date="2022", + level="COMM", + land_id=str(land.id), + land_type=land.land_type, + territory_name=land.name, + user=mondiagartif_user, + import_status=Project.Status.SUCCESS, + import_date=timezone.now(), + import_error=None, + ) + trigger_async_tasks_rnu_pakage_one_off(project) + break From 58c9dc01d77c88c1d7dcd840d7196107a30e8c5e Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Tue, 2 Jul 2024 00:40:42 +0200 Subject: [PATCH 05/17] feat(create_rnu_diagnostics): optimize command --- project/urls.py | 1 + project/views/RNUPackagesProgressView.py | 70 +++++++++++++++++++ project/views/__init__.py | 1 + .../commands/create_rnu_diagnostics.py | 37 ++++++++-- 4 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 project/views/RNUPackagesProgressView.py diff --git a/project/urls.py b/project/urls.py index 1bb977da9..d99ff5456 100644 --- a/project/urls.py +++ b/project/urls.py @@ -195,6 +195,7 @@ path("/export-excel", views.ExportExcelView.as_view(), name="export-excel"), # SUB APPS path("test", views.TestView.as_view(), name="test"), + path(route="rnu-packages-progress", view=views.RNUPackagesProgressView.as_view(), name="rnu-packages-progress"), ] diff --git a/project/views/RNUPackagesProgressView.py b/project/views/RNUPackagesProgressView.py new file mode 100644 index 000000000..0d0ec6b4f --- /dev/null +++ b/project/views/RNUPackagesProgressView.py @@ -0,0 +1,70 @@ +from django.db.models import F, IntegerField, Sum +from django.db.models.functions import Cast +from rest_framework.response import Response +from rest_framework.views import APIView + +from project.models import Project +from public_data.models import Commune +from public_data.models.sudocuh import DocumentUrbanismeChoices, Sudocuh +from users.models import User + + +class RNUPackagesProgressView(APIView): + def get(self, request): + diagnostic_to_create = Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU) + of_those_with_ocsge = Commune.objects.filter( + insee__in=diagnostic_to_create.values("code_insee"), + ocsge_available=True, + ) + of_those_with_ocsge_count = of_those_with_ocsge.count() + diagnostic_to_create_count = diagnostic_to_create.count() + diagnostic_created = Project.objects.filter( + user=User.objects.get(email="rnu.package@mondiagartif.beta.gouv.fr"), + ) + diagnostic_created_count = diagnostic_created.count() + + async_fields = [ + "async_add_city_done", + "async_set_combined_emprise_done", + "async_cover_image_done", + "async_find_first_and_last_ocsge_done", + "async_add_comparison_lands_done", + "async_ocsge_coverage_status_done", + ] + + async_fields_with_ocsge = [ + "async_theme_map_understand_artif_done", + "async_theme_map_gpu_done", + "async_theme_map_fill_gpu_done", + ] + + aggregate_results = [] + + for field in async_fields: + aggregate_results.append( + diagnostic_created.aggregate( + **{ + f"{field}_count": Sum(Cast(field, IntegerField())), + f"{field}_percentage": F(f"{field}_count") * 100.0 / diagnostic_to_create_count, + } + ) + ) + for field in async_fields_with_ocsge: + aggregate_results.append( + diagnostic_created.aggregate( + **{ + f"{field}_count": Sum(Cast(field, IntegerField())), + f"{field}_percentage": F(f"{field}_count") * 100.0 / of_those_with_ocsge_count, + } + ) + ) + + return Response( + { + "diagnostic_to_create_count": diagnostic_to_create_count, + "of_those_with_ocsge_count": of_those_with_ocsge_count, + "diagnostic_created_count": diagnostic_created_count, + "diangostic_created_percentage": f"{diagnostic_created_count / diagnostic_to_create_count * 100}%", + "async_operations_progress": aggregate_results, + } + ) diff --git a/project/views/__init__.py b/project/views/__init__.py index 69faad1a0..4c4c70168 100644 --- a/project/views/__init__.py +++ b/project/views/__init__.py @@ -4,3 +4,4 @@ from .export import * from .map import * from .report import * +from .RNUPackagesProgressView import RNUPackagesProgressView diff --git a/public_data/management/commands/create_rnu_diagnostics.py b/public_data/management/commands/create_rnu_diagnostics.py index ccc3be0c9..92c1ecc73 100644 --- a/public_data/management/commands/create_rnu_diagnostics.py +++ b/public_data/management/commands/create_rnu_diagnostics.py @@ -1,14 +1,14 @@ import logging -from datetime import datetime from django.core.management.base import BaseCommand from django.utils import timezone -from project.models import Project +from project.models import Emprise, Project from project.models.create import trigger_async_tasks_rnu_pakage_one_off from public_data.models import Commune, Land from public_data.models.sudocuh import DocumentUrbanismeChoices, Sudocuh from users.models import User +from utils.db import fix_poly logger = logging.getLogger("management.commands") @@ -23,9 +23,11 @@ def handle(self, *args, **options): last_name="Athlani", organism=User.ORGANISMS.DDT, function="Développeur", - defaults={"email_checked": datetime.now()}, + defaults={"email_checked": timezone.now()}, ) + emprise_to_create = [] + for commune in Commune.objects.filter( insee__in=[Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU).values("code_insee")] ): @@ -43,6 +45,33 @@ def handle(self, *args, **options): import_status=Project.Status.SUCCESS, import_date=timezone.now(), import_error=None, + async_add_city_done=True, + first_year_ocsge=commune.first_millesime, + last_year_ocsge=commune.last_millesime, + available_millesimes=[commune.first_millesime, commune.last_millesime], + async_find_first_and_last_ocsge_done=True, + ocsge_coverage_status=Project.OcsgeCoverageStatus.COMPLETE_UNIFORM + if commune.ocsge_available + else Project.OcsgeCoverageStatus.NO_DATA, + async_ocsge_coverage_status_done=True, + async_set_combined_emprise_done=True, + ) + project.cities.add(commune) + + emprise_to_create.append( + Emprise( + mpoly=fix_poly(commune.mpoly), + srid_source=commune.srid_source, + project=project, + ) ) + + if len(emprise_to_create) > 100: + Emprise.objects.bulk_create(emprise_to_create) + emprise_to_create = [] + + if emprise_to_create: + Emprise.objects.bulk_create(emprise_to_create) + + for project in Project.objects.filter(user=mondiagartif_user): trigger_async_tasks_rnu_pakage_one_off(project) - break From f0834ef31acddf0ebe8a69faacfbe3f8089cff4b Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Tue, 2 Jul 2024 09:13:27 +0200 Subject: [PATCH 06/17] feat(RNUPackagesProgressView): add elapsed time( --- project/views/RNUPackagesProgressView.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/project/views/RNUPackagesProgressView.py b/project/views/RNUPackagesProgressView.py index 0d0ec6b4f..626f46f13 100644 --- a/project/views/RNUPackagesProgressView.py +++ b/project/views/RNUPackagesProgressView.py @@ -1,5 +1,6 @@ from django.db.models import F, IntegerField, Sum from django.db.models.functions import Cast +from django.utils import timezone from rest_framework.response import Response from rest_framework.views import APIView @@ -18,8 +19,10 @@ def get(self, request): ) of_those_with_ocsge_count = of_those_with_ocsge.count() diagnostic_to_create_count = diagnostic_to_create.count() + mda_user = User.objects.get(email="rnu.package@mondiagartif.beta.gouv.fr") + diagnostic_created = Project.objects.filter( - user=User.objects.get(email="rnu.package@mondiagartif.beta.gouv.fr"), + user=mda_user, ) diagnostic_created_count = diagnostic_created.count() @@ -59,8 +62,14 @@ def get(self, request): ) ) + time_diff = timezone.now() - mda_user.date_joined + hours = time_diff.seconds // 3600 + minutes = (time_diff.seconds % 3600) // 60 + seconds = time_diff.seconds % 60 + return Response( { + "elapsed_time": f"{hours}h {minutes}m {seconds}s", "diagnostic_to_create_count": diagnostic_to_create_count, "of_those_with_ocsge_count": of_those_with_ocsge_count, "diagnostic_created_count": diagnostic_created_count, From 558f89e22a608d89460805c04263ab562e4b8d55 Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Sun, 14 Jul 2024 17:24:01 +0200 Subject: [PATCH 07/17] feat(rnu-packages): remove unecessary steps --- .../commands/create_rnu_diagnostics.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/public_data/management/commands/create_rnu_diagnostics.py b/public_data/management/commands/create_rnu_diagnostics.py index 92c1ecc73..7feda28db 100644 --- a/public_data/management/commands/create_rnu_diagnostics.py +++ b/public_data/management/commands/create_rnu_diagnostics.py @@ -26,8 +26,6 @@ def handle(self, *args, **options): defaults={"email_checked": timezone.now()}, ) - emprise_to_create = [] - for commune in Commune.objects.filter( insee__in=[Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU).values("code_insee")] ): @@ -55,23 +53,24 @@ def handle(self, *args, **options): else Project.OcsgeCoverageStatus.NO_DATA, async_ocsge_coverage_status_done=True, async_set_combined_emprise_done=True, + async_theme_map_gpu_done=True, + async_theme_map_fill_gpu_done=True, + async_add_comparison_lands_done=True, ) - project.cities.add(commune) - emprise_to_create.append( - Emprise( - mpoly=fix_poly(commune.mpoly), - srid_source=commune.srid_source, - project=project, - ) + Emprise.objects.create( + mpoly=fix_poly(commune.mpoly), + srid_source=commune.srid_source, + project=project, ) - if len(emprise_to_create) > 100: - Emprise.objects.bulk_create(emprise_to_create) - emprise_to_create = [] + similar_lands_public_keys = [ + comparison_land.public_key for comparison_land in project.get_comparison_lands() + ] + + project.refresh_from_db() - if emprise_to_create: - Emprise.objects.bulk_create(emprise_to_create) + project = project.add_look_a_like(public_key=similar_lands_public_keys, many=True) for project in Project.objects.filter(user=mondiagartif_user): trigger_async_tasks_rnu_pakage_one_off(project) From 86de0294291556ca82486d39f121440f00e9fcdc Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Sun, 14 Jul 2024 17:49:52 +0200 Subject: [PATCH 08/17] feat(rnu-package): add create_rnu_packages command --- project/models/RNUPackage.py | 8 +++++ project/models/__init__.py | 2 ++ project/tasks/project.py | 35 +++++++++++++++++-- .../commands/create_rnu_packages.py | 21 +++++++++++ 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 project/models/RNUPackage.py create mode 100644 public_data/management/commands/create_rnu_packages.py diff --git a/project/models/RNUPackage.py b/project/models/RNUPackage.py new file mode 100644 index 000000000..e33f5e53c --- /dev/null +++ b/project/models/RNUPackage.py @@ -0,0 +1,8 @@ +from django.db import models + + +class RNUPackage(models.Model): + file = models.FileField(upload_to="rnu_packages/") + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + departement_official_id = models.CharField(max_length=10) diff --git a/project/models/__init__.py b/project/models/__init__.py index 53beade84..f372d53b0 100644 --- a/project/models/__init__.py +++ b/project/models/__init__.py @@ -8,11 +8,13 @@ "trigger_async_tasks", "user_directory_path", "RequestedDocumentChoices", + "RNUPackage", ] from .project_base import Emprise, Project, ProjectCommune from .request import ErrorTracking, Request, RequestedDocumentChoices +from .RNUPackage import RNUPackage from .utils import user_directory_path # isort: split diff --git a/project/tasks/project.py b/project/tasks/project.py index 3f83a365f..c2ed8585e 100644 --- a/project/tasks/project.py +++ b/project/tasks/project.py @@ -1,5 +1,6 @@ import io import logging +import zipfile from datetime import timedelta from typing import Any, Dict, Literal @@ -17,9 +18,14 @@ from matplotlib.lines import Line2D from matplotlib_scalebar.scalebar import ScaleBar -from project.models import Emprise, Project, Request, RequestedDocumentChoices -from public_data.domain.containers import PublicDataContainer -from public_data.models import AdminRef, ArtificialArea, Land, OcsgeDiff +from project.models import ( + Emprise, + Project, + Request, + RequestedDocumentChoices, + RNUPackage, +) +from public_data.models import ArtificialArea, Departement, Land, OcsgeDiff from public_data.models.gpu import ArtifAreaZoneUrba, ZoneUrba from utils.db import fix_poly from utils.emails import SibTemplateEmail @@ -883,3 +889,26 @@ def alert_on_blocked_diagnostic(self) -> None: finally: logger.info("End alert_on_blocked_diagnostic") + + +@shared_task(max_retries=5) +def create_zip_departement_rnu_package_one_off(departement_id: str) -> None: + departement = Departement.objects.get(source_id=departement_id) + requests_created_by_the_rnu_package_service_account = Request.objects.filter( + email="rnu.package@mondiagartif.beta.gouv.fr", + project__land_id=departement.pk, + ) + + file_name = f"rnu_package_departement_{departement_id}.zip" + + with zipfile.ZipFile(file_name, "a", compression=zipfile.ZIP_DEFLATED) as zipf: + for request in requests_created_by_the_rnu_package_service_account: + file_name_in_zip = f"{departement_id}_COMM_{request.project.land.official_id}.docx" + zipf.write( + filename=request.sent_file, + arcname=file_name_in_zip, + ) + RNUPackage.objects.create( + departement_official_id=departement.source_id, + file_name=file_name, + ) diff --git a/public_data/management/commands/create_rnu_packages.py b/public_data/management/commands/create_rnu_packages.py new file mode 100644 index 000000000..3ebbd2ef3 --- /dev/null +++ b/public_data/management/commands/create_rnu_packages.py @@ -0,0 +1,21 @@ +import logging + +import celery +from django.core.management.base import BaseCommand + +from project.tasks import create_zip_departement_rnu_package_one_off +from public_data.models import Departement + +logger = logging.getLogger("management.commands") + + +class Command(BaseCommand): + help = "create_rnu_diagnostics" + + def handle(self, *args, **options): + tasks = [] + + for departement in Departement.objects.all(): + tasks.append(create_zip_departement_rnu_package_one_off.si(departement.source_id)) + + celery.group(*tasks).apply_async(queue="long") From 0d30500fcd9635e9695de3f6cae4fb2a551ee4f2 Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Mon, 15 Jul 2024 08:55:09 +0200 Subject: [PATCH 09/17] feat(rnu-package): add celery tasks --- project/migrations/0084_rnupackage.py | 22 +++++++++++ project/tasks/project.py | 15 ++++--- project/views/RNUPackagesProgressView.py | 39 ++++++++++++------- .../commands/create_rnu_diagnostics.py | 16 ++++++-- .../commands/create_rnu_packages.py | 12 +++++- 5 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 project/migrations/0084_rnupackage.py diff --git a/project/migrations/0084_rnupackage.py b/project/migrations/0084_rnupackage.py new file mode 100644 index 000000000..66b884a08 --- /dev/null +++ b/project/migrations/0084_rnupackage.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.13 on 2024-07-14 15:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("project", "0083_alter_historicalrequest_organism_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="RNUPackage", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("file", models.FileField(upload_to="rnu_packages/")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("departement_official_id", models.CharField(max_length=10)), + ], + ), + ] diff --git a/project/tasks/project.py b/project/tasks/project.py index c2ed8585e..147a85dfd 100644 --- a/project/tasks/project.py +++ b/project/tasks/project.py @@ -894,21 +894,24 @@ def alert_on_blocked_diagnostic(self) -> None: @shared_task(max_retries=5) def create_zip_departement_rnu_package_one_off(departement_id: str) -> None: departement = Departement.objects.get(source_id=departement_id) + commune_in_departement_ids_as_string = [ + str(commune_id) for commune_id in departement.commune_set.values_list("id", flat=True) + ] requests_created_by_the_rnu_package_service_account = Request.objects.filter( email="rnu.package@mondiagartif.beta.gouv.fr", - project__land_id=departement.pk, + project__land_id__in=commune_in_departement_ids_as_string, ) file_name = f"rnu_package_departement_{departement_id}.zip" with zipfile.ZipFile(file_name, "a", compression=zipfile.ZIP_DEFLATED) as zipf: for request in requests_created_by_the_rnu_package_service_account: + if not request.sent_file: + raise ValueError(f"Request {request.id} has no sent file") + file_name_in_zip = f"{departement_id}_COMM_{request.project.land.official_id}.docx" - zipf.write( - filename=request.sent_file, - arcname=file_name_in_zip, - ) + zipf.writestr(file_name_in_zip, request.sent_file.read()) RNUPackage.objects.create( departement_official_id=departement.source_id, - file_name=file_name, + file=file_name, ) diff --git a/project/views/RNUPackagesProgressView.py b/project/views/RNUPackagesProgressView.py index 626f46f13..dca525834 100644 --- a/project/views/RNUPackagesProgressView.py +++ b/project/views/RNUPackagesProgressView.py @@ -5,7 +5,7 @@ from rest_framework.views import APIView from project.models import Project -from public_data.models import Commune +from public_data.models import Commune, Departement from public_data.models.sudocuh import DocumentUrbanismeChoices, Sudocuh from users.models import User @@ -13,8 +13,10 @@ class RNUPackagesProgressView(APIView): def get(self, request): diagnostic_to_create = Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU) - of_those_with_ocsge = Commune.objects.filter( + from_commune_table = Commune.objects.filter( insee__in=diagnostic_to_create.values("code_insee"), + ) + of_those_with_ocsge = from_commune_table.filter( ocsge_available=True, ) of_those_with_ocsge_count = of_those_with_ocsge.count() @@ -37,8 +39,6 @@ def get(self, request): async_fields_with_ocsge = [ "async_theme_map_understand_artif_done", - "async_theme_map_gpu_done", - "async_theme_map_fill_gpu_done", ] aggregate_results = [] @@ -67,13 +67,26 @@ def get(self, request): minutes = (time_diff.seconds % 3600) // 60 seconds = time_diff.seconds % 60 - return Response( - { - "elapsed_time": f"{hours}h {minutes}m {seconds}s", - "diagnostic_to_create_count": diagnostic_to_create_count, - "of_those_with_ocsge_count": of_those_with_ocsge_count, - "diagnostic_created_count": diagnostic_created_count, - "diangostic_created_percentage": f"{diagnostic_created_count / diagnostic_to_create_count * 100}%", - "async_operations_progress": aggregate_results, + response_data = { + "elapsed_time": f"{hours}h {minutes}m {seconds}s", + "diagnostic_to_create_count": diagnostic_to_create_count, + "of_those_with_ocsge_count": of_those_with_ocsge_count, + "diagnostic_created_count": diagnostic_created_count, + "diangostic_created_percentage": f"{diagnostic_created_count / diagnostic_to_create_count * 100}%", + "async_operations_progress": aggregate_results, + } + + for departement in Departement.objects.all(): + response_data[f"department_{departement.source_id}"] = { + "diagnostic_to_create_count": diagnostic_to_create.filter( + code_insee__startswith=departement.source_id + ).count(), + "of_those_with_ocsge_count": of_those_with_ocsge.filter(departement=departement).count(), + "diagnostic_created_count": diagnostic_created.annotate( + land_id_as_int=Cast("land_id", output_field=IntegerField()) + ) + .filter(land_id_as_int__in=from_commune_table.filter(departement=departement).values("id")) + .count(), } - ) + + return Response(response_data) diff --git a/public_data/management/commands/create_rnu_diagnostics.py b/public_data/management/commands/create_rnu_diagnostics.py index 7feda28db..71c913436 100644 --- a/public_data/management/commands/create_rnu_diagnostics.py +++ b/public_data/management/commands/create_rnu_diagnostics.py @@ -16,6 +16,9 @@ class Command(BaseCommand): help = "create_rnu_diagnostics" + def add_arguments(self, parser): + parser.add_argument("--departement", type=str, required=True) + def handle(self, *args, **options): mondiagartif_user, _ = User.objects.get_or_create( email="rnu.package@mondiagartif.beta.gouv.fr", @@ -26,8 +29,11 @@ def handle(self, *args, **options): defaults={"email_checked": timezone.now()}, ) + projects = [] + for commune in Commune.objects.filter( - insee__in=[Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU).values("code_insee")] + departement__source_id=options["departement"], + insee__in=[Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU).values("code_insee")], ): land = Land(public_key=f"COMM_{commune.pk}") project = Project.objects.create( @@ -58,6 +64,8 @@ def handle(self, *args, **options): async_add_comparison_lands_done=True, ) + project.cities.add(commune) + Emprise.objects.create( mpoly=fix_poly(commune.mpoly), srid_source=commune.srid_source, @@ -70,7 +78,9 @@ def handle(self, *args, **options): project.refresh_from_db() - project = project.add_look_a_like(public_key=similar_lands_public_keys, many=True) + project.add_look_a_like(public_key=similar_lands_public_keys, many=True) + + projects.append(project) - for project in Project.objects.filter(user=mondiagartif_user): + for project in projects: trigger_async_tasks_rnu_pakage_one_off(project) diff --git a/public_data/management/commands/create_rnu_packages.py b/public_data/management/commands/create_rnu_packages.py index 3ebbd2ef3..011ab07ff 100644 --- a/public_data/management/commands/create_rnu_packages.py +++ b/public_data/management/commands/create_rnu_packages.py @@ -10,12 +10,20 @@ class Command(BaseCommand): - help = "create_rnu_diagnostics" + help = "create_rnu_packages" + + def add_arguments(self, parser): + parser.add_argument("--departement", type=str, required=False) def handle(self, *args, **options): tasks = [] - for departement in Departement.objects.all(): + departements = Departement.objects.all() + + if options["departement"]: + departements = Departement.objects.filter(source_id=options["departement"]) + + for departement in departements: tasks.append(create_zip_departement_rnu_package_one_off.si(departement.source_id)) celery.group(*tasks).apply_async(queue="long") From eba6759c2de5bc6eecceffec166fe3149d45e9fd Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Mon, 15 Jul 2024 14:43:59 +0200 Subject: [PATCH 10/17] feat(rnu-packages): set done when word is created --- project/tasks/project.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/project/tasks/project.py b/project/tasks/project.py index 147a85dfd..648abc863 100644 --- a/project/tasks/project.py +++ b/project/tasks/project.py @@ -25,7 +25,8 @@ RequestedDocumentChoices, RNUPackage, ) -from public_data.models import ArtificialArea, Departement, Land, OcsgeDiff +from public_data.domain.containers import PublicDataContainer +from public_data.models import AdminRef, ArtificialArea, Departement, Land, OcsgeDiff from public_data.models.gpu import ArtifAreaZoneUrba, ZoneUrba from utils.db import fix_poly from utils.emails import SibTemplateEmail @@ -330,6 +331,8 @@ def generate_word_diagnostic_rnu_package_one_off(self, project_id) -> int: buffer = renderer.render_to_docx(context=context) filename = renderer.get_file_name() req.sent_file.save(filename, buffer, save=True) + req.done = True + req.save(update_fields=["done"]) logger.info("Word created and saved") return request_id From 444cc10ada5651e8075e425cd08d920952e41db5 Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Tue, 16 Jul 2024 11:02:01 +0200 Subject: [PATCH 11/17] feat(RNUPackage): add model to admin --- project/admin.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/project/admin.py b/project/admin.py index d87f9b3d4..abb81919b 100644 --- a/project/admin.py +++ b/project/admin.py @@ -6,7 +6,7 @@ from simple_history.admin import SimpleHistoryAdmin from project import tasks -from project.models import ErrorTracking, Project, Request +from project.models import ErrorTracking, Project, Request, RNUPackage from project.models.exceptions import TooOldException @@ -243,3 +243,18 @@ def response_change(self, request, obj): messages.add_message(request, messages.INFO, msg) return HttpResponseRedirect(".") return super().response_change(request, obj) + + +@admin.register(RNUPackage) +class RNUPackageAdmin(admin.ModelAdmin): + model = RNUPackage + list_display = ( + "departement_official_id", + "created_at", + "updated_at", + ) + search_fields = ("departement_official_id",) + readonly_fields = ( + "created_at", + "updated_at", + ) From 202909e95a55a101f3688aa479101509318192c7 Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Tue, 16 Jul 2024 11:03:53 +0200 Subject: [PATCH 12/17] feat(RNUPackage): add constraints to model --- ...lter_rnupackage_departement_official_id.py | 17 ++++++++ .../migrations/0086_rnupackage_app_version.py | 18 +++++++++ .../migrations/0087_alter_rnupackage_file.py | 17 ++++++++ .../migrations/0088_alter_rnupackage_file.py | 17 ++++++++ project/models/RNUPackage.py | 8 +++- project/tasks/project.py | 40 +++++++++---------- .../commands/create_rnu_diagnostics.py | 31 +++++++++++--- .../commands/create_rnu_packages.py | 30 +++++++++++++- 8 files changed, 149 insertions(+), 29 deletions(-) create mode 100644 project/migrations/0085_alter_rnupackage_departement_official_id.py create mode 100644 project/migrations/0086_rnupackage_app_version.py create mode 100644 project/migrations/0087_alter_rnupackage_file.py create mode 100644 project/migrations/0088_alter_rnupackage_file.py diff --git a/project/migrations/0085_alter_rnupackage_departement_official_id.py b/project/migrations/0085_alter_rnupackage_departement_official_id.py new file mode 100644 index 000000000..54adeed2b --- /dev/null +++ b/project/migrations/0085_alter_rnupackage_departement_official_id.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.13 on 2024-07-16 07:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("project", "0084_rnupackage"), + ] + + operations = [ + migrations.AlterField( + model_name="rnupackage", + name="departement_official_id", + field=models.CharField(max_length=10, unique=True), + ), + ] diff --git a/project/migrations/0086_rnupackage_app_version.py b/project/migrations/0086_rnupackage_app_version.py new file mode 100644 index 000000000..d81cbb8e8 --- /dev/null +++ b/project/migrations/0086_rnupackage_app_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.13 on 2024-07-16 08:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("project", "0085_alter_rnupackage_departement_official_id"), + ] + + operations = [ + migrations.AddField( + model_name="rnupackage", + name="app_version", + field=models.CharField(default="", max_length=10), + preserve_default=False, + ), + ] diff --git a/project/migrations/0087_alter_rnupackage_file.py b/project/migrations/0087_alter_rnupackage_file.py new file mode 100644 index 000000000..57339c234 --- /dev/null +++ b/project/migrations/0087_alter_rnupackage_file.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.13 on 2024-07-16 08:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("project", "0086_rnupackage_app_version"), + ] + + operations = [ + migrations.AlterField( + model_name="rnupackage", + name="file", + field=models.FileField(upload_to="rnu_packages"), + ), + ] diff --git a/project/migrations/0088_alter_rnupackage_file.py b/project/migrations/0088_alter_rnupackage_file.py new file mode 100644 index 000000000..2969182b9 --- /dev/null +++ b/project/migrations/0088_alter_rnupackage_file.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.13 on 2024-07-16 08:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("project", "0087_alter_rnupackage_file"), + ] + + operations = [ + migrations.AlterField( + model_name="rnupackage", + name="file", + field=models.FileField(blank=True, null=True, upload_to="rnu_packages"), + ), + ] diff --git a/project/models/RNUPackage.py b/project/models/RNUPackage.py index e33f5e53c..545e87e09 100644 --- a/project/models/RNUPackage.py +++ b/project/models/RNUPackage.py @@ -2,7 +2,11 @@ class RNUPackage(models.Model): - file = models.FileField(upload_to="rnu_packages/") + file = models.FileField(upload_to="rnu_packages", blank=True, null=True) + app_version = models.CharField(max_length=10) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - departement_official_id = models.CharField(max_length=10) + departement_official_id = models.CharField( + max_length=10, + unique=True, + ) diff --git a/project/tasks/project.py b/project/tasks/project.py index 648abc863..0cede44bc 100644 --- a/project/tasks/project.py +++ b/project/tasks/project.py @@ -1,5 +1,6 @@ import io import logging +import os import zipfile from datetime import timedelta from typing import Any, Dict, Literal @@ -291,14 +292,13 @@ class WordAlreadySentException(Exception): pass -@shared_task(bind=True, max_retries=6, queue="long") +@shared_task(bind=True, max_retries=10, queue="long") def generate_word_diagnostic_rnu_package_one_off(self, project_id) -> int: from diagnostic_word.renderers import ( ConsoReportRenderer, FullReportRenderer, LocalReportRenderer, ) - from highcharts.charts import RateLimitExceededException project = Project.objects.get(id=int(project_id)) request = Request.objects.filter(project=project).first() @@ -336,26 +336,11 @@ def generate_word_diagnostic_rnu_package_one_off(self, project_id) -> int: logger.info("Word created and saved") return request_id - except ( - RateLimitExceededException, - Project.DoesNotExist, - WaitAsyncTaskException, - WordAlreadySentException, - ) as exc: - req.record_exception(exc) - logger.error("Error while generating word: %s", exc) - logger.exception(exc) - self.retry(exc=exc, countdown=5 ** (self.request.retries + 1)) - except Request.DoesNotExist as exc: - logger.error("Request doesn't not exist, no retry.") - logger.exception(exc) except Exception as exc: - logger.error("Unknow exception, please investigate.") req.record_exception(exc) + logger.error("Error while generating word: %s", exc) logger.exception(exc) - self.retry(exc=exc, countdown=900) - finally: - logger.info("End generate word for request=%d", request_id) + self.retry(exc=exc, countdown=10) @shared_task(bind=True, max_retries=6, queue="long") @@ -914,7 +899,20 @@ def create_zip_departement_rnu_package_one_off(departement_id: str) -> None: file_name_in_zip = f"{departement_id}_COMM_{request.project.land.official_id}.docx" zipf.writestr(file_name_in_zip, request.sent_file.read()) - RNUPackage.objects.create( + + try: + package = RNUPackage.objects.get( + departement_official_id=departement.source_id, + ) + package.app_version = settings.OFFICIAL_VERSION + package.save() + except RNUPackage.DoesNotExist: + package = RNUPackage.objects.create( departement_official_id=departement.source_id, - file=file_name, + app_version=settings.OFFICIAL_VERSION, ) + + with open(file_name, "rb") as buffer: + package.file.save(name=file_name, content=buffer, save=True) + + os.remove(file_name) diff --git a/public_data/management/commands/create_rnu_diagnostics.py b/public_data/management/commands/create_rnu_diagnostics.py index 71c913436..950e032fd 100644 --- a/public_data/management/commands/create_rnu_diagnostics.py +++ b/public_data/management/commands/create_rnu_diagnostics.py @@ -3,7 +3,7 @@ from django.core.management.base import BaseCommand from django.utils import timezone -from project.models import Emprise, Project +from project.models import Emprise, Project, Request from project.models.create import trigger_async_tasks_rnu_pakage_one_off from public_data.models import Commune, Land from public_data.models.sudocuh import DocumentUrbanismeChoices, Sudocuh @@ -31,18 +31,35 @@ def handle(self, *args, **options): projects = [] - for commune in Commune.objects.filter( + communes = Commune.objects.filter( departement__source_id=options["departement"], insee__in=[Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU).values("code_insee")], - ): - land = Land(public_key=f"COMM_{commune.pk}") + ) + + logger.info(f"Found {len(communes)} RNU communes") + + projects_to_delete = Project.objects.filter( + user=mondiagartif_user, + land_id__in=[str(commune.id) for commune in communes], + ) + + request_to_delete = Request.objects.filter(project__in=projects_to_delete) + + _, detail_request_deleted = request_to_delete.delete() + _, detail_project_deleted = projects_to_delete.delete() + + logger.info(f"Deleted : {detail_request_deleted}") + logger.info(f"Deleted : {detail_project_deleted}") + + for commune in communes: + land = Land(public_key=f"COMM_{commune.id}") project = Project.objects.create( name=f"Diagnostic de {land.name}", is_public=True, analyse_start_date="2011", analyse_end_date="2022", level="COMM", - land_id=str(land.id), + land_id=str(commune.id), land_type=land.land_type, territory_name=land.name, user=mondiagartif_user, @@ -82,5 +99,9 @@ def handle(self, *args, **options): projects.append(project) + logger.info(f"Created {len(projects)} projects") + for project in projects: trigger_async_tasks_rnu_pakage_one_off(project) + + logger.info("All projects have been created and async tasks have been triggered") diff --git a/public_data/management/commands/create_rnu_packages.py b/public_data/management/commands/create_rnu_packages.py index 011ab07ff..808fd5ac3 100644 --- a/public_data/management/commands/create_rnu_packages.py +++ b/public_data/management/commands/create_rnu_packages.py @@ -3,8 +3,10 @@ import celery from django.core.management.base import BaseCommand +from project.models import Request from project.tasks import create_zip_departement_rnu_package_one_off -from public_data.models import Departement +from public_data.models import Commune, Departement, Sudocuh +from public_data.models.sudocuh import DocumentUrbanismeChoices logger = logging.getLogger("management.commands") @@ -23,7 +25,33 @@ def handle(self, *args, **options): if options["departement"]: departements = Departement.objects.filter(source_id=options["departement"]) + communes = Commune.objects.all() + + if options["departement"]: + communes = Commune.objects.filter( + departement__source_id=options["departement"], + insee__in=[Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU).values("code_insee")], + ) + + commune_count = communes.count() + request_for_communes_count = Request.objects.filter( + done=True, project__land_id__in=[str(commune.id) for commune in communes] + ).count() + + if commune_count != request_for_communes_count: + raise ValueError( + ( + f"Commune count ({commune_count}) does not match " + "request for communes count ({request_for_communes_count})" + ) + ) + else: + logger.info( + f"Commune count ({commune_count}) matches request for communes count ({request_for_communes_count})" + ) + for departement in departements: + logger.info(f"Creating RNU package for departement {departement.source_id}") tasks.append(create_zip_departement_rnu_package_one_off.si(departement.source_id)) celery.group(*tasks).apply_async(queue="long") From 40eedb1f7ba1506b1aa1caf1a3ab6eefd3bc6831 Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Tue, 16 Jul 2024 11:21:00 +0200 Subject: [PATCH 13/17] fix(create_rnu_package): log string interpo --- public_data/management/commands/create_rnu_packages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public_data/management/commands/create_rnu_packages.py b/public_data/management/commands/create_rnu_packages.py index 808fd5ac3..42b3d45d2 100644 --- a/public_data/management/commands/create_rnu_packages.py +++ b/public_data/management/commands/create_rnu_packages.py @@ -42,7 +42,7 @@ def handle(self, *args, **options): raise ValueError( ( f"Commune count ({commune_count}) does not match " - "request for communes count ({request_for_communes_count})" + f"request for communes count ({request_for_communes_count})" ) ) else: From 782c1cb49b4591ef945dcd73a4ae47f55a41e24f Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Thu, 18 Jul 2024 10:15:01 +0200 Subject: [PATCH 14/17] feat(rnu-packages): misc --- home/templates/home/download.html | 43 ++++++--- home/templates/home/home.html | 2 +- home/templates/home/home_rapport_local.html | 2 +- home/views.py | 9 +- project/models/RNUPackage.py | 15 +++ project/tasks/project.py | 12 ++- .../templates/project/rnu_package_notice.html | 83 +++++++++++++++++ project/urls.py | 1 - project/views/RNUPackagesProgressView.py | 92 ------------------- project/views/__init__.py | 1 - .../commands/create_rnu_diagnostics.py | 6 +- .../commands/create_rnu_packages.py | 42 +++++---- users/templates/users/signin.html | 24 ++++- 13 files changed, 196 insertions(+), 136 deletions(-) create mode 100644 project/templates/project/rnu_package_notice.html delete mode 100644 project/views/RNUPackagesProgressView.py diff --git a/home/templates/home/download.html b/home/templates/home/download.html index 6d0bc36a9..349243d60 100644 --- a/home/templates/home/download.html +++ b/home/templates/home/download.html @@ -18,17 +18,38 @@ {% block content %}
-

Téléchargements

-

Trames de rapport triennal local des communes sous RNU

-

Mon Diagnostic Artificialisation met à disposition des DDT les trames de rapport triennal local par paquets de leurs communes sous RNU

-
    - {% for departement in departements %} -
  • -
    {{ departement.source_id }} {{ departement.name }}
    - -
  • - {% endfor %} -
+

Trames de rapport triennal local des communes au RNU

+

+ Mon Diagnostic Artificialisation met à disposition des DDT les trames de rapport triennal local par paquets de leurs communes au RNU +

+
+
+
+
+ + + + + + + + + + + {% for package in rnu_packages %} + + + + + + + {% endfor %} + +
DépartementNombre de communes au RNUDate de créationLien
{{ package.departement_official_id }} - {{ package.departement.name }}{{ package.communes|length }}{{ package.created_at }}Lien de téléchargement
+
+
+
+
{% include "home/partials/newsletter_form.html" %} diff --git a/home/templates/home/home.html b/home/templates/home/home.html index c02fbe924..bac8712c7 100644 --- a/home/templates/home/home.html +++ b/home/templates/home/home.html @@ -30,7 +30,7 @@

Mon Diagnostic Artificialisation vous aide à analyser et ma

Nouveau

Exportez une trame de votre rapport triennal local de suivi de l'artificialisation des sols conformément à l'article L. 2231-1 du code général des collectivités territoriales.
- Pour les DDT, ces trames sont disponibles en téléchargement par paquets pour les communes sous RNU. + Pour les DDT, ces trames sont disponibles en téléchargement par paquets pour les communes au RNU.

diff --git a/home/templates/home/home_rapport_local.html b/home/templates/home/home_rapport_local.html index 88d46ee93..4eb67767e 100644 --- a/home/templates/home/home_rapport_local.html +++ b/home/templates/home/home_rapport_local.html @@ -29,7 +29,7 @@

Préparer le rapport triennal local de suivi de l’artifici

- Pour les DDT, ces trames sont disponibles en téléchargement par paquets pour les communes sous RNU. + Pour les DDT, ces trames sont disponibles en téléchargement par paquets pour les communes au RNU.

diff --git a/home/views.py b/home/views.py index 4e68e1602..624bc48e6 100644 --- a/home/views.py +++ b/home/views.py @@ -3,7 +3,7 @@ from django.conf import settings from django.contrib import messages -from django.contrib.auth.mixins import UserPassesTestMixin +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.db.models import F, Value from django.http import HttpRequest, HttpResponse, HttpResponseGone from django.shortcuts import redirect @@ -12,8 +12,7 @@ from django_app_parameter import app_parameter from brevo.connectors import Brevo -from project.models import Request -from public_data.models import Departement +from project.models import Request, RNUPackage from users.models import User from utils.functions import get_url_with_domain from utils.htmx import HtmxRedirectMixin, StandAloneMixin @@ -36,13 +35,13 @@ def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) -class DownloadView(BreadCrumbMixin, TemplateView): +class DownloadView(LoginRequiredMixin, BreadCrumbMixin, TemplateView): template_name = "home/download.html" def get_context_data(self, **kwargs): kwargs |= { "form": NewsletterForm(), - "departements": Departement.objects.all().order_by("source_id"), + "rnu_packages": RNUPackage.objects.all().order_by("departement_official_id"), } return super().get_context_data(**kwargs) diff --git a/project/models/RNUPackage.py b/project/models/RNUPackage.py index 545e87e09..b86b969eb 100644 --- a/project/models/RNUPackage.py +++ b/project/models/RNUPackage.py @@ -1,4 +1,8 @@ from django.db import models +from django.utils.functional import cached_property + +from public_data.models import Departement +from public_data.models.sudocuh import DocumentUrbanismeChoices, Sudocuh class RNUPackage(models.Model): @@ -10,3 +14,14 @@ class RNUPackage(models.Model): max_length=10, unique=True, ) + + @cached_property + def departement(self): + return Departement.objects.get(source_id=self.departement_official_id) + + @cached_property + def communes(self): + sudocuh = Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU) + return self.departement.commune_set.filter( + insee__in=sudocuh.values("code_insee"), + ) diff --git a/project/tasks/project.py b/project/tasks/project.py index 0cede44bc..f52e6462c 100644 --- a/project/tasks/project.py +++ b/project/tasks/project.py @@ -29,6 +29,7 @@ from public_data.domain.containers import PublicDataContainer from public_data.models import AdminRef, ArtificialArea, Departement, Land, OcsgeDiff from public_data.models.gpu import ArtifAreaZoneUrba, ZoneUrba +from public_data.storages import DataStorage from utils.db import fix_poly from utils.emails import SibTemplateEmail from utils.functions import get_url_with_domain @@ -890,9 +891,18 @@ def create_zip_departement_rnu_package_one_off(departement_id: str) -> None: project__land_id__in=commune_in_departement_ids_as_string, ) + notice_file_path = f"rnu_packages/NOTICE_{departement_id}.pdf" + rnu_communes_map_file_path = f"rnu_packages/COMM_DU_{departement_id}.pdf" + file_name = f"rnu_package_departement_{departement_id}.zip" - with zipfile.ZipFile(file_name, "a", compression=zipfile.ZIP_DEFLATED) as zipf: + with zipfile.ZipFile(file_name, "a", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zipf: + notice_file = DataStorage().open(notice_file_path, "rb") + rnu_communes_map_file = DataStorage().open(rnu_communes_map_file_path, "rb") + + zipf.writestr(f"NOTICE_{departement_id}.pdf", notice_file.read()) + zipf.writestr(f"COMM_DU_{departement_id}.pdf", rnu_communes_map_file.read()) + for request in requests_created_by_the_rnu_package_service_account: if not request.sent_file: raise ValueError(f"Request {request.id} has no sent file") diff --git a/project/templates/project/rnu_package_notice.html b/project/templates/project/rnu_package_notice.html new file mode 100644 index 000000000..3b6dc3108 --- /dev/null +++ b/project/templates/project/rnu_package_notice.html @@ -0,0 +1,83 @@ +{% load sri %} +{% sri_static "assets/styles/main.css" %} + + + + + + + +

Paquet de rapports locaux des communes au RNU du département {{ object.departement.name }} ({{ object.departement.source_id }})

+
    +
  • Date de production du paquet : {{ object.created_at }}
  • +
  • Source de donnée ayant permis de déterminer la liste des communes au RNU : + + Sudocuh + +
  • +
+

+ Ce paquet de trames de rapport local s'adresse aux DDT. + Il contient un rapport local par commune au RNU du département {{ object.departement.name }} ({{ object.departement.source_id }}). +

+

+ Une version web de ces diagnostics est disponible. + Un lien est disponible dans le bas de page de chaque rapport. +

+ +
+
+
+
+ + + + + + + + + + + + + + + + + + {% for commune in communes %} + + + + + {% endfor %} + +
+ Liste des fichiers du paquet +
FichierChemin
Ce documentNOTICE_{{ object.departement_official_id }}.pdf
Carte des communes au RNU du départementCOMM_DU_{{ object.departement_official_id }}.pdf
Rapport de + {{ commune.name }} + {{ commune.departement.source_id }}_COMM_{{ commune.insee }}.docx
+
+
+
+
+
+

+ Crée par Mon Diagnostic Artificialisation +

+ + + \ No newline at end of file diff --git a/project/urls.py b/project/urls.py index d99ff5456..1bb977da9 100644 --- a/project/urls.py +++ b/project/urls.py @@ -195,7 +195,6 @@ path("/export-excel", views.ExportExcelView.as_view(), name="export-excel"), # SUB APPS path("test", views.TestView.as_view(), name="test"), - path(route="rnu-packages-progress", view=views.RNUPackagesProgressView.as_view(), name="rnu-packages-progress"), ] diff --git a/project/views/RNUPackagesProgressView.py b/project/views/RNUPackagesProgressView.py deleted file mode 100644 index dca525834..000000000 --- a/project/views/RNUPackagesProgressView.py +++ /dev/null @@ -1,92 +0,0 @@ -from django.db.models import F, IntegerField, Sum -from django.db.models.functions import Cast -from django.utils import timezone -from rest_framework.response import Response -from rest_framework.views import APIView - -from project.models import Project -from public_data.models import Commune, Departement -from public_data.models.sudocuh import DocumentUrbanismeChoices, Sudocuh -from users.models import User - - -class RNUPackagesProgressView(APIView): - def get(self, request): - diagnostic_to_create = Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU) - from_commune_table = Commune.objects.filter( - insee__in=diagnostic_to_create.values("code_insee"), - ) - of_those_with_ocsge = from_commune_table.filter( - ocsge_available=True, - ) - of_those_with_ocsge_count = of_those_with_ocsge.count() - diagnostic_to_create_count = diagnostic_to_create.count() - mda_user = User.objects.get(email="rnu.package@mondiagartif.beta.gouv.fr") - - diagnostic_created = Project.objects.filter( - user=mda_user, - ) - diagnostic_created_count = diagnostic_created.count() - - async_fields = [ - "async_add_city_done", - "async_set_combined_emprise_done", - "async_cover_image_done", - "async_find_first_and_last_ocsge_done", - "async_add_comparison_lands_done", - "async_ocsge_coverage_status_done", - ] - - async_fields_with_ocsge = [ - "async_theme_map_understand_artif_done", - ] - - aggregate_results = [] - - for field in async_fields: - aggregate_results.append( - diagnostic_created.aggregate( - **{ - f"{field}_count": Sum(Cast(field, IntegerField())), - f"{field}_percentage": F(f"{field}_count") * 100.0 / diagnostic_to_create_count, - } - ) - ) - for field in async_fields_with_ocsge: - aggregate_results.append( - diagnostic_created.aggregate( - **{ - f"{field}_count": Sum(Cast(field, IntegerField())), - f"{field}_percentage": F(f"{field}_count") * 100.0 / of_those_with_ocsge_count, - } - ) - ) - - time_diff = timezone.now() - mda_user.date_joined - hours = time_diff.seconds // 3600 - minutes = (time_diff.seconds % 3600) // 60 - seconds = time_diff.seconds % 60 - - response_data = { - "elapsed_time": f"{hours}h {minutes}m {seconds}s", - "diagnostic_to_create_count": diagnostic_to_create_count, - "of_those_with_ocsge_count": of_those_with_ocsge_count, - "diagnostic_created_count": diagnostic_created_count, - "diangostic_created_percentage": f"{diagnostic_created_count / diagnostic_to_create_count * 100}%", - "async_operations_progress": aggregate_results, - } - - for departement in Departement.objects.all(): - response_data[f"department_{departement.source_id}"] = { - "diagnostic_to_create_count": diagnostic_to_create.filter( - code_insee__startswith=departement.source_id - ).count(), - "of_those_with_ocsge_count": of_those_with_ocsge.filter(departement=departement).count(), - "diagnostic_created_count": diagnostic_created.annotate( - land_id_as_int=Cast("land_id", output_field=IntegerField()) - ) - .filter(land_id_as_int__in=from_commune_table.filter(departement=departement).values("id")) - .count(), - } - - return Response(response_data) diff --git a/project/views/__init__.py b/project/views/__init__.py index 4c4c70168..69faad1a0 100644 --- a/project/views/__init__.py +++ b/project/views/__init__.py @@ -4,4 +4,3 @@ from .export import * from .map import * from .report import * -from .RNUPackagesProgressView import RNUPackagesProgressView diff --git a/public_data/management/commands/create_rnu_diagnostics.py b/public_data/management/commands/create_rnu_diagnostics.py index 950e032fd..78eafbaff 100644 --- a/public_data/management/commands/create_rnu_diagnostics.py +++ b/public_data/management/commands/create_rnu_diagnostics.py @@ -17,7 +17,7 @@ class Command(BaseCommand): help = "create_rnu_diagnostics" def add_arguments(self, parser): - parser.add_argument("--departement", type=str, required=True) + parser.add_argument("--departement", type=str) def handle(self, *args, **options): mondiagartif_user, _ = User.objects.get_or_create( @@ -32,10 +32,12 @@ def handle(self, *args, **options): projects = [] communes = Commune.objects.filter( - departement__source_id=options["departement"], insee__in=[Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU).values("code_insee")], ) + if options["departement"]: + communes = communes.filter(departement__source_id=options["departement"]) + logger.info(f"Found {len(communes)} RNU communes") projects_to_delete = Project.objects.filter( diff --git a/public_data/management/commands/create_rnu_packages.py b/public_data/management/commands/create_rnu_packages.py index 42b3d45d2..133ca3049 100644 --- a/public_data/management/commands/create_rnu_packages.py +++ b/public_data/management/commands/create_rnu_packages.py @@ -7,6 +7,7 @@ from project.tasks import create_zip_departement_rnu_package_one_off from public_data.models import Commune, Departement, Sudocuh from public_data.models.sudocuh import DocumentUrbanismeChoices +from users.models import User logger = logging.getLogger("management.commands") @@ -17,29 +18,21 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("--departement", type=str, required=False) - def handle(self, *args, **options): - tasks = [] - - departements = Departement.objects.all() - - if options["departement"]: - departements = Departement.objects.filter(source_id=options["departement"]) - - communes = Commune.objects.all() - - if options["departement"]: - communes = Commune.objects.filter( - departement__source_id=options["departement"], - insee__in=[Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU).values("code_insee")], - ) + def check_requests_are_created_for_departement(self, departement): + communes = Commune.objects.filter( + insee__in=[Sudocuh.objects.filter(du_opposable=DocumentUrbanismeChoices.RNU).values("code_insee")], + departement=departement, + ) commune_count = communes.count() request_for_communes_count = Request.objects.filter( - done=True, project__land_id__in=[str(commune.id) for commune in communes] + done=True, + project__land_id__in=[str(commune.id) for commune in communes], + user=User.objects.get(email="rnu.package@mondiagartif.beta.gouv.fr"), ).count() if commune_count != request_for_communes_count: - raise ValueError( + logger.error( ( f"Commune count ({commune_count}) does not match " f"request for communes count ({request_for_communes_count})" @@ -50,8 +43,19 @@ def handle(self, *args, **options): f"Commune count ({commune_count}) matches request for communes count ({request_for_communes_count})" ) + return True + + def handle(self, *args, **options): + tasks = [] + + departements = Departement.objects.all() + + if options["departement"]: + departements = departements.filter(source_id=options["departement"]) + for departement in departements: - logger.info(f"Creating RNU package for departement {departement.source_id}") - tasks.append(create_zip_departement_rnu_package_one_off.si(departement.source_id)) + if self.check_requests_are_created_for_departement(departement): + logger.info(f"Creating RNU package for departement {departement.source_id}") + tasks.append(create_zip_departement_rnu_package_one_off.si(departement.source_id)) celery.group(*tasks).apply_async(queue="long") diff --git a/users/templates/users/signin.html b/users/templates/users/signin.html index a584df398..28c6dda0e 100644 --- a/users/templates/users/signin.html +++ b/users/templates/users/signin.html @@ -8,8 +8,28 @@
- +

Connexion

+ {% if next == '/telechargements' %} +
+
+
+
+

+ La page de téléchargement de paquet de rapports locaux est uniquement accessible aux utilisateurs enregistrés et connectés. +

+
+

+ + Vous pouvez vous connecter ci-dessous, ou créer un compte. + +

+
+
+
+
+ {% endif %} +
{% csrf_token %} @@ -19,7 +39,7 @@

Connexion

Mot de pass oublié ? Réinitialisez le. -
Pas encore de compte ? Inscrivez-vous. +
Pas encore de compte ? Inscrivez-vous.

From 75dddb7ca0056f99554cc6bcda85ffaf6368b47c Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Thu, 18 Jul 2024 11:47:22 +0200 Subject: [PATCH 15/17] fix(create_rnu_packages): check_requests_are_created_for_departement boolean return --- public_data/management/commands/create_rnu_packages.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public_data/management/commands/create_rnu_packages.py b/public_data/management/commands/create_rnu_packages.py index 133ca3049..e409d0924 100644 --- a/public_data/management/commands/create_rnu_packages.py +++ b/public_data/management/commands/create_rnu_packages.py @@ -38,12 +38,13 @@ def check_requests_are_created_for_departement(self, departement): f"request for communes count ({request_for_communes_count})" ) ) + return False else: logger.info( f"Commune count ({commune_count}) matches request for communes count ({request_for_communes_count})" ) - return True + return True def handle(self, *args, **options): tasks = [] From a803d764a034e5f8f0d8568a05c1d40a3dbe3136 Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Thu, 18 Jul 2024 14:10:36 +0200 Subject: [PATCH 16/17] feat(users): add updated_at and created_at fields --- .../0012_user_created_at_user_updated_at.py | 22 +++++++++++++++++++ users/migrations/0013_auto_20240718_1408.py | 16 ++++++++++++++ users/models.py | 3 +++ 3 files changed, 41 insertions(+) create mode 100644 users/migrations/0012_user_created_at_user_updated_at.py create mode 100644 users/migrations/0013_auto_20240718_1408.py diff --git a/users/migrations/0012_user_created_at_user_updated_at.py b/users/migrations/0012_user_created_at_user_updated_at.py new file mode 100644 index 000000000..29bd76714 --- /dev/null +++ b/users/migrations/0012_user_created_at_user_updated_at.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.13 on 2024-07-18 12:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0011_alter_user_organism"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="created_at", + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name="Créé le"), + ), + migrations.AddField( + model_name="user", + name="updated_at", + field=models.DateTimeField(auto_now=True, null=True, verbose_name="Mis à jour le"), + ), + ] diff --git a/users/migrations/0013_auto_20240718_1408.py b/users/migrations/0013_auto_20240718_1408.py new file mode 100644 index 000000000..a186bc99d --- /dev/null +++ b/users/migrations/0013_auto_20240718_1408.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.13 on 2024-07-18 12:08 + +from django.db import migrations + + +def set_created_and_update_at_to_none(apps, schema_editor): + User = apps.get_model("users", "User") + User.objects.update(created_at=None, updated_at=None) + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0012_user_created_at_user_updated_at"), + ] + + operations = [migrations.RunPython(set_created_and_update_at_to_none)] diff --git a/users/models.py b/users/models.py index e3e84c689..4f832e16f 100644 --- a/users/models.py +++ b/users/models.py @@ -39,6 +39,9 @@ class ORGANISMS(models.TextChoices): USERNAME_FIELD = "email" REQUIRED_FIELDS: List[str] = [] + created_at = models.DateTimeField("Créé le", auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField("Mis à jour le", auto_now=True, blank=True, null=True) + objects = UserManager() @staticmethod From 9e063e5fca1ba2ea00c258ea3a32f7e09c27e880 Mon Sep 17 00:00:00 2001 From: "Alexis A." Date: Thu, 18 Jul 2024 14:44:44 +0200 Subject: [PATCH 17/17] feat(RNUPackageRequest): add model --- home/templates/home/download.html | 2 +- home/urls.py | 1 + home/views.py | 19 +++++++++- project/admin.py | 19 +++++++++- project/migrations/0089_rnupackagerequest.py | 38 ++++++++++++++++++++ project/models/RNUPackageRequest.py | 13 +++++++ project/models/__init__.py | 2 ++ users/models.py | 5 +++ 8 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 project/migrations/0089_rnupackagerequest.py create mode 100644 project/models/RNUPackageRequest.py diff --git a/home/templates/home/download.html b/home/templates/home/download.html index 349243d60..054a3e072 100644 --- a/home/templates/home/download.html +++ b/home/templates/home/download.html @@ -41,7 +41,7 @@

Trames de rapport triennal local des communes au RNU

{{ package.departement_official_id }} - {{ package.departement.name }} {{ package.communes|length }} {{ package.created_at }} - Lien de téléchargement + Lien de téléchargement {% endfor %} diff --git a/home/urls.py b/home/urls.py index fec6bc307..192278cdd 100644 --- a/home/urls.py +++ b/home/urls.py @@ -9,6 +9,7 @@ path("", views.HomeView.as_view(), name="home"), path("rapport-local", views.HomeRapportLocalView.as_view(), name="home_rapport_local"), path("telechargements", views.DownloadView.as_view(), name="downloads"), + path("telechargements/rnu-package/", views.download_package_request, name="download_rnu_package"), path("mentions-legales", views.LegalNoticeView.as_view(), name="cgv"), path("confidentialité", views.PrivacyView.as_view(), name="privacy"), path("test", views.TestView.as_view(), name="test"), diff --git a/home/views.py b/home/views.py index 624bc48e6..8cc937117 100644 --- a/home/views.py +++ b/home/views.py @@ -3,6 +3,7 @@ from django.conf import settings from django.contrib import messages +from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.db.models import F, Value from django.http import HttpRequest, HttpResponse, HttpResponseGone @@ -12,7 +13,7 @@ from django_app_parameter import app_parameter from brevo.connectors import Brevo -from project.models import Request, RNUPackage +from project.models import Request, RNUPackage, RNUPackageRequest from users.models import User from utils.functions import get_url_with_domain from utils.htmx import HtmxRedirectMixin, StandAloneMixin @@ -46,6 +47,22 @@ def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) +@login_required +def download_package_request(request: Request, departement: str) -> HttpResponse: + rnu_package = RNUPackage.objects.get(departement_official_id=departement) + user: User = request.user + RNUPackageRequest.objects.create( + user=request.user, + rnu_package=rnu_package, + departement_official_id=rnu_package.departement_official_id, + email=user.email, + requested_at=timezone.now(), + requested_diagnostics_before_package_request=user.request_set.filter(done=True).count(), + account_created_for_package=user.created_today, + ) + return redirect(rnu_package.file.url) + + class HomeRapportLocalView(BreadCrumbMixin, TemplateView): template_name = "home/home_rapport_local.html" diff --git a/project/admin.py b/project/admin.py index abb81919b..8b4a80503 100644 --- a/project/admin.py +++ b/project/admin.py @@ -6,7 +6,13 @@ from simple_history.admin import SimpleHistoryAdmin from project import tasks -from project.models import ErrorTracking, Project, Request, RNUPackage +from project.models import ( + ErrorTracking, + Project, + Request, + RNUPackage, + RNUPackageRequest, +) from project.models.exceptions import TooOldException @@ -258,3 +264,14 @@ class RNUPackageAdmin(admin.ModelAdmin): "created_at", "updated_at", ) + + +@admin.register(RNUPackageRequest) +class RNUPackageRequestAdmin(admin.ModelAdmin): + model = RNUPackageRequest + list_display = ( + "user", + "rnu_package", + "departement_official_id", + ) + search_fields = ("email",) diff --git a/project/migrations/0089_rnupackagerequest.py b/project/migrations/0089_rnupackagerequest.py new file mode 100644 index 000000000..2b02e08eb --- /dev/null +++ b/project/migrations/0089_rnupackagerequest.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.13 on 2024-07-18 12:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("project", "0088_alter_rnupackage_file"), + ] + + operations = [ + migrations.CreateModel( + name="RNUPackageRequest", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("departement_official_id", models.CharField(max_length=10)), + ("email", models.EmailField(max_length=254)), + ("requested_at", models.DateTimeField(auto_now_add=True)), + ("requested_diagnostics_before_package_request", models.IntegerField()), + ("account_created_for_package", models.BooleanField()), + ( + "rnu_package", + models.ForeignKey( + null=True, on_delete=django.db.models.deletion.SET_NULL, to="project.rnupackage" + ), + ), + ( + "user", + models.ForeignKey( + null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL + ), + ), + ], + ), + ] diff --git a/project/models/RNUPackageRequest.py b/project/models/RNUPackageRequest.py new file mode 100644 index 000000000..961ee0b7a --- /dev/null +++ b/project/models/RNUPackageRequest.py @@ -0,0 +1,13 @@ +from django.db import models + +from .RNUPackage import RNUPackage + + +class RNUPackageRequest(models.Model): + rnu_package = models.ForeignKey(RNUPackage, on_delete=models.SET_NULL, null=True) + departement_official_id = models.CharField(max_length=10) + user = models.ForeignKey("users.User", on_delete=models.SET_NULL, null=True) + email = models.EmailField() + requested_at = models.DateTimeField(auto_now_add=True) + requested_diagnostics_before_package_request = models.IntegerField() + account_created_for_package = models.BooleanField() diff --git a/project/models/__init__.py b/project/models/__init__.py index f372d53b0..af3a485f7 100644 --- a/project/models/__init__.py +++ b/project/models/__init__.py @@ -9,12 +9,14 @@ "user_directory_path", "RequestedDocumentChoices", "RNUPackage", + "RNUPackageRequest", ] from .project_base import Emprise, Project, ProjectCommune from .request import ErrorTracking, Request, RequestedDocumentChoices from .RNUPackage import RNUPackage +from .RNUPackageRequest import RNUPackageRequest from .utils import user_directory_path # isort: split diff --git a/users/models.py b/users/models.py index 4f832e16f..999c66557 100644 --- a/users/models.py +++ b/users/models.py @@ -2,6 +2,7 @@ from django.contrib.auth.models import AbstractUser from django.db import models +from django.utils import timezone from .managers import UserManager @@ -75,6 +76,10 @@ def greetings(self): def __str__(self): return self.email + @property + def created_today(self): + return self.created_at.date() == timezone.now().date() + def save(self, *args, **kwargs): self.organism_group = User.get_group(self.organism) super().save(*args, **kwargs)