- When setting a new task to DOING, the previous DOING task now switches to PENDING instead of TODO - Task display order now prioritizes by status: DONE < DOING < PENDING < TODO, with order field respected within each status group
489 lines
15 KiB
Python
489 lines
15 KiB
Python
from flask import Flask, render_template, request, jsonify, session, redirect, url_for
|
|
from datetime import datetime
|
|
import database
|
|
import auth
|
|
import config
|
|
|
|
app = Flask(__name__)
|
|
app.secret_key = config.SECRET_KEY
|
|
|
|
database.init_db()
|
|
|
|
|
|
@app.route("/")
|
|
def index():
|
|
if "user_id" in session:
|
|
return redirect(url_for("goals_page"))
|
|
return redirect(url_for("login_page"))
|
|
|
|
|
|
@app.route("/login")
|
|
def login_page():
|
|
return render_template("login.html")
|
|
|
|
|
|
@app.route("/register")
|
|
def register_page():
|
|
return render_template("register.html")
|
|
|
|
|
|
@app.route("/goals")
|
|
@auth.login_required
|
|
def goals_page():
|
|
return render_template("goals.html")
|
|
|
|
|
|
@app.route("/tasks")
|
|
@auth.login_required
|
|
def tasks_page():
|
|
return render_template("tasks.html")
|
|
|
|
|
|
@app.route("/admin")
|
|
@auth.login_required
|
|
def admin_page():
|
|
user = auth.get_current_user()
|
|
if not user or user.get("role") != "admin":
|
|
return redirect(url_for("goals_page"))
|
|
return render_template("admin.html")
|
|
|
|
|
|
@app.route("/notes")
|
|
@auth.login_required
|
|
def notes_page():
|
|
return render_template("notes.html")
|
|
|
|
|
|
@app.route("/api/auth/register", methods=["POST"])
|
|
def api_register():
|
|
data = request.get_json()
|
|
username = data.get("username", "").strip()
|
|
password = data.get("password", "")
|
|
|
|
if not username or not password:
|
|
return jsonify({"success": False, "message": "Username and password are required"}), 400
|
|
|
|
if len(password) < 6:
|
|
return jsonify({"success": False, "message": "Password must be at least 6 characters"}), 400
|
|
|
|
if database.get_user_by_username(username):
|
|
return jsonify({"success": False, "message": "Username already exists"}), 400
|
|
|
|
password_hash = auth.hash_password(password)
|
|
user = database.create_user(username, password_hash)
|
|
|
|
session["user_id"] = user["id"]
|
|
session["username"] = username
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"message": "Registration successful",
|
|
"user_id": user["id"]
|
|
}), 201
|
|
|
|
|
|
@app.route("/api/auth/login", methods=["POST"])
|
|
def api_login():
|
|
data = request.get_json()
|
|
username = data.get("username", "").strip()
|
|
password = data.get("password", "")
|
|
|
|
if not username or not password:
|
|
return jsonify({"success": False, "message": "Username and password are required"}), 400
|
|
|
|
user = database.get_user_by_username(username)
|
|
if not user or not auth.check_password(password, user["password_hash"]):
|
|
return jsonify({"success": False, "message": "Invalid credentials"}), 401
|
|
|
|
session["user_id"] = user["id"]
|
|
session["username"] = username
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"message": "Login successful",
|
|
"user": {
|
|
"user_id": user["id"],
|
|
"username": user["username"],
|
|
"role": user["role"],
|
|
"max_goals": user["max_goals"],
|
|
"selected_goal_id": user.get("selected_goal_id")
|
|
}
|
|
})
|
|
|
|
|
|
@app.route("/api/auth/logout", methods=["POST"])
|
|
@auth.login_required
|
|
def api_logout():
|
|
session.clear()
|
|
return jsonify({"success": True, "message": "Logged out"})
|
|
|
|
|
|
@app.route("/api/auth/me", methods=["GET"])
|
|
@auth.login_required
|
|
def api_me():
|
|
user = auth.get_current_user()
|
|
if not user:
|
|
return jsonify({"success": False, "message": "User not found"}), 404
|
|
return jsonify({
|
|
"user_id": user["id"],
|
|
"username": user["username"],
|
|
"role": user["role"],
|
|
"max_goals": user["max_goals"],
|
|
"selected_goal_id": user.get("selected_goal_id")
|
|
})
|
|
|
|
|
|
@app.route("/api/user/selected-goal", methods=["PATCH"])
|
|
@auth.login_required
|
|
def api_set_selected_goal():
|
|
data = request.get_json()
|
|
goal_id = data.get("goal_id")
|
|
database.update_user(session["user_id"], selected_goal_id=goal_id)
|
|
return jsonify({"success": True, "selected_goal_id": goal_id})
|
|
|
|
|
|
@app.route("/api/goals", methods=["GET"])
|
|
@auth.login_required
|
|
def api_get_goals():
|
|
user_id = session["user_id"]
|
|
goals = database.get_goals_by_user(user_id)
|
|
result = []
|
|
for goal in goals:
|
|
goal_dict = dict(goal)
|
|
goal_dict["id"] = goal["id"]
|
|
result.append(goal_dict)
|
|
return jsonify(result)
|
|
|
|
|
|
@app.route("/api/goals", methods=["POST"])
|
|
@auth.login_required
|
|
def api_create_goal():
|
|
data = request.get_json()
|
|
title = data.get("title", "").strip()
|
|
|
|
if not title:
|
|
return jsonify({"success": False, "message": "Title is required"}), 400
|
|
|
|
user_id = session["user_id"]
|
|
user = database.get_user_by_id(user_id)
|
|
|
|
if database.count_goals_by_user(user_id) >= user["max_goals"]:
|
|
return jsonify({
|
|
"success": False,
|
|
"message": f"Goal limit reached ({user['max_goals']})"
|
|
}), 400
|
|
|
|
goal = database.create_goal(user_id, title)
|
|
return jsonify({"success": True, "goal_id": goal["id"]}), 201
|
|
|
|
|
|
@app.route("/api/goals/<int:goal_id>", methods=["PUT"])
|
|
@auth.login_required
|
|
def api_update_goal(goal_id):
|
|
goal = database.get_goal_by_id(goal_id)
|
|
if not goal or goal["user_id"] != session["user_id"]:
|
|
return jsonify({"success": False, "message": "Goal not found"}), 404
|
|
|
|
data = request.get_json()
|
|
updates = {}
|
|
|
|
if "title" in data:
|
|
title = data["title"].strip()
|
|
if not title:
|
|
return jsonify({"success": False, "message": "Title cannot be empty"}), 400
|
|
updates["title"] = title
|
|
|
|
if "activated" in data:
|
|
updates["activated"] = bool(data["activated"])
|
|
|
|
database.update_goal(goal_id, **updates)
|
|
return jsonify({"success": True})
|
|
|
|
|
|
@app.route("/api/goals/<int:goal_id>", methods=["DELETE"])
|
|
@auth.login_required
|
|
def api_delete_goal(goal_id):
|
|
goal = database.get_goal_by_id(goal_id)
|
|
if not goal or goal["user_id"] != session["user_id"]:
|
|
return jsonify({"success": False, "message": "Goal not found"}), 404
|
|
|
|
database.delete_goal(goal_id)
|
|
return jsonify({"success": True})
|
|
|
|
|
|
@app.route("/api/goals/<int:goal_id>/toggle", methods=["PATCH"])
|
|
@auth.login_required
|
|
def api_toggle_goal(goal_id):
|
|
goal = database.get_goal_by_id(goal_id)
|
|
if not goal or goal["user_id"] != session["user_id"]:
|
|
return jsonify({"success": False, "message": "Goal not found"}), 404
|
|
|
|
new_status = not goal["activated"]
|
|
database.update_goal(goal_id, activated=new_status)
|
|
return jsonify({"success": True, "activated": new_status})
|
|
|
|
|
|
@app.route("/api/goals/<int:goal_id>/selected-task", methods=["PATCH"])
|
|
@auth.login_required
|
|
def api_set_selected_task(goal_id):
|
|
goal = database.get_goal_by_id(goal_id)
|
|
if not goal or goal["user_id"] != session["user_id"]:
|
|
return jsonify({"success": False, "message": "Goal not found"}), 404
|
|
|
|
data = request.get_json()
|
|
task_id = data.get("task_id")
|
|
database.update_goal(goal_id, selected_task_id=task_id)
|
|
return jsonify({"success": True, "selected_task_id": task_id})
|
|
|
|
|
|
@app.route("/api/tasks", methods=["GET"])
|
|
@auth.login_required
|
|
def api_get_tasks():
|
|
goal_id = request.args.get("goal_id", type=int)
|
|
if not goal_id:
|
|
return jsonify([])
|
|
|
|
goal = database.get_goal_by_id(goal_id)
|
|
if not goal or goal["user_id"] != session["user_id"]:
|
|
return jsonify({"success": False, "message": "Goal not found"}), 404
|
|
|
|
tasks = database.get_tasks_sorted(goal_id)
|
|
result = []
|
|
for task in tasks:
|
|
task_dict = dict(task)
|
|
task_dict["id"] = task["id"]
|
|
result.append(task_dict)
|
|
return jsonify(result)
|
|
|
|
|
|
@app.route("/api/tasks", methods=["POST"])
|
|
@auth.login_required
|
|
def api_create_task():
|
|
data = request.get_json()
|
|
goal_id = data.get("goal_id")
|
|
title = data.get("title", "").strip()
|
|
desc = data.get("desc", "").strip()
|
|
|
|
if not goal_id or not title:
|
|
return jsonify({"success": False, "message": "Goal ID and title are required"}), 400
|
|
|
|
goal = database.get_goal_by_id(goal_id)
|
|
if not goal or goal["user_id"] != session["user_id"]:
|
|
return jsonify({"success": False, "message": "Goal not found"}), 404
|
|
|
|
if not goal["activated"]:
|
|
return jsonify({"success": False, "message": "Cannot add task to deactivated goal"}), 400
|
|
|
|
task = database.create_task(goal_id, title, desc)
|
|
return jsonify({"success": True, "task_id": task["id"]}), 201
|
|
|
|
|
|
@app.route("/api/tasks/<int:task_id>", methods=["PUT"])
|
|
@auth.login_required
|
|
def api_update_task(task_id):
|
|
task = database.get_task_by_id(task_id)
|
|
if not task:
|
|
return jsonify({"success": False, "message": "Task not found"}), 404
|
|
|
|
goal = database.get_goal_by_id(task["goal_id"])
|
|
if goal["user_id"] != session["user_id"]:
|
|
return jsonify({"success": False, "message": "Task not found"}), 404
|
|
|
|
data = request.get_json()
|
|
updates = {}
|
|
|
|
if "title" in data:
|
|
title = data["title"].strip()
|
|
if not title:
|
|
return jsonify({"success": False, "message": "Title cannot be empty"}), 400
|
|
updates["title"] = title
|
|
|
|
if "desc" in data:
|
|
updates["desc"] = data["desc"]
|
|
|
|
database.update_task(task_id, **updates)
|
|
return jsonify({"success": True})
|
|
|
|
|
|
@app.route("/api/tasks/<int:task_id>", methods=["DELETE"])
|
|
@auth.login_required
|
|
def api_delete_task(task_id):
|
|
task = database.get_task_by_id(task_id)
|
|
if not task:
|
|
return jsonify({"success": False, "message": "Task not found"}), 404
|
|
|
|
goal = database.get_goal_by_id(task["goal_id"])
|
|
if goal["user_id"] != session["user_id"]:
|
|
return jsonify({"success": False, "message": "Task not found"}), 404
|
|
|
|
database.delete_task(task_id)
|
|
return jsonify({"success": True})
|
|
|
|
|
|
@app.route("/api/tasks/<int:task_id>/status", methods=["PATCH"])
|
|
@auth.login_required
|
|
def api_update_task_status(task_id):
|
|
task = database.get_task_by_id(task_id)
|
|
if not task:
|
|
return jsonify({"success": False, "message": "Task not found"}), 404
|
|
|
|
goal = database.get_goal_by_id(task["goal_id"])
|
|
if goal["user_id"] != session["user_id"]:
|
|
return jsonify({"success": False, "message": "Task not found"}), 404
|
|
|
|
data = request.get_json()
|
|
new_status = data.get("status")
|
|
|
|
if new_status not in ["todo", "doing", "pending", "done"]:
|
|
return jsonify({"success": False, "message": "Invalid status"}), 400
|
|
|
|
updates = {"status": new_status}
|
|
|
|
if new_status == "doing":
|
|
doing_task = None
|
|
all_tasks = database.get_tasks_by_goal(task["goal_id"])
|
|
for t in all_tasks:
|
|
if t.get("status") == "doing" and t["id"] != task["id"]:
|
|
doing_task = t
|
|
break
|
|
if doing_task:
|
|
database.update_task(doing_task["id"], status="pending")
|
|
updates["start_time"] = datetime.now().isoformat()
|
|
else:
|
|
updates["start_time"] = None
|
|
|
|
if new_status == "done":
|
|
updates["finished_time"] = datetime.now().isoformat()
|
|
else:
|
|
updates["finished_time"] = None
|
|
|
|
database.update_task(task_id, **updates)
|
|
return jsonify({"success": True})
|
|
|
|
|
|
@app.route("/api/tasks/<int:task_id>/order", methods=["PATCH"])
|
|
@auth.login_required
|
|
def api_update_task_order(task_id):
|
|
task = database.get_task_by_id(task_id)
|
|
if not task:
|
|
return jsonify({"success": False, "message": "Task not found"}), 404
|
|
|
|
goal = database.get_goal_by_id(task["goal_id"])
|
|
if goal["user_id"] != session["user_id"]:
|
|
return jsonify({"success": False, "message": "Task not found"}), 404
|
|
|
|
data = request.get_json()
|
|
new_order = data.get("order")
|
|
|
|
if new_order is None:
|
|
return jsonify({"success": False, "message": "Order value required"}), 400
|
|
|
|
database.update_task(task_id, order=float(new_order))
|
|
return jsonify({"success": True})
|
|
|
|
|
|
@app.route("/api/notes", methods=["GET"])
|
|
@auth.login_required
|
|
def api_get_notes():
|
|
goal_id = request.args.get("goal_id", type=int)
|
|
search = request.args.get("search", "").strip() or None
|
|
notes = database.get_notes(session["user_id"], goal_id, search)
|
|
return jsonify(notes)
|
|
|
|
|
|
@app.route("/api/notes", methods=["POST"])
|
|
@auth.login_required
|
|
def api_create_note():
|
|
data = request.get_json()
|
|
title = data.get("title", "").strip()
|
|
if not title:
|
|
return jsonify({"success": False, "message": "Title is required"}), 400
|
|
|
|
goal_id = data.get("goal_id")
|
|
task_id = data.get("task_id")
|
|
content = data.get("content", "")
|
|
|
|
note = database.create_note(session["user_id"], goal_id, task_id, title, content)
|
|
return jsonify(note), 201
|
|
|
|
|
|
@app.route("/api/notes/<int:note_id>", methods=["GET"])
|
|
@auth.login_required
|
|
def api_get_note(note_id):
|
|
note = database.get_note_by_id(note_id)
|
|
if not note or note["user_id"] != session["user_id"]:
|
|
return jsonify({"success": False, "message": "Note not found"}), 404
|
|
return jsonify(note)
|
|
|
|
|
|
@app.route("/api/notes/<int:note_id>", methods=["PUT"])
|
|
@auth.login_required
|
|
def api_update_note(note_id):
|
|
note = database.get_note_by_id(note_id)
|
|
if not note or note["user_id"] != session["user_id"]:
|
|
return jsonify({"success": False, "message": "Note not found"}), 404
|
|
|
|
data = request.get_json()
|
|
title = data.get("title", "").strip()
|
|
if not title:
|
|
return jsonify({"success": False, "message": "Title is required"}), 400
|
|
|
|
updated = database.update_note(note_id, title, data.get("content", ""))
|
|
return jsonify(updated)
|
|
|
|
|
|
@app.route("/api/notes/<int:note_id>", methods=["DELETE"])
|
|
@auth.login_required
|
|
def api_delete_note(note_id):
|
|
note = database.get_note_by_id(note_id)
|
|
if not note or note["user_id"] != session["user_id"]:
|
|
return jsonify({"success": False, "message": "Note not found"}), 404
|
|
|
|
database.delete_note(note_id)
|
|
return jsonify({"success": True})
|
|
|
|
|
|
@app.route("/api/admin/users", methods=["GET"])
|
|
@auth.admin_required
|
|
def api_get_users():
|
|
users = database.get_all_users()
|
|
result = []
|
|
for user in users:
|
|
result.append({
|
|
"user_id": user["id"],
|
|
"username": user["username"],
|
|
"role": user["role"],
|
|
"max_goals": user["max_goals"]
|
|
})
|
|
return jsonify(result)
|
|
|
|
|
|
@app.route("/api/admin/users/<int:user_id>", methods=["PUT"])
|
|
@auth.admin_required
|
|
def api_update_user(user_id):
|
|
user = database.get_user_by_id(user_id)
|
|
if not user:
|
|
return jsonify({"success": False, "message": "User not found"}), 404
|
|
|
|
data = request.get_json()
|
|
updates = {}
|
|
|
|
if "max_goals" in data:
|
|
max_goals = data["max_goals"]
|
|
if not isinstance(max_goals, int) or max_goals < 1:
|
|
return jsonify({"success": False, "message": "Invalid max_goals value"}), 400
|
|
updates["max_goals"] = max_goals
|
|
|
|
if "role" in data:
|
|
role = data["role"]
|
|
if role not in ["user", "admin"]:
|
|
return jsonify({"success": False, "message": "Invalid role"}), 400
|
|
updates["role"] = role
|
|
|
|
database.update_user(user_id, **updates)
|
|
return jsonify({"success": True})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host=config.HOST, port=config.PORT, debug=config.DEBUG)
|