diff --git a/envergo/pages/templatetags/utils.py b/envergo/pages/templatetags/utils.py index e7f72e3dd..9e70daac0 100644 --- a/envergo/pages/templatetags/utils.py +++ b/envergo/pages/templatetags/utils.py @@ -271,3 +271,21 @@ def querystring(context, *args, **kwargs): params[key] = value query_string = params.urlencode() if params else "" return f"?{query_string}" + + +@register.inclusion_tag("_truncated_comment.html") +def truncated_comment(text, uid, limit=50): + """ + Display a truncated comment with DSFR-compatible expand/collapse. + """ + if not text: + return {"text": None} + + return { + "text": text, + "limit": limit, + "uid": uid, + "is_truncated": len(text) > limit, + "head": text[:limit], + "tail": text[limit:], + } diff --git a/envergo/petitions/forms.py b/envergo/petitions/forms.py index 4a5a7cd75..f474ee96c 100644 --- a/envergo/petitions/forms.py +++ b/envergo/petitions/forms.py @@ -221,7 +221,7 @@ def request_for_info_message(): class RequestAdditionalInfoForm(forms.Form): """Let an instructor pause the instruction and request for more information.""" - response_due_date = forms.DateField( + due_date = forms.DateField( label="Date limite de réponse du demandeur", required=True, initial=three_months_from_now, diff --git a/envergo/petitions/migrations/0029_statuslog_resumed_by_and_more.py b/envergo/petitions/migrations/0029_statuslog_resumed_by_and_more.py new file mode 100644 index 000000000..016072606 --- /dev/null +++ b/envergo/petitions/migrations/0029_statuslog_resumed_by_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.23 on 2025-12-18 05:27 + +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), + ("petitions", "0028_alter_statuslog_options_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="statuslog", + name="resumed_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="resumed_logs", + to=settings.AUTH_USER_MODEL, + verbose_name="Auteur de la reprise de la procédure suite à la réception d'informations complémentaires", + ), + ), + ] diff --git a/envergo/petitions/migrations/0034_merge_20260107_0633.py b/envergo/petitions/migrations/0034_merge_20260107_0633.py new file mode 100644 index 000000000..c76b32cf7 --- /dev/null +++ b/envergo/petitions/migrations/0034_merge_20260107_0633.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.23 on 2026-01-07 05:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("petitions", "0029_statuslog_resumed_by_and_more"), + ("petitions", "0033_merge_20251211_1513"), + ] + + operations = [] diff --git a/envergo/petitions/migrations/0035_remove_statuslog_receipt_date_data_is_consistent_and_more.py b/envergo/petitions/migrations/0035_remove_statuslog_receipt_date_data_is_consistent_and_more.py new file mode 100644 index 000000000..f56926f53 --- /dev/null +++ b/envergo/petitions/migrations/0035_remove_statuslog_receipt_date_data_is_consistent_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.23 on 2026-01-07 05:34 + +from django.db import migrations, models + + +def fix_missing_resumed_by(apps, schema_editor): + """Set resumed_by for logs that have info_receipt_date but no resumed_by.""" + StatusLog = apps.get_model("petitions", "StatusLog") + User = apps.get_model("users", "User") + qs = StatusLog.objects.filter( + info_receipt_date__isnull=False, + resumed_by__isnull=True, + ) + if qs.count() == 0: + return + + user = User.objects.get(id=1) + qs.update(resumed_by=user) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("petitions", "0034_merge_20260107_0633"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="statuslog", + name="receipt_date_data_is_consistent", + ), + migrations.RunPython(fix_missing_resumed_by, migrations.RunPython.noop), + migrations.AddConstraint( + model_name="statuslog", + constraint=models.CheckConstraint( + check=models.Q( + models.Q( + ("info_receipt_date__isnull", True), + ("resumed_by__isnull", True), + ), + models.Q( + ("info_receipt_date__isnull", False), + ("resumed_by__isnull", False), + ("suspension_date__isnull", False), + ("response_due_date__isnull", False), + ), + _connector="OR", + ), + name="receipt_date_data_is_consistent", + ), + ), + ] diff --git a/envergo/petitions/migrations/0036_remove_statuslog_suspension_data_is_consistent_and_more.py b/envergo/petitions/migrations/0036_remove_statuslog_suspension_data_is_consistent_and_more.py new file mode 100644 index 000000000..08f66782f --- /dev/null +++ b/envergo/petitions/migrations/0036_remove_statuslog_suspension_data_is_consistent_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.27 on 2026-01-14 10:35 + +from django.db import migrations, models + +class Migration(migrations.Migration): + + dependencies = [ + ("petitions", "0035_remove_statuslog_receipt_date_data_is_consistent_and_more"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="statuslog", + name="suspension_data_is_consistent", + ), + migrations.RemoveConstraint( + model_name="statuslog", + name="receipt_date_data_is_consistent", + ), + migrations.AddField( + model_name="statuslog", + name="type", + field=models.CharField( + choices=[ + ("status_change", "Changement d'état"), + ("suspension", "Demande de compléments"), + ("resumption", "Compléments reçus"), + ], + default="status_change", + max_length=20, + verbose_name="Type de log", + ), + ), + migrations.AlterField( + model_name="petitionproject", + name="latest_petitioner_msg", + field=models.DateTimeField( + blank=True, + default=None, + null=True, + verbose_name="Date du dernier message pétitionnaire", + ), + ), + ] diff --git a/envergo/petitions/migrations/0037_split_logs.py b/envergo/petitions/migrations/0037_split_logs.py new file mode 100644 index 000000000..a021b2d86 --- /dev/null +++ b/envergo/petitions/migrations/0037_split_logs.py @@ -0,0 +1,62 @@ +# Generated by Django 4.2.27 on 2026-01-14 10:48 +from datetime import datetime, time + +from django.db import migrations +from django.utils import timezone + + +def split_logs(apps, schema_editor): + StatusLog = apps.get_model("petitions", "StatusLog") + suspended_qs = StatusLog.objects.filter(suspension_date__isnull=False) + for suspended in suspended_qs: + StatusLog.objects.create( + petition_project=suspended.petition_project, + type='suspension', + response_due_date=suspended.response_due_date, + original_due_date=suspended.original_due_date, + created_by=suspended.suspended_by, + update_comment="Suspension de l’instruction, message envoyé au demandeur.", + created_at=timezone.make_aware( + datetime.combine(suspended.suspension_date, time(hour=12)), + timezone.get_current_timezone(), + ), + ) + suspended.suspension_date = None + suspended.response_due_date = None + suspended.original_due_date = None + suspended.save() + + resumed_qs = StatusLog.objects.filter(info_receipt_date__isnull=False) + for resumed in resumed_qs: + interruption_days = 0 + if resumed.info_receipt_date and resumed.suspension_date : + interruption_days = resumed.info_receipt_date - resumed.suspension_date + if resumed.original_due_date: + new_due_date = resumed.original_due_date + interruption_days + else: + new_due_date = None + + StatusLog.objects.create( + petition_project=resumed.petition_project, + type='resumption', + info_receipt_date=resumed.info_receipt_date, + due_date=new_due_date, + created_by=resumed.resumed_by, + update_comment="Reprise de l’instruction, date d'échéance ajustée.", + created_at=timezone.make_aware( + datetime.combine(resumed.info_receipt_date, time(hour=12)), + timezone.get_current_timezone(), + ), + ) + resumed.info_receipt_date = None + resumed.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("petitions", "0036_remove_statuslog_suspension_data_is_consistent_and_more"), + ] + + operations = [ + migrations.RunPython(split_logs, migrations.RunPython.noop), + ] diff --git a/envergo/petitions/migrations/0038_remove_statuslog_useless_fields.py b/envergo/petitions/migrations/0038_remove_statuslog_useless_fields.py new file mode 100644 index 000000000..07b06266f --- /dev/null +++ b/envergo/petitions/migrations/0038_remove_statuslog_useless_fields.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.27 on 2026-01-14 10:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("petitions", "0037_split_logs"), + ] + + operations = [ + migrations.RemoveField( + model_name="statuslog", + name="resumed_by", + ), + migrations.RemoveField( + model_name="statuslog", + name="suspended_by", + ), + migrations.RemoveField( + model_name="statuslog", + name="suspension_date", + ), + migrations.AddConstraint( + model_name="statuslog", + constraint=models.CheckConstraint( + check=models.Q( + models.Q( + ("type", "suspension"), ("response_due_date__isnull", False) + ), + models.Q( + ("type", "status_change"), + ("response_due_date__isnull", True), + ("original_due_date__isnull", True), + ("info_receipt_date__isnull", True), + ), + models.Q( + ("type", "resumption"), ("info_receipt_date__isnull", False) + ), + _connector="OR", + ), + name="suspension_data_is_consistent", + ), + ),] diff --git a/envergo/petitions/migrations/0039_remove_statuslog_response_due_date.py b/envergo/petitions/migrations/0039_remove_statuslog_response_due_date.py new file mode 100644 index 000000000..23e17d38e --- /dev/null +++ b/envergo/petitions/migrations/0039_remove_statuslog_response_due_date.py @@ -0,0 +1,48 @@ +# Generated by Django 4.2.27 on 2026-01-19 10:38 + +from django.db import migrations, models + + +def cp_due_date(apps, schema_editor): + StatusLog = apps.get_model("petitions", "StatusLog") + suspension_qs = StatusLog.objects.filter(type="suspension") + for suspension in suspension_qs: + suspension.due_date = suspension.response_due_date + suspension.save() + +class Migration(migrations.Migration): + + dependencies = [ + ("petitions", "0038_remove_statuslog_useless_fields"), + ] + + operations = [ + + migrations.RunPython(cp_due_date, migrations.RunPython.noop), + migrations.RemoveConstraint( + model_name="statuslog", + name="suspension_data_is_consistent", + ), + migrations.AddConstraint( + model_name="statuslog", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("type", "suspension"), ("due_date__isnull", False)), + models.Q( + ("type", "status_change"), + ("original_due_date__isnull", True), + ("info_receipt_date__isnull", True), + ), + models.Q( + ("type", "resumption"), ("info_receipt_date__isnull", False) + ), + _connector="OR", + ), + name="suspension_data_is_consistent", + ), + ), + migrations.RemoveField( + model_name="statuslog", + name="response_due_date", + ), + ] diff --git a/envergo/petitions/models.py b/envergo/petitions/models.py index efc0481bc..6327b83fd 100644 --- a/envergo/petitions/models.py +++ b/envergo/petitions/models.py @@ -60,6 +60,12 @@ ("dropped", "Classé sans suite"), ) +LOG_TYPES = Choices( + ("status_change", "Changement d'état"), + ("suspension", "Demande de compléments"), + ("resumption", "Compléments reçus"), +) + # This session key is used when we are not able to find the real user session key. SESSION_KEY = "untracked_dossier_submission" @@ -172,9 +178,18 @@ def save(self, *args, **kwargs): @cached_property def current_status(self): - # Make sure the `status_history` is prefetched with the correct ordering - log = self.status_history.first() - return log + """Get the latest status_change log.""" + return self.status_history.filter(type=LOG_TYPES.status_change).first() + + @cached_property + def latest_suspension(self): + """Get the latest suspension log.""" + return self.status_history.filter(type=LOG_TYPES.suspension).first() + + @cached_property + def latest_resumption(self): + """Get the latest resumption log.""" + return self.status_history.filter(type=LOG_TYPES.resumption).first() @property def current_stage(self): @@ -188,11 +203,33 @@ def current_decision(self): @property def due_date(self): - return self.current_status.due_date if self.current_status else None + if self.is_paused: + return self.latest_suspension.due_date + status = self.current_status + resumption = self.latest_resumption + if resumption and status: + return ( + resumption.due_date + if resumption.created_at > status.created_at + else status.due_date + ) + elif status: + return status.due_date + elif resumption: + return resumption.due_date + else: + return None @property def is_paused(self): - return self.current_status.is_paused if self.current_status else False + """Check if there's an unresolved suspension.""" + suspension = self.latest_suspension + if not suspension: + return False + resumption = self.latest_resumption + if not resumption: + return True + return suspension.created_at > resumption.created_at def get_department_code(self): """Get department from moulinette url""" @@ -586,23 +623,27 @@ def is_valid(self, user): # Some data constraints checks # Check that all request for info suspension data is set -q_suspended = Q(suspension_date__isnull=False) & Q(response_due_date__isnull=False) +q_suspended = Q(type=LOG_TYPES.suspension) & Q(due_date__isnull=False) # Check that no single field is set q_not_suspended = ( - Q(suspension_date__isnull=True) - & Q(response_due_date__isnull=True) + Q(type=LOG_TYPES.status_change) & Q(original_due_date__isnull=True) + & Q(info_receipt_date__isnull=True) ) -# Check that the receipt date is only set if the project was suspended -q_receipt_date = Q(info_receipt_date__isnull=True) | ( - Q(info_receipt_date__isnull=False) & q_suspended -) +# Check that the receipt date is only set if the project is resumed +q_resumed = Q(type=LOG_TYPES.resumption) & Q(info_receipt_date__isnull=False) class StatusLog(models.Model): - """A petition project status (stage + decision) change log entry.""" + """A petition project status log entry. + + Each entry represents one of: + - status_change: A change in stage and/or decision + - suspension: A request for additional information (pauses the process) + - resumption: Receipt of additional information (resumes the process) + """ petition_project = models.ForeignKey( PetitionProject, @@ -610,6 +651,12 @@ class StatusLog(models.Model): related_name="status_history", verbose_name="Projet", ) + type = models.CharField( + "Type de log", + max_length=20, + choices=LOG_TYPES, + default=LOG_TYPES.status_change, + ) created_by = models.ForeignKey( "users.User", on_delete=models.SET_NULL, @@ -646,26 +693,9 @@ class StatusLog(models.Model): ) # "Request for additional information" related fields - suspension_date = models.DateField( - "Date de suspension pour demande d'information complémentaire", - null=True, - blank=True, - ) - response_due_date = models.DateField( - "Échéance pour l'envoi de pièces complémentaires", - null=True, - blank=True, - ) original_due_date = models.DateField( "Date de prochaine échéance avant suspension", null=True, blank=True ) - suspended_by = models.ForeignKey( - "users.User", - related_name="suspended_logs", - on_delete=models.SET_NULL, - verbose_name="Auteur de la demande d'informations complémentaires", - null=True, - ) info_receipt_date = models.DateField( "Date de réception des pièces complémentaires", null=True, blank=True ) @@ -684,21 +714,12 @@ class Meta: name="forbid_closed_with_unset_decision", ), models.CheckConstraint( - check=q_suspended | q_not_suspended, + check=q_suspended | q_not_suspended | q_resumed, name="suspension_data_is_consistent", ), - models.CheckConstraint( - check=q_receipt_date, name="receipt_date_data_is_consistent" - ), ] ordering = ["-created_at"] - @property - def is_paused(self): - """Are we currently waiting for additional info?""" - - return self.suspension_date is not None and self.info_receipt_date is None - @property def is_closed(self): return self.stage == STAGES.closed diff --git a/envergo/petitions/templatetags/petitions.py b/envergo/petitions/templatetags/petitions.py index 160e09295..b70c716b7 100644 --- a/envergo/petitions/templatetags/petitions.py +++ b/envergo/petitions/templatetags/petitions.py @@ -240,8 +240,8 @@ def display_due_date(due_date, display_days_left=True, self_explanatory_label=Fa @register.simple_tag -def display_pause(response_due_date): - days_left = (response_due_date - date.today()).days +def display_pause(due_date): + days_left = (due_date - date.today()).days if days_left >= 7: icon_class = "" elif days_left >= 0: @@ -286,3 +286,16 @@ def display_ds_field(context, field_name): def has_edit_permission(user, project): """Check if the user can edit the project.""" return project.has_change_permission(user) + + +@register.simple_tag +def created_by_display(log): + user = getattr(log, "created_by", None) + + if not user: + return "" + + if getattr(user, "is_staff", False): + return "Administrateur" + + return getattr(user, "email", "") diff --git a/envergo/petitions/tests/test_views.py b/envergo/petitions/tests/test_views.py index de5b2a6fe..551b85c70 100644 --- a/envergo/petitions/tests/test_views.py +++ b/envergo/petitions/tests/test_views.py @@ -1,4 +1,4 @@ -from datetime import date, timedelta +from datetime import date, datetime, time, timedelta from unittest.mock import ANY, Mock, patch import factory @@ -10,6 +10,7 @@ from django.test import RequestFactory, override_settings from django.urls import reverse from django.utils import timezone +from django.utils.functional import cached_property from envergo.analytics.models import Event from envergo.geodata.conftest import france_map, loire_atlantique_map # noqa @@ -23,6 +24,7 @@ ) from envergo.petitions.models import ( DOSSIER_STATES, + LOG_TYPES, InvitationToken, LatestMessagerieAccess, ) @@ -39,6 +41,7 @@ PetitionProject34Factory, PetitionProjectFactory, SimulationFactory, + StatusLogFactory, ) from envergo.petitions.views import ( PetitionProjectCreate, @@ -50,6 +53,14 @@ pytestmark = [pytest.mark.django_db, pytest.mark.urls("config.urls_haie")] +def clear_cached_properties(instance): + for attr in dir(instance): + if attr in instance.__dict__: + value = getattr(type(instance), attr, None) + if isinstance(value, cached_property): + del instance.__dict__[attr] + + @pytest.fixture(autouse=True) def fake_haie_settings(settings): settings.ENVERGO_HAIE_DOMAIN = "testserver" @@ -1346,7 +1357,7 @@ def test_petition_project_request_for_info( kwargs={"reference": project.reference}, ) form_data = { - "response_due_date": next_month, + "due_date": next_month, "request_message": "Test", } res = client.post(rai_url, form_data, follow=True) @@ -1354,10 +1365,11 @@ def test_petition_project_request_for_info( assert "Le message au demandeur a bien été envoyé." in res.content.decode() project.refresh_from_db() - project.current_status.refresh_from_db() + clear_cached_properties(project) assert project.is_paused is True - assert project.current_status.due_date == next_month - assert project.current_status.original_due_date == today + # Suspension fields are now on the suspension log, not current_status + assert project.latest_suspension.due_date == next_month + assert project.latest_suspension.original_due_date == today @pytest.mark.django_db(transaction=True) @@ -1375,16 +1387,42 @@ def test_petition_project_resume_instruction( next_month = today + timedelta(days=30) DCConfigHaieFactory() - project = PetitionProjectFactory( - status__suspension_date=last_month, - status__original_due_date=today, - status__due_date=next_month, - status__response_due_date=next_month, + project = PetitionProjectFactory(status__due_date=today) + # Create a suspension log separately + StatusLogFactory( + petition_project=project, + type=LOG_TYPES.suspension, + created_at=timezone.make_aware( + datetime.combine(last_month, time(hour=12)), + timezone.get_current_timezone(), + ), + original_due_date=today, + due_date=next_month, ) assert project.is_paused is True - assert project.due_date == next_month - # Request for additional info + # WHEN the user try to change step while paused + + status_url = reverse( + "petition_project_instructor_procedure_view", + kwargs={"reference": project.reference}, + ) + + data = { + "stage": "closed", + "decision": "dropped", + "update_comment": "aucun retour depuis 15 ans", + "status_date": "10/09/2025", + } + res = client.post(status_url, data, follow=True) + + # THEN this step is not authorized + assert res.status_code == 200 + project.refresh_from_db() + assert project.current_stage == "to_be_processed" + assert project.current_stage == "to_be_processed" + + # Resume instruction rai_url = reverse( "petition_project_instructor_request_info_view", kwargs={"reference": project.reference}, @@ -1397,9 +1435,10 @@ def test_petition_project_resume_instruction( assert "L'instruction du dossier a repris." in res.content.decode() project.refresh_from_db() - project.current_status.refresh_from_db() + clear_cached_properties(project) assert project.is_paused is False - assert project.current_status.due_date == next_month + # The new due_date is computed on the resumption log + assert project.due_date == next_month def test_messagerie_access_stores_access_date(client, haie_instructor_44, haie_user): diff --git a/envergo/petitions/views.py b/envergo/petitions/views.py index 2bb1471d9..d67f3e7d9 100644 --- a/envergo/petitions/views.py +++ b/envergo/petitions/views.py @@ -14,8 +14,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.postgres.expressions import ArraySubquery from django.contrib.sites.models import Site -from django.core.exceptions import SuspiciousOperation -from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator +from django.core.exceptions import SuspiciousOperation, ValidationError from django.db import transaction from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery from django.db.models.functions import Coalesce @@ -43,6 +42,7 @@ UpdateView, ) from django.views.generic.detail import SingleObjectMixin +from django.views.generic.list import MultipleObjectMixin from fiona import Feature, Geometry, Properties from pyproj import Transformer from shapely.ops import transform @@ -70,7 +70,9 @@ SimulationForm, ) from envergo.petitions.models import ( + DECISIONS, DOSSIER_STATES, + LOG_TYPES, STAGES, InvitationToken, LatestMessagerieAccess, @@ -245,7 +247,10 @@ def form_valid(self, form): petition_project.demarches_simplifiees_dossier_number = dossier_number petition_project.save() - StatusLog.objects.create(petition_project=petition_project) + StatusLog.objects.create( + petition_project=petition_project, + update_comment="Création initiale", + ) Simulation.objects.create( project=petition_project, @@ -1260,7 +1265,7 @@ def get_success_url(self): class PetitionProjectInstructorProcedureView( - BasePetitionProjectInstructorView, FormView + BasePetitionProjectInstructorView, MultipleObjectMixin, FormView ): """View for display and edit the petition project procedure by the instructor""" @@ -1268,6 +1273,20 @@ class PetitionProjectInstructorProcedureView( template_name = "haie/petitions/instructor_view_procedure.html" paginate_by = 10 + def get(self, request, *args, **kwargs): + self.object = self.get_object() + self.object_list = self.object.status_history.select_related( + "created_by" + ).order_by("-created_at") + return super().get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + self.object_list = self.object.status_history.select_related( + "created_by" + ).order_by("-created_at") + return super().post(request, *args, **kwargs) + def get_initial(self): initial = super().get_initial() initial["stage"] = self.object.current_stage @@ -1276,41 +1295,12 @@ def get_initial(self): return initial def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - obj = self.object - history_qs = obj.status_history.select_related("created_by").order_by( - "-created_at" - ) - paginator = Paginator(history_qs, self.paginate_by) - - page_number = self.request.GET.get("page") - try: - page_obj = paginator.page(page_number) - except PageNotAnInteger: - page_obj = paginator.page(1) - except EmptyPage: - page_obj = paginator.page(paginator.num_pages) - - # Prefetch previous log in the current page - start = max( - page_obj.start_index() - 2, 0 - ) # -1 for 0 index, -1 to get previous log - end = page_obj.end_index() - logs = list(history_qs[start:end]) - for i, log in enumerate(logs): - log.previous_log = logs[i + 1] if i + 1 < len(logs) else None - - # Drop the extra item (first one) if it's not part of this page - if page_obj.number > 1: - logs = logs[1:] + context = super().get_context_data(**kwargs) context.update( { - "object_list": logs, - "paginator": paginator, - "page_obj": page_obj, - "is_paginated": paginator.num_pages > 1, "STAGES": STAGES, + "DECISIONS": DECISIONS, } ) @@ -1318,7 +1308,7 @@ def get_context_data(self, **kwargs): # in the "instruction" phase if self.has_change_permission( self.request, self.object - ) and obj.current_stage.startswith("instruction"): + ) and self.object.current_stage.startswith("instruction"): request_info_form = RequestAdditionalInfoForm() resume_processing_form = ResumeProcessingForm() context.update( @@ -1353,6 +1343,16 @@ def notify_admin(): ) notify(message, "haie") + if self.object.is_paused: + form.add_error( + None, + ValidationError( + "Impossible de mofidier l'état du dossier tant qu'il est en attente de compléments.", + code="modification_while_paused", + ), + ) + return self.form_invalid(form) + log = form.save(commit=False) log.petition_project = self.object log.created_by = self.request.user @@ -1387,6 +1387,7 @@ def notify_admin(): self.request, reference=self.object.reference, etape_i=previous_stage, + department=self.object.get_department_code(), etape_f=log.stage, decision_i=previous_decision, decision_f=log.decision, @@ -1430,27 +1431,37 @@ def pause_form_valid(self, form): """Instructor requested additional data.""" project = self.object - status = project.current_status + current_status = project.current_status try: with transaction.atomic(): - # Update data model - status.suspension_date = timezone.now().date() - status.info_receipt_date = None - status.original_due_date = status.due_date - status.response_due_date = form.cleaned_data["response_due_date"] - status.due_date = status.response_due_date - status.suspended_by = self.request.user - status.save() + # Create a new suspension log entry + StatusLog.objects.create( + petition_project=project, + type=LOG_TYPES.suspension, + due_date=form.cleaned_data["due_date"], + original_due_date=( + current_status.due_date if current_status else None + ), + created_by=self.request.user, + update_comment="Suspension de l’instruction, message envoyé au demandeur.", + ) # Send DS Message message = form.cleaned_data["request_message"] ds_response = send_message_dossier_ds(self.object, message) if ds_response is None or ds_response.get("errors") is not None: - # We raise an exception to make sure the data model transaction - # is aborted - raise DemarchesSimplifieesError(message="DS message not sent") + if not settings.DEMARCHES_SIMPLIFIEES["ENABLED"]: + messages.info( + self.request, + """L'accès à l'API démarches simplifiées n'est pas activée. + Le message n'est pas envoyé""", + ) + else: + # We raise an exception to make sure the data model transaction + # is aborted + raise DemarchesSimplifieesError(message="DS message not sent") # Send Mattermost notification haie_site = Site.objects.get(domain=settings.ENVERGO_HAIE_DOMAIN) @@ -1509,18 +1520,26 @@ def resume_form_valid(self, form): """Instructor received the requested additional info.""" project = self.object - status = project.current_status + suspension = project.latest_suspension - # Update model data # Compute the new due date, that is the original due date + number of interruption days # Note: if you modify this rule, you must apply the same update in the sync_new_due_date.js file - status.info_receipt_date = form.cleaned_data["info_receipt_date"] - interruption_days = status.info_receipt_date - status.suspension_date - if status.original_due_date: - status.due_date = status.original_due_date + interruption_days + info_receipt_date = form.cleaned_data["info_receipt_date"] + interruption_days = info_receipt_date - suspension.created_at.date() + if suspension.original_due_date: + new_due_date = suspension.original_due_date + interruption_days else: - status.due_date = None - status.save() + new_due_date = None + + # Create a new resumption log entry + StatusLog.objects.create( + petition_project=project, + type=LOG_TYPES.resumption, + info_receipt_date=info_receipt_date, + due_date=new_due_date, + created_by=self.request.user, + update_comment="Reprise de l’instruction, date d'échéance ajustée.", + ) # Send Mattermost notification haie_site = Site.objects.get(domain=settings.ENVERGO_HAIE_DOMAIN) diff --git a/envergo/static/sass/project_haie.scss b/envergo/static/sass/project_haie.scss index dbb3d2a96..4af44a78f 100644 --- a/envergo/static/sass/project_haie.scss +++ b/envergo/static/sass/project_haie.scss @@ -924,15 +924,16 @@ div.title-with-link-container { } td { - height: 72px; - padding-left: 0.5rem; - padding-right: 0.5rem; max-width: 200px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - a { + @supports (selector(:has(*))) { + &:has(.more.fr-collapse--expanded) .ellipsis { + display: none; + } + } + + a, + button { font-size: 0.875rem; } } diff --git a/envergo/templates/_truncated_comment.html b/envergo/templates/_truncated_comment.html new file mode 100644 index 000000000..1b1759dd9 --- /dev/null +++ b/envergo/templates/_truncated_comment.html @@ -0,0 +1,19 @@ +{% if text %} + {% if is_truncated %} +
+ {{ head }} + +
{{ tail }}
+ + +
+ {% else %} + {{ text }} + {% endif %} +{% endif %} diff --git a/envergo/templates/haie/petitions/_summary_dossier.html b/envergo/templates/haie/petitions/_summary_dossier.html index d6a4bd7c1..3d3a1d35b 100644 --- a/envergo/templates/haie/petitions/_summary_dossier.html +++ b/envergo/templates/haie/petitions/_summary_dossier.html @@ -53,7 +53,7 @@

Dossier n° {{ petition_project.demarches_simplifiees_dossier_
{% if petition_project.is_paused %} - {% display_pause petition_project.current_status.response_due_date %} + {% display_pause petition_project.latest_suspension.due_date %} {% else %} {% display_due_date petition_project.due_date False %} {% endif %} diff --git a/envergo/templates/haie/petitions/instructor_dossier_list.html b/envergo/templates/haie/petitions/instructor_dossier_list.html index 3c4912a17..6ba0315df 100644 --- a/envergo/templates/haie/petitions/instructor_dossier_list.html +++ b/envergo/templates/haie/petitions/instructor_dossier_list.html @@ -112,7 +112,7 @@

Aucun dossier n’est accessible pour le moment

{% if project.current_stage != "closed" %}
{% if project.is_paused %} - {% display_pause project.current_status.response_due_date %} + {% display_pause project.latest_suspension.due_date %} {% else %} {% display_due_date project.due_date False True %} {% endif %} diff --git a/envergo/templates/haie/petitions/instructor_view_procedure.html b/envergo/templates/haie/petitions/instructor_view_procedure.html index c8a4e219b..6068aa117 100644 --- a/envergo/templates/haie/petitions/instructor_view_procedure.html +++ b/envergo/templates/haie/petitions/instructor_view_procedure.html @@ -44,7 +44,7 @@

→ Reprendre l'instruction

{% csrf_token %} {% include '_form_snippet.html' with form=resume_processing_form %} - {% if petition_project.current_status.original_due_date %} + {% if petition_project.latest_suspension.original_due_date %}
{% endif %} @@ -77,17 +77,17 @@

→ Reprendre l'instruction


- {{ petition_project.current_status.suspension_date|date:"DATE_FORMAT" }} + {{ petition_project.latest_suspension.created_at|date:"DATE_FORMAT" }}

- {% if petition_project.current_status.original_due_date %} + {% if petition_project.latest_suspension.original_due_date %}

Échéance d'instruction avant la suspension
- {{ petition_project.current_status.original_due_date|date:"DATE_FORMAT" }} + {{ petition_project.latest_suspension.original_due_date|date:"DATE_FORMAT" }}

{% endif %} @@ -151,17 +151,20 @@

Décision

{% if is_department_instructor %} {% if petition_project.demarches_simplifiees_state == "draft" %} Ce dossier n'a pas encore été déposé par le pétitionnaire. {% endif %} + {% if petition_project.is_paused %} + Impossible tant que le dossier est en attente de compléments. + {% endif %} {% endif %} {% if paginator.count %}

Historique des changements

-
+
@@ -172,50 +175,50 @@

Historique des changements

- Date / heure de la saisie - Date effective + Date Auteur - Étape / Décision départ - Étape / Décision arrivée + Modification Commentaire {% for log in object_list %} - {{ log.created_at|date:"SHORT_DATETIME_FORMAT" }} - {{ log.status_date|date:"SHORT_DATE_FORMAT" }} - {{ log.created_by.email }} - {{ log.created_by.email }} + {{ log.created_at|date:"SHORT_DATE_FORMAT" }} +
+ {{ log.created_at|date:"TIME_FORMAT" }} + {% created_by_display log %} - {% if log.previous_log %} - {{ log.previous_log.get_stage_display }} - {% if log.previous_log.decision != "unset" %} + {{ log.get_type_display }} + {% if log.type == "status_change" %} +
+ {{ STAGES|get_choice_label:log.stage }} + {% if log.due_date %}
- {{ log.previous_log.get_decision_display }} + Échéance le {{ log.due_date|date:"SHORT_DATE_FORMAT" }} {% endif %} - {% else %} - {{ log|choice_default_label:"stage" }} + {% if log.decision != "unset" %} +
+ Décision : {{ DECISIONS|get_choice_label:log.decision }} + {% endif %} + {% elif log.type == "suspension" %} +
+ Date limite : {{ log.due_date|date:"SHORT_DATE_FORMAT" }} + {% elif log.type == "resumption" %} +
+ Réception : {{ log.info_receipt_date|date:"SHORT_DATE_FORMAT" }} +
+ Prochaine échéance : {{ log.due_date|date:"SHORT_DATE_FORMAT" }} {% endif %} - {{ log.get_stage_display }} - {% if log.decision != "unset" %} + {% if log.status_date and log.status_date != log.created_at.date %} + Changement effectif le {{ log.status_date|date:"SHORT_DATE_FORMAT" }}
- {{ log.get_decision_display }} {% endif %} - - - {{ log.update_comment }} - {{ log.update_comment }} + {% truncated_comment log.update_comment forloop.counter 60 %} {% endfor %} @@ -243,7 +246,7 @@

Historique des changements

{{ block.super }} - {% if resume_processing_form and petition_project.current_status.original_due_date %} + {% if resume_processing_form and petition_project.latest_suspension.original_due_date %}