-
-
- {% translate "Documents & Photos" %}
- {{ documents_count }}
-
-
-
- {% if documents %}
-
- {% for doc in documents %}
- {% private_file_url doc 'file' as file_url %}
- {% if doc.thumbnail %}
- {% private_file_url doc 'thumbnail' as thumb_url %}
- {% else %}
- {% private_file_url doc 'file' as thumb_url %}
- {% endif %}
-
-
+ {% endif %}
+ {% endfor %}
+
{% endif %}
diff --git a/loko/interventions/tests/test_calendar_notes_docs.py b/loko/interventions/tests/test_calendar_notes_docs.py
index bda6f2f..9baba5a 100644
--- a/loko/interventions/tests/test_calendar_notes_docs.py
+++ b/loko/interventions/tests/test_calendar_notes_docs.py
@@ -1,6 +1,6 @@
import json
from unittest import mock
-from datetime import datetime
+from datetime import datetime, timedelta
from django.test import SimpleTestCase, RequestFactory
from django.contrib.auth.models import AnonymousUser
@@ -114,10 +114,15 @@ class CalendarInterventionDetailsAjaxSimpleTests(SimpleTestCase):
def test_authorized_user_receives_notes_and_documents(
self, mock_get_obj, mock_notes_qs, mock_can_view, mock_can_view_summary
):
+ now = timezone.now()
mock_itv = mock.MagicMock(id=123, title="Intervention Réparation Éclairage")
+ mock_itv.begin_time = None
+ mock_itv.end_time = None
+ mock_itv.creation_time = now
+ mock_itv.events.filter.return_value.order_by.return_value.first.return_value = None
+ mock_itv.realized_events.filter.return_value.order_by.return_value.first.return_value = None
mock_get_obj.return_value = mock_itv
- now = timezone.now()
author = DummyUser(id=1, first_name="Jean", last_name="Dupont")
# Notes
@@ -167,6 +172,7 @@ class CalendarInterventionDetailsAjaxSimpleTests(SimpleTestCase):
)
mock_docs_qs = mock.MagicMock()
+ mock_docs_qs.all.return_value = mock_docs_qs
mock_docs_qs.select_related.return_value = mock_docs_qs
mock_docs_qs.order_by.return_value = [doc_img, doc_pdf]
mock_itv.documents = mock_docs_qs
@@ -174,21 +180,24 @@ class CalendarInterventionDetailsAjaxSimpleTests(SimpleTestCase):
user = DummyUser(id=99)
request = self._create_request(user)
- response = intervention_calendar_details_ajax(request, intervention_id=123)
- self.assertEqual(response.status_code, 200)
+ from django.utils import translation
+ with translation.override('fr'):
+ response = intervention_calendar_details_ajax(request, intervention_id=123)
+ self.assertEqual(response.status_code, 200)
- data = json.loads(response.content.decode('utf-8'))
- self.assertTrue(data['success'])
- self.assertEqual(data['notes_count'], 2)
- self.assertEqual(data['documents_count'], 2)
- self.assertEqual(data['total_count'], 4)
+ data = json.loads(response.content.decode('utf-8'))
+ self.assertTrue(data['success'])
+ self.assertEqual(data['notes_count'], 2)
+ self.assertEqual(data['documents_count'], 2)
+ self.assertEqual(data['total_count'], 4)
- # Content assertions
- self.assertIn("Nacelle 15m requise", data['html'])
- self.assertIn("Attention présence de câbles haute tension", data['html'])
- self.assertIn("photo_degats.jpg", data['html'])
- self.assertIn("rapport_securite.pdf", data['html'])
- self.assertIn("Jean D.", data['html'])
+ # Content assertions
+ self.assertIn("Avant réalisation", data['html'])
+ self.assertIn("Nacelle 15m requise", data['html'])
+ self.assertIn("Attention présence de câbles haute tension", data['html'])
+ self.assertIn("photo_degats.jpg", data['html'])
+ self.assertIn("rapport_securite.pdf", data['html'])
+ self.assertIn("Jean D.", data['html'])
# Document flags computed by view
self.assertTrue(doc_img.is_image)
@@ -207,6 +216,11 @@ class CalendarInterventionDetailsAjaxSimpleTests(SimpleTestCase):
self, mock_get_obj, mock_notes_qs, mock_can_view, mock_can_view_summary
):
mock_itv = mock.MagicMock(id=456, title="Intervention Sans Notes")
+ mock_itv.begin_time = None
+ mock_itv.end_time = None
+ mock_itv.creation_time = timezone.now()
+ mock_itv.events.filter.return_value.order_by.return_value.first.return_value = None
+ mock_itv.realized_events.filter.return_value.order_by.return_value.first.return_value = None
mock_get_obj.return_value = mock_itv
mock_filtered_notes = mock.MagicMock()
@@ -216,6 +230,7 @@ class CalendarInterventionDetailsAjaxSimpleTests(SimpleTestCase):
mock_notes_qs.filter.return_value = mock_filtered_notes
mock_docs_qs = mock.MagicMock()
+ mock_docs_qs.all.return_value = mock_docs_qs
mock_docs_qs.select_related.return_value = mock_docs_qs
mock_docs_qs.order_by.return_value = []
mock_itv.documents = mock_docs_qs
@@ -232,3 +247,75 @@ class CalendarInterventionDetailsAjaxSimpleTests(SimpleTestCase):
self.assertEqual(data['documents_count'], 0)
self.assertEqual(data['total_count'], 0)
self.assertIn("Aucune note ni document", data['html'])
+
+ @mock.patch('interventions.views.calendar.can_view_intervention_summary', return_value=False)
+ @mock.patch('interventions.views.calendar.can_view_intervention', return_value=True)
+ @mock.patch('interventions.views.calendar.InterventionNote.objects')
+ @mock.patch('interventions.views.calendar.get_object_or_404')
+ def test_chronological_sequencing_multiple_blocks(
+ self, mock_get_obj, mock_notes_qs, mock_can_view, mock_can_view_summary
+ ):
+ now = timezone.now()
+ author = DummyUser(id=1, first_name="Jean", last_name="Dupont")
+
+ mock_itv = mock.MagicMock(id=789, title="Intervention Multi Phases")
+ mock_itv.begin_time = now - timedelta(hours=2)
+ mock_itv.end_time = now + timedelta(hours=2)
+ mock_itv.creation_time = now - timedelta(days=2)
+ mock_itv.events.filter.return_value.order_by.return_value.first.return_value = None
+ mock_itv.realized_events.filter.return_value.order_by.return_value.first.return_value = None
+ mock_get_obj.return_value = mock_itv
+
+ # Note during work
+ note_during = DummyNote(
+ id=1,
+ note_type=InterventionNote.NoteType.WORK_DONE,
+ type_display="Travaux réalisés",
+ badge_class="text-bg-success text-white",
+ icon_class="bi-check-circle",
+ content="Remplacement du ballast effectué avec succès",
+ note_time=now,
+ note_author=author,
+ )
+ mock_filtered_notes = mock.MagicMock()
+ mock_filtered_notes.select_related.return_value = mock_filtered_notes
+ mock_filtered_notes.annotate.return_value = mock_filtered_notes
+ mock_filtered_notes.order_by.return_value = [note_during]
+ mock_notes_qs.filter.return_value = mock_filtered_notes
+
+ # Document before work (yesterday)
+ doc_before = DummyDoc(
+ id=20,
+ filename="constat_initial.jpg",
+ thumbnail_path="thumbnails/constat_initial.jpg",
+ upload_date=now - timedelta(days=1),
+ uploaded_by=author,
+ )
+ mock_docs_qs = mock.MagicMock()
+ mock_docs_qs.all.return_value = mock_docs_qs
+ mock_docs_qs.select_related.return_value = mock_docs_qs
+ mock_docs_qs.order_by.return_value = [doc_before]
+ mock_itv.documents = mock_docs_qs
+
+ user = DummyUser(id=99)
+ request = self._create_request(user)
+
+ from django.utils import translation
+ with translation.override('fr'):
+ response = intervention_calendar_details_ajax(request, intervention_id=789)
+ self.assertEqual(response.status_code, 200)
+
+ data = json.loads(response.content.decode('utf-8'))
+ self.assertTrue(data['success'])
+ self.assertEqual(data['notes_count'], 1)
+ self.assertEqual(data['documents_count'], 1)
+ self.assertEqual(data['total_count'], 2)
+
+ import html as html_module
+ unescaped_html = html_module.unescape(data['html'])
+
+ # Both chronological blocks are rendered
+ self.assertIn("Avant réalisation", unescaped_html)
+ self.assertIn("constat_initial.jpg", unescaped_html)
+ self.assertIn("Exécution de l'intervention", unescaped_html)
+ self.assertIn("Remplacement du ballast effectué avec succès", unescaped_html)
diff --git a/loko/interventions/views/calendar.py b/loko/interventions/views/calendar.py
index 3661a77..d73cd28 100644
--- a/loko/interventions/views/calendar.py
+++ b/loko/interventions/views/calendar.py
@@ -80,6 +80,7 @@ from interventions.permissions import (intervention_permission_required, interve
from interventions.permissions import ALLOWED_TRANSITIONS, get_allowed_transitions, user_allowed_transitions
from interventions.views.drafts import InterventionDraftManager
from interventions.utils import resolve_intervention_localisations
+from interventions.services.document_sequencing import get_sequenced_intervention_documents
from common.models import UserConfig, UserContractAccess, UserContractStatusPermission, Thematic, UserThematics
from contracts.models import ContractOrder, ContractPost, Contract, Company, CompanyTeam, CompanyMember, UserCompanyTeamAccess, CompanyMemberHistory, CompanyMemberAbsence
@@ -1759,7 +1760,8 @@ def team_daily_members_ajax(request, team_id):
@require_GET
def intervention_calendar_details_ajax(request, intervention_id):
"""
- Retourne les notes et documents d'une intervention pour la modale du calendrier.
+ Retourne les notes et documents d'une intervention pour la modale du calendrier,
+ organisés sous forme chronologique (phases: avant réalisation, exécution, après réalisation).
"""
intervention = get_object_or_404(Intervention, pk=intervention_id)
@@ -1774,35 +1776,32 @@ def intervention_calendar_details_ajax(request, intervention_id):
output_field=IntegerField(),
)
- notes = list(
+ notes = (
InterventionNote.objects
.filter(intervention=intervention)
.select_related('note_author', 'updated_by')
.annotate(type_priority_order=note_priority_case)
- .order_by('type_priority_order', '-note_time', '-id')
+ .order_by('type_priority_order', 'note_time', 'id')
)
+ sequenced_data = get_sequenced_intervention_documents(intervention, notes_qs=notes)
+
IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'}
- documents = list(
- intervention.documents
- .select_related('uploaded_by')
- .order_by('-upload_date', '-id')
- )
-
- for doc in documents:
- fn = getattr(doc, 'filename', '') or ''
- ext = os.path.splitext(fn)[1].lower() if fn else ''
- doc.ext = ext
- doc.is_image = bool(doc.thumbnail or ext in IMAGE_EXTENSIONS)
- doc.is_pdf = ext == '.pdf'
+ for block in sequenced_data['blocks']:
+ for doc in block['documents']:
+ fn = getattr(doc, 'filename', '') or ''
+ ext = os.path.splitext(fn)[1].lower() if fn else ''
+ doc.ext = ext
+ doc.is_image = bool(getattr(doc, 'thumbnail', None) or ext in IMAGE_EXTENSIONS)
+ doc.is_pdf = ext == '.pdf'
context = {
'intervention': intervention,
- 'notes': notes,
- 'documents': documents,
- 'notes_count': len(notes),
- 'documents_count': len(documents),
- 'total_count': len(notes) + len(documents),
+ 'sequenced_blocks': sequenced_data['blocks'],
+ 'notes_count': sequenced_data['total_notes_count'],
+ 'documents_count': sequenced_data['total_documents_count'],
+ 'total_count': sequenced_data['total_items_count'],
+ 'has_items': sequenced_data['has_items'],
}
html = render_to_string(
@@ -1813,9 +1812,9 @@ def intervention_calendar_details_ajax(request, intervention_id):
return JsonResponse({
'success': True,
- 'notes_count': len(notes),
- 'documents_count': len(documents),
- 'total_count': len(notes) + len(documents),
+ 'notes_count': sequenced_data['total_notes_count'],
+ 'documents_count': sequenced_data['total_documents_count'],
+ 'total_count': sequenced_data['total_items_count'],
'html': html,
})