Files
HangmanLab.Backend/api/path.py

315 lines
11 KiB
Python

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('/<int:path_id>', 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/<int:parent_id>', 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('/<int:path_id>', 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('/<int:path_id>', 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('/<int:path_id>', 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/<int:path_id>', 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/<int:path_id>', 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