From 49f68081c35f20e1fec9f1e2669ff9c7353d7f61 Mon Sep 17 00:00:00 2001 From: Cam Proudlock Date: Mon, 17 Aug 2026 13:18:26 -0400 Subject: [PATCH] Match the optional identifiers in every list's search box --- plugins/computers/api/routes.py | 5 ++ plugins/machines/api/routes.py | 5 ++ plugins/measuringtools/api/routes.py | 5 ++ plugins/network/api/routes.py | 5 ++ plugins/printers/api/asset_routes.py | 5 ++ .../test_core/test_list_search_identifiers.py | 84 +++++++++++++++++++ 6 files changed, 109 insertions(+) create mode 100644 tests/test_core/test_list_search_identifiers.py diff --git a/plugins/computers/api/routes.py b/plugins/computers/api/routes.py index 90315174..79d0a734 100644 --- a/plugins/computers/api/routes.py +++ b/plugins/computers/api/routes.py @@ -375,6 +375,11 @@ def list_computers(): Asset.assetnumber.ilike(pattern), Asset.name.ilike(pattern), Asset.serialnumber.ilike(pattern), + # The optional identifiers too (ADR-001). Global search matches + # these, and a tag read off the machine has to find it here as + # well - this box is where someone holding the label looks. + Asset.gaugelabreference.ilike(pattern), + Asset.maintenancereference.ilike(pattern), Computer.hostname.ilike(pattern), ComputerType.computertype.ilike(pattern) ) diff --git a/plugins/machines/api/routes.py b/plugins/machines/api/routes.py index a3a1dda8..b6af6721 100644 --- a/plugins/machines/api/routes.py +++ b/plugins/machines/api/routes.py @@ -189,6 +189,11 @@ def list_machines(): Asset.assetnumber.ilike(pattern), Asset.name.ilike(pattern), Asset.serialnumber.ilike(pattern), + # The optional identifiers too (ADR-001). Global search matches + # these, and a tag read off the machine has to find it here as + # well - this box is where someone holding the label looks. + Asset.gaugelabreference.ilike(pattern), + Asset.maintenancereference.ilike(pattern), MachineType.machinetype.ilike(pattern), Vendor.vendor.ilike(pattern) ) diff --git a/plugins/measuringtools/api/routes.py b/plugins/measuringtools/api/routes.py index 77395769..b0384454 100644 --- a/plugins/measuringtools/api/routes.py +++ b/plugins/measuringtools/api/routes.py @@ -179,6 +179,11 @@ def list_tools(): Asset.assetnumber.ilike(pattern), Asset.name.ilike(pattern), Asset.serialnumber.ilike(pattern), + # The optional identifiers too (ADR-001). Global search matches + # these, and a tag read off the machine has to find it here as + # well - this box is where someone holding the label looks. + Asset.gaugelabreference.ilike(pattern), + Asset.maintenancereference.ilike(pattern), MeasuringToolType.name.ilike(pattern), )) if type_id := request.args.get('typeid', type=int): diff --git a/plugins/network/api/routes.py b/plugins/network/api/routes.py index 9e1b80d6..4c7780b5 100644 --- a/plugins/network/api/routes.py +++ b/plugins/network/api/routes.py @@ -223,6 +223,11 @@ def list_network_devices(): Asset.assetnumber.ilike(pattern), Asset.name.ilike(pattern), Asset.serialnumber.ilike(pattern), + # The optional identifiers too (ADR-001). Global search matches + # these, and a tag read off the machine has to find it here as + # well - this box is where someone holding the label looks. + Asset.gaugelabreference.ilike(pattern), + Asset.maintenancereference.ilike(pattern), NetworkDevice.hostname.ilike(pattern), NetworkDeviceType.networkdevicetype.ilike(pattern), Vendor.vendor.ilike(pattern) diff --git a/plugins/printers/api/asset_routes.py b/plugins/printers/api/asset_routes.py index 12275b8b..abe0998f 100644 --- a/plugins/printers/api/asset_routes.py +++ b/plugins/printers/api/asset_routes.py @@ -254,6 +254,11 @@ def list_printers(): Asset.assetnumber.ilike(pattern), Asset.name.ilike(pattern), Asset.serialnumber.ilike(pattern), + # The optional identifiers too (ADR-001). Global search matches + # these, and a tag read off the machine has to find it here as + # well - this box is where someone holding the label looks. + Asset.gaugelabreference.ilike(pattern), + Asset.maintenancereference.ilike(pattern), Printer.hostname.ilike(pattern), Printer.windowsname.ilike(pattern), PrinterType.printertype.ilike(pattern), diff --git a/tests/test_core/test_list_search_identifiers.py b/tests/test_core/test_list_search_identifiers.py new file mode 100644 index 00000000..ad8c51ab --- /dev/null +++ b/tests/test_core/test_list_search_identifiers.py @@ -0,0 +1,84 @@ +"""Every asset list's search box matches the optional identifiers (ADR-001). + +Settings offers `gaugelabreference` and `maintenancereference` on machines, PCs, +printers, network devices and measuring tools. Both were shown on the form and +the detail page and neither was matched by any list's `?search=`, so someone +holding a physical tag could read it, type it into the list they were already +looking at, and get nothing - while global search found it. + +One parametrized test over all five lists on purpose. The search clause is +copy-pasted five times, once per plugin, which is exactly how all five came to +omit a field their forms collect; a per-plugin test would drift the same way. +""" + +import pytest + +from shopdb.core.models import Asset, AssetType + + +# (list endpoint, create endpoint, assettype) +LISTS = [ + ('/api/machines', '/api/machines', 'machine'), + ('/api/computers', '/api/computers', 'computer'), + ('/api/printers', '/api/printers', 'printer'), + ('/api/network', '/api/network', 'network_device'), + ('/api/measuringtools', '/api/measuringtools', 'measuring_tool'), +] + + +def _seed(client, db, auth_headers, createpath, assettype, assetnumber): + """Create one record of this type carrying both identifiers.""" + if not AssetType.query.filter_by(assettype=assettype).first(): + db.session.add(AssetType(assettype=assettype, pluginname=assettype, + tablename=assettype, description=assettype)) + db.session.commit() + + resp = client.post(createpath, json={'assetnumber': assetnumber}, + headers=auth_headers) + assert resp.status_code == 201, (createpath, resp.get_json()) + + asset = Asset.query.filter_by(assetnumber=assetnumber).first() + asset.gaugelabreference = f'ZZG{assetnumber}' + asset.maintenancereference = f'ZZM{assetnumber}' + db.session.commit() + return asset + + +def _numbers(client, auth_headers, listpath, query): + """Asset numbers a list returns for a search term.""" + resp = client.get(f'{listpath}?search={query}', headers=auth_headers) + assert resp.status_code == 200, (listpath, resp.get_json()) + found = [] + for row in resp.get_json()['data']: + # Lists differ in shape; the asset number is the field all of them + # carry, either directly or on a nested asset. + found.append(row.get('assetnumber') + or (row.get('asset') or {}).get('assetnumber')) + return [f for f in found if f] + + +@pytest.mark.parametrize('listpath,createpath,assettype', LISTS) +def test_a_list_search_finds_a_gauge_lab_reference(client, db, auth_headers, + listpath, createpath, assettype): + assetnumber = f'ZZ{assettype[:4].upper()}01' + _seed(client, db, auth_headers, createpath, assettype, assetnumber) + assert assetnumber in _numbers(client, auth_headers, listpath, + f'ZZG{assetnumber}') + + +@pytest.mark.parametrize('listpath,createpath,assettype', LISTS) +def test_a_list_search_finds_a_maintenance_reference(client, db, auth_headers, + listpath, createpath, assettype): + assetnumber = f'ZZ{assettype[:4].upper()}02' + _seed(client, db, auth_headers, createpath, assettype, assetnumber) + assert assetnumber in _numbers(client, auth_headers, listpath, + f'ZZM{assetnumber}') + + +@pytest.mark.parametrize('listpath,createpath,assettype', LISTS) +def test_a_list_search_still_excludes_a_non_match(client, db, auth_headers, + listpath, createpath, assettype): + """Widening the clause must not turn the search box into a pass-through.""" + assetnumber = f'ZZ{assettype[:4].upper()}03' + _seed(client, db, auth_headers, createpath, assettype, assetnumber) + assert _numbers(client, auth_headers, listpath, 'ZZNOTHINGMATCHESTHIS') == []