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;
}
.cal-nd-filter-btn {
transition: all 0.15s ease;
.cal-sequence-block {
background-color: #f8fafc;
border-color: #e2e8f0 !important;
}

View file

@ -5138,32 +5138,6 @@ document.addEventListener('DOMContentLoaded', function () {
if (summaryText) {
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 {
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 %}
<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="btn-group btn-group-sm" role="group" aria-label="{% translate 'Filtrer les notes et documents' %}">
<button type="button" class="btn btn-outline-primary active cal-nd-filter-btn" data-filter="all">
<i class="bi bi-grid me-1"></i>{% translate "Tout" %}
<span class="badge text-bg-primary rounded-pill ms-1" id="cal-count-all">{{ total_count }}</span>
</button>
<button type="button" class="btn btn-outline-primary cal-nd-filter-btn" data-filter="notes">
<i class="bi bi-sticky me-1"></i>{% translate "Notes" %}
<span class="badge text-bg-secondary rounded-pill ms-1" id="cal-count-notes">{{ notes_count }}</span>
</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>
<h6 class="mb-0 fw-bold text-dark d-flex align-items-center" style="font-size: 0.88rem;">
<i class="bi bi-journal-text text-primary me-2"></i>{% translate "Notes & Documents" %}
</h6>
{% if total_count > 0 %}
<span class="badge text-bg-light border text-muted" style="font-size: 0.75rem;">
{{ total_count }} {% if total_count > 1 %}{% translate "éléments" %}{% else %}{% translate "élément" %}{% endif %}
</span>
{% endif %}
</div>
{% if total_count == 0 %}
{% if not has_items %}
<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>
<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>
</div>
{% else %}
<!-- Section Notes -->
<div id="cal-modal-notes-section" class="cal-nd-section mb-3">
<div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="mb-0 fw-bold text-dark d-flex align-items-center" style="font-size: 0.85rem;">
<i class="bi bi-sticky-fill text-warning me-2"></i>{% translate "Notes" %}
<span class="badge bg-secondary-subtle text-secondary-emphasis rounded-pill ms-2" style="font-size: 0.72rem;">{{ notes_count }}</span>
</h6>
</div>
{% if notes %}
<div class="cal-notes-list d-flex flex-column gap-2">
{% for note in notes %}
<div class="card shadow-none border bg-light-subtle rounded-3 cal-note-card">
<div class="card-body p-2 px-3">
<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" }}
<div class="cal-chronological-blocks d-flex flex-column gap-3">
{% for block in sequenced_blocks %}
{% if block.has_items %}
<div class="cal-sequence-block p-3 rounded-3 bg-light-subtle border">
<!-- En-tête de la phase chronologique -->
<div class="d-flex justify-content-between align-items-center mb-3 pb-2 border-bottom">
<div class="d-flex align-items-center gap-2 flex-wrap">
<span class="badge {{ block.badge_class }} py-1 px-2 d-inline-flex align-items-center gap-1" style="font-size: 0.8rem;">
<i class="bi {{ block.icon }}"></i>
{{ block.title }}
</span>
<span class="text-secondary small fw-medium" style="font-size: 0.75rem;">
{% if block.documents_count > 0 %}{{ block.documents_count }} {% if block.documents_count > 1 %}{% translate "fichiers" %}{% else %}{% translate "fichier" %}{% endif %}{% endif %}
{% if block.documents_count > 0 and block.notes_count > 0 %}&bull;{% endif %}
{% if block.notes_count > 0 %}{{ block.notes_count }} {% if block.notes_count > 1 %}{% translate "notes" %}{% else %}{% translate "note" %}{% endif %}{% endif %}
</span>
</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>
<div class="d-flex justify-content-between align-items-center mt-1 pt-1 border-top border-light-subtle" style="font-size: 0.72rem;">
<span class="text-muted">
<i class="bi bi-person me-1"></i>{{ note.note_author|short_name|default:"—" }}
</span>
{% if note.updated_at %}
<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 %}">
<i class="bi bi-pencil-fill me-1" style="font-size: 0.65rem;"></i>{% translate "Modifié" %}
</span>
<!-- Documents & Photos de la phase -->
{% if block.documents %}
<div class="cal-block-documents mb-3">
<div class="row g-2">
{% for doc in block.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 %}
<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>
{% 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>
{% endif %}
<!-- Section Documents -->
<div id="cal-modal-docs-section" class="cal-nd-section {% if notes %}mt-4 pt-3 border-top{% endif %}">
<div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="mb-0 fw-bold text-dark d-flex align-items-center" style="font-size: 0.85rem;">
<i class="bi bi-paperclip text-primary me-2"></i>{% translate "Documents & Photos" %}
<span class="badge bg-secondary-subtle text-secondary-emphasis rounded-pill ms-2" style="font-size: 0.72rem;">{{ documents_count }}</span>
</h6>
</div>
{% if documents %}
<div class="row g-2">
{% 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 %}
<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>
<!-- Notes de la phase -->
{% if block.notes %}
<div class="cal-block-notes {% if block.documents %}border-top pt-2 mt-2{% endif %} d-flex flex-column gap-2">
{% for note in block.notes %}
<div class="card shadow-none border bg-white rounded-3 cal-note-card">
<div class="card-body p-2 px-3">
<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>
</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 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>
<div class="d-flex justify-content-between align-items-center mt-1 pt-1 border-top border-light-subtle" style="font-size: 0.72rem;">
<span class="text-muted">
<i class="bi bi-person me-1"></i>{{ note.note_author|short_name|default:"—" }}
</span>
{% if note.updated_at %}
<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 %}">
<i class="bi bi-pencil-fill me-1" style="font-size: 0.65rem;"></i>{% translate "Modifié" %}
</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% 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>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>

View file

@ -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)

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.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,
})