search: asset lists search the type column they display

Every asset list shows a Type column (and printers a Model, machines and
network a Vendor), but the search filters only looked at the asset number,
name, serial and hostname. Searching a type returned zero rows: 'Part Washer'
on machines, 'Standard' on PCs, 'Thermal' on printers.

Extend the search on machines, computers, printers, network devices,
measuring tools and the unified asset list to cover the type name plus the
vendor/model where the list shows them. Joins are outer joins so an asset
missing a type or vendor still matches on its own fields; the core list uses
a correlated EXISTS instead, since its type-name filter already joins
AssetType.
This commit is contained in:
cproudlock
2026-07-31 09:02:51 -04:00
parent 71982fc0f1
commit b16f143467
8 changed files with 340 additions and 33 deletions

View File

@@ -363,14 +363,20 @@ def list_computers():
if exactassetnumber := request.args.get('assetnumber'):
query = query.filter(Asset.assetnumber == exactassetnumber)
# Search filter
# Search filter. Covers the type name too - it is a column in the list, so
# searching 'Standard' must find the PCs of that type. Outer join so a PC
# with no type still matches on its own fields.
if search := request.args.get('search'):
query = query.filter(
pattern = f'%{search}%'
query = query.outerjoin(
ComputerType, Computer.computertypeid == ComputerType.computertypeid
).filter(
db.or_(
Asset.assetnumber.ilike(f'%{search}%'),
Asset.name.ilike(f'%{search}%'),
Asset.serialnumber.ilike(f'%{search}%'),
Computer.hostname.ilike(f'%{search}%')
Asset.assetnumber.ilike(pattern),
Asset.name.ilike(pattern),
Asset.serialnumber.ilike(pattern),
Computer.hostname.ilike(pattern),
ComputerType.computertype.ilike(pattern)
)
)

View File

@@ -3,7 +3,7 @@
from flask import Blueprint, request
from flask_jwt_extended import jwt_required
from shopdb.api import db, Asset, AssetType, AuditLog, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query, resolve_dualpath_pairs, dualpath_single_machine_enabled
from shopdb.api import db, Asset, AssetType, AuditLog, Vendor, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query, resolve_dualpath_pairs, dualpath_single_machine_enabled
from ..models import Machine, MachineType
@@ -174,13 +174,23 @@ def list_machines():
if exactassetnumber := request.args.get('assetnumber'):
query = query.filter(Asset.assetnumber == exactassetnumber)
# Search filter
# Search filter. Covers what the list actually SHOWS - the asset fields plus
# the type and vendor names, which are columns in the table. Searching a type
# like 'Part Washer' used to return nothing. Outer joins so a machine with no
# type or vendor still matches on its own fields.
if search := request.args.get('search'):
query = query.filter(
pattern = f'%{search}%'
query = query.outerjoin(
MachineType, Machine.machinetypeid == MachineType.machinetypeid
).outerjoin(
Vendor, Machine.vendorid == Vendor.vendorid
).filter(
db.or_(
Asset.assetnumber.ilike(f'%{search}%'),
Asset.name.ilike(f'%{search}%'),
Asset.serialnumber.ilike(f'%{search}%')
Asset.assetnumber.ilike(pattern),
Asset.name.ilike(pattern),
Asset.serialnumber.ilike(pattern),
MachineType.machinetype.ilike(pattern),
Vendor.vendor.ilike(pattern)
)
)

View File

@@ -168,11 +168,18 @@ def list_tools():
# Exact-match natural-key lookup for idempotent import (asset number).
if exactassetnumber := request.args.get('assetnumber'):
query = query.filter(Asset.assetnumber == exactassetnumber)
# Type is a column in the list, so it has to be searchable. Outer join so a
# tool with no type still matches on its own fields.
if search := request.args.get('search'):
query = query.filter(db.or_(
Asset.assetnumber.ilike(f'%{search}%'),
Asset.name.ilike(f'%{search}%'),
Asset.serialnumber.ilike(f'%{search}%'),
pattern = f'%{search}%'
query = query.outerjoin(
MeasuringToolType,
MeasuringTool.measuringtooltypeid == MeasuringToolType.measuringtooltypeid
).filter(db.or_(
Asset.assetnumber.ilike(pattern),
Asset.name.ilike(pattern),
Asset.serialnumber.ilike(pattern),
MeasuringToolType.name.ilike(pattern),
))
if type_id := request.args.get('typeid', type=int):
query = query.filter(MeasuringTool.measuringtooltypeid == type_id)

View File

@@ -201,14 +201,24 @@ def list_network_devices():
if exactassetnumber := request.args.get('assetnumber'):
query = query.filter(Asset.assetnumber == exactassetnumber)
# Search filter
# Search filter. Type and vendor are columns in the list, so both must be
# searchable. Outer joins so a device missing either still matches on its
# own fields.
if search := request.args.get('search'):
query = query.filter(
pattern = f'%{search}%'
query = query.outerjoin(
NetworkDeviceType,
NetworkDevice.networkdevicetypeid == NetworkDeviceType.networkdevicetypeid
).outerjoin(
Vendor, NetworkDevice.vendorid == Vendor.vendorid
).filter(
db.or_(
Asset.assetnumber.ilike(f'%{search}%'),
Asset.name.ilike(f'%{search}%'),
Asset.serialnumber.ilike(f'%{search}%'),
NetworkDevice.hostname.ilike(f'%{search}%')
Asset.assetnumber.ilike(pattern),
Asset.name.ilike(pattern),
Asset.serialnumber.ilike(pattern),
NetworkDevice.hostname.ilike(pattern),
NetworkDeviceType.networkdevicetype.ilike(pattern),
Vendor.vendor.ilike(pattern)
)
)

View File

@@ -240,15 +240,24 @@ def list_printers():
if exactassetnumber := request.args.get('assetnumber'):
query = query.filter(Asset.assetnumber == exactassetnumber)
# Search filter
# Search filter. Type and model are columns in the list, so searching
# 'Thermal' must find the thermal printers. Outer joins so a printer missing
# either still matches on its own fields.
if search := request.args.get('search'):
query = query.filter(
pattern = f'%{search}%'
query = query.outerjoin(
PrinterType, Printer.printertypeid == PrinterType.printertypeid
).outerjoin(
Model, Printer.modelnumberid == Model.modelnumberid
).filter(
db.or_(
Asset.assetnumber.ilike(f'%{search}%'),
Asset.name.ilike(f'%{search}%'),
Asset.serialnumber.ilike(f'%{search}%'),
Printer.hostname.ilike(f'%{search}%'),
Printer.windowsname.ilike(f'%{search}%')
Asset.assetnumber.ilike(pattern),
Asset.name.ilike(pattern),
Asset.serialnumber.ilike(pattern),
Printer.hostname.ilike(pattern),
Printer.windowsname.ilike(pattern),
PrinterType.printertype.ilike(pattern),
Model.modelnumber.ilike(pattern)
)
)