"""Global search API endpoint.""" from flask import Blueprint, request from flask_jwt_extended import jwt_required from shopdb.extensions import db from shopdb.core.models import ( Machine, Application, KnowledgeBase, Asset, AssetType ) from shopdb.utils.responses import success_response search_bp = Blueprint('search', __name__) @search_bp.route('', methods=['GET']) @jwt_required(optional=True) def global_search(): """ Global search across multiple entity types. Returns combined results from: - Machines (equipment and PCs) - Applications - Knowledge Base articles - Printers (if available) Results are sorted by relevance score. """ query = request.args.get('q', '').strip() if not query or len(query) < 2: return success_response({ 'results': [], 'query': query, 'message': 'Search query must be at least 2 characters' }) if len(query) > 200: return success_response({ 'results': [], 'query': query[:200], 'message': 'Search query too long' }) results = [] search_term = f'%{query}%' # Search Machines (Equipment and PCs) try: machines = Machine.query.filter( Machine.isactive == True, db.or_( Machine.machinenumber.ilike(search_term), Machine.alias.ilike(search_term), Machine.hostname.ilike(search_term), Machine.serialnumber.ilike(search_term), Machine.notes.ilike(search_term) ) ).limit(10).all() except Exception as e: import logging logging.error(f"Machine search failed: {e}") machines = [] for m in machines: # Determine type: PC, Printer, or Equipment is_pc = m.pctypeid is not None is_printer = m.is_printer # Calculate relevance - exact matches score higher relevance = 15 if m.machinenumber and query.lower() == m.machinenumber.lower(): relevance = 100 elif m.hostname and query.lower() == m.hostname.lower(): relevance = 100 elif m.alias and query.lower() in m.alias.lower(): relevance = 50 display_name = m.hostname if is_pc and m.hostname else m.machinenumber if m.alias and not is_pc: display_name = f"{m.machinenumber} ({m.alias})" # Determine result type and URL if is_printer: result_type = 'printer' url = f"/printers/{m.machineid}" elif is_pc: result_type = 'pc' url = f"/pcs/{m.machineid}" else: result_type = 'machine' url = f"/machines/{m.machineid}" # Get location - prefer machine's own location, fall back to parent machine's location location_name = None if m.location: location_name = m.location.locationname elif m.parent_relationships: # Check parent machines for location for rel in m.parent_relationships: if rel.parent_machine and rel.parent_machine.location: location_name = rel.parent_machine.location.locationname break # Get machinetype from model (single source of truth) mt = m.derived_machinetype results.append({ 'type': result_type, 'id': m.machineid, 'title': display_name, 'subtitle': mt.machinetype if mt else None, 'location': location_name, 'url': url, 'relevance': relevance }) # Search Applications try: apps = Application.query.filter( Application.isactive == True, db.or_( Application.appname.ilike(search_term), Application.appdescription.ilike(search_term) ) ).limit(10).all() for app in apps: relevance = 20 if query.lower() == app.appname.lower(): relevance = 100 elif query.lower() in app.appname.lower(): relevance = 50 results.append({ 'type': 'application', 'id': app.appid, 'title': app.appname, 'subtitle': app.appdescription[:100] if app.appdescription else None, 'url': f"/applications/{app.appid}", 'relevance': relevance }) except Exception as e: import logging logging.error(f"Application search failed: {e}") # Search Knowledge Base try: kb_articles = KnowledgeBase.query.filter( KnowledgeBase.isactive == True, db.or_( KnowledgeBase.shortdescription.ilike(search_term), KnowledgeBase.keywords.ilike(search_term) ) ).limit(20).all() for kb in kb_articles: # Weight by clicks and keyword match relevance = 10 + (kb.clicks or 0) * 0.1 if kb.keywords and query.lower() in kb.keywords.lower(): relevance += 15 results.append({ 'type': 'knowledgebase', 'id': kb.linkid, 'title': kb.shortdescription, 'subtitle': kb.application.appname if kb.application else None, 'url': f"/knowledgebase/{kb.linkid}", 'linkurl': kb.linkurl, 'relevance': relevance }) except Exception as e: import logging logging.error(f"KnowledgeBase search failed: {e}") # Search Printers (check if printers model exists) try: from shopdb.plugins.printers.models import Printer printers = Printer.query.filter( Printer.isactive == True, db.or_( Printer.printercsfname.ilike(search_term), Printer.printerwindowsname.ilike(search_term), Printer.serialnumber.ilike(search_term), Printer.fqdn.ilike(search_term) ) ).limit(10).all() for p in printers: relevance = 15 if p.printercsfname and query.lower() == p.printercsfname.lower(): relevance = 100 display_name = p.printercsfname or p.printerwindowsname or f"Printer #{p.printerid}" results.append({ 'type': 'printer', 'id': p.printerid, 'title': display_name, 'subtitle': p.printerwindowsname if p.printercsfname else None, 'url': f"/printers/{p.printerid}", 'relevance': relevance }) except Exception as e: import logging logging.error(f"Printer search failed: {e}") # Search Employees (separate database) try: import pymysql emp_conn = pymysql.connect( host='localhost', user='root', password='rootpassword', database='wjf_employees', cursorclass=pymysql.cursors.DictCursor ) with emp_conn.cursor() as cur: cur.execute(''' SELECT SSO, First_Name, Last_Name, Team, Role FROM employees WHERE First_Name LIKE %s OR Last_Name LIKE %s OR CAST(SSO AS CHAR) LIKE %s ORDER BY Last_Name, First_Name LIMIT 10 ''', (search_term, search_term, search_term)) employees = cur.fetchall() emp_conn.close() for emp in employees: full_name = f"{emp['First_Name'].strip()} {emp['Last_Name'].strip()}" sso_str = str(emp['SSO']) # Calculate relevance relevance = 20 if query == sso_str: relevance = 100 elif query.lower() == full_name.lower(): relevance = 95 elif query.lower() in full_name.lower(): relevance = 60 results.append({ 'type': 'employee', 'id': emp['SSO'], 'title': full_name, 'subtitle': emp.get('Team') or emp.get('Role') or f"SSO: {sso_str}", 'url': f"/employees/{emp['SSO']}", 'relevance': relevance }) except Exception as e: import logging logging.error(f"Employee search failed: {e}") # Search unified Assets table try: assets = Asset.query.join(AssetType).filter( Asset.isactive == True, db.or_( Asset.assetnumber.ilike(search_term), Asset.name.ilike(search_term), Asset.serialnumber.ilike(search_term), Asset.notes.ilike(search_term) ) ).limit(10).all() for asset in assets: # Calculate relevance relevance = 15 if asset.assetnumber and query.lower() == asset.assetnumber.lower(): relevance = 100 elif asset.name and query.lower() == asset.name.lower(): relevance = 90 elif asset.serialnumber and query.lower() == asset.serialnumber.lower(): relevance = 85 elif asset.name and query.lower() in asset.name.lower(): relevance = 50 # Determine URL and type based on asset type asset_type_name = asset.assettype.assettype if asset.assettype else 'asset' url_map = { 'equipment': f"/equipment/{asset.assetid}", 'computer': f"/pcs/{asset.assetid}", 'network_device': f"/network/{asset.assetid}", 'printer': f"/printers/{asset.assetid}", } url = url_map.get(asset_type_name, f"/assets/{asset.assetid}") display_name = asset.display_name subtitle = None if asset.name and asset.assetnumber != asset.name: subtitle = asset.assetnumber # Get location name location_name = asset.location.locationname if asset.location else None results.append({ 'type': asset_type_name, 'id': asset.assetid, 'title': display_name, 'subtitle': subtitle, 'location': location_name, 'url': url, 'relevance': relevance }) except Exception as e: import logging logging.error(f"Asset search failed: {e}") # Sort by relevance (highest first) results.sort(key=lambda x: x['relevance'], reverse=True) # Remove duplicates (prefer higher relevance) seen_ids = {} unique_results = [] for r in results: key = (r['type'], r['id']) if key not in seen_ids: seen_ids[key] = True unique_results.append(r) # Limit total results unique_results = unique_results[:30] return success_response({ 'results': unique_results, 'query': query, 'total': len(unique_results) })