feat: introduce can_inspect permission per thematic with restricted access to inspection views
This commit is contained in:
parent
d725da4c57
commit
41619256a9
11 changed files with 120 additions and 8 deletions
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
18
streetup/common/migrations/0006_userthematics_can_inspect.py
Normal file
18
streetup/common/migrations/0006_userthematics_can_inspect.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue