diff --git a/app.py b/app.py index 8f839bd..330bb60 100644 --- a/app.py +++ b/app.py @@ -66,13 +66,13 @@ def api_register(): password_hash = auth.hash_password(password) user = database.create_user(username, password_hash) - session["user_id"] = user.doc_id + session["user_id"] = user["id"] session["username"] = username return jsonify({ "success": True, "message": "Registration successful", - "user_id": user.doc_id + "user_id": user["id"] }), 201 @@ -89,14 +89,14 @@ def api_login(): if not user or not auth.check_password(password, user["password_hash"]): return jsonify({"success": False, "message": "Invalid credentials"}), 401 - session["user_id"] = user.doc_id + session["user_id"] = user["id"] session["username"] = username return jsonify({ "success": True, "message": "Login successful", "user": { - "user_id": user.doc_id, + "user_id": user["id"], "username": user["username"], "role": user["role"], "max_goals": user["max_goals"] @@ -118,7 +118,7 @@ def api_me(): if not user: return jsonify({"success": False, "message": "User not found"}), 404 return jsonify({ - "user_id": user.doc_id, + "user_id": user["id"], "username": user["username"], "role": user["role"], "max_goals": user["max_goals"] @@ -133,7 +133,7 @@ def api_get_goals(): result = [] for goal in goals: goal_dict = dict(goal) - goal_dict["id"] = goal.doc_id + goal_dict["id"] = goal["id"] result.append(goal_dict) return jsonify(result) @@ -157,7 +157,7 @@ def api_create_goal(): }), 400 goal = database.create_goal(user_id, title) - return jsonify({"success": True, "goal_id": goal.doc_id}), 201 + return jsonify({"success": True, "goal_id": goal["id"]}), 201 @app.route("/api/goals/", methods=["PUT"]) @@ -221,7 +221,7 @@ def api_get_tasks(): result = [] for task in tasks: task_dict = dict(task) - task_dict["id"] = task.doc_id + task_dict["id"] = task["id"] result.append(task_dict) return jsonify(result) @@ -245,7 +245,7 @@ def api_create_task(): 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.doc_id}), 201 + return jsonify({"success": True, "task_id": task["id"]}), 201 @app.route("/api/tasks/", methods=["PUT"]) @@ -313,11 +313,11 @@ def api_update_task_status(task_id): 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.doc_id != task.doc_id: + if t.get("status") == "doing" and t["id"] != task["id"]: doing_task = t break if doing_task: - database.update_task(doing_task.doc_id, status="todo") + database.update_task(doing_task["id"], status="todo") updates["start_time"] = datetime.now().isoformat() else: updates["start_time"] = None @@ -359,7 +359,7 @@ def api_get_users(): result = [] for user in users: result.append({ - "user_id": user.doc_id, + "user_id": user["id"], "username": user["username"], "role": user["role"], "max_goals": user["max_goals"] diff --git a/config.py b/config.py index e8f1ecb..fe333b1 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,6 @@ import os -DB_PATH = os.path.join(os.path.dirname(__file__), "data", "db.json") +DB_PATH = os.path.join(os.path.dirname(__file__), "data", "db.sqlite") DEFAULT_ADMIN_USERNAME = "admin" DEFAULT_ADMIN_PASSWORD = "admin123" diff --git a/database.py b/database.py index b028a43..994727c 100644 --- a/database.py +++ b/database.py @@ -1,137 +1,219 @@ -import os -from tinydb import TinyDB, Query -from tinydb.operations import increment -import config +from schema import get_connection -os.makedirs(os.path.dirname(config.DB_PATH), exist_ok=True) -db = TinyDB(config.DB_PATH) - -users_table = db.table("users") -goals_table = db.table("goals") -tasks_table = db.table("tasks") - -User = Query() -Goal = Query() -Task = Query() +def row_to_dict(row): + if row is None: + return None + return dict(row) def init_db(): - if users_table.count(User.role == "admin") == 0: - import bcrypt - password_hash = bcrypt.hashpw( - config.DEFAULT_ADMIN_PASSWORD.encode("utf-8"), - bcrypt.gensalt() - ).decode("utf-8") - users_table.insert({ - "username": config.DEFAULT_ADMIN_USERNAME, - "password_hash": password_hash, - "role": "admin", - "max_goals": 100 - }) + from schema import init_db as _init_db, migrate_from_tinydb + _init_db() + migrate_from_tinydb() def get_user_by_username(username): - result = users_table.search(User.username == username) - return result[0] if result else None + conn = get_connection() + try: + cur = conn.execute("SELECT * FROM users WHERE username = ?", (username,)) + return row_to_dict(cur.fetchone()) + finally: + conn.close() def get_user_by_id(user_id): - return users_table.get(doc_id=user_id) + conn = get_connection() + try: + cur = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)) + return row_to_dict(cur.fetchone()) + finally: + conn.close() def create_user(username, password_hash, role="user", max_goals=None): if max_goals is None: - max_goals = config.DEFAULT_MAX_GOALS - doc_id = users_table.insert({ - "username": username, - "password_hash": password_hash, - "role": role, - "max_goals": max_goals - }) - return users_table.get(doc_id=doc_id) + max_goals = 5 + conn = get_connection() + try: + cur = conn.execute( + "INSERT INTO users (username, password_hash, role, max_goals) VALUES (?, ?, ?, ?)", + (username, password_hash, role, max_goals) + ) + conn.commit() + return row_to_dict(conn.execute("SELECT * FROM users WHERE id = ?", (cur.lastrowid,)).fetchone()) + finally: + conn.close() def update_user(user_id, **kwargs): - users_table.update(kwargs, doc_ids=[user_id]) + if not kwargs: + return + sets = ", ".join(f"{k} = ?" for k in kwargs) + values = list(kwargs.values()) + [user_id] + conn = get_connection() + try: + conn.execute(f"UPDATE users SET {sets} WHERE id = ?", values) + conn.commit() + finally: + conn.close() def get_all_users(): - return users_table.all() + conn = get_connection() + try: + cur = conn.execute("SELECT * FROM users") + return [row_to_dict(r) for r in cur.fetchall()] + finally: + conn.close() def get_goals_by_user(user_id): - return goals_table.search(Goal.user_id == user_id) + conn = get_connection() + try: + cur = conn.execute("SELECT * FROM goals WHERE user_id = ?", (user_id,)) + return [row_to_dict(r) for r in cur.fetchall()] + finally: + conn.close() def get_goal_by_id(goal_id): - return goals_table.get(doc_id=goal_id) + conn = get_connection() + try: + cur = conn.execute("SELECT * FROM goals WHERE id = ?", (goal_id,)) + return row_to_dict(cur.fetchone()) + finally: + conn.close() def create_goal(user_id, title): - doc_id = goals_table.insert({ - "user_id": user_id, - "title": title, - "activated": True - }) - return goals_table.get(doc_id=doc_id) + conn = get_connection() + try: + cur = conn.execute( + "INSERT INTO goals (user_id, title, activated) VALUES (?, ?, 1)", + (user_id, title) + ) + conn.commit() + return row_to_dict(conn.execute("SELECT * FROM goals WHERE id = ?", (cur.lastrowid,)).fetchone()) + finally: + conn.close() def update_goal(goal_id, **kwargs): - goals_table.update(kwargs, doc_ids=[goal_id]) + if not kwargs: + return + sets = ", ".join(f"{k} = ?" for k in kwargs) + values = list(kwargs.values()) + [goal_id] + conn = get_connection() + try: + conn.execute(f"UPDATE goals SET {sets} WHERE id = ?", values) + conn.commit() + finally: + conn.close() def delete_goal(goal_id): - tasks = tasks_table.search(Task.goal_id == goal_id) - for task in tasks: - tasks_table.remove(doc_ids=[task.doc_id]) - goals_table.remove(doc_ids=[goal_id]) + conn = get_connection() + try: + conn.execute("DELETE FROM tasks WHERE goal_id = ?", (goal_id,)) + conn.execute("DELETE FROM goals WHERE id = ?", (goal_id,)) + conn.commit() + finally: + conn.close() def count_goals_by_user(user_id): - return len(goals_table.search(Goal.user_id == user_id)) + conn = get_connection() + try: + cur = conn.execute("SELECT COUNT(*) FROM goals WHERE user_id = ?", (user_id,)) + return cur.fetchone()[0] + finally: + conn.close() def get_tasks_by_goal(goal_id): - return tasks_table.search(Task.goal_id == goal_id) + conn = get_connection() + try: + cur = conn.execute("SELECT * FROM tasks WHERE goal_id = ?", (goal_id,)) + return [row_to_dict(r) for r in cur.fetchall()] + finally: + conn.close() def get_task_by_id(task_id): - return tasks_table.get(doc_id=task_id) + conn = get_connection() + try: + cur = conn.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)) + return row_to_dict(cur.fetchone()) + finally: + conn.close() def create_task(goal_id, title, desc="", status="todo", order=None): if order is None: - existing_tasks = tasks_table.search(Task.goal_id == goal_id) - unfinished = [t for t in existing_tasks if t.get("status") != "done"] - order = max([t.get("order", 0) for t in unfinished], default=0) + 1.0 - - doc_id = tasks_table.insert({ - "goal_id": goal_id, - "title": title, - "desc": desc, - "status": status, - "start_time": None, - "finished_time": None, - "order": order - }) - return tasks_table.get(doc_id=doc_id) + conn = get_connection() + try: + cur = conn.execute( + """SELECT COALESCE(MAX("order"), 0) + 1.0 FROM tasks + WHERE goal_id = ? AND status != 'done'""", + (goal_id,) + ) + order = cur.fetchone()[0] + finally: + conn.close() + + conn = get_connection() + try: + cur = conn.execute( + """INSERT INTO tasks (goal_id, title, desc, status, start_time, finished_time, "order") + VALUES (?, ?, ?, ?, ?, ?, ?)""", + (goal_id, title, desc, status, None, None, order) + ) + conn.commit() + return row_to_dict(conn.execute("SELECT * FROM tasks WHERE id = ?", (cur.lastrowid,)).fetchone()) + finally: + conn.close() def update_task(task_id, **kwargs): - tasks_table.update(kwargs, doc_ids=[task_id]) + if not kwargs: + return + sets = ", ".join(f"{k} = ?" for k in kwargs) + values = list(kwargs.values()) + [task_id] + conn = get_connection() + try: + conn.execute(f"UPDATE tasks SET {sets} WHERE id = ?", values) + conn.commit() + finally: + conn.close() def delete_task(task_id): - tasks_table.remove(doc_ids=[task_id]) + conn = get_connection() + try: + conn.execute("DELETE FROM tasks WHERE id = ?", (task_id,)) + conn.commit() + finally: + conn.close() def get_tasks_sorted(goal_id): - all_tasks = tasks_table.search(Task.goal_id == goal_id) - unfinished = [t for t in all_tasks if t.get("status") != "done"] - finished = [t for t in all_tasks if t.get("status") == "done"] + conn = get_connection() + try: + cur = conn.execute( + """SELECT * FROM tasks WHERE goal_id = ? AND status = 'done' + ORDER BY finished_time DESC""", + (goal_id,) + ) + finished = [row_to_dict(r) for r in cur.fetchall()] - unfinished.sort(key=lambda t: t.get("order", 0)) - finished.sort(key=lambda t: t.get("finished_time", ""), reverse=True) + cur = conn.execute( + """SELECT * FROM tasks WHERE goal_id = ? AND status != 'done' + ORDER BY "order" ASC""", + (goal_id,) + ) + unfinished = [row_to_dict(r) for r in cur.fetchall()] - return finished + unfinished + return finished + unfinished + finally: + conn.close() diff --git a/pyproject.toml b/pyproject.toml index 92831f9..f26bc42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,5 +6,4 @@ requires-python = ">=3.13" dependencies = [ "bcrypt>=5.0.0", "flask>=3.1.3", - "tinydb>=4.8.2", ] diff --git a/schema.py b/schema.py new file mode 100644 index 0000000..67b1388 --- /dev/null +++ b/schema.py @@ -0,0 +1,116 @@ +import sqlite3 +import os +import json +import config + + +CREATE_USERS = """ +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'user', + max_goals INTEGER NOT NULL DEFAULT 5 +) +""" + +CREATE_GOALS = """ +CREATE TABLE IF NOT EXISTS goals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + title TEXT NOT NULL, + activated INTEGER NOT NULL DEFAULT 1, + FOREIGN KEY (user_id) REFERENCES users(id) +) +""" + +CREATE_TASKS = """ +CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + goal_id INTEGER NOT NULL, + title TEXT NOT NULL, + desc TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'todo', + start_time TEXT, + finished_time TEXT, + "order" REAL NOT NULL DEFAULT 0.0, + FOREIGN KEY (goal_id) REFERENCES goals(id) +) +""" + + +def get_connection(): + os.makedirs(os.path.dirname(config.DB_PATH), exist_ok=True) + conn = sqlite3.connect(config.DB_PATH) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON") + return conn + + +def init_db(): + conn = get_connection() + try: + conn.execute(CREATE_USERS) + conn.execute(CREATE_GOALS) + conn.execute(CREATE_TASKS) + conn.commit() + + import bcrypt + cur = conn.execute("SELECT COUNT(*) FROM users WHERE role = 'admin'") + if cur.fetchone()[0] == 0: + password_hash = bcrypt.hashpw( + config.DEFAULT_ADMIN_PASSWORD.encode("utf-8"), + bcrypt.gensalt() + ).decode("utf-8") + conn.execute( + "INSERT INTO users (username, password_hash, role, max_goals) VALUES (?, ?, ?, ?)", + (config.DEFAULT_ADMIN_USERNAME, password_hash, "admin", 100) + ) + conn.commit() + finally: + conn.close() + + +def migrate_from_tinydb(): + json_path = os.path.join(os.path.dirname(config.DB_PATH), "db.json") + if not os.path.exists(json_path): + return + + conn = get_connection() + try: + with open(json_path, "r") as f: + data = json.load(f) + + if "users" in data: + for doc_id, record in data["users"].items(): + record["id"] = int(doc_id) + if conn.execute("SELECT COUNT(*) FROM users WHERE id = ?", (record["id"],)).fetchone()[0] == 0: + conn.execute( + "INSERT INTO users (id, username, password_hash, role, max_goals) VALUES (?, ?, ?, ?, ?)", + (record["id"], record["username"], record["password_hash"], record["role"], record["max_goals"]) + ) + + if "goals" in data: + for doc_id, record in data["goals"].items(): + record["id"] = int(doc_id) + if conn.execute("SELECT COUNT(*) FROM goals WHERE id = ?", (record["id"],)).fetchone()[0] == 0: + conn.execute( + "INSERT INTO goals (id, user_id, title, activated) VALUES (?, ?, ?, ?)", + (record["id"], record["user_id"], record["title"], 1 if record.get("activated", True) else 0) + ) + + if "tasks" in data: + for doc_id, record in data["tasks"].items(): + record["id"] = int(doc_id) + if conn.execute("SELECT COUNT(*) FROM tasks WHERE id = ?", (record["id"],)).fetchone()[0] == 0: + conn.execute( + """INSERT INTO tasks (id, goal_id, title, desc, status, start_time, finished_time, "order") + VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + (record["id"], record["goal_id"], record["title"], record.get("desc", ""), + record.get("status", "todo"), record.get("start_time"), record.get("finished_time"), + record.get("order", 0.0)) + ) + + conn.commit() + finally: + conn.close() diff --git a/static/js/goals.js b/static/js/goals.js index 96edbe4..ee210c2 100644 --- a/static/js/goals.js +++ b/static/js/goals.js @@ -60,7 +60,7 @@ function openModal(goal = null) { if (goal) { modalTitle.textContent = "Edit Goal"; - goalId.value = goal.doc_id; + goalId.value = goal.id; goalTitle.value = goal.title; } else { modalTitle.textContent = "Create Goal";