diff --git a/client/css/colors.styl b/client/css/colors.styl index cf7e7caf..aa1d6e03 100644 --- a/client/css/colors.styl +++ b/client/css/colors.styl @@ -36,6 +36,7 @@ $button-disabled-background-color = #CCC $post-thumbnail-border-color = $main-color $post-thumbnail-no-tags-border-color = #F44 $default-tag-category-background-color = $active-tab-background-color +$default-pool-category-background-color = $active-tab-background-color $new-tag-background-color = #DFC $new-tag-text-color = black $implied-tag-background-color = #FFC diff --git a/client/css/pool-list-view.styl b/client/css/pool-list-view.styl index b7ac15ed..bda6c8d9 100644 --- a/client/css/pool-list-view.styl +++ b/client/css/pool-list-view.styl @@ -1,47 +1,100 @@ @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 + 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: 2em 1.5em 2em 1.2em + 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: none + border-right: 20px solid transparent + &:before + content: ' ' + display: block + position: relative + width: 100% + height: 20px + bottom: 20px + + .thumbnail + width: 100% + height: 100% + outline-offset: -2px + background-size: cover + transition: top .1s ease-in-out, right .1s ease-in-out + background-position: 50% 30% + position: absolute + display: inline-block + box-shadow: 0 0 0 1px rgba(0,0,0,0.2) + + .thumbnail-1, .thumbnail.empty + right: -4px + top: -4px + z-index: 30 + + .thumbnail-2 + right: -10px + top: -10px + z-index: 20 + + .thumbnail-3 + right: -16px + top: -16px + z-index: 10 + + .pool-name + color: black + font-size: 1em + text-align: center + a + width: 100% + display: inline-block + + a:active, a:focus + .thumbnail + outline: 2px solid $main-color !important + +.pool-list ul li:hover + .thumbnail-wrapper + .thumbnail-1 + right: -0px + top: -0px + + .thumbnail-3 + right: -20px + top: -20px + +.pool-list ul li:has(a:focus), .pool-list ul li:has(a:active) + .thumbnail-wrapper + .thumbnail-1 + right: -0px + top: -0px + + .thumbnail-3 + right: -20px + top: -20px .pool-list-header label @@ -61,3 +114,21 @@ .darktheme .pool-list-header .append color: $inactive-link-color-darktheme + +.post-flow + ul + 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 + 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/css/pool-navigator-control.styl b/client/css/pool-navigator-control.styl new file mode 100644 index 00000000..1c4d636d --- /dev/null +++ b/client/css/pool-navigator-control.styl @@ -0,0 +1,39 @@ +@import colors + +.pool-navigator-container + padding: 0 + margin: 0 auto + + .pool-info-wrapper + box-sizing: border-box + width: 100% + margin: 0 0 1em 0 + display: flex + padding: 0.5em 1em + border: 1px solid $line-color + background: $top-navigation-color + + .pool-name + flex: 1 1 + text-align: center + overflow: hidden + white-space: nowrap + -o-text-overflow: ellipsis + text-overflow: ellipsis + + .first, .last + flex-basis: 1em + + .first, .prev, .next, .last + flex: 0 1 + white-space: nowrap + + >span + padding-top: 2px + padding-bottom: 2px + margin: 0 .25em + + +.darktheme .pool-navigator-container .pool-info-wrapper + border: 1px solid $top-navigation-color-darktheme + background: $window-color-darktheme diff --git a/client/css/pool-navigator-list.styl b/client/css/pool-navigator-list.styl new file mode 100644 index 00000000..080ad01a --- /dev/null +++ b/client/css/pool-navigator-list.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/help_search_posts.tpl b/client/html/help_search_posts.tpl index 0fe584fa..311551c4 100644 --- a/client/html/help_search_posts.tpl +++ b/client/html/help_search_posts.tpl @@ -329,6 +329,10 @@ feature-time alias of feature-time + + pool + pool order, requires pool named token + diff --git a/client/html/pool_delete.tpl b/client/html/pool_delete.tpl index 1ef7fb53..b8042756 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/pools_page.tpl b/client/html/pools_page.tpl index 0d811808..c935785f 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/html/post_main.tpl b/client/html/post_main.tpl index 84e48b1c..51ef5626 100644 --- a/client/html/post_main.tpl +++ b/client/html/post_main.tpl @@ -52,6 +52,10 @@
+ <% if (ctx.canListPools && ctx.canViewPools) { %> +
+ <% } %> +
<% if (ctx.canCreateComments) { %>

Add comment

diff --git a/client/js/controllers/pool_list_controller.js b/client/js/controllers/pool_list_controller.js index a66f8163..0590e9d9 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"); @@ -13,7 +14,6 @@ const EmptyView = require("../views/empty_view.js"); const fields = [ "id", "names", - "posts", "creationTime", "postCount", "category", @@ -100,14 +100,21 @@ class PoolListController { return uri.formatClientLink("pools", parameters); }, requestPage: (offset, limit) => { + const canEditPosts = api.hasPrivilege("pools:edit") || api.hasPrivilege("pools:edit:posts"); + const effectiveFields = fields.concat([canEditPosts ? "posts": "postsMicro"]); return PoolList.search( this._ctx.parameters.query, offset, limit, - fields + effectiveFields ); }, 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/js/controllers/post_main_controller.js b/client/js/controllers/post_main_controller.js index bd338129..d7395fc4 100644 --- a/client/js/controllers/post_main_controller.js +++ b/client/js/controllers/post_main_controller.js @@ -11,6 +11,7 @@ const PostList = require("../models/post_list.js"); const PostMainView = require("../views/post_main_view.js"); const BasePostController = require("./base_post_controller.js"); const EmptyView = require("../views/empty_view.js"); +const PoolNavigatorListControl = require("../controls/pool_navigator_list_control.js"); class PostMainController extends BasePostController { constructor(ctx, editMode) { @@ -26,6 +27,7 @@ class PostMainController extends BasePostController { ]).then( (responses) => { const [post, aroundResponse] = responses; + let aroundPool = null; // remove junk from query, but save it into history so that it can // be still accessed after history navigation / page refresh @@ -39,23 +41,36 @@ class PostMainController extends BasePostController { ) : uri.formatClientLink("post", ctx.parameters.id); router.replace(url, ctx.state, false); + misc.splitByWhitespace(parameters.query).forEach((item) => { + const found = item.match(/^pool:([0-9]+)/i); + if (found) { + const activePool = parseInt(found[1]); + post.pools.map((pool) => { + if (pool.id == activePool) { + aroundPool = pool; + } + }); + } + }); } this._post = post; this._view = new PostMainView({ post: post, editMode: editMode, - prevPostId: aroundResponse.prev - ? aroundResponse.prev.id - : null, - nextPostId: aroundResponse.next - ? aroundResponse.next.id - : null, + prevPostId: aroundPool + ? (aroundPool.previousPost ? aroundPool.previousPost.id : null) + : (aroundResponse.prev ? aroundResponse.prev.id : null), + nextPostId: aroundPool + ? (aroundPool.nextPost ? aroundPool.nextPost.id : null) + : (aroundResponse.next ? aroundResponse.next.id : null), canEditPosts: api.hasPrivilege("posts:edit"), canDeletePosts: api.hasPrivilege("posts:delete"), 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..41b68d53 --- /dev/null +++ b/client/js/controls/pool_navigator_control.js @@ -0,0 +1,34 @@ +"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, poolPostNearby) { + super(); + this._hostNode = hostNode; + this._poolPostNearby = poolPostNearby; + + views.replaceContent( + this._hostNode, + template({ + pool: poolPostNearby, + parameters: { query: `pool:${poolPostNearby.id}` }, + linkClass: misc.makeCssName(poolPostNearby.category, "pool"), + canViewPosts: api.hasPrivilege("posts:view"), + canViewPools: api.hasPrivilege("pools:view"), + firstPost: poolPostNearby.firstPost, + previousPost: poolPostNearby.previousPost, + nextPost: poolPostNearby.nextPost, + lastPost: poolPostNearby.lastPost, + getPrettyName: misc.getPrettyName, + }) + ); + } +} + +module.exports = PoolNavigatorControl; \ No newline at end of file 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..37d052f2 --- /dev/null +++ b/client/js/controls/pool_navigator_list_control.js @@ -0,0 +1,49 @@ +"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, poolPostNearby) { + super(); + this._hostNode = hostNode; + this._poolPostNearby = poolPostNearby; + this._indexToNode = {}; + + for (const entry of this._poolPostNearby) { + this._installPoolNavigatorNode(entry); + } + } + + get _poolNavigatorListNode() { + return this._hostNode; + } + + _installPoolNavigatorNode(poolPostNearby) { + const poolListItemNode = document.createElement("div"); + const poolControl = new PoolNavigatorControl( + poolListItemNode, + poolPostNearby, + ); + this._indexToNode[poolPostNearby.id] = poolListItemNode; + this._poolNavigatorListNode.appendChild(poolListItemNode); + } + + _uninstallPoolNavigatorNode(index) { + const poolListItemNode = this._indexToNode[index]; + poolListItemNode.parentNode.removeChild(poolListItemNode); + } + + _evtAdd(e) { + this._installPoolNavigatorNode(e.detail.index); + } + + _evtRemove(e) { + this._uninstallPoolNavigatorNode(e.detail.index); + } +} + +module.exports = PoolNavigatorListControl; diff --git a/client/js/models/pool.js b/client/js/models/pool.js index 51fa8a05..c61f9f28 100644 --- a/client/js/models/pool.js +++ b/client/js/models/pool.js @@ -36,7 +36,7 @@ class Pool extends events.EventTarget { } get posts() { - return this._posts; + return this._postsMicro || this._posts; } get postCount() { @@ -51,6 +51,22 @@ class Pool extends events.EventTarget { return this._lastEditTime; } + get firstPost() { + return this._firstPost; + } + + get lastPost() { + return this._lastPost; + } + + get previousPost() { + return this._previousPost; + } + + get nextPost() { + return this._nextPost; + } + set names(value) { this._names = value; } @@ -169,10 +185,15 @@ class Pool extends events.EventTarget { _creationTime: response.creationTime, _lastEditTime: response.lastEditTime, _postCount: response.postCount || 0, + _postsMicro: response.postsMicro, + _firstPost: response.firstPost || null, + _lastPost: response.lastPost || null, + _previousPost: response.previousPost || null, + _nextPost: response.nextPost || null, }; for (let obj of [this, this._orig]) { - obj._posts.sync(response.posts); + obj._posts.sync(response.posts || []); } Object.assign(this, map); diff --git a/client/js/util/views.js b/client/js/util/views.js index f6280a1c..c9130a44 100644 --- a/client/js/util/views.js +++ b/client/js/util/views.js @@ -40,19 +40,36 @@ function makeRelativeTime(time) { ); } -function makeThumbnail(url) { +function makeThumbnail(url, klass, extraProperties) { return makeElement( "span", url ? { - class: "thumbnail", + class: klass || "thumbnail", style: `background-image: url(\'${url}\')`, } : { class: "thumbnail empty" }, - makeElement("img", { alt: "thumbnail", src: url }) + makeElement("img", Object.assign({ alt: "thumbnail", src: url }, extraProperties || {})) ); } +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), i === 0 ? {fetchPriority: "high"} : {}); + } + + 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, diff --git a/client/js/views/pool_create_view.js b/client/js/views/pool_create_view.js index fc75f452..c3282ca1 100644 --- a/client/js/views/pool_create_view.js +++ b/client/js/views/pool_create_view.js @@ -30,7 +30,7 @@ class PoolCreateView extends events.EventTarget { } for (let node of this._formNode.querySelectorAll( - "input, select, textarea, posts" + "input, select, textarea" )) { node.addEventListener("change", (e) => { this.dispatchEvent(new CustomEvent("change")); diff --git a/client/js/views/post_main_view.js b/client/js/views/post_main_view.js index 5ef7f61e..6ddd00cd 100644 --- a/client/js/views/post_main_view.js +++ b/client/js/views/post_main_view.js @@ -10,6 +10,7 @@ const PostContentControl = require("../controls/post_content_control.js"); const PostNotesOverlayControl = require("../controls/post_notes_overlay_control.js"); const PostReadonlySidebarControl = require("../controls/post_readonly_sidebar_control.js"); const PostEditSidebarControl = require("../controls/post_edit_sidebar_control.js"); +const PoolNavigatorListControl = require("../controls/pool_navigator_list_control.js"); const CommentControl = require("../controls/comment_control.js"); const CommentListControl = require("../controls/comment_list_control.js"); @@ -57,6 +58,7 @@ class PostMainView { this._installSidebar(ctx); this._installCommentForm(); this._installComments(ctx.post.comments); + this._installPoolNavigators(ctx); const showPreviousImage = () => { if (ctx.prevPostId) { @@ -137,6 +139,20 @@ class PostMainView { } } + _installPoolNavigators(ctx) { + const poolNavigatorsContainerNode = document.querySelector( + "#content-holder .pool-navigators-container" + ); + if (!poolNavigatorsContainerNode) { + return; + } + + this.poolNavigatorsControl = new PoolNavigatorListControl( + poolNavigatorsContainerNode, + ctx.post.pools, + ); + } + _installCommentForm() { const commentFormContainer = document.querySelector( "#content-holder .comment-form-container" diff --git a/client/package-lock.json b/client/package-lock.json index 3aa4ca4a..31b85342 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -27,13 +27,19 @@ "html-minifier": "^3.5.18", "jimp": "^0.13.0", "pretty-error": "^3.0.3", - "stylus": "^0.54.8", + "stylus": "^0.59.0", "terser": "^4.8.1", "underscore": "^1.12.1", "watchify": "^4.0.0", "ws": "^7.4.6" } }, + "node_modules/@adobe/css-tools": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", + "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", + "dev": true + }, "node_modules/@babel/runtime": { "version": "7.10.3", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz", @@ -516,18 +522,6 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true, - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", @@ -1737,27 +1731,6 @@ "node": "*" } }, - "node_modules/css": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", - "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "source-map": "^0.6.1", - "source-map-resolve": "^0.5.2", - "urix": "^0.1.0" - } - }, - "node_modules/css-parse": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz", - "integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=", - "dev": true, - "dependencies": { - "css": "^2.0.0" - } - }, "node_modules/css-select": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", @@ -1795,15 +1768,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/csso": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", @@ -1830,15 +1794,6 @@ "ms": "2.0.0" } }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -3676,13 +3631,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "deprecated": "https://github.com/lydell/resolve-url#deprecated", - "dev": true - }, "node_modules/ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", @@ -3698,12 +3646,6 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -3794,19 +3736,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, "node_modules/source-map-support": { "version": "0.4.18", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", @@ -3816,12 +3745,6 @@ "source-map": "^0.5.6" } }, - "node_modules/source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "dev": true - }, "node_modules/stream-browserify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", @@ -3912,18 +3835,15 @@ } }, "node_modules/stylus": { - "version": "0.54.8", - "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz", - "integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==", + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz", + "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==", "dev": true, "dependencies": { - "css-parse": "~2.0.0", - "debug": "~3.1.0", + "@adobe/css-tools": "^4.0.1", + "debug": "^4.3.2", "glob": "^7.1.6", - "mkdirp": "~1.0.4", - "safer-buffer": "^2.1.2", "sax": "~1.2.4", - "semver": "^6.3.0", "source-map": "^0.7.3" }, "bin": { @@ -3931,28 +3851,33 @@ }, "engines": { "node": "*" + }, + "funding": { + "url": "https://opencollective.com/stylus" } }, - "node_modules/stylus/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "node_modules/stylus/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" + "dependencies": { + "ms": "2.1.2" }, "engines": { - "node": ">=10" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/stylus/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } + "node_modules/stylus/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/stylus/node_modules/source-map": { "version": "0.7.3", @@ -4219,13 +4144,6 @@ "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", "dev": true }, - "node_modules/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", - "dev": true - }, "node_modules/url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", @@ -4602,6 +4520,12 @@ } }, "dependencies": { + "@adobe/css-tools": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", + "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", + "dev": true + }, "@babel/runtime": { "version": "7.10.3", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz", @@ -5072,12 +4996,6 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, "available-typed-arrays": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", @@ -6246,35 +6164,6 @@ "randomfill": "^1.0.3" } }, - "css": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", - "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "source-map": "^0.6.1", - "source-map-resolve": "^0.5.2", - "urix": "^0.1.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "css-parse": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz", - "integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=", - "dev": true, - "requires": { - "css": "^2.0.0" - } - }, "css-select": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", @@ -6326,12 +6215,6 @@ "ms": "2.0.0" } }, - "decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "dev": true - }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -7829,12 +7712,6 @@ "path-parse": "^1.0.6" } }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, "ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", @@ -7850,12 +7727,6 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -7931,19 +7802,6 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, "source-map-support": { "version": "0.4.18", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", @@ -7953,12 +7811,6 @@ "source-map": "^0.5.6" } }, - "source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "dev": true - }, "stream-browserify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", @@ -8040,31 +7892,31 @@ } }, "stylus": { - "version": "0.54.8", - "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz", - "integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==", + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz", + "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==", "dev": true, "requires": { - "css-parse": "~2.0.0", - "debug": "~3.1.0", + "@adobe/css-tools": "^4.0.1", + "debug": "^4.3.2", "glob": "^7.1.6", - "mkdirp": "~1.0.4", - "safer-buffer": "^2.1.2", "sax": "~1.2.4", - "semver": "^6.3.0", "source-map": "^0.7.3" }, "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "source-map": { @@ -8287,12 +8139,6 @@ "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", "dev": true }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", diff --git a/client/package.json b/client/package.json index 76376f82..ca575aa5 100644 --- a/client/package.json +++ b/client/package.json @@ -28,7 +28,7 @@ "html-minifier": "^3.5.18", "jimp": "^0.13.0", "pretty-error": "^3.0.3", - "stylus": "^0.54.8", + "stylus": "^0.59.0", "terser": "^4.8.1", "underscore": "^1.12.1", "watchify": "^4.0.0", diff --git a/doc/API.md b/doc/API.md index 70c495ab..cfc918a4 100644 --- a/doc/API.md +++ b/doc/API.md @@ -975,6 +975,43 @@ data. Retrieves information about posts that are before or after an existing post. +## Getting pools around post +- **Request** + + `GET /post//pools-nearby` + +- **Output** + + ```json5 + [ + { + "pool": , + "firstPost": , + "lastPost": , + "nextPost": , + "previousPost": + }, + ... + ] + ``` + +- **Field meaning** + +- ``: The associated [micro pool resource](#micro-pool). +- ``: A [micro post resource](#micro-post) that displays the first post in the pool. +- ``: A [micro post resource](#micro-post) that displays the last post in the pool. +- ``: A [micro post resource](#micro-post) that displays the next post in the pool. +- ``: A [micro post resource](#micro-post) that displays the previous post in the pool. + +- **Errors** + + - the post does not exist + - privileges are too low + +- **Description** + + Retrieves extra information about any pools that the post is in. + ## Deleting post - **Request** diff --git a/server/szurubooru/api/post_api.py b/server/szurubooru/api/post_api.py index daba7f7e..69239725 100644 --- a/server/szurubooru/api/post_api.py +++ b/server/szurubooru/api/post_api.py @@ -5,12 +5,10 @@ from szurubooru import db, errors, model, rest, search from szurubooru.func import ( auth, favorites, - mime, posts, scores, serialization, snapshots, - tags, versions, ) diff --git a/server/szurubooru/func/pools.py b/server/szurubooru/func/pools.py index c3ea9f0f..e1efe006 100644 --- a/server/szurubooru/func/pools.py +++ b/server/szurubooru/func/pools.py @@ -108,6 +108,7 @@ class PoolSerializer(serialization.BaseSerializer): "lastEditTime": self.serialize_last_edit_time, "postCount": self.serialize_post_count, "posts": self.serialize_posts, + "postsMicro": self.serialize_posts_micro, } def serialize_id(self) -> Any: @@ -143,6 +144,14 @@ class PoolSerializer(serialization.BaseSerializer): ] ] + def serialize_posts_micro(self) -> Any: + posts_micro = [] + for i, rel in enumerate(self.pool.posts): + posts_micro.append(posts.serialize_micro_post(rel, None)) + if i == 2: + break + return posts_micro + def serialize_pool( pool: model.Pool, options: List[str] = [] diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index be2259cf..e3eaef41 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -1,12 +1,15 @@ import hmac import logging +from collections import namedtuple from datetime import datetime +from itertools import tee, chain, islice from typing import Any, Callable, Dict, List, Optional, Tuple import sqlalchemy as sa from szurubooru import config, db, errors, model, rest from szurubooru.func import ( + auth, comments, files, image_hash, @@ -15,7 +18,6 @@ from szurubooru.func import ( pools, scores, serialization, - snapshots, tags, users, util, @@ -96,6 +98,13 @@ FLAG_MAP = { model.Post.FLAG_SOUND: "sound", } +# https://stackoverflow.com/a/1012089 +def _get_nearby_iter(post_list): + previous_item, current_item, next_item = tee(post_list, 3) + previous_item = chain([None], previous_item) + next_item = chain(islice(next_item, 1, None), [None]) + return zip(previous_item, current_item, next_item) + def get_post_security_hash(id: int) -> str: return hmac.new( @@ -337,8 +346,10 @@ class PostSerializer(serialization.BaseSerializer): ] def serialize_pools(self) -> List[Any]: + if not auth.has_privilege(self.auth_user, "pools:list"): + return [] return [ - pools.serialize_micro_pool(pool) + {**pools.serialize_micro_pool(pool), **get_pool_posts_nearby(self.post, pool)} if auth.has_privilege(self.auth_user, "pools:view") else pools.serialize_micro_pool(pool) for pool in sorted( self.post.pools, key=lambda pool: pool.creation_time ) @@ -968,3 +979,38 @@ def search_by_image(image_content: bytes) -> List[Tuple[float, model.Post]]: ] else: return [] + +def serialize_safe_post( + post: Optional[model.Post] +) -> rest.Response: + return {"id": getattr(post, "post_id", None)} if post else None + + +def serialize_id_post( + post_id: Optional[int] +) -> rest.Response: + return serialize_safe_post(try_get_post_by_id(post_id)) if post_id else None + + +def get_pool_posts_nearby( + post: model.Post, pool: model.Pool +) -> rest.Response: + prev_post_id = None + next_post_id = None + first_post_id = pool.posts[0].post_id, + last_post_id = pool.posts[-1].post_id, + + for previous_item, current_item, next_item in _get_nearby_iter(pool.posts): + if post.post_id == current_item.post_id: + if previous_item != None: + prev_post_id = previous_item.post_id + if next_item != None: + next_post_id = next_item.post_id + break + + return { + "firstPost": serialize_id_post(first_post_id), + "lastPost": serialize_id_post(last_post_id), + "previousPost": serialize_id_post(prev_post_id), + "nextPost": serialize_id_post(next_post_id), + } diff --git a/server/szurubooru/search/configs/post_search_config.py b/server/szurubooru/search/configs/post_search_config.py index 8d4672d4..18194085 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,12 +114,26 @@ 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], order: str +) -> SaQuery: + if pool_id is None: + return query + db_query = query.join(model.PoolPost, sa.and_(model.PoolPost.post_id == model.Post.post_id, model.PoolPost.pool_id == pool_id)) + if order == tokens.SortToken.SORT_DESC: + return db_query.order_by(model.PoolPost.order.desc()) + return db_query.order_by(model.PoolPost.order.asc()) def _category_filter( @@ -153,6 +167,7 @@ def _category_filter( 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 = [] @@ -177,6 +192,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("*")) @@ -382,7 +401,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( [ ( @@ -444,6 +463,10 @@ class PostSearchConfig(BaseSearchConfig): ["feature-date", "feature-time"], (model.Post.last_feature_time, self.SORT_DESC), ), + ( + ["pool"], + lambda subquery, order: _pool_sort(subquery, self.pool_id, order) + ) ] ) 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..4e01915f 100644 --- a/server/szurubooru/search/executor.py +++ b/server/szurubooru/search/executor.py @@ -181,14 +181,19 @@ 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): + order = _get_order(sort_token.order, sort_token.SORT_DESC) + db_query = entry(db_query, order) + 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/api/test_post_retrieving.py b/server/szurubooru/tests/api/test_post_retrieving.py index ac984c24..a149973a 100644 --- a/server/szurubooru/tests/api/test_post_retrieving.py +++ b/server/szurubooru/tests/api/test_post_retrieving.py @@ -14,6 +14,7 @@ def inject_config(config_injector): "privileges": { "posts:list": model.User.RANK_REGULAR, "posts:view": model.User.RANK_REGULAR, + "pools:list": model.User.RANK_REGULAR, }, } ) @@ -125,3 +126,25 @@ def test_trying_to_retrieve_single_without_privileges( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), {"post_id": 999}, ) + + +def test_get_pool_post_around(user_factory, post_factory, pool_factory, pool_post_factory): + p1 = post_factory(id=1) + p2 = post_factory(id=2) + p3 = post_factory(id=3) + db.session.add_all([p1, p2, p3]) + + pool = pool_factory(id=1) + db.session.add(pool) + + pool_posts = [pool_post_factory(pool=pool, post=p1), pool_post_factory(pool=pool, post=p2), pool_post_factory(pool=pool, post=p3)] + db.session.add_all(pool_posts) + + result = posts.get_pool_posts_nearby(p1, pool) + assert result["previousPost"] == None and result["nextPost"]["id"] == 2 + + result = posts.get_pool_posts_nearby(p2, pool) + assert result["previousPost"]["id"] == 1 and result["nextPost"]["id"] == 3 + + result = posts.get_pool_posts_nearby(p3, pool) + assert result["previousPost"]["id"] == 2 and result["nextPost"] == None diff --git a/server/szurubooru/tests/func/test_posts.py b/server/szurubooru/tests/func/test_posts.py index fa1b3bb6..1417da13 100644 --- a/server/szurubooru/tests/func/test_posts.py +++ b/server/szurubooru/tests/func/test_posts.py @@ -105,7 +105,14 @@ def test_serialize_post( pool_category_factory, config_injector, ): - config_injector({"data_url": "http://example.com/", "secret": "test"}) + config_injector({ + "privileges": { + "pools:list": model.User.RANK_REGULAR, + "pools:view": model.User.RANK_REGULAR, + }, + "data_url": "http://example.com/", + "secret": "test" + }) with patch("szurubooru.func.comments.serialize_comment"), patch( "szurubooru.func.users.serialize_micro_user" ), patch("szurubooru.func.posts.files.has"): @@ -249,6 +256,10 @@ def test_serialize_post( "description": "desc", "category": "test-cat1", "postCount": 1, + "firstPost": {"id": 1}, + "lastPost": {"id": 1}, + "previousPost": None, + "nextPost": None, }, { "id": 2, @@ -256,6 +267,10 @@ def test_serialize_post( "description": "desc2", "category": "test-cat2", "postCount": 1, + "firstPost": {"id": 1}, + "lastPost": {"id": 1}, + "previousPost": None, + "nextPost": None, }, ], "user": "post author", 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 b86fa273..13ee16b0 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): @@ -865,6 +866,45 @@ def test_tumbleweed( 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]) + + @pytest.mark.parametrize( "input,expected_post_ids", [