loko/loko/mobile/tests_asset_mobile.py

571 lines
26 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_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,
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.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.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)