<% 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",
[