feat: add signpanel search support, symbology, and coordinate fallback in mobile asset views
This commit is contained in:
parent
bd8897499b
commit
57976c71eb
6 changed files with 231 additions and 29 deletions
|
|
@ -513,17 +513,34 @@
|
||||||
return Object.assign({}, PARKING_SPOT_OVERRIDES[spotType]);
|
return Object.assign({}, PARKING_SPOT_OVERRIDES[spotType]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Specific asset_type override
|
// 2. Road sign panel specific override (dynamic road sign face SVG/PNG)
|
||||||
|
if (assetType === 'signpanel' || (thematic === 'sign' && (item.panel_type || item.svg_url || item.icon_url))) {
|
||||||
|
const panelCode = (item.panel_type || '').trim().toUpperCase();
|
||||||
|
const iconUrl = item.icon_url || item.svg_url || (panelCode ? `/static/assets/road_signs/2025/${panelCode}.svg` : null);
|
||||||
|
const fallbackUrl = panelCode ? `/static/assets/road_signs/2025/${panelCode}.png` : null;
|
||||||
|
return {
|
||||||
|
thematic: 'sign',
|
||||||
|
color: '#0022ff',
|
||||||
|
iconUrl: iconUrl,
|
||||||
|
fallbackUrl: fallbackUrl,
|
||||||
|
iconClass: 'bi-sign-stop-fill',
|
||||||
|
glyphCodepoint: 'f82b',
|
||||||
|
mapImage: panelCode ? `road-sign-${panelCode.toLowerCase()}` : 'icon-sign',
|
||||||
|
iconSize: 40,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Specific asset_type override
|
||||||
if (assetType && ASSET_TYPE_OVERRIDES[assetType]) {
|
if (assetType && ASSET_TYPE_OVERRIDES[assetType]) {
|
||||||
return Object.assign({}, ASSET_TYPE_OVERRIDES[assetType]);
|
return Object.assign({}, ASSET_TYPE_OVERRIDES[assetType]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fallback to thematic configuration
|
// 4. Fallback to thematic configuration
|
||||||
if (thematic && THEMATIC_CONFIG[thematic]) {
|
if (thematic && THEMATIC_CONFIG[thematic]) {
|
||||||
return Object.assign({}, THEMATIC_CONFIG[thematic]);
|
return Object.assign({}, THEMATIC_CONFIG[thematic]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Ultimate fallback
|
// 5. Ultimate fallback
|
||||||
return {
|
return {
|
||||||
color: THEMATIC_COLORS[thematic] || '#2f3e8b',
|
color: THEMATIC_COLORS[thematic] || '#2f3e8b',
|
||||||
iconUrl: null,
|
iconUrl: null,
|
||||||
|
|
@ -555,7 +572,10 @@
|
||||||
|
|
||||||
if (symbology.iconUrl) {
|
if (symbology.iconUrl) {
|
||||||
const imgClass = options.imgClass || 'asset-symbology-icon-img';
|
const imgClass = options.imgClass || 'asset-symbology-icon-img';
|
||||||
return `<img src="${symbology.iconUrl}" alt="" class="${imgClass}"${customStyle}>`;
|
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 `<img src="${symbology.iconUrl}" alt="" class="${imgClass}"${customStyle}${onerrorAttr}><i class="bi ${fallbackClass}" style="display:none;${options.style || ''}"></i>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconClass = options.iconClass || symbology.iconClass || 'bi-pin-map';
|
const iconClass = options.iconClass || symbology.iconClass || 'bi-pin-map';
|
||||||
|
|
|
||||||
|
|
@ -420,6 +420,7 @@ ELEMENT_TYPE_LABELS = {
|
||||||
'street': _('Rue / Voie'),
|
'street': _('Rue / Voie'),
|
||||||
'roadway': _('Chaussée'),
|
'roadway': _('Chaussée'),
|
||||||
'sidewalk': _('Trottoir'),
|
'sidewalk': _('Trottoir'),
|
||||||
|
'roadsidewalk': _('Trottoir'),
|
||||||
'roadfurniture': _('Mobilier voirie'),
|
'roadfurniture': _('Mobilier voirie'),
|
||||||
'roaddrain': _('Drain'),
|
'roaddrain': _('Drain'),
|
||||||
'roadconcreteblockset': _('Blocs béton'),
|
'roadconcreteblockset': _('Blocs béton'),
|
||||||
|
|
@ -431,8 +432,10 @@ ELEMENT_TYPE_LABELS = {
|
||||||
'signpole': _('Poteau signalisation'),
|
'signpole': _('Poteau signalisation'),
|
||||||
'signpanel': _('Panneau'),
|
'signpanel': _('Panneau'),
|
||||||
'signparkingspot': _('Place de parking'),
|
'signparkingspot': _('Place de parking'),
|
||||||
|
'parkingspot': _('Place de parking'),
|
||||||
# --- Tricolores ---
|
# --- Tricolores ---
|
||||||
'trafficlightpole': _('Poteau tricolore'),
|
'trafficlightpole': _('Poteau tricolore'),
|
||||||
|
'trafficlightradar': _('Radar TL'),
|
||||||
'trafficlightcable': _('Câble TL'),
|
'trafficlightcable': _('Câble TL'),
|
||||||
'trafficlightlantern': _('Lanterne TL'),
|
'trafficlightlantern': _('Lanterne TL'),
|
||||||
'trafficlightcontroller': _('Contrôleur TL'),
|
'trafficlightcontroller': _('Contrôleur TL'),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% translate "Patrimoine & Assets" %}
|
{% translate "Assets" %}
|
||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
|
|
@ -266,8 +266,8 @@
|
||||||
}
|
}
|
||||||
.asset-map-marker img,
|
.asset-map-marker img,
|
||||||
.asset-symbology-icon-img {
|
.asset-symbology-icon-img {
|
||||||
width: 20px;
|
width: 22px;
|
||||||
height: 20px;
|
height: 22px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
display: block;
|
display: block;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
@ -413,11 +413,15 @@
|
||||||
}
|
}
|
||||||
.asset-icon-circle img,
|
.asset-icon-circle img,
|
||||||
#sheetIconCircle img {
|
#sheetIconCircle img {
|
||||||
width: 24px;
|
width: 26px;
|
||||||
height: 24px;
|
height: 26px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.multi-asset-card .asset-icon-circle img {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Badges de statuts */
|
/* Badges de statuts */
|
||||||
.badge-status {
|
.badge-status {
|
||||||
|
|
@ -432,6 +436,20 @@
|
||||||
.badge-status-planned { background-color: #e0e7ff; color: #3730a3; }
|
.badge-status-planned { background-color: #e0e7ff; color: #3730a3; }
|
||||||
.badge-status-removed { background-color: #f1f5f9; color: #475569; }
|
.badge-status-removed { background-color: #f1f5f9; color: #475569; }
|
||||||
|
|
||||||
|
/* Badges de types d'asset */
|
||||||
|
.badge-element-type {
|
||||||
|
font-size: 0.70rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.25;
|
||||||
|
padding: 0.15rem 0.45rem;
|
||||||
|
border-radius: 0.35rem;
|
||||||
|
background-color: #eff6ff;
|
||||||
|
color: #1e40af;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.spin-once {
|
.spin-once {
|
||||||
animation: spinOnce 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
animation: spinOnce 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
@ -526,9 +544,6 @@
|
||||||
<span id="zoneIndicator" class="badge bg-info-subtle text-info-emphasis d-none" role="button" style="cursor: pointer;" title="{% translate 'Filtré selon la zone visible sur la carte. Cliquez pour élargir à toute la commune.' %}">
|
<span id="zoneIndicator" class="badge bg-info-subtle text-info-emphasis d-none" role="button" style="cursor: pointer;" title="{% translate 'Filtré selon la zone visible sur la carte. Cliquez pour élargir à toute la commune.' %}">
|
||||||
<i class="bi bi-crop me-1"></i>{% translate "Zone visible" %} <i class="bi bi-x ms-1"></i>
|
<i class="bi bi-crop me-1"></i>{% translate "Zone visible" %} <i class="bi bi-x ms-1"></i>
|
||||||
</span>
|
</span>
|
||||||
<span id="locationIndicator" class="text-primary d-none ms-1">
|
|
||||||
<i class="bi bi-geo-alt-fill"></i> {% translate "Trié par distance" %}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="loadingSpinner" class="spinner-border spinner-border-sm text-primary d-none" role="status">
|
<div id="loadingSpinner" class="spinner-border spinner-border-sm text-primary d-none" role="status">
|
||||||
<span class="visually-hidden">{% translate "Chargement..." %}</span>
|
<span class="visually-hidden">{% translate "Chargement..." %}</span>
|
||||||
|
|
@ -629,7 +644,7 @@
|
||||||
<div class="fw-semibold text-dark mb-1" id="sheetName">-</div>
|
<div class="fw-semibold text-dark mb-1" id="sheetName">-</div>
|
||||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
<span id="sheetStatusBadge" class="badge-status badge-status-active">Actif</span>
|
<span id="sheetStatusBadge" class="badge-status badge-status-active">Actif</span>
|
||||||
<span id="sheetElementType" class="badge bg-light text-secondary border">Élément</span>
|
<span id="sheetElementType" class="badge-element-type">Élément</span>
|
||||||
<span id="sheetDistanceBadge" class="badge bg-primary-subtle text-primary d-none">
|
<span id="sheetDistanceBadge" class="badge bg-primary-subtle text-primary d-none">
|
||||||
<i class="bi bi-geo-alt me-1"></i><span id="sheetDistanceText"></span>
|
<i class="bi bi-geo-alt me-1"></i><span id="sheetDistanceText"></span>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -757,7 +772,6 @@
|
||||||
const zoneIndicator = document.getElementById('zoneIndicator');
|
const zoneIndicator = document.getElementById('zoneIndicator');
|
||||||
const resultsCountEl = document.getElementById('resultsCount');
|
const resultsCountEl = document.getElementById('resultsCount');
|
||||||
const loadingSpinner = document.getElementById('loadingSpinner');
|
const loadingSpinner = document.getElementById('loadingSpinner');
|
||||||
const locationIndicator = document.getElementById('locationIndicator');
|
|
||||||
const btnGpsLocate = document.getElementById('btnGpsLocate');
|
const btnGpsLocate = document.getElementById('btnGpsLocate');
|
||||||
const btnResetFilters = document.getElementById('btnResetFilters');
|
const btnResetFilters = document.getElementById('btnResetFilters');
|
||||||
|
|
||||||
|
|
@ -1242,6 +1256,14 @@
|
||||||
? AssetSymbology.renderIconHtml(symbology)
|
? AssetSymbology.renderIconHtml(symbology)
|
||||||
: `<i class="bi ${symbology.iconClass || 'bi-pin-map'}"></i>`;
|
: `<i class="bi ${symbology.iconClass || 'bi-pin-map'}"></i>`;
|
||||||
|
|
||||||
|
const hasCustomName = asset.name && asset.name.trim() && asset.name.toLowerCase() !== (asset.element_type || '').toLowerCase();
|
||||||
|
const typeBadgeHtml = asset.element_type
|
||||||
|
? `<span class="badge-element-type text-nowrap flex-shrink-0">${escapeHtml(asset.element_type)}</span>`
|
||||||
|
: '';
|
||||||
|
const nameHtml = hasCustomName
|
||||||
|
? `<span class="text-truncate flex-grow-1">${escapeHtml(asset.name)}</span>`
|
||||||
|
: (!asset.element_type && asset.thematic_name ? `<span class="text-truncate flex-grow-1">${escapeHtml(asset.thematic_name)}</span>` : '');
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="asset-icon-circle" style="background-color: ${thematicColor}15; color: ${thematicColor};">
|
<div class="asset-icon-circle" style="background-color: ${thematicColor}15; color: ${thematicColor};">
|
||||||
${iconHtml}
|
${iconHtml}
|
||||||
|
|
@ -1249,9 +1271,12 @@
|
||||||
<div class="flex-grow-1 text-truncate">
|
<div class="flex-grow-1 text-truncate">
|
||||||
<div class="d-flex align-items-center gap-1.5 flex-wrap">
|
<div class="d-flex align-items-center gap-1.5 flex-wrap">
|
||||||
<span class="fw-bold text-dark text-truncate">${escapeHtml(asset.code)}</span>
|
<span class="fw-bold text-dark text-truncate">${escapeHtml(asset.code)}</span>
|
||||||
<span class="${statusClass}">${escapeHtml(asset.status_display)}</span>
|
<span class="${statusClass}">${escapeHtml(asset.status_display || asset.status || 'Actif')}</span>
|
||||||
|
</div>
|
||||||
|
<div class="small text-secondary d-flex align-items-center gap-1.5 mt-0.5 overflow-hidden">
|
||||||
|
${typeBadgeHtml}
|
||||||
|
${nameHtml}
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-secondary text-truncate">${escapeHtml(asset.name || asset.element_type || asset.thematic_name)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
${distanceBadge}
|
${distanceBadge}
|
||||||
<i class="bi bi-chevron-right text-muted"></i>
|
<i class="bi bi-chevron-right text-muted"></i>
|
||||||
|
|
@ -1399,6 +1424,14 @@
|
||||||
? `<span class="badge bg-primary-subtle text-primary ms-auto me-1 text-nowrap"><i class="bi bi-geo-alt me-1"></i>${formatDistance(asset.distance_m)}</span>`
|
? `<span class="badge bg-primary-subtle text-primary ms-auto me-1 text-nowrap"><i class="bi bi-geo-alt me-1"></i>${formatDistance(asset.distance_m)}</span>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
const hasCustomName = asset.name && asset.name.trim() && asset.name.toLowerCase() !== (asset.element_type || '').toLowerCase();
|
||||||
|
const typeBadgeHtml = asset.element_type
|
||||||
|
? `<span class="badge-element-type text-nowrap flex-shrink-0">${escapeHtml(asset.element_type)}</span>`
|
||||||
|
: '';
|
||||||
|
const nameHtml = hasCustomName
|
||||||
|
? `<span class="text-truncate flex-grow-1">${escapeHtml(asset.name)}</span>`
|
||||||
|
: (!asset.element_type && asset.thematic_name ? `<span class="text-truncate flex-grow-1">${escapeHtml(asset.thematic_name)}</span>` : '');
|
||||||
|
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'multi-asset-card';
|
card.className = 'multi-asset-card';
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
|
|
@ -1410,7 +1443,10 @@
|
||||||
<span class="fw-bold text-dark text-truncate">${escapeHtml(asset.code)}</span>
|
<span class="fw-bold text-dark text-truncate">${escapeHtml(asset.code)}</span>
|
||||||
<span class="${statusClass}">${escapeHtml(asset.status_display || asset.status || 'Actif')}</span>
|
<span class="${statusClass}">${escapeHtml(asset.status_display || asset.status || 'Actif')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-secondary text-truncate">${escapeHtml(asset.name || asset.element_type || asset.thematic_name || '')}</div>
|
<div class="small text-secondary d-flex align-items-center gap-1.5 mt-0.5 overflow-hidden">
|
||||||
|
${typeBadgeHtml}
|
||||||
|
${nameHtml}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${distanceBadge}
|
${distanceBadge}
|
||||||
<i class="bi bi-chevron-right text-muted flex-shrink-0"></i>
|
<i class="bi bi-chevron-right text-muted flex-shrink-0"></i>
|
||||||
|
|
@ -1511,8 +1547,12 @@
|
||||||
: `<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 || 'Actif';
|
if (asset.element_type) {
|
||||||
sheetElementType.textContent = asset.element_type || '';
|
sheetElementType.textContent = asset.element_type;
|
||||||
|
sheetElementType.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
sheetElementType.classList.add('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
if (asset.distance_m !== null && asset.distance_m !== undefined) {
|
if (asset.distance_m !== null && asset.distance_m !== undefined) {
|
||||||
sheetDistanceBadge.classList.remove('d-none');
|
sheetDistanceBadge.classList.remove('d-none');
|
||||||
|
|
@ -1604,7 +1644,6 @@
|
||||||
lat: pos.coords.latitude,
|
lat: pos.coords.latitude,
|
||||||
lon: pos.coords.longitude,
|
lon: pos.coords.longitude,
|
||||||
};
|
};
|
||||||
locationIndicator.classList.remove('d-none');
|
|
||||||
|
|
||||||
// Marqueur sur la carte
|
// Marqueur sur la carte
|
||||||
if (map) {
|
if (map) {
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@
|
||||||
<div class="icon-wrapper">
|
<div class="icon-wrapper">
|
||||||
<i class="bi bi-pin-map"></i>
|
<i class="bi bi-pin-map"></i>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="fw-semibold">{% translate "Patrimoine & Assets" %}</h2>
|
<h2 class="fw-semibold">{% translate "Assets" %}</h2>
|
||||||
<p>{% translate "Consulter et localiser les assets sur le terrain" %}</p>
|
<p>{% translate "Consulter et localiser les assets sur le terrain" %}</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,14 @@ class MobileAssetDetailTests(TestCase):
|
||||||
"icon": "bi-p-square"
|
"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
|
# Create Roles
|
||||||
self.admin_role, _ = Role.objects.get_or_create(name="admin")
|
self.admin_role, _ = Role.objects.get_or_create(name="admin")
|
||||||
|
|
@ -237,7 +245,7 @@ class MobileAssetDetailTests(TestCase):
|
||||||
self.assertEqual(self.tree.tag_number, "TAG-99999")
|
self.assertEqual(self.tree.tag_number, "TAG-99999")
|
||||||
|
|
||||||
def test_mobile_index_shows_assets_card_when_authorized(self):
|
def test_mobile_index_shows_assets_card_when_authorized(self):
|
||||||
"""User with asset view rights sees 'Patrimoine & Assets' card on mobile home."""
|
"""User with asset view rights sees 'Assets' card on mobile home."""
|
||||||
user = User.objects.create_user(username="asset_viewer", password="password")
|
user = User.objects.create_user(username="asset_viewer", password="password")
|
||||||
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
||||||
user_config.roles.add(self.viewer_role)
|
user_config.roles.add(self.viewer_role)
|
||||||
|
|
@ -253,15 +261,10 @@ class MobileAssetDetailTests(TestCase):
|
||||||
self.assertTrue(response.context["can_view_assets"])
|
self.assertTrue(response.context["can_view_assets"])
|
||||||
self.assertContains(response, reverse("mobile:mobile_assets_index"))
|
self.assertContains(response, reverse("mobile:mobile_assets_index"))
|
||||||
content = response.content.decode()
|
content = response.content.decode()
|
||||||
self.assertTrue(
|
self.assertTrue("Assets" in content)
|
||||||
"Patrimoine & Assets" in content or
|
|
||||||
"Assets & Heritage" in content or
|
|
||||||
"Patrimoine & Assets" in content or
|
|
||||||
"Assets & Heritage" in content
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_mobile_index_hides_assets_card_when_unauthorized(self):
|
def test_mobile_index_hides_assets_card_when_unauthorized(self):
|
||||||
"""User without asset view rights does NOT see 'Patrimoine & Assets' card on mobile home."""
|
"""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 = User.objects.create_user(username="no_asset_user", password="password")
|
||||||
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
||||||
user_config.roles.add(self.viewer_role)
|
user_config.roles.add(self.viewer_role)
|
||||||
|
|
@ -474,6 +477,9 @@ class MobileAssetDetailTests(TestCase):
|
||||||
self.assertEqual(feature["properties"]["geom_type"], "MultiPolygon")
|
self.assertEqual(feature["properties"]["geom_type"], "MultiPolygon")
|
||||||
self.assertEqual(feature["properties"]["bbox"], result_item["bbox"])
|
self.assertEqual(feature["properties"]["bbox"], result_item["bbox"])
|
||||||
self.assertEqual(feature["id"], f"naturegreensurface_{self.green_surface.id}")
|
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)
|
# 2. Test parking search with parking spot (Polygon)
|
||||||
response = self.client.get(search_url, {"thematic": "parking", "q": "PK-SPOT-001"})
|
response = self.client.get(search_url, {"thematic": "parking", "q": "PK-SPOT-001"})
|
||||||
|
|
@ -485,8 +491,81 @@ class MobileAssetDetailTests(TestCase):
|
||||||
self.assertEqual(spot_res["geom_type"], "Polygon")
|
self.assertEqual(spot_res["geom_type"], "Polygon")
|
||||||
self.assertIsNotNone(spot_res["bbox"])
|
self.assertIsNotNone(spot_res["bbox"])
|
||||||
self.assertEqual(len(spot_res["bbox"]), 4)
|
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]
|
spot_feat = data["geojson"]["features"][0]
|
||||||
self.assertEqual(spot_feat["geometry"]["type"], "Polygon")
|
self.assertEqual(spot_feat["geometry"]["type"], "Polygon")
|
||||||
self.assertEqual(spot_feat["properties"]["geom_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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import re
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
|
@ -1387,6 +1388,10 @@ def mobile_assets_search(request):
|
||||||
search_q |= Q(name_fr__icontains=q)
|
search_q |= Q(name_fr__icontains=q)
|
||||||
if hasattr(model_class, 'name_nl'):
|
if hasattr(model_class, 'name_nl'):
|
||||||
search_q |= Q(name_nl__icontains=q)
|
search_q |= Q(name_nl__icontains=q)
|
||||||
|
if model_key == 'signpanel' and hasattr(model_class, 'signpanel_type'):
|
||||||
|
search_q |= Q(signpanel_type__code__icontains=q)
|
||||||
|
search_q |= Q(signpanel_type__name_fr__icontains=q)
|
||||||
|
search_q |= Q(signpanel_type__name_nl__icontains=q)
|
||||||
qs = qs.filter(search_q)
|
qs = qs.filter(search_q)
|
||||||
|
|
||||||
# Nombre total d'assets correspondants (pour savoir si limite atteinte)
|
# Nombre total d'assets correspondants (pour savoir si limite atteinte)
|
||||||
|
|
@ -1415,6 +1420,10 @@ def mobile_assets_search(request):
|
||||||
features = []
|
features = []
|
||||||
|
|
||||||
for model_key, model_class, qs in model_querysets:
|
for model_key, model_class, qs in model_querysets:
|
||||||
|
if model_key == 'signpanel' and hasattr(model_class, 'signpanel_type'):
|
||||||
|
qs = qs.select_related('signpanel_type', 'model', 'pole')
|
||||||
|
elif model_key == 'trafficlightlantern' and hasattr(model_class, 'model'):
|
||||||
|
qs = qs.select_related('model', 'pole')
|
||||||
items = list(qs)
|
items = list(qs)
|
||||||
for item in items:
|
for item in items:
|
||||||
lat = None
|
lat = None
|
||||||
|
|
@ -1476,6 +1485,30 @@ def mobile_assets_search(request):
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if (lon is None or lat is None):
|
||||||
|
for support_attr in ('pole', 'trafficlight_pole', 'publiclighting_pole'):
|
||||||
|
if hasattr(item, support_attr):
|
||||||
|
support = getattr(item, support_attr, None)
|
||||||
|
if support:
|
||||||
|
if hasattr(support, 'lat') and support.lat is not None and hasattr(support, 'lon') and support.lon is not None:
|
||||||
|
try:
|
||||||
|
lat = float(str(support.lat).replace(',', '.'))
|
||||||
|
lon = float(str(support.lon).replace(',', '.'))
|
||||||
|
break
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
if hasattr(support, 'geom') and support.geom:
|
||||||
|
try:
|
||||||
|
s_geom = support.geom.clone()
|
||||||
|
if s_geom.srid != 4326:
|
||||||
|
s_geom.transform(4326)
|
||||||
|
c = s_geom.centroid
|
||||||
|
lon = float(c.x)
|
||||||
|
lat = float(c.y)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 4. Fallback si geometry est absent mais qu'on a un point lon/lat
|
# 4. Fallback si geometry est absent mais qu'on a un point lon/lat
|
||||||
if not geometry and lon is not None and lat is not None:
|
if not geometry and lon is not None and lat is not None:
|
||||||
geometry = {
|
geometry = {
|
||||||
|
|
@ -1507,9 +1540,31 @@ def mobile_assets_search(request):
|
||||||
(getattr(item.model, f'name_{lang}', None) or getattr(item.model, 'name_fr', None) if getattr(item, 'model', None) else '') or
|
(getattr(item.model, f'name_{lang}', None) or getattr(item.model, 'name_fr', None) if getattr(item, 'model', None) else '') or
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
|
|
||||||
|
signpanel_type = getattr(item, 'signpanel_type', None) if model_key == 'signpanel' else None
|
||||||
|
panel_type_code = getattr(signpanel_type, 'code', None) if signpanel_type else None
|
||||||
|
if panel_type_code:
|
||||||
|
panel_type_code = str(panel_type_code).strip()
|
||||||
|
elif model_key == 'signpanel':
|
||||||
|
raw_code = getattr(item, 'code', '') or ''
|
||||||
|
if re.match(r'^[A-Za-z]{1,3}\d{1,3}[A-Za-z]?$', raw_code.strip()):
|
||||||
|
panel_type_code = raw_code.strip()
|
||||||
|
|
||||||
|
svg_url = f"/static/assets/road_signs/2025/{panel_type_code.upper()}.svg" if panel_type_code else None
|
||||||
|
|
||||||
|
if model_key == 'signpanel':
|
||||||
|
if signpanel_type:
|
||||||
|
sign_name = getattr(signpanel_type, f'name_{lang}', None) or getattr(signpanel_type, 'name_fr', None) or ''
|
||||||
|
if not item_name:
|
||||||
|
item_name = f"{panel_type_code} - {sign_name}" if panel_type_code and sign_name else (sign_name or panel_type_code or '')
|
||||||
|
elif not item_name and panel_type_code:
|
||||||
|
item_name = panel_type_code
|
||||||
|
|
||||||
status_val = getattr(item, 'status', 'active')
|
status_val = getattr(item, 'status', 'active')
|
||||||
status_display = item.get_status_display() if hasattr(item, 'get_status_display') else status_val
|
status_display = item.get_status_display() if hasattr(item, 'get_status_display') else status_val
|
||||||
element_type = get_element_type_label(model_key)
|
element_type = get_element_type_label(model_key)
|
||||||
|
if model_key == 'parkingspot' and hasattr(item, 'get_spot_type_display') and item.spot_type:
|
||||||
|
element_type = str(item.get_spot_type_display())
|
||||||
|
|
||||||
detail_url = reverse('mobile:mobile_asset_detail_direct', kwargs={'asset_model': model_key, 'asset_id': item.id})
|
detail_url = reverse('mobile:mobile_asset_detail_direct', kwargs={'asset_model': model_key, 'asset_id': item.id})
|
||||||
spot_type = getattr(item, 'spot_type', None)
|
spot_type = getattr(item, 'spot_type', None)
|
||||||
|
|
@ -1527,6 +1582,9 @@ def mobile_assets_search(request):
|
||||||
'thematic_icon': thematic_icon,
|
'thematic_icon': thematic_icon,
|
||||||
'model_key': model_key,
|
'model_key': model_key,
|
||||||
'asset_type': model_key,
|
'asset_type': model_key,
|
||||||
|
'panel_type': panel_type_code,
|
||||||
|
'svg_url': svg_url,
|
||||||
|
'icon_url': svg_url,
|
||||||
'spot_type': spot_type,
|
'spot_type': spot_type,
|
||||||
'element_type': element_type,
|
'element_type': element_type,
|
||||||
'geom_type': geom_type,
|
'geom_type': geom_type,
|
||||||
|
|
@ -1558,6 +1616,9 @@ def mobile_assets_search(request):
|
||||||
'thematic_icon': thematic_icon,
|
'thematic_icon': thematic_icon,
|
||||||
'model_key': model_key,
|
'model_key': model_key,
|
||||||
'asset_type': model_key,
|
'asset_type': model_key,
|
||||||
|
'panel_type': panel_type_code,
|
||||||
|
'svg_url': svg_url,
|
||||||
|
'icon_url': svg_url,
|
||||||
'spot_type': spot_type,
|
'spot_type': spot_type,
|
||||||
'element_type': element_type,
|
'element_type': element_type,
|
||||||
'geom_type': geom_type,
|
'geom_type': geom_type,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue