loko/streetup/interventions/tests/test_calendar.py

386 lines
16 KiB
Python

import json
from datetime import timedelta
from unittest import mock
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.contrib import messages
from django.contrib.messages import get_messages
from django.core.files.uploadedfile import SimpleUploadedFile
from common.models import UserConfig, Role, Thematic, UserThematics
from contracts.models import Company, CompanyTeam, CompanyMember, Contract, ContractPost, ContractOrder, ContractOrderQuote, ContractOrderQuoteItem
from assets.models import AssetCategory
from interventions.models import (
Intervention,
InterventionContractPost,
InterventionPlanificationTimeLine,
InterventionRealizationTimeLine,
Operation,
InterventionAsset,
OperationTemplate,
OperationTemplateItem,
OperationStatusSet,
OperationStatus,
STATUS_ORDERS,
InterventionDocument,
)
from django.contrib.contenttypes.models import ContentType
from interventions.views import _group_operations_by_asset_and_template, _create_equipment_operations_for_assets
from interventions.permissions import can_manage_occupations, can_delete_document
from django.urls import reverse
from django.utils import timezone
from decimal import Decimal
class CalendarEventsTests(TestCase):
def setUp(self):
User = get_user_model()
self.user = User.objects.create_user(username='calendar-user', password='pwd')
self.role_admin = Role.objects.create(name='admin')
self.user_config = UserConfig.objects.create(user=self.user, is_intern=True)
self.user_config.roles.add(self.role_admin)
self.company = Company.objects.create(name='Test Company')
self.team_a = CompanyTeam.objects.create(name='Team A', company=self.company)
self.team_b = CompanyTeam.objects.create(name='Team B', company=self.company)
# Member belonging to both Team A and Team B
self.member = CompanyMember.objects.create(user=self.user, name='Multi Team Member')
self.member.teams.add(self.team_a, self.team_b)
self.client.login(username='calendar-user', password='pwd')
def test_calendar_events_view_mode_members_no_duplicates(self):
# Request calendar events in member view mode
url = reverse('interventions:interventions_events') + '?view_mode=members'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
# data has 'teams' key
teams = data.get('teams', [])
# Verify that for each team, the member is present only once
for team in teams:
member_ids = [m['id'] for m in team.get('members', [])]
if team['id'] in [self.team_a.id, self.team_b.id]:
# Ensure the member is not duplicated in the same team
self.assertEqual(member_ids.count(self.member.id), 1)
def test_calendar_events_task_replication_fix(self):
# Create an intervention assigned to the member and specifically to Team A
itv = Intervention.objects.create(
title='Team A Task',
status='to_be_processed',
assigned_member=self.member,
assigned_team=self.team_a,
planned_begin_time=timezone.now() + timedelta(hours=1),
planned_end_time=timezone.now() + timedelta(hours=3),
)
url = reverse('interventions:interventions_events') + '?view_mode=members'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
teams = data.get('teams', [])
# Find Team A and Team B in the response
team_a_data = next((t for t in teams if t['id'] == self.team_a.id), None)
team_b_data = next((t for t in teams if t['id'] == self.team_b.id), None)
self.assertIsNotNone(team_a_data)
self.assertIsNotNone(team_b_data)
# Member should have the intervention under Team A
member_in_team_a = next((m for m in team_a_data['members'] if m['id'] == self.member.id), None)
self.assertIsNotNone(member_in_team_a)
itv_ids_in_team_a = [i['id'] for i in member_in_team_a['interventions']]
self.assertIn(itv.id, itv_ids_in_team_a)
# Member should NOT have the intervention under Team B
member_in_team_b = next((m for m in team_b_data['members'] if m['id'] == self.member.id), None)
self.assertIsNotNone(member_in_team_b)
itv_ids_in_team_b = [i['id'] for i in member_in_team_b['interventions']]
self.assertNotIn(itv.id, itv_ids_in_team_b)
def test_schedule_update_coerces_ids(self):
itv = Intervention.objects.create(
title='Test Coercion',
status='to_be_planned',
)
url = reverse('interventions:interventions_schedule_update')
# Send payload with string IDs
payload = {
'intervention_id': itv.id,
'planned_begin_time': (timezone.now() + timedelta(hours=1)).isoformat(),
'planned_end_time': (timezone.now() + timedelta(hours=3)).isoformat(),
'assigned_member_id': str(self.member.id),
'assigned_team_id': str(self.team_b.id),
}
response = self.client.post(
url,
data=json.dumps(payload),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
self.assertTrue(response.json().get('success'))
itv.refresh_from_db()
self.assertEqual(itv.assigned_member_id, self.member.id)
# It should be Team B, not Team A (which would be the fallback if the string ID was not matched)
self.assertEqual(itv.assigned_team_id, self.team_b.id)
def test_schedule_update_rejects_past_dates(self):
itv = Intervention.objects.create(
title='Test Past Date update',
status='to_be_planned',
)
url = reverse('interventions:interventions_schedule_update')
payload = {
'intervention_id': itv.id,
'planned_begin_time': (timezone.now() - timedelta(days=1)).isoformat(),
'planned_end_time': (timezone.now() + timedelta(hours=1)).isoformat(),
'assigned_member_id': str(self.member.id),
'assigned_team_id': str(self.team_a.id),
}
response = self.client.post(
url,
data=json.dumps(payload),
content_type='application/json',
HTTP_ACCEPT_LANGUAGE='fr'
)
self.assertEqual(response.status_code, 400)
self.assertIn("passé", response.json().get('error', '').lower())
def test_backlog_includes_past_unfinished_interventions(self):
past_unfinished_itv = Intervention.objects.create(
title='Past Unfinished corrective',
status='to_be_processed',
maintain_type='corrective',
planned_begin_time=timezone.now() - timedelta(days=2),
planned_end_time=timezone.now() - timedelta(days=2) + timedelta(hours=2),
assigned_team=self.team_a
)
url = reverse('interventions:interventions_events') + '?view_mode=teams_daily'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
backlog_corrective = data.get('backlog_groups', {}).get('corrective', [])
backlog_ids = [item['id'] for item in backlog_corrective]
self.assertIn(past_unfinished_itv.id, backlog_ids)
def test_unassign_intervention_moves_it_to_backlog(self):
from interventions.models import InterventionAssignment
now = timezone.now()
itv = Intervention.objects.create(
title='Intervention Today Team A',
status='to_be_processed',
maintain_type='corrective',
assigned_team=self.team_a,
planned_begin_time=now,
planned_end_time=now + timedelta(hours=2),
)
self.assertEqual(InterventionAssignment.objects.filter(intervention=itv, date=now.date()).count(), 1)
update_url = reverse('interventions:interventions_schedule_update')
payload = {
'intervention_id': itv.id,
'planned_begin_time': now.isoformat(),
'planned_end_time': (now + timedelta(hours=2)).isoformat(),
'assigned_member_id': None,
'assigned_team_id': None,
}
res = self.client.post(update_url, data=json.dumps(payload), content_type='application/json')
self.assertEqual(res.status_code, 200)
itv.refresh_from_db()
self.assertIsNone(itv.assigned_team)
self.assertIsNone(itv.assigned_member)
self.assertEqual(itv.status, 'to_be_planned')
self.assertEqual(InterventionAssignment.objects.filter(intervention=itv, date=now.date()).count(), 0)
url = reverse('interventions:interventions_events') + '?view_mode=teams_daily'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
backlog_corrective = data.get('backlog_groups', {}).get('corrective', [])
self.assertIn(itv.id, [item['id'] for item in backlog_corrective])
for team in data.get('teams', []):
for day_key, day_data in team.get('days', {}).items():
itv_ids_in_day = [item['id'] for item in day_data.get('interventions', [])]
self.assertNotIn(itv.id, itv_ids_in_day)
def test_in_preparation_interventions_only_in_backlog(self):
now = timezone.now()
itv = Intervention.objects.create(
title='In Preparation With Schedule',
status='in_preparation',
maintain_type='corrective',
planned_begin_time=now,
planned_end_time=now + timedelta(hours=2),
)
url = reverse('interventions:interventions_events') + '?view_mode=teams_daily'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
backlog_corrective = data.get('backlog_groups', {}).get('corrective', [])
self.assertIn(itv.id, [item['id'] for item in backlog_corrective])
for team in data.get('teams', []):
for day_key, day_data in team.get('days', {}).items():
itv_ids_in_day = [item['id'] for item in day_data.get('interventions', [])]
self.assertNotIn(itv.id, itv_ids_in_day)
def test_backlog_sorting_order(self):
itv_low_old = Intervention.objects.create(
title='Low Priority Old',
status='to_be_planned',
maintain_type='corrective',
priority='4',
)
itv_urgent_old = Intervention.objects.create(
title='Urgent Priority Old',
status='to_be_planned',
maintain_type='corrective',
priority='1',
)
itv_urgent_new = Intervention.objects.create(
title='Urgent Priority New',
status='to_be_planned',
maintain_type='corrective',
priority='1',
)
url = reverse('interventions:interventions_events') + '?view_mode=teams_daily'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
backlog_corrective = data.get('backlog_groups', {}).get('corrective', [])
ids = [item['id'] for item in backlog_corrective if item['id'] in {itv_low_old.id, itv_urgent_old.id, itv_urgent_new.id}]
self.assertEqual(ids, [itv_urgent_new.id, itv_urgent_old.id, itv_low_old.id])
def test_member_history_ajax_permissions_and_data(self):
from contracts.models import CompanyMember, CompanyMemberHistory
member = CompanyMember.objects.create(name="Jean Dupont", user=self.user)
member.teams.add(self.team_a)
CompanyMemberHistory.objects.create(
member=member,
team=self.team_a,
join_date=timezone.now().date() - timedelta(days=10)
)
url = reverse('interventions:member_history_ajax', args=[member.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn('member', data)
self.assertEqual(data['member']['name'], "Jean Dupont")
self.assertIn('team_history', data)
self.assertTrue(len(data['team_history']) >= 1)
self.assertEqual(data['team_history'][0]['team_name'], self.team_a.name)
def test_unassign_sans_equipe_reverts_status_to_backlog(self):
itv = Intervention.objects.create(
title='Unassigned Test',
status='to_be_processed',
maintain_type='corrective',
priority='2',
planned_begin_time=timezone.now(),
planned_end_time=timezone.now() + timedelta(hours=2),
)
url = reverse('interventions:interventions_schedule_update')
payload = {
'intervention_id': itv.id,
'member_id': None,
'team_id': None,
'unassign': True,
}
response = self.client.post(url, json.dumps(payload), content_type='application/json')
self.assertEqual(response.status_code, 200)
itv.refresh_from_db()
self.assertEqual(itv.status, 'to_be_planned')
self.assertIsNone(itv.planned_begin_time)
self.assertIsNone(itv.planned_end_time)
self.assertIsNone(itv.assigned_team)
self.assertIsNone(itv.assigned_member)
class CalendarSearchTests(TestCase):
def setUp(self):
User = get_user_model()
self.user = User.objects.create_user(username='admin', password='pwd')
self.role_admin = Role.objects.create(name='admin')
self.user_config = UserConfig.objects.create(user=self.user, is_intern=True)
self.user_config.roles.add(self.role_admin)
self.thematic = Thematic.objects.create(code='roads', name_fr='Routes', name_nl='Wegen')
UserThematics.objects.create(
user_config=self.user_config,
thematic=self.thematic,
can_view_interventions=True,
can_edit_interventions=True,
)
self.intv_matching = Intervention.objects.create(
title='Matching Search Keyword',
status='to_be_processed',
thematic=self.thematic,
expected_begin_time=timezone.now(),
expected_end_time=timezone.now() + timedelta(hours=2)
)
self.intv_other = Intervention.objects.create(
title='Completely Unrelated Title',
status='to_be_processed',
thematic=self.thematic,
expected_begin_time=timezone.now(),
expected_end_time=timezone.now() + timedelta(hours=2)
)
self.client.login(username='admin', password='pwd')
def test_interventions_calendar_selected_search(self):
url = reverse('interventions:interventions_calendar') + '?q=Keyword'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['selected_search'], 'Keyword')
def test_interventions_events_filtering(self):
url = reverse('interventions:interventions_events') + '?q=Keyword'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
events = response.json()
# Should only contain the matching intervention
event_ids = [e['id'] for e in events]
self.assertIn(self.intv_matching.id, event_ids)
self.assertNotIn(self.intv_other.id, event_ids)
def test_interventions_geojson_filtering(self):
url = reverse('interventions:interventions_geojson') + '?q=Keyword'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
geojson = response.json()
# GeoJSON is a FeatureCollection, check properties inside features
features = geojson.get('features', [])
feature_ids = [f['properties']['id'] for f in features]
self.assertIn(self.intv_matching.id, feature_ids)
self.assertNotIn(self.intv_other.id, feature_ids)