feat: restrict calendar contract view to authorized contracts for non-admin users

This commit is contained in:
kdeterme 2026-08-17 16:02:45 +02:00
parent d00bc6ff6d
commit dcd35d843f
2 changed files with 162 additions and 2 deletions

View file

@ -8,7 +8,7 @@ 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 common.models import UserConfig, Role, Thematic, UserThematics, UserContractAccess
from contracts.models import Company, CompanyTeam, CompanyMember, Contract, ContractPost, ContractOrder, ContractOrderQuote, ContractOrderQuoteItem
from assets.models import AssetCategory
from interventions.models import (
@ -384,3 +384,155 @@ class CalendarSearchTests(TestCase):
self.assertNotIn(self.intv_other.id, feature_ids)
class CalendarContractFilterTests(TestCase):
def setUp(self):
User = get_user_model()
self.thematic = Thematic.objects.create(code='lighting', name_fr='Éclairage public', name_nl='Openbare verlichting')
self.company = Company.objects.create(name='Provider Corp')
# Deux contrats sur la même thématique
today = timezone.now().date()
self.contract_allowed = Contract.objects.create(
contract_number='CTR-ALLOWED',
company=self.company,
start_date=today - timedelta(days=10),
end_date=today + timedelta(days=365),
is_active=True,
)
self.contract_allowed.thematics.add(self.thematic)
self.contract_forbidden = Contract.objects.create(
contract_number='CTR-FORBIDDEN',
company=self.company,
start_date=today - timedelta(days=10),
end_date=today + timedelta(days=365),
is_active=True,
)
self.contract_forbidden.thematics.add(self.thematic)
# Utilisateur externe
self.external_user = User.objects.create_user(username='external-user', password='pwd')
self.role_external_manager = Role.objects.create(name='external_manager')
self.external_config = UserConfig.objects.create(
user=self.external_user,
is_intern=False,
default_thematic=self.thematic,
)
self.external_config.roles.add(self.role_external_manager)
UserThematics.objects.create(
user_config=self.external_config,
thematic=self.thematic,
can_view_interventions=True,
can_edit_interventions=True,
)
# Accès uniquement à contract_allowed
UserContractAccess.objects.create(
user_config=self.external_config,
contract=self.contract_allowed,
can_view_interventions=True,
)
# Utilisateur admin
self.admin_user = User.objects.create_user(username='admin-user', password='pwd')
self.role_admin = Role.objects.get_or_create(name='admin')[0]
self.admin_config = UserConfig.objects.create(
user=self.admin_user,
is_intern=True,
default_thematic=self.thematic,
)
self.admin_config.roles.add(self.role_admin)
UserThematics.objects.create(
user_config=self.admin_config,
thematic=self.thematic,
can_view_interventions=True,
can_edit_interventions=True,
)
def test_external_user_only_sees_accessible_contracts_in_calendar(self):
self.client.login(username='external-user', password='pwd')
url = reverse('interventions:interventions_calendar')
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200)
contract_choices = response.context['contract_choices']
contract_ids = [c[0] for c in contract_choices]
# L'externe ne doit voir que CTR-ALLOWED, pas CTR-FORBIDDEN
self.assertIn(self.contract_allowed.id, contract_ids)
self.assertNotIn(self.contract_forbidden.id, contract_ids)
def test_admin_user_sees_all_contracts_for_thematic_in_calendar(self):
self.client.login(username='admin-user', password='pwd')
url = reverse('interventions:interventions_calendar')
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200)
contract_choices = response.context['contract_choices']
contract_ids = [c[0] for c in contract_choices]
# L'admin voit tous les contrats de la thématique
self.assertIn(self.contract_allowed.id, contract_ids)
self.assertIn(self.contract_forbidden.id, contract_ids)
def test_internal_user_with_limit_interventions_to_contracts_only_sees_accessible(self):
User = get_user_model()
intern_user = User.objects.create_user(username='intern-limited', password='pwd')
intern_config = UserConfig.objects.create(
user=intern_user,
is_intern=True,
limit_interventions_to_contracts=True,
default_thematic=self.thematic,
)
UserThematics.objects.create(
user_config=intern_config,
thematic=self.thematic,
can_view_interventions=True,
can_edit_interventions=True,
)
UserContractAccess.objects.create(
user_config=intern_config,
contract=self.contract_allowed,
can_view_interventions=True,
)
self.client.login(username='intern-limited', password='pwd')
url = reverse('interventions:interventions_calendar')
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200)
contract_choices = response.context['contract_choices']
contract_ids = [c[0] for c in contract_choices]
self.assertIn(self.contract_allowed.id, contract_ids)
self.assertNotIn(self.contract_forbidden.id, contract_ids)
def test_internal_user_without_limit_interventions_to_contracts_sees_all(self):
User = get_user_model()
intern_user = User.objects.create_user(username='intern-unlimited', password='pwd')
intern_config = UserConfig.objects.create(
user=intern_user,
is_intern=True,
limit_interventions_to_contracts=False,
default_thematic=self.thematic,
)
UserThematics.objects.create(
user_config=intern_config,
thematic=self.thematic,
can_view_interventions=True,
can_edit_interventions=True,
)
self.client.login(username='intern-unlimited', password='pwd')
url = reverse('interventions:interventions_calendar')
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200)
contract_choices = response.context['contract_choices']
contract_ids = [c[0] for c in contract_choices]
self.assertIn(self.contract_allowed.id, contract_ids)
self.assertIn(self.contract_forbidden.id, contract_ids)

View file

@ -209,7 +209,8 @@ def interventions_calendar(request):
.order_by(f'{"name_nl" if lang == "nl" else "name_fr"}')
)
# Contrats : filtrés par les thématiques autorisées
# Contrats : filtrés par les thématiques autorisées et les droits de l'utilisateur
is_admin = bool(role_names & {'admin', 'top_manager'}) or request.user.is_superuser
contracts_qs = (
Contract.objects
.filter(thematics__id__in=allowed_thematic_ids)
@ -217,6 +218,13 @@ def interventions_calendar(request):
.distinct()
.order_by('contract_number')
)
if not is_admin:
if not user_config.is_intern or user_config.limit_interventions_to_contracts:
accessible_contract_ids = UserContractAccess.objects.filter(
user_config=user_config,
can_view_interventions=True,
).values_list('contract_id', flat=True)
contracts_qs = contracts_qs.filter(id__in=accessible_contract_ids)
# Choix enrichis avec les codes de thématique (pour filtrage dynamique JS)
asset_category_choices = [