Buildings and levels for the floor map, and make every identifier searchable

This commit is contained in:
2026-08-17 13:03:54 -04:00
parent 47fb65fdd2
commit fbd99041b0
54 changed files with 4173 additions and 880 deletions

View File

@@ -236,13 +236,26 @@ def _search_applications(query, search_term):
def _search_knowledgebase(query, search_term):
"""Search Knowledge Base by description and keywords."""
"""Search Knowledge Base by description and keywords.
An article whose topic is a RETIRED application is excluded, matching
GET /api/knowledgebase. Filtering it out of the plugin's own listing while
global search still returned it is not a rule at all: the article was two
keystrokes away, and the result printed the retired application's name as its
subject, which reads as though it were still in service.
A null topic still matches. Not every article is about an application.
"""
results = []
try:
_require_enabled('knowledgebase')
from plugins.knowledgebase.models import KnowledgeBase
retired = db.session.query(Application.appid).filter(
Application.isactive.is_(False))
kb_articles = KnowledgeBase.query.filter(
KnowledgeBase.isactive == True,
db.or_(KnowledgeBase.appid.is_(None),
KnowledgeBase.appid.notin_(retired)),
_word_match(query, KnowledgeBase.shortdescription,
KnowledgeBase.keywords)
).limit(20).all()
@@ -339,7 +352,22 @@ def _search_employees(query, search_term):
def _search_assets(query, search_term):
"""Search unified Assets table by number, name, serial, notes."""
"""Search unified Assets table by number, name, serial, notes and the two
optional identifiers.
gaugelabreference and maintenancereference are searched for EVERY asset type
(ADR-001). Settings lets a site enable either identifier on machines, PCs,
printers and network devices, but only the measuring-tools searcher looked at
gaugelabreference and nothing looked at maintenancereference at all - so a
tag an operator was told to record was one nobody could search by. An
identifier that can be entered has to be findable, or it is a write-only
field.
The per-type `identifier_<name>_<assettype>_enabled` toggles are NOT applied
here. They govern whether the field is SHOWN on that type; a value already in
the row is still the tag written on the physical machine, and matching it is
strictly better than returning nothing to someone reading it off a label.
"""
results = []
try:
assets = Asset.query.join(AssetType).options(
@@ -348,7 +376,8 @@ def _search_assets(query, search_term):
).filter(
Asset.isactive == True,
_word_match(query, Asset.assetnumber, Asset.name,
Asset.serialnumber, Asset.notes)
Asset.serialnumber, Asset.notes,
Asset.gaugelabreference, Asset.maintenancereference)
).limit(15).all()
for asset in assets:
@@ -357,6 +386,10 @@ def _search_assets(query, search_term):
relevance = 100
elif asset.name and query.lower() == asset.name.lower():
relevance = 90
elif asset.gaugelabreference and query.lower() == asset.gaugelabreference.lower():
relevance = 88
elif asset.maintenancereference and query.lower() == asset.maintenancereference.lower():
relevance = 86
elif asset.serialnumber and query.lower() == asset.serialnumber.lower():
relevance = 85
elif asset.name and query.lower() in asset.name.lower():
@@ -408,6 +441,104 @@ def _search_measuringtools(query, search_term):
return results
def _search_usbdevices(query, search_term):
"""Search USB devices by serial, asset tag and product name.
A USB device is NOT an asset - it lives in the usb plugin's own table - so
the generic asset search cannot see it and these records were unreachable
from search entirely. Serial number is the field people actually have in
hand: it is what is printed on the stick they are holding.
currentusername is deliberately NOT searched. It records who holds the
device, and making search a way to list what a named person has checked out
is a different feature from finding a device.
"""
results = []
try:
_require_enabled('usb')
from plugins.usb.models import USBDevice
devices = USBDevice.query.filter(
USBDevice.isactive == True,
_word_match(query, USBDevice.serialnumber, USBDevice.assetnumber,
USBDevice.label, USBDevice.productname)
).limit(10).all()
for device in devices:
relevance = 20
if query.lower() == (device.serialnumber or '').lower():
relevance = 100
elif query.lower() == (device.assetnumber or '').lower():
relevance = 90
elif query.lower() == (device.label or '').lower():
relevance = 85
elif query.lower() in (device.label or '').lower():
relevance = 50
elif query.lower() in (device.productname or '').lower():
relevance = 40
results.append({
'type': 'usb_device',
'id': device.usbdeviceid,
'title': device.label or device.productname or device.serialnumber,
'subtitle': device.assetnumber or device.serialnumber,
'url': f'/usb/{device.usbdeviceid}',
'relevance': relevance,
})
except ImportError:
pass # usb plugin absent or disabled
except Exception as e:
logger.error(f"USB device search failed: {e}")
return results
def _search_printeditems(query, search_term):
"""Search printed items by bin code, gage-lab tag, name and description.
Printed items are their own records, not assets, so the generic asset search
never covered them. itemcode (the bin label, e.g. 3DP-0042) and gagelabtag
are both unique and both printed on physical labels, which makes them the
likeliest thing anyone types into search.
"""
results = []
try:
_require_enabled('printedparts')
from plugins.printedparts.models import PrintedItem
items = PrintedItem.query.filter(
PrintedItem.isactive == True,
_word_match(query, PrintedItem.itemcode, PrintedItem.gagelabtag,
PrintedItem.itemname, PrintedItem.itemdescription)
).limit(10).all()
for item in items:
relevance = 20
if query.lower() == (item.itemcode or '').lower():
relevance = 100
elif query.lower() == (item.gagelabtag or '').lower():
relevance = 95
elif query.lower() == (item.itemname or '').lower():
relevance = 90
elif query.lower() in (item.itemname or '').lower():
relevance = 50
subtitle = item.itemcode or item.gagelabtag
if item.binlocation:
subtitle = f'{subtitle} - {item.binlocation}' if subtitle else item.binlocation
results.append({
'type': 'printed_item',
'id': item.printeditemid,
'title': item.itemname,
'subtitle': subtitle,
'url': f'/printedparts/{item.printeditemid}',
'relevance': relevance,
})
except ImportError:
pass # printedparts plugin absent or disabled
except Exception as e:
logger.error(f"Printed item search failed: {e}")
return results
def _search_customfields(query, search_term):
"""Search custom-field VALUES for fields flagged searchable.
@@ -886,6 +1017,8 @@ def global_search():
results.extend(_search_employees(query, search_term))
results.extend(_search_assets(query, search_term))
results.extend(_search_measuringtools(query, search_term))
results.extend(_search_usbdevices(query, search_term))
results.extend(_search_printeditems(query, search_term))
results.extend(_search_customfields(query, search_term))
results.extend(_search_notifications(query, search_term))
results.extend(_search_hostnames(query, search_term))