fix(mobile): resolve nature tree marker symbology and attribute serialization

- 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
This commit is contained in:
kdeterme 2026-10-09 19:52:11 +02:00
parent 45bc88d1a3
commit fbd1bc452f
4 changed files with 108 additions and 3 deletions

View file

@ -291,7 +291,7 @@
naturetree: {
thematic: 'nature',
color: '#2e7d32',
iconUrl: '/static/assets/nature_tree_icon.svg?v=20260521a',
iconUrl: '/static/assets/nature_tree_icon.svg?v=20261009_round',
iconSize: 56,
iconClass: 'bi-tree-fill',
glyphCodepoint: 'f5e1',
@ -613,7 +613,8 @@
}
const isRemarkable = item.is_remarkable === true || item.is_remarkable === 'true' || item.is_remarkable === 1;
const isRegional = item.management_type === 'regional' ||
(typeof item.managing_authority === 'string' && (item.managing_authority.toLowerCase().includes('région') || item.managing_authority.toLowerCase().includes('mobilité')));
(typeof item.managing_authority === 'string' && (item.managing_authority.toLowerCase().includes('région') || item.managing_authority.toLowerCase().includes('mobilité'))) ||
(typeof item.code === 'string' && item.code.toUpperCase().startsWith('BM-ARB'));
if (isRemarkable && ASSET_TYPE_OVERRIDES.naturetree_remarkable) {
return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree_remarkable);
}

View file

@ -1135,7 +1135,7 @@
{% endblock content %}
{% block extra_scripts %}
<script src="{% static 'assets/asset_symbology.js' %}?v=20260913"></script>
<script src="{% static 'assets/asset_symbology.js' %}?v=20261009_round_trees_v2"></script>
<script>
(function() {
'use strict';
@ -2048,6 +2048,10 @@
el.style.color = color;
el.innerHTML = iconHtml;
const featId = primaryAsset.feature_id || (primaryAsset.model_key && primaryAsset.id ? `${primaryAsset.model_key}_${primaryAsset.id}` : null);
if (featId) el.dataset.featureId = featId;
el.title = primaryAsset.name || primaryAsset.code || '';
el.addEventListener('click', (e) => {
e.stopPropagation();
if (e.originalEvent) {
@ -2299,6 +2303,7 @@
: `<i class="bi ${symbology.iconClass || 'bi-pin-map'}" id="sheetIcon"></i>`;
sheetStatusBadge.className = `badge-status badge-status-${asset.status || 'active'}`;
sheetStatusBadge.textContent = asset.status_display || asset.status || "{% translate 'Actif' %}";
if (asset.element_type) {
sheetElementType.textContent = asset.element_type;
sheetElementType.classList.remove('d-none');

View file

@ -402,6 +402,68 @@ class MobileAssetDetailTests(TestCase):
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")

View file

@ -1881,6 +1881,8 @@ def mobile_assets_search(request):
qs = qs.select_related('signpanel_type', 'model', 'pole', 'trafficlight_pole', 'publiclighting_pole')
elif model_key == 'trafficlightlantern' and hasattr(model_class, 'model'):
qs = qs.select_related('model', 'pole')
elif model_key in ('naturetree', 'naturegreensurface', 'natureshrub', 'naturebench') and hasattr(model_class, 'model'):
qs = qs.select_related('model', 'location')
items = list(qs)
for item in items:
lat = None
@ -2044,6 +2046,23 @@ def mobile_assets_search(request):
elif hasattr(item, 'pole_id'):
pole_id = getattr(item, 'pole_id', None)
if model_key == 'naturetree' and not item_name:
item_name = getattr(item, 'vernacular_name', '') or getattr(item, 'scientific_name', '') or ''
management_type = getattr(item, 'management_type', None)
managing_authority = getattr(item, 'managing_authority', None)
is_remarkable = bool(getattr(item, 'is_remarkable', False))
vitality = getattr(item, 'vitality', None)
is_dead = bool(
getattr(item, 'is_dead', False)
or vitality == 'dead'
or status_val in ('dead', 'archived')
)
scientific_name = getattr(item, 'scientific_name', None) or ''
vernacular_name = getattr(item, 'vernacular_name', None) or ''
asset_sub_type = getattr(item, 'asset_sub_type', None)
model_name = getattr(getattr(item, 'model', None), f'name_{lang}', None) or getattr(getattr(item, 'model', None), 'name_fr', None) or ''
res_item = {
'id': item.id,
'feature_id': feature_id,
@ -2074,6 +2093,15 @@ def mobile_assets_search(request):
'panel_vertical_order': vertical_order,
'width': panel_width,
'height': panel_height,
'management_type': management_type,
'managing_authority': managing_authority,
'is_remarkable': is_remarkable,
'is_dead': is_dead,
'vitality': vitality,
'scientific_name': scientific_name,
'vernacular_name': vernacular_name,
'asset_sub_type': asset_sub_type,
'model': model_name,
}
results.append(res_item)
@ -2113,6 +2141,15 @@ def mobile_assets_search(request):
'panel_vertical_order': vertical_order,
'width': panel_width,
'height': panel_height,
'management_type': management_type,
'managing_authority': managing_authority,
'is_remarkable': is_remarkable,
'is_dead': is_dead,
'vitality': vitality,
'scientific_name': scientific_name,
'vernacular_name': vernacular_name,
'asset_sub_type': asset_sub_type,
'model': model_name,
}
})