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:
parent
45bc88d1a3
commit
fbd1bc452f
4 changed files with 108 additions and 3 deletions
|
|
@ -291,7 +291,7 @@
|
||||||
naturetree: {
|
naturetree: {
|
||||||
thematic: 'nature',
|
thematic: 'nature',
|
||||||
color: '#2e7d32',
|
color: '#2e7d32',
|
||||||
iconUrl: '/static/assets/nature_tree_icon.svg?v=20260521a',
|
iconUrl: '/static/assets/nature_tree_icon.svg?v=20261009_round',
|
||||||
iconSize: 56,
|
iconSize: 56,
|
||||||
iconClass: 'bi-tree-fill',
|
iconClass: 'bi-tree-fill',
|
||||||
glyphCodepoint: 'f5e1',
|
glyphCodepoint: 'f5e1',
|
||||||
|
|
@ -613,7 +613,8 @@
|
||||||
}
|
}
|
||||||
const isRemarkable = item.is_remarkable === true || item.is_remarkable === 'true' || item.is_remarkable === 1;
|
const isRemarkable = item.is_remarkable === true || item.is_remarkable === 'true' || item.is_remarkable === 1;
|
||||||
const isRegional = item.management_type === 'regional' ||
|
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) {
|
if (isRemarkable && ASSET_TYPE_OVERRIDES.naturetree_remarkable) {
|
||||||
return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree_remarkable);
|
return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree_remarkable);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1135,7 +1135,7 @@
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block extra_scripts %}
|
{% 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>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -2048,6 +2048,10 @@
|
||||||
el.style.color = color;
|
el.style.color = color;
|
||||||
el.innerHTML = iconHtml;
|
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) => {
|
el.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (e.originalEvent) {
|
if (e.originalEvent) {
|
||||||
|
|
@ -2299,6 +2303,7 @@
|
||||||
: `<i class="bi ${symbology.iconClass || 'bi-pin-map'}" id="sheetIcon"></i>`;
|
: `<i class="bi ${symbology.iconClass || 'bi-pin-map'}" id="sheetIcon"></i>`;
|
||||||
|
|
||||||
sheetStatusBadge.className = `badge-status badge-status-${asset.status || 'active'}`;
|
sheetStatusBadge.className = `badge-status badge-status-${asset.status || 'active'}`;
|
||||||
|
sheetStatusBadge.textContent = asset.status_display || asset.status || "{% translate 'Actif' %}";
|
||||||
if (asset.element_type) {
|
if (asset.element_type) {
|
||||||
sheetElementType.textContent = asset.element_type;
|
sheetElementType.textContent = asset.element_type;
|
||||||
sheetElementType.classList.remove('d-none');
|
sheetElementType.classList.remove('d-none');
|
||||||
|
|
|
||||||
|
|
@ -402,6 +402,68 @@ class MobileAssetDetailTests(TestCase):
|
||||||
self.assertEqual(spot_feature["properties"]["asset_type"], "parkingspot")
|
self.assertEqual(spot_feature["properties"]["asset_type"], "parkingspot")
|
||||||
self.assertEqual(spot_feature["properties"]["spot_type"], "bicycle")
|
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):
|
def test_generic_asset_uses_mobile_generic_template(self):
|
||||||
"""An asset model without specific template (NatureShrub) uses detail_generic_mobile.html."""
|
"""An asset model without specific template (NatureShrub) uses detail_generic_mobile.html."""
|
||||||
user = User.objects.create_user(username="shrub_viewer", password="password")
|
user = User.objects.create_user(username="shrub_viewer", password="password")
|
||||||
|
|
|
||||||
|
|
@ -1881,6 +1881,8 @@ def mobile_assets_search(request):
|
||||||
qs = qs.select_related('signpanel_type', 'model', 'pole', 'trafficlight_pole', 'publiclighting_pole')
|
qs = qs.select_related('signpanel_type', 'model', 'pole', 'trafficlight_pole', 'publiclighting_pole')
|
||||||
elif model_key == 'trafficlightlantern' and hasattr(model_class, 'model'):
|
elif model_key == 'trafficlightlantern' and hasattr(model_class, 'model'):
|
||||||
qs = qs.select_related('model', 'pole')
|
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)
|
items = list(qs)
|
||||||
for item in items:
|
for item in items:
|
||||||
lat = None
|
lat = None
|
||||||
|
|
@ -2044,6 +2046,23 @@ def mobile_assets_search(request):
|
||||||
elif hasattr(item, 'pole_id'):
|
elif hasattr(item, 'pole_id'):
|
||||||
pole_id = getattr(item, 'pole_id', None)
|
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 = {
|
res_item = {
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
'feature_id': feature_id,
|
'feature_id': feature_id,
|
||||||
|
|
@ -2074,6 +2093,15 @@ def mobile_assets_search(request):
|
||||||
'panel_vertical_order': vertical_order,
|
'panel_vertical_order': vertical_order,
|
||||||
'width': panel_width,
|
'width': panel_width,
|
||||||
'height': panel_height,
|
'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)
|
results.append(res_item)
|
||||||
|
|
||||||
|
|
@ -2113,6 +2141,15 @@ def mobile_assets_search(request):
|
||||||
'panel_vertical_order': vertical_order,
|
'panel_vertical_order': vertical_order,
|
||||||
'width': panel_width,
|
'width': panel_width,
|
||||||
'height': panel_height,
|
'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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue