refactor: redesign calendar notes and documents view with chronological sequencing blocks

This commit is contained in:
kdeterme 2026-10-07 10:45:37 +02:00
parent 2ef730508f
commit 499ffe824e
5 changed files with 233 additions and 180 deletions

View file

@ -1182,8 +1182,9 @@
border-color: #b0c4de !important; border-color: #b0c4de !important;
} }
.cal-nd-filter-btn { .cal-sequence-block {
transition: all 0.15s ease; background-color: #f8fafc;
border-color: #e2e8f0 !important;
} }

View file

@ -5138,32 +5138,6 @@ document.addEventListener('DOMContentLoaded', function () {
if (summaryText) { if (summaryText) {
summaryText.textContent = `${notesText} • ${docsText}`; summaryText.textContent = `${notesText} • ${docsText}`;
} }
// Setup filter buttons
const filterBtns = container.querySelectorAll('.cal-nd-filter-btn');
const notesSection = container.querySelector('#cal-modal-notes-section');
const docsSection = container.querySelector('#cal-modal-docs-section');
filterBtns.forEach(btn => {
btn.addEventListener('click', function () {
filterBtns.forEach(b => {
b.classList.remove('active');
b.classList.remove('btn-primary');
b.classList.add('btn-outline-primary');
});
this.classList.add('active');
this.classList.remove('btn-outline-primary');
this.classList.add('btn-primary');
const filter = this.getAttribute('data-filter');
if (notesSection) {
notesSection.style.display = (filter === 'all' || filter === 'notes') ? '' : 'none';
}
if (docsSection) {
docsSection.style.display = (filter === 'all' || filter === 'docs') ? '' : 'none';
}
});
});
} else { } else {
container.innerHTML = `<div class="text-muted small p-3 text-center">${gettext('Impossible de charger les notes et documents.')}</div>`; container.innerHTML = `<div class="text-muted small p-3 text-center">${gettext('Impossible de charger les notes et documents.')}</div>`;
} }

View file

@ -1,141 +1,133 @@
{% load i18n tz short_name interventions_tags document_viewer_tags %} {% load i18n tz short_name interventions_tags document_viewer_tags %}
<div class="calendar-notes-docs-wrapper"> <div class="calendar-notes-docs-wrapper">
<!-- Barre d'outils filtres / compteurs --> <!-- En-tête résumé -->
<div class="d-flex justify-content-between align-items-center mb-3 pb-2 border-bottom"> <div class="d-flex justify-content-between align-items-center mb-3 pb-2 border-bottom">
<div class="btn-group btn-group-sm" role="group" aria-label="{% translate 'Filtrer les notes et documents' %}"> <h6 class="mb-0 fw-bold text-dark d-flex align-items-center" style="font-size: 0.88rem;">
<button type="button" class="btn btn-outline-primary active cal-nd-filter-btn" data-filter="all"> <i class="bi bi-journal-text text-primary me-2"></i>{% translate "Notes & Documents" %}
<i class="bi bi-grid me-1"></i>{% translate "Tout" %} </h6>
<span class="badge text-bg-primary rounded-pill ms-1" id="cal-count-all">{{ total_count }}</span> {% if total_count > 0 %}
</button> <span class="badge text-bg-light border text-muted" style="font-size: 0.75rem;">
<button type="button" class="btn btn-outline-primary cal-nd-filter-btn" data-filter="notes"> {{ total_count }} {% if total_count > 1 %}{% translate "éléments" %}{% else %}{% translate "élément" %}{% endif %}
<i class="bi bi-sticky me-1"></i>{% translate "Notes" %} </span>
<span class="badge text-bg-secondary rounded-pill ms-1" id="cal-count-notes">{{ notes_count }}</span> {% endif %}
</button>
<button type="button" class="btn btn-outline-primary cal-nd-filter-btn" data-filter="docs">
<i class="bi bi-paperclip me-1"></i>{% translate "Documents" %}
<span class="badge text-bg-secondary rounded-pill ms-1" id="cal-count-docs">{{ documents_count }}</span>
</button>
</div>
<div class="text-muted small">
{% if total_count > 0 %}
<span>{{ total_count }} {% if total_count > 1 %}{% translate "éléments" %}{% else %}{% translate "élément" %}{% endif %}</span>
{% endif %}
</div>
</div> </div>
{% if total_count == 0 %} {% if not has_items %}
<div class="text-center text-muted py-5"> <div class="text-center text-muted py-5">
<i class="bi bi-journal-x fs-1 d-block mb-2 text-secondary opacity-50"></i> <i class="bi bi-journal-x fs-1 d-block mb-2 text-secondary opacity-50"></i>
<p class="mb-1 fw-semibold text-secondary">{% translate "Aucune note ni document" %}</p> <p class="mb-1 fw-semibold text-secondary">{% translate "Aucune note ni document" %}</p>
<p class="small text-muted mb-0">{% translate "Cette intervention n'a pas encore de note ni de document associé." %}</p> <p class="small text-muted mb-0">{% translate "Cette intervention n'a pas encore de note ni de document associé." %}</p>
</div> </div>
{% else %} {% else %}
<div class="cal-chronological-blocks d-flex flex-column gap-3">
<!-- Section Notes --> {% for block in sequenced_blocks %}
<div id="cal-modal-notes-section" class="cal-nd-section mb-3"> {% if block.has_items %}
<div class="d-flex align-items-center justify-content-between mb-2"> <div class="cal-sequence-block p-3 rounded-3 bg-light-subtle border">
<h6 class="mb-0 fw-bold text-dark d-flex align-items-center" style="font-size: 0.85rem;"> <!-- En-tête de la phase chronologique -->
<i class="bi bi-sticky-fill text-warning me-2"></i>{% translate "Notes" %} <div class="d-flex justify-content-between align-items-center mb-3 pb-2 border-bottom">
<span class="badge bg-secondary-subtle text-secondary-emphasis rounded-pill ms-2" style="font-size: 0.72rem;">{{ notes_count }}</span> <div class="d-flex align-items-center gap-2 flex-wrap">
</h6> <span class="badge {{ block.badge_class }} py-1 px-2 d-inline-flex align-items-center gap-1" style="font-size: 0.8rem;">
</div> <i class="bi {{ block.icon }}"></i>
{{ block.title }}
{% if notes %} </span>
<div class="cal-notes-list d-flex flex-column gap-2"> <span class="text-secondary small fw-medium" style="font-size: 0.75rem;">
{% for note in notes %} {% if block.documents_count > 0 %}{{ block.documents_count }} {% if block.documents_count > 1 %}{% translate "fichiers" %}{% else %}{% translate "fichier" %}{% endif %}{% endif %}
<div class="card shadow-none border bg-light-subtle rounded-3 cal-note-card"> {% if block.documents_count > 0 and block.notes_count > 0 %}&bull;{% endif %}
<div class="card-body p-2 px-3"> {% if block.notes_count > 0 %}{{ block.notes_count }} {% if block.notes_count > 1 %}{% translate "notes" %}{% else %}{% translate "note" %}{% endif %}{% endif %}
<div class="d-flex justify-content-between align-items-start gap-2 mb-1">
<div class="d-flex flex-wrap align-items-center gap-1">
<span class="badge {{ note.type_badge_class }} d-inline-flex align-items-center gap-1 py-1 px-2" style="font-size: 0.72rem;">
<i class="bi {{ note.type_icon_class }}"></i>
{{ note.get_note_type_display }}
</span>
</div>
<span class="text-muted small text-nowrap" style="font-size: 0.75rem;">
<i class="bi bi-clock me-1"></i>{{ note.note_time|timezone:"Europe/Paris"|date:"d/m/Y H:i" }}
</span> </span>
</div> </div>
{% if block.period_display %}
<span class="text-muted small text-nowrap" style="font-size: 0.72rem;">
<i class="bi bi-clock me-1"></i>{{ block.period_display }}
</span>
{% endif %}
</div>
<div class="text-dark small note-content-text py-1" style="white-space: pre-wrap; line-height: 1.45; word-break: break-word;">{{ note.content }}</div> <!-- Documents & Photos de la phase -->
{% if block.documents %}
<div class="d-flex justify-content-between align-items-center mt-1 pt-1 border-top border-light-subtle" style="font-size: 0.72rem;"> <div class="cal-block-documents mb-3">
<span class="text-muted"> <div class="row g-2">
<i class="bi bi-person me-1"></i>{{ note.note_author|short_name|default:"—" }} {% for doc in block.documents %}
</span> {% private_file_url doc 'file' as file_url %}
{% if note.updated_at %} {% if doc.thumbnail %}
<span class="text-muted fst-italic" title="{% translate 'Mis à jour le' %} {{ note.updated_at|timezone:'Europe/Paris'|date:'d/m/Y H:i' }}{% if note.updated_by %} {% translate 'par' %} {{ note.updated_by|short_name }}{% endif %}"> {% private_file_url doc 'thumbnail' as thumb_url %}
<i class="bi bi-pencil-fill me-1" style="font-size: 0.65rem;"></i>{% translate "Modifié" %} {% else %}
</span> {% private_file_url doc 'file' as thumb_url %}
{% endif %} {% endif %}
<div class="col-6 col-sm-4">
<div class="card h-100 border shadow-none bg-white rounded-3 overflow-hidden position-relative cal-doc-card">
<a href="{{ file_url }}" target="_blank" rel="noopener noreferrer" class="d-block text-decoration-none bg-light d-flex align-items-center justify-content-center text-center overflow-hidden position-relative" style="height: 105px;" title="{% translate 'Ouvrir' %} {{ doc.filename }}">
{% if doc.is_image %}
<img src="{{ thumb_url }}" alt="{{ doc.filename }}" style="width: 100%; height: 100%; object-fit: cover;" loading="lazy">
<div class="position-absolute bottom-0 end-0 p-1">
<span class="badge bg-dark bg-opacity-75 rounded-pill" style="font-size: 0.65rem;" title="{% translate 'Agrandir' %}">
<i class="bi bi-zoom-in"></i>
</span>
</div>
{% else %}
<i class="{{ doc.ext|file_icon_class }}" style="font-size: 2.3rem;"></i>
{% endif %}
</a>
<div class="card-body p-2 d-flex flex-column justify-content-between" style="min-height: 52px;">
<div class="text-truncate fw-semibold text-dark small" style="font-size: 0.76rem;" title="{{ doc.filename }}">
{{ doc.filename }}
</div>
<div class="d-flex justify-content-between align-items-center mt-1 pt-1 border-top" style="font-size: 0.7rem;">
<span class="text-muted text-truncate me-1" title="{{ doc.upload_date|timezone:'Europe/Paris'|date:'d/m/Y H:i' }}{% if doc.uploaded_by %} · {{ doc.uploaded_by|short_name }}{% endif %}">
{{ doc.upload_date|timezone:"Europe/Paris"|date:"d/m/Y" }}
</span>
<a href="{{ file_url }}" download="{{ doc.filename }}" class="text-primary text-decoration-none p-0" title="{% translate 'Télécharger' %}">
<i class="bi bi-download"></i>
</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div> </div>
</div> </div>
</div> {% endif %}
{% endfor %}
</div>
{% else %}
<div class="text-muted small fst-italic py-2 px-1">
<i class="bi bi-info-circle me-1"></i>{% translate "Aucune note pour cette intervention." %}
</div>
{% endif %}
</div>
<!-- Section Documents --> <!-- Notes de la phase -->
<div id="cal-modal-docs-section" class="cal-nd-section {% if notes %}mt-4 pt-3 border-top{% endif %}"> {% if block.notes %}
<div class="d-flex align-items-center justify-content-between mb-2"> <div class="cal-block-notes {% if block.documents %}border-top pt-2 mt-2{% endif %} d-flex flex-column gap-2">
<h6 class="mb-0 fw-bold text-dark d-flex align-items-center" style="font-size: 0.85rem;"> {% for note in block.notes %}
<i class="bi bi-paperclip text-primary me-2"></i>{% translate "Documents & Photos" %} <div class="card shadow-none border bg-white rounded-3 cal-note-card">
<span class="badge bg-secondary-subtle text-secondary-emphasis rounded-pill ms-2" style="font-size: 0.72rem;">{{ documents_count }}</span> <div class="card-body p-2 px-3">
</h6> <div class="d-flex justify-content-between align-items-start gap-2 mb-1">
</div> <div class="d-flex flex-wrap align-items-center gap-1">
<span class="badge {{ note.type_badge_class }} d-inline-flex align-items-center gap-1 py-1 px-2" style="font-size: 0.72rem;">
{% if documents %} <i class="bi {{ note.type_icon_class }}"></i>
<div class="row g-2"> {{ note.get_note_type_display }}
{% for doc in documents %} </span>
{% private_file_url doc 'file' as file_url %} </div>
{% if doc.thumbnail %} <span class="text-muted small text-nowrap" style="font-size: 0.75rem;">
{% private_file_url doc 'thumbnail' as thumb_url %} <i class="bi bi-clock me-1"></i>{{ note.note_time|timezone:"Europe/Paris"|date:"d/m/Y H:i" }}
{% else %}
{% private_file_url doc 'file' as thumb_url %}
{% endif %}
<div class="col-6 col-sm-4">
<div class="card h-100 border shadow-none bg-white rounded-3 overflow-hidden position-relative cal-doc-card">
<a href="{{ file_url }}" target="_blank" rel="noopener noreferrer" class="d-block text-decoration-none bg-light d-flex align-items-center justify-content-center text-center overflow-hidden position-relative" style="height: 105px;" title="{% translate 'Ouvrir' %} {{ doc.filename }}">
{% if doc.is_image %}
<img src="{{ thumb_url }}" alt="{{ doc.filename }}" style="width: 100%; height: 100%; object-fit: cover;" loading="lazy">
<div class="position-absolute bottom-0 end-0 p-1">
<span class="badge bg-dark bg-opacity-75 rounded-pill" style="font-size: 0.65rem;" title="{% translate 'Agrandir' %}">
<i class="bi bi-zoom-in"></i>
</span> </span>
</div> </div>
{% else %}
<i class="{{ doc.ext|file_icon_class }}" style="font-size: 2.3rem;"></i> <div class="text-dark small note-content-text py-1" style="white-space: pre-wrap; line-height: 1.45; word-break: break-word;">{{ note.content }}</div>
{% endif %}
</a> <div class="d-flex justify-content-between align-items-center mt-1 pt-1 border-top border-light-subtle" style="font-size: 0.72rem;">
<div class="card-body p-2 d-flex flex-column justify-content-between" style="min-height: 52px;"> <span class="text-muted">
<div class="text-truncate fw-semibold text-dark small" style="font-size: 0.76rem;" title="{{ doc.filename }}"> <i class="bi bi-person me-1"></i>{{ note.note_author|short_name|default:"—" }}
{{ doc.filename }} </span>
</div> {% if note.updated_at %}
<div class="d-flex justify-content-between align-items-center mt-1 pt-1 border-top" style="font-size: 0.7rem;"> <span class="text-muted fst-italic" title="{% translate 'Mis à jour le' %} {{ note.updated_at|timezone:'Europe/Paris'|date:'d/m/Y H:i' }}{% if note.updated_by %} {% translate 'par' %} {{ note.updated_by|short_name }}{% endif %}">
<span class="text-muted text-truncate me-1" title="{{ doc.upload_date|timezone:'Europe/Paris'|date:'d/m/Y H:i' }}{% if doc.uploaded_by %} · {{ doc.uploaded_by|short_name }}{% endif %}"> <i class="bi bi-pencil-fill me-1" style="font-size: 0.65rem;"></i>{% translate "Modifié" %}
{{ doc.upload_date|timezone:"Europe/Paris"|date:"d/m/Y" }} </span>
</span> {% endif %}
<a href="{{ file_url }}" download="{{ doc.filename }}" class="text-primary text-decoration-none p-0" title="{% translate 'Télécharger' %}"> </div>
<i class="bi bi-download"></i>
</a>
</div> </div>
</div> </div>
{% endfor %}
</div> </div>
</div> {% endif %}
{% endfor %}
</div>
{% else %}
<div class="text-muted small fst-italic py-2 px-1">
<i class="bi bi-info-circle me-1"></i>{% translate "Aucun document pour cette intervention." %}
</div>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %} {% endif %}
</div> </div>

View file

@ -1,6 +1,6 @@
import json import json
from unittest import mock from unittest import mock
from datetime import datetime from datetime import datetime, timedelta
from django.test import SimpleTestCase, RequestFactory from django.test import SimpleTestCase, RequestFactory
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
@ -114,10 +114,15 @@ class CalendarInterventionDetailsAjaxSimpleTests(SimpleTestCase):
def test_authorized_user_receives_notes_and_documents( def test_authorized_user_receives_notes_and_documents(
self, mock_get_obj, mock_notes_qs, mock_can_view, mock_can_view_summary 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 = 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 mock_get_obj.return_value = mock_itv
now = timezone.now()
author = DummyUser(id=1, first_name="Jean", last_name="Dupont") author = DummyUser(id=1, first_name="Jean", last_name="Dupont")
# Notes # Notes
@ -167,6 +172,7 @@ class CalendarInterventionDetailsAjaxSimpleTests(SimpleTestCase):
) )
mock_docs_qs = mock.MagicMock() 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.select_related.return_value = mock_docs_qs
mock_docs_qs.order_by.return_value = [doc_img, doc_pdf] mock_docs_qs.order_by.return_value = [doc_img, doc_pdf]
mock_itv.documents = mock_docs_qs mock_itv.documents = mock_docs_qs
@ -174,21 +180,24 @@ class CalendarInterventionDetailsAjaxSimpleTests(SimpleTestCase):
user = DummyUser(id=99) user = DummyUser(id=99)
request = self._create_request(user) request = self._create_request(user)
response = intervention_calendar_details_ajax(request, intervention_id=123) from django.utils import translation
self.assertEqual(response.status_code, 200) 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')) data = json.loads(response.content.decode('utf-8'))
self.assertTrue(data['success']) self.assertTrue(data['success'])
self.assertEqual(data['notes_count'], 2) self.assertEqual(data['notes_count'], 2)
self.assertEqual(data['documents_count'], 2) self.assertEqual(data['documents_count'], 2)
self.assertEqual(data['total_count'], 4) self.assertEqual(data['total_count'], 4)
# Content assertions # Content assertions
self.assertIn("Nacelle 15m requise", data['html']) self.assertIn("Avant réalisation", data['html'])
self.assertIn("Attention présence de câbles haute tension", data['html']) self.assertIn("Nacelle 15m requise", data['html'])
self.assertIn("photo_degats.jpg", data['html']) self.assertIn("Attention présence de câbles haute tension", data['html'])
self.assertIn("rapport_securite.pdf", data['html']) self.assertIn("photo_degats.jpg", data['html'])
self.assertIn("Jean D.", data['html']) self.assertIn("rapport_securite.pdf", data['html'])
self.assertIn("Jean D.", data['html'])
# Document flags computed by view # Document flags computed by view
self.assertTrue(doc_img.is_image) 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 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 = 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_get_obj.return_value = mock_itv
mock_filtered_notes = mock.MagicMock() mock_filtered_notes = mock.MagicMock()
@ -216,6 +230,7 @@ class CalendarInterventionDetailsAjaxSimpleTests(SimpleTestCase):
mock_notes_qs.filter.return_value = mock_filtered_notes mock_notes_qs.filter.return_value = mock_filtered_notes
mock_docs_qs = mock.MagicMock() 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.select_related.return_value = mock_docs_qs
mock_docs_qs.order_by.return_value = [] mock_docs_qs.order_by.return_value = []
mock_itv.documents = mock_docs_qs mock_itv.documents = mock_docs_qs
@ -232,3 +247,75 @@ class CalendarInterventionDetailsAjaxSimpleTests(SimpleTestCase):
self.assertEqual(data['documents_count'], 0) self.assertEqual(data['documents_count'], 0)
self.assertEqual(data['total_count'], 0) self.assertEqual(data['total_count'], 0)
self.assertIn("Aucune note ni document", data['html']) 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)

View file

@ -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.permissions import ALLOWED_TRANSITIONS, get_allowed_transitions, user_allowed_transitions
from interventions.views.drafts import InterventionDraftManager from interventions.views.drafts import InterventionDraftManager
from interventions.utils import resolve_intervention_localisations 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 common.models import UserConfig, UserContractAccess, UserContractStatusPermission, Thematic, UserThematics
from contracts.models import ContractOrder, ContractPost, Contract, Company, CompanyTeam, CompanyMember, UserCompanyTeamAccess, CompanyMemberHistory, CompanyMemberAbsence 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 @require_GET
def intervention_calendar_details_ajax(request, intervention_id): 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) intervention = get_object_or_404(Intervention, pk=intervention_id)
@ -1774,35 +1776,32 @@ def intervention_calendar_details_ajax(request, intervention_id):
output_field=IntegerField(), output_field=IntegerField(),
) )
notes = list( notes = (
InterventionNote.objects InterventionNote.objects
.filter(intervention=intervention) .filter(intervention=intervention)
.select_related('note_author', 'updated_by') .select_related('note_author', 'updated_by')
.annotate(type_priority_order=note_priority_case) .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'} IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'}
documents = list( for block in sequenced_data['blocks']:
intervention.documents for doc in block['documents']:
.select_related('uploaded_by') fn = getattr(doc, 'filename', '') or ''
.order_by('-upload_date', '-id') 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)
for doc in documents: doc.is_pdf = ext == '.pdf'
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'
context = { context = {
'intervention': intervention, 'intervention': intervention,
'notes': notes, 'sequenced_blocks': sequenced_data['blocks'],
'documents': documents, 'notes_count': sequenced_data['total_notes_count'],
'notes_count': len(notes), 'documents_count': sequenced_data['total_documents_count'],
'documents_count': len(documents), 'total_count': sequenced_data['total_items_count'],
'total_count': len(notes) + len(documents), 'has_items': sequenced_data['has_items'],
} }
html = render_to_string( html = render_to_string(
@ -1813,9 +1812,9 @@ def intervention_calendar_details_ajax(request, intervention_id):
return JsonResponse({ return JsonResponse({
'success': True, 'success': True,
'notes_count': len(notes), 'notes_count': sequenced_data['total_notes_count'],
'documents_count': len(documents), 'documents_count': sequenced_data['total_documents_count'],
'total_count': len(notes) + len(documents), 'total_count': sequenced_data['total_items_count'],
'html': html, 'html': html,
}) })