- Serialize management_type, managing_authority, is_remarkable, is_dead, vitality, and botanical names in mobile_assets_search - Allow regional (Bruxelles Mobilité), remarkable, and dead trees to display differentiated icons and colors on mobile map - Fallback tree display name to vernacular/scientific name - Update asset_symbology.js cachebuster version in mobile template - Add unit test coverage for nature tree search serialization
699 lines
32 KiB
Python
699 lines
32 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.contrib.gis.geos import Polygon, MultiPolygon
|
|
from django.utils import translation
|
|
import json
|
|
|
|
from common.models import UserConfig, Role, Thematic, UserThematics
|
|
from assets.models import NatureTree, NatureLocation, NatureShrub, ParkingSpot, NatureGreenSurface
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class MobileAssetDetailTests(TestCase):
|
|
def setUp(self):
|
|
self.client = Client()
|
|
|
|
# Create or get thematic 'nature'
|
|
self.thematic, _ = Thematic.objects.get_or_create(
|
|
code="nature",
|
|
defaults={
|
|
"name_fr": "Espaces Verts & Nature",
|
|
"name_nl": "Groen & Natuur",
|
|
"icon": "bi-tree"
|
|
}
|
|
)
|
|
self.parking_thematic, _ = Thematic.objects.get_or_create(
|
|
code="parking",
|
|
defaults={
|
|
"name_fr": "Stationnement",
|
|
"name_nl": "Parkeren",
|
|
"icon": "bi-p-square"
|
|
}
|
|
)
|
|
self.sign_thematic, _ = Thematic.objects.get_or_create(
|
|
code="sign",
|
|
defaults={
|
|
"name_fr": "Signalisation",
|
|
"name_nl": "Signalisatie",
|
|
"icon": "bi-sign-stop"
|
|
}
|
|
)
|
|
|
|
# Create Roles
|
|
self.admin_role, _ = Role.objects.get_or_create(name="admin")
|
|
self.editor_role, _ = Role.objects.get_or_create(name="editor")
|
|
self.viewer_role, _ = Role.objects.get_or_create(name="viewer")
|
|
self.observer_role, _ = Role.objects.get_or_create(name="observer")
|
|
|
|
# Create test tree asset
|
|
self.tree = NatureTree.objects.create(
|
|
code="ARB-TEST-001",
|
|
name_fr="Chêne pédonculé",
|
|
name_nl="Zomereik",
|
|
status="active",
|
|
height=15.5,
|
|
circumference=160.0,
|
|
crown_diameter=8.0,
|
|
crown_clearance=3.5,
|
|
development_stage="mature",
|
|
vitality="strong",
|
|
mechanical_state="good",
|
|
risk_level="low",
|
|
tag_number="TAG-48010",
|
|
)
|
|
|
|
# Create test shrub asset (generic asset without specific template)
|
|
self.shrub = NatureShrub.objects.create(
|
|
code="ARB-SHRUB-001",
|
|
name_fr="Rosier buisson",
|
|
name_nl="Struikroos",
|
|
status="active",
|
|
)
|
|
|
|
# Create test parking spot asset
|
|
poly = Polygon(((648000, 670000), (648010, 670000), (648010, 670010), (648000, 670010), (648000, 670000)), srid=3812)
|
|
self.spot = ParkingSpot.objects.create(
|
|
code="PK-SPOT-001",
|
|
name_fr="Arceaux vélo Gare",
|
|
name_nl="Fietsbeugels Station",
|
|
status="active",
|
|
spot_type="bicycle",
|
|
geom=poly,
|
|
)
|
|
|
|
# Create test green surface asset (MultiPolygon)
|
|
poly_surf = Polygon(((648050, 670050), (648080, 670050), (648080, 670080), (648050, 670080), (648050, 670050)), srid=3812)
|
|
self.green_surface = NatureGreenSurface.objects.create(
|
|
code="S075-2138",
|
|
name_fr="Pelouse parc central",
|
|
status="active",
|
|
geom=MultiPolygon([poly_surf]),
|
|
)
|
|
|
|
def test_anonymous_user_redirected_to_login(self):
|
|
"""Anonymous user trying to access mobile asset detail is redirected to login."""
|
|
url = reverse("mobile:mobile_asset_detail_direct", args=["naturetree", self.tree.id])
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn("/login", response.url)
|
|
|
|
def test_unauthorized_user_forbidden(self):
|
|
"""User without view permissions on nature thematic receives HTTP 403."""
|
|
user = User.objects.create_user(username="forbidden_user", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.observer_role)
|
|
# Create UserThematics with can_view_assets=False
|
|
UserThematics.objects.create(
|
|
user_config=user_config,
|
|
thematic=self.thematic,
|
|
can_view_assets=False,
|
|
can_edit_assets=False
|
|
)
|
|
|
|
self.client.login(username="forbidden_user", password="password")
|
|
url = reverse("mobile:mobile_asset_detail_direct", args=["naturetree", self.tree.id])
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
def test_authorized_user_can_view_tree_mobile(self):
|
|
"""User with view permission can view tree on mobile."""
|
|
user = User.objects.create_user(username="viewer_user", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.viewer_role)
|
|
UserThematics.objects.create(
|
|
user_config=user_config,
|
|
thematic=self.thematic,
|
|
can_view_assets=True,
|
|
can_edit_assets=False
|
|
)
|
|
|
|
self.client.login(username="viewer_user", password="password")
|
|
url = reverse("mobile:mobile_asset_detail_direct", args=["naturetree", self.tree.id])
|
|
response = self.client.get(url)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertTemplateUsed(response, "assets/naturetree_detail.html")
|
|
self.assertContains(response, "ARB-TEST-001")
|
|
self.assertContains(response, "Chêne pédonculé")
|
|
self.assertTrue(response.context["mobile_view"])
|
|
self.assertTrue(response.context["is_mobile"])
|
|
|
|
def test_mobile_read_only_user_has_no_edit_bar(self):
|
|
"""Read-only user has no edit bar or buttons on mobile."""
|
|
user = User.objects.create_user(username="readonly_user", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.viewer_role)
|
|
UserThematics.objects.create(
|
|
user_config=user_config,
|
|
thematic=self.thematic,
|
|
can_view_assets=True,
|
|
can_edit_assets=False
|
|
)
|
|
|
|
self.client.login(username="readonly_user", password="password")
|
|
url = reverse("mobile:mobile_asset_detail_direct", args=["naturetree", self.tree.id])
|
|
response = self.client.get(url)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertNotContains(response, 'id="asset-cms-edit-bar"')
|
|
self.assertNotContains(response, 'id="asset-cms-edit-btn"')
|
|
|
|
def test_mobile_editor_has_sticky_edit_bar_and_actions(self):
|
|
"""User with edit permission sees the mobile sticky edit bar and action buttons."""
|
|
user = User.objects.create_user(username="editor_user", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.admin_role)
|
|
|
|
self.client.login(username="editor_user", password="password")
|
|
url = reverse("mobile:mobile_asset_detail_direct", args=["naturetree", self.tree.id])
|
|
response = self.client.get(url)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'id="asset-cms-edit-bar"')
|
|
self.assertContains(response, 'mobile-edit-bar')
|
|
self.assertContains(response, 'id="asset-edit-mode-badge"')
|
|
self.assertContains(response, 'id="asset-cms-edit-btn"')
|
|
self.assertContains(response, 'id="asset-cms-save-btn"')
|
|
self.assertContains(response, 'id="asset-cms-cancel-btn"')
|
|
|
|
def test_tree_mobile_specific_elements(self):
|
|
"""NatureTree specific mobile design: dendrometric widgets, icons and input attributes."""
|
|
user = User.objects.create_user(username="tree_admin", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.admin_role)
|
|
|
|
self.client.login(username="tree_admin", password="password")
|
|
url = reverse("mobile:mobile_asset_detail_direct", args=["naturetree", self.tree.id])
|
|
response = self.client.get(url)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
# Responsive dendrometric widgets
|
|
self.assertContains(response, "tree-metric-widget")
|
|
self.assertContains(response, "tree-header-icon")
|
|
# Touch friendly input attributes
|
|
self.assertContains(response, 'inputmode="decimal"')
|
|
self.assertContains(response, 'inputmode="numeric"')
|
|
|
|
def test_thematic_url_route(self):
|
|
"""Route with thematic prefix /mobile/assets/nature/naturetree/<id>/ works properly."""
|
|
user = User.objects.create_user(username="thematic_user", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.admin_role)
|
|
|
|
self.client.login(username="thematic_user", password="password")
|
|
url = reverse("mobile:mobile_asset_detail", args=["nature", "naturetree", self.tree.id])
|
|
response = self.client.get(url)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.context["thematic_code"], "nature")
|
|
self.assertEqual(response.context["asset"].id, self.tree.id)
|
|
|
|
def test_update_tree_via_ajax_endpoint(self):
|
|
"""Saving updated fields via update_asset AJAX endpoint updates tree in database."""
|
|
user = User.objects.create_user(username="update_user", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.admin_role)
|
|
|
|
self.client.login(username="update_user", password="password")
|
|
update_url = reverse("assets:update_asset", args=["naturetree", self.tree.id])
|
|
|
|
payload = {
|
|
"height": 18.2,
|
|
"circumference": 175.0,
|
|
"vitality": "strong",
|
|
"tag_number": "TAG-99999"
|
|
}
|
|
|
|
response = self.client.post(
|
|
update_url,
|
|
data=json.dumps(payload),
|
|
content_type="application/json"
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertTrue(data.get("success"))
|
|
|
|
# Verify database update
|
|
self.tree.refresh_from_db()
|
|
self.assertEqual(self.tree.height, 18.2)
|
|
self.assertEqual(self.tree.circumference, 175.0)
|
|
self.assertEqual(self.tree.vitality, "strong")
|
|
self.assertEqual(self.tree.tag_number, "TAG-99999")
|
|
|
|
def test_mobile_index_shows_assets_card_when_authorized(self):
|
|
"""User with asset view rights sees 'Assets' card on mobile home."""
|
|
user = User.objects.create_user(username="asset_viewer", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.viewer_role)
|
|
UserThematics.objects.create(
|
|
user_config=user_config,
|
|
thematic=self.thematic,
|
|
can_view_assets=True
|
|
)
|
|
|
|
self.client.login(username="asset_viewer", password="password")
|
|
response = self.client.get(reverse("mobile:mobile_index"))
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertTrue(response.context["can_view_assets"])
|
|
self.assertContains(response, reverse("mobile:mobile_assets_index"))
|
|
content = response.content.decode()
|
|
self.assertTrue("Assets" in content)
|
|
|
|
def test_mobile_index_hides_assets_card_when_unauthorized(self):
|
|
"""User without asset view rights does NOT see 'Assets' card on mobile home."""
|
|
user = User.objects.create_user(username="no_asset_user", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.viewer_role)
|
|
UserThematics.objects.create(
|
|
user_config=user_config,
|
|
thematic=self.thematic,
|
|
can_view_assets=False
|
|
)
|
|
|
|
self.client.login(username="no_asset_user", password="password")
|
|
response = self.client.get(reverse("mobile:mobile_index"))
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertFalse(response.context["can_view_assets"])
|
|
self.assertNotContains(response, reverse("mobile:mobile_assets_index"))
|
|
|
|
def test_mobile_assets_index_view_permissions(self):
|
|
"""Test access controls on mobile_assets_index."""
|
|
# Anonymous -> 302
|
|
response = self.client.get(reverse("mobile:mobile_assets_index"))
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
# Unauthorized user -> 403
|
|
user_forbidden = User.objects.create_user(username="index_forbidden", password="password")
|
|
user_config = UserConfig.objects.create(user=user_forbidden, is_intern=True)
|
|
user_config.roles.add(self.viewer_role)
|
|
self.client.login(username="index_forbidden", password="password")
|
|
response = self.client.get(reverse("mobile:mobile_assets_index"))
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
# Authorized user -> 200 with template and context
|
|
UserThematics.objects.create(
|
|
user_config=user_config,
|
|
thematic=self.thematic,
|
|
can_view_assets=True
|
|
)
|
|
response = self.client.get(reverse("mobile:mobile_assets_index"))
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertTemplateUsed(response, "mobile/assets/assets_index_mobile.html")
|
|
self.assertIn("accessible_thematics", response.context)
|
|
self.assertIn("thematic_config_json", response.context)
|
|
self.assertIn("asset_status_choices", response.context)
|
|
# Assert JS script block and initial view controls are rendered
|
|
self.assertContains(response, "initMap")
|
|
self.assertContains(response, "maplibre-gl.js")
|
|
self.assertContains(response, "asset_symbology.js")
|
|
self.assertContains(response, "noResultsInZoneAlert")
|
|
self.assertContains(response, "btnResetToInitialView")
|
|
self.assertContains(response, "btnInitialView")
|
|
self.assertContains(response, "returnToInitialView")
|
|
self.assertContains(response, "handleNoResultsInZoneAlert")
|
|
|
|
def test_mobile_assets_search_api(self):
|
|
"""Test mobile_assets_search JSON endpoint with filters and search queries."""
|
|
user = User.objects.create_user(username="search_user", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.viewer_role)
|
|
UserThematics.objects.create(
|
|
user_config=user_config,
|
|
thematic=self.thematic,
|
|
can_view_assets=True
|
|
)
|
|
|
|
self.client.login(username="search_user", password="password")
|
|
search_url = reverse("mobile:mobile_assets_search")
|
|
|
|
# 1. Search without thematic returns empty results (thematic required)
|
|
response = self.client.get(search_url)
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertEqual(data["count"], 0)
|
|
self.assertEqual(data["results"], [])
|
|
self.assertFalse(data["limit_reached"])
|
|
|
|
# 2. Search with mandatory thematic 'nature'
|
|
response = self.client.get(search_url, {"thematic": "nature"})
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertIn("results", data)
|
|
self.assertIn("geojson", data)
|
|
self.assertEqual(data["limit"], 500)
|
|
self.assertFalse(data["limit_reached"])
|
|
codes = [item["code"] for item in data["results"]]
|
|
self.assertIn("ARB-TEST-001", codes)
|
|
self.assertIn("ARB-SHRUB-001", codes)
|
|
self.assertIn("S075-2138", codes)
|
|
# Check asset_type in nature results and GeoJSON properties
|
|
for item in data["results"]:
|
|
self.assertIn("asset_type", item)
|
|
self.assertIn(item["asset_type"], ["naturetree", "natureshrub", "naturegreensurface"])
|
|
for feature in data["geojson"]["features"]:
|
|
self.assertIn("asset_type", feature["properties"])
|
|
self.assertIn(feature["properties"]["asset_type"], ["naturetree", "natureshrub", "naturegreensurface"])
|
|
|
|
# 3. Test limit reached logic with limit=1: when limit is reached, return empty results and empty geojson
|
|
response = self.client.get(search_url, {"thematic": "nature", "limit": "1"})
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertEqual(data["count"], 0)
|
|
self.assertEqual(data["results"], [])
|
|
self.assertEqual(data["geojson"]["features"], [])
|
|
self.assertEqual(data["limit"], 1)
|
|
self.assertTrue(data["limit_reached"])
|
|
self.assertGreater(data["total_count"], 1)
|
|
|
|
# 4. Search with keyword 'Chêne'
|
|
response = self.client.get(search_url, {"thematic": "nature", "q": "Chêne"})
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
codes = [item["code"] for item in data["results"]]
|
|
self.assertIn("ARB-TEST-001", codes)
|
|
self.assertNotIn("ARB-SHRUB-001", codes)
|
|
|
|
# 5. Filter by status 'active'
|
|
response = self.client.get(search_url, {"thematic": "nature", "status": "active"})
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertGreaterEqual(data["count"], 1)
|
|
|
|
# 6. Test search with thematic 'parking' and check spot_type and asset_type
|
|
UserThematics.objects.create(
|
|
user_config=user_config,
|
|
thematic=self.parking_thematic,
|
|
can_view_assets=True
|
|
)
|
|
response = self.client.get(search_url, {"thematic": "parking"})
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertGreaterEqual(data["count"], 1)
|
|
spot_item = next((item for item in data["results"] if item["code"] == "PK-SPOT-001"), None)
|
|
self.assertIsNotNone(spot_item)
|
|
self.assertEqual(spot_item["asset_type"], "parkingspot")
|
|
self.assertEqual(spot_item["spot_type"], "bicycle")
|
|
spot_feature = next((f for f in data["geojson"]["features"] if f["properties"].get("code") == "PK-SPOT-001"), None)
|
|
self.assertIsNotNone(spot_feature)
|
|
self.assertEqual(spot_feature["properties"]["asset_type"], "parkingspot")
|
|
self.assertEqual(spot_feature["properties"]["spot_type"], "bicycle")
|
|
|
|
def test_mobile_assets_search_nature_tree_attributes(self):
|
|
"""Test that NatureTree specific attributes (management_type, is_remarkable, is_dead, names) are serialized in mobile search."""
|
|
user = User.objects.create_user(username="tree_search_user", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.viewer_role)
|
|
UserThematics.objects.create(
|
|
user_config=user_config,
|
|
thematic=self.thematic,
|
|
can_view_assets=True
|
|
)
|
|
self.client.login(username="tree_search_user", password="password")
|
|
|
|
regional_tree = NatureTree.objects.create(
|
|
code="BM-ARB-999",
|
|
status="active",
|
|
lon=4.4210,
|
|
lat=50.8120,
|
|
management_type="regional",
|
|
managing_authority="Bruxelles Mobilité",
|
|
is_remarkable=True,
|
|
vernacular_name="Platane d'Orient",
|
|
scientific_name="Platanus orientalis",
|
|
)
|
|
dead_tree = NatureTree.objects.create(
|
|
code="ARB-DEAD-001",
|
|
status="active",
|
|
lon=4.4215,
|
|
lat=50.8125,
|
|
is_dead=True,
|
|
vitality="dead",
|
|
vernacular_name="Frêne élevé",
|
|
scientific_name="Fraxinus excelsior",
|
|
)
|
|
search_url = reverse("mobile:mobile_assets_search")
|
|
response = self.client.get(search_url, {"thematic": "nature"})
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
|
|
# Check regional tree
|
|
reg_item = next((item for item in data["results"] if item["code"] == "BM-ARB-999"), None)
|
|
self.assertIsNotNone(reg_item)
|
|
self.assertEqual(reg_item["management_type"], "regional")
|
|
self.assertEqual(reg_item["managing_authority"], "Bruxelles Mobilité")
|
|
self.assertTrue(reg_item["is_remarkable"])
|
|
self.assertFalse(reg_item["is_dead"])
|
|
self.assertEqual(reg_item["name"], "Platane d'Orient")
|
|
self.assertEqual(reg_item["vernacular_name"], "Platane d'Orient")
|
|
self.assertEqual(reg_item["scientific_name"], "Platanus orientalis")
|
|
|
|
reg_feature = next((f for f in data["geojson"]["features"] if f["properties"].get("code") == "BM-ARB-999"), None)
|
|
self.assertIsNotNone(reg_feature)
|
|
self.assertEqual(reg_feature["properties"]["management_type"], "regional")
|
|
self.assertEqual(reg_feature["properties"]["managing_authority"], "Bruxelles Mobilité")
|
|
self.assertTrue(reg_feature["properties"]["is_remarkable"])
|
|
self.assertFalse(reg_feature["properties"]["is_dead"])
|
|
|
|
# Check dead tree
|
|
dead_item = next((item for item in data["results"] if item["code"] == "ARB-DEAD-001"), None)
|
|
self.assertIsNotNone(dead_item)
|
|
self.assertTrue(dead_item["is_dead"])
|
|
self.assertEqual(dead_item["vitality"], "dead")
|
|
|
|
def test_generic_asset_uses_mobile_generic_template(self):
|
|
"""An asset model without specific template (NatureShrub) uses detail_generic_mobile.html."""
|
|
user = User.objects.create_user(username="shrub_viewer", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.admin_role)
|
|
|
|
self.client.login(username="shrub_viewer", password="password")
|
|
url = reverse("mobile:mobile_asset_detail_direct", args=["natureshrub", self.shrub.id])
|
|
response = self.client.get(url)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertTemplateUsed(response, "assets/detail_generic_mobile.html")
|
|
self.assertContains(response, "ARB-SHRUB-001")
|
|
self.assertContains(response, "Rosier buisson")
|
|
# Assert mobile navigation tabs and scripts are present
|
|
self.assertContains(response, "mobile-asset-tabs-nav")
|
|
self.assertContains(response, 'data-target-tab="tab-infos"')
|
|
self.assertContains(response, 'data-target-tab="tab-geo"')
|
|
self.assertContains(response, 'data-target-tab="tab-docs"')
|
|
self.assertContains(response, 'data-target-tab="tab-links"')
|
|
self.assertContains(response, "asset_cms_edit_mode.js")
|
|
self.assertContains(response, "initMobileAssetTabs")
|
|
|
|
def test_specific_asset_retains_specific_template(self):
|
|
"""NatureTree continues to use its specific template naturetree_detail.html."""
|
|
user = User.objects.create_user(username="tree_viewer", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.admin_role)
|
|
|
|
self.client.login(username="tree_viewer", password="password")
|
|
url = reverse("mobile:mobile_asset_detail_direct", args=["naturetree", self.tree.id])
|
|
response = self.client.get(url)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertTemplateUsed(response, "assets/naturetree_detail.html")
|
|
|
|
def test_search_linear_and_surfacic_geometries(self):
|
|
"""Surfacic and linear assets return their real Polygon/MultiPolygon geometry, bbox, and geom_type."""
|
|
user = User.objects.create_user(username="geo_search_user", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.admin_role)
|
|
UserThematics.objects.create(
|
|
user_config=user_config,
|
|
thematic=self.thematic,
|
|
can_view_assets=True
|
|
)
|
|
UserThematics.objects.create(
|
|
user_config=user_config,
|
|
thematic=self.parking_thematic,
|
|
can_view_assets=True
|
|
)
|
|
|
|
self.client.login(username="geo_search_user", password="password")
|
|
search_url = reverse("mobile:mobile_assets_search")
|
|
|
|
# 1. Test nature search with green surface (MultiPolygon)
|
|
response = self.client.get(search_url, {"thematic": "nature", "q": "S075-2138"})
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertEqual(data["count"], 1)
|
|
|
|
result_item = data["results"][0]
|
|
self.assertEqual(result_item["code"], "S075-2138")
|
|
self.assertEqual(result_item["geom_type"], "MultiPolygon")
|
|
self.assertIsNotNone(result_item["bbox"])
|
|
self.assertEqual(len(result_item["bbox"]), 4)
|
|
self.assertIsNotNone(result_item["lat"])
|
|
self.assertIsNotNone(result_item["lon"])
|
|
|
|
feature = data["geojson"]["features"][0]
|
|
self.assertEqual(feature["geometry"]["type"], "MultiPolygon")
|
|
self.assertEqual(feature["properties"]["code"], "S075-2138")
|
|
self.assertEqual(feature["properties"]["geom_type"], "MultiPolygon")
|
|
self.assertEqual(feature["properties"]["bbox"], result_item["bbox"])
|
|
self.assertEqual(feature["id"], f"naturegreensurface_{self.green_surface.id}")
|
|
self.assertIn("element_type", result_item)
|
|
self.assertIn("element_type", feature["properties"])
|
|
self.assertTrue(len(result_item["element_type"]) > 0)
|
|
|
|
# 2. Test parking search with parking spot (Polygon)
|
|
response = self.client.get(search_url, {"thematic": "parking", "q": "PK-SPOT-001"})
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertEqual(data["count"], 1)
|
|
|
|
spot_res = data["results"][0]
|
|
self.assertEqual(spot_res["geom_type"], "Polygon")
|
|
self.assertIsNotNone(spot_res["bbox"])
|
|
self.assertEqual(len(spot_res["bbox"]), 4)
|
|
self.assertIn("element_type", spot_res)
|
|
self.assertTrue(len(spot_res["element_type"]) > 0)
|
|
|
|
spot_feat = data["geojson"]["features"][0]
|
|
self.assertEqual(spot_feat["geometry"]["type"], "Polygon")
|
|
self.assertEqual(spot_feat["properties"]["geom_type"], "Polygon")
|
|
self.assertIn("element_type", spot_feat["properties"])
|
|
|
|
def test_search_signpanel_symbology_and_coordinates(self):
|
|
"""SignPanel returns panel_type, svg_url, icon_url and inherits pole coordinates."""
|
|
user = User.objects.create_user(username="sign_search_user", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.admin_role)
|
|
UserThematics.objects.create(
|
|
user_config=user_config,
|
|
thematic=self.sign_thematic,
|
|
can_view_assets=True
|
|
)
|
|
|
|
from assets.models import SignPanel, SignPole, SignStreet, SignPanelType
|
|
from common.models import Municipality
|
|
mun, _ = Municipality.objects.get_or_create(name_fr="Bruxelles", defaults={"name_nl": "Brussel", "niscode": "21004"})
|
|
street = SignStreet.objects.create(name_fr="Avenue Louise", municipality=mun)
|
|
pole = SignPole.objects.create(
|
|
code="POT-001",
|
|
street=street,
|
|
lat=50.8466,
|
|
lon=4.3528,
|
|
status="active",
|
|
)
|
|
panel_type, _ = SignPanelType.objects.get_or_create(
|
|
code="B1",
|
|
defaults={
|
|
"name_fr": "Cédez le passage",
|
|
"name_nl": "Voorrang verlenen",
|
|
}
|
|
)
|
|
panel = SignPanel.objects.create(
|
|
code="PAN-B1-001",
|
|
street=street,
|
|
pole=pole,
|
|
signpanel_type=panel_type,
|
|
vertical_order=1,
|
|
status="active",
|
|
)
|
|
|
|
self.client.login(username="sign_search_user", password="password")
|
|
search_url = reverse("mobile:mobile_assets_search")
|
|
|
|
# 1. Search without q filter
|
|
response = self.client.get(search_url, {"thematic": "sign"})
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertTrue(data["count"] >= 1)
|
|
|
|
panel_item = next(item for item in data["results"] if item["id"] == panel.id and item["model_key"] == "signpanel")
|
|
self.assertEqual(panel_item["panel_type"], "B1")
|
|
self.assertEqual(panel_item["svg_url"], "/static/assets/road_signs/2025/B1.svg")
|
|
self.assertEqual(panel_item["icon_url"], "/static/assets/road_signs/2025/B1.svg")
|
|
self.assertEqual(panel_item["pole_id"], pole.id)
|
|
self.assertEqual(panel_item["vertical_order"], 1)
|
|
self.assertEqual(panel_item["panel_vertical_order"], 1)
|
|
self.assertAlmostEqual(panel_item["lat"], 50.8466, places=4)
|
|
self.assertAlmostEqual(panel_item["lon"], 4.3528, places=4)
|
|
self.assertIn("B1", panel_item["name"])
|
|
self.assertIn("Cédez le passage", panel_item["name"])
|
|
|
|
feature = next(f for f in data["geojson"]["features"] if f["id"] == f"signpanel_{panel.id}")
|
|
self.assertEqual(feature["properties"]["panel_type"], "B1")
|
|
self.assertEqual(feature["properties"]["svg_url"], "/static/assets/road_signs/2025/B1.svg")
|
|
self.assertEqual(feature["properties"]["icon_url"], "/static/assets/road_signs/2025/B1.svg")
|
|
self.assertEqual(feature["properties"]["pole_id"], pole.id)
|
|
self.assertEqual(feature["properties"]["vertical_order"], 1)
|
|
self.assertEqual(feature["properties"]["panel_vertical_order"], 1)
|
|
self.assertAlmostEqual(feature["geometry"]["coordinates"][1], 50.8466, places=4)
|
|
self.assertAlmostEqual(feature["geometry"]["coordinates"][0], 4.3528, places=4)
|
|
|
|
# 2. Search by panel type name
|
|
response = self.client.get(search_url, {"thematic": "sign", "q": "Cédez"})
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertEqual(data["count"], 1)
|
|
self.assertEqual(data["results"][0]["id"], panel.id)
|
|
|
|
def test_search_multiple_signpanels_on_same_pole_vertical_order(self):
|
|
"""Multiple panels on the same pole return distinct vertical_order and shared pole_id."""
|
|
user = User.objects.create_user(username="multi_panel_user", password="password")
|
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
|
user_config.roles.add(self.admin_role)
|
|
UserThematics.objects.create(
|
|
user_config=user_config,
|
|
thematic=self.sign_thematic,
|
|
can_view_assets=True
|
|
)
|
|
|
|
from assets.models import SignPanel, SignPole, SignStreet, SignPanelType
|
|
from common.models import Municipality
|
|
mun, _ = Municipality.objects.get_or_create(name_fr="Bruxelles", defaults={"name_nl": "Brussel", "niscode": "21004"})
|
|
street = SignStreet.objects.create(name_fr="Rue Royale", municipality=mun)
|
|
pole = SignPole.objects.create(
|
|
code="POT-MULTI-01",
|
|
street=street,
|
|
lat=50.8500,
|
|
lon=4.3600,
|
|
status="active",
|
|
)
|
|
type_a, _ = SignPanelType.objects.get_or_create(code="A1a", defaults={"name_fr": "Virage dangereux à droite"})
|
|
type_c, _ = SignPanelType.objects.get_or_create(code="C1", defaults={"name_fr": "Sens interdit"})
|
|
|
|
panel1 = SignPanel.objects.create(
|
|
code="PAN-A1a-001",
|
|
street=street,
|
|
pole=pole,
|
|
signpanel_type=type_a,
|
|
vertical_order=1,
|
|
status="active",
|
|
)
|
|
panel2 = SignPanel.objects.create(
|
|
code="PAN-C1-002",
|
|
street=street,
|
|
pole=pole,
|
|
signpanel_type=type_c,
|
|
vertical_order=2,
|
|
status="active",
|
|
)
|
|
|
|
self.client.login(username="multi_panel_user", password="password")
|
|
search_url = reverse("mobile:mobile_assets_search")
|
|
|
|
response = self.client.get(search_url, {"thematic": "sign"})
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
|
|
results_by_id = {r["id"]: r for r in data["results"] if r["model_key"] == "signpanel"}
|
|
self.assertIn(panel1.id, results_by_id)
|
|
self.assertIn(panel2.id, results_by_id)
|
|
|
|
self.assertEqual(results_by_id[panel1.id]["pole_id"], pole.id)
|
|
self.assertEqual(results_by_id[panel1.id]["vertical_order"], 1)
|
|
self.assertEqual(results_by_id[panel2.id]["pole_id"], pole.id)
|
|
self.assertEqual(results_by_id[panel2.id]["vertical_order"], 2)
|
|
|
|
|
|
|