- pages table: added share_mode (private/invited/anyone) and published (bool) - board.py: view_page now loads share_mode + published from DB - page_editor.html: initializes shareTab, generalAccess, pagePublished from server - Clipboard: fallback to textarea copy for HTTP (no HTTPS required) - Toast: guaranteed visible with fixed positioning and Alpine x-show - Sidebar Shared section: renders shared_pages (pages with share_mode != private)
314 lines
14 KiB
Python
314 lines
14 KiB
Python
"""FlowDeck — Database layer (SQLite with WAL mode)."""
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
|
|
|
|
def get_db_path() -> Path:
|
|
from app.config import settings
|
|
return settings.db_path
|
|
|
|
|
|
def init_db():
|
|
"""Create tables if they don't exist."""
|
|
db_path = get_db_path()
|
|
if str(db_path) != ":memory:":
|
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with get_conn() as conn:
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
login TEXT NOT NULL UNIQUE,
|
|
full_name TEXT NOT NULL DEFAULT '',
|
|
email TEXT NOT NULL DEFAULT '',
|
|
avatar_url TEXT NOT NULL DEFAULT '',
|
|
is_admin BOOLEAN NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_tokens (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
gitea_user_id INTEGER NOT NULL UNIQUE,
|
|
gitea_token TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS boards (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
project_owner TEXT NOT NULL,
|
|
project_name TEXT NOT NULL,
|
|
columns_json TEXT NOT NULL DEFAULT '["Backlog","À faire","En cours","Révision","Terminé"]',
|
|
wip_limits_json TEXT DEFAULT '{}',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(project_owner, project_name)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS cards (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
board_id INTEGER NOT NULL REFERENCES boards(id),
|
|
gitea_issue_id INTEGER,
|
|
column_name TEXT NOT NULL DEFAULT 'Backlog',
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
priority TEXT DEFAULT 'Medium',
|
|
due_date TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS notes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
project_owner TEXT NOT NULL,
|
|
project_name TEXT NOT NULL,
|
|
title TEXT NOT NULL DEFAULT 'Notes',
|
|
content TEXT NOT NULL DEFAULT '',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(project_owner, project_name, title)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS col_mapping (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
board_id INTEGER NOT NULL REFERENCES boards(id),
|
|
column_name TEXT NOT NULL,
|
|
gitea_label TEXT NOT NULL,
|
|
close_issue BOOLEAN NOT NULL DEFAULT 0,
|
|
UNIQUE(board_id, column_name)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS checklists (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
board_id INTEGER NOT NULL,
|
|
gitea_issue_id INTEGER NOT NULL,
|
|
title TEXT NOT NULL DEFAULT 'Checklist',
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(board_id, gitea_issue_id, title)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS checklist_items (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
checklist_id INTEGER NOT NULL REFERENCES checklists(id) ON DELETE CASCADE,
|
|
content TEXT NOT NULL DEFAULT '',
|
|
checked BOOLEAN NOT NULL DEFAULT 0,
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS project_properties (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
project_owner TEXT NOT NULL,
|
|
project_name TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
prop_type TEXT NOT NULL DEFAULT 'select', -- select, multi_select, date, person, text
|
|
options_json TEXT DEFAULT '[]', -- for select/multi_select
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(project_owner, project_name, name)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS property_values (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
property_id INTEGER NOT NULL REFERENCES project_properties(id) ON DELETE CASCADE,
|
|
gitea_issue_id INTEGER NOT NULL,
|
|
value TEXT NOT NULL DEFAULT '',
|
|
UNIQUE(property_id, gitea_issue_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ai_keywords (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
project_owner TEXT NOT NULL,
|
|
project_name TEXT NOT NULL,
|
|
keyword TEXT NOT NULL,
|
|
color TEXT NOT NULL DEFAULT '#787774',
|
|
usage_count INTEGER NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(project_owner, project_name, keyword)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
workspace TEXT NOT NULL,
|
|
title TEXT NOT NULL DEFAULT 'New page',
|
|
content TEXT NOT NULL DEFAULT '',
|
|
parent_section TEXT DEFAULT 'Private',
|
|
parent_id INTEGER REFERENCES pages(id),
|
|
share_mode TEXT DEFAULT 'private',
|
|
published INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- v1.3.0: Database Concept — Notion-style collections
|
|
CREATE TABLE IF NOT EXISTS collections (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
description TEXT DEFAULT '',
|
|
icon TEXT DEFAULT '📋',
|
|
schema_json TEXT NOT NULL DEFAULT '[]',
|
|
gitea_owner TEXT,
|
|
gitea_repo TEXT,
|
|
is_locked BOOLEAN NOT NULL DEFAULT 0,
|
|
is_inline BOOLEAN NOT NULL DEFAULT 0,
|
|
parent_page_id INTEGER,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS collection_pages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
|
title TEXT NOT NULL DEFAULT '',
|
|
icon TEXT DEFAULT '📄',
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
parent_id INTEGER REFERENCES collection_pages(id),
|
|
gitea_issue_id INTEGER,
|
|
gitea_issue_number INTEGER,
|
|
property_values_json TEXT NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_cp_collection ON collection_pages(collection_id, position);
|
|
CREATE INDEX IF NOT EXISTS idx_cp_parent ON collection_pages(parent_id);
|
|
CREATE INDEX IF NOT EXISTS idx_cp_gitea ON collection_pages(gitea_issue_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS collection_views (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL DEFAULT 'Default View',
|
|
view_type TEXT NOT NULL DEFAULT 'table',
|
|
config_json TEXT NOT NULL DEFAULT '{}',
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- v1.4.0: Propriétés Avancées — collection_properties (remplace project_properties)
|
|
CREATE TABLE IF NOT EXISTS collection_properties (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
prop_type TEXT NOT NULL DEFAULT 'text',
|
|
options_json TEXT DEFAULT '[]',
|
|
number_format TEXT DEFAULT 'number',
|
|
related_collection_id INTEGER REFERENCES collections(id) ON DELETE SET NULL,
|
|
reverse_name TEXT,
|
|
relation_property_id INTEGER REFERENCES collection_properties(id) ON DELETE SET NULL,
|
|
target_property_id INTEGER REFERENCES collection_properties(id) ON DELETE SET NULL,
|
|
rollup_function TEXT,
|
|
formula_expression TEXT,
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
required BOOLEAN NOT NULL DEFAULT 0,
|
|
visible_in_views BOOLEAN NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(collection_id, name)
|
|
);
|
|
|
|
-- v2.0.0: Multi-user — workspaces, members, comments, history, favorites, templates
|
|
CREATE TABLE IF NOT EXISTS workspaces (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
owner_id INTEGER NOT NULL REFERENCES users(id),
|
|
settings_json TEXT NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS workspace_members (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
role TEXT NOT NULL DEFAULT 'editor',
|
|
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(workspace_id, user_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS comments (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
page_id INTEGER NOT NULL REFERENCES collection_pages(id) ON DELETE CASCADE,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
body TEXT NOT NULL DEFAULT '',
|
|
parent_id INTEGER REFERENCES comments(id),
|
|
resolved BOOLEAN NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS page_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
page_id INTEGER NOT NULL REFERENCES collection_pages(id) ON DELETE CASCADE,
|
|
user_id INTEGER REFERENCES users(id),
|
|
change_type TEXT NOT NULL,
|
|
snapshot_json TEXT NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- v2.2.0: Migrate favorites — drop old schema referencing collection_pages
|
|
DROP TABLE IF EXISTS favorites;
|
|
|
|
CREATE TABLE IF NOT EXISTS favorites (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
page_id INTEGER NOT NULL REFERENCES pages(id),
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(user_id, page_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS database_templates (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
description TEXT DEFAULT '',
|
|
schema_json TEXT NOT NULL DEFAULT '[]',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS page_templates (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL DEFAULT 'Default',
|
|
property_values_json TEXT NOT NULL DEFAULT '{}',
|
|
content_json TEXT DEFAULT '[]',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
""")
|
|
# Migration: add parent_id if missing (v1.0.0+)
|
|
try:
|
|
conn.execute("ALTER TABLE pages ADD COLUMN parent_id INTEGER REFERENCES pages(id)")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
# Migration: add sort_order for drag & drop tree reordering (v1.1.0+)
|
|
try:
|
|
conn.execute("ALTER TABLE pages ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
# Migration: add content_format for Notion-style block editor (v1.2.0+)
|
|
# 'markdown' = legacy textarea, 'blocks' = JSON array of block objects
|
|
try:
|
|
conn.execute("ALTER TABLE pages ADD COLUMN content_format TEXT NOT NULL DEFAULT 'markdown'")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
# Migration: add deleted_at for trash/soft-delete
|
|
try:
|
|
conn.execute("ALTER TABLE pages ADD COLUMN deleted_at TEXT")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
conn.commit()
|
|
|
|
|
|
@contextmanager
|
|
def get_conn():
|
|
"""Yield a SQLite connection (WAL mode, foreign keys on)."""
|
|
db_path = get_db_path()
|
|
conn = sqlite3.connect(str(db_path))
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
try:
|
|
yield conn
|
|
finally:
|
|
conn.close()
|