diff --git a/app/__init__.py b/app/__init__.py index fd92061..1f27be3 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -34,14 +34,23 @@ def create_app(): # 注册蓝图 from app.routes import main_bp from app.routes.templates import templates_bp + from app.routes.goals import goals_bp app.register_blueprint(main_bp) app.register_blueprint(templates_bp) + app.register_blueprint(goals_bp) + from app.routes import student_goals + app.register_blueprint(student_goals.student_goals_bp) # 创建数据库和目录 with app.app_context(): os.makedirs(os.path.join(BASE_DIR, "data"), exist_ok=True) os.makedirs(app.config["PDF_OUTPUT_DIR"], exist_ok=True) + + # 确保所有模型都被导入 + from app.models import Student, Problem, StudentProblem, Template, PracticePlan + from app.models import Goal, GoalRelation, StudentGoal # 新增目标相关模型 + db.create_all() # 简单迁移:为已存在的数据库添加新字段(必须在init_default_templates之前) diff --git a/app/routes/__init__.py b/app/routes/__init__.py index a3be51b..3d933f0 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -5,4 +5,4 @@ from flask import Blueprint main_bp = Blueprint("main", __name__) # 导入各路由模块 -from app.routes import students, plans, problems, settings, auth, classes, users, backup +from app.routes import students, plans, problems, settings, auth, classes, users, backup, goals diff --git a/app/routes/goals.py b/app/routes/goals.py new file mode 100644 index 0000000..14afb6d --- /dev/null +++ b/app/routes/goals.py @@ -0,0 +1,122 @@ +from flask import Blueprint, request, jsonify, render_template +from app.models import db, Goal +from app.routes.auth import login_required_json, admin_required +from app.routes import main_bp + +goals_bp = Blueprint("goals", __name__) + +# 目标管理页面路由 +@main_bp.route("/goals") +@admin_required +def goals_page(): + """目标管理页面""" + return render_template("goals.html", active_nav="goals") + +@goals_bp.route("/api/goals", methods=["GET"]) +@login_required_json +def get_goals(): + goals = Goal.query.order_by(Goal.created_at.desc()).all() + return jsonify([g.to_dict() for g in goals]) + +@goals_bp.route("/api/goals", methods=["POST"]) +@login_required_json +def create_goal(): + data = request.get_json() + goal = Goal(name=data["name"], content=data.get("content", "")) + db.session.add(goal) + db.session.commit() + return jsonify(goal.to_dict()), 201 + +@goals_bp.route("/api/goals/", methods=["GET"]) +@login_required_json +def get_goal(goal_id): + goal = Goal.query.get_or_404(goal_id) + return jsonify(goal.to_dict()) + +@goals_bp.route("/api/goals/", methods=["PUT"]) +@login_required_json +def update_goal(goal_id): + goal = Goal.query.get_or_404(goal_id) + data = request.get_json() + if "name" in data: + goal.name = data["name"] + if "content" in data: + goal.content = data["content"] + db.session.commit() + return jsonify(goal.to_dict()) + +@goals_bp.route("/api/goals/", methods=["DELETE"]) +@login_required_json +def delete_goal(goal_id): + goal = Goal.query.get_or_404(goal_id) + db.session.delete(goal) + db.session.commit() + return jsonify({"message": "删除成功"}) + +# ===== 以下是 Task 3 的循环检测和关联 API ===== + +def _has_cycle(parent_id, child_id): + """检测添加 child_id 作为 parent_id 的子目标是否会形成循环""" + visited = set() + stack = [child_id] + while stack: + current = stack.pop() + if current == parent_id: + return True + if current in visited: + continue + visited.add(current) + from app.models import GoalRelation + for rel in GoalRelation.query.filter_by(parent_goal_id=current).all(): + stack.append(rel.child_goal_id) + return False + +@goals_bp.route("/api/goals//children", methods=["GET"]) +@login_required_json +def get_goal_children(goal_id): + from app.models import GoalRelation + relations = GoalRelation.query.filter_by(parent_goal_id=goal_id).all() + child_ids = [r.child_goal_id for r in relations] + children = Goal.query.filter(Goal.id.in_(child_ids)).all() if child_ids else [] + return jsonify([c.to_dict() for c in children]) + +@goals_bp.route("/api/goals//parents", methods=["GET"]) +@login_required_json +def get_goal_parents(goal_id): + from app.models import GoalRelation + relations = GoalRelation.query.filter_by(child_goal_id=goal_id).all() + parent_ids = [r.parent_goal_id for r in relations] + parents = Goal.query.filter(Goal.id.in_(parent_ids)).all() if parent_ids else [] + return jsonify([p.to_dict() for p in parents]) + +@goals_bp.route("/api/goals//children", methods=["POST"]) +@login_required_json +def add_goal_child(goal_id): + from app.models import GoalRelation + data = request.get_json() + child_id = data["child_goal_id"] + + if goal_id == child_id: + return jsonify({"error": "不能将目标关联到自身"}), 400 + + if _has_cycle(goal_id, child_id): + return jsonify({"error": "添加此关联会形成循环引用"}), 400 + + existing = GoalRelation.query.filter_by(parent_goal_id=goal_id, child_goal_id=child_id).first() + if existing: + return jsonify({"error": "关联已存在"}), 400 + + relation = GoalRelation(parent_goal_id=goal_id, child_goal_id=child_id) + db.session.add(relation) + db.session.commit() + return jsonify({"message": "添加成功"}) + +@goals_bp.route("/api/goals//children/", methods=["DELETE"]) +@login_required_json +def remove_goal_child(goal_id, child_id): + from app.models import GoalRelation + relation = GoalRelation.query.filter_by(parent_goal_id=goal_id, child_goal_id=child_id).first() + if relation: + db.session.delete(relation) + db.session.commit() + return jsonify({"message": "移除成功"}) \ No newline at end of file diff --git a/app/routes/student_goals.py b/app/routes/student_goals.py new file mode 100644 index 0000000..7f5ee92 --- /dev/null +++ b/app/routes/student_goals.py @@ -0,0 +1,64 @@ +from flask import Blueprint, request, jsonify +from app.models import db, StudentGoal, Goal +from app.routes.auth import login_required_json + +student_goals_bp = Blueprint("student_goals", __name__) + +@student_goals_bp.route("/api/students//goals", methods=["GET"]) +@login_required_json +def get_student_goals(student_id): + records = StudentGoal.query.filter_by(student_id=student_id).all() + return jsonify([r.to_dict() for r in records]) + +@student_goals_bp.route("/api/students//goals", methods=["POST"]) +@login_required_json +def assign_goal(student_id): + data = request.get_json() + + # 检查目标是否存在 + goal = Goal.query.get(data["goal_id"]) + if not goal: + return jsonify({"error": "目标不存在"}), 404 + + # 检查是否已分配 + existing = StudentGoal.query.filter_by(student_id=student_id, goal_id=data["goal_id"]).first() + if existing: + return jsonify({"error": "目标已分配"}), 400 + + record = StudentGoal( + student_id=student_id, + goal_id=data["goal_id"], + status=data.get("status", "未开始"), + mastery_level=data.get("mastery_level", 1), + deadline=data.get("deadline") + ) + db.session.add(record) + db.session.commit() + return jsonify(record.to_dict()), 201 + +@student_goals_bp.route("/api/students//goals/", methods=["PUT"]) +@login_required_json +def update_student_goal(student_id, goal_id): + record = StudentGoal.query.filter_by(student_id=student_id, goal_id=goal_id).first_or_404() + data = request.get_json() + + if "status" in data: + record.status = data["status"] + if data["status"] == "已完成" and not record.completed_at: + from datetime import datetime + record.completed_at = datetime.now() + if "mastery_level" in data: + record.mastery_level = data["mastery_level"] + if "deadline" in data: + record.deadline = data["deadline"] + + db.session.commit() + return jsonify(record.to_dict()) + +@student_goals_bp.route("/api/students//goals/", methods=["DELETE"]) +@login_required_json +def remove_student_goal(student_id, goal_id): + record = StudentGoal.query.filter_by(student_id=student_id, goal_id=goal_id).first_or_404() + db.session.delete(record) + db.session.commit() + return jsonify({"message": "移除成功"}) \ No newline at end of file diff --git a/app/templates/goals.html b/app/templates/goals.html new file mode 100644 index 0000000..7a6762c --- /dev/null +++ b/app/templates/goals.html @@ -0,0 +1,182 @@ +{% extends "base.html" %} + +{% block content %} +
+

🎯 目标管理

+ + +
+
+
+
+
目标库
+ +
+
+
+
+
+
+
+
+ + + + + + +{% endblock %} + +{% block scripts %} +{{ super() }} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/student.html b/app/templates/student.html index bcd8c6c..bb1cf5a 100644 --- a/app/templates/student.html +++ b/app/templates/student.html @@ -56,6 +56,19 @@ + +
+
+
🎯 练习目标
+ +
+
+
+
+
+
练习方案
@@ -238,19 +251,57 @@
+ + + {% endblock %} {% block extra_js %}