feat: introduce can_inspect permission per thematic with restricted access to inspection views

This commit is contained in:
kdeterme 2026-08-07 16:36:19 +02:00
parent d725da4c57
commit 41619256a9
11 changed files with 120 additions and 8 deletions

View file

@ -292,7 +292,7 @@
const iconDisp = document.getElementById('thematic-icon-display');
const nameDisp = document.getElementById('thematic-name-display');
if (iconDisp) iconDisp.className = `bi ${selectedIcon} text-primary`;
if (iconDisp) iconDisp.className = `bi ${selectedIcon} text-primary me-2`;
if (nameDisp) nameDisp.textContent = selectedThematicName;
const categorySelect = document.getElementById('filter-category');

View file

@ -12,11 +12,13 @@
</h3>
<p class="text-muted mb-0 small">{% translate "Suivi et historique des contrôles et vérifications sur le terrain" %}</p>
</div>
{% if can_inspect %}
<div>
<a href="{% url 'assets:quick_inspection_map' %}" class="btn btn-primary shadow-sm">
<i class="bi bi-geo-alt-fill me-1"></i>{% translate "Nouvelle inspection (Carte GPS)" %}
</a>
</div>
{% endif %}
</div>
<!-- Navigation par Onglets -->

View file

@ -143,14 +143,14 @@
<div class="d-flex align-items-center gap-2">
<!-- Bouton Déclencheur Thématique -->
<button id="btn-select-thematic" type="button" class="btn filter-btn-pill d-flex align-items-center gap-1.5" data-bs-toggle="modal" data-bs-target="#thematicChooserModal">
<i id="thematic-icon-display" class="bi bi-collection text-primary"></i>
<i id="thematic-icon-display" class="bi bi-collection text-primary me-2"></i>
<span id="thematic-name-display" class="fw-semibold">{% translate "Thématique" %}</span>
<i class="bi bi-chevron-down text-muted ms-1 small"></i>
</button>
<!-- Bouton Déclencheur Catégorie -->
<button id="btn-select-category" type="button" class="btn filter-btn-pill d-flex align-items-center gap-1.5" data-bs-toggle="modal" data-bs-target="#categoryChooserModal">
<i class="bi bi-tag-fill text-secondary"></i>
<i class="bi bi-tag-fill text-secondary me-1.5"></i>
<span id="category-name-display" class="fw-semibold">{% translate "Catégorie" %}</span>
<i class="bi bi-chevron-down text-muted ms-1 small"></i>
</button>

View file

@ -91,3 +91,33 @@ class InspectionViewsTestCase(TestCase):
interv = Intervention.objects.filter(code=data['created_intervention_code']).first()
self.assertIsNotNone(interv)
self.assertEqual(interv.status, 'in_preparation')
def test_quick_inspection_map_permissions_can_inspect(self):
from common.models import UserConfig, UserThematics, Thematic
t_parking = Thematic.objects.get(code='parking')
t_other, _ = Thematic.objects.get_or_create(code='publiclighting', defaults={'name_fr': 'Éclairage public'})
user_config, _ = UserConfig.objects.get_or_create(user=self.user)
UserThematics.objects.create(user_config=user_config, thematic=t_parking, can_inspect=True)
UserThematics.objects.create(user_config=user_config, thematic=t_other, can_inspect=False)
response = self.client.get(reverse('assets:quick_inspection_map'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'parking')
self.assertNotContains(response, 'publiclighting')
def test_no_inspectable_thematic_hides_button_and_returns_403(self):
from common.models import UserConfig, UserThematics, Thematic
t_parking = Thematic.objects.get(code='parking')
user_config, _ = UserConfig.objects.get_or_create(user=self.user)
UserThematics.objects.create(user_config=user_config, thematic=t_parking, can_inspect=False)
# 1. Page liste des inspections -> Bouton 'Nouvelle inspection' masqué
resp_list = self.client.get(reverse('assets:inspections_list'))
self.assertEqual(resp_list.status_code, 200)
self.assertNotContains(resp_list, reverse('assets:quick_inspection_map'))
# 2. Tentative d'accès direct à quick_inspection_map -> HTTP 403 Forbidden
resp_map = self.client.get(reverse('assets:quick_inspection_map'))
self.assertEqual(resp_map.status_code, 403)

View file

@ -15,6 +15,15 @@ from assets.models.inspection import (
)
def get_user_inspectable_thematics(user):
"""
Retourne les thématiques pour lesquelles l'utilisateur détient la permission can_inspect.
"""
if hasattr(user, 'config'):
return user.config.get_inspectable_thematics().order_by('name_fr')
return Thematic.objects.all().order_by('name_fr')
@login_required
def inspections_list(request):
"""
@ -54,7 +63,8 @@ def inspections_list(request):
Q(inspector__username__icontains=search_q)
)
thematics = Thematic.objects.all().order_by('name_fr')
thematics = get_user_inspectable_thematics(request.user)
can_inspect = thematics.exists()
is_mobile = request.path.startswith('/mobile/')
base_template = "mobile/mobile_base.html" if is_mobile else "base.html"
@ -70,6 +80,7 @@ def inspections_list(request):
'selected_nature': nature_filter,
'search_q': search_q,
'thematics': thematics,
'can_inspect': can_inspect,
'result_choices': INSPECTION_RESULT_CHOICES,
'nature_choices': INSPECTION_NATURE_CHOICES,
}
@ -81,8 +92,11 @@ def quick_inspection_map(request):
"""
Vue cartographique d'inspection rapide optimisée mobile & GPS.
"""
thematics = Thematic.objects.all().order_by('name_fr')
categories = AssetCategory.objects.select_related('thematic').all().order_by('name_fr')
thematics = get_user_inspectable_thematics(request.user)
if not thematics.exists():
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')
is_mobile = request.path.startswith('/mobile/')
base_template = "mobile/mobile_base.html" if is_mobile else "base.html"

View file

@ -489,7 +489,7 @@ class UserThematicsInline(admin.TabularInline):
model = UserThematics
extra = 1
fields = [
'thematic', 'can_view_assets', 'can_edit_assets', 'can_validate_assets',
'thematic', 'can_view_assets', 'can_edit_assets', 'can_validate_assets', 'can_inspect',
'can_view_interventions', 'can_edit_interventions', 'can_edit_locations',
'can_process_observations',
'can_view_projects', 'can_edit_projects',
@ -584,7 +584,7 @@ class UserAssetTypeAccessInline(admin.TabularInline):
@admin.register(UserThematics)
class UserThematicsAdmin(admin.ModelAdmin):
list_display = ('user_config', 'thematic', 'can_view_interventions', 'can_edit_interventions', 'can_view_assets', 'can_edit_assets', 'can_validate_assets', 'can_process_observations')
list_display = ('user_config', 'thematic', 'can_view_interventions', 'can_edit_interventions', 'can_view_assets', 'can_edit_assets', 'can_validate_assets', 'can_inspect', 'can_process_observations')
list_filter = ('thematic',)
search_fields = ('user_config__user__username', 'thematic__name_fr', 'thematic__name_nl')
raw_id_fields = ['user_config']

View file

@ -0,0 +1,18 @@
# Generated by Django 6.0.7 on 2026-08-07 14:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0005_userconfig_regional_standard'),
]
operations = [
migrations.AddField(
model_name='userthematics',
name='can_inspect',
field=models.BooleanField(default=True, verbose_name='Peut réaliser des inspections'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 6.0.7 on 2026-08-07 14:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0006_userthematics_can_inspect'),
]
operations = [
migrations.AlterField(
model_name='userthematics',
name='can_inspect',
field=models.BooleanField(default=False, verbose_name='Peut réaliser des inspections'),
),
]

View file

@ -447,6 +447,30 @@ class UserConfig(models.Model):
return True
return self.userwarehouses.filter(can_administer=True).exists()
def get_inspectable_thematics(self):
"""Retourne le QuerySet des thématiques pour lesquelles l'utilisateur est autorisé à réaliser des inspections."""
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 Thematic.objects.all()
return Thematic.objects.filter(
userthematics__user_config=self,
userthematics__can_inspect=True
).distinct()
def can_inspect_thematic(self, thematic):
"""Vérifie si l'utilisateur peut réaliser des inspections pour une thématique donnée."""
if self.user.is_superuser or self.has_role('admin'):
return True
if not self.userthematics.exists():
return True
return self.userthematics.filter(thematic=thematic, can_inspect=True).exists()
class UserThematics(models.Model):
@ -462,6 +486,7 @@ class UserThematics(models.Model):
can_view_projects = models.BooleanField(default=False, verbose_name="Peut voir les projets") # Peut voir les projets de la thématique
can_edit_projects = models.BooleanField(default=False, verbose_name="Peut éditer les projets") # Peut créer/modifier les projets de la thématique
can_validate_assets = models.BooleanField(default=False, verbose_name="Peut valider les assets")
can_inspect = models.BooleanField(default=False, verbose_name="Peut réaliser des inspections")
class Meta:
unique_together = ("user_config", "thematic") # Empêche les doublons

View file

@ -20,10 +20,12 @@
</a>
{% endif %}
{% if can_inspect %}
<a href="{% url 'mobile:quick_inspection_map_mobile' %}" class="btn btn-outline-primary py-2.5 rounded-3 fw-semibold text-start d-flex align-items-center justify-content-between">
<span><i class="bi bi-search me-2"></i>{% translate "Nouvelle inspection" %}</span>
<i class="bi bi-geo-alt-fill"></i>
</a>
{% endif %}
{% if can_add_structures_repair %}
<a href="{% url 'mobile:add_repair_intervention_mobile' %}" class="btn btn-outline-primary py-2.5 rounded-3 fw-semibold text-start d-flex align-items-center justify-content-between">

View file

@ -110,6 +110,8 @@ def index(request):
inspection_configs.append(config)
break
can_inspect = user_config.get_inspectable_thematics().exists()
return render(request, "mobile/mobile_index.html", {
'can_edit_green_surfaces': can_edit_green_surfaces,
'can_access_stock': can_access_stock,
@ -117,6 +119,7 @@ def index(request):
'can_add_intervention_mobile': can_add_intervention_mobile,
'is_inspector': is_inspector,
'inspection_configs': inspection_configs,
'can_inspect': can_inspect,
})
@login_not_required