Add collector PC->printer links and searchable custom fields
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:
@@ -244,13 +244,18 @@ def generic_collect(pluginname):
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
return success_response({
|
||||
response = {
|
||||
'status': 'ok',
|
||||
'action': action,
|
||||
'assetid': outcome.get('assetid'),
|
||||
'identityvalue': outcome.get('identityvalue', identityvalue),
|
||||
'warnings': outcome.get('warnings', []),
|
||||
}, message=f'{pluginname} collector {action}')
|
||||
}
|
||||
# Generic passthrough: a plugin may surface extra result fields (e.g. the
|
||||
# computers plugin returns printerlinks). Known keys win over extras.
|
||||
for key, value in (outcome.get('extra') or {}).items():
|
||||
response.setdefault(key, value)
|
||||
return success_response(response, message=f'{pluginname} collector {action}')
|
||||
|
||||
|
||||
@collector_bp.route('/_schemas', methods=['GET'])
|
||||
|
||||
@@ -87,6 +87,7 @@ def create_field():
|
||||
existing.options = _normalize_options(data.get('options'))
|
||||
existing.showondetail = bool(data.get('showondetail', True))
|
||||
existing.showonform = bool(data.get('showonform', True))
|
||||
existing.searchable = bool(data.get('searchable', False))
|
||||
existing.sortorder = data.get('sortorder', 0)
|
||||
db.session.commit()
|
||||
return success_response(existing.to_dict(), message='Reactivated existing field')
|
||||
@@ -101,6 +102,7 @@ def create_field():
|
||||
options=_normalize_options(data.get('options')),
|
||||
showondetail=bool(data.get('showondetail', True)),
|
||||
showonform=bool(data.get('showonform', True)),
|
||||
searchable=bool(data.get('searchable', False)),
|
||||
sortorder=data.get('sortorder', 0),
|
||||
)
|
||||
db.session.add(field)
|
||||
@@ -124,7 +126,7 @@ def update_field(fieldid):
|
||||
field.datatype = data['datatype']
|
||||
if 'options' in data:
|
||||
field.options = _normalize_options(data['options'])
|
||||
for key in ('showondetail', 'showonform', 'isactive'):
|
||||
for key in ('showondetail', 'showonform', 'isactive', 'searchable'):
|
||||
if key in data:
|
||||
setattr(field, key, bool(data[key]))
|
||||
if 'sortorder' in data:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -33,6 +33,8 @@ class CustomField(db.Model):
|
||||
showonform = db.Column(db.Boolean, nullable=False, server_default='1')
|
||||
sortorder = db.Column(db.Integer, nullable=False, server_default='0')
|
||||
isactive = db.Column(db.Boolean, nullable=False, server_default='1')
|
||||
# When true, this field's values are matched by global search.
|
||||
searchable = db.Column(db.Boolean, nullable=False, server_default='0')
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('assettypeid', 'fieldkey', name='uq_customfield_type_key'),
|
||||
@@ -57,6 +59,7 @@ class CustomField(db.Model):
|
||||
'showonform': bool(self.showonform),
|
||||
'sortorder': self.sortorder,
|
||||
'isactive': bool(self.isactive),
|
||||
'searchable': bool(self.searchable),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user