feat: improve intervention visibility rules to correctly handle active and replanned interventions
This commit is contained in:
parent
9522eed8f8
commit
660c40f0f7
2 changed files with 121 additions and 48 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue