"""Applications API endpoints.""" from flask import Blueprint, request from flask_jwt_extended import jwt_required from shopdb.extensions import db from shopdb.core.models import ( Application, AppVersion, AppOwner, SupportTeam, InstalledApp, Machine, AuditLog ) from shopdb.utils.responses import ( success_response, error_response, paginated_response, ErrorCodes ) from shopdb.utils.pagination import get_pagination_params, paginate_query applications_bp = Blueprint('applications', __name__) @applications_bp.route('', methods=['GET']) @jwt_required(optional=True) def list_applications(): """List all applications.""" page, per_page = get_pagination_params(request) query = Application.query if request.args.get('active', 'true').lower() != 'false': query = query.filter(Application.isactive == True) # Filter out hidden unless specifically requested if request.args.get('showhidden', 'false').lower() != 'true': query = query.filter(Application.ishidden == False) # Filter by installable if request.args.get('installable') is not None: installable = request.args.get('installable').lower() == 'true' query = query.filter(Application.isinstallable == installable) if search := request.args.get('search'): query = query.filter( db.or_( Application.appname.ilike(f'%{search}%'), Application.appdescription.ilike(f'%{search}%') ) ) query = query.order_by(Application.appname) items, total = paginate_query(query, page, per_page) data = [] for app in items: app_dict = app.to_dict() if app.supportteam: app_dict['supportteam'] = { 'supportteamid': app.supportteam.supportteamid, 'teamname': app.supportteam.teamname, 'teamurl': app.supportteam.teamurl, 'owner': { 'appownerid': app.supportteam.owner.appownerid, 'appowner': app.supportteam.owner.appowner, 'sso': app.supportteam.owner.sso } if app.supportteam.owner else None } else: app_dict['supportteam'] = None app_dict['installedcount'] = app.installed_on.filter_by(isactive=True).count() data.append(app_dict) return paginated_response(data, page, per_page, total) @applications_bp.route('/', methods=['GET']) @jwt_required(optional=True) def get_application(app_id: int): """Get a single application with details.""" app = Application.query.get(app_id) if not app: return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404) data = app.to_dict() if app.supportteam: data['supportteam'] = { 'supportteamid': app.supportteam.supportteamid, 'teamname': app.supportteam.teamname, 'teamurl': app.supportteam.teamurl, 'owner': { 'appownerid': app.supportteam.owner.appownerid, 'appowner': app.supportteam.owner.appowner, 'sso': app.supportteam.owner.sso } if app.supportteam.owner else None } else: data['supportteam'] = None data['versions'] = [v.to_dict() for v in app.versions.filter_by(isactive=True).order_by(AppVersion.version.desc()).all()] data['installedcount'] = app.installed_on.filter_by(isactive=True).count() return success_response(data) @applications_bp.route('', methods=['POST']) @jwt_required() def create_application(): """Create a new application.""" data = request.get_json() if not data or not data.get('appname'): return error_response(ErrorCodes.VALIDATION_ERROR, 'appname is required') if Application.query.filter_by(appname=data['appname']).first(): return error_response( ErrorCodes.CONFLICT, f"Application '{data['appname']}' already exists", http_code=409 ) app = Application( appname=data['appname'], appdescription=data.get('appdescription'), supportteamid=data.get('supportteamid'), isinstallable=data.get('isinstallable', False), applicationnotes=data.get('applicationnotes'), installpath=data.get('installpath'), applicationlink=data.get('applicationlink'), documentationpath=data.get('documentationpath'), ishidden=data.get('ishidden', False), isprinter=data.get('isprinter', False), islicenced=data.get('islicenced', False), image=data.get('image') ) db.session.add(app) db.session.flush() AuditLog.log('created', 'Application', entityid=app.appid, entityname=app.appname) db.session.commit() return success_response(app.to_dict(), message='Application created', http_code=201) @applications_bp.route('/', methods=['PUT']) @jwt_required() def update_application(app_id: int): """Update an application.""" app = Application.query.get(app_id) if not app: return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404) data = request.get_json() if not data: return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided') if 'appname' in data and data['appname'] != app.appname: if Application.query.filter_by(appname=data['appname']).first(): return error_response( ErrorCodes.CONFLICT, f"Application '{data['appname']}' already exists", http_code=409 ) fields = [ 'appname', 'appdescription', 'supportteamid', 'isinstallable', 'applicationnotes', 'installpath', 'applicationlink', 'documentationpath', 'ishidden', 'isprinter', 'islicenced', 'image', 'isactive' ] changes = {} for key in fields: if key in data: old_val = getattr(app, key) new_val = data[key] if old_val != new_val: changes[key] = {'old': old_val, 'new': new_val} setattr(app, key, data[key]) if changes: AuditLog.log('updated', 'Application', entityid=app.appid, entityname=app.appname, changes=changes) db.session.commit() return success_response(app.to_dict(), message='Application updated') @applications_bp.route('/', methods=['DELETE']) @jwt_required() def delete_application(app_id: int): """Delete (deactivate) an application.""" app = Application.query.get(app_id) if not app: return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404) app.isactive = False AuditLog.log('deleted', 'Application', entityid=app.appid, entityname=app.appname) db.session.commit() return success_response(message='Application deleted') # ---- Versions ---- @applications_bp.route('//versions', methods=['GET']) @jwt_required(optional=True) def list_versions(app_id: int): """List all versions for an application.""" app = Application.query.get(app_id) if not app: return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404) versions = app.versions.filter_by(isactive=True).order_by(AppVersion.version.desc()).all() return success_response([v.to_dict() for v in versions]) @applications_bp.route('//versions', methods=['POST']) @jwt_required() def create_version(app_id: int): """Create a new version for an application.""" app = Application.query.get(app_id) if not app: return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404) data = request.get_json() if not data or not data.get('version'): return error_response(ErrorCodes.VALIDATION_ERROR, 'version is required') if AppVersion.query.filter_by(appid=app_id, version=data['version']).first(): return error_response( ErrorCodes.CONFLICT, f"Version '{data['version']}' already exists for this application", http_code=409 ) version = AppVersion( appid=app_id, version=data['version'], releasedate=data.get('releasedate'), notes=data.get('notes') ) db.session.add(version) db.session.commit() return success_response(version.to_dict(), message='Version created', http_code=201) # ---- Machines with this app installed ---- @applications_bp.route('//installed', methods=['GET']) @jwt_required(optional=True) def list_installed_machines(app_id: int): """List all machines that have this application installed.""" app = Application.query.get(app_id) if not app: return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404) installed = app.installed_on.filter_by(isactive=True).all() data = [] for i in installed: item = i.to_dict() if i.machine: item['machine'] = { 'machineid': i.machine.machineid, 'machinenumber': i.machine.machinenumber, 'alias': i.machine.alias, 'hostname': i.machine.hostname } data.append(item) return success_response(data) # ---- Installed Apps (per machine) ---- @applications_bp.route('/machines/', methods=['GET']) @jwt_required(optional=True) def list_machine_applications(machine_id: int): """List all applications installed on a machine.""" machine = Machine.query.get(machine_id) if not machine: return error_response(ErrorCodes.NOT_FOUND, 'Machine not found', http_code=404) installed = machine.installedapps.filter_by(isactive=True).all() return success_response([i.to_dict() for i in installed]) @applications_bp.route('/machines/', methods=['POST']) @jwt_required() def install_application(machine_id: int): """Install an application on a machine.""" machine = Machine.query.get(machine_id) if not machine: return error_response(ErrorCodes.NOT_FOUND, 'Machine 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') app = Application.query.get(data['appid']) if not app: return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404) # Check if already installed existing = InstalledApp.query.filter_by( machineid=machine_id, appid=data['appid'] ).first() if existing: if existing.isactive: return error_response( ErrorCodes.CONFLICT, 'Application already installed on this machine', http_code=409 ) # Reactivate existing.isactive = True existing.appversionid = data.get('appversionid') existing.installeddate = db.func.now() db.session.commit() return success_response(existing.to_dict(), message='Application reinstalled') installed = InstalledApp( machineid=machine_id, appid=data['appid'], appversionid=data.get('appversionid') ) db.session.add(installed) db.session.commit() return success_response(installed.to_dict(), message='Application installed', http_code=201) @applications_bp.route('/machines//', methods=['DELETE']) @jwt_required() def uninstall_application(machine_id: int, app_id: int): """Uninstall an application from a machine.""" installed = InstalledApp.query.filter_by( machineid=machine_id, appid=app_id, isactive=True ).first() if not installed: return error_response(ErrorCodes.NOT_FOUND, 'Application not installed on this machine', http_code=404) installed.isactive = False db.session.commit() return success_response(message='Application uninstalled') @applications_bp.route('/machines//', methods=['PUT']) @jwt_required() def update_installed_app(machine_id: int, app_id: int): """Update installed application (e.g., change version).""" installed = InstalledApp.query.filter_by( machineid=machine_id, appid=app_id, isactive=True ).first() if not installed: return error_response(ErrorCodes.NOT_FOUND, 'Application not installed on this machine', http_code=404) data = request.get_json() if not data: return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided') if 'appversionid' in data: installed.appversionid = data['appversionid'] db.session.commit() return success_response(installed.to_dict(), message='Installation updated') # ---- Support Teams ---- @applications_bp.route('/supportteams', methods=['GET']) @jwt_required(optional=True) def list_support_teams(): """List all support teams.""" teams = SupportTeam.query.filter_by(isactive=True).order_by(SupportTeam.teamname).all() data = [] for team in teams: team_dict = team.to_dict() team_dict['owner'] = team.owner.appowner if team.owner else None data.append(team_dict) return success_response(data) @applications_bp.route('/supportteams', methods=['POST']) @jwt_required() def create_support_team(): """Create a new support team.""" data = request.get_json() if not data or not data.get('teamname'): return error_response(ErrorCodes.VALIDATION_ERROR, 'teamname is required') team = SupportTeam( teamname=data['teamname'], teamurl=data.get('teamurl'), appownerid=data.get('appownerid') ) db.session.add(team) db.session.commit() return success_response(team.to_dict(), message='Support team created', http_code=201) # ---- App Owners ---- @applications_bp.route('/appowners', methods=['GET']) @jwt_required(optional=True) def list_app_owners(): """List all application owners.""" owners = AppOwner.query.filter_by(isactive=True).order_by(AppOwner.appowner).all() return success_response([o.to_dict() for o in owners]) @applications_bp.route('/appowners', methods=['POST']) @jwt_required() def create_app_owner(): """Create a new application owner.""" data = request.get_json() if not data or not data.get('appowner'): return error_response(ErrorCodes.VALIDATION_ERROR, 'appowner is required') owner = AppOwner( appowner=data['appowner'], sso=data.get('sso'), email=data.get('email') ) db.session.add(owner) db.session.commit() return success_response(owner.to_dict(), message='App owner created', http_code=201)