feat: restrict intervention timeline history and filter controls to internal users

This commit is contained in:
kdeterme 2026-09-25 22:53:32 +02:00
parent 2df4621e5b
commit 7911369740
3 changed files with 54 additions and 1 deletions

View file

@ -13,7 +13,8 @@
</span> </span>
</div> </div>
<!-- Filtre mode d'affichage --> <!-- Filtre mode d'affichage (réservé aux agents internes) -->
{% if is_user_intern %}
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<div class="btn-group btn-group-sm" role="group" aria-label="{% translate 'Filtrer l\'historique' %}"> <div class="btn-group btn-group-sm" role="group" aria-label="{% translate 'Filtrer l\'historique' %}">
<button type="button" <button type="button"
@ -35,6 +36,7 @@
</button> </button>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
<div class="card-body p-3"> <div class="card-body p-3">
@ -42,6 +44,7 @@
{% if events_list %} {% if events_list %}
<div class="history-timeline"> <div class="history-timeline">
{% for event in events_list %} {% for event in events_list %}
{% if is_user_intern or event.is_default_visible %}
<div class="history-timeline-item {% if not event.is_default_visible %}history-item-detailed history-item-modification d-none{% endif %}" <div class="history-timeline-item {% if not event.is_default_visible %}history-item-detailed history-item-modification d-none{% endif %}"
data-event-type="{{ event.event_type }}"> data-event-type="{{ event.event_type }}">
<!-- Node Icon --> <!-- Node Icon -->
@ -144,6 +147,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}

View file

@ -477,5 +477,47 @@ class TimelineHistoryTests(TestCase):
self.assertContains(response, 'history-status-circle') self.assertContains(response, 'history-status-circle')
self.assertContains(response, 'history-status-transition') self.assertContains(response, 'history-status-transition')
self.assertContains(response, 'bg-purple') self.assertContains(response, 'bg-purple')
self.assertContains(response, 'id="btnHistoryFull"')
def test_detail_view_hides_full_history_for_external_user(self):
"""Test that external users (is_intern=False) do NOT see full history or detailed modification/note events."""
User = get_user_model()
ext_user = User.objects.create_user(
username='prestataire_ext',
password='secret_password',
first_name='Pierre',
last_name='Martin'
)
ext_config = UserConfig.objects.create(user=ext_user, is_intern=False)
ext_role, _ = Role.objects.get_or_create(name='external_manager')
ext_config.roles.add(ext_role)
self.intervention.assigned_provider = self.company
self.intervention.save()
record_timeline_event(self.intervention, self.user, event_type='status_change', from_status='in_preparation', to_status='to_be_processed')
record_note_event(self.intervention, self.user, action='create', note_type='comment', content='Note interne confidentielle')
record_document_event(self.intervention, self.user, action='upload', filenames=['secret_doc.pdf'])
self.client.login(username='prestataire_ext', password='secret_password')
url = reverse('interventions:intervention_detail', args=[self.intervention.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# Le filtre "Historique complet" ne doit PAS être affiché
self.assertNotContains(response, 'id="btnHistoryFull"')
self.assertNotContains(response, 'id="btnHistoryDefault"')
# Aucun événement détaillé n'est dans le contexte
self.assertEqual(response.context['timeline_detailed_count'], 0)
self.assertTrue(all(e.is_default_visible for e in response.context['timeline_events']))
# Le contenu confidentiel des notes et documents n'est pas transmis dans l'historique
self.assertNotContains(response, 'Note interne confidentielle')
self.assertNotContains(response, 'secret_doc.pdf')
# Les statuts et assignations restent parfaitement visibles
self.assertContains(response, 'history-status-circle')
self.assertContains(response, 'history-status-transition')

View file

@ -682,6 +682,13 @@ def intervention_detail(request, intervention_id):
insert_idx = 1 if (timeline_events and timeline_events[0].is_creation) else 0 insert_idx = 1 if (timeline_events and timeline_events[0].is_creation) else 0
timeline_events.insert(insert_idx, synth_event) timeline_events.insert(insert_idx, synth_event)
# Sécurité / Confidentialité : l'historique complet (modifications d'attributs, notes, documents, liens)
# est strictement réservé aux agents internes.
# Les utilisateurs externes (prestataires) ne reçoivent que le flux opérationnel
# (statuts, assignations, créations, transferts).
if not is_user_intern:
timeline_events = [e for e in timeline_events if e.is_default_visible]
timeline_default_count = sum(1 for e in timeline_events if e.is_default_visible) timeline_default_count = sum(1 for e in timeline_events if e.is_default_visible)
timeline_detailed_count = sum(1 for e in timeline_events if not e.is_default_visible) timeline_detailed_count = sum(1 for e in timeline_events if not e.is_default_visible)
timeline_modification_count = timeline_detailed_count timeline_modification_count = timeline_detailed_count