feat: restrict calendar contract view to authorized contracts for non-admin users
This commit is contained in:
parent
d00bc6ff6d
commit
dcd35d843f
2 changed files with 162 additions and 2 deletions
|
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
Loading…
Reference in a new issue