diff --git a/config/settings.py b/config/settings.py index 91c3751a5..20a077355 100644 --- a/config/settings.py +++ b/config/settings.py @@ -22,7 +22,7 @@ from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.redis import RedisIntegration -OFFICIAL_VERSION = "7.3" +OFFICIAL_VERSION = "7.4" root = environ.Path(__file__) - 2 # get root of the project diff --git a/home/templates/home/download.html b/home/templates/home/download.html new file mode 100644 index 000000000..054a3e072 --- /dev/null +++ b/home/templates/home/download.html @@ -0,0 +1,64 @@ +{% extends "index.html" %} + +{% load static %} +{% load sri %} + +{% block pagetitle %}Téléchargements{% 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 %} +
+
+

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" %} +{% endblock content %} + +{% block tagging %} + +{% endblock tagging %} diff --git a/home/templates/home/home.html b/home/templates/home/home.html index 1afbbbfde..bac8712c7 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 au RNU.

diff --git a/home/templates/home/home_rapport_local.html b/home/templates/home/home_rapport_local.html index 4a1b6c16d..4eb67767e 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 au RNU. +

+
+
+
{% include "project/partials/search.html" %}
diff --git a/home/urls.py b/home/urls.py index bbde82466..192278cdd 100644 --- a/home/urls.py +++ b/home/urls.py @@ -8,6 +8,8 @@ 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="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 e4fec7aad..8cc937117 100644 --- a/home/views.py +++ b/home/views.py @@ -3,7 +3,8 @@ from django.conf import settings from django.contrib import messages -from django.contrib.auth.mixins import UserPassesTestMixin +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 from django.shortcuts import redirect @@ -12,7 +13,7 @@ from django_app_parameter import app_parameter from brevo.connectors import Brevo -from project.models import Request +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 @@ -35,6 +36,33 @@ def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) +class DownloadView(LoginRequiredMixin, BreadCrumbMixin, TemplateView): + template_name = "home/download.html" + + def get_context_data(self, **kwargs): + kwargs |= { + "form": NewsletterForm(), + "rnu_packages": RNUPackage.objects.all().order_by("departement_official_id"), + } + 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/package-lock.json b/package-lock.json index 06c673846..9224fac92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sparte", - "version": "7.3", + "version": "7.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sparte", - "version": "7.3", + "version": "7.4", "dependencies": { "@gouvfr/dsfr": "^1.8.5", "@reduxjs/toolkit": "^2.2.1", diff --git a/package.json b/package.json index a0c10251c..5c91bb1ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sparte", - "version": "7.3", + "version": "7.4", "private": true, "scripts": { "dev": "webpack --watch --mode development", diff --git a/project/admin.py b/project/admin.py index d87f9b3d4..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 +from project.models import ( + ErrorTracking, + Project, + Request, + RNUPackage, + RNUPackageRequest, +) from project.models.exceptions import TooOldException @@ -243,3 +249,29 @@ 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", + ) + + +@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/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/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/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/RNUPackage.py b/project/models/RNUPackage.py new file mode 100644 index 000000000..b86b969eb --- /dev/null +++ b/project/models/RNUPackage.py @@ -0,0 +1,27 @@ +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): + 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, + 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/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 53beade84..af3a485f7 100644 --- a/project/models/__init__.py +++ b/project/models/__init__.py @@ -8,11 +8,15 @@ "trigger_async_tasks", "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/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..f52e6462c 100644 --- a/project/tasks/project.py +++ b/project/tasks/project.py @@ -1,5 +1,7 @@ import io import logging +import os +import zipfile from datetime import timedelta from typing import Any, Dict, Literal @@ -17,10 +19,17 @@ from matplotlib.lines import Line2D from matplotlib_scalebar.scalebar import ScaleBar -from project.models import Emprise, Project, Request, RequestedDocumentChoices +from project.models import ( + Emprise, + Project, + Request, + RequestedDocumentChoices, + RNUPackage, +) from public_data.domain.containers import PublicDataContainer -from public_data.models import AdminRef, ArtificialArea, Land, OcsgeDiff +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 @@ -284,6 +293,57 @@ class WordAlreadySentException(Exception): pass +@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, + ) + + 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) + req.done = True + req.save(update_fields=["done"]) + logger.info("Word created and saved") + return request_id + + except Exception as exc: + req.record_exception(exc) + logger.error("Error while generating word: %s", exc) + logger.exception(exc) + self.retry(exc=exc, countdown=10) + + @shared_task(bind=True, max_retries=6, queue="long") def generate_word_diagnostic(self, request_id) -> int: from diagnostic_word.renderers import ( @@ -818,3 +878,51 @@ 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) + 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__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, 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") + + file_name_in_zip = f"{departement_id}_COMM_{request.project.land.official_id}.docx" + zipf.writestr(file_name_in_zip, request.sent_file.read()) + + 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, + 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/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 }})

+ +

+ 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/public_data/management/commands/create_rnu_diagnostics.py b/public_data/management/commands/create_rnu_diagnostics.py new file mode 100644 index 000000000..78eafbaff --- /dev/null +++ b/public_data/management/commands/create_rnu_diagnostics.py @@ -0,0 +1,109 @@ +import logging + +from django.core.management.base import BaseCommand +from django.utils import timezone + +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 +from users.models import User +from utils.db import fix_poly + +logger = logging.getLogger("management.commands") + + +class Command(BaseCommand): + help = "create_rnu_diagnostics" + + def add_arguments(self, parser): + parser.add_argument("--departement", type=str) + + 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": timezone.now()}, + ) + + projects = [] + + communes = Commune.objects.filter( + 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( + 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(commune.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, + 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, + async_theme_map_gpu_done=True, + async_theme_map_fill_gpu_done=True, + async_add_comparison_lands_done=True, + ) + + project.cities.add(commune) + + Emprise.objects.create( + mpoly=fix_poly(commune.mpoly), + srid_source=commune.srid_source, + project=project, + ) + + similar_lands_public_keys = [ + comparison_land.public_key for comparison_land in project.get_comparison_lands() + ] + + project.refresh_from_db() + + project.add_look_a_like(public_key=similar_lands_public_keys, many=True) + + 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 new file mode 100644 index 000000000..e409d0924 --- /dev/null +++ b/public_data/management/commands/create_rnu_packages.py @@ -0,0 +1,62 @@ +import logging + +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 Commune, Departement, Sudocuh +from public_data.models.sudocuh import DocumentUrbanismeChoices +from users.models import User + +logger = logging.getLogger("management.commands") + + +class Command(BaseCommand): + help = "create_rnu_packages" + + def add_arguments(self, parser): + parser.add_argument("--departement", type=str, required=False) + + 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], + user=User.objects.get(email="rnu.package@mondiagartif.beta.gouv.fr"), + ).count() + + if commune_count != request_for_communes_count: + logger.error( + ( + f"Commune count ({commune_count}) does not match " + 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 + + def handle(self, *args, **options): + tasks = [] + + departements = Departement.objects.all() + + if options["departement"]: + departements = departements.filter(source_id=options["departement"]) + + for departement in departements: + 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/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..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 @@ -39,6 +40,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 @@ -72,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) 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.