feat: implement asset code/name search in inspections and restrict thematic visibility based on user configuration
This commit is contained in:
parent
8c85719e45
commit
7731ece2ea
4 changed files with 98 additions and 7 deletions
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue