diff --git a/streetup/contracts/views.py b/streetup/contracts/views.py index 93c8449..78bfdde 100644 --- a/streetup/contracts/views.py +++ b/streetup/contracts/views.py @@ -189,6 +189,9 @@ def contracts_orders_detail(request, order_id): # Déterminer les droits de création d'interventions par type user_roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set() can_create_preventive = bool(user_roles & {'admin', 'manager', 'controller'}) + if not can_create_preventive and 'external_manager' in user_roles: + from interventions.permissions import is_external_manager_with_contract_creation_rights + can_create_preventive = is_external_manager_with_contract_creation_rights(request.user) can_create_ameliorative = bool(user_roles & {'admin', 'manager', 'controller', 'external_manager'}) context = { diff --git a/streetup/home/views.py b/streetup/home/views.py index d69bdc7..b183bd6 100644 --- a/streetup/home/views.py +++ b/streetup/home/views.py @@ -173,6 +173,9 @@ def index(request): # Détermine les droits de création d'interventions par type user_roles = set(user_config.roles.values_list('name', flat=True)) can_create_preventive = bool(user_roles & {'admin', 'manager', 'controller', 'top_manager'}) + if not can_create_preventive and 'external_manager' in user_roles: + from interventions.permissions import is_external_manager_with_contract_creation_rights + can_create_preventive = is_external_manager_with_contract_creation_rights(request.user) can_create_ameliorative = bool(user_roles & {'admin', 'manager', 'controller', 'external_manager', 'operator', 'top_manager'}) is_operator = 'operator' in user_roles is_admin = 'admin' in user_roles diff --git a/streetup/interventions/permissions.py b/streetup/interventions/permissions.py index 39eb458..a00bcd5 100644 --- a/streetup/interventions/permissions.py +++ b/streetup/interventions/permissions.py @@ -865,8 +865,9 @@ def intervention_add_permission_required(view_func): def intervention_add_preventive_permission_required(view_func): """ Décorateur pour les vues de création d'intervention préventive. - Seuls les admin, manager et controller peuvent créer des interventions préventives. - Les external_managers et operators ne peuvent pas en créer. + Seuls les admin, manager, controller, top_manager et les external_managers + avec droit de création d'interventions sur au moins un contrat peuvent créer des + interventions préventives. """ @wraps(view_func) def _wrapped_view(request, thematic_code=None, *args, **kwargs): @@ -878,11 +879,14 @@ def intervention_add_preventive_permission_required(view_func): if not can_add_intervention(request.user, thematic): raise PermissionDenied(_("Vous n'avez pas la permission d'ajouter une intervention pour cette thématique.")) - # Seuls admin, manager, controller peuvent créer des préventives + # Seuls admin, manager, controller, top_manager, et external_manager avec droits try: user_config = UserConfig.objects.get(user=request.user) user_roles = set(user_config.roles.values_list('name', flat=True)) - if not (user_roles & {'admin', 'manager', 'controller', 'top_manager'}): + is_allowed = bool(user_roles & {'admin', 'manager', 'controller', 'top_manager'}) + if not is_allowed and 'external_manager' in user_roles: + is_allowed = is_external_manager_with_contract_creation_rights(request.user) + if not is_allowed: raise PermissionDenied(_("Vous n'avez pas la permission de créer des interventions préventives.")) except UserConfig.DoesNotExist: raise PermissionDenied(_("Vous n'avez pas la permission de créer des interventions préventives.")) @@ -1478,8 +1482,8 @@ def should_show_created_by_provider_badge(user, intervention): except UserConfig.DoesNotExist: return False - # Visible pour admin, manager et controller - return user_config.roles.filter(name__in=['admin', 'manager', 'controller']).exists() + # Visible pour admin, manager, controller et top_manager + return user_config.roles.filter(name__in=['admin', 'manager', 'controller', 'top_manager']).exists() def is_external_manager_with_contract_creation_rights(user, contract=None): diff --git a/streetup/interventions/tests/test_preventive_templates.py b/streetup/interventions/tests/test_preventive_templates.py index 43e2723..9c765ec 100644 --- a/streetup/interventions/tests/test_preventive_templates.py +++ b/streetup/interventions/tests/test_preventive_templates.py @@ -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 ( @@ -449,5 +449,160 @@ class AssetOperationCleanupTests(TestCase): self.assertEqual(remaining_ops.count(), 2) +class ExternalManagerPreventivePermissionsTests(TestCase): + def setUp(self): + User = get_user_model() + self.role_ext_manager, _ = Role.objects.get_or_create(name='external_manager') + self.role_manager, _ = Role.objects.get_or_create(name='manager') + + self.thematic = Thematic.objects.create(code='test_thema', name_fr='Test', name_nl='Test') + + self.provider = Company.objects.create(name="Test Provider") + from datetime import date, timedelta + today = date.today() + self.contract_with_rights = Contract.objects.create( + contract_number="C-001", + company=self.provider, + is_active=True, + start_date=today - timedelta(days=30), + end_date=today + timedelta(days=365) + ) + self.contract_with_rights.thematics.add(self.thematic) + + self.contract_without_rights = Contract.objects.create( + contract_number="C-002", + company=self.provider, + is_active=True, + start_date=today - timedelta(days=30), + end_date=today + timedelta(days=365) + ) + self.contract_without_rights.thematics.add(self.thematic) + + # 1. User with creation rights + self.user_with_rights = User.objects.create_user(username='ext_with_rights', password='pwd') + self.uc_with_rights = UserConfig.objects.create(user=self.user_with_rights, is_intern=False) + self.uc_with_rights.roles.add(self.role_ext_manager) + UserThematics.objects.create( + user_config=self.uc_with_rights, + thematic=self.thematic, + can_view_assets=True, + can_edit_interventions=True, + ) + UserContractAccess.objects.create( + user_config=self.uc_with_rights, + contract=self.contract_with_rights, + can_create_interventions=True + ) + + # 2. User without creation rights + self.user_without_rights = User.objects.create_user(username='ext_without_rights', password='pwd') + self.uc_without_rights = UserConfig.objects.create(user=self.user_without_rights, is_intern=False) + self.uc_without_rights.roles.add(self.role_ext_manager) + UserThematics.objects.create( + user_config=self.uc_without_rights, + thematic=self.thematic, + can_view_assets=True, + can_edit_interventions=True, + ) + UserContractAccess.objects.create( + user_config=self.uc_without_rights, + contract=self.contract_without_rights, + can_create_interventions=False + ) + + # 3. An internal manager to view the detail page and verify the badge + self.internal_manager = User.objects.create_user(username='int_manager', password='pwd') + self.uc_int = UserConfig.objects.create(user=self.internal_manager, is_intern=True) + self.uc_int.roles.add(self.role_manager) + + def test_access_to_add_preventive_view(self): + # User with rights should access + self.client.login(username='ext_with_rights', password='pwd') + url = reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': 'test_thema'}) + + # We need to set up a mock draft in the session + session = self.client.session + session['intervention_drafts'] = { + 'draft-1': { + 'locations': [], + 'assets': [], + 'form_data': {} + } + } + session.save() + + response = self.client.get(f"{url}?draft_id=draft-1") + self.assertEqual(response.status_code, 200) + + # User without rights should be Forbidden (403) + self.client.login(username='ext_without_rights', password='pwd') + response = self.client.get(f"{url}?draft_id=draft-1") + self.assertEqual(response.status_code, 403) + + def test_contracts_filtered_in_form(self): + self.client.login(username='ext_with_rights', password='pwd') + url = reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': 'test_thema'}) + + session = self.client.session + session['intervention_drafts'] = { + 'draft-1': { + 'locations': [], + 'assets': [], + 'form_data': {} + } + } + session.save() + + response = self.client.get(f"{url}?draft_id=draft-1") + self.assertEqual(response.status_code, 200) + + # Verify that contract_with_rights is in form's queryset, but contract_without_rights is not + contracts_queryset = response.context['form'].fields['contract'].queryset + self.assertIn(self.contract_with_rights, contracts_queryset) + self.assertNotIn(self.contract_without_rights, contracts_queryset) + + def test_preventive_intervention_created_by_provider(self): + self.client.login(username='ext_with_rights', password='pwd') + url = reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': 'test_thema'}) + + session = self.client.session + session['intervention_drafts'] = { + 'draft-1': { + 'locations': [], + 'assets': [], + 'form_data': {} + } + } + session.save() + + # Submit creation form + post_data = { + 'title': 'Test Preventive Creation', + 'status': 'in_preparation', + 'type': 'care', + 'contract': self.contract_with_rights.id, + 'expected_begin_time': '2026-08-13T12:00', + 'expected_end_time': '2026-08-13T13:00', + 'recurrence_count': 0, + 'type_of_selected_elements': 'locations', + 'intervention_creation_mode': 'single_for_all', + 'draft_id': 'draft-1', + } + response = self.client.post(url, post_data) + self.assertEqual(response.status_code, 302) + + # Check created intervention + intervention = Intervention.objects.filter(title='Test Preventive Creation').first() + self.assertIsNotNone(intervention) + self.assertEqual(intervention.created_by, self.user_with_rights) + self.assertTrue(intervention.created_by_provider) + self.assertEqual(intervention.maintain_type, 'preventive') + + # Check that the badge is shown for internal manager + from interventions.permissions import should_show_created_by_provider_badge + self.assertTrue(should_show_created_by_provider_badge(self.internal_manager, intervention)) + + + diff --git a/streetup/interventions/views/forms.py b/streetup/interventions/views/forms.py index 6d3a28e..0a08852 100644 --- a/streetup/interventions/views/forms.py +++ b/streetup/interventions/views/forms.py @@ -540,6 +540,9 @@ class PreventiveInterventionForm(forms.Form): # 2. Contrats associés à au moins une thématique que l'utilisateur peut éditer # OU contrats sans thématiques (pour la rétrocompatibilité) base_contracts = user_config.contracts.filter(is_active=True) + user_roles = set(user_config.roles.values_list('name', flat=True)) + if 'external_manager' in user_roles and not (user_roles & {'admin', 'manager', 'controller', 'top_manager'}): + base_contracts = base_contracts.filter(usercontractaccess__can_create_interventions=True) # Si une thématique spécifique est fournie, filtrer aussi par cette thématique if thematic: diff --git a/streetup/interventions/views/general.py b/streetup/interventions/views/general.py index de6df5a..f2dc3f5 100644 --- a/streetup/interventions/views/general.py +++ b/streetup/interventions/views/general.py @@ -220,8 +220,9 @@ def index(request): # - Améliorative : admin, manager, controller, external_manager, operator user_roles = set(user_config.roles.values_list('name', flat=True)) - # Préventive : uniquement admin, manager, controller can_create_preventive = bool(user_roles & {'admin', 'manager', 'controller'}) + if not can_create_preventive and 'external_manager' in user_roles: + can_create_preventive = is_external_manager_with_contract_creation_rights(request.user) # Améliorative : admin, manager, controller, external_manager, operator can_create_ameliorative = bool(user_roles & {'admin', 'manager', 'controller', 'external_manager', 'operator'}) diff --git a/streetup/interventions/views/preventive.py b/streetup/interventions/views/preventive.py index a09c6a9..df6142b 100644 --- a/streetup/interventions/views/preventive.py +++ b/streetup/interventions/views/preventive.py @@ -126,6 +126,7 @@ logger = logging.getLogger(__name__) from interventions.views.helpers import * +@intervention_add_preventive_permission_required def preventive_intervention_draft(request, thematic_code): if request.method == 'POST': draft = InterventionDraftManager(request.session) @@ -147,6 +148,7 @@ def preventive_intervention_draft(request, thematic_code): ) +@intervention_add_preventive_permission_required def preventive_link_assets(request, thematic_code): draft_id = request.POST.get("preventive_draft_id") drafts = InterventionDraftManager(request.session) @@ -174,6 +176,7 @@ def preventive_link_assets(request, thematic_code): return redirect(f"{reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': thematic_code})}?draft_id={draft_id}") +@intervention_add_preventive_permission_required def preventive_remove_element(request, thematic_code): """Supprime un élément (location ou asset) de la sélection du draft""" draft_id = request.POST.get("draft_id") @@ -206,6 +209,7 @@ def preventive_remove_element(request, thematic_code): }, status=400) +@intervention_add_preventive_permission_required def intervention_add_preventive(request, thematic_code): thematic = Thematic.objects.filter(code=thematic_code).first()