171 lines
6.0 KiB
Python
171 lines
6.0 KiB
Python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.responses import FileResponse
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
import sqlite3
|
|
import uuid
|
|
from datetime import datetime
|
|
import json
|
|
|
|
app = FastAPI()
|
|
app.mount("/static", StaticFiles(directory="."), name="static")
|
|
|
|
DB = "wish.db"
|
|
|
|
def db():
|
|
conn = sqlite3.connect(DB)
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
return conn
|
|
|
|
def init():
|
|
with db() as c:
|
|
c.executescript("""
|
|
CREATE TABLE IF NOT EXISTS boards (
|
|
id TEXT PRIMARY KEY, name TEXT, color TEXT DEFAULT '#6366f1',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE IF NOT EXISTS lists (
|
|
id TEXT PRIMARY KEY, board_id TEXT REFERENCES boards(id) ON DELETE CASCADE,
|
|
title TEXT, position INTEGER DEFAULT 0
|
|
);
|
|
CREATE TABLE IF NOT EXISTS cards (
|
|
id TEXT PRIMARY KEY, list_id TEXT REFERENCES lists(id) ON DELETE CASCADE,
|
|
board_id TEXT REFERENCES boards(id) ON DELETE CASCADE,
|
|
title TEXT, description TEXT DEFAULT '', position INTEGER DEFAULT 0
|
|
);
|
|
""")
|
|
|
|
init()
|
|
|
|
class BoardIn(BaseModel):
|
|
name: str
|
|
color: str = "#6366f1"
|
|
|
|
class ListIn(BaseModel):
|
|
title: str
|
|
|
|
class CardIn(BaseModel):
|
|
title: str
|
|
description: str = ""
|
|
|
|
@app.get("/")
|
|
def index():
|
|
return FileResponse("index.html")
|
|
|
|
@app.get("/api/boards")
|
|
def boards():
|
|
with db() as c:
|
|
return [dict(r) for r in c.execute("SELECT * FROM boards ORDER BY created_at DESC").fetchall()]
|
|
|
|
@app.post("/api/boards")
|
|
def create_board(data: BoardIn):
|
|
bid = str(uuid.uuid4())
|
|
with db() as c:
|
|
c.execute("INSERT INTO boards VALUES (?,?,?,NULL)", (bid, data.name, data.color))
|
|
broadcast({"type": "board:create", "data": {"id": bid, "name": data.name, "color": data.color}})
|
|
return {"id": bid, "name": data.name, "color": data.color}
|
|
|
|
@app.delete("/api/boards/{bid}")
|
|
def delete_board(bid: str):
|
|
with db() as c:
|
|
c.execute("DELETE FROM boards WHERE id=?", (bid,))
|
|
broadcast({"type": "board:delete", "data": {"id": bid}})
|
|
return {"ok": True}
|
|
|
|
@app.get("/api/boards/{bid}/lists")
|
|
def lists(bid: str):
|
|
with db() as c:
|
|
return [dict(r) for r in c.execute("SELECT * FROM lists WHERE board_id=? ORDER BY position", (bid,)).fetchall()]
|
|
|
|
@app.post("/api/boards/{bid}/lists")
|
|
def create_list(bid: str, data: ListIn):
|
|
lid = str(uuid.uuid4())
|
|
with db() as c:
|
|
p = c.execute("SELECT COALESCE(MAX(position),-1) m FROM lists WHERE board_id=?", (bid,)).fetchone()["m"]
|
|
c.execute("INSERT INTO lists VALUES (?,?,?,?)", (lid, bid, data.title, p+1))
|
|
broadcast({"type": "list:create", "data": {"id": lid, "board_id": bid, "title": data.title, "position": p+1}})
|
|
return {"id": lid, "board_id": bid, "title": data.title, "position": p+1}
|
|
|
|
@app.patch("/api/lists/{lid}")
|
|
def update_list(lid: str, data: dict):
|
|
with db() as c:
|
|
u, ps = [], []
|
|
if "title" in data: u.append("title=?"); ps.append(data["title"])
|
|
if "position" in data: u.append("position=?"); ps.append(data["position"])
|
|
if u:
|
|
ps.append(lid)
|
|
c.execute(f"UPDATE lists SET {', '.join(u)} WHERE id=?", ps)
|
|
row = c.execute("SELECT * FROM lists WHERE id=?", (lid,)).fetchone()
|
|
broadcast({"type": "list:update", "data": dict(row)})
|
|
return {"ok": True}
|
|
|
|
@app.delete("/api/lists/{lid}")
|
|
def delete_list(lid: str):
|
|
with db() as c:
|
|
c.execute("DELETE FROM lists WHERE id=?", (lid,))
|
|
broadcast({"type": "list:delete", "data": {"id": lid}})
|
|
return {"ok": True}
|
|
|
|
@app.get("/api/lists/{lid}/cards")
|
|
def cards(lid: str):
|
|
with db() as c:
|
|
return [dict(r) for r in c.execute("SELECT * FROM cards WHERE list_id=? ORDER BY position", (lid,)).fetchall()]
|
|
|
|
@app.post("/api/lists/{lid}/cards")
|
|
def create_card(lid: str, data: CardIn):
|
|
cid = str(uuid.uuid4())
|
|
with db() as c:
|
|
bid = c.execute("SELECT board_id FROM lists WHERE id=?", (lid,)).fetchone()["board_id"]
|
|
p = c.execute("SELECT COALESCE(MAX(position),-1) m FROM cards WHERE list_id=?", (lid,)).fetchone()["m"]
|
|
c.execute("INSERT INTO cards VALUES (?,?,?,?,?,?)", (cid, lid, bid, data.title, data.description, p+1))
|
|
broadcast({"type": "card:create", "data": {"id": cid, "list_id": lid, "board_id": bid, "title": data.title, "description": data.description, "position": p+1}})
|
|
return {"id": cid, "list_id": lid, "board_id": bid, "title": data.title, "description": data.description, "position": p+1}
|
|
|
|
@app.patch("/api/cards/{cid}")
|
|
def update_card(cid: str, data: dict):
|
|
with db() as c:
|
|
u, ps = [], []
|
|
for k in ("title","description","position","list_id"):
|
|
if k in data: u.append(f"{k}=?"); ps.append(data[k])
|
|
if u:
|
|
ps.append(cid)
|
|
c.execute(f"UPDATE cards SET {', '.join(u)} WHERE id=?", ps)
|
|
row = c.execute("SELECT * FROM cards WHERE id=?", (cid,)).fetchone()
|
|
broadcast({"type": "card:update", "data": dict(row)})
|
|
return {"ok": True}
|
|
|
|
@app.delete("/api/cards/{cid}")
|
|
def delete_card(cid: str):
|
|
with db() as c:
|
|
c.execute("DELETE FROM cards WHERE id=?", (cid,))
|
|
broadcast({"type": "card:delete", "data": {"id": cid}})
|
|
return {"ok": True}
|
|
|
|
# WebSocket broadcast
|
|
clients = set()
|
|
|
|
def broadcast(msg):
|
|
payload = json.dumps(msg, ensure_ascii=False)
|
|
for c in list(clients):
|
|
try:
|
|
import asyncio
|
|
asyncio.get_event_loop().run_until_complete(c.send(payload))
|
|
except:
|
|
clients.discard(c)
|
|
|
|
@app.websocket("/ws")
|
|
async def ws(websocket: WebSocket):
|
|
await websocket.accept()
|
|
clients.add(websocket)
|
|
try:
|
|
while True:
|
|
await websocket.receive_text()
|
|
except WebSocketDisconnect:
|
|
clients.discard(websocket)
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=4000)
|