"""Computers plugin API endpoints.""" from flask import Blueprint, request from flask_jwt_extended import jwt_required from shopdb.extensions import db from shopdb.core.models import Asset, AssetType, OperatingSystem, Application, AppVersion from shopdb.utils.responses import ( success_response, error_response, paginated_response, ErrorCodes ) from shopdb.utils.pagination import get_pagination_params, paginate_query from ..models import Computer, ComputerType, ComputerInstalledApp computers_bp = Blueprint('computers', __name__) # ============================================================================= # Computer Types # ============================================================================= @computers_bp.route('/types', methods=['GET']) @jwt_required() def list_computer_types(): """List all computer types.""" page, per_page = get_pagination_params(request) query = ComputerType.query if request.args.get('active', 'true').lower() != 'false': query = query.filter(ComputerType.isactive == True) if search := request.args.get('search'): query = query.filter(ComputerType.computertype.ilike(f'%{search}%')) query = query.order_by(ComputerType.computertype) items, total = paginate_query(query, page, per_page) data = [t.to_dict() for t in items] return paginated_response(data, page, per_page, total) @computers_bp.route('/types/', methods=['GET']) @jwt_required() def get_computer_type(type_id: int): """Get a single computer type.""" t = ComputerType.query.get(type_id) if not t: return error_response( ErrorCodes.NOT_FOUND, f'Computer type with ID {type_id} not found', http_code=404 ) return success_response(t.to_dict()) @computers_bp.route('/types', methods=['POST']) @jwt_required() def create_computer_type(): """Create a new computer type.""" data = request.get_json() if not data or not data.get('computertype'): return error_response(ErrorCodes.VALIDATION_ERROR, 'computertype is required') if ComputerType.query.filter_by(computertype=data['computertype']).first(): return error_response( ErrorCodes.CONFLICT, f"Computer type '{data['computertype']}' already exists", http_code=409 ) t = ComputerType( computertype=data['computertype'], description=data.get('description'), icon=data.get('icon') ) db.session.add(t) db.session.commit() return success_response(t.to_dict(), message='Computer type created', http_code=201) @computers_bp.route('/types/', methods=['PUT']) @jwt_required() def update_computer_type(type_id: int): """Update a computer type.""" t = ComputerType.query.get(type_id) if not t: return error_response( ErrorCodes.NOT_FOUND, f'Computer type with ID {type_id} not found', http_code=404 ) data = request.get_json() if not data: return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided') if 'computertype' in data and data['computertype'] != t.computertype: if ComputerType.query.filter_by(computertype=data['computertype']).first(): return error_response( ErrorCodes.CONFLICT, f"Computer type '{data['computertype']}' already exists", http_code=409 ) for key in ['computertype', 'description', 'icon', 'isactive']: if key in data: setattr(t, key, data[key]) db.session.commit() return success_response(t.to_dict(), message='Computer type updated') # ============================================================================= # Computers CRUD # ============================================================================= @computers_bp.route('', methods=['GET']) @jwt_required() def list_computers(): """ List all computers with filtering and pagination. Query parameters: - page, per_page: Pagination - active: Filter by active status - search: Search by asset number, name, or hostname - type_id: Filter by computer type ID - os_id: Filter by operating system ID - location_id: Filter by location ID - businessunit_id: Filter by business unit ID - shopfloor: Filter by shopfloor flag (true/false) """ page, per_page = get_pagination_params(request) # Join Computer with Asset query = db.session.query(Computer).join(Asset) # Active filter if request.args.get('active', 'true').lower() != 'false': query = query.filter(Asset.isactive == True) # Search filter if search := request.args.get('search'): query = query.filter( db.or_( Asset.assetnumber.ilike(f'%{search}%'), Asset.name.ilike(f'%{search}%'), Asset.serialnumber.ilike(f'%{search}%'), Computer.hostname.ilike(f'%{search}%') ) ) # Computer type filter if type_id := request.args.get('type_id'): query = query.filter(Computer.computertypeid == int(type_id)) # OS filter if os_id := request.args.get('os_id'): query = query.filter(Computer.osid == int(os_id)) # Location filter if location_id := request.args.get('location_id'): query = query.filter(Asset.locationid == int(location_id)) # Business unit filter if bu_id := request.args.get('businessunit_id'): query = query.filter(Asset.businessunitid == int(bu_id)) # Shopfloor filter if shopfloor := request.args.get('shopfloor'): query = query.filter(Computer.isshopfloor == (shopfloor.lower() == 'true')) # Sorting sort_by = request.args.get('sort', 'hostname') sort_dir = request.args.get('dir', 'asc') if sort_by == 'hostname': col = Computer.hostname elif sort_by == 'assetnumber': col = Asset.assetnumber elif sort_by == 'name': col = Asset.name elif sort_by == 'lastreporteddate': col = Computer.lastreporteddate else: col = Computer.hostname query = query.order_by(col.desc() if sort_dir == 'desc' else col) items, total = paginate_query(query, page, per_page) # Build response with both asset and computer data data = [] for comp in items: item = comp.asset.to_dict() if comp.asset else {} item['computer'] = comp.to_dict() data.append(item) return paginated_response(data, page, per_page, total) @computers_bp.route('/', methods=['GET']) @jwt_required() def get_computer(computer_id: int): """Get a single computer with full details.""" comp = Computer.query.get(computer_id) if not comp: return error_response( ErrorCodes.NOT_FOUND, f'Computer with ID {computer_id} not found', http_code=404 ) result = comp.asset.to_dict() if comp.asset else {} result['computer'] = comp.to_dict() return success_response(result) @computers_bp.route('/by-asset/', methods=['GET']) @jwt_required() def get_computer_by_asset(asset_id: int): """Get computer data by asset ID.""" comp = Computer.query.filter_by(assetid=asset_id).first() if not comp: return error_response( ErrorCodes.NOT_FOUND, f'Computer for asset {asset_id} not found', http_code=404 ) result = comp.asset.to_dict() if comp.asset else {} result['computer'] = comp.to_dict() return success_response(result) @computers_bp.route('/by-hostname/', methods=['GET']) @jwt_required() def get_computer_by_hostname(hostname: str): """Get computer by hostname.""" comp = Computer.query.filter_by(hostname=hostname).first() if not comp: return error_response( ErrorCodes.NOT_FOUND, f'Computer with hostname {hostname} not found', http_code=404 ) result = comp.asset.to_dict() if comp.asset else {} result['computer'] = comp.to_dict() return success_response(result) @computers_bp.route('', methods=['POST']) @jwt_required() def create_computer(): """ Create new computer (creates both Asset and Computer records). Required fields: - assetnumber: Business identifier Optional fields: - name, serialnumber, statusid, locationid, businessunitid - computertypeid, hostname, osid - isvnc, iswinrm, isshopfloor - mapleft, maptop, notes """ data = request.get_json() if not data: return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided') if not data.get('assetnumber'): return error_response(ErrorCodes.VALIDATION_ERROR, 'assetnumber is required') # Check for duplicate assetnumber if Asset.query.filter_by(assetnumber=data['assetnumber']).first(): return error_response( ErrorCodes.CONFLICT, f"Asset with number '{data['assetnumber']}' already exists", http_code=409 ) # Check for duplicate hostname if data.get('hostname'): if Computer.query.filter_by(hostname=data['hostname']).first(): return error_response( ErrorCodes.CONFLICT, f"Computer with hostname '{data['hostname']}' already exists", http_code=409 ) # Get computer asset type computer_type = AssetType.query.filter_by(assettype='computer').first() if not computer_type: return error_response( ErrorCodes.INTERNAL_ERROR, 'Computer asset type not found. Plugin may not be properly installed.', http_code=500 ) # Create the core asset asset = Asset( assetnumber=data['assetnumber'], name=data.get('name'), serialnumber=data.get('serialnumber'), assettypeid=computer_type.assettypeid, statusid=data.get('statusid', 1), locationid=data.get('locationid'), businessunitid=data.get('businessunitid'), mapleft=data.get('mapleft'), maptop=data.get('maptop'), notes=data.get('notes') ) db.session.add(asset) db.session.flush() # Get the assetid # Create the computer extension comp = Computer( assetid=asset.assetid, computertypeid=data.get('computertypeid'), hostname=data.get('hostname'), osid=data.get('osid'), loggedinuser=data.get('loggedinuser'), lastreporteddate=data.get('lastreporteddate'), lastboottime=data.get('lastboottime'), isvnc=data.get('isvnc', False), iswinrm=data.get('iswinrm', False), isshopfloor=data.get('isshopfloor', False) ) db.session.add(comp) db.session.commit() result = asset.to_dict() result['computer'] = comp.to_dict() return success_response(result, message='Computer created', http_code=201) @computers_bp.route('/', methods=['PUT']) @jwt_required() def update_computer(computer_id: int): """Update computer (both Asset and Computer records).""" comp = Computer.query.get(computer_id) if not comp: return error_response( ErrorCodes.NOT_FOUND, f'Computer with ID {computer_id} not found', http_code=404 ) data = request.get_json() if not data: return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided') asset = comp.asset # Check for conflicting assetnumber if 'assetnumber' in data and data['assetnumber'] != asset.assetnumber: if Asset.query.filter_by(assetnumber=data['assetnumber']).first(): return error_response( ErrorCodes.CONFLICT, f"Asset with number '{data['assetnumber']}' already exists", http_code=409 ) # Check for conflicting hostname if 'hostname' in data and data['hostname'] != comp.hostname: existing = Computer.query.filter_by(hostname=data['hostname']).first() if existing and existing.computerid != computer_id: return error_response( ErrorCodes.CONFLICT, f"Computer with hostname '{data['hostname']}' already exists", http_code=409 ) # Update asset fields asset_fields = ['assetnumber', 'name', 'serialnumber', 'statusid', 'locationid', 'businessunitid', 'mapleft', 'maptop', 'notes', 'isactive'] for key in asset_fields: if key in data: setattr(asset, key, data[key]) # Update computer fields computer_fields = ['computertypeid', 'hostname', 'osid', 'loggedinuser', 'lastreporteddate', 'lastboottime', 'isvnc', 'iswinrm', 'isshopfloor'] for key in computer_fields: if key in data: setattr(comp, key, data[key]) db.session.commit() result = asset.to_dict() result['computer'] = comp.to_dict() return success_response(result, message='Computer updated') @computers_bp.route('/', methods=['DELETE']) @jwt_required() def delete_computer(computer_id: int): """Delete (soft delete) computer.""" comp = Computer.query.get(computer_id) if not comp: return error_response( ErrorCodes.NOT_FOUND, f'Computer with ID {computer_id} not found', http_code=404 ) # Soft delete the asset comp.asset.isactive = False db.session.commit() return success_response(message='Computer deleted') # ============================================================================= # Installed Applications # ============================================================================= @computers_bp.route('//apps', methods=['GET']) @jwt_required() def get_installed_apps(computer_id: int): """Get all installed applications for a computer.""" comp = Computer.query.get(computer_id) if not comp: return error_response( ErrorCodes.NOT_FOUND, f'Computer with ID {computer_id} not found', http_code=404 ) apps = ComputerInstalledApp.query.filter_by( computerid=computer_id, isactive=True ).all() data = [app.to_dict() for app in apps] return success_response(data) @computers_bp.route('//apps', methods=['POST']) @jwt_required() def add_installed_app(computer_id: int): """Add an installed application to a computer.""" comp = Computer.query.get(computer_id) if not comp: return error_response( ErrorCodes.NOT_FOUND, f'Computer with ID {computer_id} not found', http_code=404 ) data = request.get_json() if not data or not data.get('appid'): return error_response(ErrorCodes.VALIDATION_ERROR, 'appid is required') appid = data['appid'] # Validate app exists if not Application.query.get(appid): return error_response(ErrorCodes.NOT_FOUND, f'Application {appid} not found', http_code=404) # Check for duplicate existing = ComputerInstalledApp.query.filter_by( computerid=computer_id, appid=appid ).first() if existing: if existing.isactive: return error_response( ErrorCodes.CONFLICT, 'This application is already installed on this computer', http_code=409 ) else: # Reactivate existing.isactive = True existing.appversionid = data.get('appversionid') db.session.commit() return success_response(existing.to_dict(), message='Application reinstalled') # Create new installation record installed = ComputerInstalledApp( computerid=computer_id, appid=appid, appversionid=data.get('appversionid') ) db.session.add(installed) db.session.commit() return success_response(installed.to_dict(), message='Application installed', http_code=201) @computers_bp.route('//apps/', methods=['DELETE']) @jwt_required() def remove_installed_app(computer_id: int, app_id: int): """Remove an installed application from a computer.""" installed = ComputerInstalledApp.query.filter_by( computerid=computer_id, appid=app_id, isactive=True ).first() if not installed: return error_response( ErrorCodes.NOT_FOUND, 'Installation record not found', http_code=404 ) installed.isactive = False db.session.commit() return success_response(message='Application uninstalled') # ============================================================================= # Status Reporting # ============================================================================= @computers_bp.route('//report', methods=['POST']) @jwt_required(optional=True) def report_status(computer_id: int): """ Report computer status (for agent-based reporting). This endpoint can be called periodically by a client agent to update status information. """ comp = Computer.query.get(computer_id) if not comp: return error_response( ErrorCodes.NOT_FOUND, f'Computer with ID {computer_id} not found', http_code=404 ) data = request.get_json() or {} # Update status fields from datetime import datetime comp.lastreporteddate = datetime.utcnow() if 'loggedinuser' in data: comp.loggedinuser = data['loggedinuser'] if 'lastboottime' in data: comp.lastboottime = data['lastboottime'] db.session.commit() return success_response(message='Status reported') # ============================================================================= # Dashboard # ============================================================================= @computers_bp.route('/dashboard/summary', methods=['GET']) @jwt_required() def dashboard_summary(): """Get computer dashboard summary data.""" # Total active computers total = db.session.query(Computer).join(Asset).filter( Asset.isactive == True ).count() # Count by computer type by_type = db.session.query( ComputerType.computertype, db.func.count(Computer.computerid) ).join(Computer, Computer.computertypeid == ComputerType.computertypeid ).join(Asset, Asset.assetid == Computer.assetid ).filter(Asset.isactive == True ).group_by(ComputerType.computertype ).all() # Count by OS by_os = db.session.query( OperatingSystem.osname, db.func.count(Computer.computerid) ).join(Computer, Computer.osid == OperatingSystem.osid ).join(Asset, Asset.assetid == Computer.assetid ).filter(Asset.isactive == True ).group_by(OperatingSystem.osname ).all() # Count shopfloor vs non-shopfloor shopfloor_count = db.session.query(Computer).join(Asset).filter( Asset.isactive == True, Computer.isshopfloor == True ).count() return success_response({ 'total': total, 'by_type': [{'type': t, 'count': c} for t, c in by_type], 'by_os': [{'os': o, 'count': c} for o, c in by_os], 'shopfloor': shopfloor_count, 'non_shopfloor': total - shopfloor_count })