This commit is contained in:
noirscape 2024-03-01 10:25:13 -07:00 committed by GitHub
commit f7b9c9b175
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 373 additions and 4 deletions

View file

@ -0,0 +1,34 @@
@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
.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

View 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

View file

@ -0,0 +1,49 @@
<div class='pool-navigator-container'>
<div class='pool-info-wrapper'>
<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.previousPost) { %>
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.previousPost.id, ctx.parameters) %>'>
<% } %>
prev
<% if (ctx.canViewPosts && ctx.previousPost) { %>
</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>

View file

@ -0,0 +1,4 @@
<div class='pool-navigators'>
<ul>
</ul>
</div>

View file

@ -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>
<% } %>

View file

@ -11,11 +11,17 @@ 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) {
super(ctx);
let poolPostsNearby = Promise.resolve({results: []});
if (api.hasPrivilege("pools:list") && api.hasPrivilege("pools:view")) {
poolPostsNearby = PostList.getNearbyPoolPosts(ctx.parameters.id);
}
let parameters = ctx.parameters;
Promise.all([
Post.get(ctx.parameters.id),
@ -23,9 +29,10 @@ class PostMainController extends BasePostController {
ctx.parameters.id,
parameters ? parameters.query : null
),
poolPostsNearby
]).then(
(responses) => {
const [post, aroundResponse] = responses;
const [post, aroundResponse, poolPostsNearby] = responses;
// remove junk from query, but save it into history so that it can
// be still accessed after history navigation / page refresh
@ -44,6 +51,7 @@ class PostMainController extends BasePostController {
this._post = post;
this._view = new PostMainView({
post: post,
poolPostsNearby: poolPostsNearby,
editMode: editMode,
prevPostId: aroundResponse.prev
? aroundResponse.prev.id
@ -56,6 +64,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,
});

View file

@ -0,0 +1,33 @@
"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.pool,
parameters: { query: `pool:${poolPostNearby.pool.id}` },
linkClass: misc.makeCssName(poolPostNearby.pool.category, "pool"),
canViewPosts: api.hasPrivilege("posts:view"),
canViewPools: api.hasPrivilege("pools:view"),
firstPost: poolPostNearby.firstPost,
previousPost: poolPostNearby.previousPost,
nextPost: poolPostNearby.nextPost,
lastPost: poolPostNearby.lastPost,
})
);
}
}
module.exports = PoolNavigatorControl;

View file

@ -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 (let [i, entry] of this._poolPostNearby.entries()) {
this._installPoolNavigatorNode(entry, i);
}
}
get _poolNavigatorListNode() {
return this._hostNode;
}
_installPoolNavigatorNode(poolPostNearby, i) {
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;

View file

@ -16,6 +16,14 @@ class PostList extends AbstractList {
);
}
static getNearbyPoolPosts(id) {
return api.get(
uri.formatApiLink("post", id, "pools-nearby", {
fields: "id",
})
);
}
static search(text, offset, limit, fields) {
return api
.get(

View file

@ -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.poolPostsNearby);
const showPreviousImage = () => {
if (ctx.prevPostId) {
@ -137,6 +139,20 @@ class PostMainView {
}
}
_installPoolNavigators(poolPostsNearby) {
const poolNavigatorsContainerNode = document.querySelector(
"#content-holder .pool-navigators-container"
);
if (!poolNavigatorsContainerNode) {
return;
}
this.poolNavigatorsControl = new PoolNavigatorListControl(
poolNavigatorsContainerNode,
poolPostsNearby,
);
}
_installCommentForm() {
const commentFormContainer = document.querySelector(
"#content-holder .comment-form-container"

View file

@ -975,6 +975,43 @@ data.
Retrieves information about posts that are before or after an existing post.
## Getting pools around post
- **Request**
`GET /post/<id>/pools-nearby`
- **Output**
```json5
[
{
"pool": <pool>,
"firstPost": <first-post>,
"lastPost": <last-post>,
"nextPost": <next-post>,
"previousPost": <previous-post>
},
...
]
```
- **Field meaning**
- `<pool>`: The associated [micro pool resource](#micro-pool).
- `<first-post>`: A [micro post resource](#micro-post) that displays the first post in the pool.
- `<last-post>`: A [micro post resource](#micro-post) that displays the last post in the pool.
- `<next-post>`: A [micro post resource](#micro-post) that displays the next post in the pool.
- `<previous-post>`: 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**

View file

@ -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,
)
@ -283,6 +281,19 @@ def get_posts_around(
ctx, post_id, lambda post: _serialize_post(ctx, post)
)
@rest.routes.get("/post/(?P<post_id>[^/]+)/pools-nearby/?")
def get_pools_around(
ctx: rest.Context, params: Dict[str, str]
) -> rest.Response:
auth.verify_privilege(ctx.user, "posts:list")
auth.verify_privilege(ctx.user, "posts:view")
auth.verify_privilege(ctx.user, "pools:list")
_search_executor_config.user = ctx.user
post = _get_post(params)
results = posts.get_pools_nearby(post)
return posts.serialize_pool_posts_nearby(results)
@rest.routes.post("/posts/reverse-search/?")
def get_posts_by_image(

View file

@ -1,6 +1,8 @@
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
@ -15,7 +17,6 @@ from szurubooru.func import (
pools,
scores,
serialization,
snapshots,
tags,
users,
util,
@ -96,6 +97,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(
@ -968,3 +976,47 @@ def search_by_image(image_content: bytes) -> List[Tuple[float, model.Post]]:
]
else:
return []
PoolPostsNearby = namedtuple('PoolPostsNearby', 'pool first_post prev_post next_post last_post')
def get_pools_nearby(
post: model.Post
) -> List[PoolPostsNearby]:
response = []
pools = post.pools
for pool in pools:
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
resp_entry = PoolPostsNearby(
pool=pool,
first_post=first_post_id,
last_post=last_post_id,
prev_post=prev_post_id,
next_post=next_post_id,
)
response.append(resp_entry)
return response
def serialize_pool_posts_nearby(
nearby: List[PoolPostsNearby]
) -> Optional[rest.Response]:
return [
{
"pool": pools.serialize_micro_pool(entry.pool),
"firstPost": serialize_micro_post(try_get_post_by_id(entry.first_post), None),
"lastPost": serialize_micro_post(try_get_post_by_id(entry.last_post), None),
"previousPost": serialize_micro_post(try_get_post_by_id(entry.prev_post), None),
"nextPost": serialize_micro_post(try_get_post_by_id(entry.next_post), None),
} for entry in nearby
]

View file

@ -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,55 @@ 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, context_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 = api.post_api.get_pools_around(context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), {"post_id": 2})
assert result[0]["previousPost"]["id"] == 1 and result[0]["nextPost"]["id"] == 3
def test_get_pool_post_around_start(user_factory, post_factory, pool_factory, pool_post_factory, context_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 = api.post_api.get_pools_around(context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), {"post_id": 1})
assert result[0]["previousPost"] == None and result[0]["nextPost"]["id"] == 2
def test_get_pool_post_around_end(user_factory, post_factory, pool_factory, pool_post_factory, context_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 = api.post_api.get_pools_around(context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), {"post_id": 3})
assert result[0]["previousPost"]["id"] == 2 and result[0]["nextPost"] == None
def test_get_pool_post_around_no_pool(user_factory, post_factory, pool_factory, pool_post_factory, context_factory):
p1 = post_factory(id=1)
db.session.add(p1)
result = api.post_api.get_pools_around(context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), {"post_id": 1})
assert result == []