ADR-013 Phase 3: search routes via get_asset_presentation, not a hardcoded map

Global-search rows built the plugin detail URL from a hardcoded url_map of
plugin routes in core. Now core prefers a plugin's declared
get_asset_presentation route (ADR-010), substituting the core assetid via the
plugin's by-asset resolver; types that have not declared fall back to the legacy
id-keyed map, so nothing breaks. Measuring tools (which declare the route) link
through it now; machines/PCs/printers/network migrate off the hardcode as they
add a by-asset route + declaration. Presentation map is collected once per
search (cached on flask.g). 2 consumer tests; 26 search tests green.
This commit is contained in:
cproudlock
2026-07-18 22:56:05 -04:00
parent b669561421
commit 8a2f984393
2 changed files with 92 additions and 9 deletions

View File

@@ -106,6 +106,32 @@ def _classify_query(query, integrations):
}
def _asset_presentation_map():
"""assettype -> route from enabled plugins' get_asset_presentation (ADR-010).
Lets core route a plugin-owned asset type in search rows without hardcoding
the route. Cached per request (built once per search, not per result).
"""
from flask import g
cached = getattr(g, '_asset_presentation_map', None)
if cached is not None:
return cached
result = {}
pm = current_app.extensions.get('plugin_manager')
if pm:
for plugin_name, plugin in pm.get_all_plugins().items():
if not pm.registry.is_enabled(plugin_name):
continue
try:
for entry in plugin.get_asset_presentation() or []:
if entry.get('assettype') and entry.get('route'):
result[entry['assettype']] = entry['route']
except Exception:
continue
g._asset_presentation_map = result
return result
def _get_asset_result(asset, query, relevance=None):
"""Build a search result dict from an Asset object."""
asset_type_name = asset.assettype.assettype if asset.assettype else 'asset'
@@ -122,14 +148,24 @@ def _get_asset_result(asset, query, relevance=None):
elif asset_type_name == 'measuring_tool' and hasattr(asset, 'measuringtool') and asset.measuringtool:
plugin_id = asset.measuringtool.measuringtoolid
url_map = {
'machine': f"/machines/{plugin_id}",
'computer': f"/pcs/{plugin_id}",
'network_device': f"/network/{plugin_id}",
'printer': f"/printers/{plugin_id}",
'measuring_tool': f"/measuringtools/{plugin_id}",
}
url = url_map.get(asset_type_name, f"/assets/{asset.assetid}")
# Prefer a plugin's declared get_asset_presentation route (ADR-010): core
# stops hardcoding the plugin's route as each plugin declares one. Those
# routes take the core assetid (via a by-asset resolver). Types that have
# not declared fall back to the legacy id-keyed map below.
presentation = _asset_presentation_map()
if asset_type_name in presentation:
url = presentation[asset_type_name].replace('{assetid}', str(asset.assetid))
result_id = asset.assetid
else:
url_map = {
'machine': f"/machines/{plugin_id}",
'computer': f"/pcs/{plugin_id}",
'network_device': f"/network/{plugin_id}",
'printer': f"/printers/{plugin_id}",
'measuring_tool': f"/measuringtools/{plugin_id}",
}
url = url_map.get(asset_type_name, f"/assets/{asset.assetid}")
result_id = plugin_id
display_name = asset.display_name
subtitle = None
@@ -143,7 +179,7 @@ def _get_asset_result(asset, query, relevance=None):
return {
'type': asset_type_name,
'id': plugin_id,
'id': result_id,
'title': display_name,
'subtitle': subtitle,
'location': location_name,

View File

@@ -0,0 +1,47 @@
"""Search consumes get_asset_presentation for plugin-owned routes (ADR-010).
Core no longer hardcodes a plugin's search route once the plugin declares one:
measuring tools (which declare a by-asset presentation route) link through it,
while a type that has not declared falls back to the legacy id-keyed route.
"""
from types import SimpleNamespace
from shopdb.core.api.search import _get_asset_result
def _fake_asset(assettype, assetid, **extra):
asset = SimpleNamespace(
assettype=SimpleNamespace(assettype=assettype),
assetid=assetid,
display_name=f'{assettype}-{assetid}',
name=None,
assetnumber=f'AN{assetid}',
location=None,
machine=None, computer=None, network_device=None, printer=None,
measuringtool=None,
)
for key, value in extra.items():
setattr(asset, key, value)
return asset
def test_measuring_tool_uses_declared_presentation_route(app):
"""A declared by-asset route is used, keyed on the core assetid."""
with app.app_context():
tool = _fake_asset(
'measuring_tool', 641,
measuringtool=SimpleNamespace(measuringtoolid=99))
result = _get_asset_result(tool, 'x')
assert result['url'] == '/measuringtools/by-asset/641'
assert result['id'] == 641
def test_machine_falls_back_to_legacy_route(app):
"""A type with no declared presentation keeps the legacy id-keyed route."""
with app.app_context():
machine = _fake_asset(
'machine', 80, machine=SimpleNamespace(machineid=12))
result = _get_asset_result(machine, 'x')
assert result['url'] == '/machines/12'
assert result['id'] == 12