Add list of posts to pools

This commit is contained in:
Ruin0x11 2020-05-04 00:09:33 -07:00
parent d59ecb8e23
commit e6bf102bc0
29 changed files with 267 additions and 117 deletions

View file

@ -14,7 +14,7 @@
white-space: nowrap white-space: nowrap
background: $top-navigation-color background: $top-navigation-color
.names .names
width: 28% width: 84%
.usages .usages
text-align: center text-align: center
width: 8% width: 8%
@ -33,7 +33,7 @@
&:not(:last-child):after &:not(:last-child):after
content: ', ' content: ', '
@media (max-width: 800px) @media (max-width: 800px)
.implications, .suggestions .posts
display: none display: none
.pool-list-header .pool-list-header

View file

@ -20,7 +20,7 @@
</tr> </tr>
<tr> <tr>
<td><code>uploader</code></td> <td><code>uploader</code></td>
<td>uploaded by given use (accepts wildcards)r</td> <td>uploaded by given user (accepts wildcards)</td>
</tr> </tr>
<tr> <tr>
<td><code>upload</code></td> <td><code>upload</code></td>
@ -42,6 +42,10 @@
<td><code>source</code></td> <td><code>source</code></td>
<td>having given source URL (accepts wildcards)</td> <td>having given source URL (accepts wildcards)</td>
</tr> </tr>
<tr>
<td><code>pool</code></td>
<td>belonging to given pool ID</td>
</tr>
<tr> <tr>
<td><code>tag-count</code></td> <td><code>tag-count</code></td>
<td>having given number of tags</td> <td>having given number of tags</td>

View file

@ -22,6 +22,12 @@
value: '', value: '',
}) %> }) %>
</li> </li>
<li class='posts'>
<%= ctx.makeTextInput({
text: 'Posts',
value: '',
}) %>
</li>
</ul> </ul>
<% if (ctx.canCreate) { %> <% if (ctx.canCreate) { %>

View file

@ -28,6 +28,14 @@
}) %> }) %>
<% } %> <% } %>
</li> </li>
<li class='posts'>
<% if (ctx.canEditPosts) { %>
<%= ctx.makeTextInput({
text: 'Posts',
value: ctx.pool.posts.map(post => post.id).join(' ')
}) %>
<% } %>
</li>
</ul> </ul>
<% if (ctx.canEditAnything) { %> <% if (ctx.canEditAnything) { %>

View file

@ -6,7 +6,7 @@
</li> </li>
<li> <li>
<p>Posts between the two pools will be combined. <p>Posts in the two pools will be combined.
Category needs to be handled manually.</p> Category needs to be handled manually.</p>
<%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge this pool.'}) %> <%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge this pool.'}) %>

View file

@ -9,7 +9,7 @@
Aliases:<br/> Aliases:<br/>
<ul><!-- <ul><!--
--><% for (let name of ctx.pool.names.slice(1)) { %><!-- --><% for (let name of ctx.pool.names.slice(1)) { %><!--
--><li><%= ctx.makePoolLink(ctx.pool, false, false, name) %></li><!-- --><li><%= ctx.makePoolLink(ctx.pool.id, false, false, ctx.pool, name) %></li><!--
--><% } %><!-- --><% } %><!--
--></ul> --></ul>
</section> </section>
@ -18,6 +18,6 @@
<section class='description'> <section class='description'>
<hr/> <hr/>
<%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %> <%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeColons(ctx.pool.names[0])}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p> <p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
</section> </section>
</div> </div>

View file

@ -30,12 +30,12 @@
<td class='names'> <td class='names'>
<ul> <ul>
<% for (let name of pool.names) { %> <% for (let name of pool.names) { %>
<li><%= ctx.makePoolLink(pool, false, false, name) %></li> <li><%= ctx.makePoolLink(pool.id, false, false, pool, name) %></li>
<% } %> <% } %>
</ul> </ul>
</td> </td>
<td class='post-count'> <td class='post-count'>
<a href='<%- ctx.formatClientLink('pools', {query: 'pool:' + pool.id}) %>'><%- pool.postCount %></a> <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + pool.id}) %>'><%- pool.postCount %></a>
</td> </td>
<td class='creation-time'> <td class='creation-time'>
<%= ctx.makeRelativeTime(pool.creationTime) %> <%= ctx.makeRelativeTime(pool.creationTime) %>

View file

@ -5,6 +5,7 @@ const api = require('../api.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
const uri = require('../util/uri.js'); const uri = require('../util/uri.js');
const Pool = require('../models/pool.js'); const Pool = require('../models/pool.js');
const Post = require('../models/post.js');
const PoolCategoryList = require('../models/pool_category_list.js'); const PoolCategoryList = require('../models/pool_category_list.js');
const topNavigation = require('../models/top_navigation.js'); const topNavigation = require('../models/top_navigation.js');
const PoolView = require('../views/pool_view.js'); const PoolView = require('../views/pool_view.js');
@ -42,6 +43,7 @@ class PoolController {
canEditNames: api.hasPrivilege('pools:edit:names'), canEditNames: api.hasPrivilege('pools:edit:names'),
canEditCategory: api.hasPrivilege('pools:edit:category'), canEditCategory: api.hasPrivilege('pools:edit:category'),
canEditDescription: api.hasPrivilege('pools:edit:description'), canEditDescription: api.hasPrivilege('pools:edit:description'),
canEditPosts: api.hasPrivilege('pools:edit:posts'),
canMerge: api.hasPrivilege('pools:merge'), canMerge: api.hasPrivilege('pools:merge'),
canDelete: api.hasPrivilege('pools:delete'), canDelete: api.hasPrivilege('pools:delete'),
categories: categories, categories: categories,
@ -84,6 +86,12 @@ class PoolController {
if (e.detail.description !== undefined) { if (e.detail.description !== undefined) {
e.detail.pool.description = e.detail.description; 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(() => { e.detail.pool.save().then(() => {
this._view.showSuccess('Pool saved.'); this._view.showSuccess('Pool saved.');
this._view.enableForm(); this._view.enableForm();

View file

@ -39,7 +39,7 @@ class PoolCreateController {
_evtCreate(e) { _evtCreate(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
e.detail.pool.save() api.post(uri.formatApiLink('pool'), e.detail)
.then(() => { .then(() => {
this._view.clearMessages(); this._view.clearMessages();
misc.disableExitConfirmation(); misc.disableExitConfirmation();
@ -50,10 +50,6 @@ class PoolCreateController {
this._view.enableForm(); this._view.enableForm();
}); });
} }
_evtChange(e) {
misc.enableExitConfirmation();
}
} }
module.exports = router => { module.exports = router => {

View file

@ -13,26 +13,26 @@ const EmptyView = require('../views/empty_view.js');
const fields = [ const fields = [
'id', 'id',
'names', 'names',
/* 'suggestions', 'posts',
* 'implications', */
'creationTime', 'creationTime',
'postCount', 'postCount',
'category']; 'category'];
class PoolListController { class PoolListController {
constructor(ctx) { constructor(ctx) {
this._pageController = new PageController();
if (!api.hasPrivilege('pools:list')) { if (!api.hasPrivilege('pools:list')) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view pools.'); this._view.showError('You don\'t have privileges to view pools.');
return; return;
} }
this._ctx = ctx;
topNavigation.activate('pools'); topNavigation.activate('pools');
topNavigation.setTitle('Listing pools'); topNavigation.setTitle('Listing pools');
this._ctx = ctx;
this._pageController = new PageController();
this._headerView = new PoolsHeaderView({ this._headerView = new PoolsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode, hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters, parameters: ctx.parameters,

View file

@ -17,18 +17,19 @@ const fields = [
class PostListController { class PostListController {
constructor(ctx) { constructor(ctx) {
this._pageController = new PageController();
if (!api.hasPrivilege('posts:list')) { if (!api.hasPrivilege('posts:list')) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view posts.'); this._view.showError('You don\'t have privileges to view posts.');
return; return;
} }
this._ctx = ctx;
topNavigation.activate('posts'); topNavigation.activate('posts');
topNavigation.setTitle('Listing posts'); topNavigation.setTitle('Listing posts');
this._ctx = ctx;
this._pageController = new PageController();
this._headerView = new PostsHeaderView({ this._headerView = new PostsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode, hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters, parameters: ctx.parameters,

View file

@ -20,18 +20,19 @@ const fields = [
class TagListController { class TagListController {
constructor(ctx) { constructor(ctx) {
this._pageController = new PageController();
if (!api.hasPrivilege('tags:list')) { if (!api.hasPrivilege('tags:list')) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view tags.'); this._view.showError('You don\'t have privileges to view tags.');
return; return;
} }
this._ctx = ctx;
topNavigation.activate('tags'); topNavigation.activate('tags');
topNavigation.setTitle('Listing tags'); topNavigation.setTitle('Listing tags');
this._ctx = ctx;
this._pageController = new PageController();
this._headerView = new TagsHeaderView({ this._headerView = new TagsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode, hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters, parameters: ctx.parameters,

View file

@ -12,6 +12,8 @@ const EmptyView = require('../views/empty_view.js');
class UserListController { class UserListController {
constructor(ctx) { constructor(ctx) {
this._pageController = new PageController();
if (!api.hasPrivilege('users:list')) { if (!api.hasPrivilege('users:list')) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view users.'); this._view.showError('You don\'t have privileges to view users.');
@ -22,7 +24,6 @@ class UserListController {
topNavigation.setTitle('Listing users'); topNavigation.setTitle('Listing users');
this._ctx = ctx; this._ctx = ctx;
this._pageController = new PageController();
this._headerView = new UsersHeaderView({ this._headerView = new UsersHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode, hostNode: this._pageController.view.pageHeaderHolderNode,

View file

@ -9,10 +9,6 @@ function _poolListToMatches(pools, options) {
return pool2.postCount - pool1.postCount; return pool2.postCount - pool1.postCount;
}).map(pool => { }).map(pool => {
let cssName = misc.makeCssName(pool.category, 'pool'); let cssName = misc.makeCssName(pool.category, 'pool');
// TODO
if (options.isPooledWith(pool.id)) {
cssName += ' disabled';
}
const caption = ( const caption = (
'<span class="' + cssName + '">' '<span class="' + cssName + '">'
+ misc.escapeHtml(pool.names[0] + ' (' + pool.postCount + ')') + misc.escapeHtml(pool.names[0] + ' (' + pool.postCount + ')')
@ -28,10 +24,6 @@ class PoolAutoCompleteControl extends AutoCompleteControl {
constructor(input, options) { constructor(input, options) {
const minLengthForPartialSearch = 3; const minLengthForPartialSearch = 3;
options = Object.assign({
isPooledWith: poolId => false,
}, options);
options.getMatches = text => { options.getMatches = text => {
const term = misc.escapeSearchTerm(text); const term = misc.escapeSearchTerm(text);
const query = ( const query = (

View file

@ -7,15 +7,13 @@ const misc = require('../util/misc.js');
class Pool extends events.EventTarget { class Pool extends events.EventTarget {
constructor() { constructor() {
// const PoolList = require('./pool_list.js'); const PostList = require('./post_list.js');
super(); super();
this._orig = {}; this._orig = {};
for (let obj of [this, this._orig]) { for (let obj of [this, this._orig]) {
// TODO obj._posts = new PostList();
// obj._suggestions = new PoolList();
// obj._implications = new PoolList();
} }
this._updateFromResponse({}); this._updateFromResponse({});
@ -25,8 +23,7 @@ class Pool extends events.EventTarget {
get names() { return this._names; } get names() { return this._names; }
get category() { return this._category; } get category() { return this._category; }
get description() { return this._description; } get description() { return this._description; }
/* get suggestions() { return this._suggestions; } get posts() { return this._posts; }
* get implications() { return this._implications; } */
get postCount() { return this._postCount; } get postCount() { return this._postCount; }
get creationTime() { return this._creationTime; } get creationTime() { return this._creationTime; }
get lastEditTime() { return this._lastEditTime; } get lastEditTime() { return this._lastEditTime; }
@ -61,15 +58,9 @@ class Pool extends events.EventTarget {
if (this._description !== this._orig._description) { if (this._description !== this._orig._description) {
detail.description = this._description; detail.description = this._description;
} }
// TODO if (misc.arraysDiffer(this._posts, this._orig._posts)) {
// if (misc.arraysDiffer(this._implications, this._orig._implications)) { detail.posts = this._posts.map(post => post.id);
// 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]);
// }
let promise = this._id ? let promise = this._id ?
api.put(uri.formatApiLink('pool', this._id), detail) : api.put(uri.formatApiLink('pool', this._id), detail) :
@ -138,13 +129,11 @@ class Pool extends events.EventTarget {
_description: response.description, _description: response.description,
_creationTime: response.creationTime, _creationTime: response.creationTime,
_lastEditTime: response.lastEditTime, _lastEditTime: response.lastEditTime,
_postCount: response.usages || 0, _postCount: response.postCount || 0,
}; };
for (let obj of [this, this._orig]) { for (let obj of [this, this._orig]) {
// TODO obj._posts.sync(response.posts);
// obj._suggestions.sync(response.suggestions);
// obj._implications.sync(response.implications);
} }
Object.assign(this, map); Object.assign(this, map);

View file

@ -163,11 +163,6 @@ function escapeHtml(unsafe) {
} }
function arraysDiffer(source1, source2, orderImportant) { function arraysDiffer(source1, source2, orderImportant) {
if ((source1 instanceof Array && source2 === undefined)
|| (source1 === undefined && source2 instanceof Array)) {
return true
}
source1 = [...source1]; source1 = [...source1];
source2 = [...source2]; source2 = [...source2];
if (orderImportant === true) { if (orderImportant === true) {

View file

@ -221,20 +221,20 @@ function makeTagLink(name, includeHash, includeCount, tag) {
misc.escapeHtml(text)); misc.escapeHtml(text));
} }
function makePoolLink(pool, includeHash, includeCount, name) { function makePoolLink(id, includeHash, includeCount, pool, name) {
const category = pool.category; const category = pool ? pool.category : 'unknown';
let text = name ? name : pool.names[0]; let text = name ? name : pool.names[0];
if (includeHash === true) { if (includeHash === true) {
text = '#' + text; text = '#' + text;
} }
if (includeCount === true) { if (includeCount === true) {
text += ' (' + pool.postCount + ')'; text += ' (' + (pool ? pool.postCount : 0) + ')';
} }
return api.hasPrivilege('pools:view') ? return api.hasPrivilege('pools:view') ?
makeElement( makeElement(
'a', 'a',
{ {
href: uri.formatClientLink('pool', pool.id), href: uri.formatClientLink('pool', id),
class: misc.makeCssName(category, 'pool'), class: misc.makeCssName(category, 'pool'),
}, },
misc.escapeHtml(text)) : misc.escapeHtml(text)) :

View file

@ -22,8 +22,13 @@ class PoolCreateView extends events.EventTarget {
'input', e => this._evtNameInput(e)); 'input', e => this._evtNameInput(e));
} }
if (this._postsFieldNode) {
this._postsFieldNode.addEventListener(
'input', e => this._evtPostsInput(e));
}
for (let node of this._formNode.querySelectorAll( for (let node of this._formNode.querySelectorAll(
'input, select, textarea')) { 'input, select, textarea, posts')) {
node.addEventListener( node.addEventListener(
'change', e => { 'change', e => {
this.dispatchEvent(new CustomEvent('change')); this.dispatchEvent(new CustomEvent('change'));
@ -74,16 +79,31 @@ class PoolCreateView extends events.EventTarget {
this._namesFieldNode.setCustomValidity(''); 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) { _evtSubmit(e) {
e.preventDefault(); 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', { this.dispatchEvent(new CustomEvent('submit', {
detail: { 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() { get _descriptionFieldNode() {
return this._formNode.querySelector('.description textarea'); return this._formNode.querySelector('.description textarea');
} }
get _postsFieldNode() {
return this._formNode.querySelector('.posts input');
}
} }
module.exports = PoolCreateView; module.exports = PoolCreateView;

View file

@ -4,6 +4,7 @@ const events = require('../events.js');
const api = require('../api.js'); const api = require('../api.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const Post = require('../models/post.js');
const template = views.getTemplate('pool-edit'); const template = views.getTemplate('pool-edit');
@ -22,8 +23,13 @@ class PoolEditView extends events.EventTarget {
'input', e => this._evtNameInput(e)); 'input', e => this._evtNameInput(e));
} }
if (this._postsFieldNode) {
this._postsFieldNode.addEventListener(
'input', e => this._evtPostsInput(e));
}
for (let node of this._formNode.querySelectorAll( for (let node of this._formNode.querySelectorAll(
'input, select, textarea')) { 'input, select, textarea, posts')) {
node.addEventListener( node.addEventListener(
'change', e => { 'change', e => {
this.dispatchEvent(new CustomEvent('change')); this.dispatchEvent(new CustomEvent('change'));
@ -74,6 +80,21 @@ class PoolEditView extends events.EventTarget {
this._namesFieldNode.setCustomValidity(''); 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) { _evtSubmit(e) {
e.preventDefault(); e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', { this.dispatchEvent(new CustomEvent('submit', {
@ -91,6 +112,10 @@ class PoolEditView extends events.EventTarget {
description: this._descriptionFieldNode ? description: this._descriptionFieldNode ?
this._descriptionFieldNode.value : this._descriptionFieldNode.value :
undefined, undefined,
posts: this._postsFieldNode ?
misc.splitByWhitespace(this._postsFieldNode.value) :
undefined,
}, },
})); }));
} }
@ -110,6 +135,10 @@ class PoolEditView extends events.EventTarget {
get _descriptionFieldNode() { get _descriptionFieldNode() {
return this._formNode.querySelector('.description textarea'); return this._formNode.querySelector('.description textarea');
} }
get _postsFieldNode() {
return this._formNode.querySelector('.posts input');
}
} }
module.exports = PoolEditView; module.exports = PoolEditView;

View file

@ -36,6 +36,8 @@ function _makeResourceLink(type, id) {
return views.makeTagLink(id, true); return views.makeTagLink(id, true);
} else if (type === 'tag_category') { } else if (type === 'tag_category') {
return 'category "' + id + '"'; return 'category "' + id + '"';
} else if (type === 'pool') {
return views.makePoolLink(id, true);
} }
} }
@ -113,6 +115,19 @@ function _makeItemModification(type, data) {
if (diff.flags) { if (diff.flags) {
_extend(lines, ['Changed 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('<br/>'); return lines.join('<br/>');

View file

@ -129,6 +129,10 @@ privileges:
'tag_categories:set_default': moderator 'tag_categories:set_default': moderator
'pools:create': regular 'pools:create': regular
'pools:edit:names': power
'pools:edit:category': power
'pools:edit:description': power
'pools:edit:posts': power
'pools:list': regular 'pools:list': regular
'pools:view': anonymous 'pools:view': anonymous
'pools:merge': moderator 'pools:merge': moderator

View file

@ -16,17 +16,6 @@ def _get_pool(params: Dict[str, str]) -> model.Pool:
return pools.get_pool_by_id(params['pool_id']) 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/?') @rest.routes.get('/pools/?')
def get_pools(ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: def get_pools(ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response:
auth.verify_privilege(ctx.user, 'pools:list') 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)) ctx, lambda pool: _serialize(ctx, pool))
@rest.routes.post('/pools/?') @rest.routes.post('/pool/?')
def create_pool( def create_pool(
ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response:
auth.verify_privilege(ctx.user, 'pools:create') auth.verify_privilege(ctx.user, 'pools:create')
@ -42,14 +31,9 @@ def create_pool(
names = ctx.get_param_as_string_list('names') names = ctx.get_param_as_string_list('names')
category = ctx.get_param_as_string('category') category = ctx.get_param_as_string('category')
description = ctx.get_param_as_string('description', default='') description = ctx.get_param_as_string('description', default='')
# TODO posts = ctx.get_param_as_int_list('posts', default=[])
# suggestions = ctx.get_param_as_string_list('suggestions', default=[])
# implications = ctx.get_param_as_string_list('implications', default=[])
# _create_if_needed(suggestions, ctx.user) pool = pools.create_pool(names, category, posts)
# _create_if_needed(implications, ctx.user)
pool = pools.create_pool(names, category)
pools.update_pool_description(pool, description) pools.update_pool_description(pool, description)
ctx.session.add(pool) ctx.session.add(pool)
ctx.session.flush() 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') auth.verify_privilege(ctx.user, 'pools:edit:description')
pools.update_pool_description( pools.update_pool_description(
pool, ctx.get_param_as_string('description')) pool, ctx.get_param_as_string('description'))
# TODO if ctx.has_param('posts'):
# if ctx.has_param('suggestions'): auth.verify_privilege(ctx.user, 'pools:edit:posts')
# auth.verify_privilege(ctx.user, 'pools:edit:suggestions') posts = ctx.get_param_as_int_list('posts')
# suggestions = ctx.get_param_as_string_list('suggestions') pools.update_pool_posts(pool, posts)
# _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)
pool.last_edit_time = datetime.utcnow() pool.last_edit_time = datetime.utcnow()
ctx.session.flush() ctx.session.flush()
snapshots.modify(pool, ctx.user) snapshots.modify(pool, ctx.user)

View file

@ -3,7 +3,7 @@ from typing import Any, Optional, Tuple, List, Dict, Callable
from datetime import datetime from datetime import datetime
import sqlalchemy as sa import sqlalchemy as sa
from szurubooru import config, db, model, errors, rest 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 pass
class InvalidPoolRelationError(errors.ValidationError): class InvalidPoolDuplicateError(errors.ValidationError):
pass pass
@ -60,6 +60,10 @@ def _check_name_intersection(
return len(set(names1).intersection(names2)) > 0 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]: def sort_pools(pools: List[model.Pool]) -> List[model.Pool]:
default_category_name = pool_categories.get_default_category_name() default_category_name = pool_categories.get_default_category_name()
return sorted( return sorted(
@ -84,7 +88,8 @@ class PoolSerializer(serialization.BaseSerializer):
'description': self.serialize_description, 'description': self.serialize_description,
'creationTime': self.serialize_creation_time, 'creationTime': self.serialize_creation_time,
'lastEditTime': self.serialize_last_edit_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: def serialize_id(self) -> Any:
@ -111,6 +116,13 @@ class PoolSerializer(serialization.BaseSerializer):
def serialize_post_count(self) -> Any: def serialize_post_count(self) -> Any:
return self.pool.post_count return self.pool.post_count
def serialize_posts(self) -> Any:
return [
{
'id': post.post_id
}
for post in self.pool.posts]
def serialize_pool( def serialize_pool(
pool: model.Pool, options: List[str] = []) -> Optional[rest.Response]: pool: model.Pool, options: List[str] = []) -> Optional[rest.Response]:
@ -180,7 +192,8 @@ def get_or_create_pools_by_names(
if not found: if not found:
new_pool = create_pool( new_pool = create_pool(
names=[name], names=[name],
category_name=pool_category_name) category_name=pool_category_name,
post_ids=[])
db.session.add(new_pool) db.session.add(new_pool)
new_pools.append(new_pool) new_pools.append(new_pool)
return existing_pools, new_pools return existing_pools, new_pools
@ -245,11 +258,13 @@ def merge_pools(source_pool: model.Pool, target_pool: model.Pool) -> None:
def create_pool( def create_pool(
names: List[str], names: List[str],
category_name: str) -> model.Pool: category_name: str,
post_ids: List[int]) -> model.Pool:
pool = model.Pool() pool = model.Pool()
pool.creation_time = datetime.utcnow() pool.creation_time = datetime.utcnow()
update_pool_names(pool, names) update_pool_names(pool, names)
update_pool_category_name(pool, category_name) update_pool_category_name(pool, category_name)
update_pool_posts(pool, post_ids)
return pool 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): if util.value_exceeds_column_size(description, model.Pool.description):
raise InvalidPoolDescriptionError('Description is too long.') raise InvalidPoolDescriptionError('Description is too long.')
pool.description = description or None 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)

View file

@ -334,6 +334,22 @@ def get_post_by_id(post_id: int) -> model.Post:
return 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]: def try_get_current_post_feature() -> Optional[model.PostFeature]:
return ( return (
db.session db.session

View file

@ -38,7 +38,7 @@ def get_pool_snapshot(pool: model.Pool) -> Dict[str, Any]:
return { return {
'names': [pool_name.name for pool_name in pool.names], 'names': [pool_name.name for pool_name in pool.names],
'category': pool.category.name, 'category': pool.category.name,
# TODO 'posts': [post.post_id for post in pool.posts]
} }

View file

@ -45,8 +45,18 @@ def upgrade():
sa.PrimaryKeyConstraint('pool_name_id'), sa.PrimaryKeyConstraint('pool_name_id'),
sa.UniqueConstraint('name')) 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(): def downgrade():
op.drop_index(op.f('ix_pool_name_ord'), table_name='pool_name') 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_name')
op.drop_table('pool') op.drop_table('pool')
op.drop_table('pool_category') op.drop_table('pool_category')

View file

@ -11,7 +11,7 @@ from szurubooru.model.post import (
PostNote, PostNote,
PostFeature, PostFeature,
PostSignature) 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.pool_category import PoolCategory
from szurubooru.model.comment import Comment, CommentScore from szurubooru.model.comment import Comment, CommentScore
from szurubooru.model.snapshot import Snapshot from szurubooru.model.snapshot import Snapshot

View file

@ -1,5 +1,8 @@
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.ext.associationproxy import association_proxy
from szurubooru.model.base import Base from szurubooru.model.base import Base
import szurubooru.model as model
class PoolName(Base): class PoolName(Base):
@ -18,6 +21,32 @@ class PoolName(Base):
def __init__(self, name: str, order: int) -> None: def __init__(self, name: str, order: int) -> None:
self.name = name self.name = name
self.order = order 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): class Pool(Base):
__tablename__ = 'pool' __tablename__ = 'pool'
@ -40,18 +69,23 @@ class Pool(Base):
cascade='all,delete-orphan', cascade='all,delete-orphan',
lazy='joined', lazy='joined',
order_by='PoolName.order') 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( post_count = sa.orm.column_property(
sa.sql.expression.select([randint(1, 1000)]) (
.limit(1) sa.sql.expression.select(
.as_scalar()) [sa.sql.expression.func.count(PoolPost.post_id)])
.where(PoolPost.pool_id == pool_id)
.as_scalar()
),
deferred=True)
first_name = sa.orm.column_property( first_name = sa.orm.column_property(
( (
@ -63,7 +97,6 @@ class Pool(Base):
), ),
deferred=True) deferred=True)
__mapper_args__ = { __mapper_args__ = {
'version_id_col': version, 'version_id_col': version,
'version_id_generator': False, 'version_id_generator': False,

View file

@ -104,6 +104,18 @@ def _note_filter(
search_util.create_str_filter)(query, criterion, negated) 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): class PostSearchConfig(BaseSearchConfig):
def __init__(self) -> None: def __init__(self) -> None:
self.user = None # type: Optional[model.User] self.user = None # type: Optional[model.User]
@ -350,6 +362,11 @@ class PostSearchConfig(BaseSearchConfig):
search_util.create_str_filter( search_util.create_str_filter(
model.Post.flags_string, _flag_transformer) model.Post.flags_string, _flag_transformer)
), ),
(
['pool'],
_pool_filter
),
]) ])
@property @property