loko/loko/assets/tests_management_zones.py

597 lines
25 KiB
Python

from django.test import TestCase, Client
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.contrib.gis.geos import Polygon, MultiPolygon, Point
from common.models import Thematic, Role, UserConfig, UserThematics
from assets.models import (
ManagementZoneType,
ManagementZone,
ManagementZoneAsset,
NatureGreenSurface,
NatureLocation,
)
User = get_user_model()
class ManagementZoneModelTests(TestCase):
def setUp(self):
self.thematic_nature = Thematic.objects.create(
code="nature",
name_fr="Nature",
name_nl="Natuur"
)
self.zone_type = ManagementZoneType.objects.create(
code="fauche_tardive",
name_fr="Fauche tardive",
name_nl="Laat maaien",
thematic=self.thematic_nature,
default_color="#28a745",
description="Zone de gestion écologique avec fauche annuelle en automne."
)
def test_management_zone_type_bilingual_and_str(self):
self.assertEqual(self.zone_type.name_fr, "Fauche tardive")
self.assertEqual(self.zone_type.name_nl, "Laat maaien")
self.assertIn("Fauche tardive", str(self.zone_type))
def test_management_zone_creation_geometry_and_color_fallback(self):
poly = Polygon(((0, 0), (100, 0), (100, 100), (0, 100), (0, 0)))
multi_poly = MultiPolygon([poly], srid=3812)
# 1. Sans couleur explicite -> prend la couleur par défaut du type
zone1 = ManagementZone.objects.create(
code="ZG-NAT-001",
name_fr="Marais de Jette - Prairie",
name_nl="Moeras van Jette - Weide",
thematic=self.thematic_nature,
zone_type=self.zone_type,
geom=multi_poly,
target_frequency_days=365,
seasonality_notes="Fauche en septembre; pas de taille au printemps",
instructions="Attention aux zones humides et aux ornières.",
)
self.assertEqual(zone1.get_color(), "#28a745")
self.assertEqual(zone1.geom.srid, 3812)
self.assertEqual(zone1.geom.geom_type, "MultiPolygon")
# GeoJSON property
geojson = zone1.geojson_geometry
self.assertIsNotNone(geojson)
self.assertEqual(geojson["type"], "MultiPolygon")
# Spatial intersection
pt_inside = Point(50, 50, srid=3812)
pt_outside = Point(200, 200, srid=3812)
self.assertTrue(ManagementZone.objects.filter(pk=zone1.pk, geom__intersects=pt_inside).exists())
self.assertFalse(ManagementZone.objects.filter(pk=zone1.pk, geom__intersects=pt_outside).exists())
# 2. Avec couleur personnalisée explicite
zone2 = ManagementZone.objects.create(
code="ZG-NAT-002",
name_fr="Zone spéciale",
thematic=self.thematic_nature,
color="#ff5722",
)
self.assertEqual(zone2.get_color(), "#ff5722")
class ManagementZoneAssetTests(TestCase):
def setUp(self):
self.thematic_nature = Thematic.objects.create(code="nature", name_fr="Nature", name_nl="Natuur")
self.zone = ManagementZone.objects.create(
code="ZG-NAT-TEST",
name_fr="Zone test",
thematic=self.thematic_nature,
)
self.location = NatureLocation.objects.create(code="LOC-01", name_fr="Parc de Jette")
self.green_surface = NatureGreenSurface.objects.create(
code="GS-01",
name_fr="Pelouse 1",
location=self.location,
)
def test_explicit_asset_association_and_exclusion(self):
# Association explicite d'inclusion
link = ManagementZoneAsset.objects.create(
management_zone=self.zone,
asset=self.green_surface,
is_excluded=False,
notes="Inclus dans le plan"
)
self.assertIn("Inclus", str(link))
self.assertEqual(self.zone.explicit_assets.count(), 1)
self.assertEqual(self.zone.explicit_assets.first().asset, self.green_surface)
# Bascule en exclusion
link.is_excluded = True
link.save()
self.assertIn("Exclu", str(link))
class ManagementZoneGeojsonEndpointTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(username="nature_manager", password="password")
self.role_manager = Role.objects.create(name="manager")
self.config = UserConfig.objects.create(user=self.user)
self.config.roles.add(self.role_manager)
self.thematic_nature = Thematic.objects.create(code="nature", name_fr="Nature", name_nl="Natuur")
self.thematic_roads = Thematic.objects.create(code="roads", name_fr="Voiries", name_nl="Wegen")
UserThematics.objects.create(
user_config=self.config,
thematic=self.thematic_nature,
can_view_assets=True,
can_edit_assets=True
)
self.zone_type = ManagementZoneType.objects.create(
code="fauche",
name_fr="Fauche tardive",
thematic=self.thematic_nature,
default_color="#198754"
)
poly = Polygon(((0, 0), (10, 0), (10, 10), (0, 10), (0, 0)))
self.multi_poly = MultiPolygon([poly], srid=3812)
self.zone_nature = ManagementZone.objects.create(
code="ZG-NAT-1",
name_fr="Prairie fleurie",
thematic=self.thematic_nature,
zone_type=self.zone_type,
geom=self.multi_poly,
instructions="Ne pas tondre au printemps",
target_frequency_days=180,
seasonality_notes="Septembre",
)
self.zone_roads = ManagementZone.objects.create(
code="ZG-ROAD-1",
name_fr="Itinéraire déneigement P1",
thematic=self.thematic_roads,
geom=self.multi_poly,
color="#0d6efd",
)
def test_anonymous_access_redirects(self):
url = reverse("assets:management_zones_geojson")
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
def test_authenticated_user_access_nature_zones(self):
self.client.login(username="nature_manager", password="password")
url = reverse("assets:management_zones_geojson") + "?thematic=nature"
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data["type"], "FeatureCollection")
self.assertEqual(len(data["features"]), 1)
feat = data["features"][0]
self.assertEqual(feat["id"], self.zone_nature.id)
self.assertEqual(feat["properties"]["code"], "ZG-NAT-1")
self.assertEqual(feat["properties"]["name"], "Prairie fleurie")
self.assertEqual(feat["properties"]["thematic"], "nature")
self.assertEqual(feat["properties"]["zone_type_name"], "Fauche tardive")
self.assertEqual(feat["properties"]["color"], "#198754")
self.assertEqual(feat["properties"]["instructions"], "Ne pas tondre au printemps")
self.assertEqual(feat["properties"]["target_frequency_days"], 180)
self.assertEqual(feat["geometry"]["type"], "MultiPolygon")
def test_user_without_thematic_permission_denied(self):
self.client.login(username="nature_manager", password="password")
# nature_manager n'a pas accès à la thématique 'roads'
url = reverse("assets:management_zones_geojson") + "?thematic=roads"
response = self.client.get(url)
self.assertEqual(response.status_code, 403)
def test_superuser_access_all_thematics(self):
admin = User.objects.create_superuser(username="superadmin", password="password", email="admin@example.com")
self.client.login(username="superadmin", password="password")
# Sans paramètre thématique, superuser reçoit toutes les zones avec géométrie
url = reverse("assets:management_zones_geojson")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(len(data["features"]), 2)
import json
from django.core.files.uploadedfile import SimpleUploadedFile
from assets.permissions import (
can_view_management_zones,
can_edit_management_zones,
can_delete_management_zone,
get_editable_thematics_for_management_zones,
)
from assets.forms import ManagementZoneForm
from assets.models import ManagementZoneDocument
class ManagementZonesPermissionAndCRUDTests(TestCase):
def setUp(self):
# Users
self.user_manager = User.objects.create_user(username="mgr", password="password")
self.user_viewer = User.objects.create_user(username="viewer", password="password")
self.user_editor = User.objects.create_user(username="editor", password="password")
self.user_no_access = User.objects.create_user(username="no_access", password="password")
# Thematics
self.thematic_nature = Thematic.objects.create(code="nature", name_fr="Nature", name_nl="Natuur")
self.thematic_roads = Thematic.objects.create(code="roads", name_fr="Voiries", name_nl="Wegen")
# Roles & Configs
self.role_manager = Role.objects.create(name="manager")
self.role_operator = Role.objects.create(name="operator")
self.config_mgr = UserConfig.objects.create(user=self.user_manager)
self.config_mgr.roles.add(self.role_manager)
self.config_viewer = UserConfig.objects.create(user=self.user_viewer)
UserThematics.objects.create(
user_config=self.config_viewer,
thematic=self.thematic_nature,
can_view_assets=True,
can_edit_assets=False,
can_edit_management_zones=False
)
self.config_editor = UserConfig.objects.create(user=self.user_editor)
UserThematics.objects.create(
user_config=self.config_editor,
thematic=self.thematic_nature,
can_view_assets=True,
can_edit_assets=False,
can_edit_management_zones=True # Specific permission
)
self.config_no_access = UserConfig.objects.create(user=self.user_no_access)
# Type de zone
self.zone_type = ManagementZoneType.objects.create(
code="fauche_eco",
name_fr="Fauche écologique",
thematic=self.thematic_nature,
default_color="#20c997"
)
poly = Polygon(((0, 0), (50, 0), (50, 50), (0, 50), (0, 0)))
self.zone = ManagementZone.objects.create(
code="ZG-TEST-01",
name_fr="Zone Test Marais",
name_nl="Testzone Moeras",
thematic=self.thematic_nature,
zone_type=self.zone_type,
geom=MultiPolygon([poly], srid=3812),
target_frequency_days=90,
seasonality_notes="Avril-Septembre",
instructions="Attention à la faune aquatique",
is_active=True
)
def test_permission_functions(self):
# Manager can view and edit any thematic
self.assertTrue(can_view_management_zones(self.user_manager))
self.assertTrue(can_edit_management_zones(self.user_manager))
self.assertTrue(can_edit_management_zones(self.user_manager, thematic=self.thematic_nature))
# Viewer can view nature, but cannot edit
self.assertTrue(can_view_management_zones(self.user_viewer, thematic=self.thematic_nature))
self.assertFalse(can_view_management_zones(self.user_viewer, thematic=self.thematic_roads))
self.assertFalse(can_edit_management_zones(self.user_viewer))
self.assertFalse(can_edit_management_zones(self.user_viewer, thematic=self.thematic_nature))
# Editor with can_edit_management_zones=True
self.assertTrue(can_view_management_zones(self.user_editor, thematic=self.thematic_nature))
self.assertTrue(can_edit_management_zones(self.user_editor))
self.assertTrue(can_edit_management_zones(self.user_editor, thematic=self.thematic_nature))
self.assertFalse(can_edit_management_zones(self.user_editor, thematic=self.thematic_roads))
# Editable thematics
editable = get_editable_thematics_for_management_zones(self.user_editor)
self.assertIn(self.thematic_nature, editable)
self.assertNotIn(self.thematic_roads, editable)
def test_management_zone_form_with_geom_geojson(self):
# Test initial geojson
form = ManagementZoneForm(instance=self.zone, user=self.user_editor)
self.assertIn('geom_geojson', form.initial)
geom_dict = json.loads(form.initial['geom_geojson'])
self.assertEqual(geom_dict['type'], 'MultiPolygon')
# Test saving with new GeoJSON polygon in WGS84
geojson_polygon = {
"type": "Polygon",
"coordinates": [[[4.35, 50.84], [4.36, 50.84], [4.36, 50.85], [4.35, 50.85], [4.35, 50.84]]]
}
data = {
'code': 'ZG-NEW-01',
'name_fr': 'Nouvelle zone',
'thematic': self.thematic_nature.pk,
'zone_type': self.zone_type.pk,
'color': '#ff9900',
'target_frequency_days': 30,
'geom_geojson': json.dumps(geojson_polygon),
'is_active': True,
}
form = ManagementZoneForm(data=data, user=self.user_editor)
self.assertTrue(form.is_valid(), form.errors)
new_zone = form.save()
self.assertEqual(new_zone.code, 'ZG-NEW-01')
self.assertIsNotNone(new_zone.geom)
self.assertEqual(new_zone.geom.srid, 3812)
self.assertEqual(new_zone.geom.geom_type, 'MultiPolygon')
def test_management_zones_index_view(self):
url = reverse("assets:management_zones_index")
# Anonymous -> 302
res = self.client.get(url)
self.assertEqual(res.status_code, 302)
# No access user -> 403
self.client.login(username="no_access", password="password")
res = self.client.get(url)
self.assertEqual(res.status_code, 403)
# Viewer -> 200
self.client.login(username="viewer", password="password")
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
self.assertContains(res, "ZG-TEST-01")
self.assertFalse(res.context['can_create'])
# Editor -> 200 and can_create is True
self.client.login(username="editor", password="password")
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
self.assertTrue(res.context['can_create'])
def test_management_zone_create_view(self):
url = reverse("assets:management_zone_create")
# Viewer cannot create -> 403
self.client.login(username="viewer", password="password")
res = self.client.get(url)
self.assertEqual(res.status_code, 403)
# Editor can access form
self.client.login(username="editor", password="password")
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
self.assertContains(res, "layer-switcher-control.js")
self.assertContains(res, "AddressSearchControl")
# Post valid zone
geojson_polygon = {
"type": "Polygon",
"coordinates": [[[4.35, 50.84], [4.36, 50.84], [4.36, 50.85], [4.35, 50.85], [4.35, 50.84]]]
}
post_data = {
'code': 'ZG-CREATED-01',
'name_fr': 'Zone créée',
'thematic': self.thematic_nature.pk,
'zone_type': self.zone_type.pk,
'color': '#28a745',
'target_frequency_days': 15,
'geom_geojson': json.dumps(geojson_polygon),
'instructions': 'Passage le matin uniquement',
'is_active': True,
}
res = self.client.post(url, post_data)
self.assertEqual(res.status_code, 302)
created_zone = ManagementZone.objects.get(code='ZG-CREATED-01')
self.assertEqual(created_zone.created_by, self.user_editor)
self.assertEqual(created_zone.instructions, 'Passage le matin uniquement')
self.assertEqual(created_zone.geom.srid, 3812)
def test_management_zone_edit_view(self):
url = reverse("assets:management_zone_edit", kwargs={'pk': self.zone.pk})
# Viewer cannot edit -> 403
self.client.login(username="viewer", password="password")
res = self.client.get(url)
self.assertEqual(res.status_code, 403)
# Editor can edit
self.client.login(username="editor", password="password")
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
self.assertContains(res, "layer-switcher-control.js")
self.assertContains(res, "AddressSearchControl")
post_data = {
'code': self.zone.code,
'name_fr': 'Nom Modifié Marais',
'thematic': self.thematic_nature.pk,
'zone_type': self.zone_type.pk,
'color': '#ff5500',
'target_frequency_days': 120,
'geom_geojson': json.dumps({
"type": "Polygon",
"coordinates": [[[4.35, 50.84], [4.37, 50.84], [4.37, 50.86], [4.35, 50.86], [4.35, 50.84]]]
}),
'instructions': 'Consigne mise à jour',
'is_active': True,
}
res = self.client.post(url, post_data)
self.assertEqual(res.status_code, 302)
self.zone.refresh_from_db()
self.assertEqual(self.zone.name_fr, 'Nom Modifié Marais')
self.assertEqual(self.zone.color, '#ff5500')
self.assertEqual(self.zone.target_frequency_days, 120)
def test_management_zone_detail_view(self):
url = reverse("assets:management_zone_detail", kwargs={'pk': self.zone.pk})
# Viewer can view
self.client.login(username="viewer", password="password")
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
self.assertContains(res, "ZG-TEST-01")
self.assertFalse(res.context['can_edit'])
# Editor can view with can_edit=True
self.client.login(username="editor", password="password")
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
self.assertTrue(res.context['can_edit'])
def test_management_zone_delete_view(self):
zone_to_del = ManagementZone.objects.create(
code="ZG-TO-DELETE",
name_fr="A supprimer",
thematic=self.thematic_nature,
)
url = reverse("assets:management_zone_delete", kwargs={'pk': zone_to_del.pk})
# Viewer cannot delete -> 403
self.client.login(username="viewer", password="password")
res = self.client.post(url)
self.assertEqual(res.status_code, 403)
# Editor can delete
self.client.login(username="editor", password="password")
res = self.client.post(url)
self.assertEqual(res.status_code, 302)
self.assertFalse(ManagementZone.objects.filter(pk=zone_to_del.pk).exists())
def test_management_zone_document_upload_and_delete(self):
self.client.login(username="editor", password="password")
# Upload document
upload_url = reverse("assets:management_zone_add_document", kwargs={'pk': self.zone.pk})
fake_file = SimpleUploadedFile("rapport_tonte.pdf", b"Dummy PDF content", content_type="application/pdf")
res = self.client.post(upload_url, {'documents': [fake_file]})
self.assertEqual(res.status_code, 302)
self.assertEqual(self.zone.documents.count(), 1)
doc = self.zone.documents.first()
self.assertEqual(doc.uploaded_by, self.user_editor)
# Delete document
delete_url = reverse("assets:management_zone_delete_document", kwargs={'pk': self.zone.pk, 'document_id': doc.pk})
res = self.client.post(delete_url)
self.assertEqual(res.status_code, 302)
self.assertEqual(self.zone.documents.count(), 0)
def test_management_zone_types_ajax(self):
self.client.login(username="editor", password="password")
url = reverse("assets:management_zone_types_ajax") + f"?thematic_id={self.thematic_nature.pk}"
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
data = res.json()
self.assertEqual(len(data['types']), 1)
self.assertEqual(data['types'][0]['code'], 'fauche_eco')
class ManagementZoneUnifiedViewIntegrationTests(TestCase):
def setUp(self):
self.thematic_nature = Thematic.objects.create(name_fr="Nature", name_nl="Natuur", code="nature")
self.thematic_roads = Thematic.objects.create(name_fr="Voirie", name_nl="Wegen", code="roads")
self.user_editor = User.objects.create_user(username="editor_unified", password="password")
self.user_viewer = User.objects.create_user(username="viewer_unified", password="password")
self.config_editor = UserConfig.objects.create(user=self.user_editor)
self.config_viewer = UserConfig.objects.create(user=self.user_viewer)
UserThematics.objects.create(
user_config=self.config_editor,
thematic=self.thematic_nature,
can_view_assets=True,
can_edit_assets=True,
can_edit_management_zones=True,
)
UserThematics.objects.create(
user_config=self.config_viewer,
thematic=self.thematic_nature,
can_view_assets=True,
can_edit_assets=False,
can_edit_management_zones=False,
)
self.zone_type = ManagementZoneType.objects.create(
thematic=self.thematic_nature,
code="fauche_eco",
name_fr="Fauche tardive",
name_nl="Laat maaien",
default_color="#27ae60",
)
poly = Polygon(((149000, 170000), (149100, 170000), (149100, 170100), (149000, 170100), (149000, 170000)))
multi_poly = MultiPolygon([poly], srid=3812)
self.zone = ManagementZone.objects.create(
thematic=self.thematic_nature,
zone_type=self.zone_type,
code="ZN-TEST-001",
name_fr="Pelouse A",
name_nl="Grasveld A",
geom=multi_poly,
is_active=True,
created_by=self.user_editor,
)
def test_build_thematic_config_includes_management_zones_url(self):
from assets.views.core import build_thematic_config
config = build_thematic_config([self.thematic_nature, self.thematic_roads])
self.assertIn("nature", config)
self.assertIn("management_zones_geojson_url", config["nature"])
self.assertEqual(config["nature"]["management_zones_geojson_url"], reverse("assets:management_zones_geojson"))
def test_build_unified_add_modal_config_permissions(self):
from assets.views.core import build_unified_add_modal_config
# Editor can edit management zones in nature
editor_config = build_unified_add_modal_config([self.thematic_nature], user=self.user_editor)
self.assertIn("management_zone_thematics", editor_config)
editor_codes = [t["code"] for t in editor_config["management_zone_thematics"]]
self.assertIn("nature", editor_codes)
# Viewer cannot edit management zones
viewer_config = build_unified_add_modal_config([self.thematic_nature], user=self.user_viewer)
viewer_codes = [t["code"] for t in viewer_config["management_zone_thematics"]]
self.assertNotIn("nature", viewer_codes)
def test_management_zones_geojson_properties_for_unified_table(self):
self.client.login(username="editor_unified", password="password")
url = reverse("assets:management_zones_geojson") + f"?thematic={self.thematic_nature.code}"
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
data = res.json()
self.assertEqual(len(data["features"]), 1)
feat = data["features"][0]
props = feat["properties"]
self.assertTrue(props["is_management_zone"])
self.assertFalse(props["is_location"])
self.assertEqual(props["asset_type"], "managementzone")
self.assertIn(props["element_type"], ["Zone de gestion", "Management zone"])
self.assertEqual(props["category"], "Fauche tardive")
self.assertEqual(props["status"], "active")
self.assertIsNotNone(props["lon"])
self.assertIsNotNone(props["lat"])
self.assertIsNotNone(props["area"])
self.assertEqual(props["detail_url"], reverse("assets:management_zone_detail", kwargs={"pk": self.zone.pk}))
def test_unified_index_page_contains_management_zone_elements(self):
self.client.login(username="editor_unified", password="password")
url = reverse("assets:index")
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
# Check filter modal checkboxes
self.assertContains(res, 'id="type-cb-assets"')
self.assertContains(res, 'id="type-cb-locations"')
self.assertContains(res, 'id="type-cb-management-zones"')
# Check add modal card
self.assertContains(res, 'id="unified-add-management-zone-thematic"')
self.assertContains(res, 'id="unified-add-management-zone-open"')