Add collector PC->printer links and searchable custom fields
All checks were successful
CI / backend (push) Successful in 1m24s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s

Collector: the computers collector schema gains defaultprinter and
printers; apply_collector_payload resolves each reported identifier to
a printer asset (windowsname/hostname/sharename/assetnumber/IP,
first-hit case-insensitive) and idempotently syncs relationships -
defaultprinter (directional) for the default, connectedto for the
rest. Collector-created rows are tagged so a re-report archives dropped
links while manual relationships are never touched; unresolved
identifiers warn instead of failing. Both PC and printer detail pages
show the links via the shared relationships card (no frontend change).
GE-Enforce Win32_Printer collection snippet documented.

Searchable custom fields: a per-field searchable flag (migration 7d24);
global search matches custom-field values on flagged active fields and
routes each hit to the asset detail page, reusing the existing
(type,id) dedupe and search_<type>_enabled domain filter. Searchable
toggle on the Custom Fields settings page.

822 tests pass; both verified live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-12 14:01:20 -04:00
parent 9daf1578a7
commit 275224822e
12 changed files with 664 additions and 6 deletions

View File

@@ -12,7 +12,8 @@ from sqlalchemy.orm import joinedload
from shopdb.extensions import db
from shopdb.core.models import (
Application, Setting,
Asset, AssetType, Communication, Vendor, Model
Asset, AssetType, Communication, Vendor, Model,
CustomField, CustomFieldValue
)
from shopdb.core.api.settings import get_cached_settings
from shopdb.utils.responses import success_response
@@ -343,6 +344,41 @@ def _search_measuringtools(query, search_term):
return results
def _search_customfields(query, search_term):
"""Search custom-field VALUES for fields flagged searchable.
Joins CustomFieldValue -> CustomField (searchable + active) -> Asset (active)
and emits a normal asset result routed to that asset's detail page. Results
are deduped against built-in-field asset matches by the shared (type, id) key
in global_search, and the asset's search_<type>_enabled domain toggle is
applied by the same end-of-request domain filter. The matched field label is
put in the subtitle so a value hit reads sensibly.
"""
results = []
try:
rows = db.session.query(Asset, CustomField).join(
CustomFieldValue, CustomFieldValue.assetid == Asset.assetid
).join(
CustomField, CustomField.fieldid == CustomFieldValue.fieldid
).options(
joinedload(Asset.assettype),
joinedload(Asset.location),
).filter(
Asset.isactive == True,
CustomField.searchable == True,
CustomField.isactive == True,
CustomFieldValue.value.ilike(search_term),
).limit(15).all()
for asset, field in rows:
result = _get_asset_result(asset, query, relevance=40)
result['subtitle'] = field.label
results.append(result)
except Exception as e:
logger.error(f"Custom field search failed: {e}")
return results
def _search_by_ip(query, search_term):
"""Search Communications table for IP address matches."""
results = []
@@ -799,6 +835,7 @@ 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_customfields(query, search_term))
results.extend(_search_notifications(query, search_term))
results.extend(_search_hostnames(query, search_term))
results.extend(_search_vendor_model_type(query, search_term))