diff --git a/app/migrations.py b/app/migrations.py index 523ff1b..3df9fb3 100644 --- a/app/migrations.py +++ b/app/migrations.py @@ -9,6 +9,19 @@ def add_signature_format(): db.execute('ALTER TABLE "users" ADD COLUMN "signature_markup_language" TEXT NOT NULL DEFAULT "babycode"') db.execute('ALTER TABLE "users" ADD COLUMN "signature_format_version" INTEGER DEFAULT NULL') +def create_default_bookmark_collections(): + from .constants import PermissionLevel + q = """SELECT users.id FROM users + LEFT JOIN bookmark_collections bc ON (users.id = bc.user_id AND bc.is_default = TRUE) + WHERE bc.id IS NULL and users.permission IS NOT ?""" + user_ids_without_default_collection = db.query(q, PermissionLevel.SYSTEM.value) + if len(user_ids_without_default_collection) == 0: + return + + from .models import BookmarkCollections + for user in user_ids_without_default_collection: + BookmarkCollections.create_default(user['id']) + # format: [str|tuple(str, any...)|callable] MIGRATIONS = [ migrate_old_avatars, @@ -16,6 +29,7 @@ MIGRATIONS = [ 'ALTER TABLE "users" ADD COLUMN "invited_by" INTEGER REFERENCES users(id)', # invitation system 'ALTER TABLE "post_history" ADD COLUMN "format_version" INTEGER DEFAULT NULL', add_signature_format, + create_default_bookmark_collections, ] def run_migrations(): diff --git a/app/models.py b/app/models.py index 1a26f5b..da2e7ab 100644 --- a/app/models.py +++ b/app/models.py @@ -101,6 +101,11 @@ class Users(Model): return False + def get_bookmark_collections(self): + q = 'SELECT id FROM bookmark_collections WHERE user_id = ? ORDER BY sort_order ASC' + res = db.query(q, self.id) + return [BookmarkCollections.find({'id': bc['id']}) for bc in res] + class Topics(Model): table = "topics" @@ -225,7 +230,7 @@ class Threads(Model): class Posts(Model): FULL_POSTS_QUERY = """ SELECT - posts.id, posts.created_at, post_history.content, post_history.edited_at, users.username, users.status, avatars.file_path AS avatar_path, posts.thread_id, users.id AS user_id, post_history.original_markup, users.signature_rendered, threads.slug AS thread_slug, threads.is_locked AS thread_is_locked + posts.id, posts.created_at, post_history.content, post_history.edited_at, users.username, users.status, avatars.file_path AS avatar_path, posts.thread_id, users.id AS user_id, post_history.original_markup, users.signature_rendered, threads.slug AS thread_slug, threads.is_locked AS thread_is_locked, threads.title AS thread_title FROM posts JOIN @@ -239,6 +244,10 @@ class Posts(Model): table = "posts" + def get_full_post_view(self): + q = f'{self.FULL_POSTS_QUERY} WHERE posts.id = ?' + return db.fetch_one(q, self.id) + class PostHistory(Model): table = "post_history" @@ -317,3 +326,45 @@ class PasswordResetLinks(Model): class InviteKeys(Model): table = 'invite_keys' + + +class BookmarkCollections(Model): + table = 'bookmark_collections' + + @classmethod + def create_default(cls, user_id): + q = """INSERT INTO bookmark_collections (user_id, name, is_default, sort_order) + VALUES (?, "Bookmarks", 1, 0) RETURNING id + """ + res = db.fetch_one(q, user_id) + + def has_posts(self): + q = 'SELECT EXISTS(SELECT 1 FROM bookmarked_posts WHERE collection_id = ?) as e' + res = db.fetch_one(q, self.id)['e'] + return int(res) == 1 + + def has_threads(self): + q = 'SELECT EXISTS(SELECT 1 FROM bookmarked_threads WHERE collection_id = ?) as e' + res = db.fetch_one(q, self.id)['e'] + return int(res) == 1 + + def is_empty(self): + return not (self.has_posts() or self.has_threads()) + + def get_threads(self): + q = 'SELECT thread_id FROM bookmarked_threads WHERE collection_id = ?' + res = db.query(q, self.id) + return [Threads.find({'id': bt['thread_id']}) for bt in res] + + def get_posts(self): + q = 'SELECT post_id FROM bookmarked_posts WHERE collection_id = ?' + res = db.query(q, self.id) + return [Posts.find({'id': bt['post_id']}) for bt in res] + + +class BookmarkedPosts(Model): + table = 'bookmarked_posts' + + +class BookmarkedThreads(Model): + table = 'bookmarked_threads' diff --git a/app/schema.py b/app/schema.py index 7351947..54ba328 100644 --- a/app/schema.py +++ b/app/schema.py @@ -96,6 +96,30 @@ SCHEMA = [ "key" TEXT NOT NULL UNIQUE )""", + """CREATE TABLE IF NOT EXISTS "bookmark_collections" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "user_id" REFERENCES users(id) ON DELETE CASCADE, + "name" TEXT NOT NULL, + "is_default" BOOLEAN NOT NULL DEFAULT FALSE, + "sort_order" INTEGER NOT NULL DEFAULT 0 + )""", + + """CREATE TABLE IF NOT EXISTS "bookmarked_posts" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "collection_id" REFERENCES bookmark_collections(id) ON DELETE CASCADE, + "post_id" REFERENCES posts(id) ON DELETE CASCADE, + "note" TEXT, + UNIQUE(collection_id, post_id) + )""", + + """CREATE TABLE IF NOT EXISTS "bookmarked_threads" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "collection_id" REFERENCES bookmark_collections(id) ON DELETE CASCADE, + "thread_id" REFERENCES threads(id) ON DELETE CASCADE, + "note" TEXT, + UNIQUE(collection_id, thread_id) + )""", + # INDEXES "CREATE INDEX IF NOT EXISTS idx_post_history_post_id ON post_history(post_id)", "CREATE INDEX IF NOT EXISTS idx_posts_thread ON posts(thread_id, created_at, id)", @@ -110,6 +134,15 @@ SCHEMA = [ "CREATE INDEX IF NOT EXISTS reaction_post_text ON reactions(post_id, reaction_text)", "CREATE INDEX IF NOT EXISTS reaction_user_post_text ON reactions(user_id, post_id, reaction_text)", + + "CREATE INDEX IF NOT EXISTS idx_bookmark_collections_user_id ON bookmark_collections(user_id)", + "CREATE INDEX IF NOT EXISTS idx_bookmark_collections_user_default ON bookmark_collections(user_id, is_default) WHERE is_default = 1", + + "CREATE INDEX IF NOT EXISTS idx_bookmarked_posts_collection ON bookmarked_posts(collection_id)", + "CREATE INDEX IF NOT EXISTS idx_bookmarked_posts_post ON bookmarked_posts(post_id)", + + "CREATE INDEX IF NOT EXISTS idx_bookmarked_threads_collection ON bookmarked_threads(collection_id)", + "CREATE INDEX IF NOT EXISTS idx_bookmarked_threads_thread ON bookmarked_threads(thread_id)", ] def create():