from flask import Blueprint, request, jsonify import api from api import require_auth, etag_response from db import get_db from db.models.Markdown import Markdown from db.models.Path import Path from api import limiter import logging from events import path_created, path_updated, path_deleted logger = logging.getLogger(__name__) path_bp = Blueprint('path', __name__, url_prefix='/api/path') @path_bp.route('/', methods=['GET']) @limiter.limit(api.get_rate_limit) @etag_response def get_root_paths(): """ Get all paths under the root path. This endpoint retrieves a list of all paths that are direct children of the root path (parent_id=1). Returns: A JSON array containing all root paths. Response Codes: - 200: Success """ with get_db() as session: paths = session.query(Path).filter(Path.parent_id == 1) return jsonify([pth.to_dict() for pth in paths]), 200 @path_bp.route('/', methods=['GET']) @limiter.limit(api.get_rate_limit) @etag_response def get_path(path_id): """ Get a specific path by ID. This endpoint retrieves a path by its ID. Request: - path_id (int): The ID of the path to retrieve Returns: A JSON object containing the path. Response Codes: - 200: Success - 404: Path not found """ with get_db() as session: path = session.query(Path).get(path_id) if path is None: return jsonify({"error": "file not found"}), 404 return jsonify(path.to_dict()), 200 @path_bp.route('/parent/', methods=['GET']) @limiter.limit(api.get_rate_limit) @etag_response def get_path_by_parent(parent_id): """ Get all paths under a specific parent path. This endpoint retrieves a list of all paths that are direct children of the specified parent path. Request: - parent_id (int): The ID of the parent path Returns: A JSON array containing all child paths of the specified parent. Response Codes: - 200: Success """ with get_db() as session: paths = session.query(Path).filter(Path.parent_id == parent_id).all() return jsonify([pth.to_dict() for pth in paths]), 200 @path_bp.route('/', methods=['POST']) @limiter.limit(api.get_rate_limit) @require_auth(roles=['admin', 'creator']) def create_path(): """ Create a new path. This endpoint creates a new path with the provided name and parent ID. It requires authentication with either 'admin' or 'creator' role. Request: - name (str): The name of the path - parent_id (int): The ID of the parent path Returns: A JSON object containing the created path. Response Codes: - 201: Created successfully - 400: Bad request (missing required fields) - 404: Parent path not found - 409: Conflict (path already exists under the parent) """ data = request.json if not data or 'name' not in data or 'parent_id' not in data: return jsonify({"error": "bad request"}), 400 with get_db() as session: if data['parent_id'] != 1 and not session.query(Path).get(data['parent_id']): return jsonify({"error": "path not found"}), 404 if session.query(Path).filter_by(name=data['name'], parent_id=data['parent_id']).first(): return jsonify({"error": "Path already exists under the parent"}), 409 new_path = Path(name=data['name'], parent_id=data['parent_id']) session.add(new_path) session.commit() path_created.send(None, payload=new_path.to_dict()) return jsonify(new_path.to_dict()), 201 @path_bp.route('/', methods=['PUT']) @limiter.limit(api.get_rate_limit) @require_auth(roles=['admin']) def update_path(path_id): """ Update a path. This endpoint updates an existing path with the provided name and parent ID. It requires authentication with the 'admin' role. Request: - path_id (int): The ID of the path to update - name (str): The new name for the path - parent_id (int): The new parent ID for the path Returns: A JSON object containing the updated path. Response Codes: - 200: Updated successfully - 400: Bad request (missing required fields) - 404: Path not found - 409: Conflict (path already exists under the parent) """ data = request.json if not data or 'name' not in data or 'parent_id' not in data: return jsonify({"error": "bad request"}), 400 with get_db() as session: path = session.query(Path).get(path_id) if path is None: return jsonify({"error": "path not found"}), 404 if session.query(Path).filter_by(name=data['name'], parent_id=data['parent_id']).first(): return jsonify({"error": "Path already exists under the parent"}), 409 path.name = data['name'] path.parent_id = data['parent_id'] session.commit() path_updated.send(None, payload=path.to_dict()) return jsonify(path.to_dict()), 200 @path_bp.route('/', methods=['PATCH']) @limiter.limit(api.get_rate_limit) @require_auth(roles=['admin']) def patch_path(path_id): """ Partially update a path. This endpoint partially updates an existing path with the provided data. Unlike the PUT method, this endpoint only updates the fields that are provided in the request. It requires authentication with the 'admin' role. Request: - path_id (int): The ID of the path to update - name (str, optional): The new name for the path - parent_id (int, optional): The new parent ID for the path - setting_id (int, optional): The new setting ID for the path Returns: A JSON object containing the updated path. Response Codes: - 200: Updated successfully - 400: Bad request (empty data) - 404: Path not found - 409: Conflict (path already exists under the parent) """ data = request.json if not data: return jsonify({"error": "bad request"}), 400 with get_db() as session: path = session.query(Path).get(path_id) if path is None: return jsonify({"error": "path not found"}), 404 updated_name =data.get('name', path.name) updated_parent_id = data.get('parent_id', path.parent_id) updated_setting_id = data.get('setting_id', path.setting_id) if session.query(Path).filter(Path.name==updated_name, Path.parent_id==updated_parent_id, Path.id != path_id).first(): return jsonify({"error": "Path already exists under the parent"}), 409 path.name = updated_name path.parent_id = updated_parent_id path.setting_id = updated_setting_id session.commit() path_updated.send(None, payload=path.to_dict()) return jsonify(path.to_dict()), 200 @path_bp.route('/', methods=['DELETE']) @limiter.limit(api.get_rate_limit) @require_auth(roles=['admin']) def delete_path(path_id): """ Delete a path. This endpoint deletes an existing path. The path must be empty (no child paths or markdowns) to be deleted. It requires authentication with the 'admin' role. Request: - path_id (int): The ID of the path to delete Returns: A JSON object with a success message. Response Codes: - 200: Deleted successfully - 404: Path not found - 409: Conflict (path contains child paths or markdowns) """ with get_db() as session: path = session.query(Path).get(path_id) if not path: return jsonify({"error": "path not found"}), 404 if session.query(Path).filter_by(parent_id=path_id).first(): return jsonify({"error": "can not delete non empty path"}), 409 if session.query(Markdown).filter_by(path_id=path_id).first(): return jsonify({"error": "can not delete non empty path"}), 409 pth = path.to_dict() session.delete(path) session.commit() path_deleted.send(None, payload=pth) return jsonify({"message": "path deleted"}), 200 @path_bp.route('/move_forward/', methods=['PATCH']) @require_auth(roles=['admin']) @limiter.limit(api.get_rate_limit) def move_forward(path_id): """ Move a path forward in display order. This endpoint moves a path one position forward in the display order by swapping its order value with the previous path in the same parent. This affects how paths are displayed in the UI. It requires authentication with the 'admin' role. Request: - path_id (int): The ID of the path to move forward Returns: A JSON object containing the updated path. Response Codes: - 200: Moved successfully - 400: Bad request (already at the first position) - 404: Path not found """ with get_db() as session: path = session.query(Path).get(path_id) if not path: return jsonify({"error": "file not found"}), 404 siblings = session.query(Path).filter(Path.parent_id == path.parent_id).order_by(Path.order).all() current_index = siblings.index(path) if current_index == 0: return jsonify({"error": "already at the first position"}), 400 previous_path = siblings[current_index - 1] path.order, previous_path.order = previous_path.order, path.order session.commit() path_updated.send(None, payload=path.to_dict()) return jsonify(path.to_dict()), 200 @path_bp.route('/move_backward/', methods=['PATCH']) @require_auth(roles=['admin']) @limiter.limit(api.get_rate_limit) def move_backward(path_id): """ Move a path backward in display order. This endpoint moves a path one position backward in the display order by swapping its order value with the next path in the same parent. This affects how paths are displayed in the UI. It requires authentication with the 'admin' role. Request: - path_id (int): The ID of the path to move backward Returns: A JSON object containing the updated path. Response Codes: - 200: Moved successfully - 400: Bad request (already at the last position) - 404: Path not found """ with get_db() as session: path = session.query(Path).get(path_id) if not path: return jsonify({"error": "file not found"}), 404 siblings = session.query(Path).filter(Path.parent_id == path.parent_id).order_by(Path.order).all() current_index = siblings.index(path) if current_index == len(siblings) - 1: return jsonify({"error": "already at the last position"}), 400 next_path = siblings[current_index + 1] path.order, next_path.order = next_path.order, path.order session.commit() path_updated.send(None, payload=path.to_dict()) return jsonify(path.to_dict()), 200