473 lines
19 KiB
Python
473 lines
19 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 OccupationPermissionsTests(TestCase):
|
|
def setUp(self):
|
|
self.User = get_user_model()
|
|
self.technician_role, _ = Role.objects.get_or_create(name='technician')
|
|
self.external_role, _ = Role.objects.get_or_create(name='external_manager')
|
|
|
|
self.company = Company.objects.create(name='Test Company')
|
|
self.team = CompanyTeam.objects.create(name='Team A', company=self.company)
|
|
|
|
def _create_user_with_role(self, username, role):
|
|
user = self.User.objects.create_user(username=username, password='pwd')
|
|
user_config = UserConfig.objects.create(user=user)
|
|
user_config.roles.add(role)
|
|
return user, user_config
|
|
|
|
def test_can_manage_occupations_for_assigned_technician(self):
|
|
user, _ = self._create_user_with_role('tech', self.technician_role)
|
|
member = CompanyMember.objects.create(user=user, name='Tech One')
|
|
member.teams.add(self.team)
|
|
intervention = Intervention.objects.create(title='Test intervention', assigned_member=member)
|
|
|
|
self.assertTrue(can_manage_occupations(user, intervention))
|
|
def test_cannot_manage_occupations_for_unrelated_technician(self):
|
|
user, _ = self._create_user_with_role('other-tech', self.technician_role)
|
|
member = CompanyMember.objects.create(user=user, name='Tech Two')
|
|
member.teams.add(self.team)
|
|
other_company = Company.objects.create(name='Other Company')
|
|
other_team = CompanyTeam.objects.create(name='Team B', company=other_company)
|
|
intervention = Intervention.objects.create(title='Another intervention', assigned_team=other_team)
|
|
|
|
self.assertFalse(can_manage_occupations(user, intervention))
|
|
|
|
def test_external_manager_can_manage(self):
|
|
user, _ = self._create_user_with_role('external', self.external_role)
|
|
intervention = Intervention.objects.create(title='External intervention')
|
|
|
|
self.assertTrue(can_manage_occupations(user, intervention))
|
|
|
|
|
|
|
|
|
|
class TeamDailyVisibilityTests(TestCase):
|
|
def setUp(self):
|
|
from contracts.models import Company, CompanyTeam, CompanyMember
|
|
from common.models import Role, UserConfig, Thematic, UserThematics, UserContractAccess
|
|
|
|
self.User = get_user_model()
|
|
self.tech_user = self.User.objects.create_user(username='tech-user', password='pwd')
|
|
self.role_tech, _ = Role.objects.get_or_create(name='technician')
|
|
self.tech_config = UserConfig.objects.create(user=self.tech_user, is_intern=False)
|
|
self.tech_config.roles.add(self.role_tech)
|
|
|
|
self.company = Company.objects.create(name='Test Company')
|
|
|
|
# Team with restriction: only today's interventions from 6 AM
|
|
from datetime import time
|
|
self.team = CompanyTeam.objects.create(
|
|
name='Restricted Team',
|
|
company=self.company,
|
|
restrict_today_interventions_visibility=True,
|
|
visibility_start_hour=time(6, 0)
|
|
)
|
|
|
|
self.member = CompanyMember.objects.create(user=self.tech_user, name='Technician Doe')
|
|
self.member.teams.add(self.team)
|
|
|
|
self.thematic = Thematic.objects.create(code='roads', name_fr='Voirie', name_nl='Wegen')
|
|
UserThematics.objects.create(
|
|
user_config=self.tech_config,
|
|
thematic=self.thematic,
|
|
can_view_interventions=True,
|
|
can_edit_interventions=True,
|
|
)
|
|
|
|
# Grant access via contract as well
|
|
self.contract = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number='C-TEST',
|
|
start_date=timezone.now().date() - timedelta(days=10),
|
|
end_date=timezone.now().date() + timedelta(days=10),
|
|
is_active=True
|
|
)
|
|
UserContractAccess.objects.create(
|
|
user_config=self.tech_config,
|
|
contract=self.contract,
|
|
can_view_interventions=True,
|
|
)
|
|
|
|
@mock.patch('django.utils.timezone.now')
|
|
def test_visibility_before_and_after_start_hour(self, mock_now):
|
|
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
|
|
# 1. Test before 6 AM (3:30 AM UTC = 5:30 AM Brussels local time CEST)
|
|
mock_now.return_value = datetime(2026, 6, 17, 3, 30, 0, tzinfo=pytz.UTC)
|
|
|
|
# Create today's, yesterday's, and tomorrow's interventions
|
|
itv_today = Intervention.objects.create(
|
|
title='Today Task',
|
|
status='to_be_processed',
|
|
contract=self.contract,
|
|
thematic=self.thematic,
|
|
assigned_provider=self.company,
|
|
planned_begin_time=datetime(2026, 6, 17, 10, 0, 0, tzinfo=pytz.UTC),
|
|
)
|
|
itv_yesterday = Intervention.objects.create(
|
|
title='Yesterday Task',
|
|
status='to_be_processed',
|
|
contract=self.contract,
|
|
thematic=self.thematic,
|
|
assigned_provider=self.company,
|
|
planned_begin_time=datetime(2026, 6, 16, 10, 0, 0, tzinfo=pytz.UTC),
|
|
)
|
|
itv_tomorrow = Intervention.objects.create(
|
|
title='Tomorrow Task',
|
|
status='to_be_processed',
|
|
contract=self.contract,
|
|
thematic=self.thematic,
|
|
assigned_provider=self.company,
|
|
planned_begin_time=datetime(2026, 6, 18, 10, 0, 0, tzinfo=pytz.UTC),
|
|
)
|
|
|
|
# Before 6 AM, technician should not see anything (empty queryset and False for can_view)
|
|
self.assertFalse(can_view_intervention(self.tech_user, itv_today))
|
|
self.assertFalse(can_view_intervention(self.tech_user, itv_yesterday))
|
|
|
|
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
|
|
self.assertEqual(visible_qs.count(), 0)
|
|
|
|
# 2. Test after 6 AM (7:30 AM UTC = 9:30 AM Brussels local time)
|
|
mock_now.return_value = datetime(2026, 6, 17, 7, 30, 0, tzinfo=pytz.UTC)
|
|
|
|
# After 6 AM, they should only be able to view today's intervention
|
|
self.assertTrue(can_view_intervention(self.tech_user, itv_today))
|
|
self.assertFalse(can_view_intervention(self.tech_user, itv_yesterday))
|
|
self.assertFalse(can_view_intervention(self.tech_user, itv_tomorrow))
|
|
|
|
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
|
|
self.assertEqual(visible_qs.count(), 1)
|
|
self.assertIn(itv_today, visible_qs)
|
|
self.assertNotIn(itv_yesterday, visible_qs)
|
|
self.assertNotIn(itv_tomorrow, visible_qs)
|
|
|
|
@mock.patch('django.utils.timezone.now')
|
|
def test_visibility_privileged_users_not_restricted(self, mock_now):
|
|
from datetime import datetime
|
|
import pytz
|
|
from interventions.models import Intervention
|
|
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
|
|
from common.models import Role
|
|
|
|
# Fix today to 2026-06-17 at 3:30 AM UTC (before start hour)
|
|
mock_now.return_value = datetime(2026, 6, 17, 3, 30, 0, tzinfo=pytz.UTC)
|
|
|
|
itv_yesterday = Intervention.objects.create(
|
|
title='Yesterday Task',
|
|
status='to_be_processed',
|
|
contract=self.contract,
|
|
thematic=self.thematic,
|
|
assigned_provider=self.company,
|
|
planned_begin_time=datetime(2026, 6, 16, 10, 0, 0, tzinfo=pytz.UTC),
|
|
)
|
|
|
|
# If user gets role external_manager, they should see it despite being in the restricted team
|
|
role_ext_mgr, _ = Role.objects.get_or_create(name='external_manager')
|
|
self.tech_config.roles.add(role_ext_mgr)
|
|
|
|
self.assertTrue(can_view_intervention(self.tech_user, itv_yesterday))
|
|
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
|
|
self.assertIn(itv_yesterday, visible_qs)
|
|
|
|
|
|
|
|
|
|
class TeamAssignedOnlyVisibilityTests(TestCase):
|
|
def setUp(self):
|
|
from contracts.models import Company, CompanyTeam, CompanyMember
|
|
from common.models import Role, UserConfig, Thematic, UserThematics, UserContractAccess
|
|
|
|
self.User = get_user_model()
|
|
self.tech_user = self.User.objects.create_user(username='tech-user-2', password='pwd')
|
|
self.role_tech, _ = Role.objects.get_or_create(name='technician')
|
|
self.tech_config = UserConfig.objects.create(user=self.tech_user, is_intern=False)
|
|
self.tech_config.roles.add(self.role_tech)
|
|
|
|
self.company = Company.objects.create(name='Test Company 2')
|
|
|
|
# Restricted and Unrestricted Teams
|
|
self.team_restricted = CompanyTeam.objects.create(
|
|
name='Restricted Team 2',
|
|
company=self.company,
|
|
restrict_to_assigned_interventions=True
|
|
)
|
|
self.team_unrestricted = CompanyTeam.objects.create(
|
|
name='Unrestricted Team 2',
|
|
company=self.company,
|
|
restrict_to_assigned_interventions=False
|
|
)
|
|
self.other_team = CompanyTeam.objects.create(
|
|
name='Other Team 2',
|
|
company=self.company,
|
|
restrict_to_assigned_interventions=False
|
|
)
|
|
|
|
self.member = CompanyMember.objects.create(user=self.tech_user, name='Technician Doe 2')
|
|
|
|
self.thematic = Thematic.objects.create(code='roads2', name_fr='Voirie 2', name_nl='Wegen 2')
|
|
UserThematics.objects.create(
|
|
user_config=self.tech_config,
|
|
thematic=self.thematic,
|
|
can_view_interventions=True,
|
|
can_edit_interventions=True,
|
|
)
|
|
|
|
self.contract = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number='C-TEST-2',
|
|
start_date=timezone.now().date() - timedelta(days=10),
|
|
end_date=timezone.now().date() + timedelta(days=10),
|
|
is_active=True
|
|
)
|
|
UserContractAccess.objects.create(
|
|
user_config=self.tech_config,
|
|
contract=self.contract,
|
|
can_view_interventions=True,
|
|
)
|
|
|
|
def test_visibility_restricted_team_only(self):
|
|
from interventions.models import Intervention
|
|
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
|
|
|
|
# Member only belongs to restricted team
|
|
self.member.teams.add(self.team_restricted)
|
|
|
|
# 1. Assigned to team
|
|
itv_team = Intervention.objects.create(
|
|
title='Team Task',
|
|
status='to_be_processed',
|
|
contract=self.contract,
|
|
thematic=self.thematic,
|
|
assigned_provider=self.company,
|
|
assigned_team=self.team_restricted,
|
|
)
|
|
# 2. Assigned to member
|
|
itv_member = Intervention.objects.create(
|
|
title='Member Task',
|
|
status='to_be_processed',
|
|
contract=self.contract,
|
|
thematic=self.thematic,
|
|
assigned_provider=self.company,
|
|
assigned_member=self.member,
|
|
)
|
|
# 3. Unassigned company task
|
|
itv_unassigned = Intervention.objects.create(
|
|
title='Unassigned Task',
|
|
status='to_be_processed',
|
|
contract=self.contract,
|
|
thematic=self.thematic,
|
|
assigned_provider=self.company,
|
|
)
|
|
# 4. Assigned to other team
|
|
itv_other = Intervention.objects.create(
|
|
title='Other Team Task',
|
|
status='to_be_processed',
|
|
contract=self.contract,
|
|
thematic=self.thematic,
|
|
assigned_provider=self.company,
|
|
assigned_team=self.other_team,
|
|
)
|
|
|
|
# Check individual visibility
|
|
self.assertTrue(can_view_intervention(self.tech_user, itv_team))
|
|
self.assertTrue(can_view_intervention(self.tech_user, itv_member))
|
|
self.assertFalse(can_view_intervention(self.tech_user, itv_unassigned))
|
|
self.assertFalse(can_view_intervention(self.tech_user, itv_other))
|
|
|
|
# Check list visibility
|
|
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
|
|
self.assertEqual(visible_qs.count(), 2)
|
|
self.assertIn(itv_team, visible_qs)
|
|
self.assertIn(itv_member, visible_qs)
|
|
self.assertNotIn(itv_unassigned, visible_qs)
|
|
self.assertNotIn(itv_other, visible_qs)
|
|
|
|
def test_visibility_unrestricted_team(self):
|
|
from interventions.models import Intervention
|
|
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
|
|
|
|
# Member only belongs to unrestricted team
|
|
self.member.teams.add(self.team_unrestricted)
|
|
|
|
# Unassigned company task
|
|
itv_unassigned = Intervention.objects.create(
|
|
title='Unassigned Task',
|
|
status='to_be_processed',
|
|
contract=self.contract,
|
|
thematic=self.thematic,
|
|
assigned_provider=self.company,
|
|
)
|
|
|
|
self.assertTrue(can_view_intervention(self.tech_user, itv_unassigned))
|
|
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
|
|
self.assertIn(itv_unassigned, visible_qs)
|
|
|
|
def test_visibility_mixed_teams(self):
|
|
from interventions.models import Intervention
|
|
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
|
|
|
|
# Member belongs to both restricted and unrestricted teams of the same company
|
|
self.member.teams.add(self.team_restricted, self.team_unrestricted)
|
|
|
|
# Unassigned company task
|
|
itv_unassigned = Intervention.objects.create(
|
|
title='Unassigned Task',
|
|
status='to_be_processed',
|
|
contract=self.contract,
|
|
thematic=self.thematic,
|
|
assigned_provider=self.company,
|
|
)
|
|
|
|
self.assertTrue(can_view_intervention(self.tech_user, itv_unassigned))
|
|
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
|
|
self.assertIn(itv_unassigned, visible_qs)
|
|
|
|
def test_visibility_privileged_user_not_restricted(self):
|
|
from interventions.models import Intervention
|
|
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
|
|
from common.models import Role
|
|
|
|
# Member belongs only to restricted team
|
|
self.member.teams.add(self.team_restricted)
|
|
|
|
# Give the user a privileged role: operator
|
|
role_operator, _ = Role.objects.get_or_create(name='operator')
|
|
self.tech_config.roles.add(role_operator)
|
|
|
|
# Unassigned company task
|
|
itv_unassigned = Intervention.objects.create(
|
|
title='Unassigned Task',
|
|
status='to_be_processed',
|
|
contract=self.contract,
|
|
thematic=self.thematic,
|
|
assigned_provider=self.company,
|
|
)
|
|
|
|
self.assertTrue(can_view_intervention(self.tech_user, itv_unassigned))
|
|
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
|
|
self.assertIn(itv_unassigned, visible_qs)
|
|
|
|
def test_is_restricted_team_context(self):
|
|
from interventions.models import Intervention
|
|
from django.urls import reverse
|
|
|
|
# 1. User belongs only to restricted team
|
|
self.member.teams.add(self.team_restricted)
|
|
|
|
itv = Intervention.objects.create(
|
|
title='Team Task',
|
|
status='to_be_processed',
|
|
contract=self.contract,
|
|
thematic=self.thematic,
|
|
assigned_provider=self.company,
|
|
assigned_team=self.team_restricted,
|
|
)
|
|
|
|
self.client.login(username='tech-user-2', password='pwd')
|
|
url = reverse('mobile:intervention_detail_mobile', args=[itv.id])
|
|
resp = self.client.get(url)
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertTrue(resp.context['is_restricted_team'])
|
|
|
|
# 2. User has an unrestricted team too
|
|
self.member.teams.add(self.team_unrestricted)
|
|
resp = self.client.get(url)
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertFalse(resp.context['is_restricted_team'])
|
|
|
|
|
|
|
|
|
|
class CreatorVisibilityTests(TestCase):
|
|
def setUp(self):
|
|
from django.contrib.auth import get_user_model
|
|
from common.models import Role, UserConfig, Thematic, UserThematics
|
|
self.User = get_user_model()
|
|
self.user1 = self.User.objects.create_user(username='creator-user-1', password='pwd')
|
|
self.user2 = self.User.objects.create_user(username='other-user-2', password='pwd')
|
|
|
|
self.role_tech, _ = Role.objects.get_or_create(name='technician')
|
|
|
|
self.config1 = UserConfig.objects.create(user=self.user1, is_intern=False, limit_interventions_to_contracts=True)
|
|
self.config1.roles.add(self.role_tech)
|
|
|
|
self.config2 = UserConfig.objects.create(user=self.user2, is_intern=False, limit_interventions_to_contracts=True)
|
|
self.config2.roles.add(self.role_tech)
|
|
|
|
self.thematic = Thematic.objects.create(code='roads_creator', name_fr='Voirie Creator', name_nl='Wegen Creator')
|
|
UserThematics.objects.create(
|
|
user_config=self.config1,
|
|
thematic=self.thematic,
|
|
can_view_interventions=True,
|
|
can_edit_interventions=True,
|
|
)
|
|
UserThematics.objects.create(
|
|
user_config=self.config2,
|
|
thematic=self.thematic,
|
|
can_view_interventions=True,
|
|
can_edit_interventions=True,
|
|
)
|
|
|
|
def test_creator_can_view_own_created_intervention(self):
|
|
from interventions.models import Intervention
|
|
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
|
|
|
|
# Create intervention with no contract, created by user1
|
|
itv = Intervention.objects.create(
|
|
title='Created by User 1',
|
|
status='to_be_processed',
|
|
contract=None,
|
|
thematic=self.thematic,
|
|
created_by=self.user1,
|
|
)
|
|
|
|
# User 1 should see it (creator)
|
|
self.assertTrue(can_view_intervention(self.user1, itv))
|
|
self.assertIn(itv, filter_viewable_interventions_for_user(self.user1))
|
|
|
|
# User 2 should NOT see it (not the creator, and no contract assigned)
|
|
self.assertFalse(can_view_intervention(self.user2, itv))
|
|
self.assertNotIn(itv, filter_viewable_interventions_for_user(self.user2))
|
|
|
|
|
|
|
|
|