feat: improve intervention visibility rules to correctly handle active and replanned interventions

This commit is contained in:
kdeterme 2026-09-14 11:33:24 +02:00
parent 9522eed8f8
commit 660c40f0f7
2 changed files with 121 additions and 48 deletions

View file

@ -194,6 +194,62 @@ def get_allowed_statuses_for_user(user):
return list(statuses) return list(statuses)
def get_today_intervention_visibility_q(today):
"""
Condition Q pour filtrer les interventions actives le jour 'today' :
1. Planifiée pour aujourd'hui (date exacte ou comprise dans la période planifiée [planned_begin, planned_end])
2. Démarrée aujourd'hui (begin_time__date == today)
3. Travail en cours ou en pause (status in ['in_progress', 'on_pause'])
4. Créée aujourd'hui sans planification préalable (urgences du jour)
"""
return (
Q(planned_begin_time__date=today) |
Q(planned_begin_time__date__lte=today, planned_end_time__date__gte=today) |
Q(begin_time__date=today) |
Q(status__in=['in_progress', 'on_pause']) |
Q(planned_begin_time__isnull=True, begin_time__isnull=True, expected_begin_time__date=today) |
Q(planned_begin_time__isnull=True, begin_time__isnull=True, expected_begin_time__isnull=True, creation_time__date=today)
)
def is_intervention_active_for_today(intervention, today):
"""
Vérifie si une instance d'intervention concerne la journée 'today' :
1. Travail en cours ou en pause
2. Planifiée pour aujourd'hui (date exacte ou comprise dans la période planifiée)
3. Démarrée aujourd'hui
4. Créée aujourd'hui sans planification ni démarrage préalable (urgences du jour)
"""
if intervention.status in ('in_progress', 'on_pause'):
return True
if intervention.planned_begin_time:
p_begin = timezone.localtime(intervention.planned_begin_time).date() if timezone.is_aware(intervention.planned_begin_time) else intervention.planned_begin_time.date()
if intervention.planned_end_time:
p_end = timezone.localtime(intervention.planned_end_time).date() if timezone.is_aware(intervention.planned_end_time) else intervention.planned_end_time.date()
if p_begin <= today <= p_end:
return True
elif p_begin == today:
return True
if intervention.begin_time:
b_date = timezone.localtime(intervention.begin_time).date() if timezone.is_aware(intervention.begin_time) else intervention.begin_time.date()
if b_date == today:
return True
if not intervention.planned_begin_time and not intervention.begin_time:
if intervention.expected_begin_time:
e_date = timezone.localtime(intervention.expected_begin_time).date() if timezone.is_aware(intervention.expected_begin_time) else intervention.expected_begin_time.date()
if e_date == today:
return True
elif intervention.creation_time:
c_date = timezone.localtime(intervention.creation_time).date() if timezone.is_aware(intervention.creation_time) else intervention.creation_time.date()
if c_date == today:
return True
return False
def can_view_intervention(user, intervention): def can_view_intervention(user, intervention):
""" """
@ -232,21 +288,8 @@ def can_view_intervention(user, intervention):
local_now = timezone.localtime(timezone.now()) local_now = timezone.localtime(timezone.now())
today = local_now.date() today = local_now.date()
# Vérifier la restriction de date (doit être aujourd'hui) # Vérifier la restriction de date (doit concerner aujourd'hui)
itv_date = ( if not is_intervention_active_for_today(intervention, today):
intervention.begin_time or
intervention.planned_begin_time or
intervention.expected_begin_time or
intervention.creation_time
)
if itv_date:
if timezone.is_aware(itv_date):
itv_date = timezone.localtime(itv_date)
itv_date_only = itv_date.date()
else:
itv_date_only = None
if itv_date_only != today:
return False return False
# Vérifier la restriction d'heure # Vérifier la restriction d'heure
@ -550,17 +593,7 @@ def get_accessible_interventions_for_technician(user):
return base_qs.none() return base_qs.none()
# Filtrer sur les interventions du jour uniquement # Filtrer sur les interventions du jour uniquement
from django.db.models.functions import Coalesce, Cast qs = qs.filter(get_today_intervention_visibility_q(today))
from django.db.models import DateTimeField, DateField
effective_begin_expr = Coalesce(
'begin_time',
'planned_begin_time',
'expected_begin_time',
'creation_time',
output_field=DateTimeField()
)
qs = qs.annotate(effective_begin_date=Cast(effective_begin_expr, output_field=DateField()))
qs = qs.filter(effective_begin_date=today)
return qs return qs
else: else:
qs = base_qs.filter(assigned_member__user=user) qs = base_qs.filter(assigned_member__user=user)
@ -584,17 +617,7 @@ def get_accessible_interventions_for_technician(user):
return base_qs.none() return base_qs.none()
# Filtrer sur les interventions du jour uniquement # Filtrer sur les interventions du jour uniquement
from django.db.models.functions import Coalesce, Cast qs = qs.filter(get_today_intervention_visibility_q(today))
from django.db.models import DateTimeField, DateField
effective_begin_expr = Coalesce(
'begin_time',
'planned_begin_time',
'expected_begin_time',
'creation_time',
output_field=DateTimeField()
)
qs = qs.annotate(effective_begin_date=Cast(effective_begin_expr, output_field=DateField()))
qs = qs.filter(effective_begin_date=today)
return qs return qs
@ -758,17 +781,7 @@ def filter_viewable_interventions_for_user(user, limit_to_default_thematic=False
return Intervention.objects.none() return Intervention.objects.none()
# Filtrer sur les interventions du jour uniquement # Filtrer sur les interventions du jour uniquement
from django.db.models.functions import Coalesce, Cast qs = qs.filter(get_today_intervention_visibility_q(today))
from django.db.models import DateTimeField, DateField
effective_begin_expr = Coalesce(
'begin_time',
'planned_begin_time',
'expected_begin_time',
'creation_time',
output_field=DateTimeField()
)
qs = qs.annotate(effective_begin_date=Cast(effective_begin_expr, output_field=DateField()))
qs = qs.filter(effective_begin_date=today)
# Exclusion des interventions bloquées pour les utilisateurs externes (prestataires) # Exclusion des interventions bloquées pour les utilisateurs externes (prestataires)
if not getattr(user_config, 'is_intern', False): if not getattr(user_config, 'is_intern', False):
qs = qs.exclude(is_blocked_due_to_contract=True) qs = qs.exclude(is_blocked_due_to_contract=True)

View file

@ -208,6 +208,66 @@ class TeamDailyVisibilityTests(TestCase):
visible_qs = filter_viewable_interventions_for_user(self.tech_user) visible_qs = filter_viewable_interventions_for_user(self.tech_user)
self.assertIn(itv_yesterday, visible_qs) self.assertIn(itv_yesterday, visible_qs)
@mock.patch('django.utils.timezone.now')
def test_visibility_replanned_past_intervention_visible_today(self, mock_now):
"""
Une intervention débutée dans le passé mais replanifiée pour aujourd'hui
doit être visible par le technicien restreint à aujourd'hui.
De même pour une intervention en cours (in_progress).
Une intervention débutée dans le passé et replanifiée pour demain ne doit pas être visible aujourd'hui.
"""
from datetime import datetime
import pytz
from interventions.models import Intervention
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
# Fix today to 2026-06-17 at 8:00 AM UTC (after start hour)
mock_now.return_value = datetime(2026, 6, 17, 8, 0, 0, tzinfo=pytz.UTC)
# 1. Began 2 weeks ago, replanned for today -> MUST be visible today
itv_replanned_today = Intervention.objects.create(
title='Replanned Today Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
assigned_team=self.team,
begin_time=datetime(2026, 6, 1, 9, 0, 0, tzinfo=pytz.UTC),
planned_begin_time=datetime(2026, 6, 17, 10, 0, 0, tzinfo=pytz.UTC),
)
# 2. Began 2 weeks ago, replanned for tomorrow -> MUST NOT be visible today
itv_replanned_tomorrow = Intervention.objects.create(
title='Replanned Tomorrow Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
assigned_team=self.team,
begin_time=datetime(2026, 6, 1, 9, 0, 0, tzinfo=pytz.UTC),
planned_begin_time=datetime(2026, 6, 18, 10, 0, 0, tzinfo=pytz.UTC),
)
# 3. Began yesterday, currently in_progress without today's planned_begin_time -> MUST be visible
itv_in_progress = Intervention.objects.create(
title='In Progress Task',
status='in_progress',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
assigned_team=self.team,
begin_time=datetime(2026, 6, 16, 15, 0, 0, tzinfo=pytz.UTC),
)
self.assertTrue(can_view_intervention(self.tech_user, itv_replanned_today))
self.assertFalse(can_view_intervention(self.tech_user, itv_replanned_tomorrow))
self.assertTrue(can_view_intervention(self.tech_user, itv_in_progress))
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
self.assertIn(itv_replanned_today, visible_qs)
self.assertNotIn(itv_replanned_tomorrow, visible_qs)
self.assertIn(itv_in_progress, visible_qs)