fix(interventions,mobile): allow multi-thematic visibility for contractor teams and prevent persistent search query

This commit is contained in:
kdeterme 2026-09-28 21:01:59 +02:00
parent de73de7328
commit 8b13236a26
3 changed files with 146 additions and 7 deletions

View file

@ -773,7 +773,17 @@ def filter_viewable_interventions_for_user(user, limit_to_default_thematic=False
qs = Intervention.objects.filter(q)
if limit_to_default_thematic and user_config.default_thematic:
qs = qs.filter(thematic=user_config.default_thematic)
if company_member:
assigned_q = (
Q(assigned_member=company_member) |
Q(assigned_team__in=list(company_member.teams.all())) |
Q(contract__allow_team_members_edit=True, assigned_member__teams__in=list(company_member.teams.all())) |
Q(assigned_control_team__in=list(company_member.teams.all())) |
Q(assigned_controller=company_member)
)
qs = qs.filter(Q(thematic=user_config.default_thematic) | assigned_q)
else:
qs = qs.filter(thematic=user_config.default_thematic)
if company_member:
qs = apply_maintain_type_filter(qs, user)

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 (
@ -804,6 +804,126 @@ class QuickProviderContractUpdateTests(TestCase):
self.assertTrue(can_edit_intervention(self.user, self.intervention))
class TeamMultiThematicVisibilityTests(TestCase):
def setUp(self):
self.User = get_user_model()
self.tech_role, _ = Role.objects.get_or_create(name='technician')
self.thematic_structures = Thematic.objects.create(code='structures', name_fr="Ouvrages d'art", name_nl='Kunstwerken')
self.thematic_its = Thematic.objects.create(code='its', name_fr='ITS', name_nl='ITS')
self.company = Company.objects.create(name='Eiffage')
self.team = CompanyTeam.objects.create(name='Equipe de jour Tunnels', company=self.company)
self.team.thematics.add(self.thematic_structures, self.thematic_its)
self.contract = Contract.objects.create(
contract_number='CNT-TUNNELS',
company=self.company,
start_date=timezone.now().date() - timedelta(days=10),
end_date=timezone.now().date() + timedelta(days=365),
is_active=True,
)
self.contract.thematics.add(self.thematic_structures, self.thematic_its)
self.user = self.User.objects.create_user(username='tech-zidan', password='pwd')
self.user_config = UserConfig.objects.create(
user=self.user,
is_intern=False,
default_thematic=self.thematic_structures,
limit_interventions_to_contracts=True,
)
self.user_config.roles.add(self.tech_role)
UserThematics.objects.create(
user_config=self.user_config,
thematic=self.thematic_structures,
can_view_interventions=True,
can_edit_interventions=True,
)
UserThematics.objects.create(
user_config=self.user_config,
thematic=self.thematic_its,
can_view_interventions=True,
can_edit_interventions=True,
)
UserContractAccess.objects.create(
user_config=self.user_config,
contract=self.contract,
can_view_interventions=True,
)
self.member = CompanyMember.objects.create(user=self.user, name='Zidane')
self.member.teams.add(self.team)
# Intervention 1: thematic ITS, assigned to the technician's team
self.itv_its = Intervention.objects.create(
title='Camera defect',
thematic=self.thematic_its,
status='to_be_processed',
assigned_provider=self.company,
assigned_team=self.team,
contract=self.contract,
)
# Intervention 2: thematic structures, assigned to the technician's team
self.itv_struct = Intervention.objects.create(
title='Lighting defect',
thematic=self.thematic_structures,
status='to_be_processed',
assigned_provider=self.company,
assigned_team=self.team,
contract=self.contract,
)
def test_filter_viewable_keeps_team_assigned_intervention_even_with_limit_to_default_thematic(self):
from interventions.permissions import filter_viewable_interventions_for_user
# Even if limit_to_default_thematic is True, the ITS intervention assigned to their team must remain visible
qs_limited = filter_viewable_interventions_for_user(self.user, limit_to_default_thematic=True)
self.assertTrue(qs_limited.filter(id=self.itv_its.id).exists())
self.assertTrue(qs_limited.filter(id=self.itv_struct.id).exists())
def test_filter_viewable_without_limit_shows_all_contract_thematics(self):
from interventions.permissions import filter_viewable_interventions_for_user
qs = filter_viewable_interventions_for_user(self.user, limit_to_default_thematic=False)
self.assertTrue(qs.filter(id=self.itv_its.id).exists())
self.assertTrue(qs.filter(id=self.itv_struct.id).exists())
def test_mobile_index_does_not_persist_search_query_q(self):
self.client.login(username='tech-zidan', password='pwd')
url = reverse('mobile:interventions_index_mobile')
# Visit mobile index with a search query
response = self.client.get(f"{url}?q=Camera&status=to_be_processed")
self.assertEqual(response.status_code, 200)
self.user_config.refresh_from_db()
saved = self.user_config.mobile_intervention_list_filters
self.assertNotIn('q', saved)
self.assertEqual(saved.get('status'), ['to_be_processed'])
def test_mobile_index_does_not_restore_stale_search_query_q(self):
# Simulate an old search query trapped in user_config
self.user_config.mobile_intervention_list_filters = {
'q': 'stale_search',
'status': ['to_be_processed'],
}
self.user_config.save()
self.client.login(username='tech-zidan', password='pwd')
url = reverse('mobile:interventions_index_mobile')
# Access mobile index without GET params -> redirects with saved filters
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
# Verify redirect target does NOT include q=stale_search
self.assertNotIn('q=', response.url)
self.assertIn('status=to_be_processed', response.url)

View file

@ -148,6 +148,8 @@ def interventions_index_mobile(request):
if saved_filters:
for key, value in saved_filters.items():
if key == 'q':
continue
if isinstance(value, list):
query_params.setlist(key, value)
elif value is not None:
@ -173,10 +175,7 @@ def interventions_index_mobile(request):
from contracts.services_teams import sync_member_current_teams
sync_member_current_teams(company_member_current)
# permissions
interventions = filter_viewable_interventions_for_user(request.user, limit_to_default_thematic=True)
# Get user config and company member for maintain type tabs logic
# Get user config and company member for permissions and maintain type tabs logic
user_config = None
company_member = None
try:
@ -187,10 +186,20 @@ def interventions_index_mobile(request):
if user_config and not user_config.is_intern:
company_member = CompanyMember.objects.prefetch_related('teams__company', 'visible_maintain_types').filter(user=request.user).first()
# Save the current filters to user config
# permissions
# Les utilisateurs internes avec une thématique par défaut sont restreints à celle-ci.
# Pour les prestataires externes (membres d'équipes dont le contrat/métier couvre souvent
# plusieurs thématiques comme Ouvrages d'art + ITS + Centres de contrôle pour les Tunnels),
# ne pas restreindre à une seule thématique par défaut afin qu'ils voient toutes les interventions de leur contrat/équipes.
limit_thematic = bool(user_config and user_config.is_intern)
interventions = filter_viewable_interventions_for_user(request.user, limit_to_default_thematic=limit_thematic)
# Save the current filters to user config (excluding temporary search query 'q')
if user_config and request.GET:
filters_to_save = {}
for key in request.GET:
if key == 'q':
continue
values = request.GET.getlist(key)
if key == 'status':
filters_to_save[key] = values