- Posts between the two pools will be combined.
+
Posts in the two pools will be combined.
Category needs to be handled manually.
<%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge this pool.'}) %>
diff --git a/client/html/pool_summary.tpl b/client/html/pool_summary.tpl
index 7b7c7636..8f4e27d0 100644
--- a/client/html/pool_summary.tpl
+++ b/client/html/pool_summary.tpl
@@ -9,7 +9,7 @@
Aliases:
<% for (let name of ctx.pool.names.slice(1)) { %>- <%= ctx.makePoolLink(ctx.pool, false, false, name) %>
- <%= ctx.makePoolLink(ctx.pool.id, false, false, ctx.pool, name) %>
<% } %>
@@ -18,6 +18,6 @@
diff --git a/client/html/pools_page.tpl b/client/html/pools_page.tpl
index 19f3f2a2..6b394839 100644
--- a/client/html/pools_page.tpl
+++ b/client/html/pools_page.tpl
@@ -30,12 +30,12 @@
<% for (let name of pool.names) { %>
- - <%= ctx.makePoolLink(pool, false, false, name) %>
+ - <%= ctx.makePoolLink(pool.id, false, false, pool, name) %>
<% } %>
|
- '><%- pool.postCount %>
+ '><%- pool.postCount %>
|
<%= ctx.makeRelativeTime(pool.creationTime) %>
diff --git a/client/js/controllers/pool_controller.js b/client/js/controllers/pool_controller.js
index 8623cf84..99e2e9bc 100644
--- a/client/js/controllers/pool_controller.js
+++ b/client/js/controllers/pool_controller.js
@@ -5,6 +5,7 @@ const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const Pool = require('../models/pool.js');
+const Post = require('../models/post.js');
const PoolCategoryList = require('../models/pool_category_list.js');
const topNavigation = require('../models/top_navigation.js');
const PoolView = require('../views/pool_view.js');
@@ -42,6 +43,7 @@ class PoolController {
canEditNames: api.hasPrivilege('pools:edit:names'),
canEditCategory: api.hasPrivilege('pools:edit:category'),
canEditDescription: api.hasPrivilege('pools:edit:description'),
+ canEditPosts: api.hasPrivilege('pools:edit:posts'),
canMerge: api.hasPrivilege('pools:merge'),
canDelete: api.hasPrivilege('pools:delete'),
categories: categories,
@@ -84,6 +86,12 @@ class PoolController {
if (e.detail.description !== undefined) {
e.detail.pool.description = e.detail.description;
}
+ if (e.detail.posts !== undefined) {
+ e.detail.pool.posts.clear()
+ for (let post_id of e.detail.posts) {
+ e.detail.pool.posts.add(Post.fromResponse({ id: parseInt(post_id) }))
+ }
+ }
e.detail.pool.save().then(() => {
this._view.showSuccess('Pool saved.');
this._view.enableForm();
diff --git a/client/js/controllers/pool_create_controller.js b/client/js/controllers/pool_create_controller.js
index 0aa3c764..ef6dded2 100644
--- a/client/js/controllers/pool_create_controller.js
+++ b/client/js/controllers/pool_create_controller.js
@@ -39,7 +39,7 @@ class PoolCreateController {
_evtCreate(e) {
this._view.clearMessages();
this._view.disableForm();
- e.detail.pool.save()
+ api.post(uri.formatApiLink('pool'), e.detail)
.then(() => {
this._view.clearMessages();
misc.disableExitConfirmation();
@@ -50,10 +50,6 @@ class PoolCreateController {
this._view.enableForm();
});
}
-
- _evtChange(e) {
- misc.enableExitConfirmation();
- }
}
module.exports = router => {
diff --git a/client/js/controllers/pool_list_controller.js b/client/js/controllers/pool_list_controller.js
index 626dc1ec..1de3926c 100644
--- a/client/js/controllers/pool_list_controller.js
+++ b/client/js/controllers/pool_list_controller.js
@@ -13,26 +13,26 @@ const EmptyView = require('../views/empty_view.js');
const fields = [
'id',
'names',
- /* 'suggestions',
- * 'implications', */
+ 'posts',
'creationTime',
'postCount',
'category'];
class PoolListController {
constructor(ctx) {
+ this._pageController = new PageController();
+
if (!api.hasPrivilege('pools:list')) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view pools.');
return;
}
+ this._ctx = ctx;
+
topNavigation.activate('pools');
topNavigation.setTitle('Listing pools');
- this._ctx = ctx;
- this._pageController = new PageController();
-
this._headerView = new PoolsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
diff --git a/client/js/controllers/post_list_controller.js b/client/js/controllers/post_list_controller.js
index fd1adfea..c79824af 100644
--- a/client/js/controllers/post_list_controller.js
+++ b/client/js/controllers/post_list_controller.js
@@ -17,18 +17,19 @@ const fields = [
class PostListController {
constructor(ctx) {
+ this._pageController = new PageController();
+
if (!api.hasPrivilege('posts:list')) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view posts.');
return;
}
+ this._ctx = ctx;
+
topNavigation.activate('posts');
topNavigation.setTitle('Listing posts');
- this._ctx = ctx;
- this._pageController = new PageController();
-
this._headerView = new PostsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
diff --git a/client/js/controllers/tag_list_controller.js b/client/js/controllers/tag_list_controller.js
index 8bc7dbba..e82461d0 100644
--- a/client/js/controllers/tag_list_controller.js
+++ b/client/js/controllers/tag_list_controller.js
@@ -20,18 +20,19 @@ const fields = [
class TagListController {
constructor(ctx) {
+ this._pageController = new PageController();
+
if (!api.hasPrivilege('tags:list')) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view tags.');
return;
}
+ this._ctx = ctx;
+
topNavigation.activate('tags');
topNavigation.setTitle('Listing tags');
- this._ctx = ctx;
- this._pageController = new PageController();
-
this._headerView = new TagsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
diff --git a/client/js/controllers/user_list_controller.js b/client/js/controllers/user_list_controller.js
index fa878d85..5d666f8a 100644
--- a/client/js/controllers/user_list_controller.js
+++ b/client/js/controllers/user_list_controller.js
@@ -12,6 +12,8 @@ const EmptyView = require('../views/empty_view.js');
class UserListController {
constructor(ctx) {
+ this._pageController = new PageController();
+
if (!api.hasPrivilege('users:list')) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view users.');
@@ -22,7 +24,6 @@ class UserListController {
topNavigation.setTitle('Listing users');
this._ctx = ctx;
- this._pageController = new PageController();
this._headerView = new UsersHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode,
diff --git a/client/js/controls/pool_auto_complete_control.js b/client/js/controls/pool_auto_complete_control.js
index 3c4bff7c..0230c41d 100644
--- a/client/js/controls/pool_auto_complete_control.js
+++ b/client/js/controls/pool_auto_complete_control.js
@@ -9,10 +9,6 @@ function _poolListToMatches(pools, options) {
return pool2.postCount - pool1.postCount;
}).map(pool => {
let cssName = misc.makeCssName(pool.category, 'pool');
- // TODO
- if (options.isPooledWith(pool.id)) {
- cssName += ' disabled';
- }
const caption = (
''
+ misc.escapeHtml(pool.names[0] + ' (' + pool.postCount + ')')
@@ -28,10 +24,6 @@ class PoolAutoCompleteControl extends AutoCompleteControl {
constructor(input, options) {
const minLengthForPartialSearch = 3;
- options = Object.assign({
- isPooledWith: poolId => false,
- }, options);
-
options.getMatches = text => {
const term = misc.escapeSearchTerm(text);
const query = (
diff --git a/client/js/models/pool.js b/client/js/models/pool.js
index e51832b7..8099d2bd 100644
--- a/client/js/models/pool.js
+++ b/client/js/models/pool.js
@@ -7,15 +7,13 @@ const misc = require('../util/misc.js');
class Pool extends events.EventTarget {
constructor() {
- // const PoolList = require('./pool_list.js');
+ const PostList = require('./post_list.js');
super();
this._orig = {};
for (let obj of [this, this._orig]) {
- // TODO
- // obj._suggestions = new PoolList();
- // obj._implications = new PoolList();
+ obj._posts = new PostList();
}
this._updateFromResponse({});
@@ -25,8 +23,7 @@ class Pool extends events.EventTarget {
get names() { return this._names; }
get category() { return this._category; }
get description() { return this._description; }
- /* get suggestions() { return this._suggestions; }
- * get implications() { return this._implications; } */
+ get posts() { return this._posts; }
get postCount() { return this._postCount; }
get creationTime() { return this._creationTime; }
get lastEditTime() { return this._lastEditTime; }
@@ -61,15 +58,9 @@ class Pool extends events.EventTarget {
if (this._description !== this._orig._description) {
detail.description = this._description;
}
- // TODO
- // if (misc.arraysDiffer(this._implications, this._orig._implications)) {
- // detail.implications = this._implications.map(
- // relation => relation.names[0]);
- // }
- // if (misc.arraysDiffer(this._suggestions, this._orig._suggestions)) {
- // detail.suggestions = this._suggestions.map(
- // relation => relation.names[0]);
- // }
+ if (misc.arraysDiffer(this._posts, this._orig._posts)) {
+ detail.posts = this._posts.map(post => post.id);
+ }
let promise = this._id ?
api.put(uri.formatApiLink('pool', this._id), detail) :
@@ -138,13 +129,11 @@ class Pool extends events.EventTarget {
_description: response.description,
_creationTime: response.creationTime,
_lastEditTime: response.lastEditTime,
- _postCount: response.usages || 0,
+ _postCount: response.postCount || 0,
};
for (let obj of [this, this._orig]) {
- // TODO
- // obj._suggestions.sync(response.suggestions);
- // obj._implications.sync(response.implications);
+ obj._posts.sync(response.posts);
}
Object.assign(this, map);
diff --git a/client/js/util/misc.js b/client/js/util/misc.js
index 5911a363..aa62097d 100644
--- a/client/js/util/misc.js
+++ b/client/js/util/misc.js
@@ -163,11 +163,6 @@ function escapeHtml(unsafe) {
}
function arraysDiffer(source1, source2, orderImportant) {
- if ((source1 instanceof Array && source2 === undefined)
- || (source1 === undefined && source2 instanceof Array)) {
- return true
- }
-
source1 = [...source1];
source2 = [...source2];
if (orderImportant === true) {
diff --git a/client/js/util/views.js b/client/js/util/views.js
index 38020d82..1a4d03db 100644
--- a/client/js/util/views.js
+++ b/client/js/util/views.js
@@ -221,20 +221,20 @@ function makeTagLink(name, includeHash, includeCount, tag) {
misc.escapeHtml(text));
}
-function makePoolLink(pool, includeHash, includeCount, name) {
- const category = pool.category;
+function makePoolLink(id, includeHash, includeCount, pool, name) {
+ const category = pool ? pool.category : 'unknown';
let text = name ? name : pool.names[0];
if (includeHash === true) {
text = '#' + text;
}
if (includeCount === true) {
- text += ' (' + pool.postCount + ')';
+ text += ' (' + (pool ? pool.postCount : 0) + ')';
}
return api.hasPrivilege('pools:view') ?
makeElement(
'a',
{
- href: uri.formatClientLink('pool', pool.id),
+ href: uri.formatClientLink('pool', id),
class: misc.makeCssName(category, 'pool'),
},
misc.escapeHtml(text)) :
diff --git a/client/js/views/pool_create_view.js b/client/js/views/pool_create_view.js
index 20218e53..26fca59c 100644
--- a/client/js/views/pool_create_view.js
+++ b/client/js/views/pool_create_view.js
@@ -22,8 +22,13 @@ class PoolCreateView extends events.EventTarget {
'input', e => this._evtNameInput(e));
}
+ if (this._postsFieldNode) {
+ this._postsFieldNode.addEventListener(
+ 'input', e => this._evtPostsInput(e));
+ }
+
for (let node of this._formNode.querySelectorAll(
- 'input, select, textarea')) {
+ 'input, select, textarea, posts')) {
node.addEventListener(
'change', e => {
this.dispatchEvent(new CustomEvent('change'));
@@ -74,16 +79,31 @@ class PoolCreateView extends events.EventTarget {
this._namesFieldNode.setCustomValidity('');
}
+ _evtPostsInput(e) {
+ const regex = /^\d+$/;
+ const list = misc.splitByWhitespace(this._postsFieldNode.value);
+
+ for (let item of list) {
+ if (!regex.test(item)) {
+ this._postsFieldNode.setCustomValidity(
+ `Pool ID "${item}" is not an integer.`);
+ return;
+ }
+ }
+
+ this._postsFieldNode.setCustomValidity('');
+ }
+
_evtSubmit(e) {
e.preventDefault();
- let pool = new Pool()
- pool.names = misc.splitByWhitespace(this._namesFieldNode.value);
- pool.category = this._categoryFieldNode.value;
- pool.description = this._descriptionFieldNode.value;
this.dispatchEvent(new CustomEvent('submit', {
detail: {
- pool: pool,
+ names: misc.splitByWhitespace(this._namesFieldNode.value),
+ category: this._categoryFieldNode.value,
+ description: this._descriptionFieldNode.value,
+ posts: misc.splitByWhitespace(this._postsFieldNode.value)
+ .map(i => parseInt(i, 10))
},
}));
}
@@ -103,6 +123,10 @@ class PoolCreateView extends events.EventTarget {
get _descriptionFieldNode() {
return this._formNode.querySelector('.description textarea');
}
+
+ get _postsFieldNode() {
+ return this._formNode.querySelector('.posts input');
+ }
}
module.exports = PoolCreateView;
diff --git a/client/js/views/pool_edit_view.js b/client/js/views/pool_edit_view.js
index fac84307..7cd8bc9e 100644
--- a/client/js/views/pool_edit_view.js
+++ b/client/js/views/pool_edit_view.js
@@ -4,6 +4,7 @@ const events = require('../events.js');
const api = require('../api.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
+const Post = require('../models/post.js');
const template = views.getTemplate('pool-edit');
@@ -22,8 +23,13 @@ class PoolEditView extends events.EventTarget {
'input', e => this._evtNameInput(e));
}
+ if (this._postsFieldNode) {
+ this._postsFieldNode.addEventListener(
+ 'input', e => this._evtPostsInput(e));
+ }
+
for (let node of this._formNode.querySelectorAll(
- 'input, select, textarea')) {
+ 'input, select, textarea, posts')) {
node.addEventListener(
'change', e => {
this.dispatchEvent(new CustomEvent('change'));
@@ -74,6 +80,21 @@ class PoolEditView extends events.EventTarget {
this._namesFieldNode.setCustomValidity('');
}
+ _evtPostsInput(e) {
+ const regex = /^\d+$/;
+ const list = misc.splitByWhitespace(this._postsFieldNode.value);
+
+ for (let item of list) {
+ if (!regex.test(item)) {
+ this._postsFieldNode.setCustomValidity(
+ `Pool ID "${item}" is not an integer.`);
+ return;
+ }
+ }
+
+ this._postsFieldNode.setCustomValidity('');
+ }
+
_evtSubmit(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
@@ -91,6 +112,10 @@ class PoolEditView extends events.EventTarget {
description: this._descriptionFieldNode ?
this._descriptionFieldNode.value :
undefined,
+
+ posts: this._postsFieldNode ?
+ misc.splitByWhitespace(this._postsFieldNode.value) :
+ undefined,
},
}));
}
@@ -110,6 +135,10 @@ class PoolEditView extends events.EventTarget {
get _descriptionFieldNode() {
return this._formNode.querySelector('.description textarea');
}
+
+ get _postsFieldNode() {
+ return this._formNode.querySelector('.posts input');
+ }
}
module.exports = PoolEditView;
diff --git a/client/js/views/snapshots_page_view.js b/client/js/views/snapshots_page_view.js
index 77fbe132..e7ea264e 100644
--- a/client/js/views/snapshots_page_view.js
+++ b/client/js/views/snapshots_page_view.js
@@ -36,6 +36,8 @@ function _makeResourceLink(type, id) {
return views.makeTagLink(id, true);
} else if (type === 'tag_category') {
return 'category "' + id + '"';
+ } else if (type === 'pool') {
+ return views.makePoolLink(id, true);
}
}
@@ -113,6 +115,19 @@ function _makeItemModification(type, data) {
if (diff.flags) {
_extend(lines, ['Changed flags']);
}
+
+ } else if (type === 'pool') {
+ if (diff.names) {
+ _extend(lines, _formatBasicChange(diff.names, 'names'));
+ }
+ if (diff.category) {
+ _extend(
+ lines, _formatBasicChange(diff.category, 'category'));
+ }
+ if (diff.posts) {
+ _extend(
+ lines, _formatBasicChange(diff.posts, 'posts'));
+ }
}
return lines.join(' ');
diff --git a/server/config.yaml.dist b/server/config.yaml.dist
index 68076239..baf3a34e 100644
--- a/server/config.yaml.dist
+++ b/server/config.yaml.dist
@@ -129,6 +129,10 @@ privileges:
'tag_categories:set_default': moderator
'pools:create': regular
+ 'pools:edit:names': power
+ 'pools:edit:category': power
+ 'pools:edit:description': power
+ 'pools:edit:posts': power
'pools:list': regular
'pools:view': anonymous
'pools:merge': moderator
diff --git a/server/szurubooru/api/pool_api.py b/server/szurubooru/api/pool_api.py
index b2ec11b9..69b99515 100644
--- a/server/szurubooru/api/pool_api.py
+++ b/server/szurubooru/api/pool_api.py
@@ -16,17 +16,6 @@ def _get_pool(params: Dict[str, str]) -> model.Pool:
return pools.get_pool_by_id(params['pool_id'])
-# def _create_if_needed(pool_names: List[str], user: model.User) -> None:
-# if not pool_names:
-# return
-# _existing_pools, new_pools = pools.get_or_create_pools_by_names(pool_names)
-# if len(new_pools):
-# auth.verify_privilege(user, 'pools:create')
-# db.session.flush()
-# for pool in new_pools:
-# snapshots.create(pool, user)
-
-
@rest.routes.get('/pools/?')
def get_pools(ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response:
auth.verify_privilege(ctx.user, 'pools:list')
@@ -34,7 +23,7 @@ def get_pools(ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response:
ctx, lambda pool: _serialize(ctx, pool))
-@rest.routes.post('/pools/?')
+@rest.routes.post('/pool/?')
def create_pool(
ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response:
auth.verify_privilege(ctx.user, 'pools:create')
@@ -42,14 +31,9 @@ def create_pool(
names = ctx.get_param_as_string_list('names')
category = ctx.get_param_as_string('category')
description = ctx.get_param_as_string('description', default='')
- # TODO
- # suggestions = ctx.get_param_as_string_list('suggestions', default=[])
- # implications = ctx.get_param_as_string_list('implications', default=[])
+ posts = ctx.get_param_as_int_list('posts', default=[])
- # _create_if_needed(suggestions, ctx.user)
- # _create_if_needed(implications, ctx.user)
-
- pool = pools.create_pool(names, category)
+ pool = pools.create_pool(names, category, posts)
pools.update_pool_description(pool, description)
ctx.session.add(pool)
ctx.session.flush()
@@ -81,17 +65,10 @@ def update_pool(ctx: rest.Context, params: Dict[str, str]) -> rest.Response:
auth.verify_privilege(ctx.user, 'pools:edit:description')
pools.update_pool_description(
pool, ctx.get_param_as_string('description'))
- # TODO
- # if ctx.has_param('suggestions'):
- # auth.verify_privilege(ctx.user, 'pools:edit:suggestions')
- # suggestions = ctx.get_param_as_string_list('suggestions')
- # _create_if_needed(suggestions, ctx.user)
- # pools.update_pool_suggestions(pool, suggestions)
- # if ctx.has_param('implications'):
- # auth.verify_privilege(ctx.user, 'pools:edit:implications')
- # implications = ctx.get_param_as_string_list('implications')
- # _create_if_needed(implications, ctx.user)
- # pools.update_pool_implications(pool, implications)
+ if ctx.has_param('posts'):
+ auth.verify_privilege(ctx.user, 'pools:edit:posts')
+ posts = ctx.get_param_as_int_list('posts')
+ pools.update_pool_posts(pool, posts)
pool.last_edit_time = datetime.utcnow()
ctx.session.flush()
snapshots.modify(pool, ctx.user)
diff --git a/server/szurubooru/func/pools.py b/server/szurubooru/func/pools.py
index d35fd496..4fc4d36d 100644
--- a/server/szurubooru/func/pools.py
+++ b/server/szurubooru/func/pools.py
@@ -3,7 +3,7 @@ from typing import Any, Optional, Tuple, List, Dict, Callable
from datetime import datetime
import sqlalchemy as sa
from szurubooru import config, db, model, errors, rest
-from szurubooru.func import util, pool_categories, serialization
+from szurubooru.func import util, pool_categories, serialization, posts
@@ -23,7 +23,7 @@ class InvalidPoolNameError(errors.ValidationError):
pass
-class InvalidPoolRelationError(errors.ValidationError):
+class InvalidPoolDuplicateError(errors.ValidationError):
pass
@@ -60,6 +60,10 @@ def _check_name_intersection(
return len(set(names1).intersection(names2)) > 0
+def _check_post_duplication(post_ids: List[int]) -> bool:
+ return len(post_ids) != len(set(post_ids))
+
+
def sort_pools(pools: List[model.Pool]) -> List[model.Pool]:
default_category_name = pool_categories.get_default_category_name()
return sorted(
@@ -84,7 +88,8 @@ class PoolSerializer(serialization.BaseSerializer):
'description': self.serialize_description,
'creationTime': self.serialize_creation_time,
'lastEditTime': self.serialize_last_edit_time,
- 'postCount': self.serialize_post_count
+ 'postCount': self.serialize_post_count,
+ 'posts': self.serialize_posts
}
def serialize_id(self) -> Any:
@@ -111,6 +116,13 @@ class PoolSerializer(serialization.BaseSerializer):
def serialize_post_count(self) -> Any:
return self.pool.post_count
+ def serialize_posts(self) -> Any:
+ return [
+ {
+ 'id': post.post_id
+ }
+ for post in self.pool.posts]
+
def serialize_pool(
pool: model.Pool, options: List[str] = []) -> Optional[rest.Response]:
@@ -180,7 +192,8 @@ def get_or_create_pools_by_names(
if not found:
new_pool = create_pool(
names=[name],
- category_name=pool_category_name)
+ category_name=pool_category_name,
+ post_ids=[])
db.session.add(new_pool)
new_pools.append(new_pool)
return existing_pools, new_pools
@@ -245,11 +258,13 @@ def merge_pools(source_pool: model.Pool, target_pool: model.Pool) -> None:
def create_pool(
names: List[str],
- category_name: str) -> model.Pool:
+ category_name: str,
+ post_ids: List[int]) -> model.Pool:
pool = model.Pool()
pool.creation_time = datetime.utcnow()
update_pool_names(pool, names)
update_pool_category_name(pool, category_name)
+ update_pool_posts(pool, post_ids)
return pool
@@ -299,4 +314,13 @@ def update_pool_description(pool: model.Pool, description: str) -> None:
if util.value_exceeds_column_size(description, model.Pool.description):
raise InvalidPoolDescriptionError('Description is too long.')
pool.description = description or None
-
+
+
+
+def update_pool_posts(pool: model.Pool, post_ids: List[int]) -> None:
+ assert pool
+ if _check_post_duplication(post_ids):
+ raise InvalidPoolDuplicateError('Duplicate post in pool.')
+ pool.posts.clear()
+ for post in posts.get_posts_by_ids(post_ids):
+ pool.posts.append(post)
diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py
index f0224b40..64b00c99 100644
--- a/server/szurubooru/func/posts.py
+++ b/server/szurubooru/func/posts.py
@@ -334,6 +334,22 @@ def get_post_by_id(post_id: int) -> model.Post:
return post
+def get_posts_by_ids(ids: List[int]) -> List[model.Pool]:
+ if len(ids) == 0:
+ return []
+ posts = (
+ db.session.query(model.Post)
+ .filter(
+ sa.sql.or_(
+ model.Post.post_id == post_id
+ for post_id in ids))
+ .all())
+ id_order = {
+ v: k for k, v in enumerate(ids)
+ }
+ return sorted(posts, key=lambda post: id_order.get(post.post_id))
+
+
def try_get_current_post_feature() -> Optional[model.PostFeature]:
return (
db.session
diff --git a/server/szurubooru/func/snapshots.py b/server/szurubooru/func/snapshots.py
index e9ce07c9..66464679 100644
--- a/server/szurubooru/func/snapshots.py
+++ b/server/szurubooru/func/snapshots.py
@@ -38,7 +38,7 @@ def get_pool_snapshot(pool: model.Pool) -> Dict[str, Any]:
return {
'names': [pool_name.name for pool_name in pool.names],
'category': pool.category.name,
- # TODO
+ 'posts': [post.post_id for post in pool.posts]
}
diff --git a/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py b/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py
index 9b663ac6..be87340a 100644
--- a/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py
+++ b/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py
@@ -45,8 +45,18 @@ def upgrade():
sa.PrimaryKeyConstraint('pool_name_id'),
sa.UniqueConstraint('name'))
+ op.create_table(
+ 'pool_post',
+ sa.Column('pool_id', sa.Integer(), nullable=False),
+ sa.Column('post_id', sa.Integer(), nullable=False),
+ sa.Column('ord', sa.Integer(), nullable=False, index=True),
+ sa.ForeignKeyConstraint(['pool_id'], ['pool.id'], ondelete='CASCADE'),
+ sa.ForeignKeyConstraint(['post_id'], ['post.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('pool_id', 'post_id'))
+
def downgrade():
op.drop_index(op.f('ix_pool_name_ord'), table_name='pool_name')
+ op.drop_table('pool_post')
op.drop_table('pool_name')
op.drop_table('pool')
op.drop_table('pool_category')
diff --git a/server/szurubooru/model/__init__.py b/server/szurubooru/model/__init__.py
index cf18b0f7..4f6cb2a6 100644
--- a/server/szurubooru/model/__init__.py
+++ b/server/szurubooru/model/__init__.py
@@ -11,7 +11,7 @@ from szurubooru.model.post import (
PostNote,
PostFeature,
PostSignature)
-from szurubooru.model.pool import Pool, PoolName
+from szurubooru.model.pool import Pool, PoolName, PoolPost
from szurubooru.model.pool_category import PoolCategory
from szurubooru.model.comment import Comment, CommentScore
from szurubooru.model.snapshot import Snapshot
diff --git a/server/szurubooru/model/pool.py b/server/szurubooru/model/pool.py
index 2505150e..ecd6522c 100644
--- a/server/szurubooru/model/pool.py
+++ b/server/szurubooru/model/pool.py
@@ -1,5 +1,8 @@
import sqlalchemy as sa
+from sqlalchemy.ext.orderinglist import ordering_list
+from sqlalchemy.ext.associationproxy import association_proxy
from szurubooru.model.base import Base
+import szurubooru.model as model
class PoolName(Base):
@@ -18,6 +21,32 @@ class PoolName(Base):
def __init__(self, name: str, order: int) -> None:
self.name = name
self.order = order
+
+
+class PoolPost(Base):
+ __tablename__ = 'pool_post'
+
+ pool_id = sa.Column(
+ 'pool_id',
+ sa.Integer,
+ sa.ForeignKey('pool.id'),
+ nullable=False,
+ primary_key=True,
+ index=True)
+ post_id = sa.Column(
+ 'post_id',
+ sa.Integer,
+ sa.ForeignKey('post.id'),
+ nullable=False,
+ primary_key=True,
+ index=True)
+ order = sa.Column('ord', sa.Integer, nullable=False, index=True)
+
+ pool = sa.orm.relationship('Pool', back_populates='_posts')
+ post = sa.orm.relationship('Post')
+
+ def __init__(self, post: model.Post) -> None:
+ self.post_id = post.post_id
class Pool(Base):
__tablename__ = 'pool'
@@ -40,18 +69,23 @@ class Pool(Base):
cascade='all,delete-orphan',
lazy='joined',
order_by='PoolName.order')
+ _posts = sa.orm.relationship(
+ 'PoolPost',
+ back_populates='pool',
+ cascade='all,delete-orphan',
+ lazy='joined',
+ order_by='PoolPost.order',
+ collection_class=ordering_list('order'))
+ posts = association_proxy('_posts', 'post')
- # post_count = sa.orm.column_property(
- # sa.sql.expression.select(
- # [sa.sql.expression.func.count(PostPool.post_id)])
- # .where(PostPool.pool_id == pool_id)
- # .correlate_except(PostPool))
- # TODO
- from random import randint
post_count = sa.orm.column_property(
- sa.sql.expression.select([randint(1, 1000)])
- .limit(1)
- .as_scalar())
+ (
+ sa.sql.expression.select(
+ [sa.sql.expression.func.count(PoolPost.post_id)])
+ .where(PoolPost.pool_id == pool_id)
+ .as_scalar()
+ ),
+ deferred=True)
first_name = sa.orm.column_property(
(
@@ -63,7 +97,6 @@ class Pool(Base):
),
deferred=True)
-
__mapper_args__ = {
'version_id_col': version,
'version_id_generator': False,
diff --git a/server/szurubooru/search/configs/post_search_config.py b/server/szurubooru/search/configs/post_search_config.py
index 9c8de2eb..281826fa 100644
--- a/server/szurubooru/search/configs/post_search_config.py
+++ b/server/szurubooru/search/configs/post_search_config.py
@@ -104,6 +104,18 @@ def _note_filter(
search_util.create_str_filter)(query, criterion, negated)
+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)
+
+
class PostSearchConfig(BaseSearchConfig):
def __init__(self) -> None:
self.user = None # type: Optional[model.User]
@@ -350,6 +362,11 @@ class PostSearchConfig(BaseSearchConfig):
search_util.create_str_filter(
model.Post.flags_string, _flag_transformer)
),
+
+ (
+ ['pool'],
+ _pool_filter
+ ),
])
@property
|