From 8e8b15a1d8d8a938d953b1831826d1a1accbf869 Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Sat, 8 May 2021 22:08:11 -0700 Subject: [PATCH] Route for getting previous/next posts in pool --- client/js/controllers/post_main_controller.js | 13 ++- .../controls/pool_navigator_list_control.js | 2 +- client/js/models/post_list.js | 9 +++ server/requirements.txt | 1 + server/szurubooru/api/post_api.py | 13 +++ server/szurubooru/func/posts.py | 79 ++++++++++++++++++- server/szurubooru/migrations/env.py | 4 + server/szurubooru/migrations/functions.py | 46 +++++++++++ ...8dc7_add_get_pool_posts_around_function.py | 33 ++++++++ 9 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 server/szurubooru/migrations/functions.py create mode 100644 server/szurubooru/migrations/versions/f0b8a4298dc7_add_get_pool_posts_around_function.py diff --git a/client/js/controllers/post_main_controller.js b/client/js/controllers/post_main_controller.js index 871d98dd..a55acb9d 100644 --- a/client/js/controllers/post_main_controller.js +++ b/client/js/controllers/post_main_controller.js @@ -16,6 +16,14 @@ class PostMainController extends BasePostController { constructor(ctx, editMode) { super(ctx); + let poolPostsAround = Promise.resolve({results: [], activePool: null}) + if (api.hasPrivilege("pools.list") && api.hasPrivilege("pools.view")) { + poolPostsAround = PostList.getPoolPostsAround( + ctxt.parameters.id, + parameters ? parameters.query : null + ); + } + let parameters = ctx.parameters; Promise.all([ Post.get(ctx.parameters.id), @@ -23,9 +31,10 @@ class PostMainController extends BasePostController { ctx.parameters.id, parameters ? parameters.query : null ), + poolPostsAround ]).then( (responses) => { - const [post, aroundResponse] = responses; + const [post, aroundResponse, poolPostsAroundResponse] = responses; // remove junk from query, but save it into history so that it can // be still accessed after history navigation / page refresh @@ -44,6 +53,8 @@ class PostMainController extends BasePostController { this._post = post; this._view = new PostMainView({ post: post, + poolPostsAround: poolPostsAroundResponse.results, + activePool: poolPostsAroundResponse.activePool, editMode: editMode, prevPostId: aroundResponse.prev ? aroundResponse.prev.id diff --git a/client/js/controls/pool_navigator_list_control.js b/client/js/controls/pool_navigator_list_control.js index 88c52d32..dd6a09b7 100644 --- a/client/js/controls/pool_navigator_list_control.js +++ b/client/js/controls/pool_navigator_list_control.js @@ -7,7 +7,7 @@ const PoolNavigatorControl = require("../controls/pool_navigator_control.js"); const template = views.getTemplate("pool-navigator-list"); class PoolNavigatorListControl extends events.EventTarget { - constructor(hostNode, a) { + constructor(hostNode, pools) { super(); this._hostNode = hostNode; diff --git a/client/js/models/post_list.js b/client/js/models/post_list.js index 8c2c9d4e..884dc5f0 100644 --- a/client/js/models/post_list.js +++ b/client/js/models/post_list.js @@ -16,6 +16,15 @@ class PostList extends AbstractList { ); } + static getPoolPostsAround(id, searchQuery) { + return api.get( + uri.formatApiLink("post", id, "pool-posts-around", { + query: PostList._decorateSearchQuery(searchQuery || ""), + fields: "id", + }) + ); + } + static search(text, offset, limit, fields) { return api .get( diff --git a/server/requirements.txt b/server/requirements.txt index 043c4f12..7df6a6d5 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -9,4 +9,5 @@ pillow>=4.3.0 pynacl>=1.2.1 pytz>=2018.3 pyRFC3339>=1.0 +alembic_utils>=0.5.6 youtube_dl diff --git a/server/szurubooru/api/post_api.py b/server/szurubooru/api/post_api.py index daba7f7e..fe5d1a3a 100644 --- a/server/szurubooru/api/post_api.py +++ b/server/szurubooru/api/post_api.py @@ -284,6 +284,19 @@ def get_posts_around( ) +@rest.routes.get("/post/(?P[^/]+)/pool-posts-around/?") +def get_pool_posts_around( + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:list") + auth.verify_privilege(ctx.user, "pools:list") + auth.verify_privilege(ctx.user, "pools:view") + _search_executor_config.user = ctx.user + post = _get_post(params) + results = posts.get_pool_posts_around(post) + return posts.serialize_pool_posts_around(results) + + @rest.routes.post("/posts/reverse-search/?") def get_posts_by_image( ctx: rest.Context, _params: Dict[str, str] = {} diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index 0493681e..cc553167 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -2,6 +2,7 @@ import hmac import logging from datetime import datetime from typing import Any, Callable, Dict, List, Optional, Tuple +from collections import namedtuple import sqlalchemy as sa @@ -134,12 +135,16 @@ def get_post_content_path(post: model.Post) -> str: ) +def get_post_thumbnail_path_from_id(post_id: int) -> str: + return "generated-thumbnails/%d_%s.jpg" % ( + post_id, + get_post_security_hash(post_id), + ) + + def get_post_thumbnail_path(post: model.Post) -> str: assert post - return "generated-thumbnails/%d_%s.jpg" % ( - post.post_id, - get_post_security_hash(post.post_id), - ) + return get_post_thumbnail_path_from_id(post.post_id) def get_post_thumbnail_backup_path(post: model.Post) -> str: @@ -967,3 +972,69 @@ def search_by_image(image_content: bytes) -> List[Tuple[float, model.Post]]: ] else: return [] + + +PoolPostsAround = namedtuple('PoolPostsAround', 'pool prev_post next_post') + +def get_pool_posts_around(post: model.Post) -> List[PoolPostsAround]: + around = dict() + pool_ids = set() + post_ids = set() + + dbquery = """ + SELECT around.ord, around.pool_id, around.post_id, around.delta + FROM pool_post pp, + LATERAL get_pool_posts_around(pp.pool_id, pp.post_id) around + WHERE pp.post_id = :post_id; + """ + + for order, pool_id, post_id, delta in db.session.execute(dbquery, {"post_id": post.post_id}): + if pool_id not in around: + around[pool_id] = [None, None] + if delta < 0: + around[pool_id][0] = post_id + elif delta > 0: + around[pool_id][1] = post_id + pool_ids.add(pool_id) + post_ids.add(post_id) + + pools = dict() + posts = dict() + + for pool in db.session.query(model.Pool).filter(model.Pool.pool_id.in_(pool_ids)).all(): + pools[pool.pool_id] = pool + + for result in db.session.query(model.Post.post_id).filter(model.Post.post_id.in_(post_ids)).all(): + post_id = result[0] + posts[post_id] = { "id": post_id, "thumbnailUrl": get_post_thumbnail_path_from_id(post_id) } + + results = [] + + for pool_id, entry in around.items(): + prev_post = None + next_post = None + if entry[0] is not None: + prev_post = posts[entry[0]] + if entry[1] is not None: + next_post = posts[entry[1]] + results.append(PoolPostsAround(pools[pool_id], prev_post, next_post)) + + return results + + +def sort_pool_posts_around(around: List[PoolPostsAround]) -> List[PoolPostsAround]: + return sorted( + around, + key=lambda entry: entry.pool.pool_id, + ) + + +def serialize_pool_posts_around(around: List[PoolPostsAround]) -> Optional[rest.Response]: + return [ + { + "pool": pools.serialize_micro_pool(entry.pool), + "prev_post": entry.prev_post, + "next_post": entry.next_post + } + for entry in sort_pool_posts_around(around) + ] diff --git a/server/szurubooru/migrations/env.py b/server/szurubooru/migrations/env.py index cd4f6ad1..6b155888 100644 --- a/server/szurubooru/migrations/env.py +++ b/server/szurubooru/migrations/env.py @@ -11,6 +11,7 @@ import sys from time import sleep import alembic +from alembic_utils.replaceable_entity import register_entities import sqlalchemy as sa @@ -21,6 +22,9 @@ sys.path.append(os.path.join(dir_to_self, *[os.pardir] * 2)) import szurubooru.config # noqa: E402 import szurubooru.model.base # noqa: E402 + +from szurubooru.migrations.functions import get_pool_posts_around # noqa: E402 +register_entities([get_pool_posts_around]) # fmt: on diff --git a/server/szurubooru/migrations/functions.py b/server/szurubooru/migrations/functions.py new file mode 100644 index 00000000..b6ebb069 --- /dev/null +++ b/server/szurubooru/migrations/functions.py @@ -0,0 +1,46 @@ +from alembic_utils.pg_function import PGFunction + +get_pool_posts_around = PGFunction.from_sql(""" +CREATE OR REPLACE FUNCTION public.get_pool_posts_around( + P_POOL_ID int, + P_POST_ID int +) + RETURNS TABLE ( + ORD int, + POOL_ID int, + POST_ID int, + DELTA int + ) + LANGUAGE PLPGSQL +AS $$ +BEGIN + RETURN QUERY WITH main AS ( + SELECT * FROM pool_post WHERE pool_post.pool_id = P_POOL_ID AND pool_post.post_id = P_POST_ID + ), + around AS ( + (SELECT pool_post.ord, + pool_post.pool_id, + pool_post.post_id, + 1 as delta, + main.ord AS target_ord, + main.pool_id AS target_pool_id + FROM pool_post, main + WHERE pool_post.ord > main.ord + AND pool_post.pool_id = main.pool_id + ORDER BY pool_post.ord ASC LIMIT 1) + UNION + (SELECT pool_post.ord, + pool_post.pool_id, + pool_post.post_id, + -1 as delta, + main.ord AS target_ord, + main.pool_id AS target_pool_id + FROM pool_post, main + WHERE pool_post.ord < main.ord + AND pool_post.pool_id = main.pool_id + ORDER BY pool_post.ord DESC LIMIT 1) + ) + SELECT around.ord, around.pool_id, around.post_id, around.delta FROM around; +END +$$ +""") diff --git a/server/szurubooru/migrations/versions/f0b8a4298dc7_add_get_pool_posts_around_function.py b/server/szurubooru/migrations/versions/f0b8a4298dc7_add_get_pool_posts_around_function.py new file mode 100644 index 00000000..3882a3af --- /dev/null +++ b/server/szurubooru/migrations/versions/f0b8a4298dc7_add_get_pool_posts_around_function.py @@ -0,0 +1,33 @@ +''' +add get pool posts around function + +Revision ID: f0b8a4298dc7 +Created at: 2021-05-08 21:23:48.782025 +''' + +import sqlalchemy as sa +from alembic import op + +from alembic_utils.pg_function import PGFunction +from sqlalchemy import text as sql_text + +revision = 'f0b8a4298dc7' +down_revision = 'adcd63ff76a2' +branch_labels = None +depends_on = None + +def upgrade(): + public_get_pool_posts_around = PGFunction( + schema="public", + signature="get_pool_posts_around( P_POOL_ID int, P_POST_ID int )", + definition='returns TABLE (\n ORD int,\n POOL_ID int,\n POST_ID int,\n DELTA int\n )\n LANGUAGE PLPGSQL\nAS $$\nBEGIN\n RETURN QUERY WITH main AS (\n SELECT * FROM pool_post WHERE pool_post.pool_id = P_POOL_ID AND pool_post.post_id = P_POST_ID\n ),\n around AS (\n (SELECT pool_post.ord,\n pool_post.pool_id,\n pool_post.post_id,\n 1 as delta,\n main.ord AS target_ord,\n main.pool_id AS target_pool_id\n FROM pool_post, main\n WHERE pool_post.ord > main.ord\n AND pool_post.pool_id = main.pool_id\n ORDER BY pool_post.ord ASC LIMIT 1)\n UNION\n (SELECT pool_post.ord,\n pool_post.pool_id,\n pool_post.post_id,\n -1 as delta,\n main.ord AS target_ord,\n main.pool_id AS target_pool_id\n FROM pool_post, main\n WHERE pool_post.ord < main.ord\n AND pool_post.pool_id = main.pool_id\n ORDER BY pool_post.ord DESC LIMIT 1)\n )\n SELECT around.ord, around.pool_id, around.post_id, around.delta FROM around;\nEND\n$$' + ) + op.create_entity(public_get_pool_posts_around) + +def downgrade(): + public_get_pool_posts_around = PGFunction( + schema="public", + signature="get_pool_posts_around( P_POOL_ID int, P_POST_ID int )", + definition='returns TABLE (\n ORD int,\n POOL_ID int,\n POST_ID int,\n DELTA int\n )\n LANGUAGE PLPGSQL\nAS $$\nBEGIN\n RETURN QUERY WITH main AS (\n SELECT * FROM pool_post WHERE pool_post.pool_id = P_POOL_ID AND pool_post.post_id = P_POST_ID\n ),\n around AS (\n (SELECT pool_post.ord,\n pool_post.pool_id,\n pool_post.post_id,\n 1 as delta,\n main.ord AS target_ord,\n main.pool_id AS target_pool_id\n FROM pool_post, main\n WHERE pool_post.ord > main.ord\n AND pool_post.pool_id = main.pool_id\n ORDER BY pool_post.ord ASC LIMIT 1)\n UNION\n (SELECT pool_post.ord,\n pool_post.pool_id,\n pool_post.post_id,\n -1 as delta,\n main.ord AS target_ord,\n main.pool_id AS target_pool_id\n FROM pool_post, main\n WHERE pool_post.ord < main.ord\n AND pool_post.pool_id = main.pool_id\n ORDER BY pool_post.ord DESC LIMIT 1)\n )\n SELECT around.ord, around.pool_id, around.post_id, around.delta FROM around;\nEND\n$$' + ) + op.drop_entity(public_get_pool_posts_around)