From 587f4abf8ab1bca60698b463a30d4c63097a6cf8 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Fri, 9 Oct 2026 13:32:09 +0200 Subject: [PATCH 1/5] =?UTF-8?q?feat(nature):=20distinction=20arbres=20r?= =?UTF-8?q?=C3=A9gionaux,=20filtre=20gestionnaire,=20tableau=20compact=20e?= =?UTF-8?q?t=20essences=20botaniques?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- loko/assets/static/assets/asset_symbology.js | 48 ++++++++++++++++++- .../assets/nature_tree_icon_regional.svg | 20 ++++++++ .../assets/nature_tree_icon_remarkable.svg | 20 ++++++++ loko/assets/static/assets/unified_index.js | 15 +++++- .../static/assets/unified_map_layers.js | 10 ++++ .../templates/assets/unified_index.html | 20 ++++++-- loko/assets/views/core.py | 9 ++++ 7 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 loko/assets/static/assets/nature_tree_icon_regional.svg create mode 100644 loko/assets/static/assets/nature_tree_icon_remarkable.svg diff --git a/loko/assets/static/assets/asset_symbology.js b/loko/assets/static/assets/asset_symbology.js index 949baa3..b83a761 100644 --- a/loko/assets/static/assets/asset_symbology.js +++ b/loko/assets/static/assets/asset_symbology.js @@ -261,6 +261,24 @@ glyphCodepoint: 'f30b', mapImage: 'icon-naturerwiasset', }, + naturetree_regional: { + thematic: 'nature', + color: '#1d4ed8', + iconUrl: '/static/assets/nature_tree_icon_regional.svg?v=20261009a', + iconSize: 56, + iconClass: 'bi-tree-fill', + glyphCodepoint: 'f5e1', + mapImage: 'icon-nature-regional', + }, + naturetree_remarkable: { + thematic: 'nature', + color: '#d97706', + iconUrl: '/static/assets/nature_tree_icon_remarkable.svg?v=20261009a', + iconSize: 56, + iconClass: 'bi-tree-fill', + glyphCodepoint: 'f5e1', + mapImage: 'icon-nature-remarkable', + }, naturetree: { thematic: 'nature', color: '#2e7d32', @@ -578,6 +596,20 @@ } // 3. Specific asset_type override + if (assetType === 'naturetree' || (thematic === 'nature' && (assetType === 'naturetree' || !assetType))) { + const isRemarkable = item.is_remarkable === true || item.is_remarkable === 'true'; + const isRegional = item.management_type === 'regional' || + (typeof item.managing_authority === 'string' && (item.managing_authority.toLowerCase().includes('région') || item.managing_authority.toLowerCase().includes('mobilité'))); + if (isRemarkable && ASSET_TYPE_OVERRIDES.naturetree_remarkable) { + return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree_remarkable); + } + if (isRegional && ASSET_TYPE_OVERRIDES.naturetree_regional) { + return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree_regional); + } + if (ASSET_TYPE_OVERRIDES.naturetree) { + return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree); + } + } if (assetType === 'naturebench') { const isPlanter = (item.asset_sub_type === 'planter') || ['Bac à fleurs / Jardinière', 'Bloembak', 'Jardinière', 'Bac à fleurs', 'Bac à plantes'].includes(item.model) || @@ -625,12 +657,17 @@ const customStyle = options.style ? ` style="${options.style}"` : ''; + const classList = ['asset-symbology-icon-img']; + if (options.imgClass) classList.push(options.imgClass); + const imgClass = classList.join(' '); + const defaultImgStyle = 'width: 20px; height: 20px; max-width: 20px; max-height: 20px; object-fit: contain; vertical-align: middle; display: inline-block;'; + const finalStyle = options.style ? ` style="${defaultImgStyle} ${options.style}"` : ` style="${defaultImgStyle}"`; + if (symbology.iconUrl) { - const imgClass = options.imgClass || 'asset-symbology-icon-img'; const fallbackUrl = symbology.fallbackUrl || (symbology.iconUrl.includes('.svg') ? symbology.iconUrl.replace(/\.svg(\?.*)?$/, '.png$1') : ''); const fallbackClass = options.iconClass || symbology.iconClass || 'bi-sign-stop-fill'; const onerrorAttr = ` onerror="this.onerror=null; ${fallbackUrl ? `if(this.src && this.src.indexOf('.svg') !== -1){ this.src='${fallbackUrl}'; } else {` : ''} this.style.display='none'; if(this.nextElementSibling){ this.nextElementSibling.style.display=''; } ${fallbackUrl ? '}' : ''}"`; - return ``; + return ``; } const iconClass = options.iconClass || symbology.iconClass || 'bi-pin-map'; @@ -653,6 +690,8 @@ publiclighting: injected.publiclighting || THEMATIC_CONFIG.publiclighting.iconUrl, publiclightingelectricalcabinet: injected.publiclightingelectricalcabinet || ASSET_TYPE_OVERRIDES.publiclightingelectricalcabinet.iconUrl, nature: injected.nature || THEMATIC_CONFIG.nature.iconUrl, + natureregional: injected.natureregional || (ASSET_TYPE_OVERRIDES.naturetree_regional && ASSET_TYPE_OVERRIDES.naturetree_regional.iconUrl), + natureremarkable: injected.natureremarkable || (ASSET_TYPE_OVERRIDES.naturetree_remarkable && ASSET_TYPE_OVERRIDES.naturetree_remarkable.iconUrl), structures: injected.structures || THEMATIC_CONFIG.structures.iconUrl, roaddrain: injected.roaddrain || ASSET_TYPE_OVERRIDES.roaddrain.iconUrl, roadconcreteblockset: injected.roadconcreteblockset || ASSET_TYPE_OVERRIDES.roadconcreteblockset.iconUrl, @@ -773,6 +812,11 @@ ['==', ['slice', ['get', 'code'], 0, 7], 'CSE-BAC'], ], 'icon-natureplanter', ['==', ['get', 'asset_type'], 'naturebench'], 'icon-naturebench', + ['all', ['==', ['get', 'thematic'], 'nature'], ['==', ['get', 'is_remarkable'], true]], 'icon-nature-remarkable', + ['all', ['==', ['get', 'thematic'], 'nature'], ['==', ['get', 'management_type'], 'regional']], 'icon-nature-regional', + ['all', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'is_remarkable'], true]], 'icon-nature-remarkable', + ['all', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'management_type'], 'regional']], 'icon-nature-regional', + ['==', ['get', 'asset_type'], 'naturetree'], 'icon-nature', ['==', ['get', 'asset_type'], 'drain'], 'icon-roaddrain', ['==', ['get', 'asset_type'], 'roaddrain'], 'icon-roaddrain', ['==', ['get', 'category'], 'Ralentisseur'], 'icon-speedbump', diff --git a/loko/assets/static/assets/nature_tree_icon_regional.svg b/loko/assets/static/assets/nature_tree_icon_regional.svg new file mode 100644 index 0000000..547ddaf --- /dev/null +++ b/loko/assets/static/assets/nature_tree_icon_regional.svg @@ -0,0 +1,20 @@ + diff --git a/loko/assets/static/assets/nature_tree_icon_remarkable.svg b/loko/assets/static/assets/nature_tree_icon_remarkable.svg new file mode 100644 index 0000000..00258b2 --- /dev/null +++ b/loko/assets/static/assets/nature_tree_icon_remarkable.svg @@ -0,0 +1,20 @@ + diff --git a/loko/assets/static/assets/unified_index.js b/loko/assets/static/assets/unified_index.js index 4331729..c2b8ade 100644 --- a/loko/assets/static/assets/unified_index.js +++ b/loko/assets/static/assets/unified_index.js @@ -549,7 +549,7 @@ function buildColumns() { const symb = AssetSymbology.getAssetSymbology(row); const colorStyle = symb.color ? `color:${symb.color};` : ''; iconPrefix = AssetSymbology.renderIconHtml(symb, { - style: `${colorStyle} font-size: 0.95rem; vertical-align: -1px; margin-right: 5px;`, + style: `${colorStyle} width: 20px; height: 20px; vertical-align: middle; margin-right: 6px;`, imgClass: 'me-1' }); } @@ -1593,6 +1593,11 @@ function buildAssetsGeojsonPostBody(thematicCode, bbox, includeNonGeoFilters) { } } if (thematicCode === 'nature') { + if (Array.isArray(thematicFilters.management_type) && thematicFilters.management_type.length) { + payload.management_type = thematicFilters.management_type; + } else if (thematicFilters.management_type) { + payload.management_type = [thematicFilters.management_type]; + } if (thematicFilters.location_type) { payload.location_type = thematicFilters.location_type; } @@ -2028,6 +2033,9 @@ function collectClickedAssets(clickEvent) { detail_url: p.detail_url || null, feature_id: id, extra_info: p.plots_count != null ? `${p.plots_count} plot(s)` : (p.bollard_type || ''), + management_type: p.management_type || '', + managing_authority: p.managing_authority || '', + is_remarkable: p.is_remarkable || false, }); }); @@ -2729,6 +2737,11 @@ function loadTableData(features) { model: p.model || '', status: p.status || '', detail_url: p.detail_url || null, + management_type: p.management_type || '', + managing_authority: p.managing_authority || '', + is_remarkable: p.is_remarkable || false, + scientific_name: p.scientific_name || '', + vernacular_name: p.vernacular_name || '', lon: p.lon, lat: p.lat, length: Number.isFinite(length) ? length : 0, diff --git a/loko/assets/static/assets/unified_map_layers.js b/loko/assets/static/assets/unified_map_layers.js index 7fdcd79..36beb22 100644 --- a/loko/assets/static/assets/unified_map_layers.js +++ b/loko/assets/static/assets/unified_map_layers.js @@ -140,6 +140,16 @@ function loadUnifiedMapIcons(map) { .then(data => { if (!map.hasImage('icon-nature')) map.addImage('icon-nature', data); }) .catch(e => console.warn('unified_map_layers: could not load nature icon', e)); } + if (iconUrls.natureregional && !map.hasImage('icon-nature-regional')) { + loadNormalisedIcon(iconUrls.natureregional, 56) + .then(data => { if (!map.hasImage('icon-nature-regional')) map.addImage('icon-nature-regional', data); }) + .catch(e => console.warn('unified_map_layers: could not load nature regional icon', e)); + } + if (iconUrls.natureremarkable && !map.hasImage('icon-nature-remarkable')) { + loadNormalisedIcon(iconUrls.natureremarkable, 56) + .then(data => { if (!map.hasImage('icon-nature-remarkable')) map.addImage('icon-nature-remarkable', data); }) + .catch(e => console.warn('unified_map_layers: could not load nature remarkable icon', e)); + } // structures: dedicated bridge/tunnel SVG (52 px) if (iconUrls.structures && !map.hasImage('icon-structures')) { diff --git a/loko/assets/templates/assets/unified_index.html b/loko/assets/templates/assets/unified_index.html index d661480..c8a5be6 100644 --- a/loko/assets/templates/assets/unified_index.html +++ b/loko/assets/templates/assets/unified_index.html @@ -480,7 +480,7 @@ text-transform: uppercase !important; letter-spacing: 0.04em !important; border-bottom: 1.5px solid #e2e8f0 !important; - padding: 0.65rem 0.75rem !important; + padding: 0.45rem 0.65rem !important; vertical-align: middle !important; white-space: nowrap !important; } @@ -489,16 +489,28 @@ padding: 0 !important; font-size: 0.74rem !important; font-weight: 600 !important; - line-height: 1.4 !important; + line-height: 1.3 !important; } /* Rows & Cells */ .bootstrap-table.bootstrap5 #unified-table td { - padding: 0.65rem 0.75rem !important; + padding: 0.32rem 0.65rem !important; vertical-align: middle !important; - font-size: 0.83rem !important; + font-size: 0.82rem !important; color: #334155; border-bottom: 1px solid #f1f5f9 !important; + line-height: 1.3 !important; + } + + .asset-symbology-icon-img { + width: 20px !important; + height: 20px !important; + max-width: 20px !important; + max-height: 20px !important; + object-fit: contain !important; + vertical-align: middle !important; + display: inline-block !important; + margin-right: 6px !important; } .bootstrap-table.bootstrap5 #unified-table tbody tr { diff --git a/loko/assets/views/core.py b/loko/assets/views/core.py index 2afb4e5..bb12877 100644 --- a/loko/assets/views/core.py +++ b/loko/assets/views/core.py @@ -753,6 +753,15 @@ def build_conditional_filter_config(accessible_thematics, user=None): # Nature if 'nature' in accessible_codes: config['nature'] = [ + { + 'key': 'management_type', + 'label': _('Gestionnaire'), + 'type': 'multi', + 'options': [ + {'value': 'municipal', 'label': _('Commune (Watermael-Boitsfort)')}, + {'value': 'regional', 'label': _('Région (Bruxelles Mobilité)')}, + ], + }, { 'key': 'location_type', 'label': _('Type de localisation'), From f271d65be29e85d2cc9b30b01d1793a72a0e2e33 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Fri, 9 Oct 2026 13:50:49 +0200 Subject: [PATCH 2/5] fix(assets): dynamic nature management_type filter based on visible assets and scope nature endpoints by user accessible municipalities --- .../migrations/0032_merge_20261009_1348.py | 14 ++ loko/assets/tests.py | 91 +++++++++++ loko/assets/views/core.py | 117 ++++++++++++-- loko/assets/views/nature.py | 148 ++++++++++++++++-- 4 files changed, 351 insertions(+), 19 deletions(-) create mode 100644 loko/assets/migrations/0032_merge_20261009_1348.py diff --git a/loko/assets/migrations/0032_merge_20261009_1348.py b/loko/assets/migrations/0032_merge_20261009_1348.py new file mode 100644 index 0000000..93b84ef --- /dev/null +++ b/loko/assets/migrations/0032_merge_20261009_1348.py @@ -0,0 +1,14 @@ +# Generated by Django 6.0.7 on 2026-10-09 11:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0031_itsgeoasset_extra_data_itsbollardsystem_and_more'), + ('assets', '0031_naturetree_amenity_value_and_more'), + ] + + operations = [ + ] diff --git a/loko/assets/tests.py b/loko/assets/tests.py index 23470bd..0fc930b 100644 --- a/loko/assets/tests.py +++ b/loko/assets/tests.py @@ -5441,6 +5441,97 @@ class UserAccessibleMunicipalitiesTest(TestCase): # When only 1 municipality is accessible, the filter should NOT be shown self.assertIsNone(muni_filter) + def test_nature_assets_geojson_restriction_and_filter(self): + from assets.models import NatureLocation, NatureTree + from common.models import Thematic, UserThematics + from django.urls import reverse + import json + + thematic_nature, _ = Thematic.objects.get_or_create(code="nature", defaults={"name_fr": "Nature"}) + UserThematics.objects.get_or_create(user_config=self.ext_config, thematic=thematic_nature, defaults={"can_view_assets": True}) + + loc_a = NatureLocation.objects.create(code="LOC-A", name_fr="Rue A", municipality=self.muni_a) + loc_c = NatureLocation.objects.create(code="LOC-C", name_fr="Rue C", municipality=self.muni_c) + + tree_muni = NatureTree.objects.create(code="TR-A-MUNI", name_fr="Arbre Communal", location=loc_a, management_type="municipal") + tree_reg = NatureTree.objects.create(code="TR-A-REG", name_fr="Arbre Régional", location=loc_a, management_type="regional") + tree_c = NatureTree.objects.create(code="TR-C", name_fr="Arbre Inaccessible", location=loc_c, management_type="municipal") + + self.client.force_login(self.user_ext) + + # 1. Unfiltered request + resp = self.client.post( + reverse('assets:nature_assets_geojson') + '?unified=1', + data=json.dumps({}), + content_type='application/json' + ) + self.assertEqual(resp.status_code, 200) + data = resp.json() + ids = [f['id'] for f in data.get('features', [])] + self.assertIn(tree_muni.id, ids) + self.assertIn(tree_reg.id, ids) + self.assertNotIn(tree_c.id, ids) + + # 2. Filter by municipal only + resp_muni = self.client.post( + reverse('assets:nature_assets_geojson') + '?unified=1', + data=json.dumps({'management_type': ['municipal']}), + content_type='application/json' + ) + data_muni = resp_muni.json() + muni_ids = [f['id'] for f in data_muni.get('features', [])] + self.assertIn(tree_muni.id, muni_ids) + self.assertNotIn(tree_reg.id, muni_ids) + self.assertNotIn(tree_c.id, muni_ids) + + # 3. Filter by regional only + resp_reg = self.client.post( + reverse('assets:nature_assets_geojson') + '?unified=1', + data=json.dumps({'management_type': ['regional']}), + content_type='application/json' + ) + data_reg = resp_reg.json() + reg_ids = [f['id'] for f in data_reg.get('features', [])] + self.assertIn(tree_reg.id, reg_ids) + self.assertNotIn(tree_muni.id, reg_ids) + + def test_nature_conditional_filter_config_dynamic_gestionnaire(self): + from assets.views.core import build_conditional_filter_config + from assets.models import NatureLocation, NatureTree + from common.models import Thematic, UserConfig, UserContractAccess + from contracts.models import Contract + + thematic_nature, _ = Thematic.objects.get_or_create(code="nature", defaults={"name_fr": "Nature"}) + + # Single Walloon municipality user + self.muni_a.niscode = '25018' # Court-Saint-Étienne (Wallonia) + self.muni_a.name_fr = 'Court-Saint-Étienne' + self.muni_a.save() + + user_cse = User.objects.create_user(username="test_cse_user", password="pwd") + cfg_cse = UserConfig.objects.create(user=user_cse, is_intern=False, limit_assets_to_contracts=True) + ctr_cse = Contract.objects.create(contract_number="CTR-CSE", company=self.company, start_date="2026-01-01", end_date="2026-12-31") + ctr_cse.municipalities.add(self.muni_a) + UserContractAccess.objects.create(user_config=cfg_cse, contract=ctr_cse, can_view_assets=True) + + loc = NatureLocation.objects.create(code="LOC-CSE", name_fr="Rue CSE", municipality=self.muni_a) + NatureTree.objects.create(code="TR-CSE-M", location=loc, management_type="municipal") + NatureTree.objects.create(code="TR-CSE-R", location=loc, management_type="regional") + + cfg = build_conditional_filter_config([thematic_nature], user=user_cse) + nat_filters = cfg.get('nature', []) + mgmt_filter = next((f for f in nat_filters if f['key'] == 'management_type'), None) + self.assertIsNotNone(mgmt_filter) + + labels = [opt['label'] for opt in mgmt_filter['options']] + values = [opt['value'] for opt in mgmt_filter['options']] + self.assertIn('municipal', values) + self.assertIn('regional', values) + self.assertTrue(any('Court-Saint-Étienne' in lbl for lbl in labels)) + self.assertTrue(any('Région wallonne' in lbl for lbl in labels)) + self.assertFalse(any('Boitsfort' in lbl for lbl in labels)) + self.assertFalse(any('Bruxelles Mobilité' in lbl for lbl in labels)) + class NatureLocationDetailViewTest(TestCase): def setUp(self): diff --git a/loko/assets/views/core.py b/loko/assets/views/core.py index bb12877..ed73a82 100644 --- a/loko/assets/views/core.py +++ b/loko/assets/views/core.py @@ -681,7 +681,7 @@ def build_conditional_filter_config(accessible_thematics, user=None): # Roads / public lighting / sign / artworks: municipality on locations (only if more than 1 option) if len(municipality_options) > 1: - for thematic_code in ['roads', 'publiclighting', 'sign', 'artworks']: + for thematic_code in ['roads', 'publiclighting', 'sign', 'artworks', 'nature']: if thematic_code in accessible_codes: config.setdefault(thematic_code, []).append({ 'key': 'municipality', @@ -752,16 +752,114 @@ def build_conditional_filter_config(accessible_thematics, user=None): # Nature if 'nature' in accessible_codes: - config['nature'] = [ - { + from assets.models import NatureTree + + def _get_muni_region(m): + if not m: + return None + code = str(getattr(m, 'niscode', '') or '').strip() + if code.startswith('21'): + return 'BRU' + elif code[:2] in ('25', '51', '52', '53', '55', '56', '57', '61', '62', '63', '81', '82', '83', '84', '85', '91', '92', '93') or (code and code[0] in ('5', '6', '8', '9')): + return 'WAL' + elif (code and code[0] in ('1', '3', '4', '7')) or code[:2] in ('23', '24'): + return 'VLG' + name = (getattr(m, 'name_fr', '') or '').lower() + if any(b in name for b in ('bruxelles', 'watermael', 'boitsfort', 'berchem', 'ixelles', 'etterbeek', 'uccle', 'woluwe', 'schaerbeek', 'anderlecht', 'forest', 'jette', 'ganshoren', 'evere', 'saint-gilles', 'saint-josse', 'auderghem', 'molenbeek', 'koekelberg')): + return 'BRU' + return None + + regional_labels = { + 'BRU': _('Région (Bruxelles Mobilité)'), + 'WAL': _('Région (Région wallonne)'), + 'VLG': _('Région (Région flamande)'), + } + + user_cfg = getattr(user, 'config', None) + user_is_admin = getattr(user, 'is_superuser', False) or (user_cfg and (user_cfg.has_role('admin') or user_cfg.has_role('top_manager'))) + user_is_ext = (user_cfg is None) or (not user_cfg.is_intern) + user_must_limit = user_is_ext or (user_cfg and user_cfg.limit_assets_to_contracts) + + visible_trees_qs = NatureTree.objects.exclude(status='removed') + if not user_is_admin and user_must_limit: + visible_trees_qs = visible_trees_qs.filter(location__municipality__in=municipalities) + + tree_stats = list(visible_trees_qs.values('management_type', 'location__municipality_id').distinct()) + has_trees = len(tree_stats) > 0 + muni_list = list(municipalities) + management_type_options = [] + + if len(muni_list) == 1: + single_muni = muni_list[0] + reg = _get_muni_region(single_muni) or 'BRU' + reg_label = regional_labels.get(reg, _('Région')) + + has_municipal = any(t['management_type'] == 'municipal' for t in tree_stats) if has_trees else True + has_regional = any(t['management_type'] == 'regional' for t in tree_stats) if has_trees else True + + if has_municipal: + management_type_options.append({ + 'value': 'municipal', + 'label': _('Commune (%(name)s)') % {'name': single_muni.name} + }) + if has_regional: + management_type_options.append({ + 'value': 'regional', + 'label': reg_label + }) + elif len(muni_list) > 1: + muni_map = {m.id: m for m in muni_list} + + # Municipal options for each municipality with municipal trees + for m in muni_list: + has_m_trees = any( + t['management_type'] == 'municipal' and t['location__municipality_id'] == m.id + for t in tree_stats + ) if has_trees else True + if has_m_trees: + management_type_options.append({ + 'value': f'municipal_{m.id}', + 'label': _('Commune (%(name)s)') % {'name': m.name} + }) + + # Regional options grouped by region + regions_with_regional_trees = set() + for t in tree_stats: + if t['management_type'] == 'regional': + mid = t['location__municipality_id'] + m_obj = muni_map.get(mid) + if m_obj: + reg = _get_muni_region(m_obj) + if reg: + regions_with_regional_trees.add(reg) + + if not has_trees: + for m in muni_list: + reg = _get_muni_region(m) + if reg: + regions_with_regional_trees.add(reg) + + for reg_code, reg_val in [('BRU', 'regional_bruxelles'), ('WAL', 'regional_wallonie'), ('VLG', 'regional_flandre')]: + if reg_code in regions_with_regional_trees: + management_type_options.append({ + 'value': reg_val, + 'label': regional_labels[reg_code] + }) + else: + management_type_options = [ + {'value': 'municipal', 'label': _('Communal')}, + {'value': 'regional', 'label': _('Régional')}, + ] + + nature_filters = [] + if management_type_options: + nature_filters.append({ 'key': 'management_type', 'label': _('Gestionnaire'), 'type': 'multi', - 'options': [ - {'value': 'municipal', 'label': _('Commune (Watermael-Boitsfort)')}, - {'value': 'regional', 'label': _('Région (Bruxelles Mobilité)')}, - ], - }, + 'options': management_type_options, + }) + nature_filters.extend([ { 'key': 'location_type', 'label': _('Type de localisation'), @@ -777,7 +875,8 @@ def build_conditional_filter_config(accessible_thematics, user=None): 'type': 'checkbox', 'value': '1', }, - ] + ]) + config['nature'] = nature_filters # Water if 'water' in accessible_codes: diff --git a/loko/assets/views/nature.py b/loko/assets/views/nature.py index 915095b..8d2f941 100644 --- a/loko/assets/views/nature.py +++ b/loko/assets/views/nature.py @@ -72,12 +72,32 @@ def nature_index(request): category_choices = [(m.id, m.get_name()) for m in categories] selected_management_types = request.GET.getlist('management_type') - management_type_choices = [ - ('municipal', _("Communal (Commune)")), - ('regional', _("Régional (Bruxelles Mobilité)")), - ('community', _("Intercommunal / Communautaire")), - ('private', _("Privé / Conventionné")), - ] + from assets.permissions import get_user_accessible_municipalities + _munis = list(get_user_accessible_municipalities(request.user)) + if len(_munis) == 1: + _sm = _munis[0] + _code = str(getattr(_sm, 'niscode', '') or '').strip() + _is_wal = _code[:2] in ('25', '51', '52', '53', '55', '56', '57', '61', '62', '63', '81', '82', '83', '84', '85', '91', '92', '93') or (_code and _code[0] in ('5', '6', '8', '9')) or 'court-saint' in (_sm.name_fr or '').lower() + _is_vlg = (_code and _code[0] in ('1', '3', '4', '7')) or _code[:2] in ('23', '24') + if _is_wal: + _reg_lbl = _("Régional (Région wallonne)") + elif _is_vlg: + _reg_lbl = _("Régional (Région flamande)") + else: + _reg_lbl = _("Régional (Bruxelles Mobilité)") + management_type_choices = [ + ('municipal', _("Communal (%(name)s)") % {'name': _sm.name}), + ('regional', _reg_lbl), + ('community', _("Intercommunal / Communautaire")), + ('private', _("Privé / Conventionné")), + ] + else: + management_type_choices = [ + ('municipal', _("Communal (Commune)")), + ('regional', _("Régional (Région / Bruxelles Mobilité)")), + ('community', _("Intercommunal / Communautaire")), + ('private', _("Privé / Conventionné")), + ] models = list(NatureAssetModel.objects.all()) models.sort(key=lambda m: (m.get_name() or "").lower()) @@ -462,10 +482,30 @@ def nature_locations_geojson(request): filter_geom = None + from assets.permissions import get_user_accessible_municipalities + user = request.user + user_config = getattr(user, 'config', None) + is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager'))) + is_external = (user_config is None) or (not user_config.is_intern) + must_limit = is_external or (user_config and user_config.limit_assets_to_contracts) + + municipalities = ensure_list(data.get('municipality')) if request.method == "POST" else request.GET.getlist('municipality') + include_naturelocation = location_type in ('', 'naturelocation') nature_locations = NatureLocation.objects.all() if include_naturelocation else NatureLocation.objects.none() + if not is_admin and must_limit: + accessible_munis = get_user_accessible_municipalities(user) + accessible_muni_ids = list(accessible_munis.values_list('id', flat=True)) + if municipalities: + valid_munis = [mid for mid in municipalities if (int(mid) if str(mid).isdigit() else mid) in accessible_muni_ids] + nature_locations = nature_locations.filter(municipality_id__in=valid_munis) + else: + nature_locations = nature_locations.filter(municipality_id__in=accessible_muni_ids) + elif municipalities: + nature_locations = nature_locations.filter(municipality_id__in=municipalities) + if highway: nature_locations = nature_locations.filter(is_highway=highway) @@ -707,6 +747,35 @@ def nature_assets_geojson(request): NatureBench.objects.all() ) + from assets.permissions import get_user_accessible_municipalities + user = request.user + user_config = getattr(user, 'config', None) + is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager'))) + is_external = (user_config is None) or (not user_config.is_intern) + must_limit = is_external or (user_config and user_config.limit_assets_to_contracts) + + municipalities = ensure_list(data.get('municipality')) if request.method == "POST" else request.GET.getlist('municipality') + + if not is_admin and must_limit: + accessible_munis = get_user_accessible_municipalities(user) + accessible_muni_ids = list(accessible_munis.values_list('id', flat=True)) + if municipalities: + valid_munis = [mid for mid in municipalities if (int(mid) if str(mid).isdigit() else mid) in accessible_muni_ids] + naturegreensurface_qs = naturegreensurface_qs.filter(location__municipality_id__in=valid_munis) + naturetree_qs = naturetree_qs.filter(location__municipality_id__in=valid_munis) + natureshrub_qs = natureshrub_qs.filter(location__municipality_id__in=valid_munis) + naturebench_qs = naturebench_qs.filter(location__municipality_id__in=valid_munis) + else: + naturegreensurface_qs = naturegreensurface_qs.filter(location__municipality_id__in=accessible_muni_ids) + naturetree_qs = naturetree_qs.filter(location__municipality_id__in=accessible_muni_ids) + natureshrub_qs = natureshrub_qs.filter(location__municipality_id__in=accessible_muni_ids) + naturebench_qs = naturebench_qs.filter(location__municipality_id__in=accessible_muni_ids) + elif municipalities: + naturegreensurface_qs = naturegreensurface_qs.filter(location__municipality_id__in=municipalities) + naturetree_qs = naturetree_qs.filter(location__municipality_id__in=municipalities) + natureshrub_qs = natureshrub_qs.filter(location__municipality_id__in=municipalities) + naturebench_qs = naturebench_qs.filter(location__municipality_id__in=municipalities) + include_naturelocation = location_type in ('', 'naturelocation') if not include_naturelocation: @@ -715,7 +784,6 @@ def nature_assets_geojson(request): natureshrub_qs = natureshrub_qs.none() naturebench_qs = naturebench_qs.none() - if sections_ids: naturegreensurface_qs = naturegreensurface_qs.filter(location__id__in=sections_ids) naturetree_qs = naturetree_qs.filter(location__id__in=sections_ids) @@ -734,7 +802,43 @@ def nature_assets_geojson(request): naturebench_qs = naturebench_qs.filter(model__id__in=models) if management_types: - naturetree_qs = naturetree_qs.filter(management_type__in=management_types) + q_mgmt = Q() + for val in management_types: + val_str = str(val).strip() + if val_str == 'municipal': + q_mgmt |= Q(management_type='municipal') + elif val_str == 'regional': + q_mgmt |= Q(management_type='regional') + elif val_str == 'regional_bruxelles': + q_mgmt |= Q(management_type='regional') & ( + Q(location__municipality__niscode__startswith='21') | + Q(location__municipality__name_fr__icontains='Bruxelles') | + Q(location__municipality__name_fr__icontains='Watermael') | + Q(location__municipality__name_fr__icontains='Berchem') + ) + elif val_str == 'regional_wallonie': + q_mgmt |= Q(management_type='regional') & ( + Q(location__municipality__niscode__startswith=('25', '5', '6', '8', '9')) | + Q(location__municipality__name_fr__icontains='Court-Saint') + ) + elif val_str == 'regional_flandre': + q_mgmt |= Q(management_type='regional') & ( + Q(location__municipality__niscode__startswith=('1', '3', '4', '7', '23', '24')) + ) + elif val_str.startswith('municipal_'): + m_id = val_str.split('_', 1)[1] + if m_id.isdigit(): + q_mgmt |= Q(management_type='municipal', location__municipality_id=int(m_id)) + elif val_str.startswith('authority:'): + auth_name = val_str.split(':', 1)[1] + q_mgmt |= Q(managing_authority__iexact=auth_name) + else: + q_mgmt |= Q(managing_authority__icontains=val_str) | Q(management_type=val_str) + + naturetree_qs = naturetree_qs.filter(q_mgmt) + naturegreensurface_qs = naturegreensurface_qs.none() + natureshrub_qs = natureshrub_qs.none() + naturebench_qs = naturebench_qs.none() if categories: naturegreensurface_qs = naturegreensurface_qs.filter(category__id__in=categories) naturetree_qs = naturetree_qs.filter(category__id__in=categories) @@ -884,12 +988,24 @@ def nature_locations_autocomplete(request): if len(raw_query) < 2: return JsonResponse([], safe=False) + user = request.user + user_config = getattr(user, 'config', None) + is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager'))) + is_external = (user_config is None) or (not user_config.is_intern) + must_limit = is_external or (user_config and user_config.limit_assets_to_contracts) + # Normalise la requête (équivalent de unaccent+lower côté Python) qn = strip_accents_lower(raw_query) # Prépare des colonnes "recherchables" unaccentées + minuscules + qs = NatureLocation.objects.all() + if not is_admin and must_limit: + from assets.permissions import get_user_accessible_municipalities + accessible_munis = get_user_accessible_municipalities(user) + qs = qs.filter(municipality__in=accessible_munis) + qs = ( - NatureLocation.objects + qs .annotate( code_u=Lower(Unaccent('code')), name_fr_u=Lower(Unaccent('name_fr')), @@ -954,13 +1070,25 @@ def nature_assets_autocomplete(request): location_filter = {'location_id': location_id} + user = request.user + user_config = getattr(user, 'config', None) + is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager'))) + is_external = (user_config is None) or (not user_config.is_intern) + must_limit = is_external or (user_config and user_config.limit_assets_to_contracts) + def base_qs(model, extra_filter=None): ann = { 'code_u': Lower(Unaccent('code')), 'name_fr_u': Lower(Unaccent('name_fr')), 'name_nl_u': Lower(Unaccent('name_nl')), } - q = model.objects.annotate(**ann) + q = model.objects.all() + if not is_admin and must_limit: + from assets.permissions import get_user_accessible_municipalities + accessible_munis = get_user_accessible_municipalities(user) + q = q.filter(location__municipality__in=accessible_munis) + + q = q.annotate(**ann) # Si un terme de recherche est fourni, filtrer dessus if has_search_term: From 9bdd68bea2af0069a83ee6b7960da9ce52b266b2 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Fri, 9 Oct 2026 14:04:08 +0200 Subject: [PATCH 3/5] feat: add regional and remarkable nature asset symbology icons and matching logic --- loko/assets/static/assets/asset_symbology.js | 14 ++++++++++---- loko/assets/static/assets/unified_map_layers.js | 7 ++++++- loko/assets/templates/assets/unified_index.html | 8 +++++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/loko/assets/static/assets/asset_symbology.js b/loko/assets/static/assets/asset_symbology.js index b83a761..d1eb61c 100644 --- a/loko/assets/static/assets/asset_symbology.js +++ b/loko/assets/static/assets/asset_symbology.js @@ -717,6 +717,8 @@ { name: 'icon-trafficlightradarpole', codepoint: 'f4e0', color: '#e53935' }, { name: 'icon-sign', codepoint: 'f82b', color: '#0022ff' }, { name: 'icon-nature', codepoint: 'f5e1', color: '#2e7d32' }, + { name: 'icon-nature-regional', codepoint: 'f5e1', color: '#1d4ed8' }, + { name: 'icon-nature-remarkable', codepoint: 'f5e1', color: '#d97706' }, { name: 'icon-naturebench', codepoint: 'f5e1', color: '#2e7d32' }, { name: 'icon-natureplanter', codepoint: 'f3ce', color: '#d97706' }, { name: 'icon-naturerwiasset', codepoint: 'f30b', color: '#0277bd' }, @@ -812,10 +814,14 @@ ['==', ['slice', ['get', 'code'], 0, 7], 'CSE-BAC'], ], 'icon-natureplanter', ['==', ['get', 'asset_type'], 'naturebench'], 'icon-naturebench', - ['all', ['==', ['get', 'thematic'], 'nature'], ['==', ['get', 'is_remarkable'], true]], 'icon-nature-remarkable', - ['all', ['==', ['get', 'thematic'], 'nature'], ['==', ['get', 'management_type'], 'regional']], 'icon-nature-regional', - ['all', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'is_remarkable'], true]], 'icon-nature-remarkable', - ['all', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'management_type'], 'regional']], 'icon-nature-regional', + ['all', + ['any', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'thematic'], 'nature']], + ['any', ['==', ['get', 'is_remarkable'], true], ['==', ['get', 'is_remarkable'], 'true'], ['==', ['get', 'is_remarkable'], 1]] + ], 'icon-nature-remarkable', + ['all', + ['any', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'thematic'], 'nature']], + ['any', ['==', ['get', 'management_type'], 'regional'], ['==', ['get', 'management_type'], 'Régional']] + ], 'icon-nature-regional', ['==', ['get', 'asset_type'], 'naturetree'], 'icon-nature', ['==', ['get', 'asset_type'], 'drain'], 'icon-roaddrain', ['==', ['get', 'asset_type'], 'roaddrain'], 'icon-roaddrain', diff --git a/loko/assets/static/assets/unified_map_layers.js b/loko/assets/static/assets/unified_map_layers.js index 36beb22..9fd0f9b 100644 --- a/loko/assets/static/assets/unified_map_layers.js +++ b/loko/assets/static/assets/unified_map_layers.js @@ -90,7 +90,10 @@ function loadUnifiedMapIcons(map) { // ── File-based icons (full colour, non-SDF) ───────────────────────────── // UNIFIED_ICON_URLS is set in the inline - - - + + + {% endblock script %} \ No newline at end of file From e65c40239567535c8a4569389cb5c53af35f7740 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Fri, 9 Oct 2026 14:16:12 +0200 Subject: [PATCH 4/5] feat: add basemap preference persistence and layer switcher control support --- loko/assets/static/assets/unified_index.js | 22 +++++++- .../templates/assets/unified_index.html | 4 +- loko/common/locale/en/LC_MESSAGES/django.mo | Bin 7530 -> 7601 bytes loko/common/locale/en/LC_MESSAGES/django.po | 5 ++ loko/common/locale/fr/LC_MESSAGES/django.mo | Bin 9409 -> 9513 bytes loko/common/locale/fr/LC_MESSAGES/django.po | 5 ++ loko/common/locale/nl/LC_MESSAGES/django.mo | Bin 7020 -> 7095 bytes loko/common/locale/nl/LC_MESSAGES/django.po | 5 ++ .../static/common/layer-switcher-control.js | 52 ++++++++++++++---- loko/common/tests.py | 29 ++++++++++ loko/common/views.py | 9 ++- 11 files changed, 116 insertions(+), 15 deletions(-) diff --git a/loko/assets/static/assets/unified_index.js b/loko/assets/static/assets/unified_index.js index c2b8ade..2218f18 100644 --- a/loko/assets/static/assets/unified_index.js +++ b/loko/assets/static/assets/unified_index.js @@ -748,6 +748,14 @@ function applyInitialPreferences() { window._restoredMapCenter = prefs.map_center; window._restoredMapZoom = prefs.map_zoom; } + + // Basemap preference + if (prefs.basemap) { + window._restoredBasemap = prefs.basemap; + try { + localStorage.setItem('loko_active_basemap', prefs.basemap); + } catch (e) {} + } } else if (DEFAULT_THEMATIC_CODE && DEFAULT_THEMATIC_CODE in THEMATIC_CONFIG) { // First visit: pre-select the user's default thematic activeThematics = new Set([DEFAULT_THEMATIC_CODE]); @@ -775,6 +783,9 @@ function savePreferences() { ).map(cb => cb.value); const includeNoGeomCheckbox = document.getElementById('include-no-geom'); const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true; + const activeBasemap = (window.unifiedLayerSwitcher && window.unifiedLayerSwitcher.activeBasemapId) + || localStorage.getItem('loko_active_basemap') + || 'default'; const prefs = { thematics: Array.from(activeThematics), categories: Array.from(activeCategories), @@ -787,6 +798,7 @@ function savePreferences() { include_no_geom: includeNoGeom, map_center: (typeof map !== 'undefined' && map.getCenter) ? [map.getCenter().lng, map.getCenter().lat] : null, map_zoom: (typeof map !== 'undefined' && map.getZoom) ? map.getZoom() : null, + basemap: activeBasemap, }; fetch(SAVE_ASSETS_PREFS_URL, { method: 'POST', @@ -2929,7 +2941,12 @@ maploader.addEventListener('mapLoaded', () => { // Layer switcher — Urbis & Vlaanderen orthos (top-right, topmost) if (typeof LayerSwitcherControl !== 'undefined') { + const initialBasemap = window._restoredBasemap || localStorage.getItem('loko_active_basemap') || 'default'; const layerSwitcher = new LayerSwitcherControl({ + initialBasemapId: initialBasemap, + onBasemapChange: function (basemapId) { + schedulePreferencesSave(); + }, layers: [{ id: 'basemap', name: 'Fonds de plan & Orthophotos', @@ -2937,7 +2954,10 @@ maploader.addEventListener('mapLoaded', () => { beforeLayer: (map && map.getLayer('territory-mask') ? 'territory-mask' : 'locations'), basemaps: window.MAP_BACKGROUNDS || [], }], - });// Reset view — fly back to initial Brussels Region view + }); + window.unifiedLayerSwitcher = layerSwitcher; + + // Reset view — fly back to initial Brussels Region view map.addControl({ onAdd(m) { this._map = m; diff --git a/loko/assets/templates/assets/unified_index.html b/loko/assets/templates/assets/unified_index.html index e7dea0c..ebe2085 100644 --- a/loko/assets/templates/assets/unified_index.html +++ b/loko/assets/templates/assets/unified_index.html @@ -1315,7 +1315,7 @@ {% block script %} - + - + {% endblock script %} \ No newline at end of file diff --git a/loko/common/locale/en/LC_MESSAGES/django.mo b/loko/common/locale/en/LC_MESSAGES/django.mo index 7eb40fc4940dc2501d283165dfeb535f43375bb8..a64342fe6a887eb15f1adbb2773fa5c7fe93f934 100644 GIT binary patch delta 2216 zcmXxlUrd%&9Ki9T$X}88rw9V^$3%zGM3*6;qNWrS38|$Np@7N&nYOy?-5NQXQ>%Bm z)kTfwTGOdhR^HU6T~!;aG|G##w5>I3YdNb6SoZzG!x_KlbDr})&pGFJ&Us&MuQ^{E zyOBNT?Lg@z3W>gi5N!A~i3=q&HH1pc!g*MaIoO05*n!13fGKzs7vc#_#&fs`FQWZi z#d5rdc_G9?dD?V`wOGvuE$Be6VgbH^%kXsMI68rQ=mc`+hLDS8=s>ISer&@mJcOC} z8rtuPsQn}^Vf^q3l?8lo9hc%=WD!D9dI%3bS@iuY zNLS%|T!**t0W4z{Cf0~aj34$;F{9qdSFnovx6q}%jAr&TF2>2Iy_EFrXgxlJyRa0G zUeI{@D~^NdKT-e z1?U-AiTPNEKDQm0U=O+_N74S?&t(1QQ~8_*2fl_b<&Rj2zhV_;^Oh58Ks# z@F1E<96h(EkYvMobOM*q_r5_7TnKj!xr3$ z9?DZ_Cf}h0|BL*E|G1by7W?XR_2`!D#SODq*{J|H_?tK(236FfVegJX!~MxfK_NhThT+@jc)M}dWeUS=VRdv6$ktb&Fm|5pl{L4 ze@8RDi*}I7GPo7F=-J3e6KF)g4Fi~nucK$+IGXu;==0~0uEGtR@Av;7D$X#SQSG1- zS*@@c-LrNyqapOU5hVF=3H@F80e$`twBy-yV&(;Crd8;~+t9OEN1+0fQm03 zLudS9bpJ8h@fe!%FX$HihA#aCI-yjS#|)REhp`5oKs!3{AacILi|B+7qhI&a7<2E& zs4)3(4PEj(Sb`I1riG;M1XiL;`w05nTJ*gw(dV7$7WPD+_oJD0s}<^r=LiqZ zOxZ@o^=6XcDPk94@fU2zRBig=VX7vcCtT(p!ct50&P>_$wR90Hh)0Pv@$1QX`5UNN z%84ygwGf`f*7&@XivFjmt&Li@q6cUD=K;2?CYs_SDV0U-)ZE->iN}adgk>Gk5x<$7Gdh`aH8H*Cz(7|?S9i%^U*|x) aAtQJ6>x?hcl6Q9=>hA9x939TTmGD1((8Zbn delta 2175 zcmXxkTWnNC7{Kv?mTn8%(hJZQT4+I9>T*#c2t2D1;ZqOAS#WD1n%es8Rp_-C?qGezSA-%*^-AoE>UA zvn=yNW#RLIvV~el?Jo$yjsrzpEF~eNU<{VxJgmYtEW>p;9ec1Cw_^_H=Y1$}-5 z87dsXG#UPBM#2k1N_=z@<{kbn2|1RbvEHzd2^Uo^rbHw$q#uEWdG!?+EN zq&;bfSG|d~d<$0KINXSyfnGH70rdA7{SWGk{OVJAAkessV?XvD|S2!BNvoMccBT{#+Y4LZ&o^tsho#{8i-b_}2c z52BI2gl^3~H01};0Y=csM$tof0bRgk(&fZ0$oUQP&;_N@*SsIyvO#2xVHajh^*b~g z@F22{;RL#XvuH}sqrba|K4@CJKNa1&8R-2cG|~m=!qc&TeeCZ>pL+<`;-ht;D|g5~ zq{GNhqA9z8M)((c2q)C%BTu2<*C20G?qBaJ+)Q<0K4Wq4joI&urnR)w8cQWbx$*$l zHtJ2(-ds=qa!s}|QC)Il?72I;GEtSelW*qRVjeB)^W;`ZHo|i1T~rUurP4vee6z^h zpGH@_;Xl_Mu`M2^h15+{(|jM*vW&Xr(vr=urH^_g^;+uE?C*){+Lg2{v#8hSS|Qww zw`J>#n;+gl>+0BhD|&$LpJ8LYRLcr#PxgGGYB*VPeBAJVWuK29{;~F0!T&+WxaR-> diff --git a/loko/common/locale/en/LC_MESSAGES/django.po b/loko/common/locale/en/LC_MESSAGES/django.po index cdbbd5c..5c1f710 100644 --- a/loko/common/locale/en/LC_MESSAGES/django.po +++ b/loko/common/locale/en/LC_MESSAGES/django.po @@ -553,3 +553,8 @@ msgstr "Zoom in" #: common/templates/common/components/document_viewer.html msgid "Réinitialiser" msgstr "Reset" + +#: common/views.py:683 +msgid "Format de fond de plan invalide" +msgstr "Invalid basemap format" + diff --git a/loko/common/locale/fr/LC_MESSAGES/django.mo b/loko/common/locale/fr/LC_MESSAGES/django.mo index 8d040b29ad2b38b3801bb40dab477e9af05d57d1..94e12aeb87588fbb027abf3e44f922fdb31fd5b6 100644 GIT binary patch delta 2655 zcmZwHeQ1?c9Ki82b-L-ax#pZY^Xhz?y3MT@Yn9v7S#y@TwUt^no6hCSomtaAj4oIx z3&FxHBS;~oayUc(F_B;qmXxAct{?(g5kY381-&4Bzq`)`|LAak&*!<%z4x5o`JHoa zPwfYb65r zH|FAJXqVsNT>JsYg^&n^!;%A3Vj&-_$I-YOi}5{lps%qUe?}82$O>UB&cGt9iEKx| z{}!6q5j25Q$bjJj`ui(5f$_sX+>GSGIG$!>G5UqMTztM5YjGvk;rqB0uj3S4$S6~B z1Lol#oQ{W)Vup)YjJ;?g>69-A$6^-ahhlE3umUIGHq6Ag(TW^GJ350*KAgo${1#1M z3|U)=N*s*K(F&}J?7$MDtj8EX8`9h}&@*9z+xT67Ap@ zj9~`T@p%zifm-x;&1mA=(F*QBUg10!mOu36P=D9@R~}5l!K7`+Mabkr4Z4XMBjf1i z+KFTERrGs@(Iq&ENbKUq85}@2$5AxX zbI2?7a$&OJH*`knOwaQ>99^o#=m2f#W_=x9(i7;eKaUP{1Isao4Q3)W$SV;VxSDx9 zav%DGPte3Jp$Ytm4){0PK^|}1bd%9FUx4i6(1d=zHhSKHwY=Ynb$9`n;&|3?is!$b z8#m8hq-x<5dVVjWo9171CSiOskrK4@^O3)f(1cakf+lnjGx1ZjBInS4e#c?>CsyJB z$~}eg!#r-7RA@uj_9e6euSC9sp5J50D_rKnB>G45_!cm#&#TdXnz0nOp~tWb-4kb# zSNMaAzaJH&{zNF_;s^C;1vaBU>_ju~LQ8lQo%vtrY02VCuJveibC#kV*Q1*(j_#os zBi}^#)`!S;2p>(P{(kWy4_tyibS;O{n;jN#;Q)k6G?7NMw9C*<*Me4NH=5ALXvb&J zQukpDuc8ynW<9L%G-OpnNns+n$?AAuBFoVZ)}g!oWpuz@Xy$LA71)m^b`0HYpQAIq zgdX3kxCF0b5iVez$RWhh1YSe?+2{YSGwDLV*o&@J8h`9*Xj`15|M?p3A0n!WdBkG` zMe85T-Vs1S=StiBzkLZag6#(>sZU&flGQH{k}!CYBSnr-@laE#W_wEuN%*Qx1ym zH)16*m$;W$O*~Gt63-I0Cx{tDxo>Fq5YG@r3%IN!Zf^5=(?+;42Sy)c9KVv;GbF3E eJ-#}&dQEK8hE?%BJ=wX(yGLe@_`jz^)Bgj82jfft delta 2588 zcmZA1Z)nw37{Kwzxy}Dt)7)~-U2Wbum#(I1x^m80Zt7CArkR<4Qp=TpruibLHwvdw zltI0a5GgD)F-eC=q~!~XN_=AxgauMW34ycT=oC>#)R2i9XDuEz_i887g=W6#l&HBMQ70G_B?u2xh&TN5@?amN6&OKTJ3w$iB4fPevKy5jn>!zE+n3cWTq1f z(I32kX7)OozyWl^6X*aJ(W1MIp84NM-Ns;U^7~Ts^9k69wKxx7!A1BT`qY$jj!B>Y zCOYg^Y(t;hSJ9&BKv!}aP2^W}>${M@iWppyd2EZ(gqCAJY(saX9UbRGw5C47TKoh} z;3ke>{z$O@o^1`f1GBO=qgA>AdBuJ%Bp;{I$M)Oo=Qof|h$6D6!b#|3xCX6>-RSR* zVFJHGUU8L6%9Z!$Q-3fT&A1-jx;f~I-$b91L+BYFM~m`Hbl|IKapf}zi^{(DyM@qq(&AcY>^qlx^1ZfPf0Vi&q2<=kvS&FH|Z&@JA93EYD& z>@d2spCP9fU!b-1Gn&ZtF{#V|IUJZ(J`$a<49$EZx&u?u#O9(!wh~?GM)a}WgA4FA ztia329FfmCnLq_P&Ryt2>e2f)`Ty$~?Z;l;K-lgl9MI+fE7HA8Qg!sHopgJa>q4S7 z^EUEud$#{Dwh(KGx!KR=qlwiMRYYnNmq!TZPgnU;e4dy=xK+;(;|W*&|K?-oBgLu2 zJ;ZFnwuP8PH28+LmS{@%G8b#6ny~)TpSS}yw|EKRS=t^VJm;qgE8NFs72(!V^s$w& zemn{v)t=Va)4@5!vh4fm_yqA7F^-t*{Lj&`^&>VC(})KM+uek9;n_74+lXAkRzoaL z_x`&)neFDFg}Iq{oUj<5BIXf2ZHS%B`ZHDC%KItAgMLAyhSHD8Vzhew)3JEg5w-;J zDB*vGkL3p9UcxquSWnC(Y*gDLzu#k(dSQM3r=Gb zW-gk2uN7Bto=5sChTSwS=fWOr$Ai&R=s?rxK>y$ZOxJ|aj5)j-`>`HJ@DiLr6MZUP z-;YZ;e;XU|8(fNItYrL+sD_M3`x3zqONv~(3LBz=c!q{A3GfhVyFU&BrKI=17lXrlG?)ZYQSN#BkNXof@RgL}}Dzl#GEtZlqs z$HJOe0UdZFaw{b0(oCX>me93+3*9p((9)kl_u9E4jV>A)&YW=pE%64-;wbv!KIGAG z44vUgG~v_e^X2Hdc)x+HOel+1Y6Y^LLIK@tgXkV9K19O-oVFA*QOKQl$(%S zVGLXG8T5zhC|c^T(XZt!I)DrB^Nr|0UD5Su;t87Q3z+u%f0>3ed=u#-97kXL3N2w7 z9pI06zm}U;q6N7WI?<(Di&@-)R_-x$m%oH2^eH;<4`}6n#6^rB$}}ut1*7;iw4$5x zTAYWy@p?Zx@L+T^`qgZY=ey8NIT0ygsIwU_K_4g}Ur1Pk zSsX@k43DEFd*LuLydd<>epc6Ta(BG3 z65U{a1wDkHo5ziW8_?f}q_i;AyJ~>e+_8>Efw17K34eCxj*T?#BG%4bz&nYXO0T9m za<|axCR}pT2{#ZPu7LY-d+C={bNjut{3W`T@MtGSh;4+M#~pkJv1%$;y=`84*Tnc} mc64X<;oZr2>7C4isV6e)YHIFJCLfp_OOi((o%*2ZRONr2IjI8x delta 2005 zcmX}sUuczO9Ki8kZtma7=4QI(DLt#Lc0A2crZd-En>y2UHg#EMn&>A`q0qJ6DuAC~p*`0EA315`PvWOgJaTfOC9Ndjh;m5cDk76mFLEE3h7jO(~a1woP zZh6E~%p(2eV<(mQT-b|QJd$_;eP9e7=srG;6PU)Tiim~SgjKi+XJbD)(f!HwL--=U zPvJZq#YX%Aix@v1QfcDCKj;A2R75qd!Y8o@9jFf(I}V{MJBB1H&LW=}=BpjApvjv+ z7f?heT7!!*hh}UCrWrp5snp|8a^o_ZkuiJ)|H3+~XIU>{7n*?q%-|R31iwXBcpYs& zj!v+c^s%-mL$_iMK94!f+fg@_O5BUJ_(@^`*|s=~&)`M0-H&KWZ=L){8mp$HjOYozUe<@=uoHdoGx=n@FE=3;D!dzC2V9(T@H_CsfWxyG7OLfDOoE zqZM7idMw5daWn40ERLcRy@QTdTt)uvD8(|I;R3Yb1~kF@w$MgS|+T7(`ci5}ojA zwEg+SYsvj_bV3i%OwC}H9G^&`XRH=IBW-B^`E69(tApr_KSiF6)966MXh+|ndwv65 z;h$&+lj!qx^y*eM;0kO?et(2Ci2-!NhtZXP_4s!_ZX^wUNB3p|J(LT{7u9IR2Hb-F zK^;U>J%o0A5p8!J9bi0h0-f+Y4ulhILo?BVF031CeE&b7;)8=|>I&%VcV?6@!T9kL6;I=DI1}%A1OGq= zo=hxZR=zEj==C&u80!<8(dX8n6Wolp>%(r`gPws)=!Ad9ybT^u;f;t#n86D2&zO-z zQ@9p+31SDaiD)6}2#YJTtS9WNmvFE*2v`3a;WnB2SBdwD9>NK(_Wyq!m44zm!s6ku zG!iSOY7uWGI&0=QH8~I4Hp2HiLs(vZ%>Uob)Vh*iTX6>A+q8&ykC=MNSAhr1)4z=H zHM6u49z6dSb``RvEgc=yrpsC?D+v?cN%(g&T{cj8hgdUx0k;$Fg|AC this._applyInitialBasemap()); + map.once('load', () => this._applyInitialBasemap()); } + // Re-apply if the style reloads dynamically + map.on('style.load', () => { + if (this.activeBasemapId && this.activeBasemapId !== 'default') { + this._applyInitialBasemap(); + } + }); + document.addEventListener('click', this._handleOutsideClick); return this.container; @@ -660,8 +674,11 @@ } selectBasemap(id) { + const previousId = this.activeBasemapId; this.activeBasemapId = id; - localStorage.setItem('loko_active_basemap', id); + try { + localStorage.setItem('loko_active_basemap', id); + } catch (e) {} // Update UI list items if (this.popover) { @@ -682,9 +699,20 @@ this._applyRasterBasemap(bgConfig); } } + + if (this.onBasemapChange && previousId !== id) { + try { + this.onBasemapChange(id); + } catch (err) { + console.error('[LayerSwitcherControl] onBasemapChange error:', err); + } + } } _applyInitialBasemap() { + if (!this.basemaps || this.basemaps.length === 0) { + this.basemaps = this._resolveBasemaps(); + } if (this.activeBasemapId && this.activeBasemapId !== 'default') { const bgConfig = this.basemaps.find(b => b.id === this.activeBasemapId); if (bgConfig) { @@ -693,13 +721,15 @@ return; } } - // Check if any basemap has is_default - const defaultBg = this.basemaps.find(b => b.is_default); - if (defaultBg) { - this.selectBasemap(defaultBg.id); - } else { - this._updateButtonState(); + // Check if any basemap has is_default only if activeBasemapId is not set + if (!this.activeBasemapId) { + const defaultBg = this.basemaps.find(b => b.is_default); + if (defaultBg) { + this.selectBasemap(defaultBg.id); + return; + } } + this._updateButtonState(); } _removeCurrentRasterLayer() { diff --git a/loko/common/tests.py b/loko/common/tests.py index f0ab794..48fbb8e 100644 --- a/loko/common/tests.py +++ b/loko/common/tests.py @@ -324,6 +324,35 @@ class AssetsIndexPreferencesTests(TestCase): self.assertEqual(response.status_code, 400) self.assertIn("error", response.json()) + def test_save_assets_index_preferences_with_basemap(self): + self.client.force_login(self.superuser) + save_url = reverse("common:save_assets_index_preferences") + payload = { + "thematics": ["sign"], + "basemap": "urbis_ortho", + } + response = self.client.post(save_url, json.dumps(payload), content_type="application/json") + self.assertEqual(response.status_code, 200) + self.assertTrue(response.json().get("success")) + + from common.models import UserConfig + config = UserConfig.objects.get(user=self.superuser) + self.assertEqual( + config.assets_index_preferences.get("basemap"), + "urbis_ortho" + ) + + def test_save_assets_index_preferences_invalid_basemap_returns_400(self): + self.client.force_login(self.superuser) + save_url = reverse("common:save_assets_index_preferences") + payload = { + "basemap": 9999, + } + response = self.client.post(save_url, json.dumps(payload), content_type="application/json") + self.assertEqual(response.status_code, 400) + self.assertIn("error", response.json()) + + class UserConfigCopyTests(TestCase): diff --git a/loko/common/views.py b/loko/common/views.py index 1683737..c5c7cec 100644 --- a/loko/common/views.py +++ b/loko/common/views.py @@ -663,6 +663,7 @@ def save_assets_index_preferences(request): include_no_geom = data.get('include_no_geom', True) map_center = data.get('map_center') map_zoom = data.get('map_zoom') + basemap = data.get('basemap') if not isinstance(thematics, list): return JsonResponse({'error': _('Format de thématiques invalide')}, status=400) @@ -680,13 +681,18 @@ def save_assets_index_preferences(request): return JsonResponse({'error': _('Format de filtres statut invalide')}, status=400) if not isinstance(validation_status_filters, list): return JsonResponse({'error': _('Format de filtres statut de validation invalide')}, status=400) + if basemap is not None and not isinstance(basemap, str): + return JsonResponse({'error': _('Format de fond de plan invalide')}, status=400) try: user_config, _created = UserConfig.objects.get_or_create(user=request.user) existing_prefs = user_config.assets_index_preferences or {} - existing_visible_columns = existing_prefs.get('visible_columns', []) if isinstance(existing_prefs, dict) else [] + if not isinstance(existing_prefs, dict): + existing_prefs = {} + existing_visible_columns = existing_prefs.get('visible_columns', []) if not isinstance(existing_visible_columns, list): existing_visible_columns = [] + existing_basemap = existing_prefs.get('basemap', 'default') user_config.assets_index_preferences = { 'thematics': thematics, @@ -700,6 +706,7 @@ def save_assets_index_preferences(request): 'include_no_geom': include_no_geom, 'map_center': map_center, 'map_zoom': map_zoom, + 'basemap': basemap if basemap is not None else existing_basemap, } user_config.save(update_fields=['assets_index_preferences']) return JsonResponse({'success': True}) From 095161872d93e9703a604efe929340c820b40cae Mon Sep 17 00:00:00 2001 From: kdeterme Date: Fri, 9 Oct 2026 14:31:17 +0200 Subject: [PATCH 5/5] feat: add dead tree symbology, icon, and map layer support for nature assets --- loko/assets/static/assets/asset_symbology.js | 38 +++++++++++++++---- .../static/assets/nature_tree_icon_dead.svg | 20 ++++++++++ .../assets/nature_tree_icon_regional.svg | 10 ++--- .../assets/nature_tree_icon_remarkable.svg | 4 +- loko/assets/static/assets/unified_index.js | 8 ++++ .../static/assets/unified_map_layers.js | 14 +++++-- .../templates/assets/unified_index.html | 13 ++++--- loko/assets/views/nature.py | 5 ++- 8 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 loko/assets/static/assets/nature_tree_icon_dead.svg diff --git a/loko/assets/static/assets/asset_symbology.js b/loko/assets/static/assets/asset_symbology.js index d1eb61c..0ab91b9 100644 --- a/loko/assets/static/assets/asset_symbology.js +++ b/loko/assets/static/assets/asset_symbology.js @@ -264,21 +264,30 @@ naturetree_regional: { thematic: 'nature', color: '#1d4ed8', - iconUrl: '/static/assets/nature_tree_icon_regional.svg?v=20261009a', + iconUrl: '/static/assets/nature_tree_icon_regional.svg?v=20261009_round', iconSize: 56, - iconClass: 'bi-tree-fill', + iconClass: 'bi-tree', glyphCodepoint: 'f5e1', mapImage: 'icon-nature-regional', }, naturetree_remarkable: { thematic: 'nature', color: '#d97706', - iconUrl: '/static/assets/nature_tree_icon_remarkable.svg?v=20261009a', + iconUrl: '/static/assets/nature_tree_icon_remarkable.svg?v=20261009_round', iconSize: 56, - iconClass: 'bi-tree-fill', + iconClass: 'bi-tree', glyphCodepoint: 'f5e1', mapImage: 'icon-nature-remarkable', }, + naturetree_dead: { + thematic: 'nature', + color: '#6b7280', + iconUrl: '/static/assets/nature_tree_icon_dead.svg?v=20261009_round', + iconSize: 56, + iconClass: 'bi-tree', + glyphCodepoint: 'f5e1', + mapImage: 'icon-nature-dead', + }, naturetree: { thematic: 'nature', color: '#2e7d32', @@ -597,7 +606,12 @@ // 3. Specific asset_type override if (assetType === 'naturetree' || (thematic === 'nature' && (assetType === 'naturetree' || !assetType))) { - const isRemarkable = item.is_remarkable === true || item.is_remarkable === 'true'; + const isDead = item.is_dead === true || item.is_dead === 'true' || item.is_dead === 1 || + item.vitality === 'dead' || item.status === 'dead' || item.status === 'archived'; + if (isDead && ASSET_TYPE_OVERRIDES.naturetree_dead) { + return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree_dead); + } + 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é'))); if (isRemarkable && ASSET_TYPE_OVERRIDES.naturetree_remarkable) { @@ -692,6 +706,7 @@ nature: injected.nature || THEMATIC_CONFIG.nature.iconUrl, natureregional: injected.natureregional || (ASSET_TYPE_OVERRIDES.naturetree_regional && ASSET_TYPE_OVERRIDES.naturetree_regional.iconUrl), natureremarkable: injected.natureremarkable || (ASSET_TYPE_OVERRIDES.naturetree_remarkable && ASSET_TYPE_OVERRIDES.naturetree_remarkable.iconUrl), + naturedead: injected.naturedead || (ASSET_TYPE_OVERRIDES.naturetree_dead && ASSET_TYPE_OVERRIDES.naturetree_dead.iconUrl), structures: injected.structures || THEMATIC_CONFIG.structures.iconUrl, roaddrain: injected.roaddrain || ASSET_TYPE_OVERRIDES.roaddrain.iconUrl, roadconcreteblockset: injected.roadconcreteblockset || ASSET_TYPE_OVERRIDES.roadconcreteblockset.iconUrl, @@ -717,8 +732,6 @@ { name: 'icon-trafficlightradarpole', codepoint: 'f4e0', color: '#e53935' }, { name: 'icon-sign', codepoint: 'f82b', color: '#0022ff' }, { name: 'icon-nature', codepoint: 'f5e1', color: '#2e7d32' }, - { name: 'icon-nature-regional', codepoint: 'f5e1', color: '#1d4ed8' }, - { name: 'icon-nature-remarkable', codepoint: 'f5e1', color: '#d97706' }, { name: 'icon-naturebench', codepoint: 'f5e1', color: '#2e7d32' }, { name: 'icon-natureplanter', codepoint: 'f3ce', color: '#d97706' }, { name: 'icon-naturerwiasset', codepoint: 'f30b', color: '#0277bd' }, @@ -814,6 +827,17 @@ ['==', ['slice', ['get', 'code'], 0, 7], 'CSE-BAC'], ], 'icon-natureplanter', ['==', ['get', 'asset_type'], 'naturebench'], 'icon-naturebench', + ['all', + ['any', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'thematic'], 'nature']], + ['any', + ['==', ['get', 'is_dead'], true], + ['==', ['get', 'is_dead'], 'true'], + ['==', ['get', 'is_dead'], 1], + ['==', ['get', 'status'], 'dead'], + ['==', ['get', 'status'], 'archived'], + ['==', ['get', 'vitality'], 'dead'] + ] + ], 'icon-nature-dead', ['all', ['any', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'thematic'], 'nature']], ['any', ['==', ['get', 'is_remarkable'], true], ['==', ['get', 'is_remarkable'], 'true'], ['==', ['get', 'is_remarkable'], 1]] diff --git a/loko/assets/static/assets/nature_tree_icon_dead.svg b/loko/assets/static/assets/nature_tree_icon_dead.svg new file mode 100644 index 0000000..cac81e3 --- /dev/null +++ b/loko/assets/static/assets/nature_tree_icon_dead.svg @@ -0,0 +1,20 @@ + diff --git a/loko/assets/static/assets/nature_tree_icon_regional.svg b/loko/assets/static/assets/nature_tree_icon_regional.svg index 547ddaf..a07855d 100644 --- a/loko/assets/static/assets/nature_tree_icon_regional.svg +++ b/loko/assets/static/assets/nature_tree_icon_regional.svg @@ -4,9 +4,9 @@ - - - + + + @@ -15,6 +15,6 @@ - - + + diff --git a/loko/assets/static/assets/nature_tree_icon_remarkable.svg b/loko/assets/static/assets/nature_tree_icon_remarkable.svg index 00258b2..2453db7 100644 --- a/loko/assets/static/assets/nature_tree_icon_remarkable.svg +++ b/loko/assets/static/assets/nature_tree_icon_remarkable.svg @@ -4,7 +4,7 @@ - + @@ -15,6 +15,6 @@ - + diff --git a/loko/assets/static/assets/unified_index.js b/loko/assets/static/assets/unified_index.js index 2218f18..465f66a 100644 --- a/loko/assets/static/assets/unified_index.js +++ b/loko/assets/static/assets/unified_index.js @@ -824,6 +824,12 @@ $(document).ready(function () { $('#unified-table').bootstrapTable({ columns: buildColumns(), + rowStyle: function (row) { + if (row && (row.is_dead || row.vitality === 'dead' || row.status === 'dead' || row.status === 'archived')) { + return { classes: 'text-muted opacity-75' }; + } + return {}; + }, height: calcHeight(), search: false, pagination: true, @@ -2754,6 +2760,8 @@ function loadTableData(features) { is_remarkable: p.is_remarkable || false, scientific_name: p.scientific_name || '', vernacular_name: p.vernacular_name || '', + is_dead: p.is_dead || false, + vitality: p.vitality || '', lon: p.lon, lat: p.lat, length: Number.isFinite(length) ? length : 0, diff --git a/loko/assets/static/assets/unified_map_layers.js b/loko/assets/static/assets/unified_map_layers.js index 9fd0f9b..1fab787 100644 --- a/loko/assets/static/assets/unified_map_layers.js +++ b/loko/assets/static/assets/unified_map_layers.js @@ -153,6 +153,11 @@ function loadUnifiedMapIcons(map) { .then(data => { if (!map.hasImage('icon-nature-remarkable')) map.addImage('icon-nature-remarkable', data); }) .catch(e => console.warn('unified_map_layers: could not load nature remarkable icon', e)); } + if (iconUrls.naturedead && !map.hasImage('icon-nature-dead')) { + loadNormalisedIcon(iconUrls.naturedead, 56) + .then(data => { if (!map.hasImage('icon-nature-dead')) map.addImage('icon-nature-dead', data); }) + .catch(e => console.warn('unified_map_layers: could not load nature dead icon', e)); + } // structures: dedicated bridge/tunnel SVG (52 px) if (iconUrls.structures && !map.hasImage('icon-structures')) { @@ -224,7 +229,12 @@ function loadUnifiedMapIcons(map) { // ── Bootstrap Icons glyph SDF icons (recoloured by icon-color) ────────── // codepoints and colors derived from AssetSymbology (single source of truth) - const FILE_ICON_NAMES = new Set(Object.keys(iconUrls).map(k => 'icon-' + k)); + const FILE_ICON_NAMES = new Set([ + ...Object.keys(iconUrls).map(k => 'icon-' + k), + 'icon-nature-regional', + 'icon-nature-remarkable', + 'icon-nature-dead', + ]); const GLYPH_ICONS = (typeof AssetSymbology !== 'undefined') ? AssetSymbology.getGlyphIcons() @@ -234,8 +244,6 @@ function loadUnifiedMapIcons(map) { { name: 'icon-trafficlightsequipment', codepoint: 'f1d5', color: '#331110' }, { name: 'icon-sign', codepoint: 'f82b', color: '#0022ff' }, { name: 'icon-nature', codepoint: 'f5e1', color: '#2e7d32' }, - { name: 'icon-nature-regional', codepoint: 'f5e1', color: '#1d4ed8' }, - { name: 'icon-nature-remarkable', codepoint: 'f5e1', color: '#d97706' }, { name: 'icon-structures', codepoint: 'f876', color: '#6d4c41' }, { name: 'icon-its', codepoint: 'f1d5', color: '#0288d1' }, { name: 'icon-clean', codepoint: 'f78a', color: '#5e5e5e' }, diff --git a/loko/assets/templates/assets/unified_index.html b/loko/assets/templates/assets/unified_index.html index ebe2085..897c2a4 100644 --- a/loko/assets/templates/assets/unified_index.html +++ b/loko/assets/templates/assets/unified_index.html @@ -1353,9 +1353,10 @@ trafficlightelectricalcabinet: "{% static 'assets/electrical-panel.png' %}", publiclighting: "{% static 'assets/public_lamp_post_icon.svg' %}?v=20260521c", publiclightingelectricalcabinet: "{% static 'assets/electrical-panel.png' %}", - nature: "{% static 'assets/nature_tree_icon.svg' %}?v=20260521a", - natureregional: "{% static 'assets/nature_tree_icon_regional.svg' %}?v=20261009a", - natureremarkable: "{% static 'assets/nature_tree_icon_remarkable.svg' %}?v=20261009a", + nature: "{% static 'assets/nature_tree_icon.svg' %}?v=20261009_round", + natureregional: "{% static 'assets/nature_tree_icon_regional.svg' %}?v=20261009_round", + natureremarkable: "{% static 'assets/nature_tree_icon_remarkable.svg' %}?v=20261009_round", + naturedead: "{% static 'assets/nature_tree_icon_dead.svg' %}?v=20261009_round", structures: "{% static 'assets/structures_bridge_tunnel_icon.svg' %}?v=20260522b", roaddrain: "{% static 'assets/roaddrain_grille_icon.svg' %}?v=20260521a", speedbump: "{% static 'assets/road_speedbump_icon.svg' %}?v=20260920", @@ -1372,7 +1373,7 @@ - - - + + + {% endblock script %} \ No newline at end of file diff --git a/loko/assets/views/nature.py b/loko/assets/views/nature.py index 8d2f941..e8d1c4a 100644 --- a/loko/assets/views/nature.py +++ b/loko/assets/views/nature.py @@ -721,6 +721,8 @@ def nature_assets_geojson(request): "is_remarkable": obj.get('is_remarkable', False), "scientific_name": obj.get('scientific_name') or '', "vernacular_name": obj.get('vernacular_name') or '', + "is_dead": bool(obj.get('is_dead') or obj.get('vitality') == 'dead' or obj.get('status') in ('dead', 'archived')), + "vitality": obj.get('vitality') or '', } } @@ -916,7 +918,8 @@ def nature_assets_geojson(request): 'category_name', 'model_name', 'color', 'geojson', 'category_id', 'model_id', 'location_lon', 'location_lat', 'managing_authority', 'management_type', 'is_remarkable', - 'scientific_name', 'vernacular_name' + 'scientific_name', 'vernacular_name', + 'is_dead', 'vitality' ) .order_by('code') )[:MAX_ASSET_FEATURES + 1]