From d59ecb8e230a55e6875076824723580bd3f4419b Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Sun, 3 May 2020 19:53:28 -0700 Subject: Add pool CRUD operations/pages --- client/js/controllers/pool_controller.js | 141 +++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 client/js/controllers/pool_controller.js (limited to 'client/js/controllers/pool_controller.js') diff --git a/client/js/controllers/pool_controller.js b/client/js/controllers/pool_controller.js new file mode 100644 index 0000000..8623cf8 --- /dev/null +++ b/client/js/controllers/pool_controller.js @@ -0,0 +1,141 @@ +'use strict'; + +const router = require('../router.js'); +const api = require('../api.js'); +const misc = require('../util/misc.js'); +const uri = require('../util/uri.js'); +const Pool = require('../models/pool.js'); +const PoolCategoryList = require('../models/pool_category_list.js'); +const topNavigation = require('../models/top_navigation.js'); +const PoolView = require('../views/pool_view.js'); +const EmptyView = require('../views/empty_view.js'); + +class PoolController { + constructor(ctx, section) { + if (!api.hasPrivilege('pools:view')) { + this._view = new EmptyView(); + this._view.showError('You don\'t have privileges to view pools.'); + return; + } + + Promise.all([ + PoolCategoryList.get(), + Pool.get(ctx.parameters.id) + ]).then(responses => { + const [poolCategoriesResponse, pool] = responses; + + topNavigation.activate('pools'); + topNavigation.setTitle('Pool #' + pool.names[0]); + + this._name = ctx.parameters.name; + pool.addEventListener('change', e => this._evtSaved(e, section)); + + const categories = {}; + for (let category of poolCategoriesResponse.results) { + categories[category.name] = category.name; + } + + this._view = new PoolView({ + pool: pool, + section: section, + canEditAnything: api.hasPrivilege('pools:edit'), + canEditNames: api.hasPrivilege('pools:edit:names'), + canEditCategory: api.hasPrivilege('pools:edit:category'), + canEditDescription: api.hasPrivilege('pools:edit:description'), + canMerge: api.hasPrivilege('pools:merge'), + canDelete: api.hasPrivilege('pools:delete'), + categories: categories, + escapeColons: uri.escapeColons, + }); + + this._view.addEventListener('change', e => this._evtChange(e)); + this._view.addEventListener('submit', e => this._evtUpdate(e)); + this._view.addEventListener('merge', e => this._evtMerge(e)); + this._view.addEventListener('delete', e => this._evtDelete(e)); + }, + error => { + this._view = new EmptyView(); + this._view.showError(error.message); + }); + } + + _evtChange(e) { + misc.enableExitConfirmation(); + } + + _evtSaved(e, section) { + misc.disableExitConfirmation(); + if (this._name !== e.detail.pool.names[0]) { + router.replace( + uri.formatClientLink('pool', e.detail.pool.id, section), + null, false); + } + } + + _evtUpdate(e) { + this._view.clearMessages(); + this._view.disableForm(); + if (e.detail.names !== undefined) { + e.detail.pool.names = e.detail.names; + } + if (e.detail.category !== undefined) { + e.detail.pool.category = e.detail.category; + } + if (e.detail.description !== undefined) { + e.detail.pool.description = e.detail.description; + } + e.detail.pool.save().then(() => { + this._view.showSuccess('Pool saved.'); + this._view.enableForm(); + }, error => { + this._view.showError(error.message); + this._view.enableForm(); + }); + } + + _evtMerge(e) { + this._view.clearMessages(); + this._view.disableForm(); + e.detail.pool + .merge(e.detail.targetPoolName, e.detail.addAlias) + .then(() => { + this._view.showSuccess('Pool merged.'); + this._view.enableForm(); + router.replace( + uri.formatClientLink( + 'pool', e.detail.targetPoolName, 'merge'), + null, false); + }, error => { + this._view.showError(error.message); + this._view.enableForm(); + }); + } + + _evtDelete(e) { + this._view.clearMessages(); + this._view.disableForm(); + e.detail.pool.delete() + .then(() => { + const ctx = router.show(uri.formatClientLink('pools')); + ctx.controller.showSuccess('Pool deleted.'); + }, error => { + this._view.showError(error.message); + this._view.enableForm(); + }); + } +} + +module.exports = router => { + router.enter(['pool', ':id', 'edit'], (ctx, next) => { + ctx.controller = new PoolController(ctx, 'edit'); + }); + router.enter(['pool', ':id', 'merge'], (ctx, next) => { + ctx.controller = new PoolController(ctx, 'merge'); + }); + router.enter(['pool', ':id', 'delete'], (ctx, next) => { + ctx.controller = new PoolController(ctx, 'delete'); + }); + router.enter(['pool', ':id'], (ctx, next) => { + ctx.controller = new PoolController(ctx, 'summary'); + }); +}; -- cgit v1.3 From e6bf102bc03504d9abc584f15da70a5fed7fa950 Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Mon, 4 May 2020 00:09:33 -0700 Subject: Add list of posts to pools --- client/css/pool-list-view.styl | 4 +- client/html/help_search_posts.tpl | 6 ++- client/html/pool_create.tpl | 6 +++ client/html/pool_edit.tpl | 8 ++++ client/html/pool_merge.tpl | 2 +- client/html/pool_summary.tpl | 4 +- client/html/pools_page.tpl | 4 +- client/js/controllers/pool_controller.js | 8 ++++ client/js/controllers/pool_create_controller.js | 6 +-- client/js/controllers/pool_list_controller.js | 10 ++-- client/js/controllers/post_list_controller.js | 7 +-- client/js/controllers/tag_list_controller.js | 7 +-- client/js/controllers/user_list_controller.js | 3 +- client/js/controls/pool_auto_complete_control.js | 8 ---- client/js/models/pool.js | 27 ++++------- client/js/util/misc.js | 5 -- client/js/util/views.js | 8 ++-- client/js/views/pool_create_view.js | 36 +++++++++++--- client/js/views/pool_edit_view.js | 31 +++++++++++- client/js/views/snapshots_page_view.js | 15 ++++++ server/config.yaml.dist | 4 ++ server/szurubooru/api/pool_api.py | 37 +++------------ server/szurubooru/func/pools.py | 36 +++++++++++--- server/szurubooru/func/posts.py | 16 +++++++ server/szurubooru/func/snapshots.py | 2 +- .../versions/6a2f424ec9d2_create_pool_tables.py | 10 ++++ server/szurubooru/model/__init__.py | 2 +- server/szurubooru/model/pool.py | 55 +++++++++++++++++----- .../search/configs/post_search_config.py | 17 +++++++ 29 files changed, 267 insertions(+), 117 deletions(-) (limited to 'client/js/controllers/pool_controller.js') diff --git a/client/css/pool-list-view.styl b/client/css/pool-list-view.styl index 2333a37..2daf694 100644 --- a/client/css/pool-list-view.styl +++ b/client/css/pool-list-view.styl @@ -14,7 +14,7 @@ white-space: nowrap background: $top-navigation-color .names - width: 28% + width: 84% .usages text-align: center width: 8% @@ -33,7 +33,7 @@ &:not(:last-child):after content: ', ' @media (max-width: 800px) - .implications, .suggestions + .posts display: none .pool-list-header diff --git a/client/html/help_search_posts.tpl b/client/html/help_search_posts.tpl index d1097b7..30a986f 100644 --- a/client/html/help_search_posts.tpl +++ b/client/html/help_search_posts.tpl @@ -20,7 +20,7 @@ uploader - uploaded by given use (accepts wildcards)r + uploaded by given user (accepts wildcards) upload @@ -42,6 +42,10 @@ source having given source URL (accepts wildcards) + + pool + belonging to given pool ID + tag-count having given number of tags diff --git a/client/html/pool_create.tpl b/client/html/pool_create.tpl index fec95ef..bf84dc9 100644 --- a/client/html/pool_create.tpl +++ b/client/html/pool_create.tpl @@ -22,6 +22,12 @@ value: '', }) %> +
  • + <%= ctx.makeTextInput({ + text: 'Posts', + value: '', + }) %> +
  • <% if (ctx.canCreate) { %> diff --git a/client/html/pool_edit.tpl b/client/html/pool_edit.tpl index 0a0e1b0..1cbb62e 100644 --- a/client/html/pool_edit.tpl +++ b/client/html/pool_edit.tpl @@ -28,6 +28,14 @@ }) %> <% } %> +
  • + <% if (ctx.canEditPosts) { %> + <%= ctx.makeTextInput({ + text: 'Posts', + value: ctx.pool.posts.map(post => post.id).join(' ') + }) %> + <% } %> +
  • <% if (ctx.canEditAnything) { %> diff --git a/client/html/pool_merge.tpl b/client/html/pool_merge.tpl index ce0bf92..ea5776a 100644 --- a/client/html/pool_merge.tpl +++ b/client/html/pool_merge.tpl @@ -6,7 +6,7 @@
  • -

    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 7b7c763..8f4e27d 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 @@

    <%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %> -

    This pool has '><%- ctx.pool.postCount %> post(s).

    +

    This pool has '><%- ctx.pool.postCount %> post(s).

    diff --git a/client/html/pools_page.tpl b/client/html/pools_page.tpl index 19f3f2a..6b39483 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 8623cf8..99e2e9b 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 0aa3c76..ef6dded 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 626dc1e..1de3926 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 fd1adfe..c79824a 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 8bc7dbb..e82461d 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 fa878d8..5d666f8 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 3c4bff7..0230c41 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 e51832b..8099d2b 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 5911a36..aa62097 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 38020d8..1a4d03d 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 20218e5..26fca59 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 fac8430..7cd8bc9 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 77fbe13..e7ea264 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 6807623..baf3a34 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 b2ec11b..69b9951 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=[]) - - # _create_if_needed(suggestions, ctx.user) - # _create_if_needed(implications, ctx.user) + posts = ctx.get_param_as_int_list('posts', default=[]) - 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 d35fd49..4fc4d36 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 f0224b4..64b00c9 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 e9ce07c..6646467 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 9b663ac..be87340 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 cf18b0f..4f6cb2a 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 2505150..ecd6522 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 9c8de2e..281826f 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 -- cgit v1.3 From 6b8e3f251f69c9d7d560148100cdb8df941a81c2 Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Mon, 4 May 2020 15:15:30 -0700 Subject: Implement pool merging --- client/css/pool-list-view.styl | 2 +- client/html/pool.tpl | 2 +- client/html/pools_page.tpl | 4 +- client/js/controllers/pool_controller.js | 4 +- client/js/controls/pool_input_control.js | 25 ---- client/js/models/post.js | 199 +++++++++++++++++-------------- client/js/views/pool_merge_view.js | 9 +- server/szurubooru/func/pools.py | 47 ++------ 8 files changed, 130 insertions(+), 162 deletions(-) (limited to 'client/js/controllers/pool_controller.js') diff --git a/client/css/pool-list-view.styl b/client/css/pool-list-view.styl index 2daf694..0f57649 100644 --- a/client/css/pool-list-view.styl +++ b/client/css/pool-list-view.styl @@ -15,7 +15,7 @@ background: $top-navigation-color .names width: 84% - .usages + .post-count text-align: center width: 8% .creation-time diff --git a/client/html/pool.tpl b/client/html/pool.tpl index 5af10c9..e1e5311 100644 --- a/client/html/pool.tpl +++ b/client/html/pool.tpl @@ -1,5 +1,5 @@
    -

    <%- ctx.pool.first_name %>

    +

    <%- ctx.pool.names[0] %>

      diff --git a/client/html/pool_summary.tpl b/client/html/pool_summary.tpl index 8f4e27d..2b70d27 100644 --- a/client/html/pool_summary.tpl +++ b/client/html/pool_summary.tpl @@ -6,12 +6,12 @@
      - Aliases:
      -
        <% for (let name of ctx.pool.names.slice(1)) { %>
      • <%= ctx.makePoolLink(ctx.pool.id, false, false, ctx.pool, name) %>
      • <% } %>
      + Aliases:
      +
        + <% for (let name of ctx.pool.names.slice(1)) { %> +
      • <%= ctx.makePoolLink(ctx.pool.id, false, false, ctx.pool, name) %>
      • + <% } %> +
      diff --git a/client/js/controllers/pool_controller.js b/client/js/controllers/pool_controller.js index e243cd8..22920f0 100644 --- a/client/js/controllers/pool_controller.js +++ b/client/js/controllers/pool_controller.js @@ -54,8 +54,7 @@ class PoolController { this._view.addEventListener('submit', e => this._evtUpdate(e)); this._view.addEventListener('merge', e => this._evtMerge(e)); this._view.addEventListener('delete', e => this._evtDelete(e)); - }, - error => { + }, error => { this._view = new EmptyView(); this._view.showError(error.message); }); @@ -68,9 +67,7 @@ class PoolController { _evtSaved(e, section) { misc.disableExitConfirmation(); if (this._name !== e.detail.pool.names[0]) { - router.replace( - uri.formatClientLink('pool', e.detail.pool.id, section), - null, false); + router.replace(uri.formatClientLink('pool', e.detail.pool.id, section), null, false); } } @@ -87,9 +84,9 @@ class PoolController { e.detail.pool.description = e.detail.description; } if (e.detail.posts !== undefined) { - e.detail.pool.posts.clear() + 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.posts.add(Post.fromResponse({ id: parseInt(post_id) })); } } e.detail.pool.save().then(() => { diff --git a/client/js/controllers/pool_create_controller.js b/client/js/controllers/pool_create_controller.js index ef6dded..0684693 100644 --- a/client/js/controllers/pool_create_controller.js +++ b/client/js/controllers/pool_create_controller.js @@ -16,8 +16,7 @@ class PoolCreateController { return; } - PoolCategoryList.get() - .then(poolCategoriesResponse => { + PoolCategoryList.get().then(poolCategoriesResponse => { const categories = {}; for (let category of poolCategoriesResponse.results) { categories[category.name] = category.name; diff --git a/client/js/controls/pool_input_control.js b/client/js/controls/pool_input_control.js index a827206..95dfc5c 100644 --- a/client/js/controls/pool_input_control.js +++ b/client/js/controls/pool_input_control.js @@ -81,105 +81,105 @@ class PoolInputControl extends events.EventTarget { return Promise.resolve(); } - this.pools.add(pool, false) + this.pools.add(pool, false) - const listItemNode = this._createListItemNode(pool); - if (!pool.category) { - listItemNode.classList.add('new'); - } - this._poolListNode.prependChild(listItemNode); - _fadeOutListItemNodeStatus(listItemNode); + const listItemNode = this._createListItemNode(pool); + if (!pool.category) { + listItemNode.classList.add('new'); + } + this._poolListNode.prependChild(listItemNode); + _fadeOutListItemNodeStatus(listItemNode); - this.dispatchEvent(new CustomEvent('add', { - detail: {pool: pool, source: source}, - })); - this.dispatchEvent(new CustomEvent('change')); + this.dispatchEvent(new CustomEvent('add', { + detail: {pool: pool, source: source}, + })); + this.dispatchEvent(new CustomEvent('change')); - return Promise.resolve(); + return Promise.resolve(); } - deletePool(pool) { - if (!this.pools.hasPoolId(pool.id)) { - return; - } - this.pools.removeById(pool.id); - this._hideAutoComplete(); + deletePool(pool) { + if (!this.pools.hasPoolId(pool.id)) { + return; + } + this.pools.removeById(pool.id); + this._hideAutoComplete(); - this._deleteListItemNode(pool); + this._deleteListItemNode(pool); - this.dispatchEvent(new CustomEvent('remove', { - detail: {pool: pool}, - })); - this.dispatchEvent(new CustomEvent('change')); - } + this.dispatchEvent(new CustomEvent('remove', { + detail: {pool: pool}, + })); + this.dispatchEvent(new CustomEvent('change')); + } - _createListItemNode(pool) { - const className = pool.category ? - misc.makeCssName(pool.category, 'pool') : - null; + _createListItemNode(pool) { + const className = pool.category ? + misc.makeCssName(pool.category, 'pool') : + null; - const poolLinkNode = document.createElement('a'); - if (className) { - poolLinkNode.classList.add(className); - } - poolLinkNode.setAttribute( - 'href', uri.formatClientLink('pool', pool.names[0])); + const poolLinkNode = document.createElement('a'); + if (className) { + poolLinkNode.classList.add(className); + } + poolLinkNode.setAttribute( + 'href', uri.formatClientLink('pool', pool.names[0])); - const poolIconNode = document.createElement('i'); - poolIconNode.classList.add('fa'); - poolIconNode.classList.add('fa-pool'); - poolLinkNode.appendChild(poolIconNode); + const poolIconNode = document.createElement('i'); + poolIconNode.classList.add('fa'); + poolIconNode.classList.add('fa-pool'); + poolLinkNode.appendChild(poolIconNode); - const searchLinkNode = document.createElement('a'); - if (className) { - searchLinkNode.classList.add(className); - } - searchLinkNode.setAttribute( - 'href', uri.formatClientLink( - 'posts', {query: "pool:" + pool.id})); - searchLinkNode.textContent = pool.names[0] + ' '; - - const usagesNode = document.createElement('span'); - usagesNode.classList.add('pool-usages'); - usagesNode.setAttribute('data-pseudo-content', pool.postCount); - - const removalLinkNode = document.createElement('a'); - removalLinkNode.classList.add('remove-pool'); - removalLinkNode.setAttribute('href', ''); - removalLinkNode.setAttribute('data-pseudo-content', '×'); - removalLinkNode.addEventListener('click', e => { - e.preventDefault(); - this.deletePool(pool); - }); - - const listItemNode = document.createElement('li'); - listItemNode.appendChild(removalLinkNode); - listItemNode.appendChild(poolLinkNode); - listItemNode.appendChild(searchLinkNode); - listItemNode.appendChild(usagesNode); - for (let name of pool.names) { - this._poolToListItemNode.set(name, listItemNode); + const searchLinkNode = document.createElement('a'); + if (className) { + searchLinkNode.classList.add(className); + } + searchLinkNode.setAttribute( + 'href', uri.formatClientLink( + 'posts', {query: "pool:" + pool.id})); + searchLinkNode.textContent = pool.names[0] + ' '; + + const usagesNode = document.createElement('span'); + usagesNode.classList.add('pool-usages'); + usagesNode.setAttribute('data-pseudo-content', pool.postCount); + + const removalLinkNode = document.createElement('a'); + removalLinkNode.classList.add('remove-pool'); + removalLinkNode.setAttribute('href', ''); + removalLinkNode.setAttribute('data-pseudo-content', '×'); + removalLinkNode.addEventListener('click', e => { + e.preventDefault(); + this.deletePool(pool); + }); + + const listItemNode = document.createElement('li'); + listItemNode.appendChild(removalLinkNode); + listItemNode.appendChild(poolLinkNode); + listItemNode.appendChild(searchLinkNode); + listItemNode.appendChild(usagesNode); + for (let name of pool.names) { + this._poolToListItemNode.set(name, listItemNode); + } + return listItemNode; } - return listItemNode; - } - _deleteListItemNode(pool) { - const listItemNode = this._getListItemNode(pool); - if (listItemNode) { - listItemNode.parentNode.removeChild(listItemNode); - } - for (let name of pool.names) { - this._poolToListItemNode.delete(name); + _deleteListItemNode(pool) { + const listItemNode = this._getListItemNode(pool); + if (listItemNode) { + listItemNode.parentNode.removeChild(listItemNode); + } + for (let name of pool.names) { + this._poolToListItemNode.delete(name); + } } - } - _getListItemNode(pool) { - return this._poolToListItemNode.get(pool.names[0]); - } + _getListItemNode(pool) { + return this._poolToListItemNode.get(pool.names[0]); + } - _hideAutoComplete() { - this._autoCompleteControl.hide(); - } + _hideAutoComplete() { + this._autoCompleteControl.hide(); + } } module.exports = PoolInputControl; diff --git a/client/js/models/pool_category.js b/client/js/models/pool_category.js index 4cdb68e..a77302c 100644 --- a/client/js/models/pool_category.js +++ b/client/js/models/pool_category.js @@ -17,7 +17,7 @@ class PoolCategory extends events.EventTarget { get name() { return this._name; } get color() { return this._color; } - get poolCount() { return this._poolCount; } + get poolCount() { return this._poolCount; } get isDefault() { return this._isDefault; } get isTransient() { return !this._origName; } diff --git a/server/alembic.ini b/server/alembic.ini index 98ab83d..6a4e5ff 100644 --- a/server/alembic.ini +++ b/server/alembic.ini @@ -3,7 +3,6 @@ script_location = szurubooru/migrations # overriden by szurubooru's config sqlalchemy.url = -revision_environment = true [loggers] keys = root,sqlalchemy,alembic diff --git a/server/config.yaml.dist b/server/config.yaml.dist index baf3a34..37e877f 100644 --- a/server/config.yaml.dist +++ b/server/config.yaml.dist @@ -4,7 +4,7 @@ # shown in the website title and on the front page name: szurubooru # full url to the homepage of this szurubooru site, with no trailing slash -domain: localhost # example: http://example.com +domain: # example: http://example.com # used to salt the users' password hashes and generate filenames for static content secret: change @@ -49,9 +49,6 @@ enable_safety: yes tag_name_regex: ^\S+$ tag_category_name_regex: ^[^\s%+#/]+$ -pool_name_regex: ^\S+$ -pool_category_name_regex: ^[^\s%+#/]+$ - # don't make these more restrictive unless you want to annoy people; if you do # customize them, make sure to update the instructions in the registration form # template as well. @@ -61,72 +58,72 @@ user_name_regex: '^[a-zA-Z0-9_-]{1,32}$' default_rank: regular privileges: - 'users:create:self': anonymous # Registration permission - 'users:create:any': administrator - 'users:list': regular - 'users:view': regular - 'users:edit:any:name': moderator - 'users:edit:any:pass': moderator - 'users:edit:any:email': moderator - 'users:edit:any:avatar': moderator - 'users:edit:any:rank': moderator - 'users:edit:self:name': regular - 'users:edit:self:pass': regular - 'users:edit:self:email': regular - 'users:edit:self:avatar': regular - 'users:edit:self:rank': moderator # one can't promote themselves or anyone to upper rank than their own. - 'users:delete:any': administrator - 'users:delete:self': regular - - 'user_tokens:list:any': administrator - 'user_tokens:list:self': regular - 'user_tokens:create:any': administrator - 'user_tokens:create:self': regular - 'user_tokens:edit:any': administrator - 'user_tokens:edit:self': regular - 'user_tokens:delete:any': administrator - 'user_tokens:delete:self': regular - - 'posts:create:anonymous': regular - 'posts:create:identified': regular - 'posts:list': anonymous - 'posts:reverse_search': regular - 'posts:view': anonymous - 'posts:view:featured': anonymous - 'posts:edit:content': power - 'posts:edit:flags': regular - 'posts:edit:notes': regular - 'posts:edit:relations': regular - 'posts:edit:safety': power - 'posts:edit:source': regular - 'posts:edit:tags': regular - 'posts:edit:thumbnail': power - 'posts:feature': moderator - 'posts:delete': moderator - 'posts:score': regular - 'posts:merge': moderator - 'posts:favorite': regular - 'posts:bulk-edit:tags': power - 'posts:bulk-edit:safety': power - - 'tags:create': regular - 'tags:edit:names': power - 'tags:edit:category': power - 'tags:edit:description': power - 'tags:edit:implications': power - 'tags:edit:suggestions': power - 'tags:list': regular - 'tags:view': anonymous - 'tags:merge': moderator - 'tags:delete': moderator - - 'tag_categories:create': moderator - 'tag_categories:edit:name': moderator - 'tag_categories:edit:color': moderator - 'tag_categories:list': anonymous - 'tag_categories:view': anonymous - 'tag_categories:delete': moderator - 'tag_categories:set_default': moderator + 'users:create:self': anonymous # Registration permission + 'users:create:any': administrator + 'users:list': regular + 'users:view': regular + 'users:edit:any:name': moderator + 'users:edit:any:pass': moderator + 'users:edit:any:email': moderator + 'users:edit:any:avatar': moderator + 'users:edit:any:rank': moderator + 'users:edit:self:name': regular + 'users:edit:self:pass': regular + 'users:edit:self:email': regular + 'users:edit:self:avatar': regular + 'users:edit:self:rank': moderator # one can't promote themselves or anyone to upper rank than their own. + 'users:delete:any': administrator + 'users:delete:self': regular + + 'user_tokens:list:any': administrator + 'user_tokens:list:self': regular + 'user_tokens:create:any': administrator + 'user_tokens:create:self': regular + 'user_tokens:edit:any': administrator + 'user_tokens:edit:self': regular + 'user_tokens:delete:any': administrator + 'user_tokens:delete:self': regular + + 'posts:create:anonymous': regular + 'posts:create:identified': regular + 'posts:list': anonymous + 'posts:reverse_search': regular + 'posts:view': anonymous + 'posts:view:featured': anonymous + 'posts:edit:content': power + 'posts:edit:flags': regular + 'posts:edit:notes': regular + 'posts:edit:relations': regular + 'posts:edit:safety': power + 'posts:edit:source': regular + 'posts:edit:tags': regular + 'posts:edit:thumbnail': power + 'posts:feature': moderator + 'posts:delete': moderator + 'posts:score': regular + 'posts:merge': moderator + 'posts:favorite': regular + 'posts:bulk-edit:tags': power + 'posts:bulk-edit:safety': power + + 'tags:create': regular + 'tags:edit:names': power + 'tags:edit:category': power + 'tags:edit:description': power + 'tags:edit:implications': power + 'tags:edit:suggestions': power + 'tags:list': regular + 'tags:view': anonymous + 'tags:merge': moderator + 'tags:delete': moderator + + 'tag_categories:create': moderator + 'tag_categories:edit:name': moderator + 'tag_categories:edit:color': moderator + 'tag_categories:list': anonymous + 'tag_categories:view': anonymous + 'tag_categories:delete': moderator + 'tag_categories:set_default': moderator 'pools:create': regular 'pools:edit:names': power @@ -146,19 +143,19 @@ privileges: 'pool_categories:delete': moderator 'pool_categories:set_default': moderator - 'comments:create': regular - 'comments:delete:any': moderator - 'comments:delete:own': regular - 'comments:edit:any': moderator - 'comments:edit:own': regular - 'comments:list': regular - 'comments:view': regular - 'comments:score': regular + 'comments:create': regular + 'comments:delete:any': moderator + 'comments:delete:own': regular + 'comments:edit:any': moderator + 'comments:edit:own': regular + 'comments:list': regular + 'comments:view': regular + 'comments:score': regular - 'snapshots:list': power + 'snapshots:list': power - 'uploads:create': regular - 'uploads:use_downloader': power + 'uploads:create': regular + 'uploads:use_downloader': power ## ONLY SET THESE IF DEPLOYING OUTSIDE OF DOCKER #debug: 0 # generate server logs? diff --git a/server/requirements.txt b/server/requirements.txt index 8a337f9..810452e 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -9,4 +9,3 @@ pillow>=4.3.0 pynacl>=1.2.1 pytz>=2018.3 pyRFC3339>=1.0 -youtube_dl>=2020.5.3 diff --git a/server/szurubooru/db.py b/server/szurubooru/db.py index bd30042..561b748 100644 --- a/server/szurubooru/db.py +++ b/server/szurubooru/db.py @@ -34,22 +34,3 @@ def _bump_query_count() -> None: sa.event.listen(_engine, 'after_execute', lambda *args: _bump_query_count()) - -import time -import logging - -logger = logging.getLogger("myapp.sqltime") -logger.setLevel(logging.INFO) - -def before_cursor_execute(conn, cursor, statement, - parameters, context, executemany): - conn.info.setdefault('query_start_time', []).append(time.time()) - logger.info("Start Query: %s" % statement) - -def after_cursor_execute(conn, cursor, statement, - parameters, context, executemany): - total = time.time() - conn.info['query_start_time'].pop(-1) - logger.info("Total Time: %f" % total) - -sa.event.listen(_engine, "before_cursor_execute", before_cursor_execute) -sa.event.listen(_engine, "after_cursor_execute", after_cursor_execute) diff --git a/server/szurubooru/func/pool_categories.py b/server/szurubooru/func/pool_categories.py index eca934a..83305de 100644 --- a/server/szurubooru/func/pool_categories.py +++ b/server/szurubooru/func/pool_categories.py @@ -195,5 +195,5 @@ def delete_category(category: model.PoolCategory) -> None: if (category.pool_count or 0) > 0: raise PoolCategoryIsInUseError( 'Pool category has some usages and cannot be deleted. ' + - 'Please remove this category from relevant pools first..') + 'Please remove this category from relevant pools first.') db.session.delete(category) diff --git a/server/szurubooru/func/pools.py b/server/szurubooru/func/pools.py index 1d27929..90dede2 100644 --- a/server/szurubooru/func/pools.py +++ b/server/szurubooru/func/pools.py @@ -77,6 +77,7 @@ def _duplicates(a: List[int]) -> List[int]: dupes.append(x) return dupes + def sort_pools(pools: List[model.Pool]) -> List[model.Pool]: default_category_name = pool_categories.get_default_category_name() return sorted( @@ -131,8 +132,7 @@ class PoolSerializer(serialization.BaseSerializer): def serialize_posts(self) -> Any: return [post for post in - [posts.serialize_micro_post(rel, None) - for rel in self.pool.posts]] + [posts.serialize_micro_post(rel, None) for rel in self.pool.posts]] def serialize_pool( @@ -221,7 +221,7 @@ def merge_pools(source_pool: model.Pool, target_pool: model.Pool) -> None: if source_pool.pool_id == target_pool.pool_id: raise InvalidPoolRelationError('Cannot merge pool with itself.') - def merge_posts(source_pool_id: int, target_pool_id: int) -> None: + def merge_pool_posts(source_pool_id: int, target_pool_id: int) -> None: alias1 = model.PoolPost alias2 = sa.orm.util.aliased(model.PoolPost) update_stmt = ( @@ -236,7 +236,7 @@ def merge_pools(source_pool: model.Pool, target_pool: model.Pool) -> None: update_stmt = update_stmt.values(pool_id=target_pool_id) db.session.execute(update_stmt) - merge_posts(source_pool.pool_id, target_pool.pool_id) + merge_pool_posts(source_pool.pool_id, target_pool.pool_id) delete(source_pool) @@ -304,8 +304,6 @@ def update_pool_posts(pool: model.Pool, post_ids: List[int]) -> None: assert pool dupes = _duplicates(post_ids) if len(dupes) > 0: - print(str(dupes)) - print(str(post_ids)) dupes = ', '.join(list(str(x) for x in dupes)) raise InvalidPoolDuplicateError('Duplicate post(s) in pool: ' + dupes) ret = posts.get_posts_by_ids(post_ids) diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index 723a466..62458d8 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -300,7 +300,7 @@ class PostSerializer(serialization.BaseSerializer): self.post.comments, key=lambda comment: comment.creation_time)] - def serialize_pools(self) -> Any: + def serialize_pools(self) -> List[Any]: return [ pools.serialize_pool(pool) for pool in sorted( @@ -343,7 +343,7 @@ def get_post_by_id(post_id: int) -> model.Post: return post -def get_posts_by_ids(ids: List[int]) -> List[model.Pool]: +def get_posts_by_ids(ids: List[int]) -> List[model.Post]: if len(ids) == 0: return [] posts = ( -- cgit v1.3