From 81645864ecaa2a43a55c333779e3e4ede4cbf404 Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Sat, 8 May 2021 02:38:40 -0700 Subject: [PATCH 01/11] Support sorting post search results by pool post order --- .../search/configs/post_search_config.py | 37 +++++++++++++---- server/szurubooru/search/configs/util.py | 1 + server/szurubooru/search/executor.py | 16 +++++--- server/szurubooru/search/typing.py | 2 +- .../search/configs/test_post_search_config.py | 40 +++++++++++++++++++ 5 files changed, 81 insertions(+), 15 deletions(-) diff --git a/server/szurubooru/search/configs/post_search_config.py b/server/szurubooru/search/configs/post_search_config.py index ddc003b7..8e5fe71a 100644 --- a/server/szurubooru/search/configs/post_search_config.py +++ b/server/szurubooru/search/configs/post_search_config.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict, Optional, Tuple, Callable, Union import sqlalchemy as sa @@ -114,17 +114,30 @@ def _pool_filter( query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool ) -> SaQuery: assert criterion - return search_util.create_subquery_filter( - model.Post.post_id, - model.PoolPost.post_id, - model.PoolPost.pool_id, - search_util.create_num_filter, - )(query, criterion, negated) + from szurubooru.search.configs import util as search_util + subquery = db.session.query(model.PoolPost.post_id.label("foreign_id")) + subquery = subquery.options(sa.orm.lazyload("*")) + subquery = search_util.create_num_filter(model.PoolPost.pool_id)(subquery, criterion, False) + subquery = subquery.subquery("t") + expression = model.Post.post_id.in_(subquery) + if negated: + expression = ~expression + return query.filter(expression) + + +def _pool_sort( + query: SaQuery, pool_id: Optional[int] +) -> SaQuery: + if pool_id is None: + return query + return query.join(model.PoolPost, sa.and_(model.PoolPost.post_id == model.Post.post_id, model.PoolPost.pool_id == pool_id)) \ + .order_by(model.PoolPost.order.desc()) class PostSearchConfig(BaseSearchConfig): def __init__(self) -> None: self.user = None # type: Optional[model.User] + self.pool_id = None # type: Optional[int] def on_search_query_parsed(self, search_query: SearchQuery) -> SaQuery: new_special_tokens = [] @@ -149,6 +162,10 @@ class PostSearchConfig(BaseSearchConfig): else: new_special_tokens.append(token) search_query.special_tokens = new_special_tokens + self.pool_id = None + for token in search_query.named_tokens: + if token.name == "pool" and isinstance(token.criterion, criteria.PlainCriterion): + self.pool_id = token.criterion.value def create_around_query(self) -> SaQuery: return db.session.query(model.Post).options(sa.orm.lazyload("*")) @@ -353,7 +370,7 @@ class PostSearchConfig(BaseSearchConfig): ) @property - def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: + def sort_columns(self) -> Dict[str, Union[Tuple[SaColumn, str], Callable[[SaQuery], None]]]: return util.unalias_dict( [ ( @@ -415,6 +432,10 @@ class PostSearchConfig(BaseSearchConfig): ["feature-date", "feature-time"], (model.Post.last_feature_time, self.SORT_DESC), ), + ( + ["pool"], + lambda subquery: _pool_sort(subquery, self.pool_id) + ) ] ) diff --git a/server/szurubooru/search/configs/util.py b/server/szurubooru/search/configs/util.py index 58e6ebe5..fd40b43d 100644 --- a/server/szurubooru/search/configs/util.py +++ b/server/szurubooru/search/configs/util.py @@ -205,6 +205,7 @@ def create_subquery_filter( filter_column: SaColumn, filter_factory: SaColumn, subquery_decorator: Callable[[SaQuery], None] = None, + order: SaQuery = None, ) -> Filter: filter_func = filter_factory(filter_column) diff --git a/server/szurubooru/search/executor.py b/server/szurubooru/search/executor.py index a5ef9625..beb17f8a 100644 --- a/server/szurubooru/search/executor.py +++ b/server/szurubooru/search/executor.py @@ -181,14 +181,18 @@ class Executor: _format_dict_keys(self.config.sort_columns), ) ) - column, default_order = self.config.sort_columns[ + entry = self.config.sort_columns[ sort_token.name ] - order = _get_order(sort_token.order, default_order) - if order == sort_token.SORT_ASC: - db_query = db_query.order_by(column.asc()) - elif order == sort_token.SORT_DESC: - db_query = db_query.order_by(column.desc()) + if callable(entry): + db_query = entry(db_query) + else: + column, default_order = entry + order = _get_order(sort_token.order, default_order) + if order == sort_token.SORT_ASC: + db_query = db_query.order_by(column.asc()) + elif order == sort_token.SORT_DESC: + db_query = db_query.order_by(column.desc()) db_query = self.config.finalize_query(db_query) return db_query diff --git a/server/szurubooru/search/typing.py b/server/szurubooru/search/typing.py index 686c2cb6..011f7eae 100644 --- a/server/szurubooru/search/typing.py +++ b/server/szurubooru/search/typing.py @@ -1,4 +1,4 @@ -from typing import Any, Callable +from typing import Any, Callable, Union SaColumn = Any SaQuery = Any diff --git a/server/szurubooru/tests/search/configs/test_post_search_config.py b/server/szurubooru/tests/search/configs/test_post_search_config.py index 4fb8191a..0f5336e1 100644 --- a/server/szurubooru/tests/search/configs/test_post_search_config.py +++ b/server/szurubooru/tests/search/configs/test_post_search_config.py @@ -725,6 +725,7 @@ def test_filter_by_feature_date( "sort:fav-time", "sort:feature-date", "sort:feature-time", + "sort:pool", ], ) def test_sort_tokens(verify_unpaged, post_factory, input): @@ -863,3 +864,42 @@ def test_tumbleweed( db.session.flush() verify_unpaged("special:tumbleweed", [4]) verify_unpaged("-special:tumbleweed", [1, 2, 3]) + + +def test_sort_pool( + post_factory, pool_factory, pool_category_factory, verify_unpaged +): + post1 = post_factory(id=1) + post2 = post_factory(id=2) + post3 = post_factory(id=3) + post4 = post_factory(id=4) + pool1 = pool_factory( + id=1, + names=["pool1"], + description="desc", + category=pool_category_factory("test-cat1"), + ) + pool1.posts = [post1, post4, post3] + pool2 = pool_factory( + id=2, + names=["pool2"], + description="desc", + category=pool_category_factory("test-cat2"), + ) + pool2.posts = [post3, post4, post2] + db.session.add_all( + [ + post1, + post2, + post3, + post4, + pool1, + pool2 + ] + ) + db.session.flush() + verify_unpaged("pool:1 sort:pool", [1, 4, 3]) + verify_unpaged("pool:2 sort:pool", [3, 4, 2]) + verify_unpaged("pool:1 pool:2 sort:pool", [4, 3]) + verify_unpaged("pool:2 pool:1 sort:pool", [3, 4]) + verify_unpaged("sort:pool", [1, 2, 3, 4]) From 6fd48dcb5f0b3636847f281773cf13c1f6c49345 Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Sat, 8 May 2021 02:41:26 -0700 Subject: [PATCH 02/11] Sort by pool by default from pool details page --- client/html/pool_delete.tpl | 2 +- client/html/pool_summary.tpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/html/pool_delete.tpl b/client/html/pool_delete.tpl index 1ef7fb53..f279e0df 100644 --- a/client/html/pool_delete.tpl +++ b/client/html/pool_delete.tpl @@ -1,6 +1,6 @@
-

This pool has '><%- ctx.pool.postCount %> post(s).

+

This pool has '><%- ctx.pool.postCount %> post(s).

  • diff --git a/client/html/pool_summary.tpl b/client/html/pool_summary.tpl index 8f4e27d0..b20672e4 100644 --- a/client/html/pool_summary.tpl +++ b/client/html/pool_summary.tpl @@ -18,6 +18,6 @@

    <%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %> -

    This pool has '><%- ctx.pool.postCount %> post(s).

    +

    This pool has '><%- ctx.pool.postCount %> post(s).

From 47377395a2143fb0617265464799c905651fd9ca Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Sat, 8 May 2021 05:04:39 -0700 Subject: [PATCH 03/11] Thumbnail view in pool list --- client/css/pool-list-view.styl | 109 +++++++++++------- client/html/pools_page.tpl | 59 +++------- client/js/controllers/pool_list_controller.js | 10 +- client/package.json | 2 +- 4 files changed, 94 insertions(+), 86 deletions(-) diff --git a/client/css/pool-list-view.styl b/client/css/pool-list-view.styl index b7ac15ed..272ac931 100644 --- a/client/css/pool-list-view.styl +++ b/client/css/pool-list-view.styl @@ -1,47 +1,60 @@ @import colors .pool-list - table - width: 100% - border-spacing: 0 - text-align: left - line-height: 1.3em - tr:hover td - background: $top-navigation-color - th, td - padding: 0.1em 0.5em - th - white-space: nowrap - background: $top-navigation-color - .names - width: 84% - .post-count - text-align: center - width: 8% - .creation-time - text-align: center - width: 8% - white-space: pre - ul - list-style-type: none - margin: 0 - padding: 0 - display: inline - li - padding: 0 - display: inline - &:not(:last-child):after - content: ', ' - @media (max-width: 800px) - .posts - display: none + ul + list-style-type: none + margin: 0 + padding: 0 + display: flex + align-content: flex-end + flex-wrap: wrap + margin: 0 -0.25em -.darktheme .pool-list - table - tr:hover td - background: $top-navigation-color-darktheme - th - background: $top-navigation-color-darktheme + li + position: relative + flex-grow: 1 + margin: 0 0.25em 0.5em 0.25em + display: inline-block + text-align: left + min-width: 10em + width: 12vw + &:not(.flexbox-dummy) + min-height: 7.5em + height: 9vw + + .thumbnail-wrapper + display: inline-block + width: 100% + height: 100% + line-height: 80% + font-size: 80% + color: white + outline-offset: -3px + box-shadow: 0 0 0 1px rgba(0,0,0,0.2) + + .thumbnail + width: 100% + height: 100% + outline-offset: -3px + &:not(.empty) + background-position: 50% 30% + + .pool-name + color: black + font-size: 1em + text-align: center + a + width: 100% + display: inline-block + + &:hover + background: $post-thumbnail-border-color + .thumbnail + opacity: .9 + + &:hover a, a:active, a:focus + .thumbnail + outline: 4px solid $main-color !important .pool-list-header label @@ -61,3 +74,19 @@ .darktheme .pool-list-header .append color: $inactive-link-color-darktheme + +.post-flow + ul + li + min-width: inherit + width: inherit + &:not(.flexbox-dummy) + height: 14vw + .thumbnail + outline-offset: -1px + .thumbnail-wrapper.no-tags + .thumbnail + outline: 2px solid $post-thumbnail-no-tags-border-color + &:hover a, a:active, a:focus + .thumbnail + outline: 2px solid $main-color !important diff --git a/client/html/pools_page.tpl b/client/html/pools_page.tpl index 0d811808..48530ea3 100644 --- a/client/html/pools_page.tpl +++ b/client/html/pools_page.tpl @@ -1,48 +1,19 @@ -
+<% if (ctx.postFlow) { %>
<% } else { %>
<% } %> <% if (ctx.response.results.length) { %> - - - - - - - - <% for (let pool of ctx.response.results) { %> - - - - - - <% } %> - -
- <% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.query) { %> - '>Pool name(s) - <% } else { %> - '>Pool name(s) + - <% if (ctx.parameters.query == 'sort:post-count') { %> - '>Post count - <% } else { %> - '>Post count - <% } %> - - <% if (ctx.parameters.query == 'sort:creation-time') { %> - '>Created on - <% } else { %> - '>Created on - <% } %> -
-
    - <% for (let name of pool.names) { %> -
  • <%= ctx.makePoolLink(pool.id, false, false, pool, name) %>
  • - <% } %> -
-
- '><%- pool.postCount %> - - <%= ctx.makeRelativeTime(pool.creationTime) %> -
+ +
+ <%= ctx.makePoolLink(pool.id, false, false, pool, name) %> +
+ + <% } %> + <%= ctx.makeFlexboxAlign() %> + <% } %>
diff --git a/client/js/controllers/pool_list_controller.js b/client/js/controllers/pool_list_controller.js index 91d655c5..67608684 100644 --- a/client/js/controllers/pool_list_controller.js +++ b/client/js/controllers/pool_list_controller.js @@ -2,6 +2,7 @@ const router = require("../router.js"); const api = require("../api.js"); +const settings = require("../models/settings.js"); const uri = require("../util/uri.js"); const PoolList = require("../models/pool_list.js"); const topNavigation = require("../models/top_navigation.js"); @@ -42,7 +43,9 @@ class PoolListController { }); this._headerView.addEventListener( "submit", - (e) => this._evtSubmit(e), + (e) => this._evtSubmit(e) + ); + this._headerView.addEventListener( "navigate", (e) => this._evtNavigate(e) ); @@ -106,6 +109,11 @@ class PoolListController { ); }, pageRenderer: (pageCtx) => { + Object.assign(pageCtx, { + canViewPosts: api.hasPrivilege("posts:view"), + canViewPools: api.hasPrivilege("pools:view"), + postFlow: settings.get().postFlow, + }); return new PoolsPageView(pageCtx); }, }); diff --git a/client/package.json b/client/package.json index 6c816432..991700ec 100644 --- a/client/package.json +++ b/client/package.json @@ -3,7 +3,7 @@ "private": true, "scripts": { "build": "node build.js", - "watch": "c1=\"\";while :;do c2=$(find html js css img -type f -and -not -iname '*autogen*'|sort|xargs cat|md5sum);[[ $c1 != $c2 ]]&&npm run build -- --debug --no-vendor-js;c1=$c2;sleep 1;done" + "watch": "c1=\"\";while :;do c2=$(find html js css img -type f -and -not -iname '*autogen*'|sort|xargs cat|md5sum);[[ $c1 != $c2 ]]&&npm run build -- --debug --no-vendor-js --no-web-app-files --no-binary-assets;c1=$c2;sleep 1;done" }, "dependencies": { "dompurify": "^2.0.17", From 161a3939c907a87d4adad8af722eb7414998a8f6 Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Sat, 8 May 2021 16:15:39 -0700 Subject: [PATCH 04/11] Add new sort tokens for pools; update API doc --- doc/API.md | 91 ++++++++++--------- .../search/configs/pool_search_config.py | 4 +- 2 files changed, 50 insertions(+), 45 deletions(-) diff --git a/doc/API.md b/doc/API.md index 3d280fd1..804f60a5 100644 --- a/doc/API.md +++ b/doc/API.md @@ -793,38 +793,39 @@ data. **Sort style tokens** - | `` | Description | - | ---------------- | ------------------------------------------------ | - | `random` | as random as it can get | - | `id` | highest to lowest post number | - | `score` | highest scored | - | `tag-count` | with most tags | - | `comment-count` | most commented first | - | `fav-count` | loved by most | - | `note-count` | with most annotations | - | `relation-count` | with most relations | - | `feature-count` | most often featured | - | `file-size` | largest files first | - | `image-width` | widest images first | - | `image-height` | tallest images first | - | `image-area` | largest images first | - | `width` | alias of `image-width` | - | `height` | alias of `image-height` | - | `area` | alias of `image-area` | - | `creation-date` | newest to oldest (pretty much same as id) | - | `creation-time` | alias of `creation-date` | - | `date` | alias of `creation-date` | - | `time` | alias of `creation-date` | - | `last-edit-date` | like creation-date, only looks at last edit time | - | `last-edit-time` | alias of `last-edit-date` | - | `edit-date` | alias of `last-edit-date` | - | `edit-time` | alias of `last-edit-date` | - | `comment-date` | recently commented by anyone | - | `comment-time` | alias of `comment-date` | - | `fav-date` | recently added to favorites by anyone | - | `fav-time` | alias of `fav-date` | - | `feature-date` | recently featured | - | `feature-time` | alias of `feature-time` | + | `` | Description | + | ---------------- | ------------------------------------------------ | + | `random` | as random as it can get | + | `id` | highest to lowest post number | + | `score` | highest scored | + | `tag-count` | with most tags | + | `comment-count` | most commented first | + | `fav-count` | loved by most | + | `note-count` | with most annotations | + | `relation-count` | with most relations | + | `feature-count` | most often featured | + | `file-size` | largest files first | + | `image-width` | widest images first | + | `image-height` | tallest images first | + | `image-area` | largest images first | + | `width` | alias of `image-width` | + | `height` | alias of `image-height` | + | `area` | alias of `image-area` | + | `creation-date` | newest to oldest (pretty much same as id) | + | `creation-time` | alias of `creation-date` | + | `date` | alias of `creation-date` | + | `time` | alias of `creation-date` | + | `last-edit-date` | like creation-date, only looks at last edit time | + | `last-edit-time` | alias of `last-edit-date` | + | `edit-date` | alias of `last-edit-date` | + | `edit-time` | alias of `last-edit-date` | + | `comment-date` | recently commented by anyone | + | `comment-time` | alias of `comment-date` | + | `fav-date` | recently added to favorites by anyone | + | `fav-time` | alias of `fav-date` | + | `feature-date` | recently featured | + | `feature-time` | alias of `feature-time` | + | `pool` | post order of the pool referenced by the `pool:` named token in the same search query | **Special tokens** @@ -1333,6 +1334,7 @@ data. | `` | Description | | ------------------- | ----------------------------------------- | + | `id` | having given pool number | | `name` | having given name (accepts wildcards) | | `category` | having given category (accepts wildcards) | | `creation-date` | created at given date | @@ -1345,18 +1347,19 @@ data. **Sort style tokens** - | `` | Description | - | ------------------- | ---------------------------- | - | `random` | as random as it can get | - | `name` | A to Z | - | `category` | category (A to Z) | - | `creation-date` | recently created first | - | `creation-time` | alias of `creation-date` | - | `last-edit-date` | recently edited first | - | `last-edit-time` | alias of `creation-time` | - | `edit-date` | alias of `creation-time` | - | `edit-time` | alias of `creation-time` | - | `post-count` | used in most posts first | + | `` | Description | + | ------------------- | ---------------------------- | + | `random` | as random as it can get | + | `id` | highest to lowest pool number | + | `name` | A to Z | + | `category` | category (A to Z) | + | `creation-date` | recently created first | + | `creation-time` | alias of `creation-date` | + | `last-edit-date` | recently edited first | + | `last-edit-time` | alias of `creation-time` | + | `edit-date` | alias of `creation-time` | + | `edit-time` | alias of `creation-time` | + | `post-count` | used in most posts first | **Special tokens** diff --git a/server/szurubooru/search/configs/pool_search_config.py b/server/szurubooru/search/configs/pool_search_config.py index 88b30a6e..30b4d4ea 100644 --- a/server/szurubooru/search/configs/pool_search_config.py +++ b/server/szurubooru/search/configs/pool_search_config.py @@ -30,7 +30,7 @@ class PoolSearchConfig(BaseSearchConfig): raise NotImplementedError() def finalize_query(self, query: SaQuery) -> SaQuery: - return query.order_by(model.Pool.first_name.asc()) + return query.order_by(model.Pool.pool_id.desc()) @property def anonymous_filter(self) -> Filter: @@ -45,6 +45,7 @@ class PoolSearchConfig(BaseSearchConfig): def named_filters(self) -> Dict[str, Filter]: return util.unalias_dict( [ + (["id"], search_util.create_num_filter(model.Pool.pool_id)), ( ["name"], search_util.create_subquery_filter( @@ -91,6 +92,7 @@ class PoolSearchConfig(BaseSearchConfig): ["random"], (sa.sql.expression.func.random(), self.SORT_NONE), ), + (["id"], (model.Pool.pool_id, self.SORT_DESC)), (["name"], (model.Pool.first_name, self.SORT_ASC)), (["category"], (model.PoolCategory.name, self.SORT_ASC)), ( From eee9b70b0e91bd824bbeb9235a63f48a1ae5257e Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Sat, 8 May 2021 16:15:59 -0700 Subject: [PATCH 05/11] Stacked thumbnail appearance for pool list page --- client/css/pool-list-view.styl | 22 ++++++++++++++++++++-- client/html/pools_page.tpl | 4 ++-- client/js/util/views.js | 24 +++++++++++++++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/client/css/pool-list-view.styl b/client/css/pool-list-view.styl index 272ac931..37ec877c 100644 --- a/client/css/pool-list-view.styl +++ b/client/css/pool-list-view.styl @@ -3,7 +3,6 @@ .pool-list ul list-style-type: none - margin: 0 padding: 0 display: flex align-content: flex-end @@ -13,7 +12,7 @@ li position: relative flex-grow: 1 - margin: 0 0.25em 0.5em 0.25em + margin: 2em 1.5em 2em 1.2em; display: inline-block text-align: left min-width: 10em @@ -38,6 +37,23 @@ outline-offset: -3px &:not(.empty) background-position: 50% 30% + position: absolute + display: inline-block + + .thumbnail-1 + right: -0px; + top: -0px; + z-index: 30; + + .thumbnail-2 + right: -10px; + top: -10px; + z-index: 20; + + .thumbnail-3 + right: -20px; + top: -20px; + z-index: 10; .pool-name color: black @@ -80,9 +96,11 @@ li min-width: inherit width: inherit + margin: 0 0.25em 0.5em 0.25em &:not(.flexbox-dummy) height: 14vw .thumbnail + position: static outline-offset: -1px .thumbnail-wrapper.no-tags .thumbnail diff --git a/client/html/pools_page.tpl b/client/html/pools_page.tpl index 48530ea3..c935785f 100644 --- a/client/html/pools_page.tpl +++ b/client/html/pools_page.tpl @@ -4,8 +4,8 @@ <% for (let pool of ctx.response.results) { %>
  • - <% if (ctx.canViewPosts && pool.posts.length > 0) { %> - <%= ctx.makeThumbnail(pool.posts.at(0).thumbnailUrl) %> + <% if (ctx.canViewPosts) { %> + <%= ctx.makePoolThumbnails(pool.posts, ctx.postFlow) %> <% } %>
    diff --git a/client/js/util/views.js b/client/js/util/views.js index 38c98a13..f9470375 100644 --- a/client/js/util/views.js +++ b/client/js/util/views.js @@ -40,12 +40,12 @@ function makeRelativeTime(time) { ); } -function makeThumbnail(url) { +function makeThumbnail(url, klass) { return makeElement( "span", url ? { - class: "thumbnail", + class: klass || "thumbnail", style: `background-image: url(\'${url}\')`, } : { class: "thumbnail empty" }, @@ -53,6 +53,23 @@ function makeThumbnail(url) { ); } +function makePoolThumbnails(posts, postFlow) { + if (posts.length == 0) { + return makeThumbnail(null); + } + if (postFlow) { + return makeThumbnail(posts.at(0).thumbnailUrl); + } + + let s = ""; + + for (let i = 0; i < Math.min(3, posts.length); i++) { + s += makeThumbnail(posts.at(i).thumbnailUrl, "thumbnail thumbnail-" + (i+1)); + } + + return s; +} + function makeRadio(options) { _imbueId(options); return makeElement( @@ -254,7 +271,7 @@ function makePoolLink(id, includeHash, includeCount, pool, name) { misc.escapeHtml(text) ) : makeElement( - "span", + "div", { class: misc.makeCssName(category, "pool") }, misc.escapeHtml(text) ); @@ -436,6 +453,7 @@ function getTemplate(templatePath) { makeFileSize: makeFileSize, makeMarkdown: makeMarkdown, makeThumbnail: makeThumbnail, + makePoolThumbnails: makePoolThumbnails, makeRadio: makeRadio, makeCheckbox: makeCheckbox, makeSelect: makeSelect, From 676a5ff97cbdd2dbcf47331a76d11bb5f272ec5b Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Sat, 8 May 2021 16:42:30 -0700 Subject: [PATCH 06/11] Retrieve surrounding pool posts in pool search query --- .../search/configs/base_search_config.py | 17 +++++++++++ .../search/configs/post_search_config.py | 30 +++++++++++++++++++ server/szurubooru/search/executor.py | 13 +------- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/server/szurubooru/search/configs/base_search_config.py b/server/szurubooru/search/configs/base_search_config.py index d60f3617..34b93811 100644 --- a/server/szurubooru/search/configs/base_search_config.py +++ b/server/szurubooru/search/configs/base_search_config.py @@ -1,5 +1,7 @@ from typing import Callable, Dict, Optional, Tuple +import sqlalchemy as sa + from szurubooru.search import criteria, tokens from szurubooru.search.query import SearchQuery from szurubooru.search.typing import SaColumn, SaQuery @@ -24,6 +26,21 @@ class BaseSearchConfig: def create_around_query(self) -> SaQuery: raise NotImplementedError() + def create_around_filter_queries(self, filter_query: SaQuery, entity_id: int) -> Tuple[SaQuery, SaQuery]: + prev_filter_query = ( + filter_query.filter(self.id_column > entity_id) + .order_by(None) + .order_by(sa.func.abs(self.id_column - entity_id).asc()) + .limit(1) + ) + next_filter_query = ( + filter_query.filter(self.id_column < entity_id) + .order_by(None) + .order_by(sa.func.abs(self.id_column - entity_id).asc()) + .limit(1) + ) + return (prev_filter_query, next_filter_query) + def finalize_query(self, query: SaQuery) -> SaQuery: return query diff --git a/server/szurubooru/search/configs/post_search_config.py b/server/szurubooru/search/configs/post_search_config.py index 8e5fe71a..7fb7d82f 100644 --- a/server/szurubooru/search/configs/post_search_config.py +++ b/server/szurubooru/search/configs/post_search_config.py @@ -134,6 +134,31 @@ def _pool_sort( .order_by(model.PoolPost.order.desc()) +def _posts_around_pool(filter_query: SaQuery, post_id: int, pool_id: int) -> Tuple[SaQuery, SaQuery]: + this_order = db.session.query(model.PoolPost) \ + .filter(model.PoolPost.post_id == post_id) \ + .filter(model.PoolPost.pool_id == pool_id) \ + .one().order + + filter_query = db.session.query(model.Post) \ + .join(model.PoolPost, model.PoolPost.pool_id == pool_id) \ + .filter(model.PoolPost.post_id == model.Post.post_id) + + prev_filter_query = ( + filter_query.filter(model.PoolPost.order > this_order) + .order_by(None) + .order_by(sa.func.abs(model.PoolPost.order - this_order).asc()) + .limit(1) + ) + next_filter_query = ( + filter_query.filter(model.PoolPost.order < this_order) + .order_by(None) + .order_by(sa.func.abs(model.PoolPost.order - this_order).asc()) + .limit(1) + ) + return (prev_filter_query, next_filter_query) + + class PostSearchConfig(BaseSearchConfig): def __init__(self) -> None: self.user = None # type: Optional[model.User] @@ -170,6 +195,11 @@ class PostSearchConfig(BaseSearchConfig): def create_around_query(self) -> SaQuery: return db.session.query(model.Post).options(sa.orm.lazyload("*")) + def create_around_filter_queries(self, filter_query: SaQuery, entity_id: int) -> Tuple[SaQuery, SaQuery]: + if self.pool_id is not None: + return _posts_around_pool(filter_query, entity_id, self.pool_id) + return super(PostSearchConfig, self).create_around_filter_queries(filter_query, entity_id) + def create_filter_query(self, disable_eager_loads: bool) -> SaQuery: strategy = ( sa.orm.lazyload if disable_eager_loads else sa.orm.subqueryload diff --git a/server/szurubooru/search/executor.py b/server/szurubooru/search/executor.py index beb17f8a..6b39ff4a 100644 --- a/server/szurubooru/search/executor.py +++ b/server/szurubooru/search/executor.py @@ -47,18 +47,7 @@ class Executor: filter_query = self._prepare_db_query( filter_query, search_query, False ) - prev_filter_query = ( - filter_query.filter(self.config.id_column > entity_id) - .order_by(None) - .order_by(sa.func.abs(self.config.id_column - entity_id).asc()) - .limit(1) - ) - next_filter_query = ( - filter_query.filter(self.config.id_column < entity_id) - .order_by(None) - .order_by(sa.func.abs(self.config.id_column - entity_id).asc()) - .limit(1) - ) + prev_filter_query, next_filter_query = self.config.create_around_filter_queries(filter_query, entity_id) return ( prev_filter_query.one_or_none(), next_filter_query.one_or_none(), From 748f0e16eb301d0c7e8b539a182ce5076279149c Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Sat, 8 May 2021 18:12:41 -0700 Subject: [PATCH 07/11] temp --- client/css/pool-navigator-control.styl | 9 ++++ client/css/pool-navigator-list-control.styl | 9 ++++ client/html/pool_navigator.tpl | 2 + client/html/pool_navigator_list.tpl | 4 ++ client/html/post_main.tpl | 4 ++ client/js/controllers/post_main_controller.js | 2 + client/js/controls/pool_navigator_control.js | 26 ++++++++++ .../controls/pool_navigator_list_control.js | 50 +++++++++++++++++++ client/js/views/post_main_view.js | 16 ++++++ 9 files changed, 122 insertions(+) create mode 100644 client/css/pool-navigator-control.styl create mode 100644 client/css/pool-navigator-list-control.styl create mode 100644 client/html/pool_navigator.tpl create mode 100644 client/html/pool_navigator_list.tpl create mode 100644 client/js/controls/pool_navigator_control.js create mode 100644 client/js/controls/pool_navigator_list_control.js diff --git a/client/css/pool-navigator-control.styl b/client/css/pool-navigator-control.styl new file mode 100644 index 00000000..aaf6fac2 --- /dev/null +++ b/client/css/pool-navigator-control.styl @@ -0,0 +1,9 @@ +@import colors +$pool-navigator-header-background-color = $top-navigation-color +$pool-navigator-header-background-color-darktheme = $top-navigation-color-darktheme + +.pool-navigator-container + padding: 0 0 0 60px + +.darktheme .pool-navigator-container + background: $pool-navigator-header-background-color-darktheme diff --git a/client/css/pool-navigator-list-control.styl b/client/css/pool-navigator-list-control.styl new file mode 100644 index 00000000..080ad01a --- /dev/null +++ b/client/css/pool-navigator-list-control.styl @@ -0,0 +1,9 @@ +.pool-navigators>ul + list-style-type: none + margin: 0 + padding: 0 + + >li + margin-bottom: 1em + &:last-child + margin-bottom: 0 diff --git a/client/html/pool_navigator.tpl b/client/html/pool_navigator.tpl new file mode 100644 index 00000000..e71c4194 --- /dev/null +++ b/client/html/pool_navigator.tpl @@ -0,0 +1,2 @@ +
    +
    diff --git a/client/html/pool_navigator_list.tpl b/client/html/pool_navigator_list.tpl new file mode 100644 index 00000000..0ea2e7e5 --- /dev/null +++ b/client/html/pool_navigator_list.tpl @@ -0,0 +1,4 @@ +
    +
      +
    +
    diff --git a/client/html/post_main.tpl b/client/html/post_main.tpl index 54c57333..b2e1e6f5 100644 --- a/client/html/post_main.tpl +++ b/client/html/post_main.tpl @@ -54,6 +54,10 @@
    + <% if (ctx.canListPools && ctx.canViewPools) { %> +
    + <% } %> + <% if (ctx.canListComments) { %>
    <% } %> diff --git a/client/js/controllers/post_main_controller.js b/client/js/controllers/post_main_controller.js index 95cfdb52..871d98dd 100644 --- a/client/js/controllers/post_main_controller.js +++ b/client/js/controllers/post_main_controller.js @@ -56,6 +56,8 @@ class PostMainController extends BasePostController { canFeaturePosts: api.hasPrivilege("posts:feature"), canListComments: api.hasPrivilege("comments:list"), canCreateComments: api.hasPrivilege("comments:create"), + canListPools: api.hasPrivilege("pools:list"), + canViewPools: api.hasPrivilege("pools:view"), parameters: parameters, }); diff --git a/client/js/controls/pool_navigator_control.js b/client/js/controls/pool_navigator_control.js new file mode 100644 index 00000000..afaa45ee --- /dev/null +++ b/client/js/controls/pool_navigator_control.js @@ -0,0 +1,26 @@ +"use strict"; + +const api = require("../api.js"); +const misc = require("../util/misc.js"); +const events = require("../events.js"); +const views = require("../util/views.js"); + +const template = views.getTemplate("pool-navigator"); + +class PoolNavigatorControl extends events.EventTarget { + constructor(hostNode, pool) { + super(); + this._hostNode = hostNode; + this._pool = pool; + } + + // get _formNode() { + // return this._hostNode.querySelector("form"); + // } + + // get _scoreContainerNode() { + // return this._hostNode.querySelector(".score-container"); + // } +} + +module.exports = PoolNavigatorControl; diff --git a/client/js/controls/pool_navigator_list_control.js b/client/js/controls/pool_navigator_list_control.js new file mode 100644 index 00000000..88c52d32 --- /dev/null +++ b/client/js/controls/pool_navigator_list_control.js @@ -0,0 +1,50 @@ +"use strict"; + +const events = require("../events.js"); +const views = require("../util/views.js"); +const PoolNavigatorControl = require("../controls/pool_navigator_control.js"); + +const template = views.getTemplate("pool-navigator-list"); + +class PoolNavigatorListControl extends events.EventTarget { + constructor(hostNode, a) { + super(); + this._hostNode = hostNode; + + const poolList = []; + for (let pool of poolList) { + this._installPoolNavigatorNode(pool); + } + } + + get _poolNavigatorListNode() { + return this._hostNode.querySelector("ul"); + } + + _installPoolNavigatorNode(pool) { + const poolListItemNode = document.createElement("li"); + const poolControl = new PoolNavigatorControl( + pool + ); + // events.proxyEvent(commentControl, this, "submit"); + // events.proxyEvent(commentControl, this, "score"); + // events.proxyEvent(commentControl, this, "delete"); + // this._commentIdToNode[comment.id] = commentListItemNode; + this._poolNavigatorListNode.appendChild(poolListItemNode); + } + + _uninstallCommentNode(pool) { + const poolListItemNode = this._commentIdToNode[pool.id]; + poolListItemNode.parentNode.removeChild(poolListItemNode); + } + + // _evtAdd(e) { + // this._installPoolNode(e.detail.comment); + // } + + // _evtRemove(e) { + // this._uninstallPoolNode(e.detail.comment); + // } +} + +module.exports = PoolNavigatorListControl; diff --git a/client/js/views/post_main_view.js b/client/js/views/post_main_view.js index c38a9337..5c031243 100644 --- a/client/js/views/post_main_view.js +++ b/client/js/views/post_main_view.js @@ -12,6 +12,7 @@ const PostReadonlySidebarControl = require("../controls/post_readonly_sidebar_co const PostEditSidebarControl = require("../controls/post_edit_sidebar_control.js"); const CommentControl = require("../controls/comment_control.js"); const CommentListControl = require("../controls/comment_list_control.js"); +const PoolNavigatorListControl = require("../controls/pool_navigator_list_control.js"); const template = views.getTemplate("post-main"); @@ -58,6 +59,7 @@ class PostMainView { this._installSidebar(ctx); this._installCommentForm(); this._installComments(ctx.post.comments); + this._installPoolNavigators(ctx); const showPreviousImage = () => { if (ctx.prevPostId) { @@ -138,6 +140,20 @@ class PostMainView { } } + _installPoolNavigators(ctx) { + const poolNavigatorsContainerNode = document.querySelector( + "#content-holder .poolnavigators-container" + ); + if (!poolNavigatorsContainerNode) { + return; + } + + this.poolNavigatorsControl = new PoolNavigatorListControl( + poolNavigatorsContainerNode, + null + ); + } + _installCommentForm() { const commentFormContainer = document.querySelector( "#content-holder .comment-form-container" From 8e8b15a1d8d8a938d953b1831826d1a1accbf869 Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Sat, 8 May 2021 22:08:11 -0700 Subject: [PATCH 08/11] 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) From 7750e43714e777efa9c3e7f8fd2f94dfba2542d8 Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Sat, 8 May 2021 23:15:19 -0700 Subject: [PATCH 09/11] Initial implementation of pool navigation inside posts --- client/css/colors.styl | 2 + client/css/pool-navigator-control.styl | 31 +++++++++++-- client/html/pool_navigator.tpl | 29 ++++++++++++ client/js/controllers/post_main_controller.js | 18 +++++--- client/js/controls/pool_navigator_control.js | 27 ++++++----- .../controls/pool_navigator_list_control.js | 45 +++++++++++-------- client/js/views/post_main_view.js | 9 ++-- server/szurubooru/func/posts.py | 4 +- 8 files changed, 123 insertions(+), 42 deletions(-) diff --git a/client/css/colors.styl b/client/css/colors.styl index cf7e7caf..e9458139 100644 --- a/client/css/colors.styl +++ b/client/css/colors.styl @@ -18,6 +18,8 @@ $message-error-border-color = #FCC $message-error-background-color = #FFF5F5 $message-success-border-color = #D3E3D3 $message-success-background-color = #F5FFF5 +$pool-navigator-border-color = #888 +$pool-navigator-background-color = #EEE $input-bad-border-color = #FCC $input-bad-background-color = #FFF5F5 $input-good-border-color = #D3E3D3 diff --git a/client/css/pool-navigator-control.styl b/client/css/pool-navigator-control.styl index aaf6fac2..c9f96a53 100644 --- a/client/css/pool-navigator-control.styl +++ b/client/css/pool-navigator-control.styl @@ -1,9 +1,34 @@ @import colors -$pool-navigator-header-background-color = $top-navigation-color -$pool-navigator-header-background-color-darktheme = $top-navigation-color-darktheme .pool-navigator-container - padding: 0 0 0 60px + padding: 0 0.50em 0 0.50em + margin: 0 auto + + .pool-info-wrapper + box-sizing: border-box + width: 100% + max-width: 40em + margin: 0 0 1em 0 + display: flex + padding: 0.5em 1em + border: 1px solid $pool-navigator-border-color + background: $pool-navigator-background-color + &.active + font-weight: bold + + .pool-name + flex: 1 1; + text-align: center; + overflow: hidden; + white-space: nowrap; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + + .prev, .next + flex: 0 1; + margin: 0 .25em; + white-space: nowrap; + .darktheme .pool-navigator-container background: $pool-navigator-header-background-color-darktheme diff --git a/client/html/pool_navigator.tpl b/client/html/pool_navigator.tpl index e71c4194..88e7b53e 100644 --- a/client/html/pool_navigator.tpl +++ b/client/html/pool_navigator.tpl @@ -1,2 +1,31 @@
    +
    + + <% if (ctx.canViewPosts && ctx.prevPost) { %> + + <% } %> + ‹ prev + <% if (ctx.canViewPosts && ctx.prevPost) { %> + + <% } %> + + + <% if (ctx.canViewPools) { %> + + <% } %> + <%- ctx.pool.names[0] %> + <% if (ctx.canViewPools) { %> + + <% } %> + + + <% if (ctx.canViewPosts && ctx.nextPost) { %> + + <% } %> + next › + <% if (ctx.canViewPosts && ctx.nextPost) { %> + + <% } %> + +
    diff --git a/client/js/controllers/post_main_controller.js b/client/js/controllers/post_main_controller.js index a55acb9d..a2717e85 100644 --- a/client/js/controllers/post_main_controller.js +++ b/client/js/controllers/post_main_controller.js @@ -16,10 +16,10 @@ 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")) { + let poolPostsAround = Promise.resolve({results: [], activePool: null}); + if (api.hasPrivilege("pools:list") && api.hasPrivilege("pools:view")) { poolPostsAround = PostList.getPoolPostsAround( - ctxt.parameters.id, + ctx.parameters.id, parameters ? parameters.query : null ); } @@ -35,6 +35,7 @@ class PostMainController extends BasePostController { ]).then( (responses) => { const [post, aroundResponse, poolPostsAroundResponse] = responses; + let activePool = null; // remove junk from query, but save it into history so that it can // be still accessed after history navigation / page refresh @@ -48,13 +49,20 @@ class PostMainController extends BasePostController { ) : uri.formatClientLink("post", ctx.parameters.id); router.replace(url, ctx.state, false); + console.log(parameters.query); + parameters.query.split(" ").forEach((item) => { + const found = item.match(/^pool:([0-9]+)/i); + if (found) { + activePool = parseInt(found[1]); + } + }); } this._post = post; this._view = new PostMainView({ post: post, - poolPostsAround: poolPostsAroundResponse.results, - activePool: poolPostsAroundResponse.activePool, + poolPostsAround: poolPostsAroundResponse, + activePool: activePool, editMode: editMode, prevPostId: aroundResponse.prev ? aroundResponse.prev.id diff --git a/client/js/controls/pool_navigator_control.js b/client/js/controls/pool_navigator_control.js index afaa45ee..54de6c7a 100644 --- a/client/js/controls/pool_navigator_control.js +++ b/client/js/controls/pool_navigator_control.js @@ -8,19 +8,26 @@ const views = require("../util/views.js"); const template = views.getTemplate("pool-navigator"); class PoolNavigatorControl extends events.EventTarget { - constructor(hostNode, pool) { + constructor(hostNode, poolPostAround, isActivePool) { super(); this._hostNode = hostNode; - this._pool = pool; + this._poolPostAround = poolPostAround; + this._isActivePool = isActivePool; + + views.replaceContent( + this._hostNode, + template({ + pool: poolPostAround.pool, + parameters: { query: `pool:${poolPostAround.pool.id}` }, + linkClass: misc.makeCssName(poolPostAround.pool.category, "pool"), + canViewPosts: api.hasPrivilege("posts:view"), + canViewPools: api.hasPrivilege("pools:view"), + prevPost: poolPostAround.prevPost, + nextPost: poolPostAround.nextPost, + isActivePool: isActivePool + }) + ); } - - // get _formNode() { - // return this._hostNode.querySelector("form"); - // } - - // get _scoreContainerNode() { - // return this._hostNode.querySelector(".score-container"); - // } } module.exports = PoolNavigatorControl; diff --git a/client/js/controls/pool_navigator_list_control.js b/client/js/controls/pool_navigator_list_control.js index dd6a09b7..bc2ff221 100644 --- a/client/js/controls/pool_navigator_list_control.js +++ b/client/js/controls/pool_navigator_list_control.js @@ -7,44 +7,53 @@ const PoolNavigatorControl = require("../controls/pool_navigator_control.js"); const template = views.getTemplate("pool-navigator-list"); class PoolNavigatorListControl extends events.EventTarget { - constructor(hostNode, pools) { + constructor(hostNode, poolPostsAround, activePool) { super(); this._hostNode = hostNode; + this._poolPostsAround = poolPostsAround; + this._activePool = activePool; + this._indexToNode = {}; - const poolList = []; - for (let pool of poolList) { - this._installPoolNavigatorNode(pool); + for (let [i, entry] of this._poolPostsAround.entries()) { + this._installPoolNavigatorNode(entry, i); } } get _poolNavigatorListNode() { - return this._hostNode.querySelector("ul"); + return this._hostNode; } - _installPoolNavigatorNode(pool) { - const poolListItemNode = document.createElement("li"); + _installPoolNavigatorNode(poolPostAround, i) { + const isActivePool = poolPostAround.pool.id == this._activePool + const poolListItemNode = document.createElement("div"); const poolControl = new PoolNavigatorControl( - pool + poolListItemNode, + poolPostAround, + isActivePool ); // events.proxyEvent(commentControl, this, "submit"); // events.proxyEvent(commentControl, this, "score"); // events.proxyEvent(commentControl, this, "delete"); - // this._commentIdToNode[comment.id] = commentListItemNode; - this._poolNavigatorListNode.appendChild(poolListItemNode); + this._indexToNode[poolPostAround.id] = poolListItemNode; + if (isActivePool) { + this._poolNavigatorListNode.insertBefore(poolListItemNode, this._poolNavigatorListNode.firstChild); + } else { + this._poolNavigatorListNode.appendChild(poolListItemNode); + } } - _uninstallCommentNode(pool) { - const poolListItemNode = this._commentIdToNode[pool.id]; + _uninstallPoolNavigatorNode(index) { + const poolListItemNode = this._indexToNode[index]; poolListItemNode.parentNode.removeChild(poolListItemNode); } - // _evtAdd(e) { - // this._installPoolNode(e.detail.comment); - // } + _evtAdd(e) { + this._installPoolNavigatorNode(e.detail.index); + } - // _evtRemove(e) { - // this._uninstallPoolNode(e.detail.comment); - // } + _evtRemove(e) { + this._uninstallPoolNavigatorNode(e.detail.index); + } } module.exports = PoolNavigatorListControl; diff --git a/client/js/views/post_main_view.js b/client/js/views/post_main_view.js index 5c031243..d8168ad4 100644 --- a/client/js/views/post_main_view.js +++ b/client/js/views/post_main_view.js @@ -59,7 +59,7 @@ class PostMainView { this._installSidebar(ctx); this._installCommentForm(); this._installComments(ctx.post.comments); - this._installPoolNavigators(ctx); + this._installPoolNavigators(ctx.poolPostsAround, ctx.activePool); const showPreviousImage = () => { if (ctx.prevPostId) { @@ -140,9 +140,9 @@ class PostMainView { } } - _installPoolNavigators(ctx) { + _installPoolNavigators(poolPostsAround, activePool) { const poolNavigatorsContainerNode = document.querySelector( - "#content-holder .poolnavigators-container" + "#content-holder .pool-navigators-container" ); if (!poolNavigatorsContainerNode) { return; @@ -150,7 +150,8 @@ class PostMainView { this.poolNavigatorsControl = new PoolNavigatorListControl( poolNavigatorsContainerNode, - null + poolPostsAround, + activePool ); } diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index cc553167..7b0e81e2 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -1033,8 +1033,8 @@ def serialize_pool_posts_around(around: List[PoolPostsAround]) -> Optional[rest. return [ { "pool": pools.serialize_micro_pool(entry.pool), - "prev_post": entry.prev_post, - "next_post": entry.next_post + "prevPost": entry.prev_post, + "nextPost": entry.next_post } for entry in sort_pool_posts_around(around) ] From e1c97049da986d8ff0a463d0ddcdf784fc8e29ad Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Sat, 8 May 2021 23:27:09 -0700 Subject: [PATCH 10/11] Add pool posts around test --- client/css/pool-navigator-control.styl | 1 - client/html/pool_navigator.tpl | 2 +- server/szurubooru/tests/func/test_posts.py | 23 ++++++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/client/css/pool-navigator-control.styl b/client/css/pool-navigator-control.styl index c9f96a53..e34bdb4e 100644 --- a/client/css/pool-navigator-control.styl +++ b/client/css/pool-navigator-control.styl @@ -7,7 +7,6 @@ .pool-info-wrapper box-sizing: border-box width: 100% - max-width: 40em margin: 0 0 1em 0 display: flex padding: 0.5em 1em diff --git a/client/html/pool_navigator.tpl b/client/html/pool_navigator.tpl index 88e7b53e..8841da39 100644 --- a/client/html/pool_navigator.tpl +++ b/client/html/pool_navigator.tpl @@ -13,7 +13,7 @@ <% if (ctx.canViewPools) { %> <% } %> - <%- ctx.pool.names[0] %> + Pool: <%- ctx.pool.names[0] %> <% if (ctx.canViewPools) { %> <% } %> diff --git a/server/szurubooru/tests/func/test_posts.py b/server/szurubooru/tests/func/test_posts.py index 6139555b..360f4022 100644 --- a/server/szurubooru/tests/func/test_posts.py +++ b/server/szurubooru/tests/func/test_posts.py @@ -1147,3 +1147,26 @@ def test_search_by_image(post_factory, config_injector, read_asset): result2 = posts.search_by_image(read_asset("png.png")) assert not result2 + + +def test_get_pool_posts_around(post_factory, pool_factory, config_injector): + from szurubooru.migrations.functions import get_pool_posts_around + db.session.execute(get_pool_posts_around.to_sql_statement_create()) + db.session.flush() + + config_injector({"allow_broken_uploads": False, "secret": "test"}) + post1 = post_factory(id=1) + post2 = post_factory(id=2) + post3 = post_factory(id=3) + post4 = post_factory(id=4) + pool1 = pool_factory(id=1) + pool2 = pool_factory(id=2) + pool1.posts = [post1, post2, post3, post4] + pool2.posts = [post3, post4, post2] + db.session.add_all([post1, post2, post3, post4, pool1, pool2]) + db.session.flush() + around = posts.get_pool_posts_around(post2) + assert around[0].prev_post["id"] == post1.post_id + assert around[0].next_post["id"] == post3.post_id + assert around[1].prev_post["id"] == post4.post_id + assert around[1].next_post == None From 28eaf53dfda522bf71b3fa4276649f53385d033c Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Sun, 9 May 2021 00:49:23 -0700 Subject: [PATCH 11/11] Add first/last pool post to pool navigator --- client/css/colors.styl | 2 +- client/css/pool-navigator-control.styl | 9 ++++-- client/css/post-main-view.styl | 5 +++- client/html/pool_navigator.tpl | 18 ++++++++++++ client/js/controls/pool_navigator_control.js | 2 ++ server/szurubooru/func/posts.py | 30 ++++++++++++++------ server/szurubooru/migrations/functions.py | 24 +++++++++++++++- server/szurubooru/tests/func/test_posts.py | 4 +++ 8 files changed, 80 insertions(+), 14 deletions(-) diff --git a/client/css/colors.styl b/client/css/colors.styl index e9458139..51ae30ed 100644 --- a/client/css/colors.styl +++ b/client/css/colors.styl @@ -18,7 +18,7 @@ $message-error-border-color = #FCC $message-error-background-color = #FFF5F5 $message-success-border-color = #D3E3D3 $message-success-background-color = #F5FFF5 -$pool-navigator-border-color = #888 +$pool-navigator-border-color = #AAA $pool-navigator-background-color = #EEE $input-bad-border-color = #FCC $input-bad-background-color = #FFF5F5 diff --git a/client/css/pool-navigator-control.styl b/client/css/pool-navigator-control.styl index e34bdb4e..ed6cd16a 100644 --- a/client/css/pool-navigator-control.styl +++ b/client/css/pool-navigator-control.styl @@ -1,7 +1,7 @@ @import colors .pool-navigator-container - padding: 0 0.50em 0 0.50em + padding: 0 margin: 0 auto .pool-info-wrapper @@ -14,6 +14,8 @@ background: $pool-navigator-background-color &.active font-weight: bold + font-size: 1.10em; + padding: 0.58em 1em .pool-name flex: 1 1; @@ -23,7 +25,10 @@ -o-text-overflow: ellipsis; text-overflow: ellipsis; - .prev, .next + .first, .last + flex-basis: 1em; + + .first, .prev, .next, .last flex: 0 1; margin: 0 .25em; white-space: nowrap; diff --git a/client/css/post-main-view.styl b/client/css/post-main-view.styl index 48f3c158..c372e4a2 100644 --- a/client/css/post-main-view.styl +++ b/client/css/post-main-view.styl @@ -40,11 +40,14 @@ width: 100% .post-container - margin-bottom: 2em + margin-bottom: 1em .post-content margin: 0 + .pool-navigators-container + margin-bottom: 2em + @media (max-width: 800px) .post-view flex-wrap: wrap diff --git a/client/html/pool_navigator.tpl b/client/html/pool_navigator.tpl index 8841da39..b70c238d 100644 --- a/client/html/pool_navigator.tpl +++ b/client/html/pool_navigator.tpl @@ -1,5 +1,14 @@
    + + <% if (ctx.canViewPosts && ctx.firstPost) { %> + + <% } %> + « + <% if (ctx.canViewPosts && ctx.firstPost) { %> + + <% } %> + <% if (ctx.canViewPosts && ctx.prevPost) { %> @@ -27,5 +36,14 @@ <% } %> + + <% if (ctx.canViewPosts && ctx.lastPost) { %> + + <% } %> + » + <% if (ctx.canViewPosts && ctx.lastPost) { %> + + <% } %> +
    diff --git a/client/js/controls/pool_navigator_control.js b/client/js/controls/pool_navigator_control.js index 54de6c7a..5961ac70 100644 --- a/client/js/controls/pool_navigator_control.js +++ b/client/js/controls/pool_navigator_control.js @@ -22,8 +22,10 @@ class PoolNavigatorControl extends events.EventTarget { linkClass: misc.makeCssName(poolPostAround.pool.category, "pool"), canViewPosts: api.hasPrivilege("posts:view"), canViewPools: api.hasPrivilege("pools:view"), + firstPost: poolPostAround.firstPost, prevPost: poolPostAround.prevPost, nextPost: poolPostAround.nextPost, + lastPost: poolPostAround.lastPost, isActivePool: isActivePool }) ); diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index 7b0e81e2..9dd3b1c6 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -974,7 +974,7 @@ def search_by_image(image_content: bytes) -> List[Tuple[float, model.Post]]: return [] -PoolPostsAround = namedtuple('PoolPostsAround', 'pool prev_post next_post') +PoolPostsAround = namedtuple('PoolPostsAround', 'pool first_post prev_post next_post last_post') def get_pool_posts_around(post: model.Post) -> List[PoolPostsAround]: around = dict() @@ -990,11 +990,15 @@ def get_pool_posts_around(post: model.Post) -> List[PoolPostsAround]: 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] = [None, None, None, None] + if delta == -2: around[pool_id][0] = post_id - elif delta > 0: + elif delta == -1: around[pool_id][1] = post_id + elif delta == 1: + around[pool_id][2] = post_id + elif delta == 2: + around[pool_id][3] = post_id pool_ids.add(pool_id) post_ids.add(post_id) @@ -1011,13 +1015,19 @@ def get_pool_posts_around(post: model.Post) -> List[PoolPostsAround]: results = [] for pool_id, entry in around.items(): + first_post = None prev_post = None next_post = None - if entry[0] is not None: - prev_post = posts[entry[0]] + last_post = None if entry[1] is not None: - next_post = posts[entry[1]] - results.append(PoolPostsAround(pools[pool_id], prev_post, next_post)) + prev_post = posts[entry[1]] + if entry[0] is not None: + first_post = posts[entry[0]] + if entry[2] is not None: + next_post = posts[entry[2]] + if entry[3] is not None: + last_post = posts[entry[3]] + results.append(PoolPostsAround(pools[pool_id], first_post, prev_post, next_post, last_post)) return results @@ -1033,8 +1043,10 @@ def serialize_pool_posts_around(around: List[PoolPostsAround]) -> Optional[rest. return [ { "pool": pools.serialize_micro_pool(entry.pool), + "firstPost": entry.first_post, "prevPost": entry.prev_post, - "nextPost": entry.next_post + "nextPost": entry.next_post, + "lastPost": entry.last_post } for entry in sort_pool_posts_around(around) ] diff --git a/server/szurubooru/migrations/functions.py b/server/szurubooru/migrations/functions.py index b6ebb069..ae39e62b 100644 --- a/server/szurubooru/migrations/functions.py +++ b/server/szurubooru/migrations/functions.py @@ -39,8 +39,30 @@ BEGIN WHERE pool_post.ord < main.ord AND pool_post.pool_id = main.pool_id ORDER BY pool_post.ord DESC LIMIT 1) + UNION + (SELECT pool_post.ord, + pool_post.pool_id, + pool_post.post_id, + 2 as delta, + main.ord AS target_ord, + main.pool_id AS target_pool_id + FROM pool_post, main + WHERE pool_post.ord = (SELECT MAX(pool_post.ord) FROM pool_post) + AND pool_post.pool_id = main.pool_id + ORDER BY pool_post.ord DESC LIMIT 1) + UNION + (SELECT pool_post.ord, + pool_post.pool_id, + pool_post.post_id, + -2 as delta, + main.ord AS target_ord, + main.pool_id AS target_pool_id + FROM pool_post, main + WHERE pool_post.ord = (SELECT MIN(pool_post.ord) FROM pool_post) + 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; + SELECT around.ord, around.pool_id, around.post_id, around.delta FROM around; END $$ """) diff --git a/server/szurubooru/tests/func/test_posts.py b/server/szurubooru/tests/func/test_posts.py index 360f4022..5e08a4a8 100644 --- a/server/szurubooru/tests/func/test_posts.py +++ b/server/szurubooru/tests/func/test_posts.py @@ -1166,7 +1166,11 @@ def test_get_pool_posts_around(post_factory, pool_factory, config_injector): db.session.add_all([post1, post2, post3, post4, pool1, pool2]) db.session.flush() around = posts.get_pool_posts_around(post2) + assert around[0].first_post["id"] == post1.post_id assert around[0].prev_post["id"] == post1.post_id assert around[0].next_post["id"] == post3.post_id + assert around[0].last_post["id"] == post4.post_id + assert around[1].first_post["id"] == post3.post_id assert around[1].prev_post["id"] == post4.post_id assert around[1].next_post == None + assert around[1].last_post == None