diff --git a/streetup/assets/static/assets/inspection_map.js b/streetup/assets/static/assets/inspection_map.js
index 3b9fb1a..9a796c4 100644
--- a/streetup/assets/static/assets/inspection_map.js
+++ b/streetup/assets/static/assets/inspection_map.js
@@ -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');
diff --git a/streetup/assets/templates/assets/inspections_list.html b/streetup/assets/templates/assets/inspections_list.html
index ac88198..261bca0 100644
--- a/streetup/assets/templates/assets/inspections_list.html
+++ b/streetup/assets/templates/assets/inspections_list.html
@@ -12,11 +12,13 @@
{% translate "Suivi et historique des contrôles et vérifications sur le terrain" %}
+ {% if can_inspect %}
diff --git a/streetup/assets/tests_inspections_views.py b/streetup/assets/tests_inspections_views.py
index 9952553..06dd0ca 100644
--- a/streetup/assets/tests_inspections_views.py
+++ b/streetup/assets/tests_inspections_views.py
@@ -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)
diff --git a/streetup/assets/views/inspections.py b/streetup/assets/views/inspections.py
index 82fa8ee..eb569f9 100644
--- a/streetup/assets/views/inspections.py
+++ b/streetup/assets/views/inspections.py
@@ -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"
diff --git a/streetup/common/admin.py b/streetup/common/admin.py
index 795a082..6ec5b2e 100644
--- a/streetup/common/admin.py
+++ b/streetup/common/admin.py
@@ -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']
diff --git a/streetup/common/migrations/0006_userthematics_can_inspect.py b/streetup/common/migrations/0006_userthematics_can_inspect.py
new file mode 100644
index 0000000..a47f3c9
--- /dev/null
+++ b/streetup/common/migrations/0006_userthematics_can_inspect.py
@@ -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'),
+ ),
+ ]
diff --git a/streetup/common/migrations/0007_alter_userthematics_can_inspect.py b/streetup/common/migrations/0007_alter_userthematics_can_inspect.py
new file mode 100644
index 0000000..3046a2a
--- /dev/null
+++ b/streetup/common/migrations/0007_alter_userthematics_can_inspect.py
@@ -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'),
+ ),
+ ]
diff --git a/streetup/common/models.py b/streetup/common/models.py
index a2e336d..9fed840 100644
--- a/streetup/common/models.py
+++ b/streetup/common/models.py
@@ -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
diff --git a/streetup/mobile/templates/mobile/mobile_index.html b/streetup/mobile/templates/mobile/mobile_index.html
index 0bb1cd3..344330b 100644
--- a/streetup/mobile/templates/mobile/mobile_index.html
+++ b/streetup/mobile/templates/mobile/mobile_index.html
@@ -20,10 +20,12 @@
{% endif %}
+ {% if can_inspect %}
{% translate "Nouvelle inspection" %}
+ {% endif %}
{% if can_add_structures_repair %}
diff --git a/streetup/mobile/views.py b/streetup/mobile/views.py
index 3d5428f..e022ccb 100644
--- a/streetup/mobile/views.py
+++ b/streetup/mobile/views.py
@@ -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