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.contrib.messages import get_messages
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
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 contracts.models import Company, CompanyTeam, CompanyMember, Contract, ContractPost, ContractOrder, ContractOrderQuote, ContractOrderQuoteItem
|
||||||
from assets.models import AssetCategory
|
from assets.models import AssetCategory
|
||||||
from interventions.models import (
|
from interventions.models import (
|
||||||
|
|
@ -384,3 +384,155 @@ class CalendarSearchTests(TestCase):
|
||||||
self.assertNotIn(self.intv_other.id, feature_ids)
|
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"}')
|
.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 = (
|
contracts_qs = (
|
||||||
Contract.objects
|
Contract.objects
|
||||||
.filter(thematics__id__in=allowed_thematic_ids)
|
.filter(thematics__id__in=allowed_thematic_ids)
|
||||||
|
|
@ -217,6 +218,13 @@ def interventions_calendar(request):
|
||||||
.distinct()
|
.distinct()
|
||||||
.order_by('contract_number')
|
.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)
|
# Choix enrichis avec les codes de thématique (pour filtrage dynamique JS)
|
||||||
asset_category_choices = [
|
asset_category_choices = [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue