Migrate from TinyDB to SQLite
- Replace TinyDB (JSON file) with sqlite3 for data persistence - Add schema.py: table creation + data migration from db.json - Rewrite database.py: all CRUD operations use sqlite3 directly - All data retains original IDs via migration script - Remove tinydb dependency from pyproject.toml
This commit is contained in:
parent
b060ba6bf8
commit
a8fe6ed7b3
24
app.py
24
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/<int:goal_id>", 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/<int:task_id>", 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"]
|
||||
|
||||
@ -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"
|
||||
|
||||
234
database.py
234
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
|
||||
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()
|
||||
|
||||
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(
|
||||
"""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()
|
||||
|
||||
@ -6,5 +6,4 @@ requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"bcrypt>=5.0.0",
|
||||
"flask>=3.1.3",
|
||||
"tinydb>=4.8.2",
|
||||
]
|
||||
|
||||
116
schema.py
Normal file
116
schema.py
Normal file
@ -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()
|
||||
@ -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";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user