loko/loko/inspections/tests.py

1143 lines
47 KiB
Python

from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from assets.models import NatureRWIZ, NatureRWIAsset, ParkingSpot
from interventions.models import Intervention
from common.models import Thematic
from .models import Inspection, ParkingSpotInspection, GiepInspection, InspectionDocument
from .registry import get_inspection_partial_template
User = get_user_model()
class InspectionModelAndViewsTestCase(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='inspector_user',
password='password123',
first_name='Jean',
last_name='Valjean'
)
from common.models import UserConfig, Role, UserThematics
admin_role, _ = Role.objects.get_or_create(name='admin')
config = UserConfig.objects.create(user=self.user)
config.roles.add(admin_role)
self.water_thematic, _ = Thematic.objects.get_or_create(
code='water',
defaults={'name_fr': 'Gestion de l\'Eau / GIEP'}
)
UserThematics.objects.create(
user_config=config,
thematic=self.water_thematic,
can_view_assets=True,
can_edit_assets=True
)
self.thematic, _ = Thematic.objects.get_or_create(
code='parking',
defaults={'name_fr': 'Stationnement'}
)
UserThematics.objects.create(
user_config=config,
thematic=self.thematic,
can_view_assets=True,
can_edit_assets=True
)
from assets.models import AssetCategory
self.parking_cat, _ = AssetCategory.objects.get_or_create(
code='parking_spot',
thematic=self.thematic,
defaults={
'name_fr': 'Emplacements de stationnement',
'name_nl': 'Parkeerplaatsen'
}
)
self.spot = ParkingSpot.objects.create(
name_fr='Emplacement Vélo Test',
category=self.parking_cat
)
self.spot_ct = ContentType.objects.get_for_model(ParkingSpot)
self.parking_cat.allowed_models.add(self.spot_ct)
self.rwiz = NatureRWIZ.objects.create(
name_fr='Zone GIEP Test'
)
self.rwiz_ct = ContentType.objects.get_for_model(NatureRWIZ)
self.mission_interv = Intervention.objects.create(
title="Mission inspection GIEP Nord",
thematic=self.thematic,
status="in_progress"
)
self.work_interv = Intervention.objects.create(
title="Travaux réfection par Prestataire A",
thematic=self.thematic,
status="done"
)
def test_inspection_creation_with_interventions(self):
insp = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
mission_intervention=self.mission_interv,
checked_intervention=self.work_interv,
nature='technical',
inspector_role='provider',
result_status='compliant',
notes="Tiers contrôle OK"
)
self.assertTrue(insp.is_mission_inspection)
self.assertTrue(insp.is_cross_check)
self.assertEqual(insp.mission_intervention, self.mission_interv)
self.assertEqual(insp.checked_intervention, self.work_interv)
self.assertIn(insp, self.mission_interv.generated_inspections.all())
self.assertIn(insp, self.work_interv.quality_inspections.all())
def test_giep_inspection_model(self):
giep_insp = GiepInspection.objects.create(
asset_content_type=self.rwiz_ct,
asset_object_id=self.rwiz.id,
inspector=self.user,
result_status='compliant',
date_derniere_pluie='aujourdhui',
orage_moins_une_heure=True,
data={
'ouvrages': [
{'local_id': '0', 'device_type': 'evc', 'name': 'Noue 1'}
]
}
)
self.assertEqual(giep_insp.detailed_inspection, giep_insp)
self.assertIn("1 ouvrage(s) GIEP", giep_insp.details_summary)
self.assertIn(str(giep_insp.get_date_derniere_pluie_display()), giep_insp.details_summary)
def test_create_inspection_api(self):
self.client.login(username='inspector_user', password='password123')
url = reverse('inspections:create_inspection_api')
response = self.client.post(url, {
'content_type_id': self.spot_ct.id,
'object_id': self.spot.id,
'result_status': 'compliant',
'nature': 'visual',
'inspector_role': 'provider',
'mission_intervention_id': self.mission_interv.id,
'checked_intervention_id': self.work_interv.id,
'notes': 'Inspection via API'
})
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertTrue(data['success'])
insp = Inspection.objects.get(pk=data['inspection_id'])
self.assertEqual(insp.mission_intervention, self.mission_interv)
self.assertEqual(insp.checked_intervention, self.work_interv)
def test_create_giep_inspection_api(self):
self.client.login(username='inspector_user', password='password123')
url = reverse('inspections:create_inspection_api')
import json
giep_json = json.dumps({
'ouvrages': [
{'local_id': '0', 'device_type': 'evc', 'questions': {'engorgement': 'aucun'}}
]
})
response = self.client.post(url, {
'content_type_id': self.rwiz_ct.id,
'object_id': self.rwiz.id,
'result_status': 'compliant',
'nature': 'visual',
'inspector_role': 'observer',
'date_derniere_pluie': 'moins_24h',
'orage_moins_une_heure': 'false',
'giep_data': giep_json,
'notes': 'Inspection GIEP via API'
})
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertTrue(data['success'])
insp = GiepInspection.objects.get(pk=data['inspection_id'])
self.assertEqual(insp.date_derniere_pluie, 'moins_24h')
self.assertFalse(insp.orage_moins_une_heure)
self.assertEqual(len(insp.data.get('ouvrages', [])), 1)
def test_registry_giep_template(self):
tmpl_rwiz = get_inspection_partial_template('naturerwiz')
tmpl_asset = get_inspection_partial_template('naturerwiasset')
self.assertEqual(tmpl_rwiz, 'inspections/components/asset_fields/_naturerwiz.html')
self.assertIsNone(tmpl_asset)
def test_registry_detail_templates(self):
from .registry import get_inspection_detail_partial_template
giep_insp = GiepInspection(asset=self.rwiz)
parking_insp = ParkingSpotInspection(asset=self.spot)
tmpl_giep = get_inspection_detail_partial_template(giep_insp)
tmpl_parking = get_inspection_detail_partial_template(parking_insp)
self.assertEqual(tmpl_giep, 'inspections/components/asset_details/_naturerwiz.html')
self.assertEqual(tmpl_parking, 'inspections/components/asset_details/_parkingspot.html')
def test_zirw_detail_view_renders_cleanly(self):
self.client.login(username='inspector_user', password='password123')
url = reverse('assets:zirw_detail', kwargs={'location_id': self.rwiz.id})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_spatial_location_resolution_for_rwi_asset(self):
from assets.templatetags.asset_tags import asset_location_link
from django.contrib.gis.geos import Polygon, MultiPolygon, Point
poly = Polygon(((0, 0), (0, 10), (10, 10), (10, 0), (0, 0)), srid=3812)
rwiz_poly = NatureRWIZ.objects.create(
code="RWIZ-TEST",
name_fr="Zone Test Polygons",
geom=MultiPolygon(poly, srid=3812)
)
rwi_asset = NatureRWIAsset.objects.create(
code="RWI-TEST-01",
geom=Point(5, 5, srid=3812)
)
ctx = asset_location_link({'request': None}, rwi_asset)
self.assertEqual(ctx['location'], rwiz_poly)
def test_can_edit_inspection_permissions(self):
other_user = User.objects.create_user(
username='regular_user',
password='password123'
)
insp = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
result_status='compliant',
notes="Initial note"
)
# Author can edit
self.assertTrue(insp.can_edit(self.user))
# Superuser can edit
admin_user = User.objects.create_superuser(
username='admin_superuser',
password='password123'
)
self.assertTrue(insp.can_edit(admin_user))
# Other non-author non-admin cannot edit
self.assertFalse(insp.can_edit(other_user))
def test_update_inspection_api_author_success(self):
insp = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
result_status='compliant',
notes="Initial note"
)
initial_updated_at = insp.updated_at
self.client.login(username='inspector_user', password='password123')
url = reverse('inspections:update_inspection_api', kwargs={'inspection_id': insp.id})
response = self.client.post(url, {
'result_status': 'non_compliant',
'notes': 'Modifié par auteur',
'nature': 'detailed'
})
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertTrue(data['success'])
insp.refresh_from_db()
self.assertEqual(insp.result_status, 'non_compliant')
self.assertEqual(insp.notes, 'Modifié par auteur')
self.assertEqual(insp.nature, 'detailed')
self.assertGreaterEqual(insp.updated_at, initial_updated_at)
def test_update_inspection_api_with_photos(self):
from django.core.files.uploadedfile import SimpleUploadedFile
from inspections.models import InspectionDocument
insp = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
result_status='compliant',
notes="Inspection sans photo initiale"
)
self.assertEqual(insp.documents.count(), 0)
photo1 = SimpleUploadedFile("photo1.jpg", b"fake_jpeg_content_1", content_type="image/jpeg")
photo2 = SimpleUploadedFile("photo2.jpg", b"fake_jpeg_content_2", content_type="image/jpeg")
self.client.login(username='inspector_user', password='password123')
url = reverse('inspections:update_inspection_api', kwargs={'inspection_id': insp.id})
response = self.client.post(url, {
'result_status': 'non_compliant',
'notes': 'Ajout de photos lors de la modification',
'photos': [photo1, photo2]
})
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertTrue(data['success'])
insp.refresh_from_db()
self.assertEqual(insp.documents.count(), 2)
doc_names = [d.filename for d in insp.documents.all()]
self.assertTrue(any("photo1" in name for name in doc_names))
self.assertTrue(any("photo2" in name for name in doc_names))
self.assertTrue(all(d.is_image for d in insp.documents.all()))
def test_update_inspection_api_forbidden(self):
other_user = User.objects.create_user(
username='other_user_stranger',
password='password123'
)
insp = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
result_status='compliant',
notes="Original note"
)
self.client.login(username='other_user_stranger', password='password123')
url = reverse('inspections:update_inspection_api', kwargs={'inspection_id': insp.id})
response = self.client.post(url, {
'result_status': 'damaged',
'notes': 'Hacked note'
})
self.assertEqual(response.status_code, 403)
data = response.json()
self.assertFalse(data['success'])
insp.refresh_from_db()
self.assertEqual(insp.result_status, 'compliant')
self.assertEqual(insp.notes, 'Original note')
def test_update_parking_spot_inspection_api(self):
from inspections.models import ParkingSpotInspection
ps_insp = ParkingSpotInspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
result_status='compliant',
notes="Initial parking inspection",
verified_standard_stand_count=5,
verified_cargo_stand_count=2,
verified_marking_quality='good'
)
self.client.login(username='inspector_user', password='password123')
url = reverse('inspections:update_inspection_api', kwargs={'inspection_id': ps_insp.id})
response = self.client.post(url, {
'result_status': 'partially_compliant',
'verified_standard_stand_count': '10',
'verified_cargo_stand_count': '4',
'verified_drop_off_stand_count': '1',
'verified_has_scooter_drop_zone': 'true',
'verified_marking_quality': 'damaged',
'apply_physical_counts_to_asset': 'true'
})
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertTrue(data['success'])
ps_insp.refresh_from_db()
self.assertEqual(ps_insp.result_status, 'partially_compliant')
self.assertEqual(ps_insp.verified_standard_stand_count, 10)
self.assertEqual(ps_insp.verified_cargo_stand_count, 4)
self.assertEqual(ps_insp.verified_drop_off_stand_count, 1)
self.assertTrue(ps_insp.verified_has_scooter_drop_zone)
self.assertEqual(ps_insp.verified_marking_quality, 'damaged')
def test_delete_inspection_document_api(self):
from inspections.models import InspectionDocument
from django.core.files.uploadedfile import SimpleUploadedFile
insp = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
result_status='compliant'
)
dummy_file = SimpleUploadedFile("test_doc.txt", b"file content", content_type="text/plain")
doc = InspectionDocument.objects.create(
inspection=insp,
file=dummy_file
)
self.client.login(username='inspector_user', password='password123')
url = reverse('inspections:delete_inspection_document_api', kwargs={'document_id': doc.id})
response = self.client.post(url)
self.assertFalse(InspectionDocument.objects.filter(id=doc.id).exists())
def test_inspections_list_thematic_filter(self):
self.client.login(username='inspector_user', password='password123')
# Create inspections on both water and parking
insp_water = Inspection.objects.create(
asset_content_type=self.rwiz_ct,
asset_object_id=self.rwiz.id,
inspector=self.user,
result_status='compliant'
)
insp_parking = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
result_status='compliant'
)
# Mobile list test
url_mobile = reverse('mobile:inspections_list_mobile')
resp_water_m = self.client.get(url_mobile, {'thematic': 'water', 'tab': 'all'})
self.assertEqual(resp_water_m.status_code, 200)
inspections_water = list(resp_water_m.context['inspections'])
self.assertIn(insp_water, inspections_water)
self.assertNotIn(insp_parking, inspections_water)
resp_parking_m = self.client.get(url_mobile, {'thematic': 'parking', 'tab': 'all'})
self.assertEqual(resp_parking_m.status_code, 200)
inspections_parking = list(resp_parking_m.context['inspections'])
self.assertIn(insp_parking, inspections_parking)
self.assertNotIn(insp_water, inspections_parking)
# Desktop GeoJSON test
url_geojson = reverse('inspections:inspections_geojson_get')
resp_water_g = self.client.get(url_geojson, {'thematic': 'water'})
self.assertEqual(resp_water_g.status_code, 200)
feat_ids_water = [f['properties']['id'] for f in resp_water_g.json()['features']]
self.assertIn(insp_water.id, feat_ids_water)
self.assertNotIn(insp_parking.id, feat_ids_water)
def test_toggle_cancel_inspection_api(self):
self.client.login(username='inspector_user', password='password123')
insp = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
result_status='compliant'
)
self.assertFalse(insp.is_cancelled)
# Cancel inspection
url = reverse('inspections:toggle_cancel_inspection_api', kwargs={'inspection_id': insp.id})
resp = self.client.post(url, {'action': 'cancel'})
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertTrue(data['success'])
self.assertTrue(data['is_cancelled'])
self.assertEqual(data['result_status'], 'cancelled')
insp.refresh_from_db()
self.assertTrue(insp.is_cancelled)
self.assertEqual(insp.status_badge_class, 'bg-secondary')
# Restore inspection
resp_restore = self.client.post(url, {'action': 'restore'})
self.assertEqual(resp_restore.status_code, 200)
data_restore = resp_restore.json()
self.assertTrue(data_restore['success'])
self.assertFalse(data_restore['is_cancelled'])
self.assertEqual(data_restore['result_status'], 'compliant')
insp.refresh_from_db()
self.assertFalse(insp.is_cancelled)
self.assertEqual(insp.result_status, 'compliant')
def test_toggle_cancel_inspection_permissions(self):
other_user = User.objects.create_user(
username='stranger_user',
password='password123'
)
insp = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
result_status='compliant'
)
url = reverse('inspections:toggle_cancel_inspection_api', kwargs={'inspection_id': insp.id})
# Stranger cannot cancel
self.client.login(username='stranger_user', password='password123')
resp = self.client.post(url, {'action': 'cancel'})
self.assertEqual(resp.status_code, 403)
insp.refresh_from_db()
self.assertEqual(insp.result_status, 'compliant')
# Superuser can cancel
other_user.is_superuser = True
other_user.save()
resp_admin = self.client.post(url, {'action': 'cancel'})
self.assertEqual(resp_admin.status_code, 200)
insp.refresh_from_db()
self.assertEqual(insp.result_status, 'cancelled')
def test_asset_compliance_recalculation_on_cancellation(self):
self.client.login(username='inspector_user', password='password123')
# 1st inspection (compliant)
date1 = timezone.now() - timezone.timedelta(days=2)
insp1 = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
inspection_date=date1,
result_status='compliant',
apply_changes_to_asset=True
)
self.spot.refresh_from_db()
self.assertEqual(self.spot.inspection_status, 'compliant')
self.assertTrue(self.spot.is_compliant)
# 2nd inspection (damaged)
date2 = timezone.now()
insp2 = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
inspection_date=date2,
result_status='damaged',
notes="Arceau cassé",
apply_changes_to_asset=True
)
self.spot.refresh_from_db()
self.assertEqual(self.spot.inspection_status, 'damaged')
self.assertFalse(self.spot.is_compliant)
self.assertEqual(self.spot.non_compliance_reason, "Arceau cassé")
# Cancel 2nd inspection -> asset should fall back to 1st inspection (compliant)
url2 = reverse('inspections:toggle_cancel_inspection_api', kwargs={'inspection_id': insp2.id})
self.client.post(url2, {'action': 'cancel'})
self.spot.refresh_from_db()
self.assertEqual(self.spot.inspection_status, 'compliant')
self.assertTrue(self.spot.is_compliant)
self.assertIsNone(self.spot.non_compliance_reason)
self.assertEqual(self.spot.last_inspection_date, date1)
# Cancel 1st inspection -> no active inspections remain, reset to default
url1 = reverse('inspections:toggle_cancel_inspection_api', kwargs={'inspection_id': insp1.id})
self.client.post(url1, {'action': 'cancel'})
self.spot.refresh_from_db()
self.assertEqual(self.spot.inspection_status, 'unknown')
self.assertIsNone(self.spot.last_inspection_date)
def test_desktop_inspections_list_view(self):
self.client.login(username='inspector_user', password='password123')
url = reverse('inspections:inspections_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'inspections/inspections_index.html')
self.assertIn('thematic_choices', response.context)
self.assertIn('result_status_choices', response.context)
self.assertIn('saved_sort', response.context)
def test_mobile_inspections_list_view(self):
self.client.login(username='inspector_user', password='password123')
response = self.client.get(reverse('mobile:inspections_list_mobile'))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'inspections/inspections_list_mobile.html')
def test_inspections_geojson_get_endpoint(self):
self.client.login(username='inspector_user', password='password123')
insp1 = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
result_status='compliant',
notes='Test note search query'
)
url = reverse('inspections:inspections_geojson_get')
# Test basic GET
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertEqual(data['type'], 'FeatureCollection')
self.assertTrue(any(f['properties']['id'] == insp1.id for f in data['features']))
# Test thematic filter
resp_water = self.client.get(url, {'thematic': 'water'})
self.assertEqual(resp_water.status_code, 200)
data_water = resp_water.json()
self.assertFalse(any(f['properties']['id'] == insp1.id for f in data_water['features']))
# Test search filter (q)
resp_q = self.client.get(url, {'q': 'search query'})
self.assertEqual(resp_q.status_code, 200)
data_q = resp_q.json()
self.assertTrue(any(f['properties']['id'] == insp1.id for f in data_q['features']))
def test_inspections_export_excel(self):
self.client.login(username='inspector_user', password='password123')
Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.user,
result_status='compliant',
notes='Test Excel Export Note'
)
url = reverse('inspections:inspections_export_excel')
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp['Content-Type'],
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
self.assertTrue(resp.has_header('Content-Disposition'))
self.assertIn('attachment; filename="inspections_export_', resp['Content-Disposition'])
def test_save_inspection_list_preferences(self):
self.client.login(username='inspector_user', password='password123')
url = reverse('common:save_inspection_list_preferences')
import json
payload = {
'filters': {'thematic': ['parking'], 'mine': '1'},
'sort': {'sortName': 'inspection_date', 'sortOrder': 'asc'}
}
resp = self.client.post(url, json.dumps(payload), content_type='application/json')
self.assertEqual(resp.status_code, 200)
self.assertTrue(resp.json()['success'])
self.user.config.refresh_from_db()
self.assertEqual(self.user.config.inspection_list_filters, payload['filters'])
self.assertEqual(self.user.config.inspection_list_sort, payload['sort'])
class ExternalUserInspectionVisibilityTestCase(TestCase):
def setUp(self):
self.client = Client()
# Utilisateur interne
self.internal_user = User.objects.create_user(
username='intern_user',
password='password123',
first_name='Intern',
last_name='User'
)
from common.models import UserConfig, Role, UserThematics, UserContractAccess
from contracts.models import Contract
from assets.models import Structure, StructureContract, AssetCategory
config_intern = UserConfig.objects.create(
user=self.internal_user,
is_intern=True,
limit_assets_to_contracts=False
)
# Utilisateur externe
self.external_user = User.objects.create_user(
username='extern_user',
password='password123',
first_name='Extern',
last_name='User'
)
self.external_config = UserConfig.objects.create(
user=self.external_user,
is_intern=False,
limit_assets_to_contracts=True
)
role_ext, _ = Role.objects.get_or_create(name='external_manager')
self.external_config.roles.add(role_ext)
# Thématiques
self.thematic_parking, _ = Thematic.objects.get_or_create(
code='parking',
defaults={'name_fr': 'Stationnement'}
)
self.thematic_water, _ = Thematic.objects.get_or_create(
code='water',
defaults={'name_fr': 'Gestion de l\'Eau'}
)
self.thematic_structures, _ = Thematic.objects.get_or_create(
code='structures',
defaults={'name_fr': 'Ouvrages d\'art'}
)
# L'externe a accès aux assets Parking et Structures, mais PAS à Water
UserThematics.objects.create(
user_config=self.external_config,
thematic=self.thematic_parking,
can_view_assets=True,
can_view_interventions=True
)
UserThematics.objects.create(
user_config=self.external_config,
thematic=self.thematic_structures,
can_view_assets=True,
can_view_interventions=True
)
UserThematics.objects.create(
user_config=self.external_config,
thematic=self.thematic_water,
can_view_assets=False,
can_view_interventions=False
)
# Contrats
from contracts.models import Company, Contract
self.company = Company.objects.create(name="Company Ext Test")
today = timezone.now().date()
self.contract_a = Contract.objects.create(
company=self.company,
contract_number="CTR-EXT-A",
start_date=today,
end_date=today + timezone.timedelta(days=365),
is_active=True
)
self.contract_a.thematics.add(self.thematic_structures)
self.contract_b = Contract.objects.create(
company=self.company,
contract_number="CTR-EXT-B",
start_date=today,
end_date=today + timezone.timedelta(days=365),
is_active=True
)
self.contract_b.thematics.add(self.thematic_structures)
# L'externe a accès au contrat A mais pas au B
uca_a = UserContractAccess.objects.create(
user_config=self.external_config,
contract=self.contract_a,
can_view_assets=True,
can_view_interventions=True
)
from common.models import UserContractStatusPermission
UserContractStatusPermission.objects.create(
user_contract=uca_a,
status='in_progress',
can_view=True
)
# Assets
self.spot = ParkingSpot.objects.create(name_fr="Emplacement Ext")
self.spot_ct = ContentType.objects.get_for_model(ParkingSpot)
self.rwiz = NatureRWIZ.objects.create(name_fr="Bassin Water")
self.rwiz_ct = ContentType.objects.get_for_model(NatureRWIZ)
self.struct_a = Structure.objects.create(code="STR-A", name_fr="Pont A")
StructureContract.objects.create(structure=self.struct_a, contract=self.contract_a, status='active')
self.struct_ct = ContentType.objects.get_for_model(Structure)
self.struct_b = Structure.objects.create(code="STR-B", name_fr="Pont B")
StructureContract.objects.create(structure=self.struct_b, contract=self.contract_b, status='active')
# Interventions
self.interv_a = Intervention.objects.create(
title="Intervention Contrat A",
contract=self.contract_a,
thematic=self.thematic_structures,
status="in_progress"
)
self.interv_b = Intervention.objects.create(
title="Intervention Contrat B",
contract=self.contract_b,
thematic=self.thematic_structures,
status="in_progress"
)
def test_external_user_thematic_filtering(self):
from inspections.permissions import can_view_inspection, filter_viewable_inspections_for_user
insp_parking = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.internal_user,
result_status='compliant'
)
insp_water = Inspection.objects.create(
asset_content_type=self.rwiz_ct,
asset_object_id=self.rwiz.id,
inspector=self.internal_user,
result_status='compliant'
)
# L'externe peut voir le parking (can_view_assets=True sur parking)
self.assertTrue(can_view_inspection(self.external_user, insp_parking))
# L'externe ne peut pas voir water (can_view_assets=False sur water)
self.assertFalse(can_view_inspection(self.external_user, insp_water))
# QuerySet filtering
qs = filter_viewable_inspections_for_user(self.external_user)
self.assertIn(insp_parking, qs)
self.assertNotIn(insp_water, qs)
def test_external_user_contract_filtering(self):
from inspections.permissions import can_view_inspection, filter_viewable_inspections_for_user
insp_struct_a = Inspection.objects.create(
asset_content_type=self.struct_ct,
asset_object_id=self.struct_a.id,
inspector=self.internal_user,
result_status='compliant'
)
insp_struct_b = Inspection.objects.create(
asset_content_type=self.struct_ct,
asset_object_id=self.struct_b.id,
inspector=self.internal_user,
result_status='compliant'
)
# L'externe a accès au contrat A -> voit insp_struct_a
self.assertTrue(can_view_inspection(self.external_user, insp_struct_a))
# L'externe n'a pas accès au contrat B -> ne voit pas insp_struct_b
self.assertFalse(can_view_inspection(self.external_user, insp_struct_b))
# QuerySet filtering
qs = filter_viewable_inspections_for_user(self.external_user)
self.assertIn(insp_struct_a, qs)
self.assertNotIn(insp_struct_b, qs)
def test_external_user_intervention_linked_inspection(self):
from inspections.permissions import can_view_inspection, filter_viewable_inspections_for_user
# Inspection sur asset d'une thématique non autorisée, mais liée à une intervention autorisée (mission)
insp_mission = Inspection.objects.create(
asset_content_type=self.rwiz_ct,
asset_object_id=self.rwiz.id,
inspector=self.internal_user,
mission_intervention=self.interv_a,
result_status='compliant'
)
# Inspection liée à une intervention non autorisée
insp_mission_unauth = Inspection.objects.create(
asset_content_type=self.rwiz_ct,
asset_object_id=self.rwiz.id,
inspector=self.internal_user,
mission_intervention=self.interv_b,
result_status='compliant'
)
self.assertTrue(can_view_inspection(self.external_user, insp_mission))
self.assertFalse(can_view_inspection(self.external_user, insp_mission_unauth))
qs = filter_viewable_inspections_for_user(self.external_user)
self.assertIn(insp_mission, qs)
self.assertNotIn(insp_mission_unauth, qs)
def test_external_user_is_inspector_always_visible(self):
from inspections.permissions import can_view_inspection, filter_viewable_inspections_for_user
# Inspection réalisée par l'externe lui-même sur une thématique normalement restreinte
insp_by_extern = Inspection.objects.create(
asset_content_type=self.rwiz_ct,
asset_object_id=self.rwiz.id,
inspector=self.external_user,
result_status='compliant'
)
self.assertTrue(can_view_inspection(self.external_user, insp_by_extern))
qs = filter_viewable_inspections_for_user(self.external_user)
self.assertIn(insp_by_extern, qs)
def test_external_user_inspections_list_view(self):
insp_parking = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.internal_user,
result_status='compliant'
)
insp_water = Inspection.objects.create(
asset_content_type=self.rwiz_ct,
asset_object_id=self.rwiz.id,
inspector=self.internal_user,
result_status='compliant'
)
self.client.login(username='extern_user', password='password123')
# Test mobile
url_mobile = reverse('mobile:inspections_list_mobile')
response = self.client.get(url_mobile, {'tab': 'all'})
self.assertEqual(response.status_code, 200)
inspections = list(response.context['inspections'])
self.assertIn(insp_parking, inspections)
self.assertNotIn(insp_water, inspections)
# Test desktop GeoJSON
url_geojson = reverse('inspections:inspections_geojson_get')
resp_geojson = self.client.get(url_geojson)
self.assertEqual(resp_geojson.status_code, 200)
feat_ids = [f['properties']['id'] for f in resp_geojson.json()['features']]
self.assertIn(insp_parking.id, feat_ids)
self.assertNotIn(insp_water.id, feat_ids)
def test_external_user_inspection_detail_api(self):
insp_parking = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.internal_user,
result_status='compliant'
)
insp_water = Inspection.objects.create(
asset_content_type=self.rwiz_ct,
asset_object_id=self.rwiz.id,
inspector=self.internal_user,
result_status='compliant'
)
self.client.login(username='extern_user', password='password123')
# Inspection autorisée -> 200
url_ok = reverse('inspections:get_inspection_detail_api', kwargs={'inspection_id': insp_parking.id})
res_ok = self.client.get(url_ok)
self.assertEqual(res_ok.status_code, 200)
self.assertTrue(res_ok.json()['success'])
# Inspection non autorisée -> 403
url_forbidden = reverse('inspections:get_inspection_detail_api', kwargs={'inspection_id': insp_water.id})
res_forbidden = self.client.get(url_forbidden)
self.assertEqual(res_forbidden.status_code, 403)
self.assertFalse(res_forbidden.json()['success'])
def test_guard_inspection_document(self):
from inspections.models import InspectionDocument
from django.core.files.uploadedfile import SimpleUploadedFile
from inspections.apps import InspectionsConfig
insp_parking = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.internal_user,
result_status='compliant'
)
insp_water = Inspection.objects.create(
asset_content_type=self.rwiz_ct,
asset_object_id=self.rwiz.id,
inspector=self.internal_user,
result_status='compliant'
)
dummy_file = SimpleUploadedFile("doc.txt", b"content", content_type="text/plain")
doc_parking = InspectionDocument.objects.create(inspection=insp_parking, file=dummy_file)
doc_water = InspectionDocument.objects.create(inspection=insp_water, file=dummy_file)
from inspections.permissions import can_view_inspection
self.assertTrue(can_view_inspection(self.external_user, doc_parking.inspection))
self.assertFalse(can_view_inspection(self.external_user, doc_water.inspection))
def test_internal_user_sees_all_inspections(self):
from inspections.permissions import can_view_inspection, filter_viewable_inspections_for_user
insp_parking = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=self.external_user,
result_status='compliant'
)
insp_water = Inspection.objects.create(
asset_content_type=self.rwiz_ct,
asset_object_id=self.rwiz.id,
inspector=self.external_user,
result_status='compliant'
)
insp_struct_b = Inspection.objects.create(
asset_content_type=self.struct_ct,
asset_object_id=self.struct_b.id,
inspector=self.external_user,
result_status='compliant'
)
self.assertTrue(can_view_inspection(self.internal_user, insp_parking))
self.assertTrue(can_view_inspection(self.internal_user, insp_water))
self.assertTrue(can_view_inspection(self.internal_user, insp_struct_b))
qs = filter_viewable_inspections_for_user(self.internal_user)
self.assertIn(insp_parking, qs)
self.assertIn(insp_water, qs)
self.assertIn(insp_struct_b, qs)
def test_signpanel_inspection_creation_and_asset_sync(self):
from assets.models import SignPanel, SignPanelType, SignPole, SignStreet
from inspections.models import SignPanelInspection
street = SignStreet.objects.create(code='STR-TEST-1', name_fr='Rue Test Signalisation')
type_c1, _ = SignPanelType.objects.get_or_create(code='C1', defaults={'name_fr': 'Sens interdit'})
type_d7, _ = SignPanelType.objects.get_or_create(code='D7', defaults={'name_fr': 'Piste cyclable'})
pole = SignPole.objects.create(code='P-TEST-1', street=street, status='active')
panel = SignPanel.objects.create(
pole=pole,
code='PAN-TEST-1',
signpanel_type=type_c1,
signpanel_text='Sauf bus',
inspection_status='compliant',
status='active'
)
panel_ct = ContentType.objects.get_for_model(SignPanel)
# 1. Inspection avec modification du type et de la propreté (dégradé -> is_compliant=False)
insp = SignPanelInspection.objects.create(
asset_content_type=panel_ct,
asset_object_id=panel.id,
inspector=self.internal_user,
result_status='damaged',
verified_signpanel_type=type_d7,
verified_signpanel_text='Sauf vélos',
verified_cleanliness='vandalized',
ai_detected_code='D7',
ai_confidence=0.97,
apply_sign_updates_to_asset=True
)
panel.refresh_from_db()
self.assertEqual(panel.signpanel_type, type_d7)
self.assertEqual(panel.signpanel_text, 'Sauf vélos')
self.assertFalse(panel.is_compliant)
self.assertEqual(insp.detailed_inspection, insp)
self.assertIn('Tagué', insp.details_summary)
# 2. Test API create_inspection_api pour SignPanel
self.client.force_login(self.internal_user)
response = self.client.post(reverse('inspections:create_inspection_api'), {
'content_type_id': panel_ct.id,
'object_id': panel.id,
'result_status': 'missing',
'verified_cleanliness': 'missing',
'verified_signpanel_type_id': type_c1.id,
'verified_signpanel_text': 'Texte test',
'apply_changes_to_asset': 'true'
})
self.assertEqual(response.status_code, 200)
panel.refresh_from_db()
self.assertEqual(panel.status, 'inactive')
self.assertFalse(panel.is_compliant)
# 3. Test API get_inspection_detail_api
created_insp = SignPanelInspection.objects.filter(asset_object_id=panel.id).order_by('-id').first()
detail_res = self.client.get(reverse('inspections:get_inspection_detail_api', kwargs={'inspection_id': created_insp.id}))
self.assertEqual(detail_res.status_code, 200)
data = detail_res.json()
self.assertTrue(data['is_signpanel'])
class ViewerInspectionPermissionsAndGeojsonTestCase(TestCase):
def setUp(self):
self.client = Client()
self.viewer_user = User.objects.create_user(username='viewer_user', password='password123')
from common.models import UserConfig, Role, UserThematics
viewer_role, _ = Role.objects.get_or_create(name='viewer')
self.viewer_config = UserConfig.objects.create(user=self.viewer_user, is_intern=False)
self.viewer_config.roles.add(viewer_role)
self.water_thematic, _ = Thematic.objects.get_or_create(
code='water',
defaults={'name_fr': "Gestion de l'Eau"}
)
self.parking_thematic, _ = Thematic.objects.get_or_create(
code='parking',
defaults={'name_fr': "Stationnement"}
)
# Viewer only has access to water
UserThematics.objects.create(
user_config=self.viewer_config,
thematic=self.water_thematic,
can_view_assets=True,
can_view_interventions=True,
can_view_projects=True
)
# Create assets
from assets.models import NatureRWIZ, ParkingSpot
from django.contrib.gis.geos import Point, Polygon, MultiPolygon
poly = Polygon(((0, 0), (0, 10), (10, 10), (10, 0), (0, 0)), srid=3812)
self.rwiz = NatureRWIZ.objects.create(
code='RWIZ-001',
geom=MultiPolygon(poly, srid=3812)
)
self.spot = ParkingSpot.objects.create(
code='SPOT-001',
geom=poly
)
self.rwiz_ct = ContentType.objects.get_for_model(NatureRWIZ)
self.spot_ct = ContentType.objects.get_for_model(ParkingSpot)
inspector = User.objects.create_user(username='insp_user', password='password123')
self.insp_water = Inspection.objects.create(
asset_content_type=self.rwiz_ct,
asset_object_id=self.rwiz.id,
inspector=inspector,
result_status='compliant'
)
self.insp_parking = Inspection.objects.create(
asset_content_type=self.spot_ct,
asset_object_id=self.spot.id,
inspector=inspector,
result_status='compliant'
)
from django.core.files.uploadedfile import SimpleUploadedFile
dummy_file = SimpleUploadedFile("doc.txt", b"content", content_type="text/plain")
InspectionDocument.objects.create(inspection=self.insp_water, file=dummy_file)
def test_viewer_thematic_permissions(self):
from inspections.permissions import can_view_inspection, filter_viewable_inspections_for_user
self.assertTrue(can_view_inspection(self.viewer_user, self.insp_water))
self.assertFalse(can_view_inspection(self.viewer_user, self.insp_parking))
qs = filter_viewable_inspections_for_user(self.viewer_user)
self.assertIn(self.insp_water, qs)
self.assertNotIn(self.insp_parking, qs)
def test_inspections_geojson_for_viewer(self):
self.client.login(username='viewer_user', password='password123')
url = reverse('inspections:inspections_geojson')
# Test GET response correctness
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data['type'], 'FeatureCollection')
feature_ids = [f['id'] for f in data['features']]
self.assertIn(self.insp_water.id, feature_ids)
self.assertNotIn(self.insp_parking.id, feature_ids)
water_feature = next(f for f in data['features'] if f['id'] == self.insp_water.id)
self.assertEqual(water_feature['properties']['documents_count'], 1)
self.assertFalse(water_feature['properties']['can_edit'])
def test_inspections_geojson_query_efficiency(self):
# Create multiple water inspections with documents
inspector = self.insp_water.inspector
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test.utils import CaptureQueriesContext
from django.db import connection
for i in range(5):
insp = Inspection.objects.create(
asset_content_type=self.rwiz_ct,
asset_object_id=self.rwiz.id,
inspector=inspector,
result_status='compliant'
)
dummy = SimpleUploadedFile(f"doc_{i}.txt", b"content", content_type="text/plain")
InspectionDocument.objects.create(inspection=insp, file=dummy)
self.client.login(username='viewer_user', password='password123')
url = reverse('inspections:inspections_geojson')
# Ensure query count doesn't blow up (O(1) database queries instead of O(N))
with CaptureQueriesContext(connection) as ctx:
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# Number of queries should be bounded and independent of inspection count
# (O(1) queries instead of O(N), unaffected by inspection count)
self.assertLessEqual(len(ctx), 28)