Merge branch 'old-pool-divergence' into update/Ruin0x11/improve-pools
This commit is contained in:
commit
2ce243d7e7
33 changed files with 795 additions and 167 deletions
|
@ -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 = #AAA
|
||||
$pool-navigator-background-color = #EEE
|
||||
$input-bad-border-color = #FCC
|
||||
$input-bad-background-color = #FFF5F5
|
||||
$input-good-border-color = #D3E3D3
|
||||
|
|
|
@ -1,47 +1,76 @@
|
|||
@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-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%
|
||||
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
|
||||
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 +90,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
|
||||
|
|
38
client/css/pool-navigator-control.styl
Normal file
38
client/css/pool-navigator-control.styl
Normal file
|
@ -0,0 +1,38 @@
|
|||
@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 $pool-navigator-border-color
|
||||
background: $pool-navigator-background-color
|
||||
&.active
|
||||
font-weight: bold
|
||||
font-size: 1.10em;
|
||||
padding: 0.58em 1em
|
||||
|
||||
.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;
|
||||
margin: 0 .25em;
|
||||
white-space: nowrap;
|
||||
|
||||
|
||||
.darktheme .pool-navigator-container
|
||||
background: $pool-navigator-header-background-color-darktheme
|
9
client/css/pool-navigator-list-control.styl
Normal file
9
client/css/pool-navigator-list-control.styl
Normal file
|
@ -0,0 +1,9 @@
|
|||
.pool-navigators>ul
|
||||
list-style-type: none
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
>li
|
||||
margin-bottom: 1em
|
||||
&:last-child
|
||||
margin-bottom: 0
|
|
@ -40,11 +40,14 @@
|
|||
width: 100%
|
||||
|
||||
.post-container
|
||||
margin-bottom: 2em
|
||||
margin-bottom: 1em
|
||||
|
||||
.post-content
|
||||
margin: 0
|
||||
|
||||
.pool-navigators-container
|
||||
margin-bottom: 2em
|
||||
|
||||
.darktheme .post-view
|
||||
>.sidebar
|
||||
nav.buttons
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class='pool-delete'>
|
||||
<form>
|
||||
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
|
||||
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id + ' sort:pool'}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
|
||||
|
||||
<ul class='input'>
|
||||
<li>
|
||||
|
|
49
client/html/pool_navigator.tpl
Normal file
49
client/html/pool_navigator.tpl
Normal file
|
@ -0,0 +1,49 @@
|
|||
<div class='pool-navigator-container'>
|
||||
<div class='pool-info-wrapper <%= ctx.isActivePool ? "active" : "" %>'>
|
||||
<span class='first'>
|
||||
<% if (ctx.canViewPosts && ctx.firstPost) { %>
|
||||
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.firstPost.id, ctx.parameters) %>'>
|
||||
<% } %>
|
||||
«
|
||||
<% if (ctx.canViewPosts && ctx.firstPost) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
<span class='prev'>
|
||||
<% if (ctx.canViewPosts && ctx.prevPost) { %>
|
||||
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.prevPost.id, ctx.parameters) %>'>
|
||||
<% } %>
|
||||
‹ prev
|
||||
<% if (ctx.canViewPosts && ctx.prevPost) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
<span class='pool-name'>
|
||||
<% if (ctx.canViewPools) { %>
|
||||
<a class='<%- ctx.linkClass %>' href='<%= ctx.formatClientLink("pool", ctx.pool.id) %>'>
|
||||
<% } %>
|
||||
Pool: <%- ctx.pool.names[0] %>
|
||||
<% if (ctx.canViewPools) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
<span class='next'>
|
||||
<% if (ctx.canViewPosts && ctx.nextPost) { %>
|
||||
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.nextPost.id, ctx.parameters) %>'>
|
||||
<% } %>
|
||||
next ›
|
||||
<% if (ctx.canViewPosts && ctx.nextPost) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
<span class='last'>
|
||||
<% if (ctx.canViewPosts && ctx.lastPost) { %>
|
||||
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.lastPost.id, ctx.parameters) %>'>
|
||||
<% } %>
|
||||
»
|
||||
<% if (ctx.canViewPosts && ctx.lastPost) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
4
client/html/pool_navigator_list.tpl
Normal file
4
client/html/pool_navigator_list.tpl
Normal file
|
@ -0,0 +1,4 @@
|
|||
<div class='pool-navigators'>
|
||||
<ul>
|
||||
</ul>
|
||||
</div>
|
|
@ -18,6 +18,6 @@
|
|||
<section class='description'>
|
||||
<hr/>
|
||||
<%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %>
|
||||
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
|
||||
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id + ' sort:pool'}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
@ -1,48 +1,19 @@
|
|||
<div class='pool-list table-wrap'>
|
||||
<% if (ctx.postFlow) { %><div class='pool-list post-flow'><% } else { %><div class='pool-list'><% } %>
|
||||
<% if (ctx.response.results.length) { %>
|
||||
<table>
|
||||
<thead>
|
||||
<th class='names'>
|
||||
<% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.query) { %>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:name'}) %>'>Pool name(s)</a>
|
||||
<% } else { %>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:name'}) %>'>Pool name(s)</a>
|
||||
<ul>
|
||||
<% for (let pool of ctx.response.results) { %>
|
||||
<li data-pool-id='<%= pool.id %>'>
|
||||
<a class='thumbnail-wrapper' href='<%= ctx.canViewPools ? ctx.formatClientLink("pool", pool.id) : "" %>'>
|
||||
<% if (ctx.canViewPosts) { %>
|
||||
<%= ctx.makePoolThumbnails(pool.posts, ctx.postFlow) %>
|
||||
<% } %>
|
||||
</th>
|
||||
<th class='post-count'>
|
||||
<% if (ctx.parameters.query == 'sort:post-count') { %>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:post-count'}) %>'>Post count</a>
|
||||
<% } else { %>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:post-count'}) %>'>Post count</a>
|
||||
<% } %>
|
||||
</th>
|
||||
<th class='creation-time'>
|
||||
<% if (ctx.parameters.query == 'sort:creation-time') { %>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:creation-time'}) %>'>Created on</a>
|
||||
<% } else { %>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:creation-time'}) %>'>Created on</a>
|
||||
<% } %>
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for (let pool of ctx.response.results) { %>
|
||||
<tr>
|
||||
<td class='names'>
|
||||
<ul>
|
||||
<% for (let name of pool.names) { %>
|
||||
<li><%= ctx.makePoolLink(pool.id, false, false, pool, name) %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</td>
|
||||
<td class='post-count'>
|
||||
<a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + pool.id}) %>'><%- pool.postCount %></a>
|
||||
</td>
|
||||
<td class='creation-time'>
|
||||
<%= ctx.makeRelativeTime(pool.creationTime) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</a>
|
||||
<div class='pool-name'>
|
||||
<%= ctx.makePoolLink(pool.id, false, false, pool, name) %>
|
||||
</div>
|
||||
</li>
|
||||
<% } %>
|
||||
<%= ctx.makeFlexboxAlign() %>
|
||||
</ul>
|
||||
<% } %>
|
||||
</div>
|
||||
|
|
|
@ -54,6 +54,10 @@
|
|||
<div class='content'>
|
||||
<div class='post-container'></div>
|
||||
|
||||
<% if (ctx.canListPools && ctx.canViewPools) { %>
|
||||
<div class='pool-navigators-container'></div>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.canListComments) { %>
|
||||
<div class='comments-container'></div>
|
||||
<% } %>
|
||||
|
|
|
@ -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");
|
||||
|
@ -108,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);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
ctx.parameters.id,
|
||||
parameters ? parameters.query : null
|
||||
);
|
||||
}
|
||||
|
||||
let parameters = ctx.parameters;
|
||||
Promise.all([
|
||||
Post.get(ctx.parameters.id),
|
||||
|
@ -23,9 +31,11 @@ class PostMainController extends BasePostController {
|
|||
ctx.parameters.id,
|
||||
parameters ? parameters.query : null
|
||||
),
|
||||
poolPostsAround
|
||||
]).then(
|
||||
(responses) => {
|
||||
const [post, aroundResponse] = 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
|
||||
|
@ -39,11 +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,
|
||||
activePool: activePool,
|
||||
editMode: editMode,
|
||||
prevPostId: aroundResponse.prev
|
||||
? aroundResponse.prev.id
|
||||
|
@ -56,6 +75,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,
|
||||
});
|
||||
|
||||
|
|
35
client/js/controls/pool_navigator_control.js
Normal file
35
client/js/controls/pool_navigator_control.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
"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, poolPostAround, isActivePool) {
|
||||
super();
|
||||
this._hostNode = hostNode;
|
||||
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"),
|
||||
firstPost: poolPostAround.firstPost,
|
||||
prevPost: poolPostAround.prevPost,
|
||||
nextPost: poolPostAround.nextPost,
|
||||
lastPost: poolPostAround.lastPost,
|
||||
isActivePool: isActivePool
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolNavigatorControl;
|
59
client/js/controls/pool_navigator_list_control.js
Normal file
59
client/js/controls/pool_navigator_list_control.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
"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, poolPostsAround, activePool) {
|
||||
super();
|
||||
this._hostNode = hostNode;
|
||||
this._poolPostsAround = poolPostsAround;
|
||||
this._activePool = activePool;
|
||||
this._indexToNode = {};
|
||||
|
||||
for (let [i, entry] of this._poolPostsAround.entries()) {
|
||||
this._installPoolNavigatorNode(entry, i);
|
||||
}
|
||||
}
|
||||
|
||||
get _poolNavigatorListNode() {
|
||||
return this._hostNode;
|
||||
}
|
||||
|
||||
_installPoolNavigatorNode(poolPostAround, i) {
|
||||
const isActivePool = poolPostAround.pool.id == this._activePool
|
||||
const poolListItemNode = document.createElement("div");
|
||||
const poolControl = new PoolNavigatorControl(
|
||||
poolListItemNode,
|
||||
poolPostAround,
|
||||
isActivePool
|
||||
);
|
||||
// events.proxyEvent(commentControl, this, "submit");
|
||||
// events.proxyEvent(commentControl, this, "score");
|
||||
// events.proxyEvent(commentControl, this, "delete");
|
||||
this._indexToNode[poolPostAround.id] = poolListItemNode;
|
||||
if (isActivePool) {
|
||||
this._poolNavigatorListNode.insertBefore(poolListItemNode, this._poolNavigatorListNode.firstChild);
|
||||
} else {
|
||||
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;
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
@ -57,6 +58,7 @@ class PostMainView {
|
|||
this._installSidebar(ctx);
|
||||
this._installCommentForm();
|
||||
this._installComments(ctx.post.comments);
|
||||
this._installPoolNavigators(ctx.poolPostsAround, ctx.activePool);
|
||||
|
||||
const showPreviousImage = () => {
|
||||
if (ctx.prevPostId) {
|
||||
|
@ -137,6 +139,21 @@ class PostMainView {
|
|||
}
|
||||
}
|
||||
|
||||
_installPoolNavigators(poolPostsAround, activePool) {
|
||||
const poolNavigatorsContainerNode = document.querySelector(
|
||||
"#content-holder .pool-navigators-container"
|
||||
);
|
||||
if (!poolNavigatorsContainerNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.poolNavigatorsControl = new PoolNavigatorListControl(
|
||||
poolNavigatorsContainerNode,
|
||||
poolPostsAround,
|
||||
activePool
|
||||
);
|
||||
}
|
||||
|
||||
_installCommentForm() {
|
||||
const commentFormContainer = document.querySelector(
|
||||
"#content-holder .comment-form-container"
|
||||
|
|
91
doc/API.md
91
doc/API.md
|
@ -794,38 +794,39 @@ data.
|
|||
|
||||
**Sort style tokens**
|
||||
|
||||
| `<value>` | 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` |
|
||||
| `<value>` | 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**
|
||||
|
||||
|
@ -1357,6 +1358,7 @@ data.
|
|||
|
||||
| `<key>` | Description |
|
||||
| ------------------- | ----------------------------------------- |
|
||||
| `id` | having given pool number |
|
||||
| `name` | having given name (accepts wildcards) |
|
||||
| `category` | having given category (accepts wildcards) |
|
||||
| `creation-date` | created at given date |
|
||||
|
@ -1369,18 +1371,19 @@ data.
|
|||
|
||||
**Sort style tokens**
|
||||
|
||||
| `<value>` | 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 |
|
||||
| `<value>` | 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**
|
||||
|
||||
|
|
|
@ -13,3 +13,4 @@ pytz>=2018.3
|
|||
pyyaml>=3.11
|
||||
SQLAlchemy>=1.0.12, <1.4
|
||||
yt-dlp
|
||||
alembic_utils>=0.5.6
|
||||
|
|
|
@ -284,6 +284,19 @@ def get_posts_around(
|
|||
)
|
||||
|
||||
|
||||
@rest.routes.get("/post/(?P<post_id>[^/]+)/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] = {}
|
||||
|
|
|
@ -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:
|
||||
|
@ -968,3 +973,81 @@ def search_by_image(image_content: bytes) -> List[Tuple[float, model.Post]]:
|
|||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
PoolPostsAround = namedtuple('PoolPostsAround', 'pool first_post prev_post next_post last_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, None, None]
|
||||
if delta == -2:
|
||||
around[pool_id][0] = post_id
|
||||
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)
|
||||
|
||||
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():
|
||||
first_post = None
|
||||
prev_post = None
|
||||
next_post = None
|
||||
last_post = None
|
||||
if entry[1] is not None:
|
||||
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
|
||||
|
||||
|
||||
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),
|
||||
"firstPost": entry.first_post,
|
||||
"prevPost": entry.prev_post,
|
||||
"nextPost": entry.next_post,
|
||||
"lastPost": entry.last_post
|
||||
}
|
||||
for entry in sort_pool_posts_around(around)
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
68
server/szurubooru/migrations/functions.py
Normal file
68
server/szurubooru/migrations/functions.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
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)
|
||||
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;
|
||||
END
|
||||
$$
|
||||
""")
|
|
@ -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)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)),
|
||||
(
|
||||
|
|
|
@ -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,49 @@ 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())
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def _category_filter(
|
||||
|
@ -153,6 +190,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,10 +215,19 @@ 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("*"))
|
||||
|
||||
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
|
||||
|
@ -382,7 +429,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 +491,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)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
@ -181,14 +170,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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, Callable
|
||||
from typing import Any, Callable, Union
|
||||
|
||||
SaColumn = Any
|
||||
SaQuery = Any
|
||||
|
|
|
@ -1221,3 +1221,30 @@ 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].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
|
||||
|
|
|
@ -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):
|
||||
|
@ -915,3 +916,42 @@ def test_search_by_tag_category(
|
|||
)
|
||||
db.session.flush()
|
||||
verify_unpaged(input, expected_post_ids)
|
||||
|
||||
|
||||
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])
|
||||
|
|
Loading…
Reference in a new issue