Buildings and levels for the floor map, and make every identifier searchable
The map was one picture of one floor. A second floor was added, the blueprint changed size, and machines moved, so a position now records WHICH DRAWING its coordinates belong to. Buildings and levels (ADR-017). Each level owns its blueprint per theme and its own native pixel size; assets.mapx/mapy are pixels of assets.levelid, not of the site. A position whose level is unknown renders "level unknown" and is never drawn on the default level, because a marker on the wrong floor plan looks entirely correct while pointing at the wrong place. Repositioning in bulk: filter by unplaced, needs-review or level, search, place, confirm. Landmark recalibration solves the transform PER AXIS from landmark pairs and never from image dimensions - the canvas grew taller without rescaling, so a dimension-derived scale would stretch Y by 1.57 and be wrong everywhere. It defaults to a dry run, reports what would land off the drawing, snapshots before applying, and clears mapverifiedat because a transform is a guess awaiting review. Snapshots restore, including the level and the review state, and a restore snapshots first so an undo is undoable. Search: gaugelabreference was matched only for measuring tools and maintenancereference was matched nowhere at all, for any asset type, while Settings happily offers both identifiers on machines and PCs. A tag an operator is told to record has to be findable or it is a write-only field. USB devices and printed items were unreachable from search entirely - neither is an asset, so the generic asset search could not see them and no searcher existed; they now match on serial, asset tag, label, bin code and gage-lab tag, honouring isactive, with Settings toggles and result labels to match. The retired-application rule was half a rule: GET /api/knowledgebase hid articles whose topic application is retired while global search still returned them and printed the retired application as the subject. A filter is only real if every path that reaches the row applies it. Contract to 0.20.0 (additive): Asset gained levelid and mapverifiedat, Location gained levelid, and resolve_asset_position returns the levelid belonging to whichever source supplied the coordinates. The five plugins that write a map position are re-pinned. The install-list text format gained levelid as a NINTH field, appended, because the shipped Pascal installer reads fields 0-7 by index. That installer still compiles in one drawing's dimensions and bundles one blueprint, so its map is accurate for the default level only; /api/maplevels is deliberately unauthenticated so it can read both at runtime once rebuilt. Recorded in PRINTER-INSTALLER.md section 6 along with the other known gaps. Migration 7d33 converts an existing single-map site into one building and one default level carrying the old map_* settings, then assigns every placed asset and location to it. Nothing moves on screen. Old settings rows are kept so a rollback still finds them. Verified end to end on MySQL 5.6 from a production-shaped database.
This commit is contained in:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user