from quart import Blueprint, jsonify, request, abort, current_app from werkzeug.exceptions import HTTPException from internal.types import System, UserRoles from quart_jwt_extended import jwt_required from routers.auth import role_required systems_bp = Blueprint('systems', __name__) @systems_bp.route("/", methods=['POST']) @jwt_required @role_required(UserRoles.MOD) async def create_system_route(): """API endpoint to create a new system.""" print("\n--- Handling POST /systems ---") try: # In Quart, you need to explicitly get the JSON request body request_data = await request.get_json() if not request_data: abort(400, "Request body must be JSON") # Bad Request if '_id' in request_data: id_search_result = await current_app.sys_db_h.find_system({"_id": request_data["_id"]}) if id_search_result: # If _id is provided and exists, return conflict abort(409, f"System with ID '{request_data['_id']}' already exists") # Check if name exists (optional, depending on requirements) if 'name' in request_data: name_search_result = await current_app.sys_db_h.find_system({"name": request_data["name"]}) if name_search_result: abort(409, f"System with name '{request_data['name']}' already exists") # Check if frequencies exists (optional, depending on requirements) if 'frequencies' in request_data: freq_search_result = await current_app.sys_db_h.find_system({"frequencies": request_data["frequencies"]}) if freq_search_result: abort(409, f"System with frequency '{request_data['frequencies']}' already exists") created_system = await current_app.sys_db_h.create_system(request_data) if created_system: print("Created new system:", created_system) return jsonify(created_system.to_dict()), 201 else: abort(500, "Failed to create system in the database.") except HTTPException: raise except Exception as e: print(f"Error creating system: {e}") # Catch any other unexpected errors abort(500, f"Internal server error: {e}") @systems_bp.route('/', methods=['GET']) @jwt_required @role_required(UserRoles.USER) async def list_systems_route(): """API endpoint to get a list of all systems.""" print("\n--- Handling GET /systems ---") try: all_systems = await current_app.sys_db_h.find_all_systems() return jsonify([system.to_dict() for system in all_systems]), 200 # 200 OK status code except HTTPException: raise except Exception as e: print(f"Error listing systems: {e}") abort(500, f"Internal server error: {e}") @systems_bp.route('/', methods=['GET']) @jwt_required @role_required(UserRoles.USER) async def get_system_route(system_id: str): """API endpoint to get details for a specific system by ID.""" print(f"\n--- Handling GET /systems/{system_id} ---") try: # Fix the query dictionary syntax system = await current_app.sys_db_h.find_system({'_id': system_id}) if system: # system is a System object, jsonify will convert it return jsonify(system.to_dict()), 200 # 200 OK else: # If system is None, it means the document was not found abort(404, f"System with ID '{system_id}' not found") # 404 Not Found except HTTPException: raise except Exception as e: print(f"Error getting system details for ID {system_id}: {e}") abort(500, f"Internal server error: {e}") @systems_bp.route('/client/', methods=['GET']) @jwt_required @role_required(UserRoles.USER) async def get_system_by_client_route(client_id: str): """API endpoint to get details for a specific system by ID.""" print(f"\n--- Handling GET /systems/client/{client_id} ---") try: # Fix the query dictionary syntax systems = await current_app.sys_db_h.find_systems({'avail_on_nodes': client_id}) if systems: # system is a System object, jsonify will convert it return jsonify([system.to_dict() for system in systems]), 200 # 200 OK else: # If system is None, it means the document was not found abort(404, f"Client with ID '{client_id}' not found") # 404 Not Found except HTTPException: raise except Exception as e: print(f"Error getting system details for client ID {client_id}: {e}") abort(500, f"Internal server error: {e}") @systems_bp.route('/', methods=['PUT']) @jwt_required @role_required(UserRoles.MOD) async def update_system_route(system_id: str): try: updated_system_data = await request.get_json() if not updated_system_data: abort(400, "No update data provided.") query = {"_id": system_id} update_system = await current_app.sys_db_h.update_system(query, {"$set": updated_system_data}) if update_system: print("Updated system:", update_system) return jsonify(update_system), 201 else: abort(500, "Failed to update system in the database.") except HTTPException: raise except Exception as e: print(f"Error updating system: {e}") # Catch any other unexpected errors abort(500, f"Internal server error: {e}") @systems_bp.route('/', methods=['DELETE']) @jwt_required @role_required(UserRoles.MOD) async def delete_system_route(system_id: str): try: query = {"_id": system_id} delete_count = await current_app.sys_db_h.delete_system(query) if delete_count is not None: if delete_count > 0: return jsonify({"message": f"Successfully deleted {delete_count} systems(s)."}), 200 else: abort(404, "System not found.") else: abort(500, "Failed to delete System.") except Exception as e: print(f"Error in delete_system_route: {e}") abort(500, f"An internal error occurred: {e}") @systems_bp.route('//assign', methods=['POST']) @jwt_required @role_required(UserRoles.MOD) async def assign_client_to_system_route(system_id: str): """ API endpoint to assign a client ID to a system's available_on_nodes list. Uses MongoDB $addToSet to add the client ID if not already present. Expects JSON body: {"client_id": "..."} """ print(f"\n--- Handling POST /systems/{system_id}/assign ---") try: request_data = await request.get_json() if not request_data or 'client_id' not in request_data: abort(400, "Request body must contain 'client_id'") client_id = request_data['client_id'] if not isinstance(client_id, str) or not client_id: abort(400, "'client_id' must be a non-empty string") # First, check if the system exists existing_system = await current_app.sys_db_h.find_system({"_id": system_id}) if existing_system is None: abort(404, f"System with ID '{system_id}' not found") # Use $addToSet to add the client_id to the avail_on_nodes array # $addToSet only adds the element if it's not already in the array update_query = {"_id": system_id} update_data = {"$addToSet": {"avail_on_nodes": client_id}} update_result = await current_app.sys_db_h.update_system(update_query, update_data) if update_result > 0: print(f"Client '{client_id}' assigned to system '{system_id}'.") status = "client_assigned" else: print(f"Client '{client_id}' was already assigned to system '{system_id}'.") status = "already_assigned" updated_system = await current_app.sys_db_h.find_system({"_id": system_id}) if updated_system: return jsonify({ "status": status, "system": updated_system.to_dict() # Return dict representation }), 200 # 200 OK else: # Should not happen if update_result.matched_count was 1, but handle defensively print(f"Update matched but couldn't fetch updated system {system_id}.") abort(500, "Failed to fetch system state after assignment attempt.") except HTTPException: raise except Exception as e: print(f"Error during system assignment: {e}") abort(500, f"Internal server error: {e}") @systems_bp.route('//dismiss', methods=['POST']) @jwt_required @role_required(UserRoles.MOD) async def dismiss_client_from_system_route(system_id: str): """ API endpoint to dismiss (remove) a client ID from a system's available_on_nodes list. Uses MongoDB $pull to remove the client ID if present. Expects JSON body: {"client_id": "..."} """ print(f"\n--- Handling POST /systems/{system_id}/deassign ---") try: request_data = await request.get_json() if not request_data or 'client_id' not in request_data: abort(400, "Request body must contain 'client_id'") client_id = request_data['client_id'] if not isinstance(client_id, str) or not client_id: abort(400, "'client_id' must be a non-empty string") # First, check if the system exists existing_system = await current_app.sys_db_h.find_system({"_id": system_id}) if existing_system is None: abort(404, f"System with ID '{system_id}' not found") # Use $pull to remove the client_id from the avail_on_nodes array # $pull removes all occurrences of the value update_query = {"_id": system_id} update_data = {"$pull": {"avail_on_nodes": client_id}} update_result = await current_app.sys_db_h.update_system(update_query, update_data) if update_result > 0: print(f"Client '{client_id}' dismissed from system '{system_id}'.") status = "client_deassigned" else: print(f"Client '{client_id}' was not found in avail_on_nodes for system '{system_id}'.") status = "not_assigned" # Note: update_result.matched_count will be 1 even if modified_count is 0 # Optionally fetch the updated document to return its current state updated_system = await current_app.sys_db_h.find_system({"_id": system_id}) if updated_system: return jsonify({ "status": status, "system": updated_system.to_dict() # Return dict representation }), 200 # 200 OK else: # Should not happen if update_result.matched_count was 1, but handle defensively print(f"Update matched but couldn't fetch updated system {system_id}.") abort(500, "Failed to fetch system state after de-assignment attempt.") except HTTPException: raise except Exception as e: print(f"Error during system de-assignment: {e}") abort(500, f"Internal server error: {e}") @systems_bp.route('/search', methods=['GET']) @jwt_required @role_required(UserRoles.MOD) async def search_systems_route(): """ API endpoint to search for systems based on query parameters. Allows searching by 'name', 'frequencies', or any other field present in the System model. Example: /systems/search?name=MySystem&frequencies=1000 """ print("\n--- Handling GET /systems/search ---") try: query_params = dict(request.args) systems = await current_app.sys_db_h.find_systems(query_params) print("Found systems", systems) if systems: # If systems are found, return them as a list of dictionaries return jsonify([system.to_dict() for system in systems]), 200 # 200 OK else: # If no systems match the query, return 404 Not Found return jsonify({"message": "No systems found matching the criteria"}), 404 except HTTPException: raise except Exception as e: print(f"Error searching systems: {e}") abort(500, f"Internal server error: {e}")