feat: implement asset code/name search in inspections and restrict thematic visibility based on user configuration

This commit is contained in:
kdeterme 2026-08-07 19:09:06 +02:00
parent 8c85719e45
commit 7731ece2ea
4 changed files with 98 additions and 7 deletions

View file

@ -44,7 +44,7 @@
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<span class="input-group-text bg-white"><i class="bi bi-search"></i></span> <span class="input-group-text bg-white"><i class="bi bi-search"></i></span>
<input type="text" name="q" value="{{ search_q }}" class="form-control" placeholder="{% translate 'Rechercher par note, inspecteur...' %}"> <input type="text" name="q" value="{{ search_q }}" class="form-control" placeholder="{% translate 'Rechercher par note, asset, ...' %}">
</div> </div>
</div> </div>

View file

@ -18,6 +18,8 @@ class InspectionViewsTestCase(TestCase):
first_name='Jean', first_name='Jean',
last_name='Dupont' last_name='Dupont'
) )
from common.models import UserConfig
UserConfig.objects.get_or_create(user=self.user)
self.client = Client() self.client = Client()
self.client.login(username='test_inspector', password='password123') self.client.login(username='test_inspector', password='password123')
@ -167,3 +169,41 @@ class InspectionViewsTestCase(TestCase):
self.assertEqual(data['type'], 'FeatureCollection') self.assertEqual(data['type'], 'FeatureCollection')
self.assertEqual(data['features'][0]['properties']['inspections'][0]['id'], insp.id) self.assertEqual(data['features'][0]['properties']['inspections'][0]['id'], insp.id)
self.assertEqual(data['features'][0]['properties']['inspections_count'], 1) self.assertEqual(data['features'][0]['properties']['inspections_count'], 1)
def test_inspections_list_search_by_asset_code(self):
self.spot.code = "SPOT-TEST-SEARCH-99"
self.spot.save()
insp = AssetInspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
result_status='compliant',
nature='visual'
)
response = self.client.get(reverse('assets:inspections_list') + '?tab=all&q=SEARCH-99')
self.assertEqual(response.status_code, 200)
self.assertIn(insp, response.context['inspections'])
def test_inspections_list_filter_thematics_uses_can_view_assets(self):
from common.models import UserConfig, UserThematics, Thematic
t_parking = Thematic.objects.get(code='parking')
t_lighting, _ = Thematic.objects.get_or_create(code='publiclighting', defaults={'name_fr': 'Éclairage public'})
user_config, _ = UserConfig.objects.get_or_create(user=self.user)
# L'utilisateur a les droits de voir les assets parking mais pas d'inspecter, et pas de voir l'éclairage public
UserThematics.objects.create(user_config=user_config, thematic=t_parking, can_view_assets=True, can_inspect=False)
UserThematics.objects.create(user_config=user_config, thematic=t_lighting, can_view_assets=False, can_inspect=False)
resp = self.client.get(reverse('assets:inspections_list'))
self.assertEqual(resp.status_code, 200)
self.assertIn(t_parking, resp.context['thematics'])
self.assertNotIn(t_lighting, resp.context['thematics'])
self.assertFalse(resp.context['can_inspect'])
def test_user_without_config_returns_none(self):
from assets.views.inspections import get_user_viewable_thematics, get_user_inspectable_thematics
user_no_config = User.objects.create_user(username='no_config_user', password='pwd')
self.assertIsNone(get_user_viewable_thematics(user_no_config))
self.assertIsNone(get_user_inspectable_thematics(user_no_config))

View file

@ -18,10 +18,21 @@ from assets.models.inspection import (
def get_user_inspectable_thematics(user): def get_user_inspectable_thematics(user):
""" """
Retourne les thématiques pour lesquelles l'utilisateur détient la permission can_inspect. Retourne les thématiques pour lesquelles l'utilisateur détient la permission can_inspect.
Si l'utilisateur n'a pas de UserConfig, retourne None.
""" """
if hasattr(user, 'config'): if hasattr(user, 'config') and user.config:
return user.config.get_inspectable_thematics().order_by('name_fr') return user.config.get_inspectable_thematics().order_by('name_fr')
return Thematic.objects.all().order_by('name_fr') return None
def get_user_viewable_thematics(user):
"""
Retourne les thématiques pour lesquelles l'utilisateur détient la permission de visualisation (can_view_assets).
Si l'utilisateur n'a pas de UserConfig, retourne None.
"""
if hasattr(user, 'config') and user.config:
return user.config.get_viewable_assets_thematics().order_by('name_fr')
return None
@login_required @login_required
@ -56,15 +67,41 @@ def inspections_list(request):
qs = qs.filter(nature=nature_filter) qs = qs.filter(nature=nature_filter)
if search_q: if search_q:
qs = qs.filter( search_filter = (
Q(notes__icontains=search_q) | Q(notes__icontains=search_q) |
Q(inspector__first_name__icontains=search_q) | Q(inspector__first_name__icontains=search_q) |
Q(inspector__last_name__icontains=search_q) | Q(inspector__last_name__icontains=search_q) |
Q(inspector__username__icontains=search_q) Q(inspector__username__icontains=search_q)
) )
thematics = get_user_inspectable_thematics(request.user) asset_q_conditions = Q()
can_inspect = thematics.exists() for ct in ContentType.objects.filter(app_label='assets'):
ModelClass = ct.model_class()
if not ModelClass:
continue
model_fields = [f.name for f in ModelClass._meta.get_fields()]
asset_search = Q()
if 'code' in model_fields:
asset_search |= Q(code__icontains=search_q)
if 'name' in model_fields:
asset_search |= Q(name__icontains=search_q)
if 'name_fr' in model_fields:
asset_search |= Q(name_fr__icontains=search_q)
if asset_search:
matching_ids = list(ModelClass.objects.filter(asset_search).values_list('pk', flat=True)[:500])
if matching_ids:
asset_q_conditions |= Q(asset_content_type=ct, asset_object_id__in=matching_ids)
if asset_q_conditions:
search_filter |= asset_q_conditions
qs = qs.filter(search_filter)
thematics = get_user_viewable_thematics(request.user)
insp_thematics = get_user_inspectable_thematics(request.user)
can_inspect = bool(insp_thematics and insp_thematics.exists())
is_mobile = request.path.startswith('/mobile/') is_mobile = request.path.startswith('/mobile/')
base_template = "mobile/mobile_base.html" if is_mobile else "base.html" base_template = "mobile/mobile_base.html" if is_mobile else "base.html"
@ -93,7 +130,7 @@ def quick_inspection_map(request):
Vue cartographique d'inspection rapide optimisée mobile & GPS. Vue cartographique d'inspection rapide optimisée mobile & GPS.
""" """
thematics = get_user_inspectable_thematics(request.user) thematics = get_user_inspectable_thematics(request.user)
if not thematics.exists(): if not thematics or not thematics.exists():
return HttpResponseForbidden(_("Vous ne disposez d'aucune thématique autorisée pour réaliser des inspections.")) return HttpResponseForbidden(_("Vous ne disposez d'aucune thématique autorisée pour réaliser des inspections."))
categories = AssetCategory.objects.filter(thematic__in=thematics).select_related('thematic').order_by('name_fr') categories = AssetCategory.objects.filter(thematic__in=thematics).select_related('thematic').order_by('name_fr')

View file

@ -447,6 +447,20 @@ class UserConfig(models.Model):
return True return True
return self.userwarehouses.filter(can_administer=True).exists() return self.userwarehouses.filter(can_administer=True).exists()
def get_viewable_assets_thematics(self):
"""Retourne le QuerySet des thématiques pour lesquelles l'utilisateur est autorisé à voir les assets."""
from common.models import Thematic
if self.user.is_superuser or self.has_role('admin'):
return Thematic.objects.all()
if not self.userthematics.exists():
return None
return Thematic.objects.filter(
userthematics__user_config=self,
userthematics__can_view_assets=True
).distinct()
def get_inspectable_thematics(self): def get_inspectable_thematics(self):
"""Retourne le QuerySet des thématiques pour lesquelles l'utilisateur est autorisé à réaliser des inspections.""" """Retourne le QuerySet des thématiques pour lesquelles l'utilisateur est autorisé à réaliser des inspections."""
from common.models import Thematic from common.models import Thematic