From 0839dafd34bc96d9687a5e955153f66e2c464c23 Mon Sep 17 00:00:00 2001 From: neobooru Date: Wed, 24 Jul 2019 16:42:37 +0200 Subject: client/auth: call tags.refreshCategoryColorMap() after login When the tag category list permission is not anonymous the category colors fail to load if you are not logged in, and because the page doesn't reload (SPA) the tag colors are still broken after logging in. Manually calling refreshCategoryColorMap after logging in solves this issue.--- client/js/controllers/auth_controller.js | 3 +++ 1 file changed, 3 insertions(+) (limited to 'client/js/controllers/auth_controller.js') diff --git a/client/js/controllers/auth_controller.js b/client/js/controllers/auth_controller.js index 2838e53..bec4b7a 100644 --- a/client/js/controllers/auth_controller.js +++ b/client/js/controllers/auth_controller.js @@ -2,6 +2,7 @@ const router = require('../router.js'); const api = require('../api.js'); +const tags = require('../tags.js'); const uri = require('../util/uri.js'); const topNavigation = require('../models/top_navigation.js'); const LoginView = require('../views/login_view.js'); @@ -24,6 +25,8 @@ class LoginController { .then(() => { const ctx = router.show(uri.formatClientLink()); ctx.controller.showSuccess('Logged in'); + // reload tag category color map, this is required when `tag_categories:list` has a permission other than anonymous + tags.refreshCategoryColorMap(); }, error => { this._loginView.showError(error.message); this._loginView.enableForm(); -- cgit v1.3 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/css/pool-categories-view.styl | 30 ++ client/css/pool-input-control.styl | 53 ++++ client/css/pool-list-view.styl | 52 ++++ client/css/pool-view.styl | 33 +++ client/html/help_search.tpl | 1 + client/html/help_search_pools.tpl | 97 +++++++ client/html/pool.tpl | 18 ++ client/html/pool_categories.tpl | 30 ++ client/html/pool_category_row.tpl | 43 +++ client/html/pool_create.tpl | 35 +++ client/html/pool_delete.tpl | 21 ++ client/html/pool_edit.tpl | 41 +++ client/html/pool_input.tpl | 0 client/html/pool_merge.tpl | 22 ++ client/html/pool_summary.tpl | 23 ++ client/html/pools_header.tpl | 22 ++ client/html/pools_page.tpl | 48 ++++ client/html/tags_page.tpl | 10 +- client/js/api.js | 4 + client/js/controllers/auth_controller.js | 2 + .../js/controllers/pool_categories_controller.js | 57 ++++ client/js/controllers/pool_controller.js | 141 ++++++++++ client/js/controllers/pool_create_controller.js | 63 +++++ client/js/controllers/pool_list_controller.js | 105 +++++++ client/js/controls/pool_auto_complete_control.js | 57 ++++ client/js/main.js | 6 + client/js/models/pool.js | 155 +++++++++++ client/js/models/pool_category.js | 90 ++++++ client/js/models/pool_category_list.js | 82 ++++++ client/js/models/pool_list.js | 30 ++ client/js/models/top_navigation.js | 1 + client/js/pools.js | 26 ++ client/js/util/misc.js | 5 + client/js/util/views.js | 25 ++ client/js/views/help_view.js | 1 + client/js/views/pool_categories_view.js | 166 +++++++++++ client/js/views/pool_create_view.js | 108 ++++++++ client/js/views/pool_delete_view.js | 53 ++++ client/js/views/pool_edit_view.js | 115 ++++++++ client/js/views/pool_merge_view.js | 77 ++++++ client/js/views/pool_summary_view.js | 23 ++ client/js/views/pool_view.js | 106 ++++++++ client/js/views/pools_header_view.js | 52 ++++ client/js/views/pools_page_view.js | 13 + client/package.json | 2 +- server/alembic.ini | 1 + server/config.yaml.dist | 179 ++++++------ server/requirements.txt | 1 + server/szurubooru/api/__init__.py | 2 + server/szurubooru/api/pool_api.py | 127 +++++++++ server/szurubooru/api/pool_category_api.py | 89 ++++++ server/szurubooru/func/pool_categories.py | 199 ++++++++++++++ server/szurubooru/func/pools.py | 302 +++++++++++++++++++++ server/szurubooru/func/snapshots.py | 20 ++ server/szurubooru/func/tags.py | 2 - .../54de8acc6cef_add_default_pool_category.py | 60 ++++ .../versions/6a2f424ec9d2_create_pool_tables.py | 52 ++++ server/szurubooru/model/__init__.py | 2 + server/szurubooru/model/pool.py | 70 +++++ server/szurubooru/model/pool_category.py | 28 ++ server/szurubooru/model/util.py | 2 + server/szurubooru/search/configs/__init__.py | 1 + .../search/configs/pool_search_config.py | 109 ++++++++ 63 files changed, 3301 insertions(+), 89 deletions(-) create mode 100644 client/css/pool-categories-view.styl create mode 100644 client/css/pool-input-control.styl create mode 100644 client/css/pool-list-view.styl create mode 100644 client/css/pool-view.styl create mode 100644 client/html/help_search_pools.tpl create mode 100644 client/html/pool.tpl create mode 100644 client/html/pool_categories.tpl create mode 100644 client/html/pool_category_row.tpl create mode 100644 client/html/pool_create.tpl create mode 100644 client/html/pool_delete.tpl create mode 100644 client/html/pool_edit.tpl create mode 100644 client/html/pool_input.tpl create mode 100644 client/html/pool_merge.tpl create mode 100644 client/html/pool_summary.tpl create mode 100644 client/html/pools_header.tpl create mode 100644 client/html/pools_page.tpl create mode 100644 client/js/controllers/pool_categories_controller.js create mode 100644 client/js/controllers/pool_controller.js create mode 100644 client/js/controllers/pool_create_controller.js create mode 100644 client/js/controllers/pool_list_controller.js create mode 100644 client/js/controls/pool_auto_complete_control.js create mode 100644 client/js/models/pool.js create mode 100644 client/js/models/pool_category.js create mode 100644 client/js/models/pool_category_list.js create mode 100644 client/js/models/pool_list.js create mode 100644 client/js/pools.js create mode 100644 client/js/views/pool_categories_view.js create mode 100644 client/js/views/pool_create_view.js create mode 100644 client/js/views/pool_delete_view.js create mode 100644 client/js/views/pool_edit_view.js create mode 100644 client/js/views/pool_merge_view.js create mode 100644 client/js/views/pool_summary_view.js create mode 100644 client/js/views/pool_view.js create mode 100644 client/js/views/pools_header_view.js create mode 100644 client/js/views/pools_page_view.js create mode 100644 server/szurubooru/api/pool_api.py create mode 100644 server/szurubooru/api/pool_category_api.py create mode 100644 server/szurubooru/func/pool_categories.py create mode 100644 server/szurubooru/func/pools.py create mode 100644 server/szurubooru/migrations/versions/54de8acc6cef_add_default_pool_category.py create mode 100644 server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py create mode 100644 server/szurubooru/model/pool.py create mode 100644 server/szurubooru/model/pool_category.py create mode 100644 server/szurubooru/search/configs/pool_search_config.py (limited to 'client/js/controllers/auth_controller.js') diff --git a/client/css/pool-categories-view.styl b/client/css/pool-categories-view.styl new file mode 100644 index 0000000..3e87635 --- /dev/null +++ b/client/css/pool-categories-view.styl @@ -0,0 +1,30 @@ +@import colors + +.content-wrapper.pool-categories + width: 100% + max-width: 45em + table + border-spacing: 0 + width: 100% + tr.default td + background: $default-pool-category-background-color + td, th + padding: .4em + &.color + input[type=text] + width: 8em + &.usages + text-align: center + &.remove, &.set-default + white-space: pre + th + white-space: nowrap + &:first-child + padding-left: 0 + &:last-child + padding-right: 0 + tfoot + display: none + form + width: auto + diff --git a/client/css/pool-input-control.styl b/client/css/pool-input-control.styl new file mode 100644 index 0000000..3f71abe --- /dev/null +++ b/client/css/pool-input-control.styl @@ -0,0 +1,53 @@ +@import colors + +div.pool-input + position: relative + + .main-control + display: flex + input + flex: 5 + button + flex: 1 + margin: 0 0 0 0.5em + + +ul.compact-pools + width: 100% + margin: 0.5em 0 0 0 + padding: 0 + li + margin: 0 + width: 100% + line-height: 140% + white-space: nowrap + overflow: hidden + text-overflow: ellipsis + transition: background-color 0.5s linear + a + display: inline + a:focus + outline: 0 + box-shadow: inset 0 0 0 2px $main-color + &.implication + background: $implied-pool-background-color + color: $implied-pool-text-color + &.new + background: $new-pool-background-color + color: $new-pool-text-color + &.duplicate + background: $duplicate-pool-background-color + color: $duplicate-pool-text-color + i + padding-right: 0.4em + +div.pool-input, ul.compact-pools + .pool-usages, .pool-weight, .remove-pool + color: $inactive-link-color + unselectable() + .pool-usages, .pool-weight + font-size: 90% + .pool-usages, .pool-weight + margin-left: 0.7em + .remove-pool + margin-right: 0.5em diff --git a/client/css/pool-list-view.styl b/client/css/pool-list-view.styl new file mode 100644 index 0000000..2333a37 --- /dev/null +++ b/client/css/pool-list-view.styl @@ -0,0 +1,52 @@ +@import colors + +.pool-list + table + width: 100% + border-spacing: 0 + text-align: left + line-height: 1.3em + tr:hover td + background: $top-navigation-color + th, td + padding: 0.1em 0.5em + th + white-space: nowrap + background: $top-navigation-color + .names + width: 28% + .usages + text-align: center + width: 8% + .creation-time + text-align: center + width: 8% + white-space: pre + ul + list-style-type: none + margin: 0 + padding: 0 + display: inline + li + padding: 0 + display: inline + &:not(:last-child):after + content: ', ' + @media (max-width: 800px) + .implications, .suggestions + display: none + +.pool-list-header + label + display: none !important + text-align: left + form + width: auto + input[name=search-text] + width: 25em + @media (max-width: 1000px) + width: 100% + .append + vertical-align: middle + font-size: 0.95em + color: $inactive-link-color diff --git a/client/css/pool-view.styl b/client/css/pool-view.styl new file mode 100644 index 0000000..bf16b79 --- /dev/null +++ b/client/css/pool-view.styl @@ -0,0 +1,33 @@ +#pool + width: 100% + max-width: 40em + h1 + word-break: break-all + line-height: 130% + margin-top: 0 + form + width: 100% + .pool-edit + textarea + height: 10em + .pool-summary + section + &.description + margin: 1.5em 0 0 0 + &.details + vertical-align: top + padding-right: 0.5em + ul + margin: 0 + padding: 0 + list-style-type: none + li + display: inline + margin: 0 + padding: 0 + li:not(:last-of-type):after + content: ', ' + ul:empty:after + content: '(none)' + section + margin-bottom: 1em diff --git a/client/html/help_search.tpl b/client/html/help_search.tpl index 7073789..d8708b0 100644 --- a/client/html/help_search.tpl +++ b/client/html/help_search.tpl @@ -4,6 +4,7 @@ -->
  • '>Posts
  • '>Users
  • '>Tags
  • '>Pools
  • diff --git a/client/html/help_search_pools.tpl b/client/html/help_search_pools.tpl new file mode 100644 index 0000000..a1f0c80 --- /dev/null +++ b/client/html/help_search_pools.tpl @@ -0,0 +1,97 @@ +

    Anonymous tokens

    + +

    Same as name token.

    + +

    Named tokens

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    namehaving given name (accepts wildcards)
    categoryhaving given category (accepts wildcards)
    creation-datecreated at given date
    creation-timealias of creation-date
    last-edit-dateedited at given date
    last-edit-timealias of last-edit-date
    edit-datealias of last-edit-date
    edit-timealias of last-edit-date
    post-countalias of usages
    + +

    Sort style tokens

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    randomas random as it can get
    nameA to Z
    categorycategory (A to Z)
    creation-daterecently created first
    creation-timealias of creation-date
    last-edit-daterecently edited first
    last-edit-timealias of creation-time
    edit-datealias of creation-time
    edit-timealias of creation-time
    post-countnumber of posts
    + +

    Special tokens

    + +

    None.

    diff --git a/client/html/pool.tpl b/client/html/pool.tpl new file mode 100644 index 0000000..5af10c9 --- /dev/null +++ b/client/html/pool.tpl @@ -0,0 +1,18 @@ +
    +

    <%- ctx.pool.first_name %>

    + +
    +
    diff --git a/client/html/pool_categories.tpl b/client/html/pool_categories.tpl new file mode 100644 index 0000000..b045038 --- /dev/null +++ b/client/html/pool_categories.tpl @@ -0,0 +1,30 @@ +
    +
    +

    Pool categories

    +
    + + + + + + + + + + +
    Category nameCSS colorUsages
    +
    + + <% if (ctx.canCreate) { %> +

    Add new category

    + <% } %> + +
    + + <% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canDelete) { %> +
    + +
    + <% } %> +
    +
    diff --git a/client/html/pool_category_row.tpl b/client/html/pool_category_row.tpl new file mode 100644 index 0000000..5ff4456 --- /dev/null +++ b/client/html/pool_category_row.tpl @@ -0,0 +1,43 @@ +<% if (ctx.poolCategory.isDefault) { %><% + %><% +%><% } else { %><% + %><% +%><% } %> + + <% if (ctx.canEditName) { %> + <%= ctx.makeTextInput({value: ctx.poolCategory.name, required: true}) %> + <% } else { %> + <%- ctx.poolCategory.name %> + <% } %> + + + <% if (ctx.canEditColor) { %> + <%= ctx.makeColorInput({value: ctx.poolCategory.color}) %> + <% } else { %> + <%- ctx.poolCategory.color %> + <% } %> + + + <% if (ctx.poolCategory.name) { %> + '> + <%- ctx.poolCategory.poolCount %> + + <% } else { %> + <%- ctx.poolCategory.poolCount %> + <% } %> + + <% if (ctx.canDelete) { %> + + <% if (ctx.poolCategory.poolCount) { %> + Remove + <% } else { %> + Remove + <% } %> + + <% } %> + <% if (ctx.canSetDefault) { %> + + Make default + + <% } %> + diff --git a/client/html/pool_create.tpl b/client/html/pool_create.tpl new file mode 100644 index 0000000..fec95ef --- /dev/null +++ b/client/html/pool_create.tpl @@ -0,0 +1,35 @@ +
    +
    + + + <% if (ctx.canCreate) { %> +
    + +
    + +
    + <% } %> +
    +
    diff --git a/client/html/pool_delete.tpl b/client/html/pool_delete.tpl new file mode 100644 index 0000000..1ef7fb5 --- /dev/null +++ b/client/html/pool_delete.tpl @@ -0,0 +1,21 @@ +
    +
    +

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

    + + + +
    + +
    + +
    +
    +
    diff --git a/client/html/pool_edit.tpl b/client/html/pool_edit.tpl new file mode 100644 index 0000000..0a0e1b0 --- /dev/null +++ b/client/html/pool_edit.tpl @@ -0,0 +1,41 @@ +
    +
    + + + <% if (ctx.canEditAnything) { %> +
    + +
    + +
    + <% } %> +
    +
    diff --git a/client/html/pool_input.tpl b/client/html/pool_input.tpl new file mode 100644 index 0000000..e69de29 diff --git a/client/html/pool_merge.tpl b/client/html/pool_merge.tpl new file mode 100644 index 0000000..ce0bf92 --- /dev/null +++ b/client/html/pool_merge.tpl @@ -0,0 +1,22 @@ +
    +
    + + +
    + +
    + +
    +
    +
    diff --git a/client/html/pool_summary.tpl b/client/html/pool_summary.tpl new file mode 100644 index 0000000..7b7c763 --- /dev/null +++ b/client/html/pool_summary.tpl @@ -0,0 +1,23 @@ +
    +
    +
    + Category: + '><%- ctx.pool.category %> +
    + +
    + Aliases:
    +
      <% for (let name of ctx.pool.names.slice(1)) { %>
    • <%= ctx.makePoolLink(ctx.pool, false, false, name) %>
    • <% } %>
    +
    +
    + +
    +
    + <%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %> +

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

    +
    +
    diff --git a/client/html/pools_header.tpl b/client/html/pools_header.tpl new file mode 100644 index 0000000..c54959f --- /dev/null +++ b/client/html/pools_header.tpl @@ -0,0 +1,22 @@ +
    +
    + + +
    + + '>Syntax help + + <% if (ctx.canCreate) { %> + '>Add new pool + <% } %> + + <% if (ctx.canEditPoolCategories) { %> + '>Pool categories + <% } %> +
    +
    +
    diff --git a/client/html/pools_page.tpl b/client/html/pools_page.tpl new file mode 100644 index 0000000..19f3f2a --- /dev/null +++ b/client/html/pools_page.tpl @@ -0,0 +1,48 @@ +
    + <% if (ctx.response.results.length) { %> + + + + + + + + <% for (let pool of ctx.response.results) { %> + + + + + + <% } %> + +
    + <% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.query) { %> + '>Pool name(s) + <% } else { %> + '>Pool name(s) + <% } %> + + <% if (ctx.parameters.query == 'sort:post-count') { %> + '>Post Count + <% } else { %> + '>Post Count + <% } %> + + <% if (ctx.parameters.query == 'sort:creation-time') { %> + '>Created on + <% } else { %> + '>Created on + <% } %> +
    +
      + <% for (let name of pool.names) { %> +
    • <%= ctx.makePoolLink(pool, false, false, name) %>
    • + <% } %> +
    +
    + '><%- pool.postCount %> + + <%= ctx.makeRelativeTime(pool.creationTime) %> +
    + <% } %> +
    diff --git a/client/html/tags_page.tpl b/client/html/tags_page.tpl index 8c97398..2e08239 100644 --- a/client/html/tags_page.tpl +++ b/client/html/tags_page.tpl @@ -3,35 +3,35 @@
    - <% if (ctx.query == 'sort:name' || !ctx.query) { %> + <% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.query) { %> '>Tag name(s) <% } else { %> '>Tag name(s) <% } %> - <% if (ctx.query == 'sort:implication-count') { %> + <% if (ctx.parameters.query == 'sort:implication-count') { %> '>Implications <% } else { %> '>Implications <% } %> - <% if (ctx.query == 'sort:suggestion-count') { %> + <% if (ctx.parameters.query == 'sort:suggestion-count') { %> '>Suggestions <% } else { %> '>Suggestions <% } %> - <% if (ctx.query == 'sort:usages') { %> + <% if (ctx.parameters.query == 'sort:usages') { %> '>Usages <% } else { %> '>Usages <% } %> - <% if (ctx.query == 'sort:creation-time') { %> + <% if (ctx.parameters.query == 'sort:creation-time') { %> '>Created on <% } else { %> '>Created on diff --git a/client/js/api.js b/client/js/api.js index 07ec5ec..14c5bcc 100644 --- a/client/js/api.js +++ b/client/js/api.js @@ -84,6 +84,10 @@ class Api extends events.EventTarget { return remoteConfig.tagNameRegex; } + getPoolNameRegex() { + return remoteConfig.poolNameRegex; + } + getPasswordRegex() { return remoteConfig.passwordRegex; } diff --git a/client/js/controllers/auth_controller.js b/client/js/controllers/auth_controller.js index bec4b7a..a6ec97d 100644 --- a/client/js/controllers/auth_controller.js +++ b/client/js/controllers/auth_controller.js @@ -3,6 +3,7 @@ const router = require('../router.js'); const api = require('../api.js'); const tags = require('../tags.js'); +const pools = require('../pools.js'); const uri = require('../util/uri.js'); const topNavigation = require('../models/top_navigation.js'); const LoginView = require('../views/login_view.js'); @@ -27,6 +28,7 @@ class LoginController { ctx.controller.showSuccess('Logged in'); // reload tag category color map, this is required when `tag_categories:list` has a permission other than anonymous tags.refreshCategoryColorMap(); + pools.refreshCategoryColorMap(); }, error => { this._loginView.showError(error.message); this._loginView.enableForm(); diff --git a/client/js/controllers/pool_categories_controller.js b/client/js/controllers/pool_categories_controller.js new file mode 100644 index 0000000..4c2c3b7 --- /dev/null +++ b/client/js/controllers/pool_categories_controller.js @@ -0,0 +1,57 @@ +'use strict'; + +const api = require('../api.js'); +const pools = require('../pools.js'); +const PoolCategoryList = require('../models/pool_category_list.js'); +const topNavigation = require('../models/top_navigation.js'); +const PoolCategoriesView = require('../views/pool_categories_view.js'); +const EmptyView = require('../views/empty_view.js'); + +class PoolCategoriesController { + constructor() { + if (!api.hasPrivilege('poolCategories:list')) { + this._view = new EmptyView(); + this._view.showError( + 'You don\'t have privileges to view pool categories.'); + return; + } + + topNavigation.activate('pools'); + topNavigation.setTitle('Listing pools'); + PoolCategoryList.get().then(response => { + this._poolCategories = response.results; + this._view = new PoolCategoriesView({ + poolCategories: this._poolCategories, + canEditName: api.hasPrivilege('poolCategories:edit:name'), + canEditColor: api.hasPrivilege('poolCategories:edit:color'), + canDelete: api.hasPrivilege('poolCategories:delete'), + canCreate: api.hasPrivilege('poolCategories:create'), + canSetDefault: api.hasPrivilege('poolCategories:setDefault'), + }); + this._view.addEventListener('submit', e => this._evtSubmit(e)); + }, error => { + this._view = new EmptyView(); + this._view.showError(error.message); + }); + } + + _evtSubmit(e) { + this._view.clearMessages(); + this._view.disableForm(); + this._poolCategories.save() + .then(() => { + pools.refreshCategoryColorMap(); + this._view.enableForm(); + this._view.showSuccess('Changes saved.'); + }, error => { + this._view.enableForm(); + this._view.showError(error.message); + }); + } +} + +module.exports = router => { + router.enter(['pool-categories'], (ctx, next) => { + ctx.controller = new PoolCategoriesController(ctx, next); + }); +}; 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'); + }); +}; diff --git a/client/js/controllers/pool_create_controller.js b/client/js/controllers/pool_create_controller.js new file mode 100644 index 0000000..0aa3c76 --- /dev/null +++ b/client/js/controllers/pool_create_controller.js @@ -0,0 +1,63 @@ +'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 PoolCategoryList = require('../models/pool_category_list.js'); +const PoolCreateView = require('../views/pool_create_view.js'); +const EmptyView = require('../views/empty_view.js'); + +class PoolCreateController { + constructor(ctx) { + if (!api.hasPrivilege('pools:create')) { + this._view = new EmptyView(); + this._view.showError('You don\'t have privileges to create pools.'); + return; + } + + PoolCategoryList.get() + .then(poolCategoriesResponse => { + const categories = {}; + for (let category of poolCategoriesResponse.results) { + categories[category.name] = category.name; + } + + this._view = new PoolCreateView({ + canCreate: api.hasPrivilege('pools:create'), + categories: categories, + escapeColons: uri.escapeColons, + }); + + this._view.addEventListener('submit', e => this._evtCreate(e)); + }, error => { + this._view = new EmptyView(); + this._view.showError(error.message); + }); + } + + _evtCreate(e) { + this._view.clearMessages(); + this._view.disableForm(); + e.detail.pool.save() + .then(() => { + this._view.clearMessages(); + misc.disableExitConfirmation(); + const ctx = router.show(uri.formatClientLink('pools')); + ctx.controller.showSuccess('Pool created.'); + }, error => { + this._view.showError(error.message); + this._view.enableForm(); + }); + } + + _evtChange(e) { + misc.enableExitConfirmation(); + } +} + +module.exports = router => { + router.enter(['pool', 'create'], (ctx, next) => { + ctx.controller = new PoolCreateController(ctx, 'create'); + }); +}; diff --git a/client/js/controllers/pool_list_controller.js b/client/js/controllers/pool_list_controller.js new file mode 100644 index 0000000..626dc1e --- /dev/null +++ b/client/js/controllers/pool_list_controller.js @@ -0,0 +1,105 @@ +'use strict'; + +const router = require('../router.js'); +const api = require('../api.js'); +const uri = require('../util/uri.js'); +const PoolList = require('../models/pool_list.js'); +const topNavigation = require('../models/top_navigation.js'); +const PageController = require('../controllers/page_controller.js'); +const PoolsHeaderView = require('../views/pools_header_view.js'); +const PoolsPageView = require('../views/pools_page_view.js'); +const EmptyView = require('../views/empty_view.js'); + +const fields = [ + 'id', + 'names', + /* 'suggestions', + * 'implications', */ + 'creationTime', + 'postCount', + 'category']; + +class PoolListController { + constructor(ctx) { + if (!api.hasPrivilege('pools:list')) { + this._view = new EmptyView(); + this._view.showError('You don\'t have privileges to view pools.'); + return; + } + + 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, + canCreate: api.hasPrivilege('pools:create'), + canEditPoolCategories: api.hasPrivilege('poolCategories:edit'), + }); + this._headerView.addEventListener( + 'submit', e => this._evtSubmit(e), + 'navigate', e => this._evtNavigate(e)); + + this._syncPageController(); + } + + showSuccess(message) { + this._pageController.showSuccess(message); + } + + showError(message) { + this._pageController.showError(message); + } + + _evtSubmit(e) { + this._view.clearMessages(); + this._view.disableForm(); + e.detail.pool.save() + .then(() => { + this._installView(e.detail.pool, 'edit'); + this._view.showSuccess('Pool created.'); + router.replace( + uri.formatClientLink( + 'pool', e.detail.pool.id, 'edit'), + null, false); + }, error => { + this._view.showError(error.message); + this._view.enableForm(); + }); + } + + _evtNavigate(e) { + router.showNoDispatch( + uri.formatClientLink('pools', e.detail.parameters)); + Object.assign(this._ctx.parameters, e.detail.parameters); + this._syncPageController(); + } + + _syncPageController() { + this._pageController.run({ + parameters: this._ctx.parameters, + defaultLimit: 50, + getClientUrlForPage: (offset, limit) => { + const parameters = Object.assign( + {}, this._ctx.parameters, {offset: offset, limit: limit}); + return uri.formatClientLink('pools', parameters); + }, + requestPage: (offset, limit) => { + return PoolList.search( + this._ctx.parameters.query, offset, limit, fields); + }, + pageRenderer: pageCtx => { + return new PoolsPageView(pageCtx); + }, + }); + } +} + +module.exports = router => { + router.enter( + ['pools'], + (ctx, next) => { ctx.controller = new PoolListController(ctx); }); +}; diff --git a/client/js/controls/pool_auto_complete_control.js b/client/js/controls/pool_auto_complete_control.js new file mode 100644 index 0000000..3c4bff7 --- /dev/null +++ b/client/js/controls/pool_auto_complete_control.js @@ -0,0 +1,57 @@ +'use strict'; + +const misc = require('../util/misc.js'); +const PoolList = require('../models/pool_list.js'); +const AutoCompleteControl = require('./auto_complete_control.js'); + +function _poolListToMatches(pools, options) { + return [...pools].sort((pool1, pool2) => { + 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 + ')') + + ''); + return { + caption: caption, + value: pool, + }; + }); +} + +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 = ( + text.length < minLengthForPartialSearch + ? term + '*' + : '*' + term + '*') + ' sort:post-count'; + + return new Promise((resolve, reject) => { + PoolList.search( + query, 0, this._options.maxResults, + ['id', 'names', 'category', 'postCount']) + .then( + response => resolve( + _poolListToMatches(response.results, this._options)), + reject); + }); + }; + + super(input, options); + } +}; + +module.exports = PoolAutoCompleteControl; diff --git a/client/js/main.js b/client/js/main.js index dd974fd..61469a2 100644 --- a/client/js/main.js +++ b/client/js/main.js @@ -27,6 +27,7 @@ router.enter( }); const tags = require('./tags.js'); +const pools = require('./pools.js'); const api = require('./api.js'); api.fetchConfig().then(() => { @@ -45,6 +46,10 @@ api.fetchConfig().then(() => { controllers.push(require('./controllers/tag_controller.js')); controllers.push(require('./controllers/tag_list_controller.js')); controllers.push(require('./controllers/tag_categories_controller.js')); + controllers.push(require('./controllers/pool_create_controller.js')); + controllers.push(require('./controllers/pool_controller.js')); + controllers.push(require('./controllers/pool_list_controller.js')); + controllers.push(require('./controllers/pool_categories_controller.js')); controllers.push(require('./controllers/settings_controller.js')); controllers.push(require('./controllers/user_controller.js')); controllers.push(require('./controllers/user_list_controller.js')); @@ -61,6 +66,7 @@ api.fetchConfig().then(() => { }).then(() => { api.loginFromCookies().then(() => { tags.refreshCategoryColorMap(); + pools.refreshCategoryColorMap(); router.start(); }, error => { if (window.location.href.indexOf('login') !== -1) { diff --git a/client/js/models/pool.js b/client/js/models/pool.js new file mode 100644 index 0000000..e51832b --- /dev/null +++ b/client/js/models/pool.js @@ -0,0 +1,155 @@ +'use strict'; + +const api = require('../api.js'); +const uri = require('../util/uri.js'); +const events = require('../events.js'); +const misc = require('../util/misc.js'); + +class Pool extends events.EventTarget { + constructor() { + // const PoolList = require('./pool_list.js'); + + super(); + this._orig = {}; + + for (let obj of [this, this._orig]) { + // TODO + // obj._suggestions = new PoolList(); + // obj._implications = new PoolList(); + } + + this._updateFromResponse({}); + } + + get id() { return this._id; } + 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 postCount() { return this._postCount; } + get creationTime() { return this._creationTime; } + get lastEditTime() { return this._lastEditTime; } + + set names(value) { this._names = value; } + set category(value) { this._category = value; } + set description(value) { this._description = value; } + + static fromResponse(response) { + const ret = new Pool(); + ret._updateFromResponse(response); + return ret; + } + + static get(id) { + return api.get(uri.formatApiLink('pool', id)) + .then(response => { + return Promise.resolve(Pool.fromResponse(response)); + }); + } + + save() { + const detail = {version: this._version}; + + // send only changed fields to avoid user privilege violation + if (misc.arraysDiffer(this._names, this._orig._names, true)) { + detail.names = this._names; + } + if (this._category !== this._orig._category) { + detail.category = this._category; + } + 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]); + // } + + let promise = this._id ? + api.put(uri.formatApiLink('pool', this._id), detail) : + api.post(uri.formatApiLink('pools'), detail); + return promise + .then(response => { + this._updateFromResponse(response); + this.dispatchEvent(new CustomEvent('change', { + detail: { + pool: this, + }, + })); + return Promise.resolve(); + }); + } + + merge(targetId, addAlias) { + return api.get(uri.formatApiLink('pool', targetId)) + .then(response => { + return api.post(uri.formatApiLink('pool-merge'), { + removeVersion: this._version, + remove: this._id, + mergeToVersion: response.version, + mergeTo: targetId, + }); + }).then(response => { + if (!addAlias) { + return Promise.resolve(response); + } + return api.put(uri.formatApiLink('pool', targetId), { + version: response.version, + names: response.names.concat(this._names), + }); + }).then(response => { + this._updateFromResponse(response); + this.dispatchEvent(new CustomEvent('change', { + detail: { + pool: this, + }, + })); + return Promise.resolve(); + }); + } + + delete() { + return api.delete( + uri.formatApiLink('pool', this._id), + {version: this._version}) + .then(response => { + this.dispatchEvent(new CustomEvent('delete', { + detail: { + pool: this, + }, + })); + return Promise.resolve(); + }); + } + + _updateFromResponse(response) { + const map = { + _id: response.id, + _version: response.version, + _origName: response.names ? response.names[0] : null, + _names: response.names, + _category: response.category, + _description: response.description, + _creationTime: response.creationTime, + _lastEditTime: response.lastEditTime, + _postCount: response.usages || 0, + }; + + for (let obj of [this, this._orig]) { + // TODO + // obj._suggestions.sync(response.suggestions); + // obj._implications.sync(response.implications); + } + + Object.assign(this, map); + Object.assign(this._orig, map); + } +}; + +module.exports = Pool; diff --git a/client/js/models/pool_category.js b/client/js/models/pool_category.js new file mode 100644 index 0000000..4cdb68e --- /dev/null +++ b/client/js/models/pool_category.js @@ -0,0 +1,90 @@ +'use strict'; + +const api = require('../api.js'); +const uri = require('../util/uri.js'); +const events = require('../events.js'); + +class PoolCategory extends events.EventTarget { + constructor() { + super(); + this._name = ''; + this._color = '#000000'; + this._poolCount = 0; + this._isDefault = false; + this._origName = null; + this._origColor = null; + } + + get name() { return this._name; } + get color() { return this._color; } + get poolCount() { return this._poolCount; } + get isDefault() { return this._isDefault; } + get isTransient() { return !this._origName; } + + set name(value) { this._name = value; } + set color(value) { this._color = value; } + + static fromResponse(response) { + const ret = new PoolCategory(); + ret._updateFromResponse(response); + return ret; + } + + save() { + const detail = {version: this._version}; + + if (this.name !== this._origName) { + detail.name = this.name; + } + if (this.color !== this._origColor) { + detail.color = this.color; + } + + if (!Object.keys(detail).length) { + return Promise.resolve(); + } + + let promise = this._origName ? + api.put( + uri.formatApiLink('pool-category', this._origName), + detail) : + api.post(uri.formatApiLink('pool-categories'), detail); + + return promise + .then(response => { + this._updateFromResponse(response); + this.dispatchEvent(new CustomEvent('change', { + detail: { + poolCategory: this, + }, + })); + return Promise.resolve(); + }); + } + + delete() { + return api.delete( + uri.formatApiLink('pool-category', this._origName), + {version: this._version}) + .then(response => { + this.dispatchEvent(new CustomEvent('delete', { + detail: { + poolCategory: this, + }, + })); + return Promise.resolve(); + }); + } + + _updateFromResponse(response) { + this._version = response.version; + this._name = response.name; + this._color = response.color; + this._isDefault = response.default; + this._poolCount = response.usages; + this._origName = this.name; + this._origColor = this.color; + } +} + +module.exports = PoolCategory; diff --git a/client/js/models/pool_category_list.js b/client/js/models/pool_category_list.js new file mode 100644 index 0000000..2699620 --- /dev/null +++ b/client/js/models/pool_category_list.js @@ -0,0 +1,82 @@ +'use strict'; + +const api = require('../api.js'); +const uri = require('../util/uri.js'); +const AbstractList = require('./abstract_list.js'); +const PoolCategory = require('./pool_category.js'); + +class PoolCategoryList extends AbstractList { + constructor() { + super(); + this._defaultCategory = null; + this._origDefaultCategory = null; + this._deletedCategories = []; + this.addEventListener('remove', e => this._evtCategoryDeleted(e)); + } + + static fromResponse(response) { + const ret = super.fromResponse(response); + ret._defaultCategory = null; + for (let poolCategory of ret) { + if (poolCategory.isDefault) { + ret._defaultCategory = poolCategory; + } + } + ret._origDefaultCategory = ret._defaultCategory; + return ret; + } + + static get() { + return api.get(uri.formatApiLink('pool-categories')) + .then(response => { + return Promise.resolve(Object.assign( + {}, + response, + {results: PoolCategoryList.fromResponse(response.results)})); + }); + } + + get defaultCategory() { + return this._defaultCategory; + } + + set defaultCategory(poolCategory) { + this._defaultCategory = poolCategory; + } + + save() { + let promises = []; + for (let poolCategory of this) { + promises.push(poolCategory.save()); + } + for (let poolCategory of this._deletedCategories) { + promises.push(poolCategory.delete()); + } + + if (this._defaultCategory !== this._origDefaultCategory) { + promises.push( + api.put( + uri.formatApiLink( + 'pool-category', + this._defaultCategory.name, + 'default'))); + } + + return Promise.all(promises) + .then(response => { + this._deletedCategories = []; + return Promise.resolve(); + }); + } + + _evtCategoryDeleted(e) { + if (!e.detail.poolCategory.isTransient) { + this._deletedCategories.push(e.detail.poolCategory); + } + } +} + +PoolCategoryList._itemClass = PoolCategory; +PoolCategoryList._itemName = 'poolCategory'; + +module.exports = PoolCategoryList; diff --git a/client/js/models/pool_list.js b/client/js/models/pool_list.js new file mode 100644 index 0000000..d72ece2 --- /dev/null +++ b/client/js/models/pool_list.js @@ -0,0 +1,30 @@ +'use strict'; + +const api = require('../api.js'); +const uri = require('../util/uri.js'); +const AbstractList = require('./abstract_list.js'); +const Pool = require('./pool.js'); + +class PoolList extends AbstractList { + static search(text, offset, limit, fields) { + return api.get( + uri.formatApiLink( + 'pools', { + query: text, + offset: offset, + limit: limit, + fields: fields.join(','), + })) + .then(response => { + return Promise.resolve(Object.assign( + {}, + response, + {results: PoolList.fromResponse(response.results)})); + }); + } +} + +PoolList._itemClass = Pool; +PoolList._itemName = 'pool'; + +module.exports = PoolList; diff --git a/client/js/models/top_navigation.js b/client/js/models/top_navigation.js index bf2cffe..41b1def 100644 --- a/client/js/models/top_navigation.js +++ b/client/js/models/top_navigation.js @@ -81,6 +81,7 @@ function _makeTopNavigation() { ret.add('upload', new TopNavigationItem('U', 'Upload', 'upload')); ret.add('comments', new TopNavigationItem('C', 'Comments', 'comments')); ret.add('tags', new TopNavigationItem('T', 'Tags', 'tags')); + ret.add('pools', new TopNavigationItem('O', 'Pools', 'pools')); ret.add('users', new TopNavigationItem('S', 'Users', 'users')); ret.add('account', new TopNavigationItem('A', 'Account', 'user/{me}')); ret.add('register', new TopNavigationItem('R', 'Register', 'register')); diff --git a/client/js/pools.js b/client/js/pools.js new file mode 100644 index 0000000..8484f0b --- /dev/null +++ b/client/js/pools.js @@ -0,0 +1,26 @@ +'use strict'; + +const misc = require('./util/misc.js'); +const PoolCategoryList = require('./models/pool_category_list.js'); + +let _stylesheet = null; + +function refreshCategoryColorMap() { + return PoolCategoryList.get().then(response => { + if (_stylesheet) { + document.head.removeChild(_stylesheet); + } + _stylesheet = document.createElement('style'); + document.head.appendChild(_stylesheet); + for (let category of response.results) { + const ruleName = misc.makeCssName(category.name, 'pool'); + _stylesheet.sheet.insertRule( + `.${ruleName} { color: ${category.color} }`, + _stylesheet.sheet.cssRules.length); + } + }); +} + +module.exports = { + refreshCategoryColorMap: refreshCategoryColorMap, +}; diff --git a/client/js/util/misc.js b/client/js/util/misc.js index aa62097..5911a36 100644 --- a/client/js/util/misc.js +++ b/client/js/util/misc.js @@ -163,6 +163,11 @@ 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 9c26fb0..38020d8 100644 --- a/client/js/util/views.js +++ b/client/js/util/views.js @@ -221,6 +221,29 @@ function makeTagLink(name, includeHash, includeCount, tag) { misc.escapeHtml(text)); } +function makePoolLink(pool, includeHash, includeCount, name) { + const category = pool.category; + let text = name ? name : pool.names[0]; + if (includeHash === true) { + text = '#' + text; + } + if (includeCount === true) { + text += ' (' + pool.postCount + ')'; + } + return api.hasPrivilege('pools:view') ? + makeElement( + 'a', + { + href: uri.formatClientLink('pool', pool.id), + class: misc.makeCssName(category, 'pool'), + }, + misc.escapeHtml(text)) : + makeElement( + 'span', + {class: misc.makeCssName(category, 'pool')}, + misc.escapeHtml(text)); +} + function makeUserLink(user) { let text = makeThumbnail(user ? user.avatarUrl : null); text += user && user.name ? misc.escapeHtml(user.name) : 'Anonymous'; @@ -400,6 +423,7 @@ function getTemplate(templatePath) { makeDateInput: makeDateInput, makePostLink: makePostLink, makeTagLink: makeTagLink, + makePoolLink: makePoolLink, makeUserLink: makeUserLink, makeFlexboxAlign: makeFlexboxAlign, makeAccessKey: makeAccessKey, @@ -529,6 +553,7 @@ module.exports = { decorateValidator: decorateValidator, makeTagLink: makeTagLink, makePostLink: makePostLink, + makePoolLink: makePoolLink, makeCheckbox: makeCheckbox, makeRadio: makeRadio, syncScrollPosition: syncScrollPosition, diff --git a/client/js/views/help_view.js b/client/js/views/help_view.js index 11f6a39..5d67083 100644 --- a/client/js/views/help_view.js +++ b/client/js/views/help_view.js @@ -17,6 +17,7 @@ const subsectionTemplates = { 'posts': views.getTemplate('help-search-posts'), 'users': views.getTemplate('help-search-users'), 'tags': views.getTemplate('help-search-tags'), + 'pools': views.getTemplate('help-search-pools'), }, }; diff --git a/client/js/views/pool_categories_view.js b/client/js/views/pool_categories_view.js new file mode 100644 index 0000000..1928358 --- /dev/null +++ b/client/js/views/pool_categories_view.js @@ -0,0 +1,166 @@ +'use strict'; + +const events = require('../events.js'); +const views = require('../util/views.js'); +const PoolCategory = require('../models/pool_category.js'); + +const template = views.getTemplate('pool-categories'); +const rowTemplate = views.getTemplate('pool-category-row'); + +class PoolCategoriesView extends events.EventTarget { + constructor(ctx) { + super(); + this._ctx = ctx; + this._hostNode = document.getElementById('content-holder'); + + views.replaceContent(this._hostNode, template(ctx)); + views.syncScrollPosition(); + views.decorateValidator(this._formNode); + + const categoriesToAdd = Array.from(ctx.poolCategories); + categoriesToAdd.sort((a, b) => { + if (b.isDefault) { + return 1; + } else if (a.isDefault) { + return -1; + } + return a.name.localeCompare(b.name); + }); + for (let poolCategory of categoriesToAdd) { + this._addPoolCategoryRowNode(poolCategory); + } + + if (this._addLinkNode) { + this._addLinkNode.addEventListener( + 'click', e => this._evtAddButtonClick(e)); + } + + ctx.poolCategories.addEventListener( + 'add', e => this._evtPoolCategoryAdded(e)); + + ctx.poolCategories.addEventListener( + 'remove', e => this._evtPoolCategoryDeleted(e)); + + this._formNode.addEventListener( + 'submit', e => this._evtSaveButtonClick(e, ctx)); + } + + enableForm() { + views.enableForm(this._formNode); + } + + disableForm() { + views.disableForm(this._formNode); + } + + clearMessages() { + views.clearMessages(this._hostNode); + } + + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } + + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _tableBodyNode() { + return this._hostNode.querySelector('tbody'); + } + + get _addLinkNode() { + return this._hostNode.querySelector('a.add'); + } + + _addPoolCategoryRowNode(poolCategory) { + const rowNode = rowTemplate( + Object.assign( + {}, this._ctx, {poolCategory: poolCategory})); + + const nameInput = rowNode.querySelector('.name input'); + if (nameInput) { + nameInput.addEventListener( + 'change', e => this._evtNameChange(e, rowNode)); + } + + const colorInput = rowNode.querySelector('.color input'); + if (colorInput) { + colorInput.addEventListener( + 'change', e => this._evtColorChange(e, rowNode)); + } + + const removeLinkNode = rowNode.querySelector('.remove a'); + if (removeLinkNode) { + removeLinkNode.addEventListener( + 'click', e => this._evtDeleteButtonClick(e, rowNode)); + } + + const defaultLinkNode = rowNode.querySelector('.set-default a'); + if (defaultLinkNode) { + defaultLinkNode.addEventListener( + 'click', e => this._evtSetDefaultButtonClick(e, rowNode)); + } + + this._tableBodyNode.appendChild(rowNode); + + rowNode._poolCategory = poolCategory; + poolCategory._rowNode = rowNode; + } + + _removePoolCategoryRowNode(poolCategory) { + const rowNode = poolCategory._rowNode; + rowNode.parentNode.removeChild(rowNode); + } + + _evtPoolCategoryAdded(e) { + this._addPoolCategoryRowNode(e.detail.poolCategory); + } + + _evtPoolCategoryDeleted(e) { + this._removePoolCategoryRowNode(e.detail.poolCategory); + } + + _evtAddButtonClick(e) { + e.preventDefault(); + this._ctx.poolCategories.add(new PoolCategory()); + } + + _evtNameChange(e, rowNode) { + rowNode._poolCategory.name = e.target.value; + } + + _evtColorChange(e, rowNode) { + e.target.value = e.target.value.toLowerCase(); + rowNode._poolCategory.color = e.target.value; + } + + _evtDeleteButtonClick(e, rowNode, link) { + e.preventDefault(); + if (e.target.classList.contains('inactive')) { + return; + } + this._ctx.poolCategories.remove(rowNode._poolCategory); + } + + _evtSetDefaultButtonClick(e, rowNode) { + e.preventDefault(); + this._ctx.poolCategories.defaultCategory = rowNode._poolCategory; + const oldRowNode = rowNode.parentNode.querySelector('tr.default'); + if (oldRowNode) { + oldRowNode.classList.remove('default'); + } + rowNode.classList.add('default'); + } + + _evtSaveButtonClick(e, ctx) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('submit')); + } +} + +module.exports = PoolCategoriesView; diff --git a/client/js/views/pool_create_view.js b/client/js/views/pool_create_view.js new file mode 100644 index 0000000..20218e5 --- /dev/null +++ b/client/js/views/pool_create_view.js @@ -0,0 +1,108 @@ +'use strict'; + +const events = require('../events.js'); +const api = require('../api.js'); +const misc = require('../util/misc.js'); +const views = require('../util/views.js'); +const Pool = require('../models/pool.js') + +const template = views.getTemplate('pool-create'); + +class PoolCreateView extends events.EventTarget { + constructor(ctx) { + super(); + + this._hostNode = document.getElementById('content-holder'); + views.replaceContent(this._hostNode, template(ctx)); + + views.decorateValidator(this._formNode); + + if (this._namesFieldNode) { + this._namesFieldNode.addEventListener( + 'input', e => this._evtNameInput(e)); + } + + for (let node of this._formNode.querySelectorAll( + 'input, select, textarea')) { + node.addEventListener( + 'change', e => { + this.dispatchEvent(new CustomEvent('change')); + }); + } + + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + } + + clearMessages() { + views.clearMessages(this._hostNode); + } + + enableForm() { + views.enableForm(this._formNode); + } + + disableForm() { + views.disableForm(this._formNode); + } + + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } + + _evtNameInput(e) { + const regex = new RegExp(api.getPoolNameRegex()); + const list = misc.splitByWhitespace(this._namesFieldNode.value); + + if (!list.length) { + this._namesFieldNode.setCustomValidity( + 'Pools must have at least one name.'); + return; + } + + for (let item of list) { + if (!regex.test(item)) { + this._namesFieldNode.setCustomValidity( + `Pool name "${item}" contains invalid symbols.`); + return; + } + } + + this._namesFieldNode.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, + }, + })); + } + + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _namesFieldNode() { + return this._formNode.querySelector('.names input'); + } + + get _categoryFieldNode() { + return this._formNode.querySelector('.category select'); + } + + get _descriptionFieldNode() { + return this._formNode.querySelector('.description textarea'); + } +} + +module.exports = PoolCreateView; diff --git a/client/js/views/pool_delete_view.js b/client/js/views/pool_delete_view.js new file mode 100644 index 0000000..d3707db --- /dev/null +++ b/client/js/views/pool_delete_view.js @@ -0,0 +1,53 @@ +'use strict'; + +const events = require('../events.js'); +const views = require('../util/views.js'); + +const template = views.getTemplate('pool-delete'); + +class PoolDeleteView extends events.EventTarget { + constructor(ctx) { + super(); + + this._hostNode = ctx.hostNode; + this._pool = ctx.pool; + views.replaceContent(this._hostNode, template(ctx)); + views.decorateValidator(this._formNode); + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + } + + clearMessages() { + views.clearMessages(this._hostNode); + } + + enableForm() { + views.enableForm(this._formNode); + } + + disableForm() { + views.disableForm(this._formNode); + } + + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } + + _evtSubmit(e) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('submit', { + detail: { + pool: this._pool, + }, + })); + } + + get _formNode() { + return this._hostNode.querySelector('form'); + } +} + +module.exports = PoolDeleteView; diff --git a/client/js/views/pool_edit_view.js b/client/js/views/pool_edit_view.js new file mode 100644 index 0000000..fac8430 --- /dev/null +++ b/client/js/views/pool_edit_view.js @@ -0,0 +1,115 @@ +'use strict'; + +const events = require('../events.js'); +const api = require('../api.js'); +const misc = require('../util/misc.js'); +const views = require('../util/views.js'); + +const template = views.getTemplate('pool-edit'); + +class PoolEditView extends events.EventTarget { + constructor(ctx) { + super(); + + this._pool = ctx.pool; + this._hostNode = ctx.hostNode; + views.replaceContent(this._hostNode, template(ctx)); + + views.decorateValidator(this._formNode); + + if (this._namesFieldNode) { + this._namesFieldNode.addEventListener( + 'input', e => this._evtNameInput(e)); + } + + for (let node of this._formNode.querySelectorAll( + 'input, select, textarea')) { + node.addEventListener( + 'change', e => { + this.dispatchEvent(new CustomEvent('change')); + }); + } + + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + } + + clearMessages() { + views.clearMessages(this._hostNode); + } + + enableForm() { + views.enableForm(this._formNode); + } + + disableForm() { + views.disableForm(this._formNode); + } + + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } + + _evtNameInput(e) { + const regex = new RegExp(api.getPoolNameRegex()); + const list = misc.splitByWhitespace(this._namesFieldNode.value); + + if (!list.length) { + this._namesFieldNode.setCustomValidity( + 'Pools must have at least one name.'); + return; + } + + for (let item of list) { + if (!regex.test(item)) { + this._namesFieldNode.setCustomValidity( + `Pool name "${item}" contains invalid symbols.`); + return; + } + } + + this._namesFieldNode.setCustomValidity(''); + } + + _evtSubmit(e) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('submit', { + detail: { + pool: this._pool, + + names: this._namesFieldNode ? + misc.splitByWhitespace(this._namesFieldNode.value) : + undefined, + + category: this._categoryFieldNode ? + this._categoryFieldNode.value : + undefined, + + description: this._descriptionFieldNode ? + this._descriptionFieldNode.value : + undefined, + }, + })); + } + + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _namesFieldNode() { + return this._formNode.querySelector('.names input'); + } + + get _categoryFieldNode() { + return this._formNode.querySelector('.category select'); + } + + get _descriptionFieldNode() { + return this._formNode.querySelector('.description textarea'); + } +} + +module.exports = PoolEditView; diff --git a/client/js/views/pool_merge_view.js b/client/js/views/pool_merge_view.js new file mode 100644 index 0000000..ea9fe6f --- /dev/null +++ b/client/js/views/pool_merge_view.js @@ -0,0 +1,77 @@ +'use strict'; + +const events = require('../events.js'); +const api = require('../api.js'); +const views = require('../util/views.js'); +const PoolAutoCompleteControl = + require('../controls/pool_auto_complete_control.js'); + +const template = views.getTemplate('pool-merge'); + +class PoolMergeView extends events.EventTarget { + constructor(ctx) { + super(); + + this._pool = ctx.pool; + this._hostNode = ctx.hostNode; + ctx.poolNamePattern = api.getPoolNameRegex(); + views.replaceContent(this._hostNode, template(ctx)); + + views.decorateValidator(this._formNode); + if (this._targetPoolFieldNode) { + this._autoCompleteControl = new PoolAutoCompleteControl( + this._targetPoolFieldNode, + { + confirm: pool => + this._autoCompleteControl.replaceSelectedText( + pool.names[0], false), + }); + } + + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + } + + clearMessages() { + views.clearMessages(this._hostNode); + } + + enableForm() { + views.enableForm(this._formNode); + } + + disableForm() { + views.disableForm(this._formNode); + } + + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } + + _evtSubmit(e) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('submit', { + detail: { + pool: this._pool, + targetPoolName: this._targetPoolFieldNode.value + }, + })); + } + + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _targetPoolFieldNode() { + return this._formNode.querySelector('input[name=target-pool]'); + } + + get _addAliasCheckboxNode() { + return this._formNode.querySelector('input[name=alias]'); + } +} + +module.exports = PoolMergeView; diff --git a/client/js/views/pool_summary_view.js b/client/js/views/pool_summary_view.js new file mode 100644 index 0000000..a5808cc --- /dev/null +++ b/client/js/views/pool_summary_view.js @@ -0,0 +1,23 @@ +'use strict'; + +const views = require('../util/views.js'); + +const template = views.getTemplate('pool-summary'); + +class PoolSummaryView { + constructor(ctx) { + this._pool = ctx.pool; + this._hostNode = ctx.hostNode; + views.replaceContent(this._hostNode, template(ctx)); + } + + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } +} + +module.exports = PoolSummaryView; diff --git a/client/js/views/pool_view.js b/client/js/views/pool_view.js new file mode 100644 index 0000000..f296e90 --- /dev/null +++ b/client/js/views/pool_view.js @@ -0,0 +1,106 @@ +'use strict'; + +const events = require('../events.js'); +const views = require('../util/views.js'); +const misc = require('../util/misc.js'); +const PoolSummaryView = require('./pool_summary_view.js'); +const PoolEditView = require('./pool_edit_view.js'); +const PoolMergeView = require('./pool_merge_view.js'); +const PoolDeleteView = require('./pool_delete_view.js'); +const EmptyView = require('../views/empty_view.js'); + +const template = views.getTemplate('pool'); + +class PoolView extends events.EventTarget { + constructor(ctx) { + super(); + + this._ctx = ctx; + ctx.pool.addEventListener('change', e => this._evtChange(e)); + ctx.section = ctx.section || 'summary'; + ctx.getPrettyPoolName = misc.getPrettyPoolName; + + this._hostNode = document.getElementById('content-holder'); + this._install(); + } + + _install() { + const ctx = this._ctx; + views.replaceContent(this._hostNode, template(ctx)); + + for (let item of this._hostNode.querySelectorAll('[data-name]')) { + item.classList.toggle( + 'active', item.getAttribute('data-name') === ctx.section); + if (item.getAttribute('data-name') === ctx.section) { + item.parentNode.scrollLeft = + item.getBoundingClientRect().left - + item.parentNode.getBoundingClientRect().left + } + } + + ctx.hostNode = this._hostNode.querySelector('.pool-content-holder'); + if (ctx.section === 'edit') { + if (!this._ctx.canEditAnything) { + this._view = new EmptyView(); + this._view.showError( + 'You don\'t have privileges to edit pools.'); + } else { + this._view = new PoolEditView(ctx); + events.proxyEvent(this._view, this, 'submit'); + } + + } else if (ctx.section === 'merge') { + if (!this._ctx.canMerge) { + this._view = new EmptyView(); + this._view.showError( + 'You don\'t have privileges to merge pools.'); + } else { + this._view = new PoolMergeView(ctx); + events.proxyEvent(this._view, this, 'submit', 'merge'); + } + + } else if (ctx.section === 'delete') { + if (!this._ctx.canDelete) { + this._view = new EmptyView(); + this._view.showError( + 'You don\'t have privileges to delete pools.'); + } else { + this._view = new PoolDeleteView(ctx); + events.proxyEvent(this._view, this, 'submit', 'delete'); + } + + } else { + this._view = new PoolSummaryView(ctx); + } + + events.proxyEvent(this._view, this, 'change'); + views.syncScrollPosition(); + } + + clearMessages() { + this._view.clearMessages(); + } + + enableForm() { + this._view.enableForm(); + } + + disableForm() { + this._view.disableForm(); + } + + showSuccess(message) { + this._view.showSuccess(message); + } + + showError(message) { + this._view.showError(message); + } + + _evtChange(e) { + this._ctx.pool = e.detail.pool; + this._install(this._ctx); + } +} + +module.exports = PoolView; diff --git a/client/js/views/pools_header_view.js b/client/js/views/pools_header_view.js new file mode 100644 index 0000000..0c18f18 --- /dev/null +++ b/client/js/views/pools_header_view.js @@ -0,0 +1,52 @@ +'use strict'; + +const events = require('../events.js'); +const misc = require('../util/misc.js'); +const search = require('../util/search.js'); +const views = require('../util/views.js'); +const PoolAutoCompleteControl = + require('../controls/pool_auto_complete_control.js'); + +const template = views.getTemplate('pools-header'); + +class PoolsHeaderView extends events.EventTarget { + constructor(ctx) { + super(); + + this._hostNode = ctx.hostNode; + views.replaceContent(this._hostNode, template(ctx)); + + if (this._queryInputNode) { + this._autoCompleteControl = new PoolAutoCompleteControl( + this._queryInputNode, + { + confirm: pool => + this._autoCompleteControl.replaceSelectedText( + misc.escapeSearchTerm(pool.names[0]), true), + }); + } + + search.searchInputNodeFocusHelper(this._queryInputNode); + + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + } + + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _queryInputNode() { + return this._hostNode.querySelector('[name=search-text]'); + } + + _evtSubmit(e) { + e.preventDefault(); + this._queryInputNode.blur(); + this.dispatchEvent(new CustomEvent('navigate', {detail: {parameters: { + query: this._queryInputNode.value, + page: 1, + }}})); + } +} + +module.exports = PoolsHeaderView; diff --git a/client/js/views/pools_page_view.js b/client/js/views/pools_page_view.js new file mode 100644 index 0000000..234d446 --- /dev/null +++ b/client/js/views/pools_page_view.js @@ -0,0 +1,13 @@ +'use strict'; + +const views = require('../util/views.js'); + +const template = views.getTemplate('pools-page'); + +class PoolsPageView { + constructor(ctx) { + views.replaceContent(ctx.hostNode, template(ctx)); + } +} + +module.exports = PoolsPageView; diff --git a/client/package.json b/client/package.json index 3d390ec..e776126 100644 --- a/client/package.json +++ b/client/package.json @@ -3,7 +3,7 @@ "private": true, "scripts": { "build": "node build.js", - "watch": "c1=\"\";while :;do c2=$(find html js css img -type f -and -not -iname '*autogen*'|sort|xargs cat|md5sum);[[ $c1 != $c2 ]]&&npm run build -- --no-vendor-js;c1=$c2;sleep 1;done" + "watch": "c1=\"\";while :;do c2=$(find html js css img -type f -and -not -iname '*autogen*'|sort|xargs cat|md5sum);[[ $c1 != $c2 ]]&&npm run build -- --debug --no-vendor-js;c1=$c2;sleep 1;done" }, "dependencies": { "font-awesome": "^4.7.0", diff --git a/server/alembic.ini b/server/alembic.ini index 6a4e5ff..98ab83d 100644 --- a/server/alembic.ini +++ b/server/alembic.ini @@ -3,6 +3,7 @@ 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 4f8f769..6807623 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: # example: http://example.com +domain: localhost # example: http://example.com # used to salt the users' password hashes and generate filenames for static content secret: change @@ -49,6 +49,9 @@ 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. @@ -58,86 +61,100 @@ 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 - - '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 - - 'uploads:create': regular - 'uploads:use_downloader': power + '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:list': regular + 'pools:view': anonymous + 'pools:merge': moderator + 'pools:delete': moderator + + 'pool_categories:create': moderator + 'pool_categories:edit:name': moderator + 'pool_categories:edit:color': moderator + 'pool_categories:list': anonymous + 'pool_categories:view': anonymous + '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 + + 'snapshots:list': 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 810452e..8a337f9 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -9,3 +9,4 @@ pillow>=4.3.0 pynacl>=1.2.1 pytz>=2018.3 pyRFC3339>=1.0 +youtube_dl>=2020.5.3 diff --git a/server/szurubooru/api/__init__.py b/server/szurubooru/api/__init__.py index 0d7f75f..28f0e98 100644 --- a/server/szurubooru/api/__init__.py +++ b/server/szurubooru/api/__init__.py @@ -4,6 +4,8 @@ import szurubooru.api.user_token_api import szurubooru.api.post_api import szurubooru.api.tag_api import szurubooru.api.tag_category_api +import szurubooru.api.pool_api +import szurubooru.api.pool_category_api import szurubooru.api.comment_api import szurubooru.api.password_reset_api import szurubooru.api.snapshot_api diff --git a/server/szurubooru/api/pool_api.py b/server/szurubooru/api/pool_api.py new file mode 100644 index 0000000..b2ec11b --- /dev/null +++ b/server/szurubooru/api/pool_api.py @@ -0,0 +1,127 @@ +from typing import Optional, List, Dict +from datetime import datetime +from szurubooru import db, model, search, rest +from szurubooru.func import auth, pools, snapshots, serialization, versions + + +_search_executor = search.Executor(search.configs.PoolSearchConfig()) + + +def _serialize(ctx: rest.Context, pool: model.Pool) -> rest.Response: + return pools.serialize_pool( + pool, options=serialization.get_serialization_options(ctx)) + + +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') + return _search_executor.execute_and_serialize( + ctx, lambda pool: _serialize(ctx, pool)) + + +@rest.routes.post('/pools/?') +def create_pool( + ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: + auth.verify_privilege(ctx.user, 'pools:create') + + 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) + + pool = pools.create_pool(names, category) + pools.update_pool_description(pool, description) + ctx.session.add(pool) + ctx.session.flush() + snapshots.create(pool, ctx.user) + ctx.session.commit() + return _serialize(ctx, pool) + + +@rest.routes.get('/pool/(?P[^/]+)/?') +def get_pool(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + auth.verify_privilege(ctx.user, 'pools:view') + pool = _get_pool(params) + return _serialize(ctx, pool) + + +@rest.routes.put('/pool/(?P[^/]+)/?') +def update_pool(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + pool = _get_pool(params) + versions.verify_version(pool, ctx) + versions.bump_version(pool) + if ctx.has_param('names'): + auth.verify_privilege(ctx.user, 'pools:edit:names') + pools.update_pool_names(pool, ctx.get_param_as_string_list('names')) + if ctx.has_param('category'): + auth.verify_privilege(ctx.user, 'pools:edit:category') + pools.update_pool_category_name( + pool, ctx.get_param_as_string('category')) + if ctx.has_param('description'): + 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) + pool.last_edit_time = datetime.utcnow() + ctx.session.flush() + snapshots.modify(pool, ctx.user) + ctx.session.commit() + return _serialize(ctx, pool) + + +@rest.routes.delete('/pool/(?P[^/]+)/?') +def delete_pool(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + pool = _get_pool(params) + versions.verify_version(pool, ctx) + auth.verify_privilege(ctx.user, 'pools:delete') + snapshots.delete(pool, ctx.user) + pools.delete(pool) + ctx.session.commit() + return {} + + +@rest.routes.post('/pool-merge/?') +def merge_pools( + ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: + source_pool_id = ctx.get_param_as_string('remove') + target_pool_id = ctx.get_param_as_string('mergeTo') + source_pool = pools.get_pool_by_id(source_pool_id) + target_pool = pools.get_pool_by_id(target_pool_id) + versions.verify_version(source_pool, ctx, 'removeVersion') + versions.verify_version(target_pool, ctx, 'mergeToVersion') + versions.bump_version(target_pool) + auth.verify_privilege(ctx.user, 'pools:merge') + pools.merge_pools(source_pool, target_pool) + snapshots.merge(source_pool, target_pool, ctx.user) + ctx.session.commit() + return _serialize(ctx, target_pool) diff --git a/server/szurubooru/api/pool_category_api.py b/server/szurubooru/api/pool_category_api.py new file mode 100644 index 0000000..f293724 --- /dev/null +++ b/server/szurubooru/api/pool_category_api.py @@ -0,0 +1,89 @@ +from typing import Dict +from szurubooru import model, rest +from szurubooru.func import ( + auth, pools, pool_categories, snapshots, serialization, versions) + + +def _serialize( + ctx: rest.Context, category: model.PoolCategory) -> rest.Response: + return pool_categories.serialize_category( + category, options=serialization.get_serialization_options(ctx)) + + +@rest.routes.get('/pool-categories/?') +def get_pool_categories( + ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: + auth.verify_privilege(ctx.user, 'pool_categories:list') + categories = pool_categories.get_all_categories() + return { + 'results': [_serialize(ctx, category) for category in categories], + } + + +@rest.routes.post('/pool-categories/?') +def create_pool_category( + ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: + auth.verify_privilege(ctx.user, 'pool_categories:create') + name = ctx.get_param_as_string('name') + color = ctx.get_param_as_string('color') + category = pool_categories.create_category(name, color) + ctx.session.add(category) + ctx.session.flush() + snapshots.create(category, ctx.user) + ctx.session.commit() + return _serialize(ctx, category) + + +@rest.routes.get('/pool-category/(?P[^/]+)/?') +def get_pool_category( + ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + auth.verify_privilege(ctx.user, 'pool_categories:view') + category = pool_categories.get_category_by_name(params['category_name']) + return _serialize(ctx, category) + + +@rest.routes.put('/pool-category/(?P[^/]+)/?') +def update_pool_category( + ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + category = pool_categories.get_category_by_name( + params['category_name'], lock=True) + versions.verify_version(category, ctx) + versions.bump_version(category) + if ctx.has_param('name'): + auth.verify_privilege(ctx.user, 'pool_categories:edit:name') + pool_categories.update_category_name( + category, ctx.get_param_as_string('name')) + if ctx.has_param('color'): + auth.verify_privilege(ctx.user, 'pool_categories:edit:color') + pool_categories.update_category_color( + category, ctx.get_param_as_string('color')) + ctx.session.flush() + snapshots.modify(category, ctx.user) + ctx.session.commit() + return _serialize(ctx, category) + + +@rest.routes.delete('/pool-category/(?P[^/]+)/?') +def delete_pool_category( + ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + category = pool_categories.get_category_by_name( + params['category_name'], lock=True) + versions.verify_version(category, ctx) + auth.verify_privilege(ctx.user, 'pool_categories:delete') + pool_categories.delete_category(category) + snapshots.delete(category, ctx.user) + ctx.session.commit() + return {} + + +@rest.routes.put('/pool-category/(?P[^/]+)/default/?') +def set_pool_category_as_default( + ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + auth.verify_privilege(ctx.user, 'pool_categories:set_default') + category = pool_categories.get_category_by_name( + params['category_name'], lock=True) + pool_categories.set_default_category(category) + ctx.session.flush() + snapshots.modify(category, ctx.user) + ctx.session.commit() + return _serialize(ctx, category) diff --git a/server/szurubooru/func/pool_categories.py b/server/szurubooru/func/pool_categories.py new file mode 100644 index 0000000..eca934a --- /dev/null +++ b/server/szurubooru/func/pool_categories.py @@ -0,0 +1,199 @@ +import re +from typing import Any, Optional, Dict, List, Callable +import sqlalchemy as sa +from szurubooru import config, db, model, errors, rest +from szurubooru.func import util, serialization, cache + + +DEFAULT_CATEGORY_NAME_CACHE_KEY = 'default-pool-category' + + +class PoolCategoryNotFoundError(errors.NotFoundError): + pass + + +class PoolCategoryAlreadyExistsError(errors.ValidationError): + pass + + +class PoolCategoryIsInUseError(errors.ValidationError): + pass + + +class InvalidPoolCategoryNameError(errors.ValidationError): + pass + + +class InvalidPoolCategoryColorError(errors.ValidationError): + pass + + +def _verify_name_validity(name: str) -> None: + name_regex = config.config['pool_category_name_regex'] + if not re.match(name_regex, name): + raise InvalidPoolCategoryNameError( + 'Name must satisfy regex %r.' % name_regex) + + +class PoolCategorySerializer(serialization.BaseSerializer): + def __init__(self, category: model.PoolCategory) -> None: + self.category = category + + def _serializers(self) -> Dict[str, Callable[[], Any]]: + return { + 'name': self.serialize_name, + 'version': self.serialize_version, + 'color': self.serialize_color, + 'usages': self.serialize_usages, + 'default': self.serialize_default, + } + + def serialize_name(self) -> Any: + return self.category.name + + def serialize_version(self) -> Any: + return self.category.version + + def serialize_color(self) -> Any: + return self.category.color + + def serialize_usages(self) -> Any: + return self.category.pool_count + + def serialize_default(self) -> Any: + return self.category.default + + +def serialize_category( + category: Optional[model.PoolCategory], + options: List[str] = []) -> Optional[rest.Response]: + if not category: + return None + return PoolCategorySerializer(category).serialize(options) + + +def create_category(name: str, color: str) -> model.PoolCategory: + category = model.PoolCategory() + update_category_name(category, name) + update_category_color(category, color) + if not get_all_categories(): + category.default = True + return category + + +def update_category_name(category: model.PoolCategory, name: str) -> None: + assert category + if not name: + raise InvalidPoolCategoryNameError('Name cannot be empty.') + expr = sa.func.lower(model.PoolCategory.name) == name.lower() + if category.pool_category_id: + expr = expr & ( + model.PoolCategory.pool_category_id != category.pool_category_id) + already_exists = ( + db.session.query(model.PoolCategory).filter(expr).count() > 0) + if already_exists: + raise PoolCategoryAlreadyExistsError( + 'A category with this name already exists.') + if util.value_exceeds_column_size(name, model.PoolCategory.name): + raise InvalidPoolCategoryNameError('Name is too long.') + _verify_name_validity(name) + category.name = name + cache.remove(DEFAULT_CATEGORY_NAME_CACHE_KEY) + + +def update_category_color(category: model.PoolCategory, color: str) -> None: + assert category + if not color: + raise InvalidPoolCategoryColorError('Color cannot be empty.') + if not re.match(r'^#?[0-9a-z]+$', color): + raise InvalidPoolCategoryColorError('Invalid color.') + if util.value_exceeds_column_size(color, model.PoolCategory.color): + raise InvalidPoolCategoryColorError('Color is too long.') + category.color = color + + +def try_get_category_by_name( + name: str, lock: bool = False) -> Optional[model.PoolCategory]: + query = ( + db.session + .query(model.PoolCategory) + .filter(sa.func.lower(model.PoolCategory.name) == name.lower())) + if lock: + query = query.with_for_update() + return query.one_or_none() + + +def get_category_by_name(name: str, lock: bool = False) -> model.PoolCategory: + category = try_get_category_by_name(name, lock) + if not category: + raise PoolCategoryNotFoundError('Pool category %r not found.' % name) + return category + + +def get_all_category_names() -> List[str]: + return [cat.name for cat in get_all_categories()] + + +def get_all_categories() -> List[model.PoolCategory]: + return db.session.query(model.PoolCategory).order_by( + model.PoolCategory.name.asc()).all() + + +def try_get_default_category( + lock: bool = False) -> Optional[model.PoolCategory]: + query = ( + db.session + .query(model.PoolCategory) + .filter(model.PoolCategory.default)) + if lock: + query = query.with_for_update() + category = query.first() + # if for some reason (e.g. as a result of migration) there's no default + # category, get the first record available. + if not category: + query = ( + db.session + .query(model.PoolCategory) + .order_by(model.PoolCategory.pool_category_id.asc())) + if lock: + query = query.with_for_update() + category = query.first() + return category + + +def get_default_category(lock: bool = False) -> model.PoolCategory: + category = try_get_default_category(lock) + if not category: + raise PoolCategoryNotFoundError('No pool category created yet.') + return category + + +def get_default_category_name() -> str: + if cache.has(DEFAULT_CATEGORY_NAME_CACHE_KEY): + return cache.get(DEFAULT_CATEGORY_NAME_CACHE_KEY) + default_category = get_default_category() + default_category_name = default_category.name + cache.put(DEFAULT_CATEGORY_NAME_CACHE_KEY, default_category_name) + return default_category_name + + +def set_default_category(category: model.PoolCategory) -> None: + assert category + old_category = try_get_default_category(lock=True) + if old_category: + db.session.refresh(old_category) + old_category.default = False + db.session.refresh(category) + category.default = True + cache.remove(DEFAULT_CATEGORY_NAME_CACHE_KEY) + + +def delete_category(category: model.PoolCategory) -> None: + assert category + if len(get_all_category_names()) == 1: + raise PoolCategoryIsInUseError('Cannot delete the last category.') + 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..') + db.session.delete(category) diff --git a/server/szurubooru/func/pools.py b/server/szurubooru/func/pools.py new file mode 100644 index 0000000..d35fd49 --- /dev/null +++ b/server/szurubooru/func/pools.py @@ -0,0 +1,302 @@ +import re +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 + + + +class PoolNotFoundError(errors.NotFoundError): + pass + + +class PoolAlreadyExistsError(errors.ValidationError): + pass + + +class PoolIsInUseError(errors.ValidationError): + pass + + +class InvalidPoolNameError(errors.ValidationError): + pass + + +class InvalidPoolRelationError(errors.ValidationError): + pass + + +class InvalidPoolCategoryError(errors.ValidationError): + pass + + +class InvalidPoolDescriptionError(errors.ValidationError): + pass + + +def _verify_name_validity(name: str) -> None: + if util.value_exceeds_column_size(name, model.PoolName.name): + raise InvalidPoolNameError('Name is too long.') + name_regex = config.config['pool_name_regex'] + if not re.match(name_regex, name): + raise InvalidPoolNameError('Name must satisfy regex %r.' % name_regex) + + +def _get_names(pool: model.Pool) -> List[str]: + assert pool + return [pool_name.name for pool_name in pool.names] + + +def _lower_list(names: List[str]) -> List[str]: + return [name.lower() for name in names] + + +def _check_name_intersection( + names1: List[str], names2: List[str], case_sensitive: bool) -> bool: + if not case_sensitive: + names1 = _lower_list(names1) + names2 = _lower_list(names2) + return len(set(names1).intersection(names2)) > 0 + + +def sort_pools(pools: List[model.Pool]) -> List[model.Pool]: + default_category_name = pool_categories.get_default_category_name() + return sorted( + pools, + key=lambda pool: ( + default_category_name == pool.category.name, + pool.category.name, + pool.names[0].name) + ) + + +class PoolSerializer(serialization.BaseSerializer): + def __init__(self, pool: model.Pool) -> None: + self.pool = pool + + def _serializers(self) -> Dict[str, Callable[[], Any]]: + return { + 'id': self.serialize_id, + 'names': self.serialize_names, + 'category': self.serialize_category, + 'version': self.serialize_version, + 'description': self.serialize_description, + 'creationTime': self.serialize_creation_time, + 'lastEditTime': self.serialize_last_edit_time, + 'postCount': self.serialize_post_count + } + + def serialize_id(self) -> Any: + return self.pool.pool_id + + def serialize_names(self) -> Any: + return [pool_name.name for pool_name in self.pool.names] + + def serialize_category(self) -> Any: + return self.pool.category.name + + def serialize_version(self) -> Any: + return self.pool.version + + def serialize_description(self) -> Any: + return self.pool.description + + def serialize_creation_time(self) -> Any: + return self.pool.creation_time + + def serialize_last_edit_time(self) -> Any: + return self.pool.last_edit_time + + def serialize_post_count(self) -> Any: + return self.pool.post_count + + +def serialize_pool( + pool: model.Pool, options: List[str] = []) -> Optional[rest.Response]: + if not pool: + return None + return PoolSerializer(pool).serialize(options) + + +def try_get_pool_by_id(pool_id: int) -> Optional[model.Pool]: + return ( + db.session + .query(model.Pool) + .filter(model.Pool.pool_id == pool_id) + .one_or_none()) + + +def get_pool_by_id(pool_id: int) -> model.Pool: + pool = try_get_pool_by_id(pool_id) + if not pool: + raise PoolNotFoundError('Pool %r not found.' % pool_id) + return pool + + +def try_get_pool_by_name(name: str) -> Optional[model.Pool]: + return ( + db.session + .query(model.Pool) + .join(model.PoolName) + .filter(sa.func.lower(model.PoolName.name) == name.lower()) + .one_or_none()) + + +def get_pool_by_name(name: str) -> model.Pool: + pool = try_get_pool_by_name(name) + if not pool: + raise PoolNotFoundError('Pool %r not found.' % name) + return pool + + +def get_pools_by_names(names: List[str]) -> List[model.Pool]: + names = util.icase_unique(names) + if len(names) == 0: + return [] + return ( + db.session.query(model.Pool) + .join(model.PoolName) + .filter( + sa.sql.or_( + sa.func.lower(model.PoolName.name) == name.lower() + for name in names)) + .all()) + + +def get_or_create_pools_by_names( + names: List[str]) -> Tuple[List[model.Pool], List[model.Pool]]: + names = util.icase_unique(names) + existing_pools = get_pools_by_names(names) + new_pools = [] + pool_category_name = pool_categories.get_default_category_name() + for name in names: + found = False + for existing_pool in existing_pools: + if _check_name_intersection( + _get_names(existing_pool), [name], False): + found = True + break + if not found: + new_pool = create_pool( + names=[name], + category_name=pool_category_name) + db.session.add(new_pool) + new_pools.append(new_pool) + return existing_pools, new_pools + + +def delete(source_pool: model.Pool) -> None: + assert source_pool + db.session.delete(source_pool) + + +def merge_pools(source_pool: model.Pool, target_pool: model.Pool) -> None: + assert source_pool + assert target_pool + 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: + pass + # alias1 = model.PostPool + # alias2 = sa.orm.util.aliased(model.PostPool) + # update_stmt = ( + # sa.sql.expression.update(alias1) + # .where(alias1.pool_id == source_pool_id)) + # update_stmt = ( + # update_stmt + # .where( + # ~sa.exists() + # .where(alias1.post_id == alias2.post_id) + # .where(alias2.pool_id == target_pool_id))) + # update_stmt = update_stmt.values(pool_id=target_pool_id) + # db.session.execute(update_stmt) + + def merge_relations( + table: model.Base, source_pool_id: int, target_pool_id: int) -> None: + alias1 = table + alias2 = sa.orm.util.aliased(table) + update_stmt = ( + sa.sql.expression.update(alias1) + .where(alias1.parent_id == source_pool_id) + .where(alias1.child_id != target_pool_id) + .where( + ~sa.exists() + .where(alias2.child_id == alias1.child_id) + .where(alias2.parent_id == target_pool_id)) + .values(parent_id=target_pool_id)) + db.session.execute(update_stmt) + + update_stmt = ( + sa.sql.expression.update(alias1) + .where(alias1.child_id == source_pool_id) + .where(alias1.parent_id != target_pool_id) + .where( + ~sa.exists() + .where(alias2.parent_id == alias1.parent_id) + .where(alias2.child_id == target_pool_id)) + .values(child_id=target_pool_id)) + db.session.execute(update_stmt) + + merge_posts(source_pool.pool_id, target_pool.pool_id) + delete(source_pool) + + +def create_pool( + names: List[str], + category_name: str) -> model.Pool: + pool = model.Pool() + pool.creation_time = datetime.utcnow() + update_pool_names(pool, names) + update_pool_category_name(pool, category_name) + return pool + + +def update_pool_category_name(pool: model.Pool, category_name: str) -> None: + assert pool + pool.category = pool_categories.get_category_by_name(category_name) + + +def update_pool_names(pool: model.Pool, names: List[str]) -> None: + # sanitize + assert pool + names = util.icase_unique([name for name in names if name]) + if not len(names): + raise InvalidPoolNameError('At least one name must be specified.') + for name in names: + _verify_name_validity(name) + + # check for existing pools + expr = sa.sql.false() + for name in names: + expr = expr | (sa.func.lower(model.PoolName.name) == name.lower()) + if pool.pool_id: + expr = expr & (model.PoolName.pool_id != pool.pool_id) + existing_pools = db.session.query(model.PoolName).filter(expr).all() + if len(existing_pools): + raise PoolAlreadyExistsError( + 'One of names is already used by another pool.') + + # remove unwanted items + for pool_name in pool.names[:]: + if not _check_name_intersection([pool_name.name], names, True): + pool.names.remove(pool_name) + # add wanted items + for name in names: + if not _check_name_intersection(_get_names(pool), [name], True): + pool.names.append(model.PoolName(name, -1)) + + # set alias order to match the request + for i, name in enumerate(names): + for pool_name in pool.names: + if pool_name.name.lower() == name.lower(): + pool_name.order = i + + +def update_pool_description(pool: model.Pool, description: str) -> None: + assert pool + if util.value_exceeds_column_size(description, model.Pool.description): + raise InvalidPoolDescriptionError('Description is too long.') + pool.description = description or None + diff --git a/server/szurubooru/func/snapshots.py b/server/szurubooru/func/snapshots.py index eade3a1..e9ce07c 100644 --- a/server/szurubooru/func/snapshots.py +++ b/server/szurubooru/func/snapshots.py @@ -24,6 +24,24 @@ def get_tag_snapshot(tag: model.Tag) -> Dict[str, Any]: } +def get_pool_category_snapshot(category: model.PoolCategory) -> Dict[str, Any]: + assert category + return { + 'name': category.name, + 'color': category.color, + 'default': True if category.default else False, + } + + +def get_pool_snapshot(pool: model.Pool) -> Dict[str, Any]: + assert pool + return { + 'names': [pool_name.name for pool_name in pool.names], + 'category': pool.category.name, + # TODO + } + + def get_post_snapshot(post: model.Post) -> Dict[str, Any]: assert post return { @@ -47,6 +65,8 @@ _snapshot_factories = { 'tag_category': lambda entity: get_tag_category_snapshot(entity), 'tag': lambda entity: get_tag_snapshot(entity), 'post': lambda entity: get_post_snapshot(entity), + 'pool_category': lambda entity: get_pool_category_snapshot(entity), + 'pool': lambda entity: get_pool_snapshot(entity), } # type: Dict[model.Base, Callable[[model.Base], Dict[str ,Any]]] diff --git a/server/szurubooru/func/tags.py b/server/szurubooru/func/tags.py index 7d92f1e..60eda50 100644 --- a/server/szurubooru/func/tags.py +++ b/server/szurubooru/func/tags.py @@ -1,5 +1,3 @@ -import json -import os import re from typing import Any, Optional, Tuple, List, Dict, Callable from datetime import datetime diff --git a/server/szurubooru/migrations/versions/54de8acc6cef_add_default_pool_category.py b/server/szurubooru/migrations/versions/54de8acc6cef_add_default_pool_category.py new file mode 100644 index 0000000..782833c --- /dev/null +++ b/server/szurubooru/migrations/versions/54de8acc6cef_add_default_pool_category.py @@ -0,0 +1,60 @@ +''' +add default pool category + +Revision ID: 54de8acc6cef +Created at: 2020-05-03 14:57:46.825766 +''' + +import sqlalchemy as sa +from alembic import op + + +revision = '54de8acc6cef' +down_revision = '6a2f424ec9d2' +branch_labels = None +depends_on = None + + +Base = sa.ext.declarative.declarative_base() + + +class PoolCategory(Base): + __tablename__ = 'pool_category' + __table_args__ = {'extend_existing': True} + + pool_category_id = sa.Column('id', sa.Integer, primary_key=True) + version = sa.Column('version', sa.Integer, nullable=False) + name = sa.Column('name', sa.Unicode(32), nullable=False) + color = sa.Column('color', sa.Unicode(32), nullable=False) + default = sa.Column('default', sa.Boolean, nullable=False) + + __mapper_args__ = { + 'version_id_col': version, + 'version_id_generator': False, + } + +def upgrade(): + session = sa.orm.session.Session(bind=op.get_bind()) + if session.query(PoolCategory).count() == 0: + category = PoolCategory() + category.name = 'default' + category.color = 'default' + category.version = 1 + category.default = True + session.add(category) + session.commit() + + +def downgrade(): + session = sa.orm.session.Session(bind=op.get_bind()) + default_category = ( + session + .query(PoolCategory) + .filter(PoolCategory.name == 'default') + .filter(PoolCategory.color == 'default') + .filter(PoolCategory.version == 1) + .filter(PoolCategory.default == 1) + .one_or_none()) + if default_category: + session.delete(default_category) + session.commit() diff --git a/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py b/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py new file mode 100644 index 0000000..9b663ac --- /dev/null +++ b/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py @@ -0,0 +1,52 @@ +''' +create pool tables + +Revision ID: 6a2f424ec9d2 +Created at: 2020-05-03 14:47:59.136410 +''' + +import sqlalchemy as sa +from alembic import op + + +revision = '6a2f424ec9d2' +down_revision = '1e280b5d5df1' +branch_labels = None +depends_on = None + +def upgrade(): + op.create_table( + 'pool_category', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('version', sa.Integer(), nullable=False, default=1), + sa.Column('name', sa.Unicode(length=32), nullable=False), + sa.Column('color', sa.Unicode(length=32), nullable=False), + sa.Column('default', sa.Boolean(), nullable=False, default=False), + sa.PrimaryKeyConstraint('id')) + + op.create_table( + 'pool', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('version', sa.Integer(), nullable=False, default=1), + sa.Column('description', sa.UnicodeText(), nullable=True), + sa.Column('category_id', sa.Integer(), nullable=False), + sa.Column('creation_time', sa.DateTime(), nullable=False), + sa.Column('last_edit_time', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['category_id'], ['pool_category.id']), + sa.PrimaryKeyConstraint('id')) + + op.create_table( + 'pool_name', + sa.Column('pool_name_id', sa.Integer(), nullable=False), + sa.Column('pool_id', sa.Integer(), nullable=False), + sa.Column('name', sa.Unicode(length=256), nullable=False), + sa.Column('ord', sa.Integer(), nullable=False, index=True), + sa.ForeignKeyConstraint(['pool_id'], ['pool.id']), + sa.PrimaryKeyConstraint('pool_name_id'), + sa.UniqueConstraint('name')) + +def downgrade(): + op.drop_index(op.f('ix_pool_name_ord'), table_name='pool_name') + 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 a043bd1..cf18b0f 100644 --- a/server/szurubooru/model/__init__.py +++ b/server/szurubooru/model/__init__.py @@ -11,6 +11,8 @@ from szurubooru.model.post import ( PostNote, PostFeature, PostSignature) +from szurubooru.model.pool import Pool, PoolName +from szurubooru.model.pool_category import PoolCategory from szurubooru.model.comment import Comment, CommentScore from szurubooru.model.snapshot import Snapshot import szurubooru.model.util diff --git a/server/szurubooru/model/pool.py b/server/szurubooru/model/pool.py new file mode 100644 index 0000000..2505150 --- /dev/null +++ b/server/szurubooru/model/pool.py @@ -0,0 +1,70 @@ +import sqlalchemy as sa +from szurubooru.model.base import Base + + +class PoolName(Base): + __tablename__ = 'pool_name' + + pool_name_id = sa.Column('pool_name_id', sa.Integer, primary_key=True) + pool_id = sa.Column( + 'pool_id', + sa.Integer, + sa.ForeignKey('pool.id'), + nullable=False, + index=True) + name = sa.Column('name', sa.Unicode(128), nullable=False, unique=True) + order = sa.Column('ord', sa.Integer, nullable=False, index=True) + + def __init__(self, name: str, order: int) -> None: + self.name = name + self.order = order + +class Pool(Base): + __tablename__ = 'pool' + + pool_id = sa.Column('id', sa.Integer, primary_key=True) + category_id = sa.Column( + 'category_id', + sa.Integer, + sa.ForeignKey('pool_category.id'), + nullable=False, + index=True) + version = sa.Column('version', sa.Integer, default=1, nullable=False) + creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) + last_edit_time = sa.Column('last_edit_time', sa.DateTime) + description = sa.Column('description', sa.UnicodeText, default=None) + + category = sa.orm.relationship('PoolCategory', lazy='joined') + names = sa.orm.relationship( + 'PoolName', + cascade='all,delete-orphan', + lazy='joined', + order_by='PoolName.order') + + # 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()) + + first_name = sa.orm.column_property( + ( + sa.sql.expression.select([PoolName.name]) + .where(PoolName.pool_id == pool_id) + .order_by(PoolName.order) + .limit(1) + .as_scalar() + ), + deferred=True) + + + __mapper_args__ = { + 'version_id_col': version, + 'version_id_generator': False, + } diff --git a/server/szurubooru/model/pool_category.py b/server/szurubooru/model/pool_category.py new file mode 100644 index 0000000..5af0b11 --- /dev/null +++ b/server/szurubooru/model/pool_category.py @@ -0,0 +1,28 @@ +from typing import Optional +import sqlalchemy as sa +from szurubooru.model.base import Base +from szurubooru.model.pool import Pool + + +class PoolCategory(Base): + __tablename__ = 'pool_category' + + pool_category_id = sa.Column('id', sa.Integer, primary_key=True) + version = sa.Column('version', sa.Integer, default=1, nullable=False) + name = sa.Column('name', sa.Unicode(32), nullable=False) + color = sa.Column( + 'color', sa.Unicode(32), nullable=False, default='#000000') + default = sa.Column('default', sa.Boolean, nullable=False, default=False) + + def __init__(self, name: Optional[str] = None) -> None: + self.name = name + + pool_count = sa.orm.column_property( + sa.sql.expression.select([sa.sql.expression.func.count('Pool.pool_id')]) + .where(Pool.category_id == pool_category_id) + .correlate_except(sa.table('Pool'))) + + __mapper_args__ = { + 'version_id_col': version, + 'version_id_generator': False, + } diff --git a/server/szurubooru/model/util.py b/server/szurubooru/model/util.py index e82539f..ab86100 100644 --- a/server/szurubooru/model/util.py +++ b/server/szurubooru/model/util.py @@ -10,6 +10,8 @@ def get_resource_info(entity: Base) -> Tuple[Any, Any, Union[str, int]]: 'tag_category': lambda category: category.name, 'comment': lambda comment: comment.comment_id, 'post': lambda post: post.post_id, + 'pool': lambda pool: pool.pool_id, + 'pool_category': lambda category: category.name, } # type: Dict[str, Callable[[Base], Any]] resource_type = entity.__table__.name diff --git a/server/szurubooru/search/configs/__init__.py b/server/szurubooru/search/configs/__init__.py index c7e3102..c6a3ea4 100644 --- a/server/szurubooru/search/configs/__init__.py +++ b/server/szurubooru/search/configs/__init__.py @@ -3,3 +3,4 @@ from .tag_search_config import TagSearchConfig from .post_search_config import PostSearchConfig from .snapshot_search_config import SnapshotSearchConfig from .comment_search_config import CommentSearchConfig +from .pool_search_config import PoolSearchConfig diff --git a/server/szurubooru/search/configs/pool_search_config.py b/server/szurubooru/search/configs/pool_search_config.py new file mode 100644 index 0000000..ddc325a --- /dev/null +++ b/server/szurubooru/search/configs/pool_search_config.py @@ -0,0 +1,109 @@ +from typing import Tuple, Dict +import sqlalchemy as sa +from szurubooru import db, model +from szurubooru.func import util +from szurubooru.search.typing import SaColumn, SaQuery +from szurubooru.search.configs import util as search_util +from szurubooru.search.configs.base_search_config import ( + BaseSearchConfig, Filter) + + +class PoolSearchConfig(BaseSearchConfig): + def create_filter_query(self, _disable_eager_loads: bool) -> SaQuery: + strategy = ( + sa.orm.lazyload + if _disable_eager_loads + else sa.orm.subqueryload) + return ( + db.session.query(model.Pool) + .join(model.PoolCategory) + .options( + strategy(model.Pool.names))) + + def create_count_query(self, _disable_eager_loads: bool) -> SaQuery: + return db.session.query(model.Pool) + + def create_around_query(self) -> SaQuery: + raise NotImplementedError() + + def finalize_query(self, query: SaQuery) -> SaQuery: + return query.order_by(model.Pool.first_name.asc()) + + @property + def anonymous_filter(self) -> Filter: + return search_util.create_subquery_filter( + model.Pool.pool_id, + model.PoolName.pool_id, + model.PoolName.name, + search_util.create_str_filter) + + @property + def named_filters(self) -> Dict[str, Filter]: + return util.unalias_dict([ + ( + ['name'], + search_util.create_subquery_filter( + model.Pool.pool_id, + model.PoolName.pool_id, + model.PoolName.name, + search_util.create_str_filter) + ), + + ( + ['category'], + search_util.create_subquery_filter( + model.Pool.category_id, + model.PoolCategory.pool_category_id, + model.PoolCategory.name, + search_util.create_str_filter) + ), + + ( + ['creation-date', 'creation-time'], + search_util.create_date_filter(model.Pool.creation_time) + ), + + ( + ['last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'], + search_util.create_date_filter(model.Pool.last_edit_time) + ), + + ( + ['post-count'], + search_util.create_num_filter(model.Pool.post_count) + ), + ]) + + @property + def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: + return util.unalias_dict([ + ( + ['random'], + (sa.sql.expression.func.random(), self.SORT_NONE) + ), + + ( + ['name'], + (model.Pool.first_name, self.SORT_ASC) + ), + + ( + ['category'], + (model.PoolCategory.name, self.SORT_ASC) + ), + + ( + ['creation-date', 'creation-time'], + (model.Pool.creation_time, self.SORT_DESC) + ), + + ( + ['last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'], + (model.Pool.last_edit_time, self.SORT_DESC) + ), + + ( + ['post-count'], + (model.Pool.post_count, self.SORT_DESC) + ), + ]) -- cgit v1.3 From 57193b57157b7a42896c887a2d5930493ac7b290 Mon Sep 17 00:00:00 2001 From: Shyam Sunder Date: Fri, 5 Jun 2020 18:03:37 -0400 Subject: client+server: implement code autoformatting using prettier and black --- .pre-commit-config.yaml | 77 +- client/.eslintrc.yml | 283 +------ client/.prettierrc.yml | 4 + client/js/api.js | 250 +++--- client/js/config.js | 4 +- client/js/controllers/auth_controller.js | 42 +- client/js/controllers/base_post_controller.js | 16 +- client/js/controllers/comments_controller.js | 75 +- client/js/controllers/help_controller.js | 18 +- client/js/controllers/home_controller.js | 31 +- client/js/controllers/not_found_controller.js | 14 +- client/js/controllers/page_controller.js | 8 +- client/js/controllers/password_reset_controller.js | 68 +- .../js/controllers/pool_categories_controller.js | 78 +- client/js/controllers/pool_controller.js | 183 +++-- client/js/controllers/pool_create_controller.js | 75 +- client/js/controllers/pool_list_controller.js | 99 ++- client/js/controllers/post_detail_controller.js | 115 +-- client/js/controllers/post_list_controller.js | 112 +-- client/js/controllers/post_main_controller.js | 310 ++++--- client/js/controllers/post_upload_controller.js | 190 +++-- client/js/controllers/settings_controller.js | 20 +- client/js/controllers/snapshots_controller.js | 51 +- client/js/controllers/tag_categories_controller.js | 76 +- client/js/controllers/tag_controller.js | 180 ++-- client/js/controllers/tag_list_controller.js | 76 +- client/js/controllers/top_navigation_controller.js | 63 +- client/js/controllers/user_controller.js | 356 ++++---- client/js/controllers/user_list_controller.js | 63 +- .../js/controllers/user_registration_controller.js | 73 +- client/js/controls/auto_complete_control.js | 175 ++-- client/js/controls/comment_control.js | 191 +++-- client/js/controls/comment_list_control.js | 29 +- client/js/controls/expander_control.js | 47 +- client/js/controls/file_dropper_control.js | 94 ++- client/js/controls/pool_auto_complete_control.js | 67 +- client/js/controls/pool_input_control.js | 134 +-- client/js/controls/post_content_control.js | 48 +- client/js/controls/post_edit_sidebar_control.js | 471 ++++++----- client/js/controls/post_notes_overlay_control.js | 374 +++++---- .../js/controls/post_readonly_sidebar_control.js | 181 +++-- client/js/controls/tag_auto_complete_control.js | 82 +- client/js/controls/tag_input_control.js | 322 ++++---- client/js/events.js | 24 +- client/js/main.js | 163 ++-- client/js/models/abstract_list.js | 46 +- client/js/models/comment.js | 78 +- client/js/models/comment_list.js | 11 +- client/js/models/info.js | 28 +- client/js/models/note.js | 10 +- client/js/models/note_list.js | 11 +- client/js/models/point.js | 12 +- client/js/models/point_list.js | 8 +- client/js/models/pool.js | 92 ++- client/js/models/pool_category.js | 63 +- client/js/models/pool_category_list.js | 46 +- client/js/models/pool_list.js | 34 +- client/js/models/post.js | 295 ++++--- client/js/models/post_list.js | 52 +- client/js/models/settings.js | 22 +- client/js/models/snapshot.js | 6 +- client/js/models/snapshot_list.js | 33 +- client/js/models/tag.js | 96 ++- client/js/models/tag_category.js | 63 +- client/js/models/tag_category_list.js | 46 +- client/js/models/tag_list.js | 42 +- client/js/models/top_navigation.js | 54 +- client/js/models/user.js | 70 +- client/js/models/user_list.js | 34 +- client/js/models/user_token.js | 74 +- client/js/pools.js | 15 +- client/js/router.js | 103 +-- client/js/tags.js | 15 +- client/js/templates.js | 4 +- client/js/util/keyboard.js | 8 +- client/js/util/markdown.js | 83 +- client/js/util/misc.js | 110 +-- client/js/util/optimized_resize.js | 10 +- client/js/util/polyfill.js | 28 +- client/js/util/progress.js | 4 +- client/js/util/search.js | 14 +- client/js/util/touch.js | 65 +- client/js/util/uri.js | 35 +- client/js/util/views.js | 454 ++++++----- client/js/views/comments_page_view.js | 22 +- client/js/views/empty_view.js | 9 +- client/js/views/endless_page_view.js | 126 +-- client/js/views/help_view.js | 80 +- client/js/views/home_view.js | 89 +- client/js/views/login_view.js | 61 +- client/js/views/manual_page_view.js | 131 +-- client/js/views/not_found_view.js | 10 +- client/js/views/password_reset_view.js | 41 +- client/js/views/pool_categories_view.js | 82 +- client/js/views/pool_create_view.js | 82 +- client/js/views/pool_delete_view.js | 24 +- client/js/views/pool_edit_view.js | 103 +-- client/js/views/pool_merge_view.js | 46 +- client/js/views/pool_summary_view.js | 6 +- client/js/views/pool_view.js | 64 +- client/js/views/pools_header_view.js | 45 +- client/js/views/pools_page_view.js | 6 +- client/js/views/post_detail_view.js | 40 +- client/js/views/post_main_view.js | 102 ++- client/js/views/post_merge_view.js | 106 ++- client/js/views/post_upload_view.js | 228 +++--- client/js/views/posts_header_view.js | 164 ++-- client/js/views/posts_page_view.js | 71 +- client/js/views/registration_view.js | 49 +- client/js/views/settings_view.js | 50 +- client/js/views/snapshots_page_view.js | 104 +-- client/js/views/tag_categories_view.js | 82 +- client/js/views/tag_delete_view.js | 24 +- client/js/views/tag_edit_view.js | 115 +-- client/js/views/tag_merge_view.js | 47 +- client/js/views/tag_summary_view.js | 6 +- client/js/views/tag_view.js | 64 +- client/js/views/tags_header_view.js | 45 +- client/js/views/tags_page_view.js | 6 +- client/js/views/top_navigation_view.js | 34 +- client/js/views/user_delete_view.js | 25 +- client/js/views/user_edit_view.js | 110 +-- client/js/views/user_summary_view.js | 6 +- client/js/views/user_tokens_view.js | 107 +-- client/js/views/user_view.js | 64 +- client/js/views/users_header_view.js | 30 +- client/js/views/users_page_view.js | 6 +- server/.dockerignore | 3 +- server/.flake8 | 5 + server/Dockerfile.test | 2 +- server/hooks/test | 6 +- server/pyproject.toml | 10 + server/setup.cfg | 20 - server/szuru-admin | 5 +- server/szurubooru/api/__init__.py | 14 +- server/szurubooru/api/comment_api.py | 82 +- server/szurubooru/api/info_api.py | 59 +- server/szurubooru/api/password_reset_api.py | 63 +- server/szurubooru/api/pool_api.py | 87 +- server/szurubooru/api/pool_category_api.py | 86 +- server/szurubooru/api/post_api.py | 265 +++--- server/szurubooru/api/snapshot_api.py | 14 +- server/szurubooru/api/tag_api.py | 110 +-- server/szurubooru/api/tag_category_api.py | 86 +- server/szurubooru/api/upload_api.py | 16 +- server/szurubooru/api/user_api.py | 101 +-- server/szurubooru/api/user_token_api.py | 96 ++- server/szurubooru/config.py | 43 +- server/szurubooru/db.py | 11 +- server/szurubooru/errors.py | 7 +- server/szurubooru/facade.py | 111 +-- server/szurubooru/func/auth.py | 99 ++- server/szurubooru/func/cache.py | 11 +- server/szurubooru/func/comments.py | 43 +- server/szurubooru/func/diff.py | 26 +- server/szurubooru/func/favorites.py | 11 +- server/szurubooru/func/file_uploads.py | 12 +- server/szurubooru/func/files.py | 9 +- server/szurubooru/func/image_hash.py | 243 +++--- server/szurubooru/func/images.py | 335 +++++--- server/szurubooru/func/mailer.py | 14 +- server/szurubooru/func/mime.py | 66 +- server/szurubooru/func/net.py | 48 +- server/szurubooru/func/pool_categories.py | 94 ++- server/szurubooru/func/pools.py | 118 +-- server/szurubooru/func/posts.py | 545 +++++++------ server/szurubooru/func/scores.py | 22 +- server/szurubooru/func/serialization.py | 12 +- server/szurubooru/func/snapshots.py | 119 +-- server/szurubooru/func/tag_categories.py | 94 ++- server/szurubooru/func/tags.py | 149 ++-- server/szurubooru/func/user_tokens.py | 70 +- server/szurubooru/func/users.py | 185 +++-- server/szurubooru/func/util.py | 67 +- server/szurubooru/func/versions.py | 12 +- server/szurubooru/middleware/__init__.py | 2 +- server/szurubooru/middleware/authenticator.py | 57 +- server/szurubooru/middleware/cache_purger.py | 2 +- server/szurubooru/middleware/request_logger.py | 7 +- server/szurubooru/migrations/env.py | 40 +- server/szurubooru/migrations/script.py.mako | 1 + .../versions/00cb3a2734db_create_tag_tables.py | 85 +- .../02ef5f73f4ab_add_hashes_to_post_file_names.py | 34 +- ...e048fb3_add_default_column_to_tag_categories.py | 20 +- .../1cd4c7b22846_change_flags_column_to_string.py | 55 +- .../versions/1e280b5d5df1_longer_tag_names.py | 33 +- .../23abaf4a0a4b_add_mime_type_to_posts.py | 13 +- .../versions/336a76ec1338_create_post_tables.py | 89 +- .../versions/3c1f0316fa7f_resize_post_columns.py | 28 +- .../46cd5229839b_add_snapshot_resource_repr.py | 15 +- .../versions/46df355634dc_add_comment_tables.py | 50 +- .../versions/49ab4e1139ef_create_indexes.py | 105 +-- .../4a020f1d271a_rename_snapshot_columns.py | 59 +- .../4c526f869323_add_description_to_tags.py | 13 +- .../52d6ea6584b8_generate_post_signature_table.py | 24 +- .../54de8acc6cef_add_default_pool_category.py | 41 +- .../versions/565e01e3cf6d_create_snapshot_table.py | 31 +- .../5f00af3004a4_add_default_tag_category.py | 43 +- .../versions/6a2f424ec9d2_create_pool_tables.py | 85 +- .../versions/7f6baf38c27c_add_versions.py | 21 +- .../versions/840b460c5613_fix_foreignkeys.py | 29 +- .../84bd402f15f0_change_flags_column_type.py | 19 +- .../9587de88a84b_create_aux_post_tables.py | 80 +- .../9837fc981ec7_add_order_to_tag_names.py | 30 +- ...c2a_update_user_table_for_hardened_passwords.py | 75 +- .../versions/a39c7f98a7fa_add_user_token_table.py | 43 +- .../versions/e5c1216a8503_create_user_table.py | 33 +- .../versions/ed6dd16a30f3_delete_post_columns.py | 57 +- server/szurubooru/model/__init__.py | 25 +- server/szurubooru/model/base.py | 1 - server/szurubooru/model/comment.py | 69 +- server/szurubooru/model/pool.py | 100 ++- server/szurubooru/model/pool_category.py | 25 +- server/szurubooru/model/post.py | 348 ++++---- server/szurubooru/model/snapshot.py | 37 +- server/szurubooru/model/tag.py | 119 +-- server/szurubooru/model/tag_category.py | 24 +- server/szurubooru/model/user.py | 131 +-- server/szurubooru/model/util.py | 31 +- server/szurubooru/rest/__init__.py | 2 +- server/szurubooru/rest/app.py | 80 +- server/szurubooru/rest/context.py | 144 ++-- server/szurubooru/rest/errors.py | 34 +- server/szurubooru/rest/middleware.py | 4 +- server/szurubooru/rest/routes.py | 16 +- server/szurubooru/search/__init__.py | 2 +- server/szurubooru/search/configs/__init__.py | 8 +- .../search/configs/base_search_config.py | 5 +- .../search/configs/comment_search_config.py | 70 +- .../search/configs/pool_search_config.py | 150 ++-- .../search/configs/post_search_config.py | 673 +++++++-------- .../search/configs/snapshot_search_config.py | 33 +- .../szurubooru/search/configs/tag_search_config.py | 189 ++--- .../search/configs/user_search_config.py | 59 +- server/szurubooru/search/configs/util.py | 127 +-- server/szurubooru/search/criteria.py | 16 +- server/szurubooru/search/executor.py | 146 ++-- server/szurubooru/search/parser.py | 79 +- server/szurubooru/search/query.py | 16 +- server/szurubooru/search/tokens.py | 13 +- server/szurubooru/search/typing.py | 1 - .../szurubooru/tests/api/test_comment_creating.py | 63 +- .../szurubooru/tests/api/test_comment_deleting.py | 46 +- server/szurubooru/tests/api/test_comment_rating.py | 124 +-- .../tests/api/test_comment_retrieving.py | 77 +- .../szurubooru/tests/api/test_comment_updating.py | 102 ++- server/szurubooru/tests/api/test_info.py | 136 ++-- server/szurubooru/tests/api/test_password_reset.py | 97 ++- .../tests/api/test_pool_category_creating.py | 55 +- .../tests/api/test_pool_category_deleting.py | 67 +- .../tests/api/test_pool_category_retrieving.py | 59 +- .../tests/api/test_pool_category_updating.py | 130 +-- server/szurubooru/tests/api/test_pool_creating.py | 85 +- server/szurubooru/tests/api/test_pool_deleting.py | 38 +- server/szurubooru/tests/api/test_pool_merging.py | 90 +- .../szurubooru/tests/api/test_pool_retrieving.py | 74 +- server/szurubooru/tests/api/test_pool_updating.py | 163 ++-- server/szurubooru/tests/api/test_post_creating.py | 469 ++++++----- server/szurubooru/tests/api/test_post_deleting.py | 34 +- .../szurubooru/tests/api/test_post_favoriting.py | 107 +-- server/szurubooru/tests/api/test_post_featuring.py | 95 ++- server/szurubooru/tests/api/test_post_merging.py | 82 +- server/szurubooru/tests/api/test_post_rating.py | 121 +-- .../szurubooru/tests/api/test_post_retrieving.py | 107 ++- server/szurubooru/tests/api/test_post_updating.py | 255 +++--- .../tests/api/test_snapshot_retrieving.py | 43 +- .../tests/api/test_tag_category_creating.py | 53 +- .../tests/api/test_tag_category_deleting.py | 69 +- .../tests/api/test_tag_category_retrieving.py | 59 +- .../tests/api/test_tag_category_updating.py | 126 +-- server/szurubooru/tests/api/test_tag_creating.py | 91 ++- server/szurubooru/tests/api/test_tag_deleting.py | 46 +- server/szurubooru/tests/api/test_tag_merging.py | 98 ++- server/szurubooru/tests/api/test_tag_retrieving.py | 80 +- .../tests/api/test_tag_siblings_retrieving.py | 44 +- server/szurubooru/tests/api/test_tag_updating.py | 205 +++-- server/szurubooru/tests/api/test_user_creating.py | 103 ++- server/szurubooru/tests/api/test_user_deleting.py | 51 +- .../szurubooru/tests/api/test_user_retrieving.py | 75 +- .../tests/api/test_user_token_creating.py | 28 +- .../tests/api/test_user_token_deleting.py | 23 +- .../tests/api/test_user_token_retrieving.py | 32 +- .../tests/api/test_user_token_updating.py | 47 +- server/szurubooru/tests/api/test_user_updating.py | 174 ++-- server/szurubooru/tests/conftest.py | 96 ++- server/szurubooru/tests/func/test_auth.py | 23 +- server/szurubooru/tests/func/test_comments.py | 47 +- server/szurubooru/tests/func/test_diff.py | 393 ++++----- server/szurubooru/tests/func/test_image_hash.py | 13 +- server/szurubooru/tests/func/test_mime.py | 114 +-- server/szurubooru/tests/func/test_net.py | 126 +-- server/szurubooru/tests/func/test_posts.py | 902 +++++++++++---------- server/szurubooru/tests/func/test_snapshots.py | 171 ++-- .../szurubooru/tests/func/test_tag_categories.py | 126 +-- server/szurubooru/tests/func/test_tags.py | 461 ++++++----- server/szurubooru/tests/func/test_user_tokens.py | 107 +-- server/szurubooru/tests/func/test_users.py | 406 +++++----- server/szurubooru/tests/func/test_util.py | 55 +- .../tests/middleware/test_authenticator.py | 62 +- server/szurubooru/tests/model/test_comment.py | 5 +- server/szurubooru/tests/model/test_pool.py | 30 +- server/szurubooru/tests/model/test_post.py | 38 +- server/szurubooru/tests/model/test_tag.py | 58 +- server/szurubooru/tests/model/test_user.py | 76 +- server/szurubooru/tests/model/test_user_token.py | 3 +- server/szurubooru/tests/rest/test_context.py | 125 +-- .../search/configs/test_comment_search_config.py | 190 +++-- .../search/configs/test_pool_search_config.py | 504 +++++++----- .../search/configs/test_post_search_config.py | 829 ++++++++++--------- .../tests/search/configs/test_tag_search_config.py | 597 ++++++++------ .../search/configs/test_user_search_config.py | 382 +++++---- server/szurubooru/tests/search/test_executor.py | 127 +-- 312 files changed, 15538 insertions(+), 12851 deletions(-) create mode 100644 client/.prettierrc.yml create mode 100644 server/.flake8 create mode 100644 server/pyproject.toml delete mode 100644 server/setup.cfg (limited to 'client/js/controllers/auth_controller.js') diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 31bcfd9..0b79174 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,50 +1,91 @@ repos: + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - - id: check-added-large-files + +- repo: https://github.com/psf/black + rev: stable + hooks: + - id: black + files: server/szurubooru/ + language_version: python3.8 + +- repo: https://github.com/timothycrosley/isort + rev: '4.3.21-2' + hooks: + - id: isort + files: server/szurubooru/ + exclude: server/szurubooru/migrations/env.py + additional_dependencies: + - toml + +- repo: https://github.com/prettier/prettier + rev: '2.0.5' + hooks: + - id: prettier + files: client/js/ + exclude: client/js/.gitignore + args: ['--config', 'client/.prettierrc.yml'] + - repo: https://github.com/pre-commit/mirrors-eslint rev: v7.1.0 hooks: - id: eslint files: client/js/ args: ['--fix'] + additional_dependencies: + - eslint-config-prettier + - repo: https://gitlab.com/pycqa/flake8 rev: '3.8.2' hooks: - id: flake8 files: server/szurubooru/ additional_dependencies: - - flake8-print - args: ['--config=server/setup.cfg'] + - flake8-print + args: ['--config=server/.flake8'] + - repo: local hooks: - - id: pytest - name: pytest - entry: >- - bash -c - 'docker build -f server/Dockerfile.test -t $(git rev-parse --short HEAD)-test server/ - && docker run --rm -t $(git rev-parse --short HEAD)-test szurubooru/ - && docker rmi --no-prune $(git rev-parse --short HEAD)-test' - language: system - types: [python] - files: server/szurubooru/ - pass_filenames: false - id: docker-build-client - name: Test building the client in Docker - entry: bash -c 'docker build -t szurubooru-client:$(git rev-parse --short HEAD) client/' + name: Docker - build client + entry: bash -c 'docker build client/' language: system types: [file] files: client/ pass_filenames: false + - id: docker-build-server - name: Test building the server in Docker - entry: bash -c 'docker build -t szurubooru-server:$(git rev-parse --short HEAD) server/' + name: Docker - build server + entry: bash -c 'docker build server/' language: system types: [file] files: server/ pass_filenames: false + + - id: pytest + name: pytest + entry: bash -c 'docker run --rm -t $(docker build -f server/Dockerfile.test -q server/) szurubooru/' + language: system + types: [python] + files: server/szurubooru/ + exclude: server/szurubooru/migrations/ + pass_filenames: false + + - id: pytest-cov + name: pytest + entry: bash -c 'docker run --rm -t $(docker build -f server/Dockerfile.test -q server/) --cov-report=term-missing:skip-covered --cov=szurubooru szurubooru/' + language: system + types: [python] + files: server/szurubooru/ + exclude: server/szurubooru/migrations/ + pass_filenames: false + verbose: true + stages: [manual] + +fail_fast: true exclude: LICENSE.md diff --git a/client/.eslintrc.yml b/client/.eslintrc.yml index a176248..a9d703f 100644 --- a/client/.eslintrc.yml +++ b/client/.eslintrc.yml @@ -2,7 +2,7 @@ env: browser: true commonjs: true es6: true -extends: 'eslint:recommended' +extends: 'prettier' globals: Atomics: readonly SharedArrayBuffer: readonly @@ -10,284 +10,3 @@ ignorePatterns: - build.js parserOptions: ecmaVersion: 11 -rules: - accessor-pairs: error - array-bracket-newline: error - array-bracket-spacing: - - error - - never - array-callback-return: error - array-element-newline: 'off' - arrow-body-style: 'off' - arrow-parens: - - error - - as-needed - arrow-spacing: - - error - - after: true - before: true - block-scoped-var: error - block-spacing: error - brace-style: - - error - - 1tbs - callback-return: 'off' - camelcase: error - class-methods-use-this: 'off' - comma-dangle: 'off' - comma-spacing: - - error - - after: true - before: false - comma-style: - - error - - last - complexity: 'off' - computed-property-spacing: - - error - - never - consistent-return: 'off' - consistent-this: 'off' - curly: error - default-case: error - default-case-last: error - default-param-last: error - dot-location: - - error - - property - dot-notation: - - error - - allowKeywords: true - eol-last: error - eqeqeq: error - func-call-spacing: error - func-name-matching: error - func-names: error - func-style: - - error - - declaration - - allowArrowFunctions: true - function-call-argument-newline: - - error - - consistent - function-paren-newline: 'off' - generator-star-spacing: error - global-require: 'off' - grouped-accessor-pairs: 'off' - guard-for-in: error - handle-callback-err: error - id-blacklist: error - id-length: 'off' - id-match: error - implicit-arrow-linebreak: - - error - - beside - indent: - - error - - 4 - indent-legacy: 'off' - init-declarations: error - jsx-quotes: error - key-spacing: error - keyword-spacing: - - error - - after: true - before: true - line-comment-position: 'off' - linebreak-style: - - error - - unix - lines-around-comment: error - lines-around-directive: error - lines-between-class-members: - - error - - always - max-classes-per-file: 'off' - max-depth: error - max-len: 'off' - max-lines: 'off' - max-lines-per-function: 'off' - max-nested-callbacks: error - max-params: 'off' - max-statements: 'off' - max-statements-per-line: error - multiline-comment-style: - - error - - separate-lines - multiline-ternary: 'off' - new-cap: error - new-parens: error - newline-after-var: 'off' - newline-before-return: 'off' - newline-per-chained-call: 'off' - no-alert: 'off' - no-array-constructor: error - no-await-in-loop: error - no-bitwise: 'off' - no-buffer-constructor: 'off' - no-caller: error - no-catch-shadow: error - no-confusing-arrow: error - no-console: error - no-constructor-return: error - no-continue: 'off' - no-div-regex: 'off' - no-duplicate-imports: error - no-else-return: 'off' - no-empty-function: 'off' - no-eq-null: error - no-eval: error - no-extend-native: error - no-extra-bind: error - no-extra-label: error - no-extra-parens: 'off' - no-floating-decimal: error - no-implicit-globals: error - no-implied-eval: error - no-inline-comments: 'off' - no-invalid-this: error - no-iterator: error - no-label-var: error - no-labels: error - no-lone-blocks: error - no-lonely-if: error - no-loop-func: 'off' - no-loss-of-precision: error - no-magic-numbers: 'off' - no-mixed-operators: error - no-mixed-requires: error - no-multi-assign: error - no-multi-spaces: - - error - - ignoreEOLComments: true - no-multi-str: error - no-multiple-empty-lines: error - no-native-reassign: error - no-negated-condition: 'off' - no-negated-in-lhs: error - no-nested-ternary: error - no-new: 'off' - no-new-func: error - no-new-object: error - no-new-require: error - no-new-wrappers: error - no-octal-escape: error - no-param-reassign: 'off' - no-path-concat: error - no-plusplus: 'off' - no-process-env: error - no-process-exit: error - no-proto: error - no-restricted-exports: error - no-restricted-globals: error - no-restricted-imports: error - no-restricted-modules: error - no-restricted-properties: error - no-restricted-syntax: error - no-return-assign: error - no-return-await: error - no-script-url: error - no-self-compare: error - no-sequences: error - no-shadow: 'off' - no-spaced-func: error - no-sync: error - no-tabs: error - no-template-curly-in-string: error - no-ternary: 'off' - no-throw-literal: 'off' - no-trailing-spaces: error - no-undef-init: error - no-undefined: 'off' - no-underscore-dangle: 'off' - no-unmodified-loop-condition: error - no-unneeded-ternary: error - no-unused-expressions: error - no-unused-vars: 'off' - no-use-before-define: 'off' - no-useless-backreference: error - no-useless-call: error - no-useless-computed-key: error - no-useless-concat: error - no-useless-constructor: error - no-useless-escape: 'off' - no-useless-rename: error - no-useless-return: error - no-var: 'off' - no-void: error - no-warning-comments: warn - no-whitespace-before-property: error - nonblock-statement-body-position: error - object-curly-newline: error - object-curly-spacing: - - error - - never - object-shorthand: 'off' - one-var: 'off' - one-var-declaration-per-line: error - operator-assignment: - - error - - always - operator-linebreak: 'off' - padded-blocks: 'off' - padding-line-between-statements: error - prefer-arrow-callback: error - prefer-const: 'off' - prefer-destructuring: 'off' - prefer-exponentiation-operator: 'off' - prefer-named-capture-group: 'off' - prefer-numeric-literals: error - prefer-object-spread: 'off' - prefer-promise-reject-errors: 'off' - prefer-reflect: 'off' - prefer-regex-literals: warn - prefer-rest-params: 'off' - prefer-spread: 'off' - prefer-template: 'off' - quote-props: 'off' - quotes: 'off' - radix: - - error - - as-needed - require-atomic-updates: error - require-await: error - require-jsdoc: 'off' - require-unicode-regexp: 'off' - rest-spread-spacing: error - semi: 'off' - semi-spacing: - - error - - after: true - before: false - semi-style: - - error - - last - sort-imports: error - sort-keys: 'off' - sort-vars: error - space-before-blocks: error - space-before-function-paren: 'off' - space-in-parens: - - error - - never - space-infix-ops: error - space-unary-ops: error - spaced-comment: - - error - - always - strict: error - switch-colon-spacing: error - symbol-description: error - template-curly-spacing: - - error - - never - template-tag-spacing: error - unicode-bom: - - error - - never - valid-jsdoc: error - vars-on-top: error - wrap-iife: error - wrap-regex: error - yield-star-spacing: error - yoda: 'off' diff --git a/client/.prettierrc.yml b/client/.prettierrc.yml new file mode 100644 index 0000000..925bde8 --- /dev/null +++ b/client/.prettierrc.yml @@ -0,0 +1,4 @@ +parser: babel +printWidth: 79 +tabWidth: 4 +quoteProps: consistent diff --git a/client/js/api.js b/client/js/api.js index befe02f..5bde6d8 100644 --- a/client/js/api.js +++ b/client/js/api.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; -const cookies = require('js-cookie'); -const request = require('superagent'); -const events = require('./events.js'); -const progress = require('./util/progress.js'); -const uri = require('./util/uri.js'); +const cookies = require("js-cookie"); +const request = require("superagent"); +const events = require("./events.js"); +const progress = require("./util/progress.js"); +const uri = require("./util/uri.js"); let fileTokens = {}; let remoteConfig = null; @@ -18,22 +18,22 @@ class Api extends events.EventTarget { this.token = null; this.cache = {}; this.allRanks = [ - 'anonymous', - 'restricted', - 'regular', - 'power', - 'moderator', - 'administrator', - 'nobody', + "anonymous", + "restricted", + "regular", + "power", + "moderator", + "administrator", + "nobody", ]; this.rankNames = new Map([ - ['anonymous', 'Anonymous'], - ['restricted', 'Restricted user'], - ['regular', 'Regular user'], - ['power', 'Power user'], - ['moderator', 'Moderator'], - ['administrator', 'Administrator'], - ['nobody', 'Nobody'], + ["anonymous", "Anonymous"], + ["restricted", "Restricted user"], + ["regular", "Regular user"], + ["power", "Power user"], + ["moderator", "Moderator"], + ["administrator", "Administrator"], + ["nobody", "Nobody"], ]); } @@ -43,11 +43,12 @@ class Api extends events.EventTarget { resolve(this.cache[url]); }); } - return this._wrappedRequest(url, request.get, {}, {}, options) - .then(response => { + return this._wrappedRequest(url, request.get, {}, {}, options).then( + (response) => { this.cache[url] = response; return Promise.resolve(response); - }); + } + ); } post(url, data, files, options) { @@ -67,10 +68,9 @@ class Api extends events.EventTarget { fetchConfig() { if (remoteConfig === null) { - return this.get(uri.formatApiLink('info')) - .then(response => { - remoteConfig = response.config; - }); + return this.get(uri.formatApiLink("info")).then((response) => { + remoteConfig = response.config; + }); } else { return Promise.resolve(); } @@ -115,7 +115,8 @@ class Api extends events.EventTarget { continue; } const rankIndex = this.allRanks.indexOf( - remoteConfig.privileges[p]); + remoteConfig.privileges[p] + ); if (minViableRank === null || rankIndex < minViableRank) { minViableRank = rankIndex; } @@ -123,17 +124,16 @@ class Api extends events.EventTarget { if (minViableRank === null) { throw `Bad privilege name: ${lookup}`; } - let myRank = this.user !== null ? - this.allRanks.indexOf(this.user.rank) : - 0; + let myRank = + this.user !== null ? this.allRanks.indexOf(this.user.rank) : 0; return myRank >= minViableRank; } loginFromCookies() { - const auth = cookies.getJSON('auth'); - return auth && auth.user && auth.token ? - this.loginWithToken(auth.user, auth.token, true) : - Promise.resolve(); + const auth = cookies.getJSON("auth"); + return auth && auth.user && auth.token + ? this.loginWithToken(auth.user, auth.token, true) + : Promise.resolve(); } loginWithToken(userName, token, doRemember) { @@ -141,63 +141,74 @@ class Api extends events.EventTarget { return new Promise((resolve, reject) => { this.userName = userName; this.token = token; - this.get('/user/' + userName + '?bump-login=true') - .then(response => { + this.get("/user/" + userName + "?bump-login=true").then( + (response) => { const options = {}; if (doRemember) { options.expires = 365; } cookies.set( - 'auth', - {'user': userName, 'token': token}, - options); + "auth", + { user: userName, token: token }, + options + ); this.user = response; resolve(); - this.dispatchEvent(new CustomEvent('login')); - }, error => { + this.dispatchEvent(new CustomEvent("login")); + }, + (error) => { reject(error); this.logout(); - }); + } + ); }); } createToken(userName, options) { let userTokenRequest = { enabled: true, - note: 'Web Login Token' + note: "Web Login Token", }; - if (typeof options.expires !== 'undefined') { - userTokenRequest.expirationTime = new Date().addDays(options.expires).toISOString() + if (typeof options.expires !== "undefined") { + userTokenRequest.expirationTime = new Date() + .addDays(options.expires) + .toISOString(); } return new Promise((resolve, reject) => { - this.post('/user-token/' + userName, userTokenRequest) - .then(response => { + this.post("/user-token/" + userName, userTokenRequest).then( + (response) => { cookies.set( - 'auth', - {'user': userName, 'token': response.token}, - options); + "auth", + { user: userName, token: response.token }, + options + ); this.userName = userName; this.token = response.token; this.userPassword = null; - }, error => { + }, + (error) => { reject(error); - }); + } + ); }); } deleteToken(userName, userToken) { return new Promise((resolve, reject) => { - this.delete('/user-token/' + userName + '/' + userToken, {}) - .then(response => { + this.delete("/user-token/" + userName + "/" + userToken, {}).then( + (response) => { const options = {}; cookies.set( - 'auth', - {'user': userName, 'token': null}, - options); + "auth", + { user: userName, token: null }, + options + ); resolve(); - }, error => { + }, + (error) => { reject(error); - }); + } + ); }); } @@ -206,8 +217,8 @@ class Api extends events.EventTarget { return new Promise((resolve, reject) => { this.userName = userName; this.userPassword = userPassword; - this.get('/user/' + userName + '?bump-login=true') - .then(response => { + this.get("/user/" + userName + "?bump-login=true").then( + (response) => { const options = {}; if (doRemember) { options.expires = 365; @@ -215,22 +226,26 @@ class Api extends events.EventTarget { this.createToken(this.userName, options); this.user = response; resolve(); - this.dispatchEvent(new CustomEvent('login')); - }, error => { + this.dispatchEvent(new CustomEvent("login")); + }, + (error) => { reject(error); this.logout(); - }); + } + ); }); } logout() { let self = this; - this.deleteToken(this.userName, this.token) - .then(response => { + this.deleteToken(this.userName, this.token).then( + (response) => { self._logout(); - }, error => { + }, + (error) => { self._logout(); - }); + } + ); } _logout() { @@ -238,17 +253,19 @@ class Api extends events.EventTarget { this.userName = null; this.userPassword = null; this.token = null; - this.dispatchEvent(new CustomEvent('logout')); + this.dispatchEvent(new CustomEvent("logout")); } forget() { - cookies.remove('auth'); + cookies.remove("auth"); } isLoggedIn(user) { if (user) { - return this.userName !== null && - this.userName.toLowerCase() === user.name.toLowerCase(); + return ( + this.userName !== null && + this.userName.toLowerCase() === user.name.toLowerCase() + ); } else { return this.userName !== null; } @@ -259,8 +276,7 @@ class Api extends events.EventTarget { } _getFullUrl(url) { - const fullUrl = - ('api/' + url).replace(/([^:])\/+/g, '$1/'); + const fullUrl = ("api/" + url).replace(/([^:])\/+/g, "$1/"); const matches = fullUrl.match(/^([^?]*)\??(.*)$/); const baseUrl = matches[1]; const request = matches[2]; @@ -285,7 +301,7 @@ class Api extends events.EventTarget { const file = files[key]; const fileId = this._getFileId(file); if (fileTokens[fileId]) { - data[key + 'Token'] = fileTokens[fileId]; + data[key + "Token"] = fileTokens[fileId]; } else { promise = promise .then(() => { @@ -293,33 +309,40 @@ class Api extends events.EventTarget { abortFunction = () => uploadPromise.abort(); return uploadPromise; }) - .then(token => { + .then((token) => { abortFunction = () => {}; fileTokens[fileId] = token; - data[key + 'Token'] = token; + data[key + "Token"] = token; return Promise.resolve(); }); } } } - promise = promise.then( - () => { + promise = promise + .then(() => { let requestPromise = this._rawRequest( - url, requestFactory, data, {}, options); + url, + requestFactory, + data, + {}, + options + ); abortFunction = () => requestPromise.abort(); return requestPromise; }) - .catch(error => { - if (error.response && error.response.name === - 'MissingOrExpiredRequiredFileError') { + .catch((error) => { + if ( + error.response && + error.response.name === "MissingOrExpiredRequiredFileError" + ) { for (let key of Object.keys(files)) { const file = files[key]; const fileId = this._getFileId(file); fileTokens[fileId] = null; } error.message = - 'The uploaded file has expired; ' + - 'please resend the form to reupload.'; + "The uploaded file has expired; " + + "please resend the form to reupload."; } return Promise.reject(error); }); @@ -331,13 +354,17 @@ class Api extends events.EventTarget { let abortFunction = () => {}; let returnedPromise = new Promise((resolve, reject) => { let uploadPromise = this._rawRequest( - 'uploads', request.post, {}, {content: file}, options); + "uploads", + request.post, + {}, + { content: file }, + options + ); abortFunction = () => uploadPromise.abort(); - return uploadPromise.then( - response => { - abortFunction = () => {}; - return resolve(response.token); - }, reject); + return uploadPromise.then((response) => { + abortFunction = () => {}; + return resolve(response.token); + }, reject); }); returnedPromise.abort = () => abortFunction(); return returnedPromise; @@ -352,7 +379,7 @@ class Api extends events.EventTarget { let returnedPromise = new Promise((resolve, reject) => { let req = requestFactory(fullUrl); - req.set('Accept', 'application/json'); + req.set("Accept", "application/json"); if (query) { req.query(query); @@ -362,7 +389,7 @@ class Api extends events.EventTarget { for (let key of Object.keys(files)) { const value = files[key]; if (value.constructor === String) { - data[key + 'Url'] = value; + data[key + "Url"] = value; } else { req.attach(key, value || new Blob()); } @@ -371,9 +398,9 @@ class Api extends events.EventTarget { if (data) { if (files && Object.keys(files).length) { - req.attach('metadata', new Blob([JSON.stringify(data)])); + req.attach("metadata", new Blob([JSON.stringify(data)])); } else { - req.set('Content-Type', 'application/json'); + req.set("Content-Type", "application/json"); req.send(data); } } @@ -382,19 +409,28 @@ class Api extends events.EventTarget { if (this.userName && this.token) { req.auth = null; // eslint-disable-next-line no-undef - req.set('Authorization', 'Token ' + new Buffer( - this.userName + ":" + this.token).toString('base64')) + req.set( + "Authorization", + "Token " + + new Buffer( + this.userName + ":" + this.token + ).toString("base64") + ); } else if (this.userName && this.userPassword) { req.auth( this.userName, - encodeURIComponent(this.userPassword) - .replace(/%([0-9A-F]{2})/g, (match, p1) => { - return String.fromCharCode('0x' + p1); - })); + encodeURIComponent(this.userPassword).replace( + /%([0-9A-F]{2})/g, + (match, p1) => { + return String.fromCharCode("0x" + p1); + } + ) + ); } } catch (e) { reject( - new Error('Authentication error (malformed credentials)')); + new Error("Authentication error (malformed credentials)") + ); } if (!options.noProgress) { @@ -402,10 +438,11 @@ class Api extends events.EventTarget { } abortFunction = () => { - req.abort(); // does *NOT* call the callback passed in .end() + req.abort(); // does *NOT* call the callback passed in .end() progress.done(); reject( - new Error('The request was aborted due to user cancel.')); + new Error("The request was aborted due to user cancel.") + ); }; req.end((error, response) => { @@ -414,7 +451,8 @@ class Api extends events.EventTarget { if (error) { if (response && response.body) { error = new Error( - response.body.description || 'Unknown error'); + response.body.description || "Unknown error" + ); error.response = response.body; } reject(error); diff --git a/client/js/config.js b/client/js/config.js index 4524be6..da8fb8f 100644 --- a/client/js/config.js +++ b/client/js/config.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; -const config = require('./.config.autogen.json'); +const config = require("./.config.autogen.json"); module.exports = config; diff --git a/client/js/controllers/auth_controller.js b/client/js/controllers/auth_controller.js index a6ec97d..e01fb9b 100644 --- a/client/js/controllers/auth_controller.js +++ b/client/js/controllers/auth_controller.js @@ -1,38 +1,40 @@ -'use strict'; +"use strict"; -const router = require('../router.js'); -const api = require('../api.js'); -const tags = require('../tags.js'); -const pools = require('../pools.js'); -const uri = require('../util/uri.js'); -const topNavigation = require('../models/top_navigation.js'); -const LoginView = require('../views/login_view.js'); +const router = require("../router.js"); +const api = require("../api.js"); +const tags = require("../tags.js"); +const pools = require("../pools.js"); +const uri = require("../util/uri.js"); +const topNavigation = require("../models/top_navigation.js"); +const LoginView = require("../views/login_view.js"); class LoginController { constructor() { api.forget(); - topNavigation.activate('login'); - topNavigation.setTitle('Login'); + topNavigation.activate("login"); + topNavigation.setTitle("Login"); this._loginView = new LoginView(); - this._loginView.addEventListener('submit', e => this._evtLogin(e)); + this._loginView.addEventListener("submit", (e) => this._evtLogin(e)); } _evtLogin(e) { this._loginView.clearMessages(); this._loginView.disableForm(); api.forget(); - api.login(e.detail.name, e.detail.password, e.detail.remember) - .then(() => { + api.login(e.detail.name, e.detail.password, e.detail.remember).then( + () => { const ctx = router.show(uri.formatClientLink()); - ctx.controller.showSuccess('Logged in'); + ctx.controller.showSuccess("Logged in"); // reload tag category color map, this is required when `tag_categories:list` has a permission other than anonymous tags.refreshCategoryColorMap(); pools.refreshCategoryColorMap(); - }, error => { + }, + (error) => { this._loginView.showError(error.message); this._loginView.enableForm(); - }); + } + ); } } @@ -41,15 +43,15 @@ class LogoutController { api.forget(); api.logout(); const ctx = router.show(uri.formatClientLink()); - ctx.controller.showSuccess('Logged out'); + ctx.controller.showSuccess("Logged out"); } } -module.exports = router => { - router.enter(['login'], (ctx, next) => { +module.exports = (router) => { + router.enter(["login"], (ctx, next) => { ctx.controller = new LoginController(); }); - router.enter(['logout'], (ctx, next) => { + router.enter(["logout"], (ctx, next) => { ctx.controller = new LogoutController(); }); }; diff --git a/client/js/controllers/base_post_controller.js b/client/js/controllers/base_post_controller.js index b86aea7..7a21449 100644 --- a/client/js/controllers/base_post_controller.js +++ b/client/js/controllers/base_post_controller.js @@ -1,19 +1,19 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const topNavigation = require('../models/top_navigation.js'); -const EmptyView = require('../views/empty_view.js'); +const api = require("../api.js"); +const topNavigation = require("../models/top_navigation.js"); +const EmptyView = require("../views/empty_view.js"); class BasePostController { constructor(ctx) { - if (!api.hasPrivilege('posts:view')) { + if (!api.hasPrivilege("posts:view")) { 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; } - topNavigation.activate('posts'); - topNavigation.setTitle('Post #' + ctx.parameters.id.toString()); + topNavigation.activate("posts"); + topNavigation.setTitle("Post #" + ctx.parameters.id.toString()); } } diff --git a/client/js/controllers/comments_controller.js b/client/js/controllers/comments_controller.js index 67e44ac..d54059e 100644 --- a/client/js/controllers/comments_controller.js +++ b/client/js/controllers/comments_controller.js @@ -1,51 +1,55 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const PostList = require('../models/post_list.js'); -const topNavigation = require('../models/top_navigation.js'); -const PageController = require('../controllers/page_controller.js'); -const CommentsPageView = require('../views/comments_page_view.js'); -const EmptyView = require('../views/empty_view.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const PostList = require("../models/post_list.js"); +const topNavigation = require("../models/top_navigation.js"); +const PageController = require("../controllers/page_controller.js"); +const CommentsPageView = require("../views/comments_page_view.js"); +const EmptyView = require("../views/empty_view.js"); -const fields = ['id', 'comments', 'commentCount', 'thumbnailUrl']; +const fields = ["id", "comments", "commentCount", "thumbnailUrl"]; class CommentsController { constructor(ctx) { - if (!api.hasPrivilege('comments:list')) { + if (!api.hasPrivilege("comments:list")) { this._view = new EmptyView(); this._view.showError( - 'You don\'t have privileges to view comments.'); + "You don't have privileges to view comments." + ); return; } - topNavigation.activate('comments'); - topNavigation.setTitle('Listing comments'); + topNavigation.activate("comments"); + topNavigation.setTitle("Listing comments"); this._pageController = new PageController(); this._pageController.run({ parameters: ctx.parameters, defaultLimit: 10, getClientUrlForPage: (offset, limit) => { - const parameters = Object.assign( - {}, ctx.parameters, {offset: offset, limit: limit}); - return uri.formatClientLink('comments', parameters); + const parameters = Object.assign({}, ctx.parameters, { + offset: offset, + limit: limit, + }); + return uri.formatClientLink("comments", parameters); }, requestPage: (offset, limit) => { return PostList.search( - 'sort:comment-date comment-count-min:1', + "sort:comment-date comment-count-min:1", offset, limit, - fields); + fields + ); }, - pageRenderer: pageCtx => { + pageRenderer: (pageCtx) => { Object.assign(pageCtx, { - canViewPosts: api.hasPrivilege('posts:view'), + canViewPosts: api.hasPrivilege("posts:view"), }); const view = new CommentsPageView(pageCtx); - view.addEventListener('submit', e => this._evtUpdate(e)); - view.addEventListener('score', e => this._evtScore(e)); - view.addEventListener('delete', e => this._evtDelete(e)); + view.addEventListener("submit", (e) => this._evtUpdate(e)); + view.addEventListener("score", (e) => this._evtScore(e)); + view.addEventListener("delete", (e) => this._evtDelete(e)); return view; }, }); @@ -54,26 +58,27 @@ class CommentsController { _evtUpdate(e) { // TODO: disable form e.detail.comment.text = e.detail.text; - e.detail.comment.save() - .catch(error => { - e.detail.target.showError(error.message); - // TODO: enable form - }); + e.detail.comment.save().catch((error) => { + e.detail.target.showError(error.message); + // TODO: enable form + }); } _evtScore(e) { - e.detail.comment.setScore(e.detail.score) - .catch(error => window.alert(error.message)); + e.detail.comment + .setScore(e.detail.score) + .catch((error) => window.alert(error.message)); } _evtDelete(e) { - e.detail.comment.delete() - .catch(error => window.alert(error.message)); + e.detail.comment + .delete() + .catch((error) => window.alert(error.message)); } } -module.exports = router => { - router.enter(['comments'], (ctx, next) => { +module.exports = (router) => { + router.enter(["comments"], (ctx, next) => { new CommentsController(ctx); }); -} +}; diff --git a/client/js/controllers/help_controller.js b/client/js/controllers/help_controller.js index 8e65346..c290a6b 100644 --- a/client/js/controllers/help_controller.js +++ b/client/js/controllers/help_controller.js @@ -1,24 +1,24 @@ -'use strict'; +"use strict"; -const topNavigation = require('../models/top_navigation.js'); -const HelpView = require('../views/help_view.js'); +const topNavigation = require("../models/top_navigation.js"); +const HelpView = require("../views/help_view.js"); class HelpController { constructor(section, subsection) { - topNavigation.activate('help'); - topNavigation.setTitle('Help'); + topNavigation.activate("help"); + topNavigation.setTitle("Help"); this._helpView = new HelpView(section, subsection); } } -module.exports = router => { - router.enter(['help'], (ctx, next) => { +module.exports = (router) => { + router.enter(["help"], (ctx, next) => { new HelpController(); }); - router.enter(['help', ':section'], (ctx, next) => { + router.enter(["help", ":section"], (ctx, next) => { new HelpController(ctx.parameters.section); }); - router.enter(['help', ':section', ':subsection'], (ctx, next) => { + router.enter(["help", ":section", ":subsection"], (ctx, next) => { new HelpController(ctx.parameters.section, ctx.parameters.subsection); }); }; diff --git a/client/js/controllers/home_controller.js b/client/js/controllers/home_controller.js index 590da67..cf56e27 100644 --- a/client/js/controllers/home_controller.js +++ b/client/js/controllers/home_controller.js @@ -1,26 +1,26 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const config = require('../config.js'); -const Info = require('../models/info.js'); -const topNavigation = require('../models/top_navigation.js'); -const HomeView = require('../views/home_view.js'); +const api = require("../api.js"); +const config = require("../config.js"); +const Info = require("../models/info.js"); +const topNavigation = require("../models/top_navigation.js"); +const HomeView = require("../views/home_view.js"); class HomeController { constructor() { - topNavigation.activate('home'); - topNavigation.setTitle('Home'); + topNavigation.activate("home"); + topNavigation.setTitle("Home"); this._homeView = new HomeView({ name: api.getName(), version: config.meta.version, buildDate: config.meta.buildDate, - canListSnapshots: api.hasPrivilege('snapshots:list'), - canListPosts: api.hasPrivilege('posts:list'), + canListSnapshots: api.hasPrivilege("snapshots:list"), + canListPosts: api.hasPrivilege("posts:list"), }); - Info.get() - .then(info => { + Info.get().then( + (info) => { this._homeView.setStats({ diskUsage: info.diskUsage, postCount: info.postCount, @@ -31,7 +31,8 @@ class HomeController { featuringTime: info.featuringTime, }); }, - error => this._homeView.showError(error.message)); + (error) => this._homeView.showError(error.message) + ); } showSuccess(message) { @@ -43,8 +44,8 @@ class HomeController { } } -module.exports = router => { +module.exports = (router) => { router.enter([], (ctx, next) => { ctx.controller = new HomeController(); }); -} +}; diff --git a/client/js/controllers/not_found_controller.js b/client/js/controllers/not_found_controller.js index 8ceeef0..c99e308 100644 --- a/client/js/controllers/not_found_controller.js +++ b/client/js/controllers/not_found_controller.js @@ -1,18 +1,18 @@ -'use strict'; +"use strict"; -const topNavigation = require('../models/top_navigation.js'); -const NotFoundView = require('../views/not_found_view.js'); +const topNavigation = require("../models/top_navigation.js"); +const NotFoundView = require("../views/not_found_view.js"); class NotFoundController { constructor(path) { - topNavigation.activate(''); - topNavigation.setTitle('Not found'); + topNavigation.activate(""); + topNavigation.setTitle("Not found"); this._notFoundView = new NotFoundView(path); } } -module.exports = router => { +module.exports = (router) => { router.enter(null, (ctx, next) => { ctx.controller = new NotFoundController(ctx.canonicalPath); }); -} +}; diff --git a/client/js/controllers/page_controller.js b/client/js/controllers/page_controller.js index ae55804..d3ee7e9 100644 --- a/client/js/controllers/page_controller.js +++ b/client/js/controllers/page_controller.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const settings = require('../models/settings.js'); -const EndlessPageView = require('../views/endless_page_view.js'); -const ManualPageView = require('../views/manual_page_view.js'); +const settings = require("../models/settings.js"); +const EndlessPageView = require("../views/endless_page_view.js"); +const ManualPageView = require("../views/manual_page_view.js"); class PageController { constructor(ctx) { diff --git a/client/js/controllers/password_reset_controller.js b/client/js/controllers/password_reset_controller.js index e0a9801..aa66c03 100644 --- a/client/js/controllers/password_reset_controller.js +++ b/client/js/controllers/password_reset_controller.js @@ -1,19 +1,20 @@ -'use strict'; +"use strict"; -const router = require('../router.js'); -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const topNavigation = require('../models/top_navigation.js'); -const PasswordResetView = require('../views/password_reset_view.js'); +const router = require("../router.js"); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const topNavigation = require("../models/top_navigation.js"); +const PasswordResetView = require("../views/password_reset_view.js"); class PasswordResetController { constructor() { - topNavigation.activate('login'); - topNavigation.setTitle('Password reminder'); + topNavigation.activate("login"); + topNavigation.setTitle("Password reminder"); this._passwordResetView = new PasswordResetView(); - this._passwordResetView.addEventListener( - 'submit', e => this._evtReset(e)); + this._passwordResetView.addEventListener("submit", (e) => + this._evtReset(e) + ); } _evtReset(e) { @@ -21,15 +22,20 @@ class PasswordResetController { this._passwordResetView.disableForm(); api.forget(); api.logout(); - api.get(uri.formatApiLink('password-reset', e.detail.userNameOrEmail)) - .then(() => { + api.get( + uri.formatApiLink("password-reset", e.detail.userNameOrEmail) + ).then( + () => { this._passwordResetView.showSuccess( - 'E-mail has been sent. To finish the procedure, ' + - 'please click the link it contains.'); - }, error => { + "E-mail has been sent. To finish the procedure, " + + "please click the link it contains." + ); + }, + (error) => { this._passwordResetView.showError(error.message); this._passwordResetView.enableForm(); - }); + } + ); } } @@ -38,26 +44,30 @@ class PasswordResetFinishController { api.forget(); api.logout(); let password = null; - api.post(uri.formatApiLink('password-reset', name), {token: token}) - .then(response => { + api.post(uri.formatApiLink("password-reset", name), { token: token }) + .then((response) => { password = response.password; return api.login(name, password, false); - }).then(() => { - const ctx = router.show(uri.formatClientLink()); - ctx.controller.showSuccess('New password: ' + password); - }, error => { - const ctx = router.show(uri.formatClientLink()); - ctx.controller.showError(error.message); - }); + }) + .then( + () => { + const ctx = router.show(uri.formatClientLink()); + ctx.controller.showSuccess("New password: " + password); + }, + (error) => { + const ctx = router.show(uri.formatClientLink()); + ctx.controller.showError(error.message); + } + ); } } -module.exports = router => { - router.enter(['password-reset'], (ctx, next) => { +module.exports = (router) => { + router.enter(["password-reset"], (ctx, next) => { ctx.controller = new PasswordResetController(); }); - router.enter(['password-reset', ':descriptor'], (ctx, next) => { - const [name, token] = ctx.parameters.descriptor.split(':', 2); + router.enter(["password-reset", ":descriptor"], (ctx, next) => { + const [name, token] = ctx.parameters.descriptor.split(":", 2); ctx.controller = new PasswordResetFinishController(name, token); }); }; diff --git a/client/js/controllers/pool_categories_controller.js b/client/js/controllers/pool_categories_controller.js index 4c2c3b7..4db725c 100644 --- a/client/js/controllers/pool_categories_controller.js +++ b/client/js/controllers/pool_categories_controller.js @@ -1,57 +1,69 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const pools = require('../pools.js'); -const PoolCategoryList = require('../models/pool_category_list.js'); -const topNavigation = require('../models/top_navigation.js'); -const PoolCategoriesView = require('../views/pool_categories_view.js'); -const EmptyView = require('../views/empty_view.js'); +const api = require("../api.js"); +const pools = require("../pools.js"); +const PoolCategoryList = require("../models/pool_category_list.js"); +const topNavigation = require("../models/top_navigation.js"); +const PoolCategoriesView = require("../views/pool_categories_view.js"); +const EmptyView = require("../views/empty_view.js"); class PoolCategoriesController { constructor() { - if (!api.hasPrivilege('poolCategories:list')) { + if (!api.hasPrivilege("poolCategories:list")) { this._view = new EmptyView(); this._view.showError( - 'You don\'t have privileges to view pool categories.'); + "You don't have privileges to view pool categories." + ); return; } - topNavigation.activate('pools'); - topNavigation.setTitle('Listing pools'); - PoolCategoryList.get().then(response => { - this._poolCategories = response.results; - this._view = new PoolCategoriesView({ - poolCategories: this._poolCategories, - canEditName: api.hasPrivilege('poolCategories:edit:name'), - canEditColor: api.hasPrivilege('poolCategories:edit:color'), - canDelete: api.hasPrivilege('poolCategories:delete'), - canCreate: api.hasPrivilege('poolCategories:create'), - canSetDefault: api.hasPrivilege('poolCategories:setDefault'), - }); - this._view.addEventListener('submit', e => this._evtSubmit(e)); - }, error => { - this._view = new EmptyView(); - this._view.showError(error.message); - }); + topNavigation.activate("pools"); + topNavigation.setTitle("Listing pools"); + PoolCategoryList.get().then( + (response) => { + this._poolCategories = response.results; + this._view = new PoolCategoriesView({ + poolCategories: this._poolCategories, + canEditName: api.hasPrivilege("poolCategories:edit:name"), + canEditColor: api.hasPrivilege( + "poolCategories:edit:color" + ), + canDelete: api.hasPrivilege("poolCategories:delete"), + canCreate: api.hasPrivilege("poolCategories:create"), + canSetDefault: api.hasPrivilege( + "poolCategories:setDefault" + ), + }); + this._view.addEventListener("submit", (e) => + this._evtSubmit(e) + ); + }, + (error) => { + this._view = new EmptyView(); + this._view.showError(error.message); + } + ); } _evtSubmit(e) { this._view.clearMessages(); this._view.disableForm(); - this._poolCategories.save() - .then(() => { + this._poolCategories.save().then( + () => { pools.refreshCategoryColorMap(); this._view.enableForm(); - this._view.showSuccess('Changes saved.'); - }, error => { + this._view.showSuccess("Changes saved."); + }, + (error) => { this._view.enableForm(); this._view.showError(error.message); - }); + } + ); } } -module.exports = router => { - router.enter(['pool-categories'], (ctx, next) => { +module.exports = (router) => { + router.enter(["pool-categories"], (ctx, next) => { ctx.controller = new PoolCategoriesController(ctx, next); }); }; diff --git a/client/js/controllers/pool_controller.js b/client/js/controllers/pool_controller.js index a06ba5c..6844171 100644 --- a/client/js/controllers/pool_controller.js +++ b/client/js/controllers/pool_controller.js @@ -1,63 +1,76 @@ -'use strict'; +"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 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'); -const EmptyView = require('../views/empty_view.js'); +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 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"); +const EmptyView = require("../views/empty_view.js"); class PoolController { constructor(ctx, section) { - if (!api.hasPrivilege('pools:view')) { + if (!api.hasPrivilege("pools:view")) { 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; } Promise.all([ PoolCategoryList.get(), - Pool.get(ctx.parameters.id) - ]).then(responses => { - const [poolCategoriesResponse, pool] = responses; + Pool.get(ctx.parameters.id), + ]).then( + (responses) => { + const [poolCategoriesResponse, pool] = responses; - topNavigation.activate('pools'); - topNavigation.setTitle('Pool #' + pool.names[0]); + topNavigation.activate("pools"); + topNavigation.setTitle("Pool #" + pool.names[0]); - this._name = ctx.parameters.name; - pool.addEventListener('change', e => this._evtSaved(e, section)); + 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; - } + 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'), - canEditPosts: api.hasPrivilege('pools:edit:posts'), - canMerge: api.hasPrivilege('pools:merge'), - canDelete: api.hasPrivilege('pools:delete'), - categories: categories, - escapeColons: uri.escapeColons, - }); + 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" + ), + canEditPosts: api.hasPrivilege("pools:edit:posts"), + 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); - }); + 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) { @@ -67,7 +80,11 @@ 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 + ); } } @@ -86,62 +103,74 @@ class PoolController { if (e.detail.posts !== undefined) { e.detail.pool.posts.clear(); for (let postId of e.detail.posts) { - e.detail.pool.posts.add(Post.fromResponse({id: parseInt(postId)})); + e.detail.pool.posts.add( + Post.fromResponse({ id: parseInt(postId) }) + ); } } - e.detail.pool.save().then(() => { - this._view.showSuccess('Pool saved.'); - this._view.enableForm(); - }, error => { - this._view.showError(error.message); - this._view.enableForm(); - }); + 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.targetPoolId, e.detail.addAlias) - .then(() => { - this._view.showSuccess('Pool merged.'); + e.detail.pool.merge(e.detail.targetPoolId, e.detail.addAlias).then( + () => { + this._view.showSuccess("Pool merged."); this._view.enableForm(); router.replace( uri.formatClientLink( - 'pool', e.detail.targetPoolId, 'merge'), + "pool", + e.detail.targetPoolId, + "merge" + ), null, - false); - }, error => { + 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 => { + 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'); +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", "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", "delete"], (ctx, next) => { + ctx.controller = new PoolController(ctx, "delete"); }); - router.enter(['pool', ':id'], (ctx, next) => { - ctx.controller = new PoolController(ctx, 'summary'); + router.enter(["pool", ":id"], (ctx, next) => { + ctx.controller = new PoolController(ctx, "summary"); }); }; diff --git a/client/js/controllers/pool_create_controller.js b/client/js/controllers/pool_create_controller.js index 2b8e7e6..7140aac 100644 --- a/client/js/controllers/pool_create_controller.js +++ b/client/js/controllers/pool_create_controller.js @@ -1,58 +1,65 @@ -'use strict'; +"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 PoolCategoryList = require('../models/pool_category_list.js'); -const PoolCreateView = require('../views/pool_create_view.js'); -const EmptyView = require('../views/empty_view.js'); +const router = require("../router.js"); +const api = require("../api.js"); +const misc = require("../util/misc.js"); +const uri = require("../util/uri.js"); +const PoolCategoryList = require("../models/pool_category_list.js"); +const PoolCreateView = require("../views/pool_create_view.js"); +const EmptyView = require("../views/empty_view.js"); class PoolCreateController { constructor(ctx) { - if (!api.hasPrivilege('pools:create')) { + if (!api.hasPrivilege("pools:create")) { this._view = new EmptyView(); - this._view.showError('You don\'t have privileges to create pools.'); + this._view.showError("You don't have privileges to create pools."); return; } - PoolCategoryList.get().then(poolCategoriesResponse => { - const categories = {}; - for (let category of poolCategoriesResponse.results) { - categories[category.name] = category.name; - } + PoolCategoryList.get().then( + (poolCategoriesResponse) => { + const categories = {}; + for (let category of poolCategoriesResponse.results) { + categories[category.name] = category.name; + } - this._view = new PoolCreateView({ - canCreate: api.hasPrivilege('pools:create'), - categories: categories, - escapeColons: uri.escapeColons, - }); + this._view = new PoolCreateView({ + canCreate: api.hasPrivilege("pools:create"), + categories: categories, + escapeColons: uri.escapeColons, + }); - this._view.addEventListener('submit', e => this._evtCreate(e)); - }, error => { - this._view = new EmptyView(); - this._view.showError(error.message); - }); + this._view.addEventListener("submit", (e) => + this._evtCreate(e) + ); + }, + (error) => { + this._view = new EmptyView(); + this._view.showError(error.message); + } + ); } _evtCreate(e) { this._view.clearMessages(); this._view.disableForm(); - api.post(uri.formatApiLink('pool'), e.detail) - .then(() => { + api.post(uri.formatApiLink("pool"), e.detail).then( + () => { this._view.clearMessages(); misc.disableExitConfirmation(); - const ctx = router.show(uri.formatClientLink('pools')); - ctx.controller.showSuccess('Pool created.'); - }, error => { + const ctx = router.show(uri.formatClientLink("pools")); + ctx.controller.showSuccess("Pool created."); + }, + (error) => { this._view.showError(error.message); this._view.enableForm(); - }); + } + ); } } -module.exports = router => { - router.enter(['pool', 'create'], (ctx, next) => { - ctx.controller = new PoolCreateController(ctx, 'create'); +module.exports = (router) => { + router.enter(["pool", "create"], (ctx, next) => { + ctx.controller = new PoolCreateController(ctx, "create"); }); }; diff --git a/client/js/controllers/pool_list_controller.js b/client/js/controllers/pool_list_controller.js index dff649f..91d655c 100644 --- a/client/js/controllers/pool_list_controller.js +++ b/client/js/controllers/pool_list_controller.js @@ -1,47 +1,51 @@ -'use strict'; +"use strict"; -const router = require('../router.js'); -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const PoolList = require('../models/pool_list.js'); -const topNavigation = require('../models/top_navigation.js'); -const PageController = require('../controllers/page_controller.js'); -const PoolsHeaderView = require('../views/pools_header_view.js'); -const PoolsPageView = require('../views/pools_page_view.js'); -const EmptyView = require('../views/empty_view.js'); +const router = require("../router.js"); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const PoolList = require("../models/pool_list.js"); +const topNavigation = require("../models/top_navigation.js"); +const PageController = require("../controllers/page_controller.js"); +const PoolsHeaderView = require("../views/pools_header_view.js"); +const PoolsPageView = require("../views/pools_page_view.js"); +const EmptyView = require("../views/empty_view.js"); const fields = [ - 'id', - 'names', - 'posts', - 'creationTime', - 'postCount', - 'category' + "id", + "names", + "posts", + "creationTime", + "postCount", + "category", ]; class PoolListController { constructor(ctx) { this._pageController = new PageController(); - if (!api.hasPrivilege('pools:list')) { + if (!api.hasPrivilege("pools:list")) { 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; } this._ctx = ctx; - topNavigation.activate('pools'); - topNavigation.setTitle('Listing pools'); + topNavigation.activate("pools"); + topNavigation.setTitle("Listing pools"); this._headerView = new PoolsHeaderView({ hostNode: this._pageController.view.pageHeaderHolderNode, parameters: ctx.parameters, - canCreate: api.hasPrivilege('pools:create'), - canEditPoolCategories: api.hasPrivilege('poolCategories:edit'), + canCreate: api.hasPrivilege("pools:create"), + canEditPoolCategories: api.hasPrivilege("poolCategories:edit"), }); this._headerView.addEventListener( - 'submit', e => this._evtSubmit(e), 'navigate', e => this._evtNavigate(e)); + "submit", + (e) => this._evtSubmit(e), + "navigate", + (e) => this._evtNavigate(e) + ); this._syncPageController(); } @@ -57,24 +61,27 @@ class PoolListController { _evtSubmit(e) { this._view.clearMessages(); this._view.disableForm(); - e.detail.pool.save() - .then(() => { - this._installView(e.detail.pool, 'edit'); - this._view.showSuccess('Pool created.'); + e.detail.pool.save().then( + () => { + this._installView(e.detail.pool, "edit"); + this._view.showSuccess("Pool created."); router.replace( - uri.formatClientLink( - 'pool', e.detail.pool.id, 'edit'), + uri.formatClientLink("pool", e.detail.pool.id, "edit"), null, - false); - }, error => { + false + ); + }, + (error) => { this._view.showError(error.message); this._view.enableForm(); - }); + } + ); } _evtNavigate(e) { router.showNoDispatch( - uri.formatClientLink('pools', e.detail.parameters)); + uri.formatClientLink("pools", e.detail.parameters) + ); Object.assign(this._ctx.parameters, e.detail.parameters); this._syncPageController(); } @@ -84,25 +91,29 @@ class PoolListController { parameters: this._ctx.parameters, defaultLimit: 50, getClientUrlForPage: (offset, limit) => { - const parameters = Object.assign( - {}, this._ctx.parameters, {offset: offset, limit: limit}); - return uri.formatClientLink('pools', parameters); + const parameters = Object.assign({}, this._ctx.parameters, { + offset: offset, + limit: limit, + }); + return uri.formatClientLink("pools", parameters); }, requestPage: (offset, limit) => { return PoolList.search( - this._ctx.parameters.query, offset, limit, fields); + this._ctx.parameters.query, + offset, + limit, + fields + ); }, - pageRenderer: pageCtx => { + pageRenderer: (pageCtx) => { return new PoolsPageView(pageCtx); }, }); } } -module.exports = router => { - router.enter( - ['pools'], - (ctx, next) => { - ctx.controller = new PoolListController(ctx); - }); +module.exports = (router) => { + router.enter(["pools"], (ctx, next) => { + ctx.controller = new PoolListController(ctx); + }); }; diff --git a/client/js/controllers/post_detail_controller.js b/client/js/controllers/post_detail_controller.js index bd9c2e6..9bab1ce 100644 --- a/client/js/controllers/post_detail_controller.js +++ b/client/js/controllers/post_detail_controller.js @@ -1,28 +1,33 @@ -'use strict'; +"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 settings = require('../models/settings.js'); -const Post = require('../models/post.js'); -const PostList = require('../models/post_list.js'); -const PostDetailView = require('../views/post_detail_view.js'); -const BasePostController = require('./base_post_controller.js'); -const EmptyView = require('../views/empty_view.js'); +const router = require("../router.js"); +const api = require("../api.js"); +const misc = require("../util/misc.js"); +const uri = require("../util/uri.js"); +const settings = require("../models/settings.js"); +const Post = require("../models/post.js"); +const PostList = require("../models/post_list.js"); +const PostDetailView = require("../views/post_detail_view.js"); +const BasePostController = require("./base_post_controller.js"); +const EmptyView = require("../views/empty_view.js"); class PostDetailController extends BasePostController { constructor(ctx, section) { super(ctx); - Post.get(ctx.parameters.id).then(post => { - this._id = ctx.parameters.id; - post.addEventListener('change', e => this._evtSaved(e, section)); - this._installView(post, section); - }, error => { - this._view = new EmptyView(); - this._view.showError(error.message); - }); + Post.get(ctx.parameters.id).then( + (post) => { + this._id = ctx.parameters.id; + post.addEventListener("change", (e) => + this._evtSaved(e, section) + ); + this._installView(post, section); + }, + (error) => { + this._view = new EmptyView(); + this._view.showError(error.message); + } + ); } showSuccess(message) { @@ -33,58 +38,68 @@ class PostDetailController extends BasePostController { this._view = new PostDetailView({ post: post, section: section, - canMerge: api.hasPrivilege('posts:merge'), + canMerge: api.hasPrivilege("posts:merge"), }); - this._view.addEventListener('select', e => this._evtSelect(e)); - this._view.addEventListener('merge', e => this._evtMerge(e)); + this._view.addEventListener("select", (e) => this._evtSelect(e)); + this._view.addEventListener("merge", (e) => this._evtMerge(e)); } _evtSelect(e) { this._view.clearMessages(); this._view.disableForm(); - Post.get(e.detail.postId).then(post => { - this._view.selectPost(post); - this._view.enableForm(); - }, error => { - this._view.showError(error.message); - this._view.enableForm(); - }); + Post.get(e.detail.postId).then( + (post) => { + this._view.selectPost(post); + this._view.enableForm(); + }, + (error) => { + this._view.showError(error.message); + this._view.enableForm(); + } + ); } _evtSaved(e, section) { misc.disableExitConfirmation(); if (this._id !== e.detail.post.id) { router.replace( - uri.formatClientLink('post', e.detail.post.id, section), + uri.formatClientLink("post", e.detail.post.id, section), null, - false); + false + ); } } _evtMerge(e) { this._view.clearMessages(); this._view.disableForm(); - e.detail.post.merge(e.detail.targetPost.id, e.detail.useOldContent) - .then(() => { - this._installView(e.detail.post, 'merge'); - this._view.showSuccess('Post merged.'); - router.replace( - uri.formatClientLink( - 'post', e.detail.targetPost.id, 'merge'), - null, - false); - }, error => { - this._view.showError(error.message); - this._view.enableForm(); - }); + e.detail.post + .merge(e.detail.targetPost.id, e.detail.useOldContent) + .then( + () => { + this._installView(e.detail.post, "merge"); + this._view.showSuccess("Post merged."); + router.replace( + uri.formatClientLink( + "post", + e.detail.targetPost.id, + "merge" + ), + null, + false + ); + }, + (error) => { + this._view.showError(error.message); + this._view.enableForm(); + } + ); } } -module.exports = router => { - router.enter( - ['post', ':id', 'merge'], - (ctx, next) => { - ctx.controller = new PostDetailController(ctx, 'merge'); - }); +module.exports = (router) => { + router.enter(["post", ":id", "merge"], (ctx, next) => { + ctx.controller = new PostDetailController(ctx, "merge"); + }); }; diff --git a/client/js/controllers/post_list_controller.js b/client/js/controllers/post_list_controller.js index 357386c..941ea9d 100644 --- a/client/js/controllers/post_list_controller.js +++ b/client/js/controllers/post_list_controller.js @@ -1,48 +1,56 @@ -'use strict'; - -const router = require('../router.js'); -const api = require('../api.js'); -const settings = require('../models/settings.js'); -const uri = require('../util/uri.js'); -const PostList = require('../models/post_list.js'); -const topNavigation = require('../models/top_navigation.js'); -const PageController = require('../controllers/page_controller.js'); -const PostsHeaderView = require('../views/posts_header_view.js'); -const PostsPageView = require('../views/posts_page_view.js'); -const EmptyView = require('../views/empty_view.js'); +"use strict"; + +const router = require("../router.js"); +const api = require("../api.js"); +const settings = require("../models/settings.js"); +const uri = require("../util/uri.js"); +const PostList = require("../models/post_list.js"); +const topNavigation = require("../models/top_navigation.js"); +const PageController = require("../controllers/page_controller.js"); +const PostsHeaderView = require("../views/posts_header_view.js"); +const PostsPageView = require("../views/posts_page_view.js"); +const EmptyView = require("../views/empty_view.js"); const fields = [ - 'id', 'thumbnailUrl', 'type', 'safety', - 'score', 'favoriteCount', 'commentCount', 'tags', 'version' + "id", + "thumbnailUrl", + "type", + "safety", + "score", + "favoriteCount", + "commentCount", + "tags", + "version", ]; class PostListController { constructor(ctx) { this._pageController = new PageController(); - if (!api.hasPrivilege('posts:list')) { + if (!api.hasPrivilege("posts:list")) { 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; } this._ctx = ctx; - topNavigation.activate('posts'); - topNavigation.setTitle('Listing posts'); + topNavigation.activate("posts"); + topNavigation.setTitle("Listing posts"); this._headerView = new PostsHeaderView({ hostNode: this._pageController.view.pageHeaderHolderNode, parameters: ctx.parameters, enableSafety: api.safetyEnabled(), - canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'), - canBulkEditSafety: api.hasPrivilege('posts:bulk-edit:safety'), + canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"), + canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"), bulkEdit: { - tags: this._bulkEditTags + tags: this._bulkEditTags, }, }); - this._headerView.addEventListener( - 'navigate', e => this._evtNavigate(e)); + this._headerView.addEventListener("navigate", (e) => + this._evtNavigate(e) + ); this._syncPageController(); } @@ -52,33 +60,35 @@ class PostListController { } get _bulkEditTags() { - return (this._ctx.parameters.tag || '').split(/\s+/).filter(s => s); + return (this._ctx.parameters.tag || "").split(/\s+/).filter((s) => s); } _evtNavigate(e) { router.showNoDispatch( - uri.formatClientLink('posts', e.detail.parameters)); + uri.formatClientLink("posts", e.detail.parameters) + ); Object.assign(this._ctx.parameters, e.detail.parameters); this._syncPageController(); } _evtTag(e) { Promise.all( - this._bulkEditTags.map(tag => e.detail.post.tags.addByName(tag))) + this._bulkEditTags.map((tag) => e.detail.post.tags.addByName(tag)) + ) .then(e.detail.post.save()) - .catch(error => window.alert(error.message)); + .catch((error) => window.alert(error.message)); } _evtUntag(e) { for (let tag of this._bulkEditTags) { e.detail.post.tags.removeByName(tag); } - e.detail.post.save().catch(error => window.alert(error.message)); + e.detail.post.save().catch((error) => window.alert(error.message)); } _evtChangeSafety(e) { e.detail.post.safety = e.detail.safety; - e.detail.post.save().catch(error => window.alert(error.message)); + e.detail.post.save().catch((error) => window.alert(error.message)); } _syncPageController() { @@ -86,39 +96,45 @@ class PostListController { parameters: this._ctx.parameters, defaultLimit: parseInt(settings.get().postsPerPage), getClientUrlForPage: (offset, limit) => { - const parameters = Object.assign( - {}, this._ctx.parameters, {offset: offset, limit: limit}); - return uri.formatClientLink('posts', parameters); + const parameters = Object.assign({}, this._ctx.parameters, { + offset: offset, + limit: limit, + }); + return uri.formatClientLink("posts", parameters); }, requestPage: (offset, limit) => { return PostList.search( - this._ctx.parameters.query, offset, limit, fields); + this._ctx.parameters.query, + offset, + limit, + fields + ); }, - pageRenderer: pageCtx => { + pageRenderer: (pageCtx) => { Object.assign(pageCtx, { - canViewPosts: api.hasPrivilege('posts:view'), - canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'), - canBulkEditSafety: - api.hasPrivilege('posts:bulk-edit:safety'), + canViewPosts: api.hasPrivilege("posts:view"), + canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"), + canBulkEditSafety: api.hasPrivilege( + "posts:bulk-edit:safety" + ), bulkEdit: { tags: this._bulkEditTags, }, }); const view = new PostsPageView(pageCtx); - view.addEventListener('tag', e => this._evtTag(e)); - view.addEventListener('untag', e => this._evtUntag(e)); - view.addEventListener( - 'changeSafety', e => this._evtChangeSafety(e)); + view.addEventListener("tag", (e) => this._evtTag(e)); + view.addEventListener("untag", (e) => this._evtUntag(e)); + view.addEventListener("changeSafety", (e) => + this._evtChangeSafety(e) + ); return view; }, }); } } -module.exports = router => { - router.enter( - ['posts'], - (ctx, next) => { - ctx.controller = new PostListController(ctx); - }); +module.exports = (router) => { + router.enter(["posts"], (ctx, next) => { + ctx.controller = new PostListController(ctx); + }); }; diff --git a/client/js/controllers/post_main_controller.js b/client/js/controllers/post_main_controller.js index 924e918..95cfdb5 100644 --- a/client/js/controllers/post_main_controller.js +++ b/client/js/controllers/post_main_controller.js @@ -1,16 +1,16 @@ -'use strict'; - -const router = require('../router.js'); -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const misc = require('../util/misc.js'); -const settings = require('../models/settings.js'); -const Comment = require('../models/comment.js'); -const Post = require('../models/post.js'); -const PostList = require('../models/post_list.js'); -const PostMainView = require('../views/post_main_view.js'); -const BasePostController = require('./base_post_controller.js'); -const EmptyView = require('../views/empty_view.js'); +"use strict"; + +const router = require("../router.js"); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const misc = require("../util/misc.js"); +const settings = require("../models/settings.js"); +const Comment = require("../models/comment.js"); +const Post = require("../models/post.js"); +const PostList = require("../models/post_list.js"); +const PostMainView = require("../views/post_main_view.js"); +const BasePostController = require("./base_post_controller.js"); +const EmptyView = require("../views/empty_view.js"); class PostMainController extends BasePostController { constructor(ctx, editMode) { @@ -21,74 +21,107 @@ class PostMainController extends BasePostController { Post.get(ctx.parameters.id), PostList.getAround( ctx.parameters.id, - parameters ? parameters.query : null), - ]).then(responses => { - const [post, aroundResponse] = responses; - - // remove junk from query, but save it into history so that it can - // be still accessed after history navigation / page refresh - if (parameters.query) { - ctx.state.parameters = parameters; - const url = editMode ? - uri.formatClientLink('post', ctx.parameters.id, 'edit') : - uri.formatClientLink('post', ctx.parameters.id); - router.replace(url, ctx.state, false); - } + parameters ? parameters.query : null + ), + ]).then( + (responses) => { + const [post, aroundResponse] = responses; - this._post = post; - this._view = new PostMainView({ - post: post, - editMode: editMode, - prevPostId: aroundResponse.prev ? aroundResponse.prev.id : null, - nextPostId: aroundResponse.next ? aroundResponse.next.id : null, - canEditPosts: api.hasPrivilege('posts:edit'), - canDeletePosts: api.hasPrivilege('posts:delete'), - canFeaturePosts: api.hasPrivilege('posts:feature'), - canListComments: api.hasPrivilege('comments:list'), - canCreateComments: api.hasPrivilege('comments:create'), - parameters: parameters, - }); - - if (this._view.sidebarControl) { - this._view.sidebarControl.addEventListener( - 'favorite', e => this._evtFavoritePost(e)); - this._view.sidebarControl.addEventListener( - 'unfavorite', e => this._evtUnfavoritePost(e)); - this._view.sidebarControl.addEventListener( - 'score', e => this._evtScorePost(e)); - this._view.sidebarControl.addEventListener( - 'fitModeChange', e => this._evtFitModeChange(e)); - this._view.sidebarControl.addEventListener( - 'change', e => this._evtPostChange(e)); - this._view.sidebarControl.addEventListener( - 'submit', e => this._evtUpdatePost(e)); - this._view.sidebarControl.addEventListener( - 'feature', e => this._evtFeaturePost(e)); - this._view.sidebarControl.addEventListener( - 'delete', e => this._evtDeletePost(e)); - this._view.sidebarControl.addEventListener( - 'merge', e => this._evtMergePost(e)); - } + // remove junk from query, but save it into history so that it can + // be still accessed after history navigation / page refresh + if (parameters.query) { + ctx.state.parameters = parameters; + const url = editMode + ? uri.formatClientLink( + "post", + ctx.parameters.id, + "edit" + ) + : uri.formatClientLink("post", ctx.parameters.id); + router.replace(url, ctx.state, false); + } - if (this._view.commentControl) { - this._view.commentControl.addEventListener( - 'change', e => this._evtCommentChange(e)); - this._view.commentControl.addEventListener( - 'submit', e => this._evtCreateComment(e)); - } + this._post = post; + this._view = new PostMainView({ + post: post, + editMode: editMode, + prevPostId: aroundResponse.prev + ? aroundResponse.prev.id + : null, + nextPostId: aroundResponse.next + ? aroundResponse.next.id + : null, + canEditPosts: api.hasPrivilege("posts:edit"), + canDeletePosts: api.hasPrivilege("posts:delete"), + canFeaturePosts: api.hasPrivilege("posts:feature"), + canListComments: api.hasPrivilege("comments:list"), + canCreateComments: api.hasPrivilege("comments:create"), + parameters: parameters, + }); + + if (this._view.sidebarControl) { + this._view.sidebarControl.addEventListener( + "favorite", + (e) => this._evtFavoritePost(e) + ); + this._view.sidebarControl.addEventListener( + "unfavorite", + (e) => this._evtUnfavoritePost(e) + ); + this._view.sidebarControl.addEventListener("score", (e) => + this._evtScorePost(e) + ); + this._view.sidebarControl.addEventListener( + "fitModeChange", + (e) => this._evtFitModeChange(e) + ); + this._view.sidebarControl.addEventListener("change", (e) => + this._evtPostChange(e) + ); + this._view.sidebarControl.addEventListener("submit", (e) => + this._evtUpdatePost(e) + ); + this._view.sidebarControl.addEventListener( + "feature", + (e) => this._evtFeaturePost(e) + ); + this._view.sidebarControl.addEventListener("delete", (e) => + this._evtDeletePost(e) + ); + this._view.sidebarControl.addEventListener("merge", (e) => + this._evtMergePost(e) + ); + } - if (this._view.commentListControl) { - this._view.commentListControl.addEventListener( - 'submit', e => this._evtUpdateComment(e)); - this._view.commentListControl.addEventListener( - 'score', e => this._evtScoreComment(e)); - this._view.commentListControl.addEventListener( - 'delete', e => this._evtDeleteComment(e)); + if (this._view.commentControl) { + this._view.commentControl.addEventListener("change", (e) => + this._evtCommentChange(e) + ); + this._view.commentControl.addEventListener("submit", (e) => + this._evtCreateComment(e) + ); + } + + if (this._view.commentListControl) { + this._view.commentListControl.addEventListener( + "submit", + (e) => this._evtUpdateComment(e) + ); + this._view.commentListControl.addEventListener( + "score", + (e) => this._evtScoreComment(e) + ); + this._view.commentListControl.addEventListener( + "delete", + (e) => this._evtDeleteComment(e) + ); + } + }, + (error) => { + this._view = new EmptyView(); + this._view.showError(error.message); } - }, error => { - this._view = new EmptyView(); - this._view.showError(error.message); - }); + ); } _evtFitModeChange(e) { @@ -100,32 +133,36 @@ class PostMainController extends BasePostController { _evtFeaturePost(e) { this._view.sidebarControl.disableForm(); this._view.sidebarControl.clearMessages(); - e.detail.post.feature() - .then(() => { - this._view.sidebarControl.showSuccess('Post featured.'); + e.detail.post.feature().then( + () => { + this._view.sidebarControl.showSuccess("Post featured."); this._view.sidebarControl.enableForm(); - }, error => { + }, + (error) => { this._view.sidebarControl.showError(error.message); this._view.sidebarControl.enableForm(); - }); + } + ); } _evtMergePost(e) { - router.show(uri.formatClientLink('post', e.detail.post.id, 'merge')); + router.show(uri.formatClientLink("post", e.detail.post.id, "merge")); } _evtDeletePost(e) { this._view.sidebarControl.disableForm(); this._view.sidebarControl.clearMessages(); - e.detail.post.delete() - .then(() => { + e.detail.post.delete().then( + () => { misc.disableExitConfirmation(); - const ctx = router.show(uri.formatClientLink('posts')); - ctx.controller.showSuccess('Post deleted.'); - }, error => { + const ctx = router.show(uri.formatClientLink("posts")); + ctx.controller.showSuccess("Post deleted."); + }, + (error) => { this._view.sidebarControl.showError(error.message); this._view.sidebarControl.enableForm(); - }); + } + ); } _evtUpdatePost(e) { @@ -150,15 +187,17 @@ class PostMainController extends BasePostController { if (e.detail.source !== undefined) { post.source = e.detail.source; } - post.save() - .then(() => { - this._view.sidebarControl.showSuccess('Post saved.'); + post.save().then( + () => { + this._view.sidebarControl.showSuccess("Post saved."); this._view.sidebarControl.enableForm(); misc.disableExitConfirmation(); - }, error => { + }, + (error) => { this._view.sidebarControl.showError(error.message); this._view.sidebarControl.enableForm(); - }); + } + ); } _evtPostChange(e) { @@ -173,79 +212,82 @@ class PostMainController extends BasePostController { this._view.commentControl.disableForm(); const comment = Comment.create(this._post.id); comment.text = e.detail.text; - comment.save() - .then(() => { + comment.save().then( + () => { this._post.comments.add(comment); this._view.commentControl.exitEditMode(); this._view.commentControl.enableForm(); misc.disableExitConfirmation(); - }, error => { + }, + (error) => { this._view.commentControl.showError(error.message); this._view.commentControl.enableForm(); - }); + } + ); } _evtUpdateComment(e) { // TODO: disable form e.detail.comment.text = e.detail.text; - e.detail.comment.save() - .catch(error => { - e.detail.target.showError(error.message); - // TODO: enable form - }); + e.detail.comment.save().catch((error) => { + e.detail.target.showError(error.message); + // TODO: enable form + }); } _evtScoreComment(e) { - e.detail.comment.setScore(e.detail.score) - .catch(error => window.alert(error.message)); + e.detail.comment + .setScore(e.detail.score) + .catch((error) => window.alert(error.message)); } _evtDeleteComment(e) { - e.detail.comment.delete() - .catch(error => window.alert(error.message)); + e.detail.comment + .delete() + .catch((error) => window.alert(error.message)); } _evtScorePost(e) { - if (!api.hasPrivilege('posts:score')) { + if (!api.hasPrivilege("posts:score")) { return; } - e.detail.post.setScore(e.detail.score) - .catch(error => window.alert(error.message)); + e.detail.post + .setScore(e.detail.score) + .catch((error) => window.alert(error.message)); } _evtFavoritePost(e) { - if (!api.hasPrivilege('posts:favorite')) { + if (!api.hasPrivilege("posts:favorite")) { return; } - e.detail.post.addToFavorites() - .catch(error => window.alert(error.message)); + e.detail.post + .addToFavorites() + .catch((error) => window.alert(error.message)); } _evtUnfavoritePost(e) { - if (!api.hasPrivilege('posts:favorite')) { + if (!api.hasPrivilege("posts:favorite")) { return; } - e.detail.post.removeFromFavorites() - .catch(error => window.alert(error.message)); + e.detail.post + .removeFromFavorites() + .catch((error) => window.alert(error.message)); } } -module.exports = router => { - router.enter(['post', ':id', 'edit'], - (ctx, next) => { - // restore parameters from history state - if (ctx.state.parameters) { - Object.assign(ctx.parameters, ctx.state.parameters); - } - ctx.controller = new PostMainController(ctx, true); - }); - router.enter( - ['post', ':id'], - (ctx, next) => { - // restore parameters from history state - if (ctx.state.parameters) { - Object.assign(ctx.parameters, ctx.state.parameters); - } - ctx.controller = new PostMainController(ctx, false); - }); +module.exports = (router) => { + router.enter(["post", ":id", "edit"], (ctx, next) => { + // restore parameters from history state + if (ctx.state.parameters) { + Object.assign(ctx.parameters, ctx.state.parameters); + } + ctx.controller = new PostMainController(ctx, true); + }); + router.enter(["post", ":id"], (ctx, next) => { + // restore parameters from history state + if (ctx.state.parameters) { + Object.assign(ctx.parameters, ctx.state.parameters); + } + ctx.controller = new PostMainController(ctx, false); + }); }; diff --git a/client/js/controllers/post_upload_controller.js b/client/js/controllers/post_upload_controller.js index f16aaf0..d317be5 100644 --- a/client/js/controllers/post_upload_controller.js +++ b/client/js/controllers/post_upload_controller.js @@ -1,40 +1,40 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const router = require('../router.js'); -const uri = require('../util/uri.js'); -const misc = require('../util/misc.js'); -const progress = require('../util/progress.js'); -const topNavigation = require('../models/top_navigation.js'); -const Post = require('../models/post.js'); -const Tag = require('../models/tag.js'); -const PostUploadView = require('../views/post_upload_view.js'); -const EmptyView = require('../views/empty_view.js'); +const api = require("../api.js"); +const router = require("../router.js"); +const uri = require("../util/uri.js"); +const misc = require("../util/misc.js"); +const progress = require("../util/progress.js"); +const topNavigation = require("../models/top_navigation.js"); +const Post = require("../models/post.js"); +const Tag = require("../models/tag.js"); +const PostUploadView = require("../views/post_upload_view.js"); +const EmptyView = require("../views/empty_view.js"); const genericErrorMessage = - 'One of the posts needs your attention; ' + + "One of the posts needs your attention; " + 'click "resume upload" when you\'re ready.'; class PostUploadController { constructor() { this._lastCancellablePromise = null; - if (!api.hasPrivilege('posts:create')) { + if (!api.hasPrivilege("posts:create")) { this._view = new EmptyView(); - this._view.showError('You don\'t have privileges to upload posts.'); + this._view.showError("You don't have privileges to upload posts."); return; } - topNavigation.activate('upload'); - topNavigation.setTitle('Upload'); + topNavigation.activate("upload"); + topNavigation.setTitle("Upload"); this._view = new PostUploadView({ - canUploadAnonymously: api.hasPrivilege('posts:create:anonymous'), - canViewPosts: api.hasPrivilege('posts:view'), + canUploadAnonymously: api.hasPrivilege("posts:create:anonymous"), + canViewPosts: api.hasPrivilege("posts:view"), enableSafety: api.safetyEnabled(), }); - this._view.addEventListener('change', e => this._evtChange(e)); - this._view.addEventListener('submit', e => this._evtSubmit(e)); - this._view.addEventListener('cancel', e => this._evtCancel(e)); + this._view.addEventListener("change", (e) => this._evtChange(e)); + this._view.addEventListener("submit", (e) => this._evtSubmit(e)); + this._view.addEventListener("cancel", (e) => this._evtCancel(e)); } _evtChange(e) { @@ -56,87 +56,109 @@ class PostUploadController { this._view.disableForm(); this._view.clearMessages(); - e.detail.uploadables.reduce( - (promise, uploadable) => promise.then(() => this._uploadSinglePost( - uploadable, e.detail.skipDuplicates)), - Promise.resolve()) - .then(() => { - this._view.clearMessages(); - misc.disableExitConfirmation(); - const ctx = router.show(uri.formatClientLink('posts')); - ctx.controller.showSuccess('Posts uploaded.'); - }, error => { - if (error.uploadable) { - if (error.similarPosts) { - error.uploadable.lookalikes = error.similarPosts; - this._view.updateUploadable(error.uploadable); - this._view.showInfo(genericErrorMessage); - this._view.showInfo( - error.message, error.uploadable); + e.detail.uploadables + .reduce( + (promise, uploadable) => + promise.then(() => + this._uploadSinglePost( + uploadable, + e.detail.skipDuplicates + ) + ), + Promise.resolve() + ) + .then( + () => { + this._view.clearMessages(); + misc.disableExitConfirmation(); + const ctx = router.show(uri.formatClientLink("posts")); + ctx.controller.showSuccess("Posts uploaded."); + }, + (error) => { + if (error.uploadable) { + if (error.similarPosts) { + error.uploadable.lookalikes = error.similarPosts; + this._view.updateUploadable(error.uploadable); + this._view.showInfo(genericErrorMessage); + this._view.showInfo( + error.message, + error.uploadable + ); + } else { + this._view.showError(genericErrorMessage); + this._view.showError( + error.message, + error.uploadable + ); + } } else { - this._view.showError(genericErrorMessage); - this._view.showError( - error.message, error.uploadable); + this._view.showError(error.message); } - } else { - this._view.showError(error.message); + this._view.enableForm(); } - this._view.enableForm(); - }); + ); } _uploadSinglePost(uploadable, skipDuplicates) { progress.start(); let reverseSearchPromise = Promise.resolve(); if (!uploadable.lookalikesConfirmed) { - reverseSearchPromise = - Post.reverseSearch(uploadable.url || uploadable.file); + reverseSearchPromise = Post.reverseSearch( + uploadable.url || uploadable.file + ); } this._lastCancellablePromise = reverseSearchPromise; - return reverseSearchPromise.then(searchResult => { - if (searchResult) { - // notify about exact duplicate - if (searchResult.exactPost) { - if (skipDuplicates) { - this._view.removeUploadable(uploadable); - return Promise.resolve(); - } else { - let error = new Error('Post already uploaded ' + - `(@${searchResult.exactPost.id})`); + return reverseSearchPromise + .then((searchResult) => { + if (searchResult) { + // notify about exact duplicate + if (searchResult.exactPost) { + if (skipDuplicates) { + this._view.removeUploadable(uploadable); + return Promise.resolve(); + } else { + let error = new Error( + "Post already uploaded " + + `(@${searchResult.exactPost.id})` + ); + error.uploadable = uploadable; + return Promise.reject(error); + } + } + + // notify about similar posts + if (searchResult.similarPosts.length) { + let error = new Error( + `Found ${searchResult.similarPosts.length} similar ` + + "posts.\nYou can resume or discard this upload." + ); error.uploadable = uploadable; + error.similarPosts = searchResult.similarPosts; return Promise.reject(error); } } - // notify about similar posts - if (searchResult.similarPosts.length) { - let error = new Error( - `Found ${searchResult.similarPosts.length} similar ` + - 'posts.\nYou can resume or discard this upload.'); - error.uploadable = uploadable; - error.similarPosts = searchResult.similarPosts; - return Promise.reject(error); - } - } - - // no duplicates, proceed with saving - let post = this._uploadableToPost(uploadable); - let savePromise = post.save(uploadable.anonymous) - .then(() => { + // no duplicates, proceed with saving + let post = this._uploadableToPost(uploadable); + let savePromise = post.save(uploadable.anonymous).then(() => { this._view.removeUploadable(uploadable); return Promise.resolve(); }); - this._lastCancellablePromise = savePromise; - return savePromise; - }).then(result => { - progress.done(); - return Promise.resolve(result); - }, error => { - error.uploadable = uploadable; - progress.done(); - return Promise.reject(error); - }); + this._lastCancellablePromise = savePromise; + return savePromise; + }) + .then( + (result) => { + progress.done(); + return Promise.resolve(result); + }, + (error) => { + error.uploadable = uploadable; + progress.done(); + return Promise.reject(error); + } + ); } _uploadableToPost(uploadable) { @@ -159,8 +181,8 @@ class PostUploadController { } } -module.exports = router => { - router.enter(['upload'], (ctx, next) => { +module.exports = (router) => { + router.enter(["upload"], (ctx, next) => { ctx.controller = new PostUploadController(); }); }; diff --git a/client/js/controllers/settings_controller.js b/client/js/controllers/settings_controller.js index 7765143..8cf2c1c 100644 --- a/client/js/controllers/settings_controller.js +++ b/client/js/controllers/settings_controller.js @@ -1,28 +1,28 @@ -'use strict'; +"use strict"; -const settings = require('../models/settings.js'); -const topNavigation = require('../models/top_navigation.js'); -const SettingsView = require('../views/settings_view.js'); +const settings = require("../models/settings.js"); +const topNavigation = require("../models/top_navigation.js"); +const SettingsView = require("../views/settings_view.js"); class SettingsController { constructor() { - topNavigation.activate('settings'); - topNavigation.setTitle('Browsing settings'); + topNavigation.activate("settings"); + topNavigation.setTitle("Browsing settings"); this._view = new SettingsView({ settings: settings.get(), }); - this._view.addEventListener('submit', e => this._evtSubmit(e)); + this._view.addEventListener("submit", (e) => this._evtSubmit(e)); } _evtSubmit(e) { this._view.clearMessages(); settings.save(e.detail); - this._view.showSuccess('Settings saved.'); + this._view.showSuccess("Settings saved."); } } -module.exports = router => { - router.enter(['settings'], (ctx, next) => { +module.exports = (router) => { + router.enter(["settings"], (ctx, next) => { ctx.controller = new SettingsController(); }); }; diff --git a/client/js/controllers/snapshots_controller.js b/client/js/controllers/snapshots_controller.js index 0e9b773..120b6cb 100644 --- a/client/js/controllers/snapshots_controller.js +++ b/client/js/controllers/snapshots_controller.js @@ -1,41 +1,43 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const SnapshotList = require('../models/snapshot_list.js'); -const PageController = require('../controllers/page_controller.js'); -const topNavigation = require('../models/top_navigation.js'); -const SnapshotsPageView = require('../views/snapshots_page_view.js'); -const EmptyView = require('../views/empty_view.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const SnapshotList = require("../models/snapshot_list.js"); +const PageController = require("../controllers/page_controller.js"); +const topNavigation = require("../models/top_navigation.js"); +const SnapshotsPageView = require("../views/snapshots_page_view.js"); +const EmptyView = require("../views/empty_view.js"); class SnapshotsController { constructor(ctx) { - if (!api.hasPrivilege('snapshots:list')) { + if (!api.hasPrivilege("snapshots:list")) { this._view = new EmptyView(); - this._view.showError('You don\'t have privileges to view history.'); + this._view.showError("You don't have privileges to view history."); return; } - topNavigation.activate(''); - topNavigation.setTitle('History'); + topNavigation.activate(""); + topNavigation.setTitle("History"); this._pageController = new PageController(); this._pageController.run({ parameters: ctx.parameters, defaultLimit: 25, getClientUrlForPage: (offset, limit) => { - const parameters = Object.assign( - {}, ctx.parameters, {offset: offset, limit: limit}); - return uri.formatClientLink('history', parameters); + const parameters = Object.assign({}, ctx.parameters, { + offset: offset, + limit: limit, + }); + return uri.formatClientLink("history", parameters); }, requestPage: (offset, limit) => { - return SnapshotList.search('', offset, limit); + return SnapshotList.search("", offset, limit); }, - pageRenderer: pageCtx => { + pageRenderer: (pageCtx) => { Object.assign(pageCtx, { - canViewPosts: api.hasPrivilege('posts:view'), - canViewUsers: api.hasPrivilege('users:view'), - canViewTags: api.hasPrivilege('tags:view'), + canViewPosts: api.hasPrivilege("posts:view"), + canViewUsers: api.hasPrivilege("users:view"), + canViewTags: api.hasPrivilege("tags:view"), }); return new SnapshotsPageView(pageCtx); }, @@ -43,9 +45,8 @@ class SnapshotsController { } } -module.exports = router => { - router.enter(['history'], - (ctx, next) => { - ctx.controller = new SnapshotsController(ctx); - }); +module.exports = (router) => { + router.enter(["history"], (ctx, next) => { + ctx.controller = new SnapshotsController(ctx); + }); }; diff --git a/client/js/controllers/tag_categories_controller.js b/client/js/controllers/tag_categories_controller.js index 49600cf..2470edb 100644 --- a/client/js/controllers/tag_categories_controller.js +++ b/client/js/controllers/tag_categories_controller.js @@ -1,57 +1,67 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const tags = require('../tags.js'); -const TagCategoryList = require('../models/tag_category_list.js'); -const topNavigation = require('../models/top_navigation.js'); -const TagCategoriesView = require('../views/tag_categories_view.js'); -const EmptyView = require('../views/empty_view.js'); +const api = require("../api.js"); +const tags = require("../tags.js"); +const TagCategoryList = require("../models/tag_category_list.js"); +const topNavigation = require("../models/top_navigation.js"); +const TagCategoriesView = require("../views/tag_categories_view.js"); +const EmptyView = require("../views/empty_view.js"); class TagCategoriesController { constructor() { - if (!api.hasPrivilege('tagCategories:list')) { + if (!api.hasPrivilege("tagCategories:list")) { this._view = new EmptyView(); this._view.showError( - 'You don\'t have privileges to view tag categories.'); + "You don't have privileges to view tag categories." + ); return; } - topNavigation.activate('tags'); - topNavigation.setTitle('Listing tags'); - TagCategoryList.get().then(response => { - this._tagCategories = response.results; - this._view = new TagCategoriesView({ - tagCategories: this._tagCategories, - canEditName: api.hasPrivilege('tagCategories:edit:name'), - canEditColor: api.hasPrivilege('tagCategories:edit:color'), - canDelete: api.hasPrivilege('tagCategories:delete'), - canCreate: api.hasPrivilege('tagCategories:create'), - canSetDefault: api.hasPrivilege('tagCategories:setDefault'), - }); - this._view.addEventListener('submit', e => this._evtSubmit(e)); - }, error => { - this._view = new EmptyView(); - this._view.showError(error.message); - }); + topNavigation.activate("tags"); + topNavigation.setTitle("Listing tags"); + TagCategoryList.get().then( + (response) => { + this._tagCategories = response.results; + this._view = new TagCategoriesView({ + tagCategories: this._tagCategories, + canEditName: api.hasPrivilege("tagCategories:edit:name"), + canEditColor: api.hasPrivilege("tagCategories:edit:color"), + canDelete: api.hasPrivilege("tagCategories:delete"), + canCreate: api.hasPrivilege("tagCategories:create"), + canSetDefault: api.hasPrivilege( + "tagCategories:setDefault" + ), + }); + this._view.addEventListener("submit", (e) => + this._evtSubmit(e) + ); + }, + (error) => { + this._view = new EmptyView(); + this._view.showError(error.message); + } + ); } _evtSubmit(e) { this._view.clearMessages(); this._view.disableForm(); - this._tagCategories.save() - .then(() => { + this._tagCategories.save().then( + () => { tags.refreshCategoryColorMap(); this._view.enableForm(); - this._view.showSuccess('Changes saved.'); - }, error => { + this._view.showSuccess("Changes saved."); + }, + (error) => { this._view.enableForm(); this._view.showError(error.message); - }); + } + ); } } -module.exports = router => { - router.enter(['tag-categories'], (ctx, next) => { +module.exports = (router) => { + router.enter(["tag-categories"], (ctx, next) => { ctx.controller = new TagCategoriesController(ctx, next); }); }; diff --git a/client/js/controllers/tag_controller.js b/client/js/controllers/tag_controller.js index 71405dc..0928b3f 100644 --- a/client/js/controllers/tag_controller.js +++ b/client/js/controllers/tag_controller.js @@ -1,63 +1,80 @@ -'use strict'; +"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 Tag = require('../models/tag.js'); -const TagCategoryList = require('../models/tag_category_list.js'); -const topNavigation = require('../models/top_navigation.js'); -const TagView = require('../views/tag_view.js'); -const EmptyView = require('../views/empty_view.js'); +const router = require("../router.js"); +const api = require("../api.js"); +const misc = require("../util/misc.js"); +const uri = require("../util/uri.js"); +const Tag = require("../models/tag.js"); +const TagCategoryList = require("../models/tag_category_list.js"); +const topNavigation = require("../models/top_navigation.js"); +const TagView = require("../views/tag_view.js"); +const EmptyView = require("../views/empty_view.js"); class TagController { constructor(ctx, section) { - if (!api.hasPrivilege('tags:view')) { + if (!api.hasPrivilege("tags:view")) { 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; } Promise.all([ TagCategoryList.get(), Tag.get(ctx.parameters.name), - ]).then(responses => { - const [tagCategoriesResponse, tag] = responses; + ]).then( + (responses) => { + const [tagCategoriesResponse, tag] = responses; - topNavigation.activate('tags'); - topNavigation.setTitle('Tag #' + tag.names[0]); + topNavigation.activate("tags"); + topNavigation.setTitle("Tag #" + tag.names[0]); - this._name = ctx.parameters.name; - tag.addEventListener('change', e => this._evtSaved(e, section)); + this._name = ctx.parameters.name; + tag.addEventListener("change", (e) => + this._evtSaved(e, section) + ); - const categories = {}; - for (let category of tagCategoriesResponse.results) { - categories[category.name] = category.name; - } + const categories = {}; + for (let category of tagCategoriesResponse.results) { + categories[category.name] = category.name; + } - this._view = new TagView({ - tag: tag, - section: section, - canEditAnything: api.hasPrivilege('tags:edit'), - canEditNames: api.hasPrivilege('tags:edit:names'), - canEditCategory: api.hasPrivilege('tags:edit:category'), - canEditImplications: api.hasPrivilege('tags:edit:implications'), - canEditSuggestions: api.hasPrivilege('tags:edit:suggestions'), - canEditDescription: api.hasPrivilege('tags:edit:description'), - canMerge: api.hasPrivilege('tags:merge'), - canDelete: api.hasPrivilege('tags:delete'), - categories: categories, - escapeColons: uri.escapeColons, - }); + this._view = new TagView({ + tag: tag, + section: section, + canEditAnything: api.hasPrivilege("tags:edit"), + canEditNames: api.hasPrivilege("tags:edit:names"), + canEditCategory: api.hasPrivilege("tags:edit:category"), + canEditImplications: api.hasPrivilege( + "tags:edit:implications" + ), + canEditSuggestions: api.hasPrivilege( + "tags:edit:suggestions" + ), + canEditDescription: api.hasPrivilege( + "tags:edit:description" + ), + canMerge: api.hasPrivilege("tags:merge"), + canDelete: api.hasPrivilege("tags: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); - }); + 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) { @@ -68,9 +85,10 @@ class TagController { misc.disableExitConfirmation(); if (this._name !== e.detail.tag.names[0]) { router.replace( - uri.formatClientLink('tag', e.detail.tag.names[0], section), + uri.formatClientLink("tag", e.detail.tag.names[0], section), null, - false); + false + ); } } @@ -86,59 +104,69 @@ class TagController { if (e.detail.description !== undefined) { e.detail.tag.description = e.detail.description; } - e.detail.tag.save().then(() => { - this._view.showSuccess('Tag saved.'); - this._view.enableForm(); - }, error => { - this._view.showError(error.message); - this._view.enableForm(); - }); + e.detail.tag.save().then( + () => { + this._view.showSuccess("Tag saved."); + this._view.enableForm(); + }, + (error) => { + this._view.showError(error.message); + this._view.enableForm(); + } + ); } _evtMerge(e) { this._view.clearMessages(); this._view.disableForm(); - e.detail.tag - .merge(e.detail.targetTagName, e.detail.addAlias) - .then(() => { - this._view.showSuccess('Tag merged.'); + e.detail.tag.merge(e.detail.targetTagName, e.detail.addAlias).then( + () => { + this._view.showSuccess("Tag merged."); this._view.enableForm(); router.replace( uri.formatClientLink( - 'tag', e.detail.targetTagName, 'merge'), + "tag", + e.detail.targetTagName, + "merge" + ), null, - false); - }, error => { + false + ); + }, + (error) => { this._view.showError(error.message); this._view.enableForm(); - }); + } + ); } _evtDelete(e) { this._view.clearMessages(); this._view.disableForm(); - e.detail.tag.delete() - .then(() => { - const ctx = router.show(uri.formatClientLink('tags')); - ctx.controller.showSuccess('Tag deleted.'); - }, error => { + e.detail.tag.delete().then( + () => { + const ctx = router.show(uri.formatClientLink("tags")); + ctx.controller.showSuccess("Tag deleted."); + }, + (error) => { this._view.showError(error.message); this._view.enableForm(); - }); + } + ); } } -module.exports = router => { - router.enter(['tag', ':name', 'edit'], (ctx, next) => { - ctx.controller = new TagController(ctx, 'edit'); +module.exports = (router) => { + router.enter(["tag", ":name", "edit"], (ctx, next) => { + ctx.controller = new TagController(ctx, "edit"); }); - router.enter(['tag', ':name', 'merge'], (ctx, next) => { - ctx.controller = new TagController(ctx, 'merge'); + router.enter(["tag", ":name", "merge"], (ctx, next) => { + ctx.controller = new TagController(ctx, "merge"); }); - router.enter(['tag', ':name', 'delete'], (ctx, next) => { - ctx.controller = new TagController(ctx, 'delete'); + router.enter(["tag", ":name", "delete"], (ctx, next) => { + ctx.controller = new TagController(ctx, "delete"); }); - router.enter(['tag', ':name'], (ctx, next) => { - ctx.controller = new TagController(ctx, 'summary'); + router.enter(["tag", ":name"], (ctx, next) => { + ctx.controller = new TagController(ctx, "summary"); }); }; diff --git a/client/js/controllers/tag_list_controller.js b/client/js/controllers/tag_list_controller.js index 34ff8b2..010c556 100644 --- a/client/js/controllers/tag_list_controller.js +++ b/client/js/controllers/tag_list_controller.js @@ -1,46 +1,47 @@ -'use strict'; +"use strict"; -const router = require('../router.js'); -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const TagList = require('../models/tag_list.js'); -const topNavigation = require('../models/top_navigation.js'); -const PageController = require('../controllers/page_controller.js'); -const TagsHeaderView = require('../views/tags_header_view.js'); -const TagsPageView = require('../views/tags_page_view.js'); -const EmptyView = require('../views/empty_view.js'); +const router = require("../router.js"); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const TagList = require("../models/tag_list.js"); +const topNavigation = require("../models/top_navigation.js"); +const PageController = require("../controllers/page_controller.js"); +const TagsHeaderView = require("../views/tags_header_view.js"); +const TagsPageView = require("../views/tags_page_view.js"); +const EmptyView = require("../views/empty_view.js"); const fields = [ - 'names', - 'suggestions', - 'implications', - 'creationTime', - 'usages', - 'category' + "names", + "suggestions", + "implications", + "creationTime", + "usages", + "category", ]; class TagListController { constructor(ctx) { this._pageController = new PageController(); - if (!api.hasPrivilege('tags:list')) { + if (!api.hasPrivilege("tags:list")) { 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; } this._ctx = ctx; - topNavigation.activate('tags'); - topNavigation.setTitle('Listing tags'); + topNavigation.activate("tags"); + topNavigation.setTitle("Listing tags"); this._headerView = new TagsHeaderView({ hostNode: this._pageController.view.pageHeaderHolderNode, parameters: ctx.parameters, - canEditTagCategories: api.hasPrivilege('tagCategories:edit'), + canEditTagCategories: api.hasPrivilege("tagCategories:edit"), }); - this._headerView.addEventListener( - 'navigate', e => this._evtNavigate(e)); + this._headerView.addEventListener("navigate", (e) => + this._evtNavigate(e) + ); this._syncPageController(); } @@ -55,7 +56,8 @@ class TagListController { _evtNavigate(e) { router.showNoDispatch( - uri.formatClientLink('tags', e.detail.parameters)); + uri.formatClientLink("tags", e.detail.parameters) + ); Object.assign(this._ctx.parameters, e.detail.parameters); this._syncPageController(); } @@ -65,25 +67,29 @@ class TagListController { parameters: this._ctx.parameters, defaultLimit: 50, getClientUrlForPage: (offset, limit) => { - const parameters = Object.assign( - {}, this._ctx.parameters, {offset: offset, limit: limit}); - return uri.formatClientLink('tags', parameters); + const parameters = Object.assign({}, this._ctx.parameters, { + offset: offset, + limit: limit, + }); + return uri.formatClientLink("tags", parameters); }, requestPage: (offset, limit) => { return TagList.search( - this._ctx.parameters.query, offset, limit, fields); + this._ctx.parameters.query, + offset, + limit, + fields + ); }, - pageRenderer: pageCtx => { + pageRenderer: (pageCtx) => { return new TagsPageView(pageCtx); }, }); } } -module.exports = router => { - router.enter( - ['tags'], - (ctx, next) => { - ctx.controller = new TagListController(ctx); - }); +module.exports = (router) => { + router.enter(["tags"], (ctx, next) => { + ctx.controller = new TagListController(ctx); + }); }; diff --git a/client/js/controllers/top_navigation_controller.js b/client/js/controllers/top_navigation_controller.js index 9cd70c1..d9bb276 100644 --- a/client/js/controllers/top_navigation_controller.js +++ b/client/js/controllers/top_navigation_controller.js @@ -1,19 +1,20 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const topNavigation = require('../models/top_navigation.js'); -const TopNavigationView = require('../views/top_navigation_view.js'); +const api = require("../api.js"); +const topNavigation = require("../models/top_navigation.js"); +const TopNavigationView = require("../views/top_navigation_view.js"); class TopNavigationController { constructor() { api.fetchConfig().then(() => { this._topNavigationView = new TopNavigationView(); - topNavigation.addEventListener( - 'activate', e => this._evtActivate(e)); + topNavigation.addEventListener("activate", (e) => + this._evtActivate(e) + ); - api.addEventListener('login', e => this._evtAuthChange(e)); - api.addEventListener('logout', e => this._evtAuthChange(e)); + api.addEventListener("login", (e) => this._evtAuthChange(e)); + api.addEventListener("logout", (e) => this._evtAuthChange(e)); this._render(); }); @@ -28,37 +29,38 @@ class TopNavigationController { } _updateNavigationFromPrivileges() { - topNavigation.get('account').url = 'user/' + api.userName; - topNavigation.get('account').imageUrl = - api.user ? api.user.avatarUrl : null; + topNavigation.get("account").url = "user/" + api.userName; + topNavigation.get("account").imageUrl = api.user + ? api.user.avatarUrl + : null; topNavigation.showAll(); - if (!api.hasPrivilege('posts:list')) { - topNavigation.hide('posts'); + if (!api.hasPrivilege("posts:list")) { + topNavigation.hide("posts"); } - if (!api.hasPrivilege('posts:create')) { - topNavigation.hide('upload'); + if (!api.hasPrivilege("posts:create")) { + topNavigation.hide("upload"); } - if (!api.hasPrivilege('comments:list')) { - topNavigation.hide('comments'); + if (!api.hasPrivilege("comments:list")) { + topNavigation.hide("comments"); } - if (!api.hasPrivilege('tags:list')) { - topNavigation.hide('tags'); + if (!api.hasPrivilege("tags:list")) { + topNavigation.hide("tags"); } - if (!api.hasPrivilege('users:list')) { - topNavigation.hide('users'); + if (!api.hasPrivilege("users:list")) { + topNavigation.hide("users"); } if (api.isLoggedIn()) { - if (!api.hasPrivilege('users:create:any')) { - topNavigation.hide('register'); + if (!api.hasPrivilege("users:create:any")) { + topNavigation.hide("register"); } - topNavigation.hide('login'); + topNavigation.hide("login"); } else { - if (!api.hasPrivilege('users:create:self')) { - topNavigation.hide('register'); + if (!api.hasPrivilege("users:create:self")) { + topNavigation.hide("register"); } - topNavigation.hide('account'); - topNavigation.hide('logout'); + topNavigation.hide("account"); + topNavigation.hide("logout"); } } @@ -66,10 +68,11 @@ class TopNavigationController { this._updateNavigationFromPrivileges(); this._topNavigationView.render({ items: topNavigation.getAll(), - name: api.getName() + name: api.getName(), }); this._topNavigationView.activate( - topNavigation.activeItem ? topNavigation.activeItem.key : ''); + topNavigation.activeItem ? topNavigation.activeItem.key : "" + ); } } diff --git a/client/js/controllers/user_controller.js b/client/js/controllers/user_controller.js index e192d87..326736b 100644 --- a/client/js/controllers/user_controller.js +++ b/client/js/controllers/user_controller.js @@ -1,23 +1,25 @@ -'use strict'; +"use strict"; -const router = require('../router.js'); -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const misc = require('../util/misc.js'); -const views = require('../util/views.js'); -const User = require('../models/user.js'); -const UserToken = require('../models/user_token.js'); -const topNavigation = require('../models/top_navigation.js'); -const UserView = require('../views/user_view.js'); -const EmptyView = require('../views/empty_view.js'); +const router = require("../router.js"); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const misc = require("../util/misc.js"); +const views = require("../util/views.js"); +const User = require("../models/user.js"); +const UserToken = require("../models/user_token.js"); +const topNavigation = require("../models/top_navigation.js"); +const UserView = require("../views/user_view.js"); +const EmptyView = require("../views/empty_view.js"); class UserController { constructor(ctx, section) { const userName = ctx.parameters.name; - if (!api.hasPrivilege('users:view') && - !api.isLoggedIn({name: userName})) { + if ( + !api.hasPrivilege("users:view") && + !api.isLoggedIn({ name: userName }) + ) { 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."); return; } @@ -25,100 +27,129 @@ class UserController { this._errorMessages = []; let userTokenPromise = Promise.resolve([]); - if (section === 'list-tokens') { - userTokenPromise = UserToken.get(userName) - .then(userTokens => { - return userTokens.map(token => { - token.isCurrentAuthToken = api.isCurrentAuthToken(token); + if (section === "list-tokens") { + userTokenPromise = UserToken.get(userName).then( + (userTokens) => { + return userTokens.map((token) => { + token.isCurrentAuthToken = api.isCurrentAuthToken( + token + ); return token; }); - }, error => { + }, + (error) => { return []; - }); + } + ); } - topNavigation.setTitle('User ' + userName); - Promise.all([ - userTokenPromise, - User.get(userName) - ]).then(responses => { - const [userTokens, user] = responses; - const isLoggedIn = api.isLoggedIn(user); - const infix = isLoggedIn ? 'self' : 'any'; + topNavigation.setTitle("User " + userName); + Promise.all([userTokenPromise, User.get(userName)]).then( + (responses) => { + const [userTokens, user] = responses; + const isLoggedIn = api.isLoggedIn(user); + const infix = isLoggedIn ? "self" : "any"; - this._name = userName; - user.addEventListener('change', e => this._evtSaved(e, section)); + this._name = userName; + user.addEventListener("change", (e) => + this._evtSaved(e, section) + ); - const myRankIndex = api.user ? - api.allRanks.indexOf(api.user.rank) : - 0; - let ranks = {}; - for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) { - if (rankIdentifier === 'anonymous') { - continue; + const myRankIndex = api.user + ? api.allRanks.indexOf(api.user.rank) + : 0; + let ranks = {}; + for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) { + if (rankIdentifier === "anonymous") { + continue; + } + if (rankIdx > myRankIndex) { + continue; + } + ranks[rankIdentifier] = api.rankNames.get(rankIdentifier); } - if (rankIdx > myRankIndex) { - continue; - } - ranks[rankIdentifier] = api.rankNames.get(rankIdentifier); - } - if (isLoggedIn) { - topNavigation.activate('account'); - } else { - topNavigation.activate('users'); - } + if (isLoggedIn) { + topNavigation.activate("account"); + } else { + topNavigation.activate("users"); + } - this._view = new UserView({ - user: user, - section: section, - isLoggedIn: isLoggedIn, - canEditName: api.hasPrivilege(`users:edit:${infix}:name`), - canEditPassword: api.hasPrivilege(`users:edit:${infix}:pass`), - canEditEmail: api.hasPrivilege(`users:edit:${infix}:email`), - canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`), - canEditAvatar: api.hasPrivilege(`users:edit:${infix}:avatar`), - canEditAnything: api.hasPrivilege(`users:edit:${infix}`), - canListTokens: api.hasPrivilege(`userTokens:list:${infix}`), - canCreateToken: api.hasPrivilege(`userTokens:create:${infix}`), - canEditToken: api.hasPrivilege(`userTokens:edit:${infix}`), - canDeleteToken: api.hasPrivilege(`userTokens:delete:${infix}`), - canDelete: api.hasPrivilege(`users:delete:${infix}`), - ranks: ranks, - tokens: userTokens, - }); - this._view.addEventListener('change', e => this._evtChange(e)); - this._view.addEventListener('submit', e => this._evtUpdate(e)); - this._view.addEventListener('delete', e => this._evtDelete(e)); - this._view.addEventListener('create-token', e => this._evtCreateToken(e)); - this._view.addEventListener('delete-token', e => this._evtDeleteToken(e)); - this._view.addEventListener('update-token', e => this._evtUpdateToken(e)); + this._view = new UserView({ + user: user, + section: section, + isLoggedIn: isLoggedIn, + canEditName: api.hasPrivilege(`users:edit:${infix}:name`), + canEditPassword: api.hasPrivilege( + `users:edit:${infix}:pass` + ), + canEditEmail: api.hasPrivilege( + `users:edit:${infix}:email` + ), + canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`), + canEditAvatar: api.hasPrivilege( + `users:edit:${infix}:avatar` + ), + canEditAnything: api.hasPrivilege(`users:edit:${infix}`), + canListTokens: api.hasPrivilege( + `userTokens:list:${infix}` + ), + canCreateToken: api.hasPrivilege( + `userTokens:create:${infix}` + ), + canEditToken: api.hasPrivilege(`userTokens:edit:${infix}`), + canDeleteToken: api.hasPrivilege( + `userTokens:delete:${infix}` + ), + canDelete: api.hasPrivilege(`users:delete:${infix}`), + ranks: ranks, + tokens: userTokens, + }); + this._view.addEventListener("change", (e) => + this._evtChange(e) + ); + this._view.addEventListener("submit", (e) => + this._evtUpdate(e) + ); + this._view.addEventListener("delete", (e) => + this._evtDelete(e) + ); + this._view.addEventListener("create-token", (e) => + this._evtCreateToken(e) + ); + this._view.addEventListener("delete-token", (e) => + this._evtDeleteToken(e) + ); + this._view.addEventListener("update-token", (e) => + this._evtUpdateToken(e) + ); - for (let message of this._successMessages) { - this.showSuccess(message); - } + for (let message of this._successMessages) { + this.showSuccess(message); + } - for (let message of this._errorMessages) { - this.showError(message); + for (let message of this._errorMessages) { + this.showError(message); + } + }, + (error) => { + this._view = new EmptyView(); + this._view.showError(error.message); } - - }, error => { - this._view = new EmptyView(); - this._view.showError(error.message); - }); + ); } showSuccess(message) { - if (typeof this._view === 'undefined') { - this._successMessages.push(message) + if (typeof this._view === "undefined") { + this._successMessages.push(message); } else { this._view.showSuccess(message); } } showError(message) { - if (typeof this._view === 'undefined') { - this._errorMessages.push(message) + if (typeof this._view === "undefined") { + this._errorMessages.push(message); } else { this._view.showError(message); } @@ -132,9 +163,10 @@ class UserController { misc.disableExitConfirmation(); if (this._name !== e.detail.user.name) { router.replace( - uri.formatClientLink('user', e.detail.user.name, section), + uri.formatClientLink("user", e.detail.user.name, section), null, - false); + false + ); } } @@ -142,7 +174,7 @@ class UserController { this._view.clearMessages(); this._view.disableForm(); const isLoggedIn = api.isLoggedIn(e.detail.user); - const infix = isLoggedIn ? 'self' : 'any'; + const infix = isLoggedIn ? "self" : "any"; if (e.detail.name !== undefined) { e.detail.user.name = e.detail.name; @@ -165,72 +197,105 @@ class UserController { } } - e.detail.user.save().then(() => { - return isLoggedIn ? - api.login( - e.detail.name || api.userName, - e.detail.password || api.userPassword, - false) : - Promise.resolve(); - }).then(() => { - this._view.showSuccess('Settings updated.'); - this._view.enableForm(); - }, error => { - this._view.showError(error.message); - this._view.enableForm(); - }); + e.detail.user + .save() + .then(() => { + return isLoggedIn + ? api.login( + e.detail.name || api.userName, + e.detail.password || api.userPassword, + false + ) + : Promise.resolve(); + }) + .then( + () => { + this._view.showSuccess("Settings updated."); + this._view.enableForm(); + }, + (error) => { + this._view.showError(error.message); + this._view.enableForm(); + } + ); } _evtDelete(e) { this._view.clearMessages(); this._view.disableForm(); const isLoggedIn = api.isLoggedIn(e.detail.user); - e.detail.user.delete() - .then(() => { + e.detail.user.delete().then( + () => { if (isLoggedIn) { api.forget(); api.logout(); } - if (api.hasPrivilege('users:list')) { - const ctx = router.show(uri.formatClientLink('users')); - ctx.controller.showSuccess('Account deleted.'); + if (api.hasPrivilege("users:list")) { + const ctx = router.show(uri.formatClientLink("users")); + ctx.controller.showSuccess("Account deleted."); } else { const ctx = router.show(uri.formatClientLink()); - ctx.controller.showSuccess('Account deleted.'); + ctx.controller.showSuccess("Account deleted."); } - }, error => { + }, + (error) => { this._view.showError(error.message); this._view.enableForm(); - }); + } + ); } _evtCreateToken(e) { this._view.clearMessages(); this._view.disableForm(); - UserToken.create(e.detail.user.name, e.detail.note, e.detail.expirationTime) - .then(response => { - const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens')); - ctx.controller.showSuccess('Token ' + response.token + ' created.'); - }, error => { + UserToken.create( + e.detail.user.name, + e.detail.note, + e.detail.expirationTime + ).then( + (response) => { + const ctx = router.show( + uri.formatClientLink( + "user", + e.detail.user.name, + "list-tokens" + ) + ); + ctx.controller.showSuccess( + "Token " + response.token + " created." + ); + }, + (error) => { this._view.showError(error.message); this._view.enableForm(); - }); + } + ); } _evtDeleteToken(e) { this._view.clearMessages(); this._view.disableForm(); if (api.isCurrentAuthToken(e.detail.userToken)) { - router.show(uri.formatClientLink('logout')); + router.show(uri.formatClientLink("logout")); } else { - e.detail.userToken.delete(e.detail.user.name) - .then(() => { - const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens')); - ctx.controller.showSuccess('Token ' + e.detail.userToken.token + ' deleted.'); - }, error => { + e.detail.userToken.delete(e.detail.user.name).then( + () => { + const ctx = router.show( + uri.formatClientLink( + "user", + e.detail.user.name, + "list-tokens" + ) + ); + ctx.controller.showSuccess( + "Token " + e.detail.userToken.token + " deleted." + ); + }, + (error) => { this._view.showError(error.message); this._view.enableForm(); - }); + } + ); } } @@ -242,27 +307,38 @@ class UserController { e.detail.userToken.note = e.detail.note; } - e.detail.userToken.save(e.detail.user.name).then(response => { - const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens')); - ctx.controller.showSuccess('Token ' + response.token + ' updated.'); - }, error => { - this._view.showError(error.message); - this._view.enableForm(); - }); + e.detail.userToken.save(e.detail.user.name).then( + (response) => { + const ctx = router.show( + uri.formatClientLink( + "user", + e.detail.user.name, + "list-tokens" + ) + ); + ctx.controller.showSuccess( + "Token " + response.token + " updated." + ); + }, + (error) => { + this._view.showError(error.message); + this._view.enableForm(); + } + ); } } -module.exports = router => { - router.enter(['user', ':name'], (ctx, next) => { - ctx.controller = new UserController(ctx, 'summary'); +module.exports = (router) => { + router.enter(["user", ":name"], (ctx, next) => { + ctx.controller = new UserController(ctx, "summary"); }); - router.enter(['user', ':name', 'edit'], (ctx, next) => { - ctx.controller = new UserController(ctx, 'edit'); + router.enter(["user", ":name", "edit"], (ctx, next) => { + ctx.controller = new UserController(ctx, "edit"); }); - router.enter(['user', ':name', 'list-tokens'], (ctx, next) => { - ctx.controller = new UserController(ctx, 'list-tokens'); + router.enter(["user", ":name", "list-tokens"], (ctx, next) => { + ctx.controller = new UserController(ctx, "list-tokens"); }); - router.enter(['user', ':name', 'delete'], (ctx, next) => { - ctx.controller = new UserController(ctx, 'delete'); + router.enter(["user", ":name", "delete"], (ctx, next) => { + ctx.controller = new UserController(ctx, "delete"); }); }; diff --git a/client/js/controllers/user_list_controller.js b/client/js/controllers/user_list_controller.js index 9860de4..d6c4fe5 100644 --- a/client/js/controllers/user_list_controller.js +++ b/client/js/controllers/user_list_controller.js @@ -1,27 +1,27 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const router = require('../router.js'); -const uri = require('../util/uri.js'); -const UserList = require('../models/user_list.js'); -const topNavigation = require('../models/top_navigation.js'); -const PageController = require('../controllers/page_controller.js'); -const UsersHeaderView = require('../views/users_header_view.js'); -const UsersPageView = require('../views/users_page_view.js'); -const EmptyView = require('../views/empty_view.js'); +const api = require("../api.js"); +const router = require("../router.js"); +const uri = require("../util/uri.js"); +const UserList = require("../models/user_list.js"); +const topNavigation = require("../models/top_navigation.js"); +const PageController = require("../controllers/page_controller.js"); +const UsersHeaderView = require("../views/users_header_view.js"); +const UsersPageView = require("../views/users_page_view.js"); +const EmptyView = require("../views/empty_view.js"); class UserListController { constructor(ctx) { this._pageController = new PageController(); - if (!api.hasPrivilege('users:list')) { + if (!api.hasPrivilege("users:list")) { 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."); return; } - topNavigation.activate('users'); - topNavigation.setTitle('Listing users'); + topNavigation.activate("users"); + topNavigation.setTitle("Listing users"); this._ctx = ctx; @@ -29,8 +29,9 @@ class UserListController { hostNode: this._pageController.view.pageHeaderHolderNode, parameters: ctx.parameters, }); - this._headerView.addEventListener( - 'navigate', e => this._evtNavigate(e)); + this._headerView.addEventListener("navigate", (e) => + this._evtNavigate(e) + ); this._syncPageController(); } @@ -41,7 +42,8 @@ class UserListController { _evtNavigate(e) { router.showNoDispatch( - uri.formatClientLink('users', e.detail.parameters)); + uri.formatClientLink("users", e.detail.parameters) + ); Object.assign(this._ctx.parameters, e.detail.parameters); this._syncPageController(); } @@ -51,17 +53,22 @@ class UserListController { parameters: this._ctx.parameters, defaultLimit: 30, getClientUrlForPage: (offset, limit) => { - const parameters = Object.assign( - {}, this._ctx.parameters, {offset: offset, limit: limit}); - return uri.formatClientLink('users', parameters); + const parameters = Object.assign({}, this._ctx.parameters, { + offset: offset, + limit: limit, + }); + return uri.formatClientLink("users", parameters); }, requestPage: (offset, limit) => { return UserList.search( - this._ctx.parameters.query, offset, limit); + this._ctx.parameters.query, + offset, + limit + ); }, - pageRenderer: pageCtx => { + pageRenderer: (pageCtx) => { Object.assign(pageCtx, { - canViewUsers: api.hasPrivilege('users:view'), + canViewUsers: api.hasPrivilege("users:view"), }); return new UsersPageView(pageCtx); }, @@ -69,10 +76,8 @@ class UserListController { } } -module.exports = router => { - router.enter( - ['users'], - (ctx, next) => { - ctx.controller = new UserListController(ctx); - }); +module.exports = (router) => { + router.enter(["users"], (ctx, next) => { + ctx.controller = new UserListController(ctx); + }); }; diff --git a/client/js/controllers/user_registration_controller.js b/client/js/controllers/user_registration_controller.js index 78b9402..89cfd8c 100644 --- a/client/js/controllers/user_registration_controller.js +++ b/client/js/controllers/user_registration_controller.js @@ -1,25 +1,25 @@ -'use strict'; +"use strict"; -const router = require('../router.js'); -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const User = require('../models/user.js'); -const topNavigation = require('../models/top_navigation.js'); -const RegistrationView = require('../views/registration_view.js'); -const EmptyView = require('../views/empty_view.js'); +const router = require("../router.js"); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const User = require("../models/user.js"); +const topNavigation = require("../models/top_navigation.js"); +const RegistrationView = require("../views/registration_view.js"); +const EmptyView = require("../views/empty_view.js"); class UserRegistrationController { constructor() { - if (!api.hasPrivilege('users:create:self')) { + if (!api.hasPrivilege("users:create:self")) { this._view = new EmptyView(); - this._view.showError('Registration is closed.'); + this._view.showError("Registration is closed."); return; } - topNavigation.activate('register'); - topNavigation.setTitle('Registration'); + topNavigation.activate("register"); + topNavigation.setTitle("Registration"); this._view = new RegistrationView(); - this._view.addEventListener('submit', e => this._evtRegister(e)); + this._view.addEventListener("submit", (e) => this._evtRegister(e)); } _evtRegister(e) { @@ -30,30 +30,35 @@ class UserRegistrationController { user.email = e.detail.email; user.password = e.detail.password; const isLoggedIn = api.isLoggedIn(); - user.save().then(() => { - if (isLoggedIn) { - return Promise.resolve(); - } else { - api.forget(); - return api.login(e.detail.name, e.detail.password, false); - } - }).then(() => { - if (isLoggedIn) { - const ctx = router.show(uri.formatClientLink('users')); - ctx.controller.showSuccess('User added!'); - } else { - const ctx = router.show(uri.formatClientLink()); - ctx.controller.showSuccess('Welcome aboard!'); - } - }, error => { - this._view.showError(error.message); - this._view.enableForm(); - }); + user.save() + .then(() => { + if (isLoggedIn) { + return Promise.resolve(); + } else { + api.forget(); + return api.login(e.detail.name, e.detail.password, false); + } + }) + .then( + () => { + if (isLoggedIn) { + const ctx = router.show(uri.formatClientLink("users")); + ctx.controller.showSuccess("User added!"); + } else { + const ctx = router.show(uri.formatClientLink()); + ctx.controller.showSuccess("Welcome aboard!"); + } + }, + (error) => { + this._view.showError(error.message); + this._view.enableForm(); + } + ); } } -module.exports = router => { - router.enter(['register'], (ctx, next) => { +module.exports = (router) => { + router.enter(["register"], (ctx, next) => { new UserRegistrationController(); }); }; diff --git a/client/js/controls/auto_complete_control.js b/client/js/controls/auto_complete_control.js index a802209..6e9bf3d 100644 --- a/client/js/controls/auto_complete_control.js +++ b/client/js/controls/auto_complete_control.js @@ -1,6 +1,6 @@ -'use strict'; +"use strict"; -const views = require('../util/views.js'); +const views = require("../util/views.js"); const KEY_TAB = 9; const KEY_RETURN = 13; @@ -10,14 +10,14 @@ const KEY_UP = 38; const KEY_DOWN = 40; function _getSelectionStart(input) { - if ('selectionStart' in input) { + if ("selectionStart" in input) { return input.selectionStart; } if (document.selection) { input.focus(); const sel = document.selection.createRange(); const selLen = document.selection.createRange().text.length; - sel.moveStart('character', -input.value.length); + sel.moveStart("character", -input.value.length); return sel.text.length - selLen; } return 0; @@ -27,18 +27,22 @@ class AutoCompleteControl { constructor(sourceInputNode, options) { this._sourceInputNode = sourceInputNode; this._options = {}; - Object.assign(this._options, { - verticalShift: 2, - maxResults: 15, - getTextToFind: () => { - const value = sourceInputNode.value; - const start = _getSelectionStart(sourceInputNode); - return value.substring(0, start).replace(/.*\s+/, ''); + Object.assign( + this._options, + { + verticalShift: 2, + maxResults: 15, + getTextToFind: () => { + const value = sourceInputNode.value; + const start = _getSelectionStart(sourceInputNode); + return value.substring(0, start).replace(/.*\s+/, ""); + }, + confirm: null, + delete: null, + getMatches: null, }, - confirm: null, - delete: null, - getMatches: null, - }, options); + options + ); this._showTimeout = null; this._results = []; @@ -49,22 +53,22 @@ class AutoCompleteControl { hide() { window.clearTimeout(this._showTimeout); - this._suggestionDiv.style.display = 'none'; + this._suggestionDiv.style.display = "none"; this._isVisible = false; } replaceSelectedText(result, addSpace) { const start = _getSelectionStart(this._sourceInputNode); - let prefix = ''; + let prefix = ""; let suffix = this._sourceInputNode.value.substring(start); let middle = this._sourceInputNode.value.substring(0, start); - const index = middle.lastIndexOf(' '); + const index = middle.lastIndexOf(" "); if (index !== -1) { prefix = this._sourceInputNode.value.substring(0, index + 1); middle = this._sourceInputNode.value.substring(index + 1); } - this._sourceInputNode.value = ( - prefix + result.toString() + ' ' + suffix.trimLeft()); + this._sourceInputNode.value = + prefix + result.toString() + " " + suffix.trimLeft(); if (!addSpace) { this._sourceInputNode.value = this._sourceInputNode.value.trim(); } @@ -86,7 +90,7 @@ class AutoCompleteControl { } _show() { - this._suggestionDiv.style.display = 'block'; + this._suggestionDiv.style.display = "block"; this._isVisible = true; } @@ -101,29 +105,32 @@ class AutoCompleteControl { _install() { if (!this._sourceInputNode) { - throw new Error('Input element was not found'); + throw new Error("Input element was not found"); } - if (this._sourceInputNode.getAttribute('data-autocomplete')) { + if (this._sourceInputNode.getAttribute("data-autocomplete")) { throw new Error( - 'Autocompletion was already added for this element'); + "Autocompletion was already added for this element" + ); } - this._sourceInputNode.setAttribute('data-autocomplete', true); - this._sourceInputNode.setAttribute('autocomplete', 'off'); + this._sourceInputNode.setAttribute("data-autocomplete", true); + this._sourceInputNode.setAttribute("autocomplete", "off"); - this._sourceInputNode.addEventListener( - 'keydown', e => this._evtKeyDown(e)); - this._sourceInputNode.addEventListener( - 'blur', e => this._evtBlur(e)); + this._sourceInputNode.addEventListener("keydown", (e) => + this._evtKeyDown(e) + ); + this._sourceInputNode.addEventListener("blur", (e) => + this._evtBlur(e) + ); this._suggestionDiv = views.htmlToDom( - '
      '); - this._suggestionList = this._suggestionDiv.querySelector('ul'); + '
        ' + ); + this._suggestionList = this._suggestionDiv.querySelector("ul"); document.body.appendChild(this._suggestionDiv); - views.monitorNodeRemoval( - this._sourceInputNode, () => { - this._uninstall(); - }); + views.monitorNodeRemoval(this._sourceInputNode, () => { + this._uninstall(); + }); } _uninstall() { @@ -174,10 +181,9 @@ class AutoCompleteControl { func(); } else { window.clearTimeout(this._showTimeout); - this._showTimeout = window.setTimeout( - () => { - this._showOrHide(); - }, 250); + this._showTimeout = window.setTimeout(() => { + this._showOrHide(); + }, 250); } } @@ -196,9 +202,11 @@ class AutoCompleteControl { } _selectPrevious() { - this._select(this._activeResult === -1 ? - this._results.length - 1 : - this._activeResult - 1); + this._select( + this._activeResult === -1 + ? this._results.length - 1 + : this._activeResult - 1 + ); } _selectNext() { @@ -206,15 +214,18 @@ class AutoCompleteControl { } _select(newActiveResult) { - this._activeResult = - newActiveResult.between(0, this._results.length - 1, true) ? - newActiveResult : - -1; + this._activeResult = newActiveResult.between( + 0, + this._results.length - 1, + true + ) + ? newActiveResult + : -1; this._refreshActiveResult(); } _updateResults(textToFind) { - this._options.getMatches(textToFind).then(matches => { + this._options.getMatches(textToFind).then((matches) => { const oldResults = this._results.slice(); this._results = matches.slice(0, this._options.maxResults); const oldResultsHash = JSON.stringify(oldResults); @@ -237,34 +248,30 @@ class AutoCompleteControl { } for (let [resultIndex, resultItem] of this._results.entries()) { let resultIndexWorkaround = resultIndex; - const listItem = document.createElement('li'); - const link = document.createElement('a'); + const listItem = document.createElement("li"); + const link = document.createElement("a"); link.innerHTML = resultItem.caption; - link.setAttribute('href', ''); - link.setAttribute('data-key', resultItem.value); - link.addEventListener( - 'mouseenter', - e => { - e.preventDefault(); - this._activeResult = resultIndexWorkaround; - this._refreshActiveResult(); - }); - link.addEventListener( - 'mousedown', - e => { - e.preventDefault(); - this._activeResult = resultIndexWorkaround; - this._confirm(this._getActiveSuggestion()); - this.hide(); - }); + link.setAttribute("href", ""); + link.setAttribute("data-key", resultItem.value); + link.addEventListener("mouseenter", (e) => { + e.preventDefault(); + this._activeResult = resultIndexWorkaround; + this._refreshActiveResult(); + }); + link.addEventListener("mousedown", (e) => { + e.preventDefault(); + this._activeResult = resultIndexWorkaround; + this._confirm(this._getActiveSuggestion()); + this.hide(); + }); listItem.appendChild(link); this._suggestionList.appendChild(listItem); } this._refreshActiveResult(); // display the suggestions offscreen to get the height - this._suggestionDiv.style.left = '-9999px'; - this._suggestionDiv.style.top = '-9999px'; + this._suggestionDiv.style.left = "-9999px"; + this._suggestionDiv.style.top = "-9999px"; this._show(); const verticalShift = this._options.verticalShift; const inputRect = this._sourceInputNode.getBoundingClientRect(); @@ -275,17 +282,23 @@ class AutoCompleteControl { // choose where to view the suggestions: if there's more space above // the input - draw the suggestions above it, otherwise below const direction = - inputRect.top + (inputRect.height / 2) < viewPortHeight / 2 ? 1 : -1; + inputRect.top + inputRect.height / 2 < viewPortHeight / 2 ? 1 : -1; let x = inputRect.left - bodyRect.left; - let y = direction === 1 ? - inputRect.bottom - bodyRect.top - verticalShift : - inputRect.top - bodyRect.top - listRect.height + verticalShift; + let y = + direction === 1 + ? inputRect.bottom - bodyRect.top - verticalShift + : inputRect.top - + bodyRect.top - + listRect.height + + verticalShift; // remove offscreen items until whole suggestion list can fit on the // screen - while ((y < 0 || y + listRect.height > viewPortHeight) && - this._suggestionList.childNodes.length) { + while ( + (y < 0 || y + listRect.height > viewPortHeight) && + this._suggestionList.childNodes.length + ) { this._suggestionList.removeChild(this._suggestionList.lastChild); const prevHeight = listRect.height; listRect = this._suggestionDiv.getBoundingClientRect(); @@ -295,19 +308,19 @@ class AutoCompleteControl { } } - this._suggestionDiv.style.left = x + 'px'; - this._suggestionDiv.style.top = y + 'px'; + this._suggestionDiv.style.left = x + "px"; + this._suggestionDiv.style.top = y + "px"; } _refreshActiveResult() { - let activeItem = this._suggestionList.querySelector('li.active'); + let activeItem = this._suggestionList.querySelector("li.active"); if (activeItem) { - activeItem.classList.remove('active'); + activeItem.classList.remove("active"); } if (this._activeResult >= 0) { - const allItems = this._suggestionList.querySelectorAll('li'); + const allItems = this._suggestionList.querySelectorAll("li"); activeItem = allItems[this._activeResult]; - activeItem.classList.add('active'); + activeItem.classList.add("active"); } } } diff --git a/client/js/controls/comment_control.js b/client/js/controls/comment_control.js index c105bc0..cdcfd53 100644 --- a/client/js/controls/comment_control.js +++ b/client/js/controls/comment_control.js @@ -1,12 +1,12 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const misc = require('../util/misc.js'); -const events = require('../events.js'); -const views = require('../util/views.js'); +const api = require("../api.js"); +const misc = require("../util/misc.js"); +const events = require("../events.js"); +const views = require("../util/views.js"); -const template = views.getTemplate('comment'); -const scoreTemplate = views.getTemplate('score'); +const template = views.getTemplate("comment"); +const scoreTemplate = views.getTemplate("score"); class CommentControl extends events.EventTarget { constructor(hostNode, comment, onlyEditing) { @@ -16,104 +16,111 @@ class CommentControl extends events.EventTarget { this._onlyEditing = onlyEditing; if (comment) { - comment.addEventListener( - 'change', e => this._evtChange(e)); - comment.addEventListener( - 'changeScore', e => this._evtChangeScore(e)); + comment.addEventListener("change", (e) => this._evtChange(e)); + comment.addEventListener("changeScore", (e) => + this._evtChangeScore(e) + ); } const isLoggedIn = comment && api.isLoggedIn(comment.user); - const infix = isLoggedIn ? 'own' : 'any'; - views.replaceContent(this._hostNode, template({ - comment: comment, - user: comment ? comment.user : api.user, - canViewUsers: api.hasPrivilege('users:view'), - canEditComment: api.hasPrivilege(`comments:edit:${infix}`), - canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`), - onlyEditing: onlyEditing, - })); + const infix = isLoggedIn ? "own" : "any"; + views.replaceContent( + this._hostNode, + template({ + comment: comment, + user: comment ? comment.user : api.user, + canViewUsers: api.hasPrivilege("users:view"), + canEditComment: api.hasPrivilege(`comments:edit:${infix}`), + canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`), + onlyEditing: onlyEditing, + }) + ); if (this._editButtonNodes) { for (let node of this._editButtonNodes) { - node.addEventListener('click', e => this._evtEditClick(e)); + node.addEventListener("click", (e) => this._evtEditClick(e)); } } if (this._deleteButtonNode) { - this._deleteButtonNode.addEventListener( - 'click', e => this._evtDeleteClick(e)); + this._deleteButtonNode.addEventListener("click", (e) => + this._evtDeleteClick(e) + ); } if (this._previewEditingButtonNode) { - this._previewEditingButtonNode.addEventListener( - 'click', e => this._evtPreviewEditingClick(e)); + this._previewEditingButtonNode.addEventListener("click", (e) => + this._evtPreviewEditingClick(e) + ); } if (this._saveChangesButtonNode) { - this._saveChangesButtonNode.addEventListener( - 'click', e => this._evtSaveChangesClick(e)); + this._saveChangesButtonNode.addEventListener("click", (e) => + this._evtSaveChangesClick(e) + ); } if (this._cancelEditingButtonNode) { - this._cancelEditingButtonNode.addEventListener( - 'click', e => this._evtCancelEditingClick(e)); + this._cancelEditingButtonNode.addEventListener("click", (e) => + this._evtCancelEditingClick(e) + ); } this._installScore(); if (onlyEditing) { - this._selectNav('edit'); - this._selectTab('edit'); + this._selectNav("edit"); + this._selectTab("edit"); } else { - this._selectNav('readonly'); - this._selectTab('preview'); + this._selectNav("readonly"); + this._selectTab("preview"); } } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _scoreContainerNode() { - return this._hostNode.querySelector('.score-container'); + return this._hostNode.querySelector(".score-container"); } get _editButtonNodes() { - return this._hostNode.querySelectorAll('li.edit>a, a.edit'); + return this._hostNode.querySelectorAll("li.edit>a, a.edit"); } get _previewEditingButtonNode() { - return this._hostNode.querySelector('li.preview>a'); + return this._hostNode.querySelector("li.preview>a"); } get _deleteButtonNode() { - return this._hostNode.querySelector('.delete'); + return this._hostNode.querySelector(".delete"); } get _upvoteButtonNode() { - return this._hostNode.querySelector('.upvote'); + return this._hostNode.querySelector(".upvote"); } get _downvoteButtonNode() { - return this._hostNode.querySelector('.downvote'); + return this._hostNode.querySelector(".downvote"); } get _saveChangesButtonNode() { - return this._hostNode.querySelector('.save-changes'); + return this._hostNode.querySelector(".save-changes"); } get _cancelEditingButtonNode() { - return this._hostNode.querySelector('.cancel-editing'); + return this._hostNode.querySelector(".cancel-editing"); } get _textareaNode() { - return this._hostNode.querySelector('.tab.edit textarea'); + return this._hostNode.querySelector(".tab.edit textarea"); } get _contentNode() { - return this._hostNode.querySelector('.tab.preview .comment-content'); + return this._hostNode.querySelector(".tab.preview .comment-content"); } get _heightKeeperNode() { - return this._hostNode.querySelector('.keep-height'); + return this._hostNode.querySelector(".keep-height"); } _installScore() { @@ -122,32 +129,35 @@ class CommentControl extends events.EventTarget { scoreTemplate({ score: this._comment ? this._comment.score : 0, ownScore: this._comment ? this._comment.ownScore : 0, - canScore: api.hasPrivilege('comments:score'), - })); + canScore: api.hasPrivilege("comments:score"), + }) + ); if (this._upvoteButtonNode) { - this._upvoteButtonNode.addEventListener( - 'click', e => this._evtScoreClick(e, 1)); + this._upvoteButtonNode.addEventListener("click", (e) => + this._evtScoreClick(e, 1) + ); } if (this._downvoteButtonNode) { - this._downvoteButtonNode.addEventListener( - 'click', e => this._evtScoreClick(e, -1)); + this._downvoteButtonNode.addEventListener("click", (e) => + this._evtScoreClick(e, -1) + ); } } enterEditMode() { - this._selectNav('edit'); - this._selectTab('edit'); + this._selectNav("edit"); + this._selectTab("edit"); } exitEditMode() { if (this._onlyEditing) { - this._selectNav('edit'); - this._selectTab('edit'); - this._setText(''); + this._selectNav("edit"); + this._selectTab("edit"); + this._setText(""); } else { - this._selectNav('readonly'); - this._selectTab('preview'); + this._selectNav("readonly"); + this._selectTab("preview"); this._setText(this._comment.text); } this._forgetHeight(); @@ -173,27 +183,31 @@ class CommentControl extends events.EventTarget { _evtScoreClick(e, score) { e.preventDefault(); - if (!api.hasPrivilege('comments:score')) { + if (!api.hasPrivilege("comments:score")) { return; } - this.dispatchEvent(new CustomEvent('score', { - detail: { - comment: this._comment, - score: this._comment.ownScore === score ? 0 : score, - }, - })); + this.dispatchEvent( + new CustomEvent("score", { + detail: { + comment: this._comment, + score: this._comment.ownScore === score ? 0 : score, + }, + }) + ); } _evtDeleteClick(e) { e.preventDefault(); - if (!window.confirm('Are you sure you want to delete this comment?')) { + if (!window.confirm("Are you sure you want to delete this comment?")) { return; } - this.dispatchEvent(new CustomEvent('delete', { - detail: { - comment: this._comment, - }, - })); + this.dispatchEvent( + new CustomEvent("delete", { + detail: { + comment: this._comment, + }, + }) + ); } _evtChange(e) { @@ -206,21 +220,24 @@ class CommentControl extends events.EventTarget { _evtPreviewEditingClick(e) { e.preventDefault(); - this._contentNode.innerHTML = - misc.formatMarkdown(this._textareaNode.value); - this._selectTab('edit'); - this._selectTab('preview'); + this._contentNode.innerHTML = misc.formatMarkdown( + this._textareaNode.value + ); + this._selectTab("edit"); + this._selectTab("preview"); } _evtSaveChangesClick(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - target: this, - comment: this._comment, - text: this._textareaNode.value, - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + target: this, + comment: this._comment, + text: this._textareaNode.value, + }, + }) + ); } _evtCancelEditingClick(e) { @@ -234,22 +251,22 @@ class CommentControl extends events.EventTarget { } _selectNav(modeName) { - for (let node of this._hostNode.querySelectorAll('nav')) { - node.classList.toggle('active', node.classList.contains(modeName)); + for (let node of this._hostNode.querySelectorAll("nav")) { + node.classList.toggle("active", node.classList.contains(modeName)); } } _selectTab(tabName) { this._ensureHeight(); - for (let node of this._hostNode.querySelectorAll('.tab, .tabs li')) { - node.classList.toggle('active', node.classList.contains(tabName)); + for (let node of this._hostNode.querySelectorAll(".tab, .tabs li")) { + node.classList.toggle("active", node.classList.contains(tabName)); } } _ensureHeight() { this._heightKeeperNode.style.minHeight = - this._heightKeeperNode.getBoundingClientRect().height + 'px'; + this._heightKeeperNode.getBoundingClientRect().height + "px"; } _forgetHeight() { diff --git a/client/js/controls/comment_list_control.js b/client/js/controls/comment_list_control.js index 4f57fd2..7717ee5 100644 --- a/client/js/controls/comment_list_control.js +++ b/client/js/controls/comment_list_control.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); -const CommentControl = require('../controls/comment_control.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); +const CommentControl = require("../controls/comment_control.js"); -const template = views.getTemplate('comment-list'); +const template = views.getTemplate("comment-list"); class CommentListControl extends events.EventTarget { constructor(hostNode, comments, reversed) { @@ -13,8 +13,8 @@ class CommentListControl extends events.EventTarget { this._comments = comments; this._commentIdToNode = {}; - comments.addEventListener('add', e => this._evtAdd(e)); - comments.addEventListener('remove', e => this._evtRemove(e)); + comments.addEventListener("add", (e) => this._evtAdd(e)); + comments.addEventListener("remove", (e) => this._evtRemove(e)); views.replaceContent(this._hostNode, template()); @@ -28,16 +28,19 @@ class CommentListControl extends events.EventTarget { } get _commentListNode() { - return this._hostNode.querySelector('ul'); + return this._hostNode.querySelector("ul"); } _installCommentNode(comment) { - const commentListItemNode = document.createElement('li'); + const commentListItemNode = document.createElement("li"); const commentControl = new CommentControl( - commentListItemNode, comment, false); - events.proxyEvent(commentControl, this, 'submit'); - events.proxyEvent(commentControl, this, 'score'); - events.proxyEvent(commentControl, this, 'delete'); + commentListItemNode, + comment, + false + ); + events.proxyEvent(commentControl, this, "submit"); + events.proxyEvent(commentControl, this, "score"); + events.proxyEvent(commentControl, this, "delete"); this._commentIdToNode[comment.id] = commentListItemNode; this._commentListNode.appendChild(commentListItemNode); } diff --git a/client/js/controls/expander_control.js b/client/js/controls/expander_control.js index 28a4ffb..11ad3ef 100644 --- a/client/js/controls/expander_control.js +++ b/client/js/controls/expander_control.js @@ -1,26 +1,28 @@ -'use strict'; +"use strict"; -const ICON_CLASS_OPENED = 'fa-chevron-down'; -const ICON_CLASS_CLOSED = 'fa-chevron-up'; +const ICON_CLASS_OPENED = "fa-chevron-down"; +const ICON_CLASS_CLOSED = "fa-chevron-up"; -const views = require('../util/views.js'); +const views = require("../util/views.js"); -const template = views.getTemplate('expander'); +const template = views.getTemplate("expander"); class ExpanderControl { constructor(name, title, nodes) { this._name = name; - nodes = Array.from(nodes).filter(n => n); + nodes = Array.from(nodes).filter((n) => n); if (!nodes.length) { return; } - const expanderNode = template({title: title}); - const toggleLinkNode = expanderNode.querySelector('a'); - const toggleIconNode = expanderNode.querySelector('i'); - const expanderContentNode = expanderNode.querySelector('div'); - toggleLinkNode.addEventListener('click', e => this._evtToggleClick(e)); + const expanderNode = template({ title: title }); + const toggleLinkNode = expanderNode.querySelector("a"); + const toggleIconNode = expanderNode.querySelector("i"); + const expanderContentNode = expanderNode.querySelector("div"); + toggleLinkNode.addEventListener("click", (e) => + this._evtToggleClick(e) + ); nodes[0].parentNode.insertBefore(expanderNode, nodes[0]); @@ -32,29 +34,30 @@ class ExpanderControl { this._toggleIconNode = toggleIconNode; expanderNode.classList.toggle( - 'collapsed', - this._allStates[this._name] === undefined ? - false : - !this._allStates[this._name]); + "collapsed", + this._allStates[this._name] === undefined + ? false + : !this._allStates[this._name] + ); this._syncIcon(); } // eslint-disable-next-line accessor-pairs set title(newTitle) { if (this._expanderNode) { - this._expanderNode - .querySelector('header span') - .textContent = newTitle; + this._expanderNode.querySelector( + "header span" + ).textContent = newTitle; } } get _isOpened() { - return !this._expanderNode.classList.contains('collapsed'); + return !this._expanderNode.classList.contains("collapsed"); } get _allStates() { try { - return JSON.parse(localStorage.getItem('expander')) || {}; + return JSON.parse(localStorage.getItem("expander")) || {}; } catch (e) { return {}; } @@ -63,12 +66,12 @@ class ExpanderControl { _save() { const newStates = Object.assign({}, this._allStates); newStates[this._name] = this._isOpened; - localStorage.setItem('expander', JSON.stringify(newStates)); + localStorage.setItem("expander", JSON.stringify(newStates)); } _evtToggleClick(e) { e.preventDefault(); - this._expanderNode.classList.toggle('collapsed'); + this._expanderNode.classList.toggle("collapsed"); this._save(); this._syncIcon(); } diff --git a/client/js/controls/file_dropper_control.js b/client/js/controls/file_dropper_control.js index a618720..2dfb492 100644 --- a/client/js/controls/file_dropper_control.js +++ b/client/js/controls/file_dropper_control.js @@ -1,9 +1,9 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); -const template = views.getTemplate('file-dropper'); +const template = views.getTemplate("file-dropper"); const KEY_RETURN = 13; @@ -17,37 +17,42 @@ class FileDropperControl extends events.EventTarget { allowMultiple: options.allowMultiple, allowUrls: options.allowUrls, lock: options.lock, - id: 'file-' + Math.random().toString(36).substring(7), + id: "file-" + Math.random().toString(36).substring(7), urlPlaceholder: - options.urlPlaceholder || 'Alternatively, paste an URL here.', + options.urlPlaceholder || "Alternatively, paste an URL here.", }); - this._dropperNode = source.querySelector('.file-dropper'); - this._urlInputNode = source.querySelector('input[type=text]'); - this._urlConfirmButtonNode = source.querySelector('button'); - this._fileInputNode = source.querySelector('input[type=file]'); - this._fileInputNode.style.display = 'none'; + this._dropperNode = source.querySelector(".file-dropper"); + this._urlInputNode = source.querySelector("input[type=text]"); + this._urlConfirmButtonNode = source.querySelector("button"); + this._fileInputNode = source.querySelector("input[type=file]"); + this._fileInputNode.style.display = "none"; this._fileInputNode.multiple = options.allowMultiple || false; this._counter = 0; - this._dropperNode.addEventListener( - 'dragenter', e => this._evtDragEnter(e)); - this._dropperNode.addEventListener( - 'dragleave', e => this._evtDragLeave(e)); - this._dropperNode.addEventListener( - 'dragover', e => this._evtDragOver(e)); - this._dropperNode.addEventListener( - 'drop', e => this._evtDrop(e)); - this._fileInputNode.addEventListener( - 'change', e => this._evtFileChange(e)); + this._dropperNode.addEventListener("dragenter", (e) => + this._evtDragEnter(e) + ); + this._dropperNode.addEventListener("dragleave", (e) => + this._evtDragLeave(e) + ); + this._dropperNode.addEventListener("dragover", (e) => + this._evtDragOver(e) + ); + this._dropperNode.addEventListener("drop", (e) => this._evtDrop(e)); + this._fileInputNode.addEventListener("change", (e) => + this._evtFileChange(e) + ); if (this._urlInputNode) { - this._urlInputNode.addEventListener( - 'keydown', e => this._evtUrlInputKeyDown(e)); + this._urlInputNode.addEventListener("keydown", (e) => + this._evtUrlInputKeyDown(e) + ); } if (this._urlConfirmButtonNode) { - this._urlConfirmButtonNode.addEventListener( - 'click', e => this._evtUrlConfirmButtonClick(e)); + this._urlConfirmButtonNode.addEventListener("click", (e) => + this._evtUrlConfirmButtonClick(e) + ); } this._originalHtml = this._dropperNode.innerHTML; @@ -56,24 +61,27 @@ class FileDropperControl extends events.EventTarget { reset() { this._dropperNode.innerHTML = this._originalHtml; - this.dispatchEvent(new CustomEvent('reset')); + this.dispatchEvent(new CustomEvent("reset")); } _emitFiles(files) { files = Array.from(files); if (this._options.lock) { - this._dropperNode.innerText = - files.map(file => file.name).join(', '); + this._dropperNode.innerText = files + .map((file) => file.name) + .join(", "); } this.dispatchEvent( - new CustomEvent('fileadd', {detail: {files: files}})); + new CustomEvent("fileadd", { detail: { files: files } }) + ); } _emitUrls(urls) { - urls = Array.from(urls).map(url => url.trim()); + urls = Array.from(urls).map((url) => url.trim()); if (this._options.lock) { - this._dropperNode.innerText = - urls.map(url => url.split(/\//).reverse()[0]).join(', '); + this._dropperNode.innerText = urls + .map((url) => url.split(/\//).reverse()[0]) + .join(", "); } for (let url of urls) { if (!url) { @@ -84,18 +92,20 @@ class FileDropperControl extends events.EventTarget { return; } } - this.dispatchEvent(new CustomEvent('urladd', {detail: {urls: urls}})); + this.dispatchEvent( + new CustomEvent("urladd", { detail: { urls: urls } }) + ); } _evtDragEnter(e) { - this._dropperNode.classList.add('active'); + this._dropperNode.classList.add("active"); this._counter++; } _evtDragLeave(e) { this._counter--; if (this._counter === 0) { - this._dropperNode.classList.remove('active'); + this._dropperNode.classList.remove("active"); } } @@ -109,12 +119,12 @@ class FileDropperControl extends events.EventTarget { _evtDrop(e) { e.preventDefault(); - this._dropperNode.classList.remove('active'); + this._dropperNode.classList.remove("active"); if (!e.dataTransfer.files.length) { - window.alert('Only files are supported.'); + window.alert("Only files are supported."); } if (!this._options.allowMultiple && e.dataTransfer.files.length > 1) { - window.alert('Cannot select multiple files.'); + window.alert("Cannot select multiple files."); } this._emitFiles(e.dataTransfer.files); } @@ -124,16 +134,16 @@ class FileDropperControl extends events.EventTarget { return; } e.preventDefault(); - this._dropperNode.classList.remove('active'); + this._dropperNode.classList.remove("active"); this._emitUrls(this._urlInputNode.value.split(/[\r\n]/)); - this._urlInputNode.value = ''; + this._urlInputNode.value = ""; } _evtUrlConfirmButtonClick(e) { e.preventDefault(); - this._dropperNode.classList.remove('active'); + this._dropperNode.classList.remove("active"); this._emitUrls(this._urlInputNode.value.split(/[\r\n]/)); - this._urlInputNode.value = ''; + this._urlInputNode.value = ""; } } diff --git a/client/js/controls/pool_auto_complete_control.js b/client/js/controls/pool_auto_complete_control.js index f720833..97794a8 100644 --- a/client/js/controls/pool_auto_complete_control.js +++ b/client/js/controls/pool_auto_complete_control.js @@ -1,43 +1,54 @@ -'use strict'; +"use strict"; -const misc = require('../util/misc.js'); -const PoolList = require('../models/pool_list.js'); -const AutoCompleteControl = require('./auto_complete_control.js'); +const misc = require("../util/misc.js"); +const PoolList = require("../models/pool_list.js"); +const AutoCompleteControl = require("./auto_complete_control.js"); function _poolListToMatches(pools, options) { - return [...pools].sort((pool1, pool2) => { - return pool2.postCount - pool1.postCount; - }).map(pool => { - let cssName = misc.makeCssName(pool.category, 'pool'); - const caption = ( - '' - + misc.escapeHtml(pool.names[0] + ' (' + pool.postCount + ')') - + ''); - return { - caption: caption, - value: pool, - }; - }); + return [...pools] + .sort((pool1, pool2) => { + return pool2.postCount - pool1.postCount; + }) + .map((pool) => { + let cssName = misc.makeCssName(pool.category, "pool"); + const caption = + '' + + misc.escapeHtml(pool.names[0] + " (" + pool.postCount + ")") + + ""; + return { + caption: caption, + value: pool, + }; + }); } class PoolAutoCompleteControl extends AutoCompleteControl { constructor(input, options) { const minLengthForPartialSearch = 3; - options.getMatches = text => { + options.getMatches = (text) => { const term = misc.escapeSearchTerm(text); - const query = ( - text.length < minLengthForPartialSearch - ? term + '*' - : '*' + term + '*') + ' sort:post-count'; + const query = + (text.length < minLengthForPartialSearch + ? term + "*" + : "*" + term + "*") + " sort:post-count"; return new Promise((resolve, reject) => { - PoolList.search( - query, 0, this._options.maxResults, ['id', 'names', 'category', 'postCount', 'version']) - .then( - response => resolve( - _poolListToMatches(response.results, this._options)), - reject); + PoolList.search(query, 0, this._options.maxResults, [ + "id", + "names", + "category", + "postCount", + "version", + ]).then( + (response) => + resolve( + _poolListToMatches(response.results, this._options) + ), + reject + ); }); }; diff --git a/client/js/controls/pool_input_control.js b/client/js/controls/pool_input_control.js index af1c744..c8995da 100644 --- a/client/js/controls/pool_input_control.js +++ b/client/js/controls/pool_input_control.js @@ -1,24 +1,24 @@ -'use strict'; - -const api = require('../api.js'); -const pools = require('../pools.js'); -const misc = require('../util/misc.js'); -const uri = require('../util/uri.js'); -const Pool = require('../models/pool.js'); -const settings = require('../models/settings.js'); -const events = require('../events.js'); -const views = require('../util/views.js'); -const PoolAutoCompleteControl = require('./pool_auto_complete_control.js'); +"use strict"; + +const api = require("../api.js"); +const pools = require("../pools.js"); +const misc = require("../util/misc.js"); +const uri = require("../util/uri.js"); +const Pool = require("../models/pool.js"); +const settings = require("../models/settings.js"); +const events = require("../events.js"); +const views = require("../util/views.js"); +const PoolAutoCompleteControl = require("./pool_auto_complete_control.js"); const KEY_SPACE = 32; const KEY_RETURN = 13; -const SOURCE_INIT = 'init'; -const SOURCE_IMPLICATION = 'implication'; -const SOURCE_USER_INPUT = 'user-input'; -const SOURCE_CLIPBOARD = 'clipboard'; +const SOURCE_INIT = "init"; +const SOURCE_IMPLICATION = "implication"; +const SOURCE_USER_INPUT = "user-input"; +const SOURCE_CLIPBOARD = "clipboard"; -const template = views.getTemplate('pool-input'); +const template = views.getTemplate("pool-input"); function _fadeOutListItemNodeStatus(listItemNode) { if (listItemNode.classList.length) { @@ -27,8 +27,7 @@ function _fadeOutListItemNodeStatus(listItemNode) { } listItemNode.fadeTimeout = window.setTimeout(() => { while (listItemNode.classList.length) { - listItemNode.classList.remove( - listItemNode.classList.item(0)); + listItemNode.classList.remove(listItemNode.classList.item(0)); } listItemNode.fadeTimeout = null; }, 2500); @@ -45,29 +44,33 @@ class PoolInputControl extends events.EventTarget { // dom const editAreaNode = template(); this._editAreaNode = editAreaNode; - this._poolInputNode = editAreaNode.querySelector('input'); - this._poolListNode = editAreaNode.querySelector('ul.compact-pools'); + this._poolInputNode = editAreaNode.querySelector("input"); + this._poolListNode = editAreaNode.querySelector("ul.compact-pools"); this._autoCompleteControl = new PoolAutoCompleteControl( - this._poolInputNode, { + this._poolInputNode, + { getTextToFind: () => { return this._poolInputNode.value; }, - confirm: pool => { - this._poolInputNode.value = ''; + confirm: (pool) => { + this._poolInputNode.value = ""; this.addPool(pool, SOURCE_USER_INPUT); }, - delete: pool => { - this._poolInputNode.value = ''; + delete: (pool) => { + this._poolInputNode.value = ""; this.deletePool(pool); }, - verticalShift: -2 - }); + verticalShift: -2, + } + ); // show - this._hostNode.style.display = 'none'; + this._hostNode.style.display = "none"; this._hostNode.parentNode.insertBefore( - this._editAreaNode, hostNode.nextSibling); + this._editAreaNode, + hostNode.nextSibling + ); // add existing pools for (let pool of [...this.pools]) { @@ -81,19 +84,21 @@ 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'); + 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(); } @@ -107,52 +112,57 @@ class PoolInputControl extends events.EventTarget { 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; + const className = pool.category + ? misc.makeCssName(pool.category, "pool") + : null; - const poolLinkNode = document.createElement('a'); + const poolLinkNode = document.createElement("a"); if (className) { poolLinkNode.classList.add(className); } poolLinkNode.setAttribute( - 'href', uri.formatClientLink('pool', pool.names[0])); + "href", + uri.formatClientLink("pool", pool.names[0]) + ); - const poolIconNode = document.createElement('i'); - poolIconNode.classList.add('fa'); - poolIconNode.classList.add('fa-pool'); + const poolIconNode = document.createElement("i"); + poolIconNode.classList.add("fa"); + poolIconNode.classList.add("fa-pool"); poolLinkNode.appendChild(poolIconNode); - const searchLinkNode = document.createElement('a'); + 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 => { + "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'); + const listItemNode = document.createElement("li"); listItemNode.appendChild(removalLinkNode); listItemNode.appendChild(poolLinkNode); listItemNode.appendChild(searchLinkNode); diff --git a/client/js/controls/post_content_control.js b/client/js/controls/post_content_control.js index d1848b3..55daca7 100644 --- a/client/js/controls/post_content_control.js +++ b/client/js/controls/post_content_control.js @@ -1,36 +1,38 @@ -'use strict'; +"use strict"; -const settings = require('../models/settings.js'); -const views = require('../util/views.js'); -const optimizedResize = require('../util/optimized_resize.js'); +const settings = require("../models/settings.js"); +const views = require("../util/views.js"); +const optimizedResize = require("../util/optimized_resize.js"); class PostContentControl { constructor(hostNode, post, viewportSizeCalculator, fitFunctionOverride) { this._post = post; this._viewportSizeCalculator = viewportSizeCalculator; this._hostNode = hostNode; - this._template = views.getTemplate('post-content'); + this._template = views.getTemplate("post-content"); let fitMode = settings.get().fitMode; - if (typeof fitFunctionOverride !== 'undefined') { + if (typeof fitFunctionOverride !== "undefined") { fitMode = fitFunctionOverride; } - this._currentFitFunction = { - 'fit-both': this.fitBoth, - 'fit-original': this.fitOriginal, - 'fit-width': this.fitWidth, - 'fit-height': this.fitHeight, - }[fitMode] || this.fitBoth; + this._currentFitFunction = + { + "fit-both": this.fitBoth, + "fit-original": this.fitOriginal, + "fit-width": this.fitWidth, + "fit-height": this.fitHeight, + }[fitMode] || this.fitBoth; this._install(); - this._post.addEventListener( - 'changeContent', e => this._evtPostContentChange(e)); + this._post.addEventListener("changeContent", (e) => + this._evtPostContentChange(e) + ); } disableOverlay() { - this._hostNode.querySelector('.post-overlay').style.display = 'none'; + this._hostNode.querySelector(".post-overlay").style.display = "none"; } fitWidth() { @@ -92,10 +94,11 @@ class PostContentControl { _resize(width, height) { const resizeListenerNodes = [this._postContentNode].concat( - ...this._postContentNode.querySelectorAll('.resize-listener')); + ...this._postContentNode.querySelectorAll(".resize-listener") + ); for (let node of resizeListenerNodes) { - node.style.width = width + 'px'; - node.style.height = height + 'px'; + node.style.width = width + "px"; + node.style.height = height + "px"; } } @@ -106,10 +109,9 @@ class PostContentControl { _install() { this._reinstall(); optimizedResize.add(() => this._refreshSize()); - views.monitorNodeRemoval( - this._hostNode, () => { - this._uninstall(); - }); + views.monitorNodeRemoval(this._hostNode, () => { + this._uninstall(); + }); } _reinstall() { @@ -118,7 +120,7 @@ class PostContentControl { autoplay: settings.get().autoplayVideos, }); if (settings.get().transparencyGrid) { - newNode.classList.add('transparency-grid'); + newNode.classList.add("transparency-grid"); } if (this._postContentNode) { this._hostNode.replaceChild(newNode, this._postContentNode); diff --git a/client/js/controls/post_edit_sidebar_control.js b/client/js/controls/post_edit_sidebar_control.js index 495dcdb..c6b7c22 100644 --- a/client/js/controls/post_edit_sidebar_control.js +++ b/client/js/controls/post_edit_sidebar_control.js @@ -1,17 +1,17 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const events = require('../events.js'); -const misc = require('../util/misc.js'); -const views = require('../util/views.js'); -const Note = require('../models/note.js'); -const Point = require('../models/point.js'); -const TagInputControl = require('./tag_input_control.js'); -const PoolInputControl = require('./pool_input_control.js'); -const ExpanderControl = require('../controls/expander_control.js'); -const FileDropperControl = require('../controls/file_dropper_control.js'); +const api = require("../api.js"); +const events = require("../events.js"); +const misc = require("../util/misc.js"); +const views = require("../util/views.js"); +const Note = require("../models/note.js"); +const Point = require("../models/point.js"); +const TagInputControl = require("./tag_input_control.js"); +const PoolInputControl = require("./pool_input_control.js"); +const ExpanderControl = require("../controls/expander_control.js"); +const FileDropperControl = require("../controls/file_dropper_control.js"); -const template = views.getTemplate('post-edit-sidebar'); +const template = views.getTemplate("post-edit-sidebar"); class PostEditSidebarControl extends events.EventTarget { constructor(hostNode, post, postContentControl, postNotesOverlayControl) { @@ -24,182 +24,222 @@ class PostEditSidebarControl extends events.EventTarget { this._postNotesOverlayControl.switchToPassiveEdit(); - views.replaceContent(this._hostNode, template({ - post: this._post, - enableSafety: api.safetyEnabled(), - hasClipboard: document.queryCommandSupported('copy'), - canEditPostSafety: api.hasPrivilege('posts:edit:safety'), - canEditPostSource: api.hasPrivilege('posts:edit:source'), - canEditPostTags: api.hasPrivilege('posts:edit:tags'), - canEditPostRelations: api.hasPrivilege('posts:edit:relations'), - canEditPostNotes: api.hasPrivilege('posts:edit:notes') && - post.type !== 'video' && - post.type !== 'flash', - canEditPostFlags: api.hasPrivilege('posts:edit:flags'), - canEditPostContent: api.hasPrivilege('posts:edit:content'), - canEditPostThumbnail: api.hasPrivilege('posts:edit:thumbnail'), - canEditPoolPosts: api.hasPrivilege('pools:edit:posts'), - canCreateAnonymousPosts: api.hasPrivilege('posts:create:anonymous'), - canDeletePosts: api.hasPrivilege('posts:delete'), - canFeaturePosts: api.hasPrivilege('posts:feature'), - canMergePosts: api.hasPrivilege('posts:merge'), - })); + views.replaceContent( + this._hostNode, + template({ + post: this._post, + enableSafety: api.safetyEnabled(), + hasClipboard: document.queryCommandSupported("copy"), + canEditPostSafety: api.hasPrivilege("posts:edit:safety"), + canEditPostSource: api.hasPrivilege("posts:edit:source"), + canEditPostTags: api.hasPrivilege("posts:edit:tags"), + canEditPostRelations: api.hasPrivilege("posts:edit:relations"), + canEditPostNotes: + api.hasPrivilege("posts:edit:notes") && + post.type !== "video" && + post.type !== "flash", + canEditPostFlags: api.hasPrivilege("posts:edit:flags"), + canEditPostContent: api.hasPrivilege("posts:edit:content"), + canEditPostThumbnail: api.hasPrivilege("posts:edit:thumbnail"), + canEditPoolPosts: api.hasPrivilege("pools:edit:posts"), + canCreateAnonymousPosts: api.hasPrivilege( + "posts:create:anonymous" + ), + canDeletePosts: api.hasPrivilege("posts:delete"), + canFeaturePosts: api.hasPrivilege("posts:feature"), + canMergePosts: api.hasPrivilege("posts:merge"), + }) + ); new ExpanderControl( - 'post-info', - 'Basic info', - this._hostNode.querySelectorAll('.safety, .relations, .flags, .post-source')); + "post-info", + "Basic info", + this._hostNode.querySelectorAll( + ".safety, .relations, .flags, .post-source" + ) + ); this._tagsExpander = new ExpanderControl( - 'post-tags', + "post-tags", `Tags (${this._post.tags.length})`, - this._hostNode.querySelectorAll('.tags')); + this._hostNode.querySelectorAll(".tags") + ); this._notesExpander = new ExpanderControl( - 'post-notes', - 'Notes', - this._hostNode.querySelectorAll('.notes')); + "post-notes", + "Notes", + this._hostNode.querySelectorAll(".notes") + ); this._poolsExpander = new ExpanderControl( - 'post-pools', + "post-pools", `Pools (${this._post.pools.length})`, - this._hostNode.querySelectorAll('.pools')); + this._hostNode.querySelectorAll(".pools") + ); new ExpanderControl( - 'post-content', - 'Content', - this._hostNode.querySelectorAll('.post-content, .post-thumbnail')); + "post-content", + "Content", + this._hostNode.querySelectorAll(".post-content, .post-thumbnail") + ); new ExpanderControl( - 'post-management', - 'Management', - this._hostNode.querySelectorAll('.management')); + "post-management", + "Management", + this._hostNode.querySelectorAll(".management") + ); this._syncExpanderTitles(); if (this._formNode) { - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => + this._evtSubmit(e) + ); } if (this._tagInputNode) { this._tagControl = new TagInputControl( - this._tagInputNode, post.tags); + this._tagInputNode, + post.tags + ); } if (this._poolInputNode) { this._poolControl = new PoolInputControl( - this._poolInputNode, post.pools); + this._poolInputNode, + post.pools + ); } if (this._contentInputNode) { this._contentFileDropper = new FileDropperControl( - this._contentInputNode, {allowUrls: true, + this._contentInputNode, + { + allowUrls: true, lock: true, - urlPlaceholder: '...or paste an URL here.'}); - this._contentFileDropper.addEventListener('fileadd', e => { + urlPlaceholder: "...or paste an URL here.", + } + ); + this._contentFileDropper.addEventListener("fileadd", (e) => { this._newPostContent = e.detail.files[0]; }); - this._contentFileDropper.addEventListener('urladd', e => { + this._contentFileDropper.addEventListener("urladd", (e) => { this._newPostContent = e.detail.urls[0]; }); } if (this._thumbnailInputNode) { this._thumbnailFileDropper = new FileDropperControl( - this._thumbnailInputNode, {lock: true}); - this._thumbnailFileDropper.addEventListener('fileadd', e => { + this._thumbnailInputNode, + { lock: true } + ); + this._thumbnailFileDropper.addEventListener("fileadd", (e) => { this._newPostThumbnail = e.detail.files[0]; - this._thumbnailRemovalLinkNode.style.display = 'block'; + this._thumbnailRemovalLinkNode.style.display = "block"; }); } if (this._thumbnailRemovalLinkNode) { - this._thumbnailRemovalLinkNode.addEventListener( - 'click', e => this._evtRemoveThumbnailClick(e)); - this._thumbnailRemovalLinkNode.style.display = - this._post.hasCustomThumbnail ? 'block' : 'none'; + this._thumbnailRemovalLinkNode.addEventListener("click", (e) => + this._evtRemoveThumbnailClick(e) + ); + this._thumbnailRemovalLinkNode.style.display = this._post + .hasCustomThumbnail + ? "block" + : "none"; } if (this._addNoteLinkNode) { - this._addNoteLinkNode.addEventListener( - 'click', e => this._evtAddNoteClick(e)); + this._addNoteLinkNode.addEventListener("click", (e) => + this._evtAddNoteClick(e) + ); } if (this._copyNotesLinkNode) { - this._copyNotesLinkNode.addEventListener( - 'click', e => this._evtCopyNotesClick(e)); + this._copyNotesLinkNode.addEventListener("click", (e) => + this._evtCopyNotesClick(e) + ); } if (this._pasteNotesLinkNode) { - this._pasteNotesLinkNode.addEventListener( - 'click', e => this._evtPasteNotesClick(e)); + this._pasteNotesLinkNode.addEventListener("click", (e) => + this._evtPasteNotesClick(e) + ); } if (this._deleteNoteLinkNode) { - this._deleteNoteLinkNode.addEventListener( - 'click', e => this._evtDeleteNoteClick(e)); + this._deleteNoteLinkNode.addEventListener("click", (e) => + this._evtDeleteNoteClick(e) + ); } if (this._featureLinkNode) { - this._featureLinkNode.addEventListener( - 'click', e => this._evtFeatureClick(e)); + this._featureLinkNode.addEventListener("click", (e) => + this._evtFeatureClick(e) + ); } if (this._mergeLinkNode) { - this._mergeLinkNode.addEventListener( - 'click', e => this._evtMergeClick(e)); + this._mergeLinkNode.addEventListener("click", (e) => + this._evtMergeClick(e) + ); } if (this._deleteLinkNode) { - this._deleteLinkNode.addEventListener( - 'click', e => this._evtDeleteClick(e)); + this._deleteLinkNode.addEventListener("click", (e) => + this._evtDeleteClick(e) + ); } - this._postNotesOverlayControl.addEventListener( - 'blur', e => this._evtNoteBlur(e)); + this._postNotesOverlayControl.addEventListener("blur", (e) => + this._evtNoteBlur(e) + ); - this._postNotesOverlayControl.addEventListener( - 'focus', e => this._evtNoteFocus(e)); + this._postNotesOverlayControl.addEventListener("focus", (e) => + this._evtNoteFocus(e) + ); - this._post.addEventListener( - 'changeContent', e => this._evtPostContentChange(e)); + this._post.addEventListener("changeContent", (e) => + this._evtPostContentChange(e) + ); - this._post.addEventListener( - 'changeThumbnail', e => this._evtPostThumbnailChange(e)); + this._post.addEventListener("changeThumbnail", (e) => + this._evtPostThumbnailChange(e) + ); if (this._formNode) { const inputNodes = this._formNode.querySelectorAll( - 'input, textarea'); + "input, textarea" + ); for (let node of inputNodes) { - node.addEventListener( - 'change', - e => this.dispatchEvent(new CustomEvent('change'))); + node.addEventListener("change", (e) => + this.dispatchEvent(new CustomEvent("change")) + ); } - this._postNotesOverlayControl.addEventListener( - 'change', - e => this.dispatchEvent(new CustomEvent('change'))); + this._postNotesOverlayControl.addEventListener("change", (e) => + this.dispatchEvent(new CustomEvent("change")) + ); } - for (let eventType of ['add', 'remove']) { - this._post.notes.addEventListener(eventType, e => { + for (let eventType of ["add", "remove"]) { + this._post.notes.addEventListener(eventType, (e) => { this._syncExpanderTitles(); }); - this._post.pools.addEventListener(eventType, e => { + this._post.pools.addEventListener(eventType, (e) => { this._syncExpanderTitles(); }); } - this._tagControl.addEventListener( - 'change', e => { - this.dispatchEvent(new CustomEvent('change')); - this._syncExpanderTitles(); - }); + this._tagControl.addEventListener("change", (e) => { + this.dispatchEvent(new CustomEvent("change")); + this._syncExpanderTitles(); + }); if (this._noteTextareaNode) { - this._noteTextareaNode.addEventListener( - 'change', e => this._evtNoteTextChangeRequest(e)); + this._noteTextareaNode.addEventListener("change", (e) => + this._evtNoteTextChangeRequest(e) + ); } - this._poolControl.addEventListener( - 'change', e => { - this.dispatchEvent(new CustomEvent('change')); - this._syncExpanderTitles(); - }); + this._poolControl.addEventListener("change", (e) => { + this.dispatchEvent(new CustomEvent("change")); + this._syncExpanderTitles(); + }); } _syncExpanderTitles() { @@ -220,37 +260,43 @@ class PostEditSidebarControl extends events.EventTarget { e.preventDefault(); this._thumbnailFileDropper.reset(); this._newPostThumbnail = null; - this._thumbnailRemovalLinkNode.style.display = 'none'; + this._thumbnailRemovalLinkNode.style.display = "none"; } _evtFeatureClick(e) { e.preventDefault(); - if (confirm('Are you sure you want to feature this post?')) { - this.dispatchEvent(new CustomEvent('feature', { - detail: { - post: this._post, - }, - })); + if (confirm("Are you sure you want to feature this post?")) { + this.dispatchEvent( + new CustomEvent("feature", { + detail: { + post: this._post, + }, + }) + ); } } _evtMergeClick(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('merge', { - detail: { - post: this._post, - }, - })); + this.dispatchEvent( + new CustomEvent("merge", { + detail: { + post: this._post, + }, + }) + ); } _evtDeleteClick(e) { e.preventDefault(); - if (confirm('Are you sure you want to delete this post?')) { - this.dispatchEvent(new CustomEvent('delete', { - detail: { - post: this._post, - }, - })); + if (confirm("Are you sure you want to delete this post?")) { + this.dispatchEvent( + new CustomEvent("delete", { + detail: { + post: this._post, + }, + }) + ); } } @@ -262,60 +308,64 @@ class PostEditSidebarControl extends events.EventTarget { _evtNoteFocus(e) { this._editedNote = e.detail.note; - this._addNoteLinkNode.classList.remove('inactive'); - this._deleteNoteLinkNode.classList.remove('inactive'); - this._noteTextareaNode.removeAttribute('disabled'); + this._addNoteLinkNode.classList.remove("inactive"); + this._deleteNoteLinkNode.classList.remove("inactive"); + this._noteTextareaNode.removeAttribute("disabled"); this._noteTextareaNode.value = e.detail.note.text; } _evtNoteBlur(e) { this._evtNoteTextChangeRequest(null); - this._addNoteLinkNode.classList.remove('inactive'); - this._deleteNoteLinkNode.classList.add('inactive'); + this._addNoteLinkNode.classList.remove("inactive"); + this._deleteNoteLinkNode.classList.add("inactive"); this._noteTextareaNode.blur(); - this._noteTextareaNode.setAttribute('disabled', 'disabled'); - this._noteTextareaNode.value = ''; + this._noteTextareaNode.setAttribute("disabled", "disabled"); + this._noteTextareaNode.value = ""; } _evtAddNoteClick(e) { e.preventDefault(); - if (e.target.classList.contains('inactive')) { + if (e.target.classList.contains("inactive")) { return; } - this._addNoteLinkNode.classList.add('inactive'); + this._addNoteLinkNode.classList.add("inactive"); this._postNotesOverlayControl.switchToDrawing(); } _evtCopyNotesClick(e) { e.preventDefault(); - let textarea = document.createElement('textarea'); - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - textarea.value = JSON.stringify([...this._post.notes].map(note => ({ - polygon: [...note.polygon].map( - point => [point.x, point.y]), - text: note.text, - }))); + let textarea = document.createElement("textarea"); + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + textarea.value = JSON.stringify( + [...this._post.notes].map((note) => ({ + polygon: [...note.polygon].map((point) => [point.x, point.y]), + text: note.text, + })) + ); document.body.appendChild(textarea); textarea.select(); let success = false; try { - success = document.execCommand('copy'); + success = document.execCommand("copy"); } catch (err) { // continue regardless of error } textarea.blur(); document.body.removeChild(textarea); - alert(success - ? 'Notes copied to clipboard.' - : 'Failed to copy the text to clipboard. Sorry.'); + alert( + success + ? "Notes copied to clipboard." + : "Failed to copy the text to clipboard. Sorry." + ); } _evtPasteNotesClick(e) { e.preventDefault(); const text = window.prompt( - 'Please enter the exported notes snapshot:'); + "Please enter the exported notes snapshot:" + ); if (!text) { return; } @@ -333,7 +383,7 @@ class PostEditSidebarControl extends events.EventTarget { _evtDeleteNoteClick(e) { e.preventDefault(); - if (e.target.classList.contains('inactive')) { + if (e.target.classList.contains("inactive")) { return; } this._post.notes.remove(this._editedNote); @@ -342,72 +392,78 @@ class PostEditSidebarControl extends events.EventTarget { _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - post: this._post, - - safety: this._safetyButtonNodes.length ? - Array.from(this._safetyButtonNodes) - .filter(node => node.checked)[0] - .value.toLowerCase() : - undefined, - - flags: this._videoFlags, - - tags: this._tagInputNode ? - misc.splitByWhitespace(this._tagInputNode.value) : - undefined, - - pools: this._poolInputNode ? - misc.splitByWhitespace(this._poolInputNode.value) : - undefined, - - relations: this._relationsInputNode ? - misc.splitByWhitespace(this._relationsInputNode.value) - .map(x => parseInt(x)) : - undefined, - - content: this._newPostContent ? - this._newPostContent : - undefined, - - thumbnail: this._newPostThumbnail !== undefined ? - this._newPostThumbnail : - undefined, + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + post: this._post, - source: this._sourceInputNode ? - this._sourceInputNode.value : - undefined, - }, - })); + safety: this._safetyButtonNodes.length + ? Array.from(this._safetyButtonNodes) + .filter((node) => node.checked)[0] + .value.toLowerCase() + : undefined, + + flags: this._videoFlags, + + tags: this._tagInputNode + ? misc.splitByWhitespace(this._tagInputNode.value) + : undefined, + + pools: this._poolInputNode + ? misc.splitByWhitespace(this._poolInputNode.value) + : undefined, + + relations: this._relationsInputNode + ? misc + .splitByWhitespace( + this._relationsInputNode.value + ) + .map((x) => parseInt(x)) + : undefined, + + content: this._newPostContent + ? this._newPostContent + : undefined, + + thumbnail: + this._newPostThumbnail !== undefined + ? this._newPostThumbnail + : undefined, + + source: this._sourceInputNode + ? this._sourceInputNode.value + : undefined, + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _submitButtonNode() { - return this._hostNode.querySelector('.submit'); + return this._hostNode.querySelector(".submit"); } get _safetyButtonNodes() { - return this._formNode.querySelectorAll('.safety input'); + return this._formNode.querySelectorAll(".safety input"); } get _tagInputNode() { - return this._formNode.querySelector('.tags input'); + return this._formNode.querySelector(".tags input"); } get _poolInputNode() { - return this._formNode.querySelector('.pools input'); + return this._formNode.querySelector(".pools input"); } get _loopVideoInputNode() { - return this._formNode.querySelector('.flags input[name=loop]'); + return this._formNode.querySelector(".flags input[name=loop]"); } get _soundVideoInputNode() { - return this._formNode.querySelector('.flags input[name=sound]'); + return this._formNode.querySelector(".flags input[name=sound]"); } get _videoFlags() { @@ -416,65 +472,68 @@ class PostEditSidebarControl extends events.EventTarget { } let ret = []; if (this._loopVideoInputNode.checked) { - ret.push('loop'); + ret.push("loop"); } if (this._soundVideoInputNode.checked) { - ret.push('sound'); + ret.push("sound"); } return ret; } get _relationsInputNode() { - return this._formNode.querySelector('.relations input'); + return this._formNode.querySelector(".relations input"); } get _contentInputNode() { - return this._formNode.querySelector('.post-content .dropper-container'); + return this._formNode.querySelector( + ".post-content .dropper-container" + ); } get _thumbnailInputNode() { return this._formNode.querySelector( - '.post-thumbnail .dropper-container'); + ".post-thumbnail .dropper-container" + ); } get _thumbnailRemovalLinkNode() { - return this._formNode.querySelector('.post-thumbnail a'); + return this._formNode.querySelector(".post-thumbnail a"); } get _sourceInputNode() { - return this._formNode.querySelector('.post-source textarea'); + return this._formNode.querySelector(".post-source textarea"); } get _featureLinkNode() { - return this._formNode.querySelector('.management .feature'); + return this._formNode.querySelector(".management .feature"); } get _mergeLinkNode() { - return this._formNode.querySelector('.management .merge'); + return this._formNode.querySelector(".management .merge"); } get _deleteLinkNode() { - return this._formNode.querySelector('.management .delete'); + return this._formNode.querySelector(".management .delete"); } get _addNoteLinkNode() { - return this._formNode.querySelector('.notes .add'); + return this._formNode.querySelector(".notes .add"); } get _copyNotesLinkNode() { - return this._formNode.querySelector('.notes .copy'); + return this._formNode.querySelector(".notes .copy"); } get _pasteNotesLinkNode() { - return this._formNode.querySelector('.notes .paste'); + return this._formNode.querySelector(".notes .paste"); } get _deleteNoteLinkNode() { - return this._formNode.querySelector('.notes .delete'); + return this._formNode.querySelector(".notes .delete"); } get _noteTextareaNode() { - return this._formNode.querySelector('.notes textarea'); + return this._formNode.querySelector(".notes textarea"); } enableForm() { diff --git a/client/js/controls/post_notes_overlay_control.js b/client/js/controls/post_notes_overlay_control.js index 7bf69b2..030f7f2 100644 --- a/client/js/controls/post_notes_overlay_control.js +++ b/client/js/controls/post_notes_overlay_control.js @@ -1,13 +1,13 @@ -'use strict'; +"use strict"; -const keyboard = require('../util/keyboard.js'); -const views = require('../util/views.js'); -const events = require('../events.js'); -const misc = require('../util/misc.js'); -const Note = require('../models/note.js'); -const Point = require('../models/point.js'); +const keyboard = require("../util/keyboard.js"); +const views = require("../util/views.js"); +const events = require("../events.js"); +const misc = require("../util/misc.js"); +const Note = require("../models/note.js"); +const Point = require("../models/point.js"); -const svgNS = 'http://www.w3.org/2000/svg'; +const svgNS = "http://www.w3.org/2000/svg"; const snapThreshold = 10; const circleSize = 10; @@ -22,19 +22,19 @@ const KEY_RETURN = 13; function _getDistance(point1, point2) { return Math.sqrt( - Math.pow(point1.x - point2.x, 2) + - Math.pow(point1.y - point2.y, 2)); + Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2) + ); } function _setNodeState(node, stateName) { if (node === null) { return; } - node.setAttribute('data-state', stateName); + node.setAttribute("data-state", stateName); } function _clearEditedNote(hostNode) { - const node = hostNode.querySelector('[data-state=\'editing\']'); + const node = hostNode.querySelector("[data-state='editing']"); _setNodeState(node, null); return node !== null; } @@ -48,7 +48,7 @@ function _getNoteCentroid(note) { const y0 = note.polygon.at(i).y; const x1 = note.polygon.at((i + 1) % vertexCount).x; const y1 = note.polygon.at((i + 1) % vertexCount).y; - const a = (x0 * y1) - (x1 * y0); + const a = x0 * y1 - x1 * y0; signedArea += a; centroid.x += (x0 + x1) * a; centroid.y += (y0 + y1) * a; @@ -82,32 +82,30 @@ class State { return false; } - evtCanvasKeyDown(e) { - } + evtCanvasKeyDown(e) {} - evtNoteMouseDown(e, hoveredNote) { - } + evtNoteMouseDown(e, hoveredNote) {} - evtCanvasMouseDown(e) { - } + evtCanvasMouseDown(e) {} - evtCanvasMouseMove(e) { - } + evtCanvasMouseMove(e) {} - evtCanvasMouseUp(e) { - } + evtCanvasMouseUp(e) {} _getScreenPoint(point) { return new Point( point.x * this._control.boundingBox.width, - point.y * this._control.boundingBox.height); + point.y * this._control.boundingBox.height + ); } _snapPoints(targetPoint, referencePoint) { const targetScreenPoint = this._getScreenPoint(targetPoint); const referenceScreenPoint = this._getScreenPoint(referencePoint); - if (_getDistance(targetScreenPoint, referenceScreenPoint) < - snapThreshold) { + if ( + _getDistance(targetScreenPoint, referenceScreenPoint) < + snapThreshold + ) { targetPoint.x = referencePoint.x; targetPoint.y = referencePoint.y; } @@ -124,15 +122,16 @@ class State { (e.clientX - this._control.boundingBox.left) / this._control.boundingBox.width, (e.clientY - this._control.boundingBox.top) / - this._control.boundingBox.height); + this._control.boundingBox.height + ); } } class ReadOnlyState extends State { constructor(control) { - super(control, 'read-only'); + super(control, "read-only"); if (_clearEditedNote(control._hostNode)) { - this._control.dispatchEvent(new CustomEvent('blur')); + this._control.dispatchEvent(new CustomEvent("blur")); } keyboard.unpause(); } @@ -144,9 +143,9 @@ class ReadOnlyState extends State { class PassiveState extends State { constructor(control) { - super(control, 'passive'); + super(control, "passive"); if (_clearEditedNote(control._hostNode)) { - this._control.dispatchEvent(new CustomEvent('blur')); + this._control.dispatchEvent(new CustomEvent("blur")); } keyboard.unpause(); } @@ -164,23 +163,24 @@ class ActiveState extends State { constructor(control, note, stateName) { super(control, stateName); if (_clearEditedNote(control._hostNode)) { - this._control.dispatchEvent(new CustomEvent('blur')); + this._control.dispatchEvent(new CustomEvent("blur")); } keyboard.pause(); if (note !== null) { this._note = note; this._control.dispatchEvent( - new CustomEvent('focus', { - detail: {note: note}, - })); - _setNodeState(this._note.groupNode, 'editing'); + new CustomEvent("focus", { + detail: { note: note }, + }) + ); + _setNodeState(this._note.groupNode, "editing"); } } } class SelectedState extends ActiveState { constructor(control, note) { - super(control, note, 'selected'); + super(control, note, "selected"); this._clickTimeout = null; this._control._hideNoteText(); } @@ -211,27 +211,40 @@ class SelectedState extends ActiveState { const mouseScreenPoint = this._getScreenPoint(mousePoint); if (e.shiftKey) { this._control._state = new ScalingNoteState( - this._control, this._note, mousePoint); + this._control, + this._note, + mousePoint + ); return; } if (this._note !== hoveredNote) { - this._control._state = - new SelectedState(this._control, hoveredNote); + this._control._state = new SelectedState( + this._control, + hoveredNote + ); return; } this._clickTimeout = window.setTimeout(() => { for (let polygonPoint of this._note.polygon) { const distance = _getDistance( mouseScreenPoint, - this._getScreenPoint(polygonPoint)); + this._getScreenPoint(polygonPoint) + ); if (distance < circleSize) { this._control._state = new MovingPointState( - this._control, this._note, polygonPoint, mousePoint); + this._control, + this._note, + polygonPoint, + mousePoint + ); return; } } this._control._state = new MovingNoteState( - this._control, this._note, mousePoint); + this._control, + this._note, + mousePoint + ); }, 100); } @@ -241,9 +254,12 @@ class SelectedState extends ActiveState { for (let polygonPoint of this._note.polygon) { const distance = _getDistance( mouseScreenPoint, - this._getScreenPoint(polygonPoint)); + this._getScreenPoint(polygonPoint) + ); polygonPoint.edgeNode.classList.toggle( - 'nearby', distance < circleSize); + "nearby", + distance < circleSize + ); } } @@ -252,16 +268,24 @@ class SelectedState extends ActiveState { const mouseScreenPoint = this._getScreenPoint(mousePoint); if (e.shiftKey) { this._control._state = new ScalingNoteState( - this._control, this._note, mousePoint); + this._control, + this._note, + mousePoint + ); return; } for (let polygonPoint of this._note.polygon) { const distance = _getDistance( mouseScreenPoint, - this._getScreenPoint(polygonPoint)); + this._getScreenPoint(polygonPoint) + ); if (distance < circleSize) { this._control._state = new MovingPointState( - this._control, this._note, polygonPoint, mousePoint); + this._control, + this._note, + polygonPoint, + mousePoint + ); return; } } @@ -283,32 +307,37 @@ class SelectedState extends ActiveState { const origin = _getNoteCentroid(this._note); const originalSize = _getNoteSize(this._note); const targetSize = new Point( - originalSize.x + (x / this._control.boundingBox.width), - originalSize.y + (y / this._control.boundingBox.height)); + originalSize.x + x / this._control.boundingBox.width, + originalSize.y + y / this._control.boundingBox.height + ); const scale = new Point( targetSize.x / originalSize.x, - targetSize.y / originalSize.y); + targetSize.y / originalSize.y + ); for (let point of this._note.polygon) { - point.x = origin.x + ((point.x - origin.x) * scale.x); - point.y = origin.y + ((point.y - origin.y) * scale.y); + point.x = origin.x + (point.x - origin.x) * scale.x; + point.y = origin.y + (point.y - origin.y) * scale.y; } } } class MovingPointState extends ActiveState { constructor(control, note, notePoint, mousePoint) { - super(control, note, 'moving-point'); + super(control, note, "moving-point"); this._notePoint = notePoint; - this._originalNotePoint = {x: notePoint.x, y: notePoint.y}; + this._originalNotePoint = { x: notePoint.x, y: notePoint.y }; this._originalPosition = mousePoint; - _setNodeState(this._note.groupNode, 'editing'); + _setNodeState(this._note.groupNode, "editing"); } evtCanvasKeyDown(e) { if (e.which === KEY_ESCAPE) { this._notePoint.x = this._originalNotePoint.x; this._notePoint.y = this._originalNotePoint.y; - this._control._state = new SelectedState(this._control, this._note); + this._control._state = new SelectedState( + this._control, + this._note + ); } } @@ -326,9 +355,11 @@ class MovingPointState extends ActiveState { class MovingNoteState extends ActiveState { constructor(control, note, mousePoint) { - super(control, note, 'moving-note'); - this._originalPolygon = [...note.polygon].map( - point => ({x: point.x, y: point.y})); + super(control, note, "moving-note"); + this._originalPolygon = [...note.polygon].map((point) => ({ + x: point.x, + y: point.y, + })); this._originalPosition = mousePoint; } @@ -338,7 +369,10 @@ class MovingNoteState extends ActiveState { this._note.polygon.at(i).x = this._originalPolygon[i].x; this._note.polygon.at(i).y = this._originalPolygon[i].y; } - this._control._state = new SelectedState(this._control, this._note); + this._control._state = new SelectedState( + this._control, + this._note + ); } } @@ -358,9 +392,11 @@ class MovingNoteState extends ActiveState { class ScalingNoteState extends ActiveState { constructor(control, note, mousePoint) { - super(control, note, 'scaling-note'); - this._originalPolygon = [...note.polygon].map( - point => ({x: point.x, y: point.y})); + super(control, note, "scaling-note"); + this._originalPolygon = [...note.polygon].map((point) => ({ + x: point.x, + y: point.y, + })); this._originalMousePoint = mousePoint; this._originalSize = _getNoteSize(note); } @@ -371,7 +407,10 @@ class ScalingNoteState extends ActiveState { this._note.polygon.at(i).x = this._originalPolygon[i].x; this._note.polygon.at(i).y = this._originalPolygon[i].y; } - this._control._state = new SelectedState(this._control, this._note); + this._control._state = new SelectedState( + this._control, + this._note + ); } } @@ -384,12 +423,16 @@ class ScalingNoteState extends ActiveState { const originalPolygonPoint = this._originalPolygon[i]; polygonPoint.x = originalMousePoint.x + - ((originalPolygonPoint.x - originalMousePoint.x) * - (1 + ((mousePoint.x - originalMousePoint.x) / originalSize.x))); + (originalPolygonPoint.x - originalMousePoint.x) * + (1 + + (mousePoint.x - originalMousePoint.x) / + originalSize.x); polygonPoint.y = originalMousePoint.y + - ((originalPolygonPoint.y - originalMousePoint.y) * - (1 + ((mousePoint.y - originalMousePoint.y) / originalSize.y))); + (originalPolygonPoint.y - originalMousePoint.y) * + (1 + + (mousePoint.y - originalMousePoint.y) / + originalSize.y); } } @@ -400,7 +443,7 @@ class ScalingNoteState extends ActiveState { class ReadyToDrawState extends ActiveState { constructor(control) { - super(control, null, 'ready-to-draw'); + super(control, null, "ready-to-draw"); } evtNoteMouseDown(e, hoveredNote) { @@ -411,23 +454,27 @@ class ReadyToDrawState extends ActiveState { const mousePoint = this._getPointFromEvent(e); if (e.shiftKey) { this._control._state = new DrawingRectangleState( - this._control, mousePoint); + this._control, + mousePoint + ); } else { this._control._state = new DrawingPolygonState( - this._control, mousePoint); + this._control, + mousePoint + ); } } } class DrawingRectangleState extends ActiveState { constructor(control, mousePoint) { - super(control, null, 'drawing-rectangle'); + super(control, null, "drawing-rectangle"); this._note = this._createNote(); this._note.polygon.add(new Point(mousePoint.x, mousePoint.y)); this._note.polygon.add(new Point(mousePoint.x, mousePoint.y)); this._note.polygon.add(new Point(mousePoint.x, mousePoint.y)); this._note.polygon.add(new Point(mousePoint.x, mousePoint.y)); - _setNodeState(this._note.groupNode, 'drawing'); + _setNodeState(this._note.groupNode, "drawing"); } evtCanvasMouseUp(e) { @@ -443,7 +490,10 @@ class DrawingRectangleState extends ActiveState { this._control._state = new ReadyToDrawState(this._control); } else { this._control._post.notes.add(this._note); - this._control._state = new SelectedState(this._control, this._note); + this._control._state = new SelectedState( + this._control, + this._note + ); } } @@ -458,11 +508,11 @@ class DrawingRectangleState extends ActiveState { class DrawingPolygonState extends ActiveState { constructor(control, mousePoint) { - super(control, null, 'drawing-polygon'); + super(control, null, "drawing-polygon"); this._note = this._createNote(); this._note.polygon.add(new Point(mousePoint.x, mousePoint.y)); this._note.polygon.add(new Point(mousePoint.x, mousePoint.y)); - _setNodeState(this._note.groupNode, 'drawing'); + _setNodeState(this._note.groupNode, "drawing"); } evtCanvasKeyDown(e) { @@ -502,11 +552,16 @@ class DrawingPolygonState extends ActiveState { } if (e.shiftKey && secondLastPoint) { - const direction = (Math.round( - Math.atan2( - secondLastPoint.y - mousePoint.y, - secondLastPoint.x - mousePoint.x) / - (2 * Math.PI / 4)) + 4) % 4; + const direction = + (Math.round( + Math.atan2( + secondLastPoint.y - mousePoint.y, + secondLastPoint.x - mousePoint.x + ) / + ((2 * Math.PI) / 4) + ) + + 4) % + 4; if (direction === 0 || direction === 2) { lastPoint.x = mousePoint.x; lastPoint.y = secondLastPoint.y; @@ -533,7 +588,10 @@ class DrawingPolygonState extends ActiveState { } else { this._control._deleteDomNode(this._note); this._control._post.notes.add(this._note); - this._control._state = new SelectedState(this._control, this._note); + this._control._state = new SelectedState( + this._control, + this._note + ); } } } @@ -544,48 +602,51 @@ class PostNotesOverlayControl extends events.EventTarget { this._post = post; this._hostNode = hostNode; - this._svgNode = document.createElementNS(svgNS, 'svg'); - this._svgNode.classList.add('resize-listener'); - this._svgNode.classList.add('notes-overlay'); - this._svgNode.setAttribute('preserveAspectRatio', 'none'); - this._svgNode.setAttribute('viewBox', '0 0 1 1'); + this._svgNode = document.createElementNS(svgNS, "svg"); + this._svgNode.classList.add("resize-listener"); + this._svgNode.classList.add("notes-overlay"); + this._svgNode.setAttribute("preserveAspectRatio", "none"); + this._svgNode.setAttribute("viewBox", "0 0 1 1"); for (let note of this._post.notes) { this._createPolygonNode(note); } this._hostNode.appendChild(this._svgNode); - this._post.addEventListener('change', e => this._evtPostChange(e)); - this._post.notes.addEventListener('remove', e => { + this._post.addEventListener("change", (e) => this._evtPostChange(e)); + this._post.notes.addEventListener("remove", (e) => { this._deleteDomNode(e.detail.note); }); - this._post.notes.addEventListener('add', e => { + this._post.notes.addEventListener("add", (e) => { this._createPolygonNode(e.detail.note); }); - const keyHandler = e => this._evtCanvasKeyDown(e); - document.addEventListener('keydown', keyHandler); - this._svgNode.addEventListener( - 'mousedown', e => this._evtCanvasMouseDown(e)); - this._svgNode.addEventListener( - 'mouseup', e => this._evtCanvasMouseUp(e)); - this._svgNode.addEventListener( - 'mousemove', e => this._evtCanvasMouseMove(e)); - - const wrapperNode = document.createElement('div'); - wrapperNode.classList.add('wrapper'); - this._textNode = document.createElement('div'); - this._textNode.classList.add('note-text'); + const keyHandler = (e) => this._evtCanvasKeyDown(e); + document.addEventListener("keydown", keyHandler); + this._svgNode.addEventListener("mousedown", (e) => + this._evtCanvasMouseDown(e) + ); + this._svgNode.addEventListener("mouseup", (e) => + this._evtCanvasMouseUp(e) + ); + this._svgNode.addEventListener("mousemove", (e) => + this._evtCanvasMouseMove(e) + ); + + const wrapperNode = document.createElement("div"); + wrapperNode.classList.add("wrapper"); + this._textNode = document.createElement("div"); + this._textNode.classList.add("note-text"); this._textNode.appendChild(wrapperNode); - this._textNode.addEventListener( - 'mouseleave', e => this._evtNoteMouseLeave(e)); + this._textNode.addEventListener("mouseleave", (e) => + this._evtNoteMouseLeave(e) + ); document.body.appendChild(this._textNode); - views.monitorNodeRemoval( - this._hostNode, () => { - this._hostNode.removeChild(this._svgNode); - document.removeEventListener('keydown', keyHandler); - document.body.removeChild(this._textNode); - this._state = new ReadOnlyState(this); - }); + views.monitorNodeRemoval(this._hostNode, () => { + this._hostNode.removeChild(this._svgNode); + document.removeEventListener("keydown", keyHandler); + document.body.removeChild(this._textNode); + this._state = new ReadOnlyState(this); + }); this._state = new ReadOnlyState(this); } @@ -613,7 +674,7 @@ class PostNotesOverlayControl extends events.EventTarget { } _evtCanvasKeyDown(e) { - const illegalNodeNames = ['textarea', 'input', 'select']; + const illegalNodeNames = ["textarea", "input", "select"]; if (illegalNodeNames.includes(e.target.nodeName.toLowerCase())) { return; } @@ -655,53 +716,58 @@ class PostNotesOverlayControl extends events.EventTarget { _evtNoteMouseLeave(e) { const newElement = e.relatedTarget; - if (newElement === this._svgNode || - (!this._svgNode.contains(newElement) && + if ( + newElement === this._svgNode || + (!this._svgNode.contains(newElement) && !this._textNode.contains(newElement) && - newElement !== this._textNode)) { + newElement !== this._textNode) + ) { this._hideNoteText(); } } _showNoteText(note) { - this._textNode.querySelector('.wrapper').innerHTML = - misc.formatMarkdown(note.text); - this._textNode.style.display = 'block'; + this._textNode.querySelector( + ".wrapper" + ).innerHTML = misc.formatMarkdown(note.text); + this._textNode.style.display = "block"; const bodyRect = document.body.getBoundingClientRect(); const noteRect = this._textNode.getBoundingClientRect(); const svgRect = this.boundingBox; const centroid = _getNoteCentroid(note); - const x = ( + const x = -bodyRect.left + svgRect.left + - (svgRect.width * centroid.x) - - (noteRect.width / 2)); - const y = ( + svgRect.width * centroid.x - + noteRect.width / 2; + const y = -bodyRect.top + svgRect.top + - (svgRect.height * centroid.y) - - (noteRect.height / 2)); - this._textNode.style.left = x + 'px'; - this._textNode.style.top = y + 'px'; + svgRect.height * centroid.y - + noteRect.height / 2; + this._textNode.style.left = x + "px"; + this._textNode.style.top = y + "px"; } _hideNoteText() { - this._textNode.style.display = 'none'; + this._textNode.style.display = "none"; } _updatePolygonNotePoints(note) { note.polygonNode.setAttribute( - 'points', - [...note.polygon].map( - point => [point.x, point.y].join(',')).join(' ')); + "points", + [...note.polygon] + .map((point) => [point.x, point.y].join(",")) + .join(" ") + ); } _createEdgeNode(point, groupNode) { - const node = document.createElementNS(svgNS, 'ellipse'); - node.setAttribute('cx', point.x); - node.setAttribute('cy', point.y); - node.setAttribute('rx', circleSize / 2 / this.boundingBox.width); - node.setAttribute('ry', circleSize / 2 / this.boundingBox.height); + const node = document.createElementNS(svgNS, "ellipse"); + node.setAttribute("cx", point.x); + node.setAttribute("cy", point.y); + node.setAttribute("rx", circleSize / 2 / this.boundingBox.width); + node.setAttribute("ry", circleSize / 2 / this.boundingBox.height); point.edgeNode = node; groupNode.appendChild(node); } @@ -713,8 +779,8 @@ class PostNotesOverlayControl extends events.EventTarget { _updateEdgeNode(point, note) { this._updatePolygonNotePoints(note); - point.edgeNode.setAttribute('cx', point.x); - point.edgeNode.setAttribute('cy', point.y); + point.edgeNode.setAttribute("cx", point.x); + point.edgeNode.setAttribute("cy", point.y); } _deleteDomNode(note) { @@ -722,17 +788,19 @@ class PostNotesOverlayControl extends events.EventTarget { } _createPolygonNode(note) { - const groupNode = document.createElementNS(svgNS, 'g'); + const groupNode = document.createElementNS(svgNS, "g"); note.groupNode = groupNode; { - const node = document.createElementNS(svgNS, 'polygon'); + const node = document.createElementNS(svgNS, "polygon"); note.polygonNode = node; - node.setAttribute('vector-effect', 'non-scaling-stroke'); - node.setAttribute('stroke-alignment', 'inside'); - node.addEventListener( - 'mouseenter', e => this._evtNoteMouseEnter(e, note)); - node.addEventListener( - 'mouseleave', e => this._evtNoteMouseLeave(e)); + node.setAttribute("vector-effect", "non-scaling-stroke"); + node.setAttribute("stroke-alignment", "inside"); + node.addEventListener("mouseenter", (e) => + this._evtNoteMouseEnter(e, note) + ); + node.addEventListener("mouseleave", (e) => + this._evtNoteMouseLeave(e) + ); this._updatePolygonNotePoints(note); groupNode.appendChild(node); } @@ -740,17 +808,17 @@ class PostNotesOverlayControl extends events.EventTarget { this._createEdgeNode(point, groupNode); } - note.polygon.addEventListener('change', e => { + note.polygon.addEventListener("change", (e) => { this._updateEdgeNode(e.detail.point, note); - this.dispatchEvent(new CustomEvent('change')); + this.dispatchEvent(new CustomEvent("change")); }); - note.polygon.addEventListener('remove', e => { + note.polygon.addEventListener("remove", (e) => { this._deleteEdgeNode(e.detail.point, note); - this.dispatchEvent(new CustomEvent('change')); + this.dispatchEvent(new CustomEvent("change")); }); - note.polygon.addEventListener('add', e => { + note.polygon.addEventListener("add", (e) => { this._createEdgeNode(e.detail.point, groupNode); - this.dispatchEvent(new CustomEvent('change')); + this.dispatchEvent(new CustomEvent("change")); }); this._svgNode.appendChild(groupNode); diff --git a/client/js/controls/post_readonly_sidebar_control.js b/client/js/controls/post_readonly_sidebar_control.js index 388c238..580ef43 100644 --- a/client/js/controls/post_readonly_sidebar_control.js +++ b/client/js/controls/post_readonly_sidebar_control.js @@ -1,14 +1,14 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const events = require('../events.js'); -const views = require('../util/views.js'); -const uri = require('../util/uri.js'); -const misc = require('../util/misc.js'); +const api = require("../api.js"); +const events = require("../events.js"); +const views = require("../util/views.js"); +const uri = require("../util/uri.js"); +const misc = require("../util/misc.js"); -const template = views.getTemplate('post-readonly-sidebar'); -const scoreTemplate = views.getTemplate('score'); -const favTemplate = views.getTemplate('fav'); +const template = views.getTemplate("post-readonly-sidebar"); +const scoreTemplate = views.getTemplate("score"); +const favTemplate = views.getTemplate("fav"); class PostReadonlySidebarControl extends events.EventTarget { constructor(hostNode, post, postContentControl) { @@ -17,19 +17,22 @@ class PostReadonlySidebarControl extends events.EventTarget { this._post = post; this._postContentControl = postContentControl; - post.addEventListener('changeFavorite', e => this._evtChangeFav(e)); - post.addEventListener('changeScore', e => this._evtChangeScore(e)); + post.addEventListener("changeFavorite", (e) => this._evtChangeFav(e)); + post.addEventListener("changeScore", (e) => this._evtChangeScore(e)); - views.replaceContent(this._hostNode, template({ - post: this._post, - enableSafety: api.safetyEnabled(), - canListPosts: api.hasPrivilege('posts:list'), - canEditPosts: api.hasPrivilege('posts:edit'), - canViewTags: api.hasPrivilege('tags:view'), - escapeColons: uri.escapeColons, - extractRootDomain: uri.extractRootDomain, - getPrettyTagName: misc.getPrettyTagName, - })); + views.replaceContent( + this._hostNode, + template({ + post: this._post, + enableSafety: api.safetyEnabled(), + canListPosts: api.hasPrivilege("posts:list"), + canEditPosts: api.hasPrivilege("posts:edit"), + canViewTags: api.hasPrivilege("tags:view"), + escapeColons: uri.escapeColons, + extractRootDomain: uri.extractRootDomain, + getPrettyTagName: misc.getPrettyTagName, + }) + ); this._installFav(); this._installScore(); @@ -38,58 +41,62 @@ class PostReadonlySidebarControl extends events.EventTarget { } get _scoreContainerNode() { - return this._hostNode.querySelector('.score-container'); + return this._hostNode.querySelector(".score-container"); } get _favContainerNode() { - return this._hostNode.querySelector('.fav-container'); + return this._hostNode.querySelector(".fav-container"); } get _upvoteButtonNode() { - return this._hostNode.querySelector('.upvote'); + return this._hostNode.querySelector(".upvote"); } get _downvoteButtonNode() { - return this._hostNode.querySelector('.downvote'); + return this._hostNode.querySelector(".downvote"); } get _addFavButtonNode() { - return this._hostNode.querySelector('.add-favorite'); + return this._hostNode.querySelector(".add-favorite"); } get _remFavButtonNode() { - return this._hostNode.querySelector('.remove-favorite'); + return this._hostNode.querySelector(".remove-favorite"); } get _fitBothButtonNode() { - return this._hostNode.querySelector('.fit-both'); + return this._hostNode.querySelector(".fit-both"); } get _fitOriginalButtonNode() { - return this._hostNode.querySelector('.fit-original'); + return this._hostNode.querySelector(".fit-original"); } get _fitWidthButtonNode() { - return this._hostNode.querySelector('.fit-width'); + return this._hostNode.querySelector(".fit-width"); } get _fitHeightButtonNode() { - return this._hostNode.querySelector('.fit-height'); + return this._hostNode.querySelector(".fit-height"); } _installFitButtons() { this._fitBothButtonNode.addEventListener( - 'click', this._eventZoomProxy( - () => this._postContentControl.fitBoth())); + "click", + this._eventZoomProxy(() => this._postContentControl.fitBoth()) + ); this._fitOriginalButtonNode.addEventListener( - 'click', this._eventZoomProxy( - () => this._postContentControl.fitOriginal())); + "click", + this._eventZoomProxy(() => this._postContentControl.fitOriginal()) + ); this._fitWidthButtonNode.addEventListener( - 'click', this._eventZoomProxy( - () => this._postContentControl.fitWidth())); + "click", + this._eventZoomProxy(() => this._postContentControl.fitWidth()) + ); this._fitHeightButtonNode.addEventListener( - 'click', this._eventZoomProxy( - () => this._postContentControl.fitHeight())); + "click", + this._eventZoomProxy(() => this._postContentControl.fitHeight()) + ); } _installFav() { @@ -98,16 +105,19 @@ class PostReadonlySidebarControl extends events.EventTarget { favTemplate({ favoriteCount: this._post.favoriteCount, ownFavorite: this._post.ownFavorite, - canFavorite: api.hasPrivilege('posts:favorite'), - })); + canFavorite: api.hasPrivilege("posts:favorite"), + }) + ); if (this._addFavButtonNode) { - this._addFavButtonNode.addEventListener( - 'click', e => this._evtAddToFavoritesClick(e)); + this._addFavButtonNode.addEventListener("click", (e) => + this._evtAddToFavoritesClick(e) + ); } if (this._remFavButtonNode) { - this._remFavButtonNode.addEventListener( - 'click', e => this._evtRemoveFromFavoritesClick(e)); + this._remFavButtonNode.addEventListener("click", (e) => + this._evtRemoveFromFavoritesClick(e) + ); } } @@ -117,77 +127,88 @@ class PostReadonlySidebarControl extends events.EventTarget { scoreTemplate({ score: this._post.score, ownScore: this._post.ownScore, - canScore: api.hasPrivilege('posts:score'), - })); + canScore: api.hasPrivilege("posts:score"), + }) + ); if (this._upvoteButtonNode) { - this._upvoteButtonNode.addEventListener( - 'click', e => this._evtScoreClick(e, 1)); + this._upvoteButtonNode.addEventListener("click", (e) => + this._evtScoreClick(e, 1) + ); } if (this._downvoteButtonNode) { - this._downvoteButtonNode.addEventListener( - 'click', e => this._evtScoreClick(e, -1)); + this._downvoteButtonNode.addEventListener("click", (e) => + this._evtScoreClick(e, -1) + ); } } _eventZoomProxy(func) { - return e => { + return (e) => { e.preventDefault(); e.target.blur(); func(); this._syncFitButton(); - this.dispatchEvent(new CustomEvent('fitModeChange', { - detail: { - mode: this._getFitMode(), - }, - })); + this.dispatchEvent( + new CustomEvent("fitModeChange", { + detail: { + mode: this._getFitMode(), + }, + }) + ); }; } _getFitMode() { const funcToName = {}; - funcToName[this._postContentControl.fitBoth] = 'fit-both'; - funcToName[this._postContentControl.fitOriginal] = 'fit-original'; - funcToName[this._postContentControl.fitWidth] = 'fit-width'; - funcToName[this._postContentControl.fitHeight] = 'fit-height'; + funcToName[this._postContentControl.fitBoth] = "fit-both"; + funcToName[this._postContentControl.fitOriginal] = "fit-original"; + funcToName[this._postContentControl.fitWidth] = "fit-width"; + funcToName[this._postContentControl.fitHeight] = "fit-height"; return funcToName[this._postContentControl._currentFitFunction]; } _syncFitButton() { const className = this._getFitMode(); - const oldNode = this._hostNode.querySelector('.zoom a.active'); + const oldNode = this._hostNode.querySelector(".zoom a.active"); const newNode = this._hostNode.querySelector(`.zoom a.${className}`); if (oldNode) { - oldNode.classList.remove('active'); + oldNode.classList.remove("active"); } - newNode.classList.add('active'); + newNode.classList.add("active"); } _evtAddToFavoritesClick(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('favorite', { - detail: { - post: this._post, - }, - })); + this.dispatchEvent( + new CustomEvent("favorite", { + detail: { + post: this._post, + }, + }) + ); } _evtRemoveFromFavoritesClick(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('unfavorite', { - detail: { - post: this._post, - }, - })); + this.dispatchEvent( + new CustomEvent("unfavorite", { + detail: { + post: this._post, + }, + }) + ); } _evtScoreClick(e, score) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('score', { - detail: { - post: this._post, - score: this._post.ownScore === score ? 0 : score, - }, - })); + this.dispatchEvent( + new CustomEvent("score", { + detail: { + post: this._post, + score: this._post.ownScore === score ? 0 : score, + }, + }) + ); } _evtChangeFav(e) { diff --git a/client/js/controls/tag_auto_complete_control.js b/client/js/controls/tag_auto_complete_control.js index f8d3f6c..8ee6aa8 100644 --- a/client/js/controls/tag_auto_complete_control.js +++ b/client/js/controls/tag_auto_complete_control.js @@ -1,51 +1,63 @@ -'use strict'; +"use strict"; -const misc = require('../util/misc.js'); -const views = require('../util/views.js'); -const TagList = require('../models/tag_list.js'); -const AutoCompleteControl = require('./auto_complete_control.js'); +const misc = require("../util/misc.js"); +const views = require("../util/views.js"); +const TagList = require("../models/tag_list.js"); +const AutoCompleteControl = require("./auto_complete_control.js"); function _tagListToMatches(tags, options) { - return [...tags].sort((tag1, tag2) => { - return tag2.usages - tag1.usages; - }).map(tag => { - let cssName = misc.makeCssName(tag.category, 'tag'); - if (options.isTaggedWith(tag.names[0])) { - cssName += ' disabled'; - } - const caption = ( - '' - + misc.escapeHtml(tag.names[0] + ' (' + tag.postCount + ')') - + ''); - return { - caption: caption, - value: tag, - }; - }); + return [...tags] + .sort((tag1, tag2) => { + return tag2.usages - tag1.usages; + }) + .map((tag) => { + let cssName = misc.makeCssName(tag.category, "tag"); + if (options.isTaggedWith(tag.names[0])) { + cssName += " disabled"; + } + const caption = + '' + + misc.escapeHtml(tag.names[0] + " (" + tag.postCount + ")") + + ""; + return { + caption: caption, + value: tag, + }; + }); } class TagAutoCompleteControl extends AutoCompleteControl { constructor(input, options) { const minLengthForPartialSearch = 3; - options = Object.assign({ - isTaggedWith: tag => false, - }, options); + options = Object.assign( + { + isTaggedWith: (tag) => false, + }, + options + ); - options.getMatches = text => { + options.getMatches = (text) => { const term = misc.escapeSearchTerm(text); - const query = ( - text.length < minLengthForPartialSearch - ? term + '*' - : '*' + term + '*') + ' sort:usages'; + const query = + (text.length < minLengthForPartialSearch + ? term + "*" + : "*" + term + "*") + " sort:usages"; return new Promise((resolve, reject) => { - TagList.search( - query, 0, this._options.maxResults, ['names', 'category', 'usages']) - .then( - response => resolve( - _tagListToMatches(response.results, this._options)), - reject); + TagList.search(query, 0, this._options.maxResults, [ + "names", + "category", + "usages", + ]).then( + (response) => + resolve( + _tagListToMatches(response.results, this._options) + ), + reject + ); }); }; diff --git a/client/js/controls/tag_input_control.js b/client/js/controls/tag_input_control.js index e0b1339..7d069f9 100644 --- a/client/js/controls/tag_input_control.js +++ b/client/js/controls/tag_input_control.js @@ -1,25 +1,25 @@ -'use strict'; - -const api = require('../api.js'); -const tags = require('../tags.js'); -const misc = require('../util/misc.js'); -const uri = require('../util/uri.js'); -const Tag = require('../models/tag.js'); -const settings = require('../models/settings.js'); -const events = require('../events.js'); -const views = require('../util/views.js'); -const TagAutoCompleteControl = require('./tag_auto_complete_control.js'); +"use strict"; + +const api = require("../api.js"); +const tags = require("../tags.js"); +const misc = require("../util/misc.js"); +const uri = require("../util/uri.js"); +const Tag = require("../models/tag.js"); +const settings = require("../models/settings.js"); +const events = require("../events.js"); +const views = require("../util/views.js"); +const TagAutoCompleteControl = require("./tag_auto_complete_control.js"); const KEY_SPACE = 32; const KEY_RETURN = 13; -const SOURCE_INIT = 'init'; -const SOURCE_IMPLICATION = 'implication'; -const SOURCE_USER_INPUT = 'user-input'; -const SOURCE_SUGGESTION = 'suggestions'; -const SOURCE_CLIPBOARD = 'clipboard'; +const SOURCE_INIT = "init"; +const SOURCE_IMPLICATION = "implication"; +const SOURCE_USER_INPUT = "user-input"; +const SOURCE_SUGGESTION = "suggestions"; +const SOURCE_CLIPBOARD = "clipboard"; -const template = views.getTemplate('tag-input'); +const template = views.getTemplate("tag-input"); function _fadeOutListItemNodeStatus(listItemNode) { if (listItemNode.classList.length) { @@ -28,8 +28,7 @@ function _fadeOutListItemNodeStatus(listItemNode) { } listItemNode.fadeTimeout = window.setTimeout(() => { while (listItemNode.classList.length) { - listItemNode.classList.remove( - listItemNode.classList.item(0)); + listItemNode.classList.remove(listItemNode.classList.item(0)); } listItemNode.fadeTimeout = null; }, 2500); @@ -51,7 +50,9 @@ class SuggestionList { } set(suggestion, weight) { - if (Object.prototype.hasOwnProperty.call(this._suggestions, suggestion)) { + if ( + Object.prototype.hasOwnProperty.call(this._suggestions, suggestion) + ) { weight = Math.max(weight, this._suggestions[suggestion]); } this._suggestions[suggestion] = weight; @@ -74,8 +75,8 @@ class SuggestionList { let nameDiff = a[0].localeCompare(b[0]); return weightDiff === 0 ? nameDiff : weightDiff; }); - return tuples.map(tuple => { - return {tagName: tuple[0], weight: tuple[1]}; + return tuples.map((tuple) => { + return { tagName: tuple[0], weight: tuple[1] }; }); } } @@ -91,45 +92,58 @@ class TagInputControl extends events.EventTarget { // dom const editAreaNode = template(); this._editAreaNode = editAreaNode; - this._tagInputNode = editAreaNode.querySelector('input'); - this._suggestionsNode = editAreaNode.querySelector('.tag-suggestions'); - this._tagListNode = editAreaNode.querySelector('ul.compact-tags'); + this._tagInputNode = editAreaNode.querySelector("input"); + this._suggestionsNode = editAreaNode.querySelector(".tag-suggestions"); + this._tagListNode = editAreaNode.querySelector("ul.compact-tags"); this._autoCompleteControl = new TagAutoCompleteControl( - this._tagInputNode, { + this._tagInputNode, + { getTextToFind: () => { return this._tagInputNode.value; }, - confirm: tag => { - this._tagInputNode.value = ''; + confirm: (tag) => { + this._tagInputNode.value = ""; // note: tags from autocomplete don't contain implications // so they need to be looked up in API this.addTagByName(tag.names[0], SOURCE_USER_INPUT); }, - delete: tag => { - this._tagInputNode.value = ''; + delete: (tag) => { + this._tagInputNode.value = ""; this.deleteTag(tag); }, verticalShift: -2, - isTaggedWith: tagName => this.tags.isTaggedWith(tagName), - }); + isTaggedWith: (tagName) => this.tags.isTaggedWith(tagName), + } + ); // dom events - this._tagInputNode.addEventListener( - 'keydown', e => this._evtInputKeyDown(e)); - this._tagInputNode.addEventListener( - 'paste', e => this._evtInputPaste(e)); - this._editAreaNode.querySelector('a.opacity').addEventListener( - 'click', e => this._evtToggleSuggestionsPopupOpacityClick(e)); - this._editAreaNode.querySelector('a.close').addEventListener( - 'click', e => this._evtCloseSuggestionsPopupClick(e)); - this._editAreaNode.querySelector('button').addEventListener( - 'click', e => this._evtAddTagButtonClick(e)); + this._tagInputNode.addEventListener("keydown", (e) => + this._evtInputKeyDown(e) + ); + this._tagInputNode.addEventListener("paste", (e) => + this._evtInputPaste(e) + ); + this._editAreaNode + .querySelector("a.opacity") + .addEventListener("click", (e) => + this._evtToggleSuggestionsPopupOpacityClick(e) + ); + this._editAreaNode + .querySelector("a.close") + .addEventListener("click", (e) => + this._evtCloseSuggestionsPopupClick(e) + ); + this._editAreaNode + .querySelector("button") + .addEventListener("click", (e) => this._evtAddTagButtonClick(e)); // show - this._hostNode.style.display = 'none'; + this._hostNode.style.display = "none"; this._hostNode.parentNode.insertBefore( - this._editAreaNode, hostNode.nextSibling); + this._editAreaNode, + hostNode.nextSibling + ); // add existing tags for (let tag of [...this.tags]) { @@ -139,7 +153,10 @@ class TagInputControl extends events.EventTarget { } addTagByText(text, source) { - for (let tagName of text.split(/\s+/).filter(word => word).reverse()) { + for (let tagName of text + .split(/\s+/) + .filter((word) => word) + .reverse()) { this.addTagByName(tagName, source); } } @@ -149,48 +166,60 @@ class TagInputControl extends events.EventTarget { if (!name) { return; } - return Tag.get(name).then(tag => { - return this.addTag(tag, source); - }, () => { - const tag = new Tag(); - tag.names = [name]; - tag.category = null; - return this.addTag(tag, source); - }); + return Tag.get(name).then( + (tag) => { + return this.addTag(tag, source); + }, + () => { + const tag = new Tag(); + tag.names = [name]; + tag.category = null; + return this.addTag(tag, source); + } + ); } addTag(tag, source) { if (source !== SOURCE_INIT && this.tags.isTaggedWith(tag.names[0])) { const listItemNode = this._getListItemNode(tag); if (source !== SOURCE_IMPLICATION) { - listItemNode.classList.add('duplicate'); + listItemNode.classList.add("duplicate"); _fadeOutListItemNodeStatus(listItemNode); } return Promise.resolve(); } - return this.tags.addByName(tag.names[0], false).then(() => { - const listItemNode = this._createListItemNode(tag); - if (!tag.category) { - listItemNode.classList.add('new'); - } - if (source === SOURCE_IMPLICATION) { - listItemNode.classList.add('implication'); - } - this._tagListNode.prependChild(listItemNode); - _fadeOutListItemNodeStatus(listItemNode); - - return Promise.all( - tag.implications.map( - implication => this.addTagByName( - implication.names[0], SOURCE_IMPLICATION))); - }).then(() => { - this.dispatchEvent(new CustomEvent('add', { - detail: {tag: tag, source: source}, - })); - this.dispatchEvent(new CustomEvent('change')); - return Promise.resolve(); - }); + return this.tags + .addByName(tag.names[0], false) + .then(() => { + const listItemNode = this._createListItemNode(tag); + if (!tag.category) { + listItemNode.classList.add("new"); + } + if (source === SOURCE_IMPLICATION) { + listItemNode.classList.add("implication"); + } + this._tagListNode.prependChild(listItemNode); + _fadeOutListItemNodeStatus(listItemNode); + + return Promise.all( + tag.implications.map((implication) => + this.addTagByName( + implication.names[0], + SOURCE_IMPLICATION + ) + ) + ); + }) + .then(() => { + this.dispatchEvent( + new CustomEvent("add", { + detail: { tag: tag, source: source }, + }) + ); + this.dispatchEvent(new CustomEvent("change")); + return Promise.resolve(); + }); } deleteTag(tag) { @@ -202,25 +231,27 @@ class TagInputControl extends events.EventTarget { this._deleteListItemNode(tag); - this.dispatchEvent(new CustomEvent('remove', { - detail: {tag: tag}, - })); - this.dispatchEvent(new CustomEvent('change')); + this.dispatchEvent( + new CustomEvent("remove", { + detail: { tag: tag }, + }) + ); + this.dispatchEvent(new CustomEvent("change")); } _evtInputPaste(e) { e.preventDefault(); - const pastedText = window.clipboardData ? - window.clipboardData.getData('Text') : - (e.originalEvent || e).clipboardData.getData('text/plain'); + const pastedText = window.clipboardData + ? window.clipboardData.getData("Text") + : (e.originalEvent || e).clipboardData.getData("text/plain"); if (pastedText.length > 2000) { - window.alert('Pasted text is too long.'); + window.alert("Pasted text is too long."); return; } this._hideAutoComplete(); this.addTagByText(pastedText, SOURCE_CLIPBOARD); - this._tagInputNode.value = ''; + this._tagInputNode.value = ""; } _evtCloseSuggestionsPopupClick(e) { @@ -231,7 +262,7 @@ class TagInputControl extends events.EventTarget { _evtAddTagButtonClick(e) { e.preventDefault(); this.addTagByName(this._tagInputNode.value, SOURCE_USER_INPUT); - this._tagInputNode.value = ''; + this._tagInputNode.value = ""; } _evtToggleSuggestionsPopupOpacityClick(e) { @@ -244,36 +275,41 @@ class TagInputControl extends events.EventTarget { e.preventDefault(); this._hideAutoComplete(); this.addTagByText(this._tagInputNode.value, SOURCE_USER_INPUT); - this._tagInputNode.value = ''; + this._tagInputNode.value = ""; } } _createListItemNode(tag) { - const className = tag.category ? - misc.makeCssName(tag.category, 'tag') : - null; + const className = tag.category + ? misc.makeCssName(tag.category, "tag") + : null; - const tagLinkNode = document.createElement('a'); + const tagLinkNode = document.createElement("a"); if (className) { tagLinkNode.classList.add(className); } tagLinkNode.setAttribute( - 'href', uri.formatClientLink('tag', tag.names[0])); + "href", + uri.formatClientLink("tag", tag.names[0]) + ); - const tagIconNode = document.createElement('i'); - tagIconNode.classList.add('fa'); - tagIconNode.classList.add('fa-tag'); + const tagIconNode = document.createElement("i"); + tagIconNode.classList.add("fa"); + tagIconNode.classList.add("fa-tag"); tagLinkNode.appendChild(tagIconNode); - const searchLinkNode = document.createElement('a'); + const searchLinkNode = document.createElement("a"); if (className) { searchLinkNode.classList.add(className); } searchLinkNode.setAttribute( - 'href', uri.formatClientLink( - 'posts', {query: uri.escapeColons(tag.names[0])})); - searchLinkNode.textContent = tag.names[0] + ' '; - searchLinkNode.addEventListener('click', e => { + "href", + uri.formatClientLink("posts", { + query: uri.escapeColons(tag.names[0]), + }) + ); + searchLinkNode.textContent = tag.names[0] + " "; + searchLinkNode.addEventListener("click", (e) => { e.preventDefault(); this._suggestions.clear(); if (tag.postCount > 0) { @@ -284,20 +320,20 @@ class TagInputControl extends events.EventTarget { } }); - const usagesNode = document.createElement('span'); - usagesNode.classList.add('tag-usages'); - usagesNode.setAttribute('data-pseudo-content', tag.postCount); + const usagesNode = document.createElement("span"); + usagesNode.classList.add("tag-usages"); + usagesNode.setAttribute("data-pseudo-content", tag.postCount); - const removalLinkNode = document.createElement('a'); - removalLinkNode.classList.add('remove-tag'); - removalLinkNode.setAttribute('href', ''); - removalLinkNode.setAttribute('data-pseudo-content', '×'); - removalLinkNode.addEventListener('click', e => { + const removalLinkNode = document.createElement("a"); + removalLinkNode.classList.add("remove-tag"); + removalLinkNode.setAttribute("href", ""); + removalLinkNode.setAttribute("data-pseudo-content", "×"); + removalLinkNode.addEventListener("click", (e) => { e.preventDefault(); this.deleteTag(tag); }); - const listItemNode = document.createElement('li'); + const listItemNode = document.createElement("li"); listItemNode.appendChild(removalLinkNode); listItemNode.appendChild(tagLinkNode); listItemNode.appendChild(searchLinkNode); @@ -327,20 +363,25 @@ class TagInputControl extends events.EventTarget { if (!browsingSettings.tagSuggestions) { return; } - api.get( - uri.formatApiLink('tag-siblings', tag.names[0]), - {noProgress: true}) - .then(response => { - return Promise.resolve(response.results); - }, response => { - return Promise.resolve([]); - }).then(siblings => { - const args = siblings.map(s => s.occurrences); + api.get(uri.formatApiLink("tag-siblings", tag.names[0]), { + noProgress: true, + }) + .then( + (response) => { + return Promise.resolve(response.results); + }, + (response) => { + return Promise.resolve([]); + } + ) + .then((siblings) => { + const args = siblings.map((s) => s.occurrences); let maxSiblingOccurrences = Math.max(1, ...args); for (let sibling of siblings) { this._suggestions.set( sibling.tag.names[0], - sibling.occurrences * 4.9 / maxSiblingOccurrences); + (sibling.occurrences * 4.9) / maxSiblingOccurrences + ); } for (let suggestion of tag.suggestions || []) { this._suggestions.set(suggestion, 5); @@ -354,10 +395,10 @@ class TagInputControl extends events.EventTarget { } _refreshSuggestionsPopup() { - if (!this._suggestionsNode.classList.contains('shown')) { + if (!this._suggestionsNode.classList.contains("shown")) { return; } - const listNode = this._suggestionsNode.querySelector('ul'); + const listNode = this._suggestionsNode.querySelector("ul"); listNode.scrollTop = 0; while (listNode.firstChild) { listNode.removeChild(listNode.firstChild); @@ -369,35 +410,36 @@ class TagInputControl extends events.EventTarget { continue; } - const addLinkNode = document.createElement('a'); + const addLinkNode = document.createElement("a"); addLinkNode.textContent = tagName; - addLinkNode.classList.add('add-tag'); - addLinkNode.setAttribute('href', ''); - Tag.get(tagName).then(tag => { + addLinkNode.classList.add("add-tag"); + addLinkNode.setAttribute("href", ""); + Tag.get(tagName).then((tag) => { addLinkNode.classList.add( - misc.makeCssName(tag.category, 'tag')); + misc.makeCssName(tag.category, "tag") + ); }); - addLinkNode.addEventListener('click', e => { + addLinkNode.addEventListener("click", (e) => { e.preventDefault(); listNode.removeChild(listItemNode); this.addTagByName(tagName, SOURCE_SUGGESTION); }); - const weightNode = document.createElement('span'); - weightNode.classList.add('tag-weight'); - weightNode.setAttribute('data-pseudo-content', weight); + const weightNode = document.createElement("span"); + weightNode.classList.add("tag-weight"); + weightNode.setAttribute("data-pseudo-content", weight); - const removalLinkNode = document.createElement('a'); - removalLinkNode.classList.add('remove-tag'); - removalLinkNode.setAttribute('href', ''); - removalLinkNode.setAttribute('data-pseudo-content', '×'); - removalLinkNode.addEventListener('click', e => { + const removalLinkNode = document.createElement("a"); + removalLinkNode.classList.add("remove-tag"); + removalLinkNode.setAttribute("href", ""); + removalLinkNode.setAttribute("data-pseudo-content", "×"); + removalLinkNode.addEventListener("click", (e) => { e.preventDefault(); listNode.removeChild(listItemNode); this._suggestions.ban(tagName); }); - const listItemNode = document.createElement('li'); + const listItemNode = document.createElement("li"); listItemNode.appendChild(removalLinkNode); listItemNode.appendChild(weightNode); listItemNode.appendChild(addLinkNode); @@ -407,19 +449,19 @@ class TagInputControl extends events.EventTarget { _closeSuggestionsPopup() { this._suggestions.clear(); - this._suggestionsNode.classList.remove('shown'); + this._suggestionsNode.classList.remove("shown"); } _removeSuggestionsPopupOpacity() { - this._suggestionsNode.classList.remove('translucent'); + this._suggestionsNode.classList.remove("translucent"); } _toggleSuggestionsPopupOpacity() { - this._suggestionsNode.classList.toggle('translucent'); + this._suggestionsNode.classList.toggle("translucent"); } _openSuggestionsPopup() { - this._suggestionsNode.classList.add('shown'); + this._suggestionsNode.classList.add("shown"); this._refreshSuggestionsPopup(); } diff --git a/client/js/events.js b/client/js/events.js index 945771a..7d6ffa6 100644 --- a/client/js/events.js +++ b/client/js/events.js @@ -1,12 +1,12 @@ -'use strict'; +"use strict"; class EventTarget { constructor() { this.eventTarget = document.createDocumentFragment(); for (let method of [ - 'addEventListener', - 'dispatchEvent', - 'removeEventListener' + "addEventListener", + "dispatchEvent", + "removeEventListener", ]) { this[method] = this.eventTarget[method].bind(this.eventTarget); } @@ -20,17 +20,19 @@ function proxyEvent(source, target, sourceEventType, targetEventType) { if (!targetEventType) { targetEventType = sourceEventType; } - source.addEventListener(sourceEventType, e => { - target.dispatchEvent(new CustomEvent(targetEventType, { - detail: e.detail, - })); + source.addEventListener(sourceEventType, (e) => { + target.dispatchEvent( + new CustomEvent(targetEventType, { + detail: e.detail, + }) + ); }); } module.exports = { - Success: 'success', - Error: 'error', - Info: 'info', + Success: "success", + Error: "error", + Info: "info", proxyEvent: proxyEvent, EventTarget: EventTarget, diff --git a/client/js/main.js b/client/js/main.js index 8406c79..a6e6d6a 100644 --- a/client/js/main.js +++ b/client/js/main.js @@ -1,82 +1,101 @@ -'use strict'; +"use strict"; -require('./util/polyfill.js'); -const misc = require('./util/misc.js'); -const views = require('./util/views.js'); -const router = require('./router.js'); +require("./util/polyfill.js"); +const misc = require("./util/misc.js"); +const views = require("./util/views.js"); +const router = require("./router.js"); -history.scrollRestoration = 'manual'; +history.scrollRestoration = "manual"; -router.exit( - null, - (ctx, next) => { - ctx.state.scrollX = window.scrollX; - ctx.state.scrollY = window.scrollY; - router.replace(router.url, ctx.state); - if (misc.confirmPageExit()) { - next(); - } - }); - -const mousetrap = require('mousetrap'); -router.enter( - null, - (ctx, next) => { - mousetrap.reset(); +router.exit(null, (ctx, next) => { + ctx.state.scrollX = window.scrollX; + ctx.state.scrollY = window.scrollY; + router.replace(router.url, ctx.state); + if (misc.confirmPageExit()) { next(); - }); + } +}); -const tags = require('./tags.js'); -const pools = require('./pools.js'); -const api = require('./api.js'); +const mousetrap = require("mousetrap"); +router.enter(null, (ctx, next) => { + mousetrap.reset(); + next(); +}); -api.fetchConfig().then(() => { - // register controller routes - let controllers = []; - controllers.push(require('./controllers/home_controller.js')); - controllers.push(require('./controllers/help_controller.js')); - controllers.push(require('./controllers/auth_controller.js')); - controllers.push(require('./controllers/password_reset_controller.js')); - controllers.push(require('./controllers/comments_controller.js')); - controllers.push(require('./controllers/snapshots_controller.js')); - controllers.push(require('./controllers/post_detail_controller.js')); - controllers.push(require('./controllers/post_main_controller.js')); - controllers.push(require('./controllers/post_list_controller.js')); - controllers.push(require('./controllers/post_upload_controller.js')); - controllers.push(require('./controllers/tag_controller.js')); - controllers.push(require('./controllers/tag_list_controller.js')); - controllers.push(require('./controllers/tag_categories_controller.js')); - controllers.push(require('./controllers/pool_create_controller.js')); - controllers.push(require('./controllers/pool_controller.js')); - controllers.push(require('./controllers/pool_list_controller.js')); - controllers.push(require('./controllers/pool_categories_controller.js')); - controllers.push(require('./controllers/settings_controller.js')); - controllers.push(require('./controllers/user_controller.js')); - controllers.push(require('./controllers/user_list_controller.js')); - controllers.push(require('./controllers/user_registration_controller.js')); +const tags = require("./tags.js"); +const pools = require("./pools.js"); +const api = require("./api.js"); - // 404 controller needs to be registered last - controllers.push(require('./controllers/not_found_controller.js')); +api.fetchConfig() + .then( + () => { + // register controller routes + let controllers = []; + controllers.push(require("./controllers/home_controller.js")); + controllers.push(require("./controllers/help_controller.js")); + controllers.push(require("./controllers/auth_controller.js")); + controllers.push( + require("./controllers/password_reset_controller.js") + ); + controllers.push(require("./controllers/comments_controller.js")); + controllers.push(require("./controllers/snapshots_controller.js")); + controllers.push( + require("./controllers/post_detail_controller.js") + ); + controllers.push(require("./controllers/post_main_controller.js")); + controllers.push(require("./controllers/post_list_controller.js")); + controllers.push( + require("./controllers/post_upload_controller.js") + ); + controllers.push(require("./controllers/tag_controller.js")); + controllers.push(require("./controllers/tag_list_controller.js")); + controllers.push( + require("./controllers/tag_categories_controller.js") + ); + controllers.push( + require("./controllers/pool_create_controller.js") + ); + controllers.push(require("./controllers/pool_controller.js")); + controllers.push(require("./controllers/pool_list_controller.js")); + controllers.push( + require("./controllers/pool_categories_controller.js") + ); + controllers.push(require("./controllers/settings_controller.js")); + controllers.push(require("./controllers/user_controller.js")); + controllers.push(require("./controllers/user_list_controller.js")); + controllers.push( + require("./controllers/user_registration_controller.js") + ); - for (let controller of controllers) { - controller(router); - } -}, error => { - window.alert('Could not fetch basic configuration from server'); -}).then(() => { - api.loginFromCookies().then(() => { - tags.refreshCategoryColorMap(); - pools.refreshCategoryColorMap(); - router.start(); - }, error => { - if (window.location.href.indexOf('login') !== -1) { - api.forget(); - router.start(); - } else { - const ctx = router.start('/'); - ctx.controller.showError( - 'An error happened while trying to log you in: ' + - error.message); + // 404 controller needs to be registered last + controllers.push(require("./controllers/not_found_controller.js")); + + for (let controller of controllers) { + controller(router); + } + }, + (error) => { + window.alert("Could not fetch basic configuration from server"); } + ) + .then(() => { + api.loginFromCookies().then( + () => { + tags.refreshCategoryColorMap(); + pools.refreshCategoryColorMap(); + router.start(); + }, + (error) => { + if (window.location.href.indexOf("login") !== -1) { + api.forget(); + router.start(); + } else { + const ctx = router.start("/"); + ctx.controller.showError( + "An error happened while trying to log you in: " + + error.message + ); + } + } + ); }); -}); diff --git a/client/js/models/abstract_list.js b/client/js/models/abstract_list.js index fb4dec8..7cf518c 100644 --- a/client/js/models/abstract_list.js +++ b/client/js/models/abstract_list.js @@ -1,6 +1,6 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); +const events = require("../events.js"); class AbstractList extends events.EventTarget { constructor() { @@ -13,13 +13,15 @@ class AbstractList extends events.EventTarget { for (let item of response) { const addedItem = this._itemClass.fromResponse(item); if (addedItem.addEventListener) { - addedItem.addEventListener('delete', e => { + addedItem.addEventListener("delete", (e) => { ret.remove(addedItem); }); - addedItem.addEventListener('change', e => { - ret.dispatchEvent(new CustomEvent('change', { - detail: e.detail, - })); + addedItem.addEventListener("change", (e) => { + ret.dispatchEvent( + new CustomEvent("change", { + detail: e.detail, + }) + ); }); } ret._list.push(addedItem); @@ -29,28 +31,32 @@ class AbstractList extends events.EventTarget { sync(plainList) { this.clear(); - for (let item of (plainList || [])) { + for (let item of plainList || []) { this.add(this.constructor._itemClass.fromResponse(item)); } } add(item) { if (item.addEventListener) { - item.addEventListener('delete', e => { + item.addEventListener("delete", (e) => { this.remove(item); }); - item.addEventListener('change', e => { - this.dispatchEvent(new CustomEvent('change', { - detail: e.detail, - })); + item.addEventListener("change", (e) => { + this.dispatchEvent( + new CustomEvent("change", { + detail: e.detail, + }) + ); }); } this._list.push(item); const detail = {}; detail[this.constructor._itemName] = item; - this.dispatchEvent(new CustomEvent('add', { - detail: detail, - })); + this.dispatchEvent( + new CustomEvent("add", { + detail: detail, + }) + ); } clear() { @@ -67,9 +73,11 @@ class AbstractList extends events.EventTarget { this._list.splice(index, 1); const detail = {}; detail[this.constructor._itemName] = itemToRemove; - this.dispatchEvent(new CustomEvent('remove', { - detail: detail, - })); + this.dispatchEvent( + new CustomEvent("remove", { + detail: detail, + }) + ); return; } } diff --git a/client/js/models/comment.js b/client/js/models/comment.js index e292ec4..4707a3a 100644 --- a/client/js/models/comment.js +++ b/client/js/models/comment.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const events = require('../events.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const events = require("../events.js"); class Comment extends events.EventTarget { constructor() { @@ -31,7 +31,7 @@ class Comment extends events.EventTarget { } get text() { - return this._text || ''; + return this._text || ""; } get user() { @@ -63,47 +63,57 @@ class Comment extends events.EventTarget { version: this._version, text: this._text, }; - let promise = this._id ? - api.put(uri.formatApiLink('comment', this.id), detail) : - api.post(uri.formatApiLink('comments'), - Object.assign({postId: this._postId}, detail)); - - return promise.then(response => { + let promise = this._id + ? api.put(uri.formatApiLink("comment", this.id), detail) + : api.post( + uri.formatApiLink("comments"), + Object.assign({ postId: this._postId }, detail) + ); + + return promise.then((response) => { this._updateFromResponse(response); - this.dispatchEvent(new CustomEvent('change', { - detail: { - comment: this, - }, - })); + this.dispatchEvent( + new CustomEvent("change", { + detail: { + comment: this, + }, + }) + ); return Promise.resolve(); }); } delete() { - return api.delete( - uri.formatApiLink('comment', this.id), - {version: this._version}) - .then(response => { - this.dispatchEvent(new CustomEvent('delete', { - detail: { - comment: this, - }, - })); + return api + .delete(uri.formatApiLink("comment", this.id), { + version: this._version, + }) + .then((response) => { + this.dispatchEvent( + new CustomEvent("delete", { + detail: { + comment: this, + }, + }) + ); return Promise.resolve(); }); } setScore(score) { - return api.put( - uri.formatApiLink('comment', this.id, 'score'), - {score: score}) - .then(response => { + return api + .put(uri.formatApiLink("comment", this.id, "score"), { + score: score, + }) + .then((response) => { this._updateFromResponse(response); - this.dispatchEvent(new CustomEvent('changeScore', { - detail: { - comment: this, - }, - })); + this.dispatchEvent( + new CustomEvent("changeScore", { + detail: { + comment: this, + }, + }) + ); return Promise.resolve(); }); } diff --git a/client/js/models/comment_list.js b/client/js/models/comment_list.js index a8e1150..bae2d7a 100644 --- a/client/js/models/comment_list.js +++ b/client/js/models/comment_list.js @@ -1,12 +1,11 @@ -'use strict'; +"use strict"; -const AbstractList = require('./abstract_list.js'); -const Comment = require('./comment.js'); +const AbstractList = require("./abstract_list.js"); +const Comment = require("./comment.js"); -class CommentList extends AbstractList { -} +class CommentList extends AbstractList {} CommentList._itemClass = Comment; -CommentList._itemName = 'comment'; +CommentList._itemName = "comment"; module.exports = CommentList; diff --git a/client/js/models/info.js b/client/js/models/info.js index 35ba867..6b03b38 100644 --- a/client/js/models/info.js +++ b/client/js/models/info.js @@ -1,22 +1,20 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const Post = require('./post.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const Post = require("./post.js"); class Info { static get() { - return api.get(uri.formatApiLink('info')) - .then(response => { - return Promise.resolve(Object.assign( - {}, - response, - { - featuredPost: response.featuredPost ? - Post.fromResponse(response.featuredPost) : - undefined - })); - }); + return api.get(uri.formatApiLink("info")).then((response) => { + return Promise.resolve( + Object.assign({}, response, { + featuredPost: response.featuredPost + ? Post.fromResponse(response.featuredPost) + : undefined, + }) + ); + }); } } diff --git a/client/js/models/note.js b/client/js/models/note.js index 1709d44..87c8b3b 100644 --- a/client/js/models/note.js +++ b/client/js/models/note.js @@ -1,13 +1,13 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const Point = require('./point.js'); -const PointList = require('./point_list.js'); +const events = require("../events.js"); +const Point = require("./point.js"); +const PointList = require("./point_list.js"); class Note extends events.EventTarget { constructor() { super(); - this._text = '…'; + this._text = "…"; this._polygon = new PointList(); } diff --git a/client/js/models/note_list.js b/client/js/models/note_list.js index b54d7f3..10db435 100644 --- a/client/js/models/note_list.js +++ b/client/js/models/note_list.js @@ -1,12 +1,11 @@ -'use strict'; +"use strict"; -const AbstractList = require('./abstract_list.js'); -const Note = require('./note.js'); +const AbstractList = require("./abstract_list.js"); +const Note = require("./note.js"); -class NoteList extends AbstractList { -} +class NoteList extends AbstractList {} NoteList._itemClass = Note; -NoteList._itemName = 'note'; +NoteList._itemName = "note"; module.exports = NoteList; diff --git a/client/js/models/point.js b/client/js/models/point.js index bbb05ec..70b4961 100644 --- a/client/js/models/point.js +++ b/client/js/models/point.js @@ -1,6 +1,6 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); +const events = require("../events.js"); class Point extends events.EventTarget { constructor(x, y) { @@ -19,12 +19,16 @@ class Point extends events.EventTarget { set x(value) { this._x = value; - this.dispatchEvent(new CustomEvent('change', {detail: {point: this}})); + this.dispatchEvent( + new CustomEvent("change", { detail: { point: this } }) + ); } set y(value) { this._y = value; - this.dispatchEvent(new CustomEvent('change', {detail: {point: this}})); + this.dispatchEvent( + new CustomEvent("change", { detail: { point: this } }) + ); } } diff --git a/client/js/models/point_list.js b/client/js/models/point_list.js index 166e664..3ecd7d7 100644 --- a/client/js/models/point_list.js +++ b/client/js/models/point_list.js @@ -1,7 +1,7 @@ -'use strict'; +"use strict"; -const AbstractList = require('./abstract_list.js'); -const Point = require('./point.js'); +const AbstractList = require("./abstract_list.js"); +const Point = require("./point.js"); class PointList extends AbstractList { get firstPoint() { @@ -18,6 +18,6 @@ class PointList extends AbstractList { } PointList._itemClass = Point; -PointList._itemName = 'point'; +PointList._itemName = "point"; module.exports = PointList; diff --git a/client/js/models/pool.js b/client/js/models/pool.js index 8a86e12..51fa8a0 100644 --- a/client/js/models/pool.js +++ b/client/js/models/pool.js @@ -1,13 +1,13 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const events = require('../events.js'); -const misc = require('../util/misc.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const events = require("../events.js"); +const misc = require("../util/misc.js"); class Pool extends events.EventTarget { constructor() { - const PostList = require('./post_list.js'); + const PostList = require("./post_list.js"); super(); this._orig = {}; @@ -70,14 +70,13 @@ class Pool extends events.EventTarget { } static get(id) { - return api.get(uri.formatApiLink('pool', id)) - .then(response => { - return Promise.resolve(Pool.fromResponse(response)); - }); + return api.get(uri.formatApiLink("pool", id)).then((response) => { + return Promise.resolve(Pool.fromResponse(response)); + }); } save() { - const detail = {version: this._version}; + const detail = { version: this._version }; // send only changed fields to avoid user privilege violation if (misc.arraysDiffer(this._names, this._orig._names, true)) { @@ -90,62 +89,71 @@ class Pool extends events.EventTarget { detail.description = this._description; } if (misc.arraysDiffer(this._posts, this._orig._posts)) { - detail.posts = this._posts.map(post => post.id); + detail.posts = this._posts.map((post) => post.id); } - let promise = this._id ? - api.put(uri.formatApiLink('pool', this._id), detail) : - api.post(uri.formatApiLink('pools'), detail); - return promise - .then(response => { - this._updateFromResponse(response); - this.dispatchEvent(new CustomEvent('change', { + let promise = this._id + ? api.put(uri.formatApiLink("pool", this._id), detail) + : api.post(uri.formatApiLink("pools"), detail); + return promise.then((response) => { + this._updateFromResponse(response); + this.dispatchEvent( + new CustomEvent("change", { detail: { pool: this, }, - })); - return Promise.resolve(); - }); + }) + ); + return Promise.resolve(); + }); } merge(targetId, addAlias) { - return api.get(uri.formatApiLink('pool', targetId)) - .then(response => { - return api.post(uri.formatApiLink('pool-merge'), { + return api + .get(uri.formatApiLink("pool", targetId)) + .then((response) => { + return api.post(uri.formatApiLink("pool-merge"), { removeVersion: this._version, remove: this._id, mergeToVersion: response.version, mergeTo: targetId, }); - }).then(response => { + }) + .then((response) => { if (!addAlias) { return Promise.resolve(response); } - return api.put(uri.formatApiLink('pool', targetId), { + return api.put(uri.formatApiLink("pool", targetId), { version: response.version, names: response.names.concat(this._names), }); - }).then(response => { + }) + .then((response) => { this._updateFromResponse(response); - this.dispatchEvent(new CustomEvent('change', { - detail: { - pool: this, - }, - })); + this.dispatchEvent( + new CustomEvent("change", { + detail: { + pool: this, + }, + }) + ); return Promise.resolve(); }); } delete() { - return api.delete( - uri.formatApiLink('pool', this._id), - {version: this._version}) - .then(response => { - this.dispatchEvent(new CustomEvent('delete', { - detail: { - pool: this, - }, - })); + return api + .delete(uri.formatApiLink("pool", this._id), { + version: this._version, + }) + .then((response) => { + this.dispatchEvent( + new CustomEvent("delete", { + detail: { + pool: this, + }, + }) + ); return Promise.resolve(); }); } diff --git a/client/js/models/pool_category.js b/client/js/models/pool_category.js index 1ce4b24..8c7df46 100644 --- a/client/js/models/pool_category.js +++ b/client/js/models/pool_category.js @@ -1,14 +1,14 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const events = require('../events.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const events = require("../events.js"); class PoolCategory extends events.EventTarget { constructor() { super(); - this._name = ''; - this._color = '#000000'; + this._name = ""; + this._color = "#000000"; this._poolCount = 0; this._isDefault = false; this._origName = null; @@ -50,7 +50,7 @@ class PoolCategory extends events.EventTarget { } save() { - const detail = {version: this._version}; + const detail = { version: this._version }; if (this.name !== this._origName) { detail.name = this.name; @@ -63,34 +63,39 @@ class PoolCategory extends events.EventTarget { return Promise.resolve(); } - let promise = this._origName ? - api.put( - uri.formatApiLink('pool-category', this._origName), - detail) : - api.post(uri.formatApiLink('pool-categories'), detail); - - return promise - .then(response => { - this._updateFromResponse(response); - this.dispatchEvent(new CustomEvent('change', { + let promise = this._origName + ? api.put( + uri.formatApiLink("pool-category", this._origName), + detail + ) + : api.post(uri.formatApiLink("pool-categories"), detail); + + return promise.then((response) => { + this._updateFromResponse(response); + this.dispatchEvent( + new CustomEvent("change", { detail: { poolCategory: this, }, - })); - return Promise.resolve(); - }); + }) + ); + return Promise.resolve(); + }); } delete() { - return api.delete( - uri.formatApiLink('pool-category', this._origName), - {version: this._version}) - .then(response => { - this.dispatchEvent(new CustomEvent('delete', { - detail: { - poolCategory: this, - }, - })); + return api + .delete(uri.formatApiLink("pool-category", this._origName), { + version: this._version, + }) + .then((response) => { + this.dispatchEvent( + new CustomEvent("delete", { + detail: { + poolCategory: this, + }, + }) + ); return Promise.resolve(); }); } diff --git a/client/js/models/pool_category_list.js b/client/js/models/pool_category_list.js index 2699620..46b7838 100644 --- a/client/js/models/pool_category_list.js +++ b/client/js/models/pool_category_list.js @@ -1,9 +1,9 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const AbstractList = require('./abstract_list.js'); -const PoolCategory = require('./pool_category.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const AbstractList = require("./abstract_list.js"); +const PoolCategory = require("./pool_category.js"); class PoolCategoryList extends AbstractList { constructor() { @@ -11,7 +11,7 @@ class PoolCategoryList extends AbstractList { this._defaultCategory = null; this._origDefaultCategory = null; this._deletedCategories = []; - this.addEventListener('remove', e => this._evtCategoryDeleted(e)); + this.addEventListener("remove", (e) => this._evtCategoryDeleted(e)); } static fromResponse(response) { @@ -27,12 +27,16 @@ class PoolCategoryList extends AbstractList { } static get() { - return api.get(uri.formatApiLink('pool-categories')) - .then(response => { - return Promise.resolve(Object.assign( - {}, - response, - {results: PoolCategoryList.fromResponse(response.results)})); + return api + .get(uri.formatApiLink("pool-categories")) + .then((response) => { + return Promise.resolve( + Object.assign({}, response, { + results: PoolCategoryList.fromResponse( + response.results + ), + }) + ); }); } @@ -57,16 +61,18 @@ class PoolCategoryList extends AbstractList { promises.push( api.put( uri.formatApiLink( - 'pool-category', + "pool-category", this._defaultCategory.name, - 'default'))); + "default" + ) + ) + ); } - return Promise.all(promises) - .then(response => { - this._deletedCategories = []; - return Promise.resolve(); - }); + return Promise.all(promises).then((response) => { + this._deletedCategories = []; + return Promise.resolve(); + }); } _evtCategoryDeleted(e) { @@ -77,6 +83,6 @@ class PoolCategoryList extends AbstractList { } PoolCategoryList._itemClass = PoolCategory; -PoolCategoryList._itemName = 'poolCategory'; +PoolCategoryList._itemName = "poolCategory"; module.exports = PoolCategoryList; diff --git a/client/js/models/pool_list.js b/client/js/models/pool_list.js index 8a3b858..a8839bb 100644 --- a/client/js/models/pool_list.js +++ b/client/js/models/pool_list.js @@ -1,25 +1,27 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const AbstractList = require('./abstract_list.js'); -const Pool = require('./pool.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const AbstractList = require("./abstract_list.js"); +const Pool = require("./pool.js"); class PoolList extends AbstractList { static search(text, offset, limit, fields) { - return api.get( - uri.formatApiLink( - 'pools', { + return api + .get( + uri.formatApiLink("pools", { query: text, offset: offset, limit: limit, - fields: fields.join(','), - })) - .then(response => { - return Promise.resolve(Object.assign( - {}, - response, - {results: PoolList.fromResponse(response.results)})); + fields: fields.join(","), + }) + ) + .then((response) => { + return Promise.resolve( + Object.assign({}, response, { + results: PoolList.fromResponse(response.results), + }) + ); }); } @@ -42,6 +44,6 @@ class PoolList extends AbstractList { } PoolList._itemClass = Pool; -PoolList._itemName = 'pool'; +PoolList._itemName = "pool"; module.exports = PoolList; diff --git a/client/js/models/post.js b/client/js/models/post.js index a11d3cb..e90d0b2 100644 --- a/client/js/models/post.js +++ b/client/js/models/post.js @@ -1,15 +1,15 @@ -'use strict'; - -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const tags = require('../tags.js'); -const events = require('../events.js'); -const TagList = require('./tag_list.js'); -const NoteList = require('./note_list.js'); -const CommentList = require('./comment_list.js'); -const PoolList = require('./pool_list.js'); -const Pool = require('./pool.js'); -const misc = require('../util/misc.js'); +"use strict"; + +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const tags = require("../tags.js"); +const events = require("../events.js"); +const TagList = require("./tag_list.js"); +const NoteList = require("./note_list.js"); +const CommentList = require("./comment_list.js"); +const PoolList = require("./pool_list.js"); +const Pool = require("./pool.js"); +const misc = require("../util/misc.js"); class Post extends events.EventTarget { constructor() { @@ -67,7 +67,7 @@ class Post extends events.EventTarget { } get sourceSplit() { - return this._source.split('\n'); + return this._source.split("\n"); } get canvasWidth() { @@ -83,11 +83,11 @@ class Post extends events.EventTarget { } get newContent() { - throw 'Invalid operation'; + throw "Invalid operation"; } get newThumbnail() { - throw 'Invalid operation'; + throw "Invalid operation"; } get flags() { @@ -99,7 +99,7 @@ class Post extends events.EventTarget { } get tagNames() { - return this._tags.map(tag => tag.names[0]); + return this._tags.map((tag) => tag.names[0]); } get notes() { @@ -174,32 +174,31 @@ class Post extends events.EventTarget { static reverseSearch(content) { let apiPromise = api.post( - uri.formatApiLink('posts', 'reverse-search'), + uri.formatApiLink("posts", "reverse-search"), {}, - {content: content}); - let returnedPromise = apiPromise - .then(response => { - if (response.exactPost) { - response.exactPost = Post.fromResponse(response.exactPost); - } - for (let item of response.similarPosts) { - item.post = Post.fromResponse(item.post); - } - return Promise.resolve(response); - }); + { content: content } + ); + let returnedPromise = apiPromise.then((response) => { + if (response.exactPost) { + response.exactPost = Post.fromResponse(response.exactPost); + } + for (let item of response.similarPosts) { + item.post = Post.fromResponse(item.post); + } + return Promise.resolve(response); + }); returnedPromise.abort = () => apiPromise.abort(); return returnedPromise; } static get(id) { - return api.get(uri.formatApiLink('post', id)) - .then(response => { - return Promise.resolve(Post.fromResponse(response)); - }); + return api.get(uri.formatApiLink("post", id)).then((response) => { + return Promise.resolve(Post.fromResponse(response)); + }); } _savePoolPosts() { - const difference = (a, b) => a.filter(post => !b.hasPoolId(post.id)); + const difference = (a, b) => a.filter((post) => !b.hasPoolId(post.id)); // find the pools where the post was added or removed const added = difference(this.pools, this._orig._pools); @@ -209,7 +208,7 @@ class Post extends events.EventTarget { // update each pool's list of posts for (let pool of added) { - let op = Pool.get(pool.id).then(response => { + let op = Pool.get(pool.id).then((response) => { if (!response.posts.hasPostId(this._id)) { response.posts.addById(this._id); return response.save(); @@ -221,7 +220,7 @@ class Post extends events.EventTarget { } for (let pool of removed) { - let op = Pool.get(pool.id).then(response => { + let op = Pool.get(pool.id).then((response) => { if (response.posts.hasPostId(this._id)) { response.posts.removeById(this._id); return response.save(); @@ -237,7 +236,7 @@ class Post extends events.EventTarget { save(anonymous) { const files = {}; - const detail = {version: this._version}; + const detail = { version: this._version }; // send only changed fields to avoid user privilege violation if (anonymous === true) { @@ -250,14 +249,14 @@ class Post extends events.EventTarget { detail.flags = this._flags; } if (misc.arraysDiffer(this._tags, this._orig._tags)) { - detail.tags = this._tags.map(tag => tag.names[0]); + detail.tags = this._tags.map((tag) => tag.names[0]); } if (misc.arraysDiffer(this._relations, this._orig._relations)) { detail.relations = this._relations; } if (misc.arraysDiffer(this._notes, this._orig._notes)) { - detail.notes = this._notes.map(note => ({ - polygon: note.polygon.map(point => [point.x, point.y]), + detail.notes = this._notes.map((note) => ({ + polygon: note.polygon.map((point) => [point.x, point.y]), text: note.text, })); } @@ -271,154 +270,187 @@ class Post extends events.EventTarget { detail.source = this._source; } - let apiPromise = this._id ? - api.put(uri.formatApiLink('post', this.id), detail, files) : - api.post(uri.formatApiLink('posts'), detail, files); + let apiPromise = this._id + ? api.put(uri.formatApiLink("post", this.id), detail, files) + : api.post(uri.formatApiLink("posts"), detail, files); - return apiPromise.then(response => { - if (misc.arraysDiffer(this._pools, this._orig._pools)) { - return this._savePoolPosts() - .then(() => Promise.resolve(response)); - } - return Promise.resolve(response); - }).then(response => { - this._updateFromResponse(response); - this.dispatchEvent( - new CustomEvent('change', {detail: {post: this}})); - if (this._newContent) { - this.dispatchEvent( - new CustomEvent('changeContent', {detail: {post: this}})); - } - if (this._newThumbnail) { - this.dispatchEvent( - new CustomEvent('changeThumbnail', {detail: {post: this}})); - } - - return Promise.resolve(); - }, error => { - if (error.response && - error.response.name === 'PostAlreadyUploadedError') { - error.message = - `Post already uploaded (@${error.response.otherPostId})`; - } - return Promise.reject(error); - }); + return apiPromise + .then((response) => { + if (misc.arraysDiffer(this._pools, this._orig._pools)) { + return this._savePoolPosts().then(() => + Promise.resolve(response) + ); + } + return Promise.resolve(response); + }) + .then( + (response) => { + this._updateFromResponse(response); + this.dispatchEvent( + new CustomEvent("change", { detail: { post: this } }) + ); + if (this._newContent) { + this.dispatchEvent( + new CustomEvent("changeContent", { + detail: { post: this }, + }) + ); + } + if (this._newThumbnail) { + this.dispatchEvent( + new CustomEvent("changeThumbnail", { + detail: { post: this }, + }) + ); + } + + return Promise.resolve(); + }, + (error) => { + if ( + error.response && + error.response.name === "PostAlreadyUploadedError" + ) { + error.message = `Post already uploaded (@${error.response.otherPostId})`; + } + return Promise.reject(error); + } + ); } feature() { - return api.post( - uri.formatApiLink('featured-post'), - {id: this._id}) - .then(response => { + return api + .post(uri.formatApiLink("featured-post"), { id: this._id }) + .then((response) => { return Promise.resolve(); }); } delete() { - return api.delete( - uri.formatApiLink('post', this.id), - {version: this._version}) - .then(response => { - this.dispatchEvent(new CustomEvent('delete', { - detail: { - post: this, - }, - })); + return api + .delete(uri.formatApiLink("post", this.id), { + version: this._version, + }) + .then((response) => { + this.dispatchEvent( + new CustomEvent("delete", { + detail: { + post: this, + }, + }) + ); return Promise.resolve(); }); } merge(targetId, useOldContent) { - return api.get(uri.formatApiLink('post', targetId)) - .then(response => { - return api.post(uri.formatApiLink('post-merge'), { + return api + .get(uri.formatApiLink("post", targetId)) + .then((response) => { + return api.post(uri.formatApiLink("post-merge"), { removeVersion: this._version, remove: this._id, mergeToVersion: response.version, mergeTo: targetId, replaceContent: useOldContent, }); - }).then(response => { + }) + .then((response) => { this._updateFromResponse(response); - this.dispatchEvent(new CustomEvent('change', { - detail: { - post: this, - }, - })); + this.dispatchEvent( + new CustomEvent("change", { + detail: { + post: this, + }, + }) + ); return Promise.resolve(); }); } setScore(score) { - return api.put( - uri.formatApiLink('post', this.id, 'score'), - {score: score}) - .then(response => { + return api + .put(uri.formatApiLink("post", this.id, "score"), { score: score }) + .then((response) => { const prevFavorite = this._ownFavorite; this._updateFromResponse(response); if (this._ownFavorite !== prevFavorite) { - this.dispatchEvent(new CustomEvent('changeFavorite', { + this.dispatchEvent( + new CustomEvent("changeFavorite", { + detail: { + post: this, + }, + }) + ); + } + this.dispatchEvent( + new CustomEvent("changeScore", { detail: { post: this, }, - })); - } - this.dispatchEvent(new CustomEvent('changeScore', { - detail: { - post: this, - }, - })); + }) + ); return Promise.resolve(); }); } addToFavorites() { - return api.post(uri.formatApiLink('post', this.id, 'favorite')) - .then(response => { + return api + .post(uri.formatApiLink("post", this.id, "favorite")) + .then((response) => { const prevScore = this._ownScore; this._updateFromResponse(response); if (this._ownScore !== prevScore) { - this.dispatchEvent(new CustomEvent('changeScore', { + this.dispatchEvent( + new CustomEvent("changeScore", { + detail: { + post: this, + }, + }) + ); + } + this.dispatchEvent( + new CustomEvent("changeFavorite", { detail: { post: this, }, - })); - } - this.dispatchEvent(new CustomEvent('changeFavorite', { - detail: { - post: this, - }, - })); + }) + ); return Promise.resolve(); }); } removeFromFavorites() { - return api.delete(uri.formatApiLink('post', this.id, 'favorite')) - .then(response => { + return api + .delete(uri.formatApiLink("post", this.id, "favorite")) + .then((response) => { const prevScore = this._ownScore; this._updateFromResponse(response); if (this._ownScore !== prevScore) { - this.dispatchEvent(new CustomEvent('changeScore', { + this.dispatchEvent( + new CustomEvent("changeScore", { + detail: { + post: this, + }, + }) + ); + } + this.dispatchEvent( + new CustomEvent("changeFavorite", { detail: { post: this, }, - })); - } - this.dispatchEvent(new CustomEvent('changeFavorite', { - detail: { - post: this, - }, - })); + }) + ); return Promise.resolve(); }); } mutateContentUrl() { this._contentUrl = - this._orig._contentUrl + - '?bypass-cache=' + - Math.round(Math.random() * 1000); + this._orig._contentUrl + + "?bypass-cache=" + + Math.round(Math.random() * 1000); } _updateFromResponse(response) { @@ -431,15 +463,18 @@ class Post extends events.EventTarget { _user: response.user, _safety: response.safety, _contentUrl: response.contentUrl, - _fullContentUrl: new URL(response.contentUrl, document.getElementsByTagName('base')[0].href).href, + _fullContentUrl: new URL( + response.contentUrl, + document.getElementsByTagName("base")[0].href + ).href, _thumbnailUrl: response.thumbnailUrl, _source: response.source, _canvasWidth: response.canvasWidth, _canvasHeight: response.canvasHeight, _fileSize: response.fileSize, - _flags: [...response.flags || []], - _relations: [...response.relations || []], + _flags: [...(response.flags || [])], + _relations: [...(response.relations || [])], _score: response.score, _commentCount: response.commentCount, diff --git a/client/js/models/post_list.js b/client/js/models/post_list.js index cd94e40..8c2c9d4 100644 --- a/client/js/models/post_list.js +++ b/client/js/models/post_list.js @@ -1,35 +1,37 @@ -'use strict'; +"use strict"; -const settings = require('../models/settings.js'); -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const AbstractList = require('./abstract_list.js'); -const Post = require('./post.js'); +const settings = require("../models/settings.js"); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const AbstractList = require("./abstract_list.js"); +const Post = require("./post.js"); class PostList extends AbstractList { static getAround(id, searchQuery) { return api.get( - uri.formatApiLink( - 'post', id, 'around', { - query: PostList._decorateSearchQuery(searchQuery || ''), - fields: 'id', - })); + uri.formatApiLink("post", id, "around", { + query: PostList._decorateSearchQuery(searchQuery || ""), + fields: "id", + }) + ); } static search(text, offset, limit, fields) { - return api.get( - uri.formatApiLink( - 'posts', { - query: PostList._decorateSearchQuery(text || ''), + return api + .get( + uri.formatApiLink("posts", { + query: PostList._decorateSearchQuery(text || ""), offset: offset, limit: limit, - fields: fields.join(','), - })) - .then(response => { - return Promise.resolve(Object.assign( - {}, - response, - {results: PostList.fromResponse(response.results)})); + fields: fields.join(","), + }) + ) + .then((response) => { + return Promise.resolve( + Object.assign({}, response, { + results: PostList.fromResponse(response.results), + }) + ); }); } @@ -43,7 +45,7 @@ class PostList extends AbstractList { } } if (disabledSafety.length) { - text = `-rating:${disabledSafety.join(',')} ${text}`; + text = `-rating:${disabledSafety.join(",")} ${text}`; } } return text.trim(); @@ -63,7 +65,7 @@ class PostList extends AbstractList { return; } - let post = Post.fromResponse({id: id}); + let post = Post.fromResponse({ id: id }); this.add(post); } @@ -77,6 +79,6 @@ class PostList extends AbstractList { } PostList._itemClass = Post; -PostList._itemName = 'post'; +PostList._itemName = "post"; module.exports = PostList; diff --git a/client/js/models/settings.js b/client/js/models/settings.js index 774aa0b..bd6ac5b 100644 --- a/client/js/models/settings.js +++ b/client/js/models/settings.js @@ -1,6 +1,6 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); +const events = require("../events.js"); const defaultSettings = { listPosts: { @@ -12,7 +12,7 @@ const defaultSettings = { endlessScroll: false, keyboardShortcuts: true, transparencyGrid: true, - fitMode: 'fit-both', + fitMode: "fit-both", tagSuggestions: true, autoplayVideos: false, postsPerPage: 42, @@ -28,7 +28,7 @@ class Settings extends events.EventTarget { _getFromLocalStorage() { let ret = Object.assign({}, defaultSettings); try { - Object.assign(ret, JSON.parse(localStorage.getItem('settings'))); + Object.assign(ret, JSON.parse(localStorage.getItem("settings"))); } catch (e) { // continue regardless of error } @@ -37,14 +37,16 @@ class Settings extends events.EventTarget { save(newSettings, silent) { newSettings = Object.assign(this.cache, newSettings); - localStorage.setItem('settings', JSON.stringify(newSettings)); + localStorage.setItem("settings", JSON.stringify(newSettings)); this.cache = this._getFromLocalStorage(); if (silent !== true) { - this.dispatchEvent(new CustomEvent('change', { - detail: { - settings: this.cache, - }, - })); + this.dispatchEvent( + new CustomEvent("change", { + detail: { + settings: this.cache, + }, + }) + ); } } diff --git a/client/js/models/snapshot.js b/client/js/models/snapshot.js index cf5a308..5f8e2ae 100644 --- a/client/js/models/snapshot.js +++ b/client/js/models/snapshot.js @@ -1,7 +1,7 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const events = require('../events.js'); +const api = require("../api.js"); +const events = require("../events.js"); class Snapshot extends events.EventTarget { constructor() { diff --git a/client/js/models/snapshot_list.js b/client/js/models/snapshot_list.js index 475b89a..9ea1bdd 100644 --- a/client/js/models/snapshot_list.js +++ b/client/js/models/snapshot_list.js @@ -1,24 +1,31 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const AbstractList = require('./abstract_list.js'); -const Snapshot = require('./snapshot.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const AbstractList = require("./abstract_list.js"); +const Snapshot = require("./snapshot.js"); class SnapshotList extends AbstractList { static search(text, offset, limit) { - return api.get(uri.formatApiLink( - 'snapshots', {query: text, offset: offset, limit: limit})) - .then(response => { - return Promise.resolve(Object.assign( - {}, - response, - {results: SnapshotList.fromResponse(response.results)})); + return api + .get( + uri.formatApiLink("snapshots", { + query: text, + offset: offset, + limit: limit, + }) + ) + .then((response) => { + return Promise.resolve( + Object.assign({}, response, { + results: SnapshotList.fromResponse(response.results), + }) + ); }); } } SnapshotList._itemClass = Snapshot; -SnapshotList._itemName = 'snapshot'; +SnapshotList._itemName = "snapshot"; module.exports = SnapshotList; diff --git a/client/js/models/tag.js b/client/js/models/tag.js index b99c63f..a5632c8 100644 --- a/client/js/models/tag.js +++ b/client/js/models/tag.js @@ -1,13 +1,13 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const events = require('../events.js'); -const misc = require('../util/misc.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const events = require("../events.js"); +const misc = require("../util/misc.js"); class Tag extends events.EventTarget { constructor() { - const TagList = require('./tag_list.js'); + const TagList = require("./tag_list.js"); super(); this._orig = {}; @@ -71,14 +71,13 @@ class Tag extends events.EventTarget { } static get(name) { - return api.get(uri.formatApiLink('tag', name)) - .then(response => { - return Promise.resolve(Tag.fromResponse(response)); - }); + return api.get(uri.formatApiLink("tag", name)).then((response) => { + return Promise.resolve(Tag.fromResponse(response)); + }); } save() { - const detail = {version: this._version}; + const detail = { version: this._version }; // send only changed fields to avoid user privilege violation if (misc.arraysDiffer(this._names, this._orig._names, true)) { @@ -92,66 +91,77 @@ class Tag extends events.EventTarget { } if (misc.arraysDiffer(this._implications, this._orig._implications)) { detail.implications = this._implications.map( - relation => relation.names[0]); + (relation) => relation.names[0] + ); } if (misc.arraysDiffer(this._suggestions, this._orig._suggestions)) { detail.suggestions = this._suggestions.map( - relation => relation.names[0]); + (relation) => relation.names[0] + ); } - let promise = this._origName ? - api.put(uri.formatApiLink('tag', this._origName), detail) : - api.post(uri.formatApiLink('tags'), detail); - return promise - .then(response => { - this._updateFromResponse(response); - this.dispatchEvent(new CustomEvent('change', { + let promise = this._origName + ? api.put(uri.formatApiLink("tag", this._origName), detail) + : api.post(uri.formatApiLink("tags"), detail); + return promise.then((response) => { + this._updateFromResponse(response); + this.dispatchEvent( + new CustomEvent("change", { detail: { tag: this, }, - })); - return Promise.resolve(); - }); + }) + ); + return Promise.resolve(); + }); } merge(targetName, addAlias) { - return api.get(uri.formatApiLink('tag', targetName)) - .then(response => { - return api.post(uri.formatApiLink('tag-merge'), { + return api + .get(uri.formatApiLink("tag", targetName)) + .then((response) => { + return api.post(uri.formatApiLink("tag-merge"), { removeVersion: this._version, remove: this._origName, mergeToVersion: response.version, mergeTo: targetName, }); - }).then(response => { + }) + .then((response) => { if (!addAlias) { return Promise.resolve(response); } - return api.put(uri.formatApiLink('tag', targetName), { + return api.put(uri.formatApiLink("tag", targetName), { version: response.version, names: response.names.concat(this._names), }); - }).then(response => { + }) + .then((response) => { this._updateFromResponse(response); - this.dispatchEvent(new CustomEvent('change', { - detail: { - tag: this, - }, - })); + this.dispatchEvent( + new CustomEvent("change", { + detail: { + tag: this, + }, + }) + ); return Promise.resolve(); }); } delete() { - return api.delete( - uri.formatApiLink('tag', this._origName), - {version: this._version}) - .then(response => { - this.dispatchEvent(new CustomEvent('delete', { - detail: { - tag: this, - }, - })); + return api + .delete(uri.formatApiLink("tag", this._origName), { + version: this._version, + }) + .then((response) => { + this.dispatchEvent( + new CustomEvent("delete", { + detail: { + tag: this, + }, + }) + ); return Promise.resolve(); }); } diff --git a/client/js/models/tag_category.js b/client/js/models/tag_category.js index 42e54d8..a8d0e64 100644 --- a/client/js/models/tag_category.js +++ b/client/js/models/tag_category.js @@ -1,14 +1,14 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const events = require('../events.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const events = require("../events.js"); class TagCategory extends events.EventTarget { constructor() { super(); - this._name = ''; - this._color = '#000000'; + this._name = ""; + this._color = "#000000"; this._tagCount = 0; this._isDefault = false; this._origName = null; @@ -50,7 +50,7 @@ class TagCategory extends events.EventTarget { } save() { - const detail = {version: this._version}; + const detail = { version: this._version }; if (this.name !== this._origName) { detail.name = this.name; @@ -63,34 +63,39 @@ class TagCategory extends events.EventTarget { return Promise.resolve(); } - let promise = this._origName ? - api.put( - uri.formatApiLink('tag-category', this._origName), - detail) : - api.post(uri.formatApiLink('tag-categories'), detail); - - return promise - .then(response => { - this._updateFromResponse(response); - this.dispatchEvent(new CustomEvent('change', { + let promise = this._origName + ? api.put( + uri.formatApiLink("tag-category", this._origName), + detail + ) + : api.post(uri.formatApiLink("tag-categories"), detail); + + return promise.then((response) => { + this._updateFromResponse(response); + this.dispatchEvent( + new CustomEvent("change", { detail: { tagCategory: this, }, - })); - return Promise.resolve(); - }); + }) + ); + return Promise.resolve(); + }); } delete() { - return api.delete( - uri.formatApiLink('tag-category', this._origName), - {version: this._version}) - .then(response => { - this.dispatchEvent(new CustomEvent('delete', { - detail: { - tagCategory: this, - }, - })); + return api + .delete(uri.formatApiLink("tag-category", this._origName), { + version: this._version, + }) + .then((response) => { + this.dispatchEvent( + new CustomEvent("delete", { + detail: { + tagCategory: this, + }, + }) + ); return Promise.resolve(); }); } diff --git a/client/js/models/tag_category_list.js b/client/js/models/tag_category_list.js index 6c1182f..2fc1522 100644 --- a/client/js/models/tag_category_list.js +++ b/client/js/models/tag_category_list.js @@ -1,9 +1,9 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const AbstractList = require('./abstract_list.js'); -const TagCategory = require('./tag_category.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const AbstractList = require("./abstract_list.js"); +const TagCategory = require("./tag_category.js"); class TagCategoryList extends AbstractList { constructor() { @@ -11,7 +11,7 @@ class TagCategoryList extends AbstractList { this._defaultCategory = null; this._origDefaultCategory = null; this._deletedCategories = []; - this.addEventListener('remove', e => this._evtCategoryDeleted(e)); + this.addEventListener("remove", (e) => this._evtCategoryDeleted(e)); } static fromResponse(response) { @@ -27,12 +27,16 @@ class TagCategoryList extends AbstractList { } static get() { - return api.get(uri.formatApiLink('tag-categories')) - .then(response => { - return Promise.resolve(Object.assign( - {}, - response, - {results: TagCategoryList.fromResponse(response.results)})); + return api + .get(uri.formatApiLink("tag-categories")) + .then((response) => { + return Promise.resolve( + Object.assign({}, response, { + results: TagCategoryList.fromResponse( + response.results + ), + }) + ); }); } @@ -57,16 +61,18 @@ class TagCategoryList extends AbstractList { promises.push( api.put( uri.formatApiLink( - 'tag-category', + "tag-category", this._defaultCategory.name, - 'default'))); + "default" + ) + ) + ); } - return Promise.all(promises) - .then(response => { - this._deletedCategories = []; - return Promise.resolve(); - }); + return Promise.all(promises).then((response) => { + this._deletedCategories = []; + return Promise.resolve(); + }); } _evtCategoryDeleted(e) { @@ -77,6 +83,6 @@ class TagCategoryList extends AbstractList { } TagCategoryList._itemClass = TagCategory; -TagCategoryList._itemName = 'tagCategory'; +TagCategoryList._itemName = "tagCategory"; module.exports = TagCategoryList; diff --git a/client/js/models/tag_list.js b/client/js/models/tag_list.js index 31a34fa..7e6b643 100644 --- a/client/js/models/tag_list.js +++ b/client/js/models/tag_list.js @@ -1,25 +1,27 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const AbstractList = require('./abstract_list.js'); -const Tag = require('./tag.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const AbstractList = require("./abstract_list.js"); +const Tag = require("./tag.js"); class TagList extends AbstractList { static search(text, offset, limit, fields) { - return api.get( - uri.formatApiLink( - 'tags', { + return api + .get( + uri.formatApiLink("tags", { query: text, offset: offset, limit: limit, - fields: fields.join(','), - })) - .then(response => { - return Promise.resolve(Object.assign( - {}, - response, - {results: TagList.fromResponse(response.results)})); + fields: fields.join(","), + }) + ) + .then((response) => { + return Promise.resolve( + Object.assign({}, response, { + results: TagList.fromResponse(response.results), + }) + ); }); } @@ -45,10 +47,12 @@ class TagList extends AbstractList { this.add(tag); if (addImplications !== false) { - return Tag.get(tagName).then(actualTag => { + return Tag.get(tagName).then((actualTag) => { return Promise.all( - actualTag.implications.map( - relation => this.addByName(relation.names[0], true))); + actualTag.implications.map((relation) => + this.addByName(relation.names[0], true) + ) + ); }); } @@ -67,6 +71,6 @@ class TagList extends AbstractList { } TagList._itemClass = Tag; -TagList._itemName = 'tag'; +TagList._itemName = "tag"; module.exports = TagList; diff --git a/client/js/models/top_navigation.js b/client/js/models/top_navigation.js index 91b8976..a469a03 100644 --- a/client/js/models/top_navigation.js +++ b/client/js/models/top_navigation.js @@ -1,7 +1,7 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const api = require('../api.js'); +const events = require("../events.js"); +const api = require("../api.js"); class TopNavigationItem { constructor(accessKey, title, url, available, imageUrl) { @@ -44,18 +44,20 @@ class TopNavigation extends events.EventTarget { activate(key) { this.activeItem = null; - this.dispatchEvent(new CustomEvent('activate', { - detail: { - key: key, - item: key ? this.get(key) : null, - }, - })); + this.dispatchEvent( + new CustomEvent("activate", { + detail: { + key: key, + item: key ? this.get(key) : null, + }, + }) + ); } setTitle(title) { api.fetchConfig().then(() => { document.oldTitle = null; - document.title = api.getName() + (title ? (' – ' + title) : ''); + document.title = api.getName() + (title ? " – " + title : ""); }); } @@ -76,24 +78,22 @@ class TopNavigation extends events.EventTarget { function _makeTopNavigation() { const ret = new TopNavigation(); - ret.add('home', new TopNavigationItem('H', 'Home', '')); - ret.add('posts', new TopNavigationItem('P', 'Posts', 'posts')); - ret.add('upload', new TopNavigationItem('U', 'Upload', 'upload')); - ret.add('comments', new TopNavigationItem('C', 'Comments', 'comments')); - ret.add('tags', new TopNavigationItem('T', 'Tags', 'tags')); - ret.add('pools', new TopNavigationItem('O', 'Pools', 'pools')); - ret.add('users', new TopNavigationItem('S', 'Users', 'users')); - ret.add('account', new TopNavigationItem('A', 'Account', 'user/{me}')); - ret.add('register', new TopNavigationItem('R', 'Register', 'register')); - ret.add('login', new TopNavigationItem('L', 'Log in', 'login')); - ret.add('logout', new TopNavigationItem('O', 'Logout', 'logout')); - ret.add('help', new TopNavigationItem('E', 'Help', 'help')); + ret.add("home", new TopNavigationItem("H", "Home", "")); + ret.add("posts", new TopNavigationItem("P", "Posts", "posts")); + ret.add("upload", new TopNavigationItem("U", "Upload", "upload")); + ret.add("comments", new TopNavigationItem("C", "Comments", "comments")); + ret.add("tags", new TopNavigationItem("T", "Tags", "tags")); + ret.add("pools", new TopNavigationItem("O", "Pools", "pools")); + ret.add("users", new TopNavigationItem("S", "Users", "users")); + ret.add("account", new TopNavigationItem("A", "Account", "user/{me}")); + ret.add("register", new TopNavigationItem("R", "Register", "register")); + ret.add("login", new TopNavigationItem("L", "Log in", "login")); + ret.add("logout", new TopNavigationItem("O", "Logout", "logout")); + ret.add("help", new TopNavigationItem("E", "Help", "help")); ret.add( - 'settings', - new TopNavigationItem( - null, - '', - 'settings')); + "settings", + new TopNavigationItem(null, "", "settings") + ); return ret; } diff --git a/client/js/models/user.js b/client/js/models/user.js index 40fbb14..28dc3ef 100644 --- a/client/js/models/user.js +++ b/client/js/models/user.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const events = require('../events.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const events = require("../events.js"); class User extends events.EventTarget { constructor() { @@ -64,11 +64,11 @@ class User extends events.EventTarget { } get avatarContent() { - throw 'Invalid operation'; + throw "Invalid operation"; } get password() { - throw 'Invalid operation'; + throw "Invalid operation"; } set name(value) { @@ -102,15 +102,14 @@ class User extends events.EventTarget { } static get(name) { - return api.get(uri.formatApiLink('user', name)) - .then(response => { - return Promise.resolve(User.fromResponse(response)); - }); + return api.get(uri.formatApiLink("user", name)).then((response) => { + return Promise.resolve(User.fromResponse(response)); + }); } save() { const files = []; - const detail = {version: this._version}; + const detail = { version: this._version }; const transient = this._orig._name; if (this._name !== this._orig._name) { @@ -133,33 +132,40 @@ class User extends events.EventTarget { detail.password = this._password; } - let promise = this._orig._name ? - api.put( - uri.formatApiLink('user', this._orig._name), detail, files) : - api.post(uri.formatApiLink('users'), detail, files); - - return promise - .then(response => { - this._updateFromResponse(response); - this.dispatchEvent(new CustomEvent('change', { + let promise = this._orig._name + ? api.put( + uri.formatApiLink("user", this._orig._name), + detail, + files + ) + : api.post(uri.formatApiLink("users"), detail, files); + + return promise.then((response) => { + this._updateFromResponse(response); + this.dispatchEvent( + new CustomEvent("change", { detail: { user: this, }, - })); - return Promise.resolve(); - }); + }) + ); + return Promise.resolve(); + }); } delete() { - return api.delete( - uri.formatApiLink('user', this._orig._name), - {version: this._version}) - .then(response => { - this.dispatchEvent(new CustomEvent('delete', { - detail: { - user: this, - }, - })); + return api + .delete(uri.formatApiLink("user", this._orig._name), { + version: this._version, + }) + .then((response) => { + this.dispatchEvent( + new CustomEvent("delete", { + detail: { + user: this, + }, + }) + ); return Promise.resolve(); }); } diff --git a/client/js/models/user_list.js b/client/js/models/user_list.js index d44622c..c537f8f 100644 --- a/client/js/models/user_list.js +++ b/client/js/models/user_list.js @@ -1,25 +1,31 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const AbstractList = require('./abstract_list.js'); -const User = require('./user.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const AbstractList = require("./abstract_list.js"); +const User = require("./user.js"); class UserList extends AbstractList { static search(text, offset, limit) { - return api.get( - uri.formatApiLink( - 'users', {query: text, offset: offset, limit: limit})) - .then(response => { - return Promise.resolve(Object.assign( - {}, - response, - {results: UserList.fromResponse(response.results)})); + return api + .get( + uri.formatApiLink("users", { + query: text, + offset: offset, + limit: limit, + }) + ) + .then((response) => { + return Promise.resolve( + Object.assign({}, response, { + results: UserList.fromResponse(response.results), + }) + ); }); } } UserList._itemClass = User; -UserList._itemName = 'user'; +UserList._itemName = "user"; module.exports = UserList; diff --git a/client/js/models/user_token.js b/client/js/models/user_token.js index e49f5aa..c9d28a2 100644 --- a/client/js/models/user_token.js +++ b/client/js/models/user_token.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const uri = require('../util/uri.js'); -const events = require('../events.js'); +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const events = require("../events.js"); class UserToken extends events.EventTarget { constructor() { @@ -48,12 +48,12 @@ class UserToken extends events.EventTarget { } static fromResponse(response) { - if (typeof response.results !== 'undefined') { + if (typeof response.results !== "undefined") { let tokenList = []; for (let responseToken of response.results) { const token = new UserToken(); token._updateFromResponse(responseToken); - tokenList.push(token) + tokenList.push(token); } return tokenList; } else { @@ -64,15 +64,16 @@ class UserToken extends events.EventTarget { } static get(userName) { - return api.get(uri.formatApiLink('user-tokens', userName)) - .then(response => { + return api + .get(uri.formatApiLink("user-tokens", userName)) + .then((response) => { return Promise.resolve(UserToken.fromResponse(response)); }); } static create(userName, note, expirationTime) { let userTokenRequest = { - enabled: true + enabled: true, }; if (note) { userTokenRequest.note = note; @@ -80,43 +81,54 @@ class UserToken extends events.EventTarget { if (expirationTime) { userTokenRequest.expirationTime = expirationTime; } - return api.post(uri.formatApiLink('user-token', userName), userTokenRequest) - .then(response => { - return Promise.resolve(UserToken.fromResponse(response)) + return api + .post(uri.formatApiLink("user-token", userName), userTokenRequest) + .then((response) => { + return Promise.resolve(UserToken.fromResponse(response)); }); } save(userName) { - const detail = {version: this._version}; + const detail = { version: this._version }; if (this._note !== this._orig._note) { detail.note = this._note; } - return api.put( - uri.formatApiLink('user-token', userName, this._orig._token), - detail) - .then(response => { + return api + .put( + uri.formatApiLink("user-token", userName, this._orig._token), + detail + ) + .then((response) => { this._updateFromResponse(response); - this.dispatchEvent(new CustomEvent('change', { - detail: { - userToken: this, - }, - })); + this.dispatchEvent( + new CustomEvent("change", { + detail: { + userToken: this, + }, + }) + ); return Promise.resolve(this); }); } delete(userName) { - return api.delete( - uri.formatApiLink('user-token', userName, this._orig._token), - {version: this._version}) - .then(response => { - this.dispatchEvent(new CustomEvent('delete', { - detail: { - userToken: this, - }, - })); + return api + .delete( + uri.formatApiLink("user-token", userName, this._orig._token), + { + version: this._version, + } + ) + .then((response) => { + this.dispatchEvent( + new CustomEvent("delete", { + detail: { + userToken: this, + }, + }) + ); return Promise.resolve(); }); } diff --git a/client/js/pools.js b/client/js/pools.js index 8484f0b..aa58d5e 100644 --- a/client/js/pools.js +++ b/client/js/pools.js @@ -1,22 +1,23 @@ -'use strict'; +"use strict"; -const misc = require('./util/misc.js'); -const PoolCategoryList = require('./models/pool_category_list.js'); +const misc = require("./util/misc.js"); +const PoolCategoryList = require("./models/pool_category_list.js"); let _stylesheet = null; function refreshCategoryColorMap() { - return PoolCategoryList.get().then(response => { + return PoolCategoryList.get().then((response) => { if (_stylesheet) { document.head.removeChild(_stylesheet); } - _stylesheet = document.createElement('style'); + _stylesheet = document.createElement("style"); document.head.appendChild(_stylesheet); for (let category of response.results) { - const ruleName = misc.makeCssName(category.name, 'pool'); + const ruleName = misc.makeCssName(category.name, "pool"); _stylesheet.sheet.insertRule( `.${ruleName} { color: ${category.color} }`, - _stylesheet.sheet.cssRules.length); + _stylesheet.sheet.cssRules.length + ); } }); } diff --git a/client/js/router.js b/client/js/router.js index b5b4855..460a641 100644 --- a/client/js/router.js +++ b/client/js/router.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; // modified page.js by visionmedia // - changed regexes to components @@ -10,13 +10,17 @@ // - rename .save() to .replaceState() // - offer .url -const clickEvent = document.ontouchstart ? 'touchstart' : 'click'; -const uri = require('./util/uri.js'); +const clickEvent = document.ontouchstart ? "touchstart" : "click"; +const uri = require("./util/uri.js"); let location = window.history.location || window.location; function _getOrigin() { - return location.protocol + '//' + location.hostname - + (location.port ? (':' + location.port) : ''); + return ( + location.protocol + + "//" + + location.hostname + + (location.port ? ":" + location.port : "") + ); } function _isSameOrigin(href) { @@ -24,15 +28,16 @@ function _isSameOrigin(href) { } function _getBaseHref() { - const bases = document.getElementsByTagName('base'); - return bases.length > 0 ? - bases[0].href.replace(_getOrigin(), '').replace(/\/+$/, '') : ''; + const bases = document.getElementsByTagName("base"); + return bases.length > 0 + ? bases[0].href.replace(_getOrigin(), "").replace(/\/+$/, "") + : ""; } class Context { constructor(path, state) { const base = _getBaseHref(); - path = path.indexOf('/') !== 0 ? '/' + path : path; + path = path.indexOf("/") !== 0 ? "/" + path : path; path = path.indexOf(base) !== 0 ? base + path : path; this.canonicalPath = path; @@ -55,7 +60,7 @@ class Context { class Route { constructor(path) { - this.method = 'GET'; + this.method = "GET"; this.path = path; this.parameterNames = []; @@ -64,16 +69,17 @@ class Route { } else { let parts = []; for (let component of this.path) { - if (component[0] === ':') { - parts.push('([^/]+)'); + if (component[0] === ":") { + parts.push("([^/]+)"); this.parameterNames.push(component.substr(1)); - } else { // assert [a-z]+ + } else { + // assert [a-z]+ parts.push(component); } } - let regexString = '^/' + parts.join('/'); - regexString += '(?:/*|/((?:(?:[a-z]+=[^/]+);)*(?:[a-z]+=[^/]+)))$'; - this.parameterNames.push('variable'); + let regexString = "^/" + parts.join("/"); + regexString += "(?:/*|/((?:(?:[a-z]+=[^/]+);)*(?:[a-z]+=[^/]+)))$"; + this.parameterNames.push("variable"); this.regex = new RegExp(regexString); } } @@ -88,7 +94,7 @@ class Route { } match(path, parameters) { - const qsIndex = path.indexOf('?'); + const qsIndex = path.indexOf("?"); const pathname = ~qsIndex ? path.slice(0, qsIndex) : path; const match = this.regex.exec(pathname); @@ -104,8 +110,8 @@ class Route { continue; } - if (name === 'variable') { - for (let word of (value || '').split(/;/)) { + if (name === "variable") { + for (let word of (value || "").split(/;/)) { const [key, subvalue] = word.split(/=/, 2); parameters[key] = uri.unescapeParam(subvalue); } @@ -148,7 +154,7 @@ class Router { this._running = true; this._onPopState = _onPopState(this); this._onClick = _onClick(this); - window.addEventListener('popstate', this._onPopState, false); + window.addEventListener("popstate", this._onPopState, false); document.addEventListener(clickEvent, this._onClick, false); const url = location.pathname + location.search + location.hash; return this.replace(url, history.state, true); @@ -160,7 +166,7 @@ class Router { } this._running = false; document.removeEventListener(clickEvent, this._onClick, false); - window.removeEventListener('popstate', this._onPopState, false); + window.removeEventListener("popstate", this._onPopState, false); } showNoDispatch(path, state) { @@ -199,11 +205,11 @@ class Router { middle(); next(); }; - const callChain = (this.ctx ? this._exits : []) - .concat( - [swap], - this._callbacks, - [this._unhandled, (ctx, next) => {}]); + const callChain = (this.ctx ? this._exits : []).concat( + [swap], + this._callbacks, + [this._unhandled, (ctx, next) => {}] + ); let i = 0; let fn = () => { @@ -226,20 +232,18 @@ class Router { } } -const _onPopState = router => { +const _onPopState = (router) => { let loaded = false; - if (document.readyState === 'complete') { + if (document.readyState === "complete") { loaded = true; } else { - window.addEventListener( - 'load', - () => { - setTimeout(() => { - loaded = true; - }, 0); - }); + window.addEventListener("load", () => { + setTimeout(() => { + loaded = true; + }, 0); + }); } - return e => { + return (e) => { if (!loaded) { return; } @@ -247,16 +251,13 @@ const _onPopState = router => { const path = e.state.path; router.replace(path, e.state, true); } else { - router.show( - location.pathname + location.hash, - undefined, - false); + router.show(location.pathname + location.hash, undefined, false); } }; }; -const _onClick = router => { - return e => { +const _onClick = (router) => { + return (e) => { if (1 !== _which(e)) { return; } @@ -268,23 +269,25 @@ const _onClick = router => { } let el = e.path ? e.path[0] : e.target; - while (el && el.nodeName !== 'A') { + while (el && el.nodeName !== "A") { el = el.parentNode; } - if (!el || el.nodeName !== 'A') { + if (!el || el.nodeName !== "A") { return; } - if (el.hasAttribute('download') || - el.getAttribute('rel') === 'external') { + if ( + el.hasAttribute("download") || + el.getAttribute("rel") === "external" + ) { return; } - const link = el.getAttribute('href'); - if (el.pathname === location.pathname && (el.hash || '#' === link)) { + const link = el.getAttribute("href"); + if (el.pathname === location.pathname && (el.hash || "#" === link)) { return; } - if (link && link.indexOf('mailto:') > -1) { + if (link && link.indexOf("mailto:") > -1) { return; } if (el.target) { @@ -295,7 +298,7 @@ const _onClick = router => { } const base = _getBaseHref(); - const orig = el.pathname + el.search + (el.hash || ''); + const orig = el.pathname + el.search + (el.hash || ""); const path = !orig.indexOf(base) ? orig.slice(base.length) : orig; if (base && orig === path) { diff --git a/client/js/tags.js b/client/js/tags.js index c037f6f..ecf9b66 100644 --- a/client/js/tags.js +++ b/client/js/tags.js @@ -1,22 +1,23 @@ -'use strict'; +"use strict"; -const misc = require('./util/misc.js'); -const TagCategoryList = require('./models/tag_category_list.js'); +const misc = require("./util/misc.js"); +const TagCategoryList = require("./models/tag_category_list.js"); let _stylesheet = null; function refreshCategoryColorMap() { - return TagCategoryList.get().then(response => { + return TagCategoryList.get().then((response) => { if (_stylesheet) { document.head.removeChild(_stylesheet); } - _stylesheet = document.createElement('style'); + _stylesheet = document.createElement("style"); document.head.appendChild(_stylesheet); for (let category of response.results) { - const ruleName = misc.makeCssName(category.name, 'tag'); + const ruleName = misc.makeCssName(category.name, "tag"); _stylesheet.sheet.insertRule( `.${ruleName} { color: ${category.color} }`, - _stylesheet.sheet.cssRules.length); + _stylesheet.sheet.cssRules.length + ); } }); } diff --git a/client/js/templates.js b/client/js/templates.js index f378cbc..88bd568 100644 --- a/client/js/templates.js +++ b/client/js/templates.js @@ -1,3 +1,3 @@ -'use strict'; +"use strict"; -module.exports = require('./.templates.autogen.js'); +module.exports = require("./.templates.autogen.js"); diff --git a/client/js/util/keyboard.js b/client/js/util/keyboard.js index 8ee6ee9..3fe3263 100644 --- a/client/js/util/keyboard.js +++ b/client/js/util/keyboard.js @@ -1,12 +1,12 @@ -'use strict'; +"use strict"; -const mousetrap = require('mousetrap'); -const settings = require('../models/settings.js'); +const mousetrap = require("mousetrap"); +const settings = require("../models/settings.js"); let paused = false; const _originalStopCallback = mousetrap.prototype.stopCallback; // eslint-disable-next-line func-names -mousetrap.prototype.stopCallback = function(...args) { +mousetrap.prototype.stopCallback = function (...args) { var self = this; if (paused) { return true; diff --git a/client/js/util/markdown.js b/client/js/util/markdown.js index 210280e..792c938 100644 --- a/client/js/util/markdown.js +++ b/client/js/util/markdown.js @@ -1,6 +1,6 @@ -'use strict'; +"use strict"; -const marked = require('marked'); +const marked = require("marked"); class BaseMarkdownWrapper { preprocess(text) { @@ -20,42 +20,44 @@ class SjisWrapper extends BaseMarkdownWrapper { preprocess(text) { return text.replace( - /\[sjis\]((?:[^\[]|\[(?!\/?sjis\]))+)\[\/sjis\]/ig, + /\[sjis\]((?:[^\[]|\[(?!\/?sjis\]))+)\[\/sjis\]/gi, (match, capture) => { - var ret = '%%%SJIS' + this.buf.length; + var ret = "%%%SJIS" + this.buf.length; this.buf.push(capture); return ret; - }); + } + ); } postprocess(text) { return text.replace( /(?:

        )?%%%SJIS(\d+)(?:<\/p>)?/, (match, capture) => { - return '

        ' + this.buf[capture] + '
        '; - }); + return '
        ' + this.buf[capture] + "
        "; + } + ); } } // fix \ before ~ being stripped away class TildeWrapper extends BaseMarkdownWrapper { preprocess(text) { - return text.replace(/\\~/g, '%%%T'); + return text.replace(/\\~/g, "%%%T"); } postprocess(text) { - return text.replace(/%%%T/g, '\\~'); + return text.replace(/%%%T/g, "\\~"); } } // prevent ^#... from being treated as headers, due to tag permalinks class TagPermalinkFixWrapper extends BaseMarkdownWrapper { preprocess(text) { - return text.replace(/^#/g, '%%%#'); + return text.replace(/^#/g, "%%%#"); } postprocess(text) { - return text.replace(/%%%#/g, '#'); + return text.replace(/%%%#/g, "#"); } } @@ -63,19 +65,23 @@ class TagPermalinkFixWrapper extends BaseMarkdownWrapper { class EntityPermalinkWrapper extends BaseMarkdownWrapper { preprocess(text) { // URL-based permalinks + text = text.replace(new RegExp("\\b/post/(\\d+)/?\\b", "g"), "@$1"); text = text.replace( - new RegExp('\\b/post/(\\d+)/?\\b', 'g'), '@$1'); - text = text.replace( - new RegExp('\\b/tag/([a-zA-Z0-9_-]+?)/?', 'g'), '#$1'); + new RegExp("\\b/tag/([a-zA-Z0-9_-]+?)/?", "g"), + "#$1" + ); text = text.replace( - new RegExp('\\b/user/([a-zA-Z0-9_-]+?)/?', 'g'), '+$1'); + new RegExp("\\b/user/([a-zA-Z0-9_-]+?)/?", "g"), + "+$1" + ); text = text.replace( /(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g, - '$1[$2]($2)'); - text = text.replace(/\]\(@(\d+)\)/g, '](/post/$1)'); - text = text.replace(/\]\(\+([a-zA-Z0-9_-]+)\)/g, '](/user/$1)'); - text = text.replace(/\]\(#([a-zA-Z0-9_-]+)\)/g, '](/posts/query=$1)'); + "$1[$2]($2)" + ); + text = text.replace(/\]\(@(\d+)\)/g, "](/post/$1)"); + text = text.replace(/\]\(\+([a-zA-Z0-9_-]+)\)/g, "](/user/$1)"); + text = text.replace(/\]\(#([a-zA-Z0-9_-]+)\)/g, "](/posts/query=$1)"); return text; } } @@ -83,51 +89,58 @@ class EntityPermalinkWrapper extends BaseMarkdownWrapper { class SearchPermalinkWrapper extends BaseMarkdownWrapper { postprocess(text) { return text.replace( - /\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/ig, - '$1'); + /\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/gi, + '$1' + ); } } class SpoilersWrapper extends BaseMarkdownWrapper { postprocess(text) { return text.replace( - /\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\]))+)\[\/spoiler\]/ig, - '$1'); + /\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\]))+)\[\/spoiler\]/gi, + '$1' + ); } } class SmallWrapper extends BaseMarkdownWrapper { postprocess(text) { return text.replace( - /\[small\]((?:[^\[]|\[(?!\/?small\]))+)\[\/small\]/ig, - '$1'); + /\[small\]((?:[^\[]|\[(?!\/?small\]))+)\[\/small\]/gi, + "$1" + ); } } class StrikeThroughWrapper extends BaseMarkdownWrapper { postprocess(text) { - text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, '$1$3'); - return text.replace(/\\~/g, '~'); + text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, "$1$3"); + return text.replace(/\\~/g, "~"); } } function createRenderer() { function sanitize(str) { - return str.replace(/&<"/g, m => { - if (m === '&') { - return '&'; + return str.replace(/&<"/g, (m) => { + if (m === "&") { + return "&"; } - if (m === '<') { - return '<'; + if (m === "<") { + return "<"; } - return '"'; + return """; }); } const renderer = new marked.Renderer(); renderer.image = (href, title, alt) => { - let [_, url, width, height] = - (/^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/).exec(href); + let [ + _, + url, + width, + height, + ] = /^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href); let res = '' + sanitize(alt);
         if (width) {
             res += ' { - const decimalPlaces = number < 20 && suffix !== 'B' ? 1 : 0; + const decimalPlaces = number < 20 && suffix !== "B" ? 1 : 0; return number.toFixed(decimalPlaces) + suffix; - }); + } + ); } function formatRelativeTime(timeString) { if (!timeString) { - return 'never'; + return "never"; } const then = Date.parse(timeString); @@ -63,17 +64,17 @@ function formatRelativeTime(timeString) { const future = now < then; const descriptions = [ - [60, 'a few seconds', null], - [60 * 2, 'a minute', null], - [60 * 60, '% minutes', 60], - [60 * 60 * 2, 'an hour', null], - [60 * 60 * 24, '% hours', 60 * 60], - [60 * 60 * 24 * 2, 'a day', null], - [60 * 60 * 24 * 30.42, '% days', 60 * 60 * 24], - [60 * 60 * 24 * 30.42 * 2, 'a month', null], - [60 * 60 * 24 * 30.42 * 12, '% months', 60 * 60 * 24 * 30.42], - [60 * 60 * 24 * 30.42 * 12 * 2, 'a year', null], - [8640000000000000 /* max*/, '% years', 60 * 60 * 24 * 30.42 * 12], + [60, "a few seconds", null], + [60 * 2, "a minute", null], + [60 * 60, "% minutes", 60], + [60 * 60 * 2, "an hour", null], + [60 * 60 * 24, "% hours", 60 * 60], + [60 * 60 * 24 * 2, "a day", null], + [60 * 60 * 24 * 30.42, "% days", 60 * 60 * 24], + [60 * 60 * 24 * 30.42 * 2, "a month", null], + [60 * 60 * 24 * 30.42 * 12, "% months", 60 * 60 * 24 * 30.42], + [60 * 60 * 24 * 30.42 * 12 * 2, "a year", null], + [8640000000000000 /* max*/, "% years", 60 * 60 * 24 * 30.42 * 12], ]; let text = null; @@ -87,10 +88,10 @@ function formatRelativeTime(timeString) { } } - if (text === 'a day') { - return future ? 'tomorrow' : 'yesterday'; + if (text === "a day") { + return future ? "tomorrow" : "yesterday"; } - return future ? 'in ' + text : text + ' ago'; + return future ? "in " + text : text + " ago"; } function formatMarkdown(text) { @@ -102,7 +103,7 @@ function formatInlineMarkdown(text) { } function splitByWhitespace(str) { - return str.split(/\s+/).filter(s => s); + return str.split(/\s+/).filter((s) => s); } function unindent(callSite, ...args) { @@ -110,28 +111,30 @@ function unindent(callSite, ...args) { let size = -1; return str.replace(/\n(\s+)/g, (m, m1) => { if (size < 0) { - size = m1.replace(/\t/g, ' ').length; + size = m1.replace(/\t/g, " ").length; } - return '\n' + m1.slice(Math.min(m1.length, size)); + return "\n" + m1.slice(Math.min(m1.length, size)); }); } - if (typeof callSite === 'string') { + if (typeof callSite === "string") { return format(callSite); } - if (typeof callSite === 'function') { + if (typeof callSite === "function") { return (...args) => format(callSite(...args)); } let output = callSite .slice(0, args.length + 1) - .map((text, i) => (i === 0 ? '' : args[i - 1]) + text) - .join(''); + .map((text, i) => (i === 0 ? "" : args[i - 1]) + text) + .join(""); return format(output); } function enableExitConfirmation() { - window.onbeforeunload = e => { - return 'Are you sure you want to leave? ' + - 'Data you have entered may not be saved.'; + window.onbeforeunload = (e) => { + return ( + "Are you sure you want to leave? " + + "Data you have entered may not be saved." + ); }; } @@ -150,16 +153,17 @@ function confirmPageExit() { } function makeCssName(text, suffix) { - return suffix + '-' + text.replace(/[^a-z0-9]/g, '_'); + return suffix + "-" + text.replace(/[^a-z0-9]/g, "_"); } function escapeHtml(unsafe) { - return unsafe.toString() - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + return unsafe + .toString() + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); } function arraysDiffer(source1, source2, orderImportant) { @@ -177,25 +181,27 @@ function arraysDiffer(source1, source2, orderImportant) { return false; } return ( - source1.filter(value => !source2.includes(value)).length > 0 || - source2.filter(value => !source1.includes(value)).length > 0); + source1.filter((value) => !source2.includes(value)).length > 0 || + source2.filter((value) => !source1.includes(value)).length > 0 + ); } function escapeSearchTerm(text) { - return text.replace(/([a-z_-]):/g, '$1\\:'); + return text.replace(/([a-z_-]):/g, "$1\\:"); } function dataURItoBlob(dataURI) { - const chunks = dataURI.split(','); - const byteString = chunks[0].indexOf('base64') >= 0 ? - window.atob(chunks[1]) : - unescape(chunks[1]); - const mimeString = chunks[0].split(':')[1].split(';')[0]; + const chunks = dataURI.split(","); + const byteString = + chunks[0].indexOf("base64") >= 0 + ? window.atob(chunks[1]) + : unescape(chunks[1]); + const mimeString = chunks[0].split(":")[1].split(";")[0]; const data = new Uint8Array(byteString.length); for (let i = 0; i < byteString.length; i++) { data[i] = byteString.charCodeAt(i); } - return new Blob([data], {type: mimeString}); + return new Blob([data], { type: mimeString }); } function getPrettyTagName(tag) { diff --git a/client/js/util/optimized_resize.js b/client/js/util/optimized_resize.js index 545c458..d45df4a 100644 --- a/client/js/util/optimized_resize.js +++ b/client/js/util/optimized_resize.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; let callbacks = []; let running = false; @@ -15,7 +15,7 @@ function resize() { } function runCallbacks() { - callbacks.forEach(callback => { + callbacks.forEach((callback) => { callback(); }); running = false; @@ -26,8 +26,8 @@ function add(callback) { } function remove(callback) { - callbacks = callbacks.filter(c => c !== callback); + callbacks = callbacks.filter((c) => c !== callback); } -window.addEventListener('resize', resize); -module.exports = {add: add, remove: remove}; +window.addEventListener("resize", resize); +module.exports = { add: add, remove: remove }; diff --git a/client/js/util/polyfill.js b/client/js/util/polyfill.js index 09f5835..ec809ff 100644 --- a/client/js/util/polyfill.js +++ b/client/js/util/polyfill.js @@ -1,11 +1,11 @@ /* eslint-disable func-names, no-extend-native */ -'use strict'; +"use strict"; // fix iterating over NodeList in Chrome and Opera NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; -NodeList.prototype.querySelector = function(...args) { +NodeList.prototype.querySelector = function (...args) { for (let node of this) { if (node.nodeType === 3) { continue; @@ -18,7 +18,7 @@ NodeList.prototype.querySelector = function(...args) { return null; }; -NodeList.prototype.querySelectorAll = function(...args) { +NodeList.prototype.querySelectorAll = function (...args) { let result = []; for (let node of this) { if (node.nodeType === 3) { @@ -32,7 +32,7 @@ NodeList.prototype.querySelectorAll = function(...args) { }; // non standard -Node.prototype.prependChild = function(child) { +Node.prototype.prependChild = function (child) { if (this.firstChild) { this.insertBefore(child, this.firstChild); } else { @@ -41,29 +41,25 @@ Node.prototype.prependChild = function(child) { }; // non standard -Promise.prototype.always = function(onResolveOrReject) { - return this.then( - onResolveOrReject, - reason => { - onResolveOrReject(reason); - throw reason; - }); +Promise.prototype.always = function (onResolveOrReject) { + return this.then(onResolveOrReject, (reason) => { + onResolveOrReject(reason); + throw reason; + }); }; // non standard -Number.prototype.between = function(a, b, inclusive) { +Number.prototype.between = function (a, b, inclusive) { const min = Math.min(a, b); const max = Math.max(a, b); - return inclusive ? - this >= min && this <= max : - this > min && this < max; + return inclusive ? this >= min && this <= max : this > min && this < max; }; // non standard Promise.prototype.abort = () => {}; // non standard -Date.prototype.addDays = function(days) { +Date.prototype.addDays = function (days) { let dat = new Date(this.valueOf()); dat.setDate(dat.getDate() + days); return dat; diff --git a/client/js/util/progress.js b/client/js/util/progress.js index 98df179..d6d12cb 100644 --- a/client/js/util/progress.js +++ b/client/js/util/progress.js @@ -1,6 +1,6 @@ -'use strict'; +"use strict"; -const nprogress = require('nprogress'); +const nprogress = require("nprogress"); let nesting = 0; diff --git a/client/js/util/search.js b/client/js/util/search.js index 8d5fca4..c540c2f 100644 --- a/client/js/util/search.js +++ b/client/js/util/search.js @@ -1,14 +1,16 @@ -'use strict'; +"use strict"; -const misc = require('./misc.js'); -const keyboard = require('../util/keyboard.js'); -const views = require('./views.js'); +const misc = require("./misc.js"); +const keyboard = require("../util/keyboard.js"); +const views = require("./views.js"); function searchInputNodeFocusHelper(inputNode) { - keyboard.bind('q', () => { + keyboard.bind("q", () => { inputNode.focus(); inputNode.setSelectionRange( - inputNode.value.length, inputNode.value.length); + inputNode.value.length, + inputNode.value.length + ); }); } diff --git a/client/js/util/touch.js b/client/js/util/touch.js index 53b0978..64bd00a 100644 --- a/client/js/util/touch.js +++ b/client/js/util/touch.js @@ -1,11 +1,11 @@ -'use strict'; +"use strict"; const direction = { NONE: null, - LEFT: 'left', - RIGHT: 'right', - DOWN: 'down', - UP: 'up' + LEFT: "left", + RIGHT: "right", + DOWN: "down", + UP: "up", }; function handleTouchStart(handler, evt) { @@ -37,20 +37,20 @@ function handleTouchMove(handler, evt) { function handleTouchEnd(handler) { switch (handler._direction) { - case direction.NONE: - return; - case direction.LEFT: - handler._swipeLeftTask(); - break; - case direction.RIGHT: - handler._swipeRightTask(); - break; - case direction.DOWN: - handler._swipeDownTask(); - break; - case direction.UP: - handler._swipeUpTask(); - // no default + case direction.NONE: + return; + case direction.LEFT: + handler._swipeLeftTask(); + break; + case direction.RIGHT: + handler._swipeRightTask(); + break; + case direction.DOWN: + handler._swipeDownTask(); + break; + case direction.UP: + handler._swipeUpTask(); + // no default } handler._xStart = null; @@ -58,11 +58,13 @@ function handleTouchEnd(handler) { } class Touch { - constructor(target, + constructor( + target, swipeLeft = () => {}, swipeRight = () => {}, swipeUp = () => {}, - swipeDown = () => {}) { + swipeDown = () => {} + ) { this._target = target; this._swipeLeftTask = swipeLeft; @@ -74,18 +76,15 @@ class Touch { this._yStart = null; this._direction = direction.NONE; - this._target.addEventListener('touchstart', - evt => { - handleTouchStart(this, evt); - }); - this._target.addEventListener('touchmove', - evt => { - handleTouchMove(this, evt); - }); - this._target.addEventListener('touchend', - () => { - handleTouchEnd(this); - }); + this._target.addEventListener("touchstart", (evt) => { + handleTouchStart(this, evt); + }); + this._target.addEventListener("touchmove", (evt) => { + handleTouchMove(this, evt); + }); + this._target.addEventListener("touchend", () => { + handleTouchEnd(this); + }); } } diff --git a/client/js/util/uri.js b/client/js/util/uri.js index 868545f..16fa4f8 100644 --- a/client/js/util/uri.js +++ b/client/js/util/uri.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; function formatApiLink(...values) { let parts = []; @@ -9,18 +9,19 @@ function formatApiLink(...values) { for (let key of Object.keys(value)) { if (value[key]) { variableParts.push( - key + '=' + encodeURIComponent(value[key].toString())); + key + "=" + encodeURIComponent(value[key].toString()) + ); } } if (variableParts.length) { - parts.push('?' + variableParts.join('&')); + parts.push("?" + variableParts.join("&")); } break; } else { parts.push(encodeURIComponent(value.toString())); } } - return '/' + parts.join('/'); + return "/" + parts.join("/"); } function escapeParam(text) { @@ -40,48 +41,52 @@ function formatClientLink(...values) { for (let key of Object.keys(value)) { if (value[key]) { variableParts.push( - key + '=' + escapeParam(value[key].toString())); + key + "=" + escapeParam(value[key].toString()) + ); } } if (variableParts.length) { - parts.push(variableParts.join(';')); + parts.push(variableParts.join(";")); } break; } else { parts.push(escapeParam(value.toString())); } } - return parts.join('/'); + return parts.join("/"); } function extractHostname(url) { // https://stackoverflow.com/a/23945027 return url - .split('/')[url.indexOf("//") > -1 ? 2 : 0] - .split(':')[0] - .split('?')[0]; + .split("/") + [url.indexOf("//") > -1 ? 2 : 0].split(":")[0] + .split("?")[0]; } function extractRootDomain(url) { // https://stackoverflow.com/a/23945027 let domain = extractHostname(url); - let splitArr = domain.split('.'); + let splitArr = domain.split("."); let arrLen = splitArr.length; // if there is a subdomain if (arrLen > 2) { - domain = splitArr[arrLen - 2] + '.' + splitArr[arrLen - 1]; + domain = splitArr[arrLen - 2] + "." + splitArr[arrLen - 1]; // check to see if it's using a Country Code Top Level Domain (ccTLD) (i.e. ".me.uk") - if (splitArr[arrLen - 2].length === 2 && splitArr[arrLen - 1].length === 2) { + if ( + splitArr[arrLen - 2].length === 2 && + splitArr[arrLen - 1].length === 2 + ) { // this is using a ccTLD - domain = splitArr[arrLen - 3] + '.' + domain; + domain = splitArr[arrLen - 3] + "." + domain; } } return domain; } function escapeColons(text) { - return text.replace(new RegExp(':', 'g'), '\\:'); + return text.replace(new RegExp(":", "g"), "\\:"); } module.exports = { diff --git a/client/js/util/views.js b/client/js/util/views.js index afdda76..f69b34b 100644 --- a/client/js/util/views.js +++ b/client/js/util/views.js @@ -1,27 +1,27 @@ -'use strict'; +"use strict"; -require('../util/polyfill.js'); -const api = require('../api.js'); -const templates = require('../templates.js'); +require("../util/polyfill.js"); +const api = require("../api.js"); +const templates = require("../templates.js"); const domParser = new DOMParser(); -const misc = require('./misc.js'); -const uri = require('./uri.js'); +const misc = require("./misc.js"); +const uri = require("./uri.js"); function _imbueId(options) { if (!options.id) { - options.id = 'gen-' + Math.random().toString(36).substring(7); + options.id = "gen-" + Math.random().toString(36).substring(7); } } function _makeLabel(options, attrs) { if (!options.text) { - return ''; + return ""; } if (!attrs) { attrs = {}; } attrs.for = options.id; - return makeElement('label', attrs, options.text); + return makeElement("label", attrs, options.text); } function makeFileSize(fileSize) { @@ -34,251 +34,282 @@ function makeMarkdown(text) { function makeRelativeTime(time) { return makeElement( - 'time', {datetime: time, title: time}, misc.formatRelativeTime(time)); + "time", + { datetime: time, title: time }, + misc.formatRelativeTime(time) + ); } function makeThumbnail(url) { return makeElement( - 'span', - url ? - {class: 'thumbnail', style: `background-image: url(\'${url}\')`} : - {class: 'thumbnail empty'}, - makeElement('img', {alt: 'thumbnail', src: url})); + "span", + url + ? { + class: "thumbnail", + style: `background-image: url(\'${url}\')`, + } + : { class: "thumbnail empty" }, + makeElement("img", { alt: "thumbnail", src: url }) + ); } function makeRadio(options) { _imbueId(options); return makeElement( - 'label', - {for: options.id}, - makeElement( - 'input', - { - id: options.id, - name: options.name, - value: options.value, - type: 'radio', - checked: options.selectedValue === options.value, - disabled: options.readonly, - required: options.required, - }), - makeElement('span', {class: 'radio'}, options.text)); + "label", + { for: options.id }, + makeElement("input", { + id: options.id, + name: options.name, + value: options.value, + type: "radio", + checked: options.selectedValue === options.value, + disabled: options.readonly, + required: options.required, + }), + makeElement("span", { class: "radio" }, options.text) + ); } function makeCheckbox(options) { _imbueId(options); return makeElement( - 'label', - {for: options.id}, - makeElement( - 'input', - { - id: options.id, - name: options.name, - value: options.value, - type: 'checkbox', - checked: options.checked !== undefined ? - options.checked : false, - disabled: options.readonly, - required: options.required, - }), - makeElement('span', {class: 'checkbox'}, options.text)); + "label", + { for: options.id }, + makeElement("input", { + id: options.id, + name: options.name, + value: options.value, + type: "checkbox", + checked: options.checked !== undefined ? options.checked : false, + disabled: options.readonly, + required: options.required, + }), + makeElement("span", { class: "checkbox" }, options.text) + ); } function makeSelect(options) { - return _makeLabel(options) + + return ( + _makeLabel(options) + makeElement( - 'select', + "select", { id: options.id, name: options.name, disabled: options.readonly, }, - ...Object.keys(options.keyValues).map(key => makeElement( - 'option', - {value: key, selected: key === options.selectedKey}, - options.keyValues[key]))); + ...Object.keys(options.keyValues).map((key) => + makeElement( + "option", + { value: key, selected: key === options.selectedKey }, + options.keyValues[key] + ) + ) + ) + ); } function makeInput(options) { - options.value = options.value || ''; - return _makeLabel(options) + makeElement('input', options); + options.value = options.value || ""; + return _makeLabel(options) + makeElement("input", options); } function makeButton(options) { - options.type = 'button'; + options.type = "button"; return makeInput(options); } function makeTextInput(options) { - options.type = 'text'; + options.type = "text"; return makeInput(options); } function makeTextarea(options) { - const value = options.value || ''; + const value = options.value || ""; delete options.value; - return _makeLabel(options) + makeElement('textarea', options, value); + return _makeLabel(options) + makeElement("textarea", options, value); } function makePasswordInput(options) { - options.type = 'password'; + options.type = "password"; return makeInput(options); } function makeEmailInput(options) { - options.type = 'email'; + options.type = "email"; return makeInput(options); } function makeColorInput(options) { - const textInput = makeElement( - 'input', { - type: 'text', - value: options.value || '', - required: options.required, - class: 'color', - }); - const backgroundPreviewNode = makeElement( - 'div', - { - class: 'preview background-preview', - style: - `border-color: ${options.value}; + const textInput = makeElement("input", { + type: "text", + value: options.value || "", + required: options.required, + class: "color", + }); + const backgroundPreviewNode = makeElement("div", { + class: "preview background-preview", + style: `border-color: ${options.value}; background-color: ${options.value}`, - }); - const textPreviewNode = makeElement( - 'div', - { - class: 'preview text-preview', - style: - `border-color: ${options.value}; + }); + const textPreviewNode = makeElement("div", { + class: "preview text-preview", + style: `border-color: ${options.value}; color: ${options.value}`, - }); + }); return makeElement( - 'label', {class: 'color'}, textInput, backgroundPreviewNode, textPreviewNode); + "label", + { class: "color" }, + textInput, + backgroundPreviewNode, + textPreviewNode + ); } function makeNumericInput(options) { - options.type = 'number'; + options.type = "number"; return makeInput(options); } function makeDateInput(options) { - options.type = 'date'; - return makeInput(options) + options.type = "date"; + return makeInput(options); } function getPostUrl(id, parameters) { return uri.formatClientLink( - 'post', id, parameters ? {query: parameters.query} : {}); + "post", + id, + parameters ? { query: parameters.query } : {} + ); } function getPostEditUrl(id, parameters) { return uri.formatClientLink( - 'post', id, 'edit', parameters ? {query: parameters.query} : {}); + "post", + id, + "edit", + parameters ? { query: parameters.query } : {} + ); } function makePostLink(id, includeHash) { let text = id; if (includeHash) { - text = '@' + id; + text = "@" + id; } - return api.hasPrivilege('posts:view') ? - makeElement( - 'a', - {href: uri.formatClientLink('post', id)}, - misc.escapeHtml(text)) : - misc.escapeHtml(text); + return api.hasPrivilege("posts:view") + ? makeElement( + "a", + { href: uri.formatClientLink("post", id) }, + misc.escapeHtml(text) + ) + : misc.escapeHtml(text); } function makeTagLink(name, includeHash, includeCount, tag) { - const category = tag ? tag.category : 'unknown'; + const category = tag ? tag.category : "unknown"; let text = misc.getPrettyTagName(name); if (includeHash === true) { - text = '#' + text; + text = "#" + text; } if (includeCount === true) { - text += ' (' + (tag ? tag.postCount : 0) + ')'; + text += " (" + (tag ? tag.postCount : 0) + ")"; } - return api.hasPrivilege('tags:view') ? - makeElement( - 'a', - { - href: uri.formatClientLink('tag', name), - class: misc.makeCssName(category, 'tag'), - }, - misc.escapeHtml(text)) : - makeElement( - 'span', - {class: misc.makeCssName(category, 'tag')}, - misc.escapeHtml(text)); + return api.hasPrivilege("tags:view") + ? makeElement( + "a", + { + href: uri.formatClientLink("tag", name), + class: misc.makeCssName(category, "tag"), + }, + misc.escapeHtml(text) + ) + : makeElement( + "span", + { class: misc.makeCssName(category, "tag") }, + misc.escapeHtml(text) + ); } function makePoolLink(id, includeHash, includeCount, pool, name) { - const category = pool ? pool.category : 'unknown'; + const category = pool ? pool.category : "unknown"; let text = name ? name : pool.names[0]; if (includeHash === true) { - text = '#' + text; + text = "#" + text; } if (includeCount === true) { - text += ' (' + (pool ? pool.postCount : 0) + ')'; + text += " (" + (pool ? pool.postCount : 0) + ")"; } - return api.hasPrivilege('pools:view') ? - makeElement( - 'a', - { - href: uri.formatClientLink('pool', id), - class: misc.makeCssName(category, 'pool'), - }, - misc.escapeHtml(text)) : - makeElement( - 'span', - {class: misc.makeCssName(category, 'pool')}, - misc.escapeHtml(text)); + return api.hasPrivilege("pools:view") + ? makeElement( + "a", + { + href: uri.formatClientLink("pool", id), + class: misc.makeCssName(category, "pool"), + }, + misc.escapeHtml(text) + ) + : makeElement( + "span", + { class: misc.makeCssName(category, "pool") }, + misc.escapeHtml(text) + ); } function makeUserLink(user) { let text = makeThumbnail(user ? user.avatarUrl : null); - text += user && user.name ? misc.escapeHtml(user.name) : 'Anonymous'; - const link = user && api.hasPrivilege('users:view') ? - makeElement( - 'a', {href: uri.formatClientLink('user', user.name)}, text) : - text; - return makeElement('span', {class: 'user'}, link); + text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous"; + const link = + user && api.hasPrivilege("users:view") + ? makeElement( + "a", + { href: uri.formatClientLink("user", user.name) }, + text + ) + : text; + return makeElement("span", { class: "user" }, link); } function makeFlexboxAlign(options) { return [...misc.range(20)] - .map(() => '
      • ').join(''); + .map(() => '
      • ') + .join(""); } function makeAccessKey(html, key) { - const regex = new RegExp('(' + key + ')', 'i'); + const regex = new RegExp("(" + key + ")", "i"); html = html.replace( - regex, '$1'); + regex, + '$1' + ); return html; } function _serializeElement(name, attributes) { return [name] - .concat(Object.keys(attributes).map(key => { - if (attributes[key] === true) { - return key; - } else if (attributes[key] === false || - attributes[key] === undefined) { - return ''; - } - const attribute = misc.escapeHtml(attributes[key] || ''); - return `${key}="${attribute}"`; - })) - .join(' '); + .concat( + Object.keys(attributes).map((key) => { + if (attributes[key] === true) { + return key; + } else if ( + attributes[key] === false || + attributes[key] === undefined + ) { + return ""; + } + const attribute = misc.escapeHtml(attributes[key] || ""); + return `${key}="${attribute}"`; + }) + ) + .join(" "); } function makeElement(name, attrs, ...content) { - return content.length !== undefined ? - `<${_serializeElement(name, attrs)}>${content.join('')}` : - `<${_serializeElement(name, attrs)}/>`; + return content.length !== undefined + ? `<${_serializeElement(name, attrs)}>${content.join("")}` + : `<${_serializeElement(name, attrs)}/>`; } function emptyContent(target) { @@ -302,25 +333,25 @@ function replaceContent(target, source) { function showMessage(target, message, className) { if (!message) { - message = 'Unknown message'; + message = "Unknown message"; } - const messagesHolderNode = target.querySelector('.messages'); + const messagesHolderNode = target.querySelector(".messages"); if (!messagesHolderNode) { return false; } - const textNode = document.createElement('div'); - textNode.innerHTML = message.replace(/\n/g, '
        '); - textNode.classList.add('message'); + const textNode = document.createElement("div"); + textNode.innerHTML = message.replace(/\n/g, "
        "); + textNode.classList.add("message"); textNode.classList.add(className); - const wrapperNode = document.createElement('div'); - wrapperNode.classList.add('message-wrapper'); + const wrapperNode = document.createElement("div"); + wrapperNode.classList.add("message-wrapper"); wrapperNode.appendChild(textNode); messagesHolderNode.appendChild(wrapperNode); return true; } function appendExclamationMark() { - if (!document.title.startsWith('!')) { + if (!document.title.startsWith("!")) { document.oldTitle = document.title; document.title = `! ${document.title}`; } @@ -328,15 +359,15 @@ function appendExclamationMark() { function showError(target, message) { appendExclamationMark(); - return showMessage(target, misc.formatInlineMarkdown(message), 'error'); + return showMessage(target, misc.formatInlineMarkdown(message), "error"); } function showSuccess(target, message) { - return showMessage(target, misc.formatInlineMarkdown(message), 'success'); + return showMessage(target, misc.formatInlineMarkdown(message), "success"); } function showInfo(target, message) { - return showMessage(target, misc.formatInlineMarkdown(message), 'info'); + return showMessage(target, misc.formatInlineMarkdown(message), "info"); } function clearMessages(target) { @@ -344,7 +375,7 @@ function clearMessages(target) { document.title = document.oldTitle; document.oldTitle = null; } - for (let messagesHolderNode of target.querySelectorAll('.messages')) { + for (let messagesHolderNode of target.querySelectorAll(".messages")) { emptyContent(messagesHolderNode); } } @@ -352,15 +383,15 @@ function clearMessages(target) { function htmlToDom(html) { // code taken from jQuery + Krasimir Tsonev's blog const wrapMap = { - _: [1, '
        ', '
        '], - option: [1, ''], - legend: [1, '
        ', '
        '], - area: [1, '', ''], - param: [1, '', ''], - thead: [1, '', '
        '], - tr: [2, '', '
        '], - td: [3, '', '
        '], - col: [2, '', '
        '], + _: [1, "
        ", "
        "], + option: [1, ""], + legend: [1, "
        ", "
        "], + area: [1, "", ""], + param: [1, "", ""], + thead: [1, "", "
        "], + tr: [2, "", "
        "], + td: [3, "", "
        "], + col: [2, "", "
        "], }; wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.thead; @@ -369,8 +400,8 @@ function htmlToDom(html) { wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; - let element = document.createElement('div'); - const match = (/<\s*(\w+)[^>]*?>/g).exec(html); + let element = document.createElement("div"); + const match = /<\s*(\w+)[^>]*?>/g.exec(html); if (match) { const tag = match[1]; @@ -382,9 +413,9 @@ function htmlToDom(html) { } else { element.innerHTML = html; } - return element.childNodes.length > 1 ? - element.childNodes : - element.firstChild; + return element.childNodes.length > 1 + ? element.childNodes + : element.firstChild; } function getTemplate(templatePath) { @@ -392,7 +423,7 @@ function getTemplate(templatePath) { throw `Missing template: ${templatePath}`; } const templateFactory = templates[templatePath]; - return ctx => { + return (ctx) => { if (!ctx) { ctx = {}; } @@ -423,7 +454,7 @@ function getTemplate(templatePath) { makeElement: makeElement, makeCssName: misc.makeCssName, makeNumericInput: makeNumericInput, - formatClientLink: uri.formatClientLink + formatClientLink: uri.formatClientLink, }); return htmlToDom(templateFactory(ctx)); }; @@ -432,49 +463,51 @@ function getTemplate(templatePath) { function decorateValidator(form) { // postpone showing form fields validity until user actually tries // to submit it (seeing red/green form w/o doing anything breaks POLA) - let submitButton = form.querySelector('.buttons input'); + let submitButton = form.querySelector(".buttons input"); if (!submitButton) { - submitButton = form.querySelector('input[type=submit]'); + submitButton = form.querySelector("input[type=submit]"); } if (submitButton) { - submitButton.addEventListener('click', e => { - form.classList.add('show-validation'); + submitButton.addEventListener("click", (e) => { + form.classList.add("show-validation"); }); } - form.addEventListener('submit', e => { - form.classList.remove('show-validation'); + form.addEventListener("submit", (e) => { + form.classList.remove("show-validation"); }); } function disableForm(form) { - for (let input of form.querySelectorAll('input')) { + for (let input of form.querySelectorAll("input")) { input.disabled = true; } } function enableForm(form) { - for (let input of form.querySelectorAll('input')) { + for (let input of form.querySelectorAll("input")) { input.disabled = false; } } function syncScrollPosition() { - window.requestAnimationFrame( - () => { - if (history.state && Object.prototype.hasOwnProperty.call(history.state, 'scrollX')) { - window.scrollTo(history.state.scrollX, history.state.scrollY); - } else { - window.scrollTo(0, 0); - } - }); + window.requestAnimationFrame(() => { + if ( + history.state && + Object.prototype.hasOwnProperty.call(history.state, "scrollX") + ) { + window.scrollTo(history.state.scrollX, history.state.scrollY); + } else { + window.scrollTo(0, 0); + } + }); } function slideDown(element) { const duration = 500; return new Promise((resolve, reject) => { const height = element.getBoundingClientRect().height; - element.style.maxHeight = '0'; - element.style.overflow = 'hidden'; + element.style.maxHeight = "0"; + element.style.overflow = "hidden"; window.setTimeout(() => { element.style.transition = `all ${duration}ms ease`; element.style.maxHeight = `${height}px`; @@ -489,7 +522,7 @@ function slideUp(element) { const duration = 500; return new Promise((resolve, reject) => { const height = element.getBoundingClientRect().height; - element.style.overflow = 'hidden'; + element.style.overflow = "hidden"; element.style.maxHeight = `${height}px`; element.style.transition = `all ${duration}ms ease`; window.setTimeout(() => { @@ -502,26 +535,27 @@ function slideUp(element) { } function monitorNodeRemoval(monitoredNode, callback) { - const mutationObserver = new MutationObserver( - mutations => { - for (let mutation of mutations) { - for (let node of mutation.removedNodes) { - if (node.contains(monitoredNode)) { - mutationObserver.disconnect(); - callback(); - return; - } + const mutationObserver = new MutationObserver((mutations) => { + for (let mutation of mutations) { + for (let node of mutation.removedNodes) { + if (node.contains(monitoredNode)) { + mutationObserver.disconnect(); + callback(); + return; } } - }); - mutationObserver.observe( - document.body, {childList: true, subtree: true}); + } + }); + mutationObserver.observe(document.body, { + childList: true, + subtree: true, + }); } -document.addEventListener('input', e => { - if (e.target.classList.contains('color')) { - let bkNode = e.target.parentNode.querySelector('.background-preview'); - let textNode = e.target.parentNode.querySelector('.text-preview'); +document.addEventListener("input", (e) => { + if (e.target.classList.contains("color")) { + let bkNode = e.target.parentNode.querySelector(".background-preview"); + let textNode = e.target.parentNode.querySelector(".text-preview"); bkNode.style.backgroundColor = e.target.value; bkNode.style.borderColor = e.target.value; textNode.style.color = e.target.value; @@ -530,8 +564,8 @@ document.addEventListener('input', e => { }); // prevent opening buttons in new tabs -document.addEventListener('click', e => { - if (e.target.getAttribute('href') === '' && e.which === 2) { +document.addEventListener("click", (e) => { + if (e.target.getAttribute("href") === "" && e.which === 2) { e.preventDefault(); } }); diff --git a/client/js/views/comments_page_view.js b/client/js/views/comments_page_view.js index d5bb294..5648b3a 100644 --- a/client/js/views/comments_page_view.js +++ b/client/js/views/comments_page_view.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); -const CommentListControl = require('../controls/comment_list_control.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); +const CommentListControl = require("../controls/comment_list_control.js"); -const template = views.getTemplate('comments-page'); +const template = views.getTemplate("comments-page"); class CommentsPageView extends events.EventTarget { constructor(ctx) { @@ -16,12 +16,14 @@ class CommentsPageView extends events.EventTarget { for (let post of ctx.response.results) { const commentListControl = new CommentListControl( sourceNode.querySelector( - `.comments-container[data-for="${post.id}"]`), + `.comments-container[data-for="${post.id}"]` + ), post.comments, - true); - events.proxyEvent(commentListControl, this, 'submit'); - events.proxyEvent(commentListControl, this, 'score'); - events.proxyEvent(commentListControl, this, 'delete'); + true + ); + events.proxyEvent(commentListControl, this, "submit"); + events.proxyEvent(commentListControl, this, "score"); + events.proxyEvent(commentListControl, this, "delete"); } views.replaceContent(this._hostNode, sourceNode); diff --git a/client/js/views/empty_view.js b/client/js/views/empty_view.js index 21843d7..59d336d 100644 --- a/client/js/views/empty_view.js +++ b/client/js/views/empty_view.js @@ -1,15 +1,16 @@ -'use strict'; +"use strict"; -const views = require('../util/views.js'); +const views = require("../util/views.js"); const template = () => { return views.htmlToDom( - '
        '); + '
        ' + ); }; class EmptyView { constructor() { - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); views.replaceContent(this._hostNode, template()); views.syncScrollPosition(); } diff --git a/client/js/views/endless_page_view.js b/client/js/views/endless_page_view.js index f30b8fb..f94c371 100644 --- a/client/js/views/endless_page_view.js +++ b/client/js/views/endless_page_view.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; -const router = require('../router.js'); -const views = require('../util/views.js'); +const router = require("../router.js"); +const views = require("../util/views.js"); -const holderTemplate = views.getTemplate('endless-pager'); -const pageTemplate = views.getTemplate('endless-pager-page'); +const holderTemplate = views.getTemplate("endless-pager"); +const pageTemplate = views.getTemplate("endless-pager-page"); function isScrolledIntoView(element) { let top = 0; @@ -12,14 +12,12 @@ function isScrolledIntoView(element) { top += element.offsetTop || 0; element = element.offsetParent; } while (element); - return ( - (top >= window.scrollY) && - (top <= window.scrollY + window.innerHeight)); + return top >= window.scrollY && top <= window.scrollY + window.innerHeight; } class EndlessPageView { constructor(ctx) { - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); views.replaceContent(this._hostNode, holderTemplate()); } @@ -40,12 +38,13 @@ class EndlessPageView { this.defaultLimit = parseInt(ctx.parameters.limit || ctx.defaultLimit); const initialOffset = parseInt(ctx.parameters.offset || 0); - this._loadPage(ctx, initialOffset, this.defaultLimit, true) - .then(pageNode => { + this._loadPage(ctx, initialOffset, this.defaultLimit, true).then( + (pageNode) => { if (initialOffset !== 0) { pageNode.scrollIntoView(); } - }); + } + ); this._timeout = window.setInterval(() => { window.requestAnimationFrame(() => { @@ -58,19 +57,19 @@ class EndlessPageView { } get pageHeaderHolderNode() { - return this._hostNode.querySelector('.page-header-holder'); + return this._hostNode.querySelector(".page-header-holder"); } get topPageGuardNode() { - return this._hostNode.querySelector('.page-guard.top'); + return this._hostNode.querySelector(".page-guard.top"); } get bottomPageGuardNode() { - return this._hostNode.querySelector('.page-guard.bottom'); + return this._hostNode.querySelector(".page-guard.bottom"); } get _pagesHolderNode() { - return this._hostNode.querySelector('.pages-holder'); + return this._hostNode.querySelector(".pages-holder"); } _destroy() { @@ -82,9 +81,10 @@ class EndlessPageView { let topPageNode = null; let element = document.elementFromPoint( window.innerWidth / 2, - window.innerHeight / 2); + window.innerHeight / 2 + ); while (element.parentNode !== null) { - if (element.classList.contains('page')) { + if (element.classList.contains("page")) { topPageNode = element; break; } @@ -93,15 +93,17 @@ class EndlessPageView { if (!topPageNode) { return; } - let topOffset = parseInt(topPageNode.getAttribute('data-offset')); - let topLimit = parseInt(topPageNode.getAttribute('data-limit')); + let topOffset = parseInt(topPageNode.getAttribute("data-offset")); + let topLimit = parseInt(topPageNode.getAttribute("data-limit")); if (topOffset !== this.currentOffset) { router.replace( ctx.getClientUrlForPage( topOffset, - topLimit === ctx.defaultLimit ? null : topLimit), + topLimit === ctx.defaultLimit ? null : topLimit + ), ctx.state, - false); + false + ); this.currentOffset = topOffset; } } @@ -115,43 +117,47 @@ class EndlessPageView { return; } - if (this.minOffsetShown > 0 && - isScrolledIntoView(this.topPageGuardNode)) { + if ( + this.minOffsetShown > 0 && + isScrolledIntoView(this.topPageGuardNode) + ) { this._loadPage( ctx, this.minOffsetShown - this.defaultLimit, this.defaultLimit, - false); + false + ); } - if (this.maxOffsetShown < this.totalRecords && - isScrolledIntoView(this.bottomPageGuardNode)) { - this._loadPage( - ctx, - this.maxOffsetShown, - this.defaultLimit, - true); + if ( + this.maxOffsetShown < this.totalRecords && + isScrolledIntoView(this.bottomPageGuardNode) + ) { + this._loadPage(ctx, this.maxOffsetShown, this.defaultLimit, true); } } _loadPage(ctx, offset, limit, append) { this._runningRequests++; return new Promise((resolve, reject) => { - ctx.requestPage(offset, limit).then(response => { - if (!this._active) { + ctx.requestPage(offset, limit).then( + (response) => { + if (!this._active) { + this._runningRequests--; + return Promise.reject(); + } + window.requestAnimationFrame(() => { + let pageNode = this._renderPage(ctx, append, response); + this._runningRequests--; + resolve(pageNode); + }); + }, + (error) => { + this.showError(error.message); this._runningRequests--; - return Promise.reject(); + reject(); } - window.requestAnimationFrame(() => { - let pageNode = this._renderPage(ctx, append, response); - this._runningRequests--; - resolve(pageNode); - }); - }, error => { - this.showError(error.message); - this._runningRequests--; - reject(); - }); + ); }); } @@ -162,30 +168,35 @@ class EndlessPageView { pageNode = pageTemplate({ totalPages: Math.ceil(response.total / response.limit), page: Math.ceil( - (response.offset + response.limit) / response.limit), + (response.offset + response.limit) / response.limit + ), }); - pageNode.setAttribute('data-offset', response.offset); - pageNode.setAttribute('data-limit', response.limit); + pageNode.setAttribute("data-offset", response.offset); + pageNode.setAttribute("data-limit", response.limit); ctx.pageRenderer({ parameters: ctx.parameters, response: response, - hostNode: pageNode.querySelector('.page-content-holder'), + hostNode: pageNode.querySelector(".page-content-holder"), }); this.totalRecords = response.total; - if (response.offset < this.minOffsetShown || - this.minOffsetShown === null) { + if ( + response.offset < this.minOffsetShown || + this.minOffsetShown === null + ) { this.minOffsetShown = response.offset; } - if (response.offset + response.results.length - > this.maxOffsetShown || - this.maxOffsetShown === null) { + if ( + response.offset + response.results.length > + this.maxOffsetShown || + this.maxOffsetShown === null + ) { this.maxOffsetShown = response.offset + response.results.length; } - response.results.addEventListener('remove', e => { + response.results.addEventListener("remove", (e) => { this.maxOffsetShown--; this.totalRecords--; }); @@ -200,10 +211,11 @@ class EndlessPageView { window.scroll( window.scrollX, - window.scrollY + pageNode.offsetHeight); + window.scrollY + pageNode.offsetHeight + ); } } else if (!response.results.length) { - this.showInfo('No data to show'); + this.showInfo("No data to show"); } this._initialPageLoad = false; diff --git a/client/js/views/help_view.js b/client/js/views/help_view.js index 1ab016d..a88b016 100644 --- a/client/js/views/help_view.js +++ b/client/js/views/help_view.js @@ -1,73 +1,81 @@ -'use strict'; +"use strict"; -const api = require('../api.js'); -const views = require('../util/views.js'); +const api = require("../api.js"); +const views = require("../util/views.js"); -const template = views.getTemplate('help'); +const template = views.getTemplate("help"); const sectionTemplates = { - 'about': views.getTemplate('help-about'), - 'keyboard': views.getTemplate('help-keyboard'), - 'search': views.getTemplate('help-search'), - 'comments': views.getTemplate('help-comments'), - 'tos': views.getTemplate('help-tos'), + about: views.getTemplate("help-about"), + keyboard: views.getTemplate("help-keyboard"), + search: views.getTemplate("help-search"), + comments: views.getTemplate("help-comments"), + tos: views.getTemplate("help-tos"), }; const subsectionTemplates = { - 'search': { - 'default': views.getTemplate('help-search-general'), - 'posts': views.getTemplate('help-search-posts'), - 'users': views.getTemplate('help-search-users'), - 'tags': views.getTemplate('help-search-tags'), - 'pools': views.getTemplate('help-search-pools'), + search: { + default: views.getTemplate("help-search-general"), + posts: views.getTemplate("help-search-posts"), + users: views.getTemplate("help-search-users"), + tags: views.getTemplate("help-search-tags"), + pools: views.getTemplate("help-search-pools"), }, }; class HelpView { constructor(section, subsection) { - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); const sourceNode = template(); const ctx = { name: api.getName(), }; - section = section || 'about'; + section = section || "about"; if (section in sectionTemplates) { views.replaceContent( - sourceNode.querySelector('.content'), - sectionTemplates[section](ctx)); + sourceNode.querySelector(".content"), + sectionTemplates[section](ctx) + ); } - subsection = subsection || 'default'; - if (section in subsectionTemplates && - subsection in subsectionTemplates[section]) { + subsection = subsection || "default"; + if ( + section in subsectionTemplates && + subsection in subsectionTemplates[section] + ) { views.replaceContent( - sourceNode.querySelector('.subcontent'), - subsectionTemplates[section][subsection](ctx)); + sourceNode.querySelector(".subcontent"), + subsectionTemplates[section][subsection](ctx) + ); } views.replaceContent(this._hostNode, sourceNode); - for (let itemNode of - sourceNode.querySelectorAll('.primary [data-name]')) { + for (let itemNode of sourceNode.querySelectorAll( + ".primary [data-name]" + )) { itemNode.classList.toggle( - 'active', - itemNode.getAttribute('data-name') === section); - if (itemNode.getAttribute('data-name') === section) { + "active", + itemNode.getAttribute("data-name") === section + ); + if (itemNode.getAttribute("data-name") === section) { itemNode.parentNode.scrollLeft = itemNode.getBoundingClientRect().left - - itemNode.parentNode.getBoundingClientRect().left + itemNode.parentNode.getBoundingClientRect().left; } } - for (let itemNode of - sourceNode.querySelectorAll('.secondary [data-name]')) { + for (let itemNode of sourceNode.querySelectorAll( + ".secondary [data-name]" + )) { itemNode.classList.toggle( - 'active', - itemNode.getAttribute('data-name') === subsection); - if (itemNode.getAttribute('data-name') === subsection) { + "active", + itemNode.getAttribute("data-name") === subsection + ); + if (itemNode.getAttribute("data-name") === subsection) { itemNode.parentNode.scrollLeft = itemNode.getBoundingClientRect().left - - itemNode.parentNode.getBoundingClientRect().left + itemNode.parentNode.getBoundingClientRect().left; } } diff --git a/client/js/views/home_view.js b/client/js/views/home_view.js index 35d4742..c91363b 100644 --- a/client/js/views/home_view.js +++ b/client/js/views/home_view.js @@ -1,22 +1,20 @@ -'use strict'; - -const router = require('../router.js'); -const uri = require('../util/uri.js'); -const misc = require('../util/misc.js'); -const views = require('../util/views.js'); -const PostContentControl = require('../controls/post_content_control.js'); -const PostNotesOverlayControl - = require('../controls/post_notes_overlay_control.js'); -const TagAutoCompleteControl = - require('../controls/tag_auto_complete_control.js'); - -const template = views.getTemplate('home'); -const footerTemplate = views.getTemplate('home-footer'); -const featuredPostTemplate = views.getTemplate('home-featured-post'); +"use strict"; + +const router = require("../router.js"); +const uri = require("../util/uri.js"); +const misc = require("../util/misc.js"); +const views = require("../util/views.js"); +const PostContentControl = require("../controls/post_content_control.js"); +const PostNotesOverlayControl = require("../controls/post_notes_overlay_control.js"); +const TagAutoCompleteControl = require("../controls/tag_auto_complete_control.js"); + +const template = views.getTemplate("home"); +const footerTemplate = views.getTemplate("home-footer"); +const featuredPostTemplate = views.getTemplate("home-featured-post"); class HomeView { constructor(ctx) { - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); this._ctx = ctx; const sourceNode = template(ctx); @@ -27,11 +25,16 @@ class HomeView { this._autoCompleteControl = new TagAutoCompleteControl( this._searchInputNode, { - confirm: tag => this._autoCompleteControl.replaceSelectedText( - misc.escapeSearchTerm(tag.names[0]), true), - }); - this._formNode.addEventListener( - 'submit', e => this._evtFormSubmit(e)); + confirm: (tag) => + this._autoCompleteControl.replaceSelectedText( + misc.escapeSearchTerm(tag.names[0]), + true + ), + } + ); + this._formNode.addEventListener("submit", (e) => + this._evtFormSubmit(e) + ); } } @@ -46,59 +49,67 @@ class HomeView { setStats(stats) { views.replaceContent( this._footerContainerNode, - footerTemplate(Object.assign({}, stats, this._ctx))); + footerTemplate(Object.assign({}, stats, this._ctx)) + ); } setFeaturedPost(postInfo) { views.replaceContent( - this._postInfoContainerNode, featuredPostTemplate(postInfo)); + this._postInfoContainerNode, + featuredPostTemplate(postInfo) + ); if (this._postContainerNode && postInfo.featuredPost) { this._postContentControl = new PostContentControl( this._postContainerNode, postInfo.featuredPost, () => { - return [ - window.innerWidth * 0.8, - window.innerHeight * 0.7, - ]; + return [window.innerWidth * 0.8, window.innerHeight * 0.7]; }, - 'fit-both'); + "fit-both" + ); this._postNotesOverlay = new PostNotesOverlayControl( - this._postContainerNode.querySelector('.post-overlay'), - postInfo.featuredPost); - - if (postInfo.featuredPost.type === 'video' - || postInfo.featuredPost.type === 'flash') { + this._postContainerNode.querySelector(".post-overlay"), + postInfo.featuredPost + ); + + if ( + postInfo.featuredPost.type === "video" || + postInfo.featuredPost.type === "flash" + ) { this._postContentControl.disableOverlay(); } } } get _footerContainerNode() { - return this._hostNode.querySelector('.footer-container'); + return this._hostNode.querySelector(".footer-container"); } get _postInfoContainerNode() { - return this._hostNode.querySelector('.post-info-container'); + return this._hostNode.querySelector(".post-info-container"); } get _postContainerNode() { - return this._hostNode.querySelector('.post-container'); + return this._hostNode.querySelector(".post-container"); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _searchInputNode() { - return this._formNode.querySelector('input[name=search-text]'); + return this._formNode.querySelector("input[name=search-text]"); } _evtFormSubmit(e) { e.preventDefault(); this._searchInputNode.blur(); - router.show(uri.formatClientLink('posts', {query: this._searchInputNode.value})); + router.show( + uri.formatClientLink("posts", { + query: this._searchInputNode.value, + }) + ); } } diff --git a/client/js/views/login_view.js b/client/js/views/login_view.js index 2c05332..64d49f9 100644 --- a/client/js/views/login_view.js +++ b/client/js/views/login_view.js @@ -1,52 +1,63 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const api = require('../api.js'); -const views = require('../util/views.js'); +const events = require("../events.js"); +const api = require("../api.js"); +const views = require("../util/views.js"); -const template = views.getTemplate('login'); +const template = views.getTemplate("login"); class LoginView extends events.EventTarget { constructor() { super(); - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); - views.replaceContent(this._hostNode, template({ - userNamePattern: api.getUserNameRegex(), - passwordPattern: api.getPasswordRegex(), - canSendMails: api.canSendMails(), - })); + views.replaceContent( + this._hostNode, + template({ + userNamePattern: api.getUserNameRegex(), + passwordPattern: api.getPasswordRegex(), + canSendMails: api.canSendMails(), + }) + ); views.syncScrollPosition(); views.decorateValidator(this._formNode); - this._userNameInputNode.setAttribute('pattern', api.getUserNameRegex()); - this._passwordInputNode.setAttribute('pattern', api.getPasswordRegex()); - this._formNode.addEventListener('submit', e => { + this._userNameInputNode.setAttribute( + "pattern", + api.getUserNameRegex() + ); + this._passwordInputNode.setAttribute( + "pattern", + api.getPasswordRegex() + ); + this._formNode.addEventListener("submit", (e) => { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - name: this._userNameInputNode.value, - password: this._passwordInputNode.value, - remember: this._rememberInputNode.checked, - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + name: this._userNameInputNode.value, + password: this._passwordInputNode.value, + remember: this._rememberInputNode.checked, + }, + }) + ); }); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _userNameInputNode() { - return this._formNode.querySelector('[name=name]'); + return this._formNode.querySelector("[name=name]"); } get _passwordInputNode() { - return this._formNode.querySelector('[name=password]'); + return this._formNode.querySelector("[name=password]"); } get _rememberInputNode() { - return this._formNode.querySelector('[name=remember-user]'); + return this._formNode.querySelector("[name=remember-user]"); } disableForm() { diff --git a/client/js/views/manual_page_view.js b/client/js/views/manual_page_view.js index 6798e9b..390994d 100644 --- a/client/js/views/manual_page_view.js +++ b/client/js/views/manual_page_view.js @@ -1,11 +1,11 @@ -'use strict'; +"use strict"; -const router = require('../router.js'); -const keyboard = require('../util/keyboard.js'); -const views = require('../util/views.js'); +const router = require("../router.js"); +const keyboard = require("../util/keyboard.js"); +const views = require("../util/views.js"); -const holderTemplate = views.getTemplate('manual-pager'); -const navTemplate = views.getTemplate('manual-pager-nav'); +const holderTemplate = views.getTemplate("manual-pager"); +const navTemplate = views.getTemplate("manual-pager-nav"); function _removeConsecutiveDuplicates(a) { return a.filter((item, pos, ary) => { @@ -22,9 +22,7 @@ function _getVisiblePageNumbers(currentPage, totalPages) { for (let i = totalPages - threshold; i <= totalPages; i++) { pagesVisible.push(i); } - for (let i = currentPage - threshold; - i <= currentPage + threshold; - i++) { + for (let i = currentPage - threshold; i <= currentPage + threshold; i++) { pagesVisible.push(i); } pagesVisible = pagesVisible.filter((item, pos, ary) => { @@ -38,18 +36,22 @@ function _getVisiblePageNumbers(currentPage, totalPages) { } function _getPages( - currentPage, pageNumbers, limit, defaultLimit, removedItems) { + currentPage, + pageNumbers, + limit, + defaultLimit, + removedItems +) { const pages = new Map(); let prevPage = 0; for (let page of pageNumbers) { if (page !== prevPage + 1) { - pages.set(page - 1, {ellipsis: true}); + pages.set(page - 1, { ellipsis: true }); } pages.set(page, { number: page, offset: - ((page - 1) * limit) - - (page > currentPage ? removedItems : 0), + (page - 1) * limit - (page > currentPage ? removedItems : 0), limit: limit === defaultLimit ? null : limit, active: currentPage === page, }); @@ -60,7 +62,7 @@ function _getPages( class ManualPageView { constructor(ctx) { - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); views.replaceContent(this._hostNode, holderTemplate()); } @@ -70,52 +72,65 @@ class ManualPageView { this.clearMessages(); views.emptyContent(this._pageNavNode); - ctx.requestPage(offset, limit).then(response => { - ctx.pageRenderer({ - parameters: ctx.parameters, - response: response, - hostNode: this._pageContentHolderNode, - }); - - keyboard.bind(['a', 'left'], () => { - this._navigateToPrevNextPage('prev'); - }); - keyboard.bind(['d', 'right'], () => { - this._navigateToPrevNextPage('next'); - }); - - let removedItems = 0; - if (response.total) { - this._refreshNav( - offset, limit, response.total, removedItems, ctx); + ctx.requestPage(offset, limit).then( + (response) => { + ctx.pageRenderer({ + parameters: ctx.parameters, + response: response, + hostNode: this._pageContentHolderNode, + }); + + keyboard.bind(["a", "left"], () => { + this._navigateToPrevNextPage("prev"); + }); + keyboard.bind(["d", "right"], () => { + this._navigateToPrevNextPage("next"); + }); + + let removedItems = 0; + if (response.total) { + this._refreshNav( + offset, + limit, + response.total, + removedItems, + ctx + ); + } + + if (!response.results.length) { + this.showInfo("No data to show"); + } + + response.results.addEventListener("remove", (e) => { + removedItems++; + this._refreshNav( + offset, + limit, + response.total, + removedItems, + ctx + ); + }); + + views.syncScrollPosition(); + }, + (response) => { + this.showError(response.message); } - - if (!response.results.length) { - this.showInfo('No data to show'); - } - - response.results.addEventListener('remove', e => { - removedItems++; - this._refreshNav( - offset, limit, response.total, removedItems, ctx); - }); - - views.syncScrollPosition(); - }, response => { - this.showError(response.message); - }); + ); } get pageHeaderHolderNode() { - return this._hostNode.querySelector('.page-header-holder'); + return this._hostNode.querySelector(".page-header-holder"); } get _pageContentHolderNode() { - return this._hostNode.querySelector('.page-content-holder'); + return this._hostNode.querySelector(".page-content-holder"); } get _pageNavNode() { - return this._hostNode.querySelector('.page-nav'); + return this._hostNode.querySelector(".page-nav"); } clearMessages() { @@ -135,11 +150,11 @@ class ManualPageView { } _navigateToPrevNextPage(className) { - const linkNode = this._hostNode.querySelector('a.' + className); - if (linkNode.classList.contains('disabled')) { + const linkNode = this._hostNode.querySelector("a." + className); + if (linkNode.classList.contains("disabled")) { return; } - router.show(linkNode.getAttribute('href')); + router.show(linkNode.getAttribute("href")); } _refreshNav(offset, limit, total, removedItems, ctx) { @@ -147,7 +162,12 @@ class ManualPageView { const totalPages = Math.ceil((total - removedItems) / limit); const pageNumbers = _getVisiblePageNumbers(currentPage, totalPages); const pages = _getPages( - currentPage, pageNumbers, limit, ctx.defaultLimit, removedItems); + currentPage, + pageNumbers, + limit, + ctx.defaultLimit, + removedItems + ); views.replaceContent( this._pageNavNode, @@ -158,7 +178,8 @@ class ManualPageView { currentPage: currentPage, totalPages: totalPages, pages: pages, - })); + }) + ); } } diff --git a/client/js/views/not_found_view.js b/client/js/views/not_found_view.js index 487613b..c930b09 100644 --- a/client/js/views/not_found_view.js +++ b/client/js/views/not_found_view.js @@ -1,14 +1,14 @@ -'use strict'; +"use strict"; -const views = require('../util/views.js'); +const views = require("../util/views.js"); -const template = views.getTemplate('not-found'); +const template = views.getTemplate("not-found"); class NotFoundView { constructor(path) { - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); - const sourceNode = template({path: path}); + const sourceNode = template({ path: path }); views.replaceContent(this._hostNode, sourceNode); views.syncScrollPosition(); } diff --git a/client/js/views/password_reset_view.js b/client/js/views/password_reset_view.js index 685fe5a..82a7d50 100644 --- a/client/js/views/password_reset_view.js +++ b/client/js/views/password_reset_view.js @@ -1,30 +1,35 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const api = require('../api.js'); -const views = require('../util/views.js'); +const events = require("../events.js"); +const api = require("../api.js"); +const views = require("../util/views.js"); -const template = views.getTemplate('password-reset'); +const template = views.getTemplate("password-reset"); class PasswordResetView extends events.EventTarget { constructor() { super(); - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); - views.replaceContent(this._hostNode, template({ - canSendMails: api.canSendMails(), - contactEmail: api.getContactEmail(), - })); + views.replaceContent( + this._hostNode, + template({ + canSendMails: api.canSendMails(), + contactEmail: api.getContactEmail(), + }) + ); views.syncScrollPosition(); views.decorateValidator(this._formNode); - this._formNode.addEventListener('submit', e => { + this._formNode.addEventListener("submit", (e) => { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - userNameOrEmail: this._userNameOrEmailFieldNode.value, - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + userNameOrEmail: this._userNameOrEmailFieldNode.value, + }, + }) + ); }); } @@ -49,11 +54,11 @@ class PasswordResetView extends events.EventTarget { } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _userNameOrEmailFieldNode() { - return this._formNode.querySelector('[name=user-name]'); + return this._formNode.querySelector("[name=user-name]"); } } diff --git a/client/js/views/pool_categories_view.js b/client/js/views/pool_categories_view.js index 1928358..ac381d5 100644 --- a/client/js/views/pool_categories_view.js +++ b/client/js/views/pool_categories_view.js @@ -1,17 +1,17 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); -const PoolCategory = require('../models/pool_category.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); +const PoolCategory = require("../models/pool_category.js"); -const template = views.getTemplate('pool-categories'); -const rowTemplate = views.getTemplate('pool-category-row'); +const template = views.getTemplate("pool-categories"); +const rowTemplate = views.getTemplate("pool-category-row"); class PoolCategoriesView extends events.EventTarget { constructor(ctx) { super(); this._ctx = ctx; - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); views.replaceContent(this._hostNode, template(ctx)); views.syncScrollPosition(); @@ -31,18 +31,22 @@ class PoolCategoriesView extends events.EventTarget { } if (this._addLinkNode) { - this._addLinkNode.addEventListener( - 'click', e => this._evtAddButtonClick(e)); + this._addLinkNode.addEventListener("click", (e) => + this._evtAddButtonClick(e) + ); } - ctx.poolCategories.addEventListener( - 'add', e => this._evtPoolCategoryAdded(e)); + ctx.poolCategories.addEventListener("add", (e) => + this._evtPoolCategoryAdded(e) + ); - ctx.poolCategories.addEventListener( - 'remove', e => this._evtPoolCategoryDeleted(e)); + ctx.poolCategories.addEventListener("remove", (e) => + this._evtPoolCategoryDeleted(e) + ); - this._formNode.addEventListener( - 'submit', e => this._evtSaveButtonClick(e, ctx)); + this._formNode.addEventListener("submit", (e) => + this._evtSaveButtonClick(e, ctx) + ); } enableForm() { @@ -66,44 +70,48 @@ class PoolCategoriesView extends events.EventTarget { } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _tableBodyNode() { - return this._hostNode.querySelector('tbody'); + return this._hostNode.querySelector("tbody"); } get _addLinkNode() { - return this._hostNode.querySelector('a.add'); + return this._hostNode.querySelector("a.add"); } _addPoolCategoryRowNode(poolCategory) { const rowNode = rowTemplate( - Object.assign( - {}, this._ctx, {poolCategory: poolCategory})); + Object.assign({}, this._ctx, { poolCategory: poolCategory }) + ); - const nameInput = rowNode.querySelector('.name input'); + const nameInput = rowNode.querySelector(".name input"); if (nameInput) { - nameInput.addEventListener( - 'change', e => this._evtNameChange(e, rowNode)); + nameInput.addEventListener("change", (e) => + this._evtNameChange(e, rowNode) + ); } - const colorInput = rowNode.querySelector('.color input'); + const colorInput = rowNode.querySelector(".color input"); if (colorInput) { - colorInput.addEventListener( - 'change', e => this._evtColorChange(e, rowNode)); + colorInput.addEventListener("change", (e) => + this._evtColorChange(e, rowNode) + ); } - const removeLinkNode = rowNode.querySelector('.remove a'); + const removeLinkNode = rowNode.querySelector(".remove a"); if (removeLinkNode) { - removeLinkNode.addEventListener( - 'click', e => this._evtDeleteButtonClick(e, rowNode)); + removeLinkNode.addEventListener("click", (e) => + this._evtDeleteButtonClick(e, rowNode) + ); } - const defaultLinkNode = rowNode.querySelector('.set-default a'); + const defaultLinkNode = rowNode.querySelector(".set-default a"); if (defaultLinkNode) { - defaultLinkNode.addEventListener( - 'click', e => this._evtSetDefaultButtonClick(e, rowNode)); + defaultLinkNode.addEventListener("click", (e) => + this._evtSetDefaultButtonClick(e, rowNode) + ); } this._tableBodyNode.appendChild(rowNode); @@ -141,7 +149,7 @@ class PoolCategoriesView extends events.EventTarget { _evtDeleteButtonClick(e, rowNode, link) { e.preventDefault(); - if (e.target.classList.contains('inactive')) { + if (e.target.classList.contains("inactive")) { return; } this._ctx.poolCategories.remove(rowNode._poolCategory); @@ -150,16 +158,16 @@ class PoolCategoriesView extends events.EventTarget { _evtSetDefaultButtonClick(e, rowNode) { e.preventDefault(); this._ctx.poolCategories.defaultCategory = rowNode._poolCategory; - const oldRowNode = rowNode.parentNode.querySelector('tr.default'); + const oldRowNode = rowNode.parentNode.querySelector("tr.default"); if (oldRowNode) { - oldRowNode.classList.remove('default'); + oldRowNode.classList.remove("default"); } - rowNode.classList.add('default'); + rowNode.classList.add("default"); } _evtSaveButtonClick(e, ctx) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit')); + this.dispatchEvent(new CustomEvent("submit")); } } diff --git a/client/js/views/pool_create_view.js b/client/js/views/pool_create_view.js index f22cf71..fc75f45 100644 --- a/client/js/views/pool_create_view.js +++ b/client/js/views/pool_create_view.js @@ -1,41 +1,43 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const api = require('../api.js'); -const misc = require('../util/misc.js'); -const views = require('../util/views.js'); -const Pool = require('../models/pool.js') +const events = require("../events.js"); +const api = require("../api.js"); +const misc = require("../util/misc.js"); +const views = require("../util/views.js"); +const Pool = require("../models/pool.js"); -const template = views.getTemplate('pool-create'); +const template = views.getTemplate("pool-create"); class PoolCreateView extends events.EventTarget { constructor(ctx) { super(); - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); views.replaceContent(this._hostNode, template(ctx)); views.decorateValidator(this._formNode); if (this._namesFieldNode) { - this._namesFieldNode.addEventListener( - 'input', e => this._evtNameInput(e)); + this._namesFieldNode.addEventListener("input", (e) => + this._evtNameInput(e) + ); } if (this._postsFieldNode) { - this._postsFieldNode.addEventListener( - 'input', e => this._evtPostsInput(e)); + this._postsFieldNode.addEventListener("input", (e) => + this._evtPostsInput(e) + ); } for (let node of this._formNode.querySelectorAll( - 'input, select, textarea, posts')) { - node.addEventListener( - 'change', e => { - this.dispatchEvent(new CustomEvent('change')); - }); + "input, select, textarea, posts" + )) { + node.addEventListener("change", (e) => { + this.dispatchEvent(new CustomEvent("change")); + }); } - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } clearMessages() { @@ -64,19 +66,21 @@ class PoolCreateView extends events.EventTarget { if (!list.length) { this._namesFieldNode.setCustomValidity( - 'Pools must have at least one name.'); + "Pools must have at least one name." + ); return; } for (let item of list) { if (!regex.test(item)) { this._namesFieldNode.setCustomValidity( - `Pool name "${item}" contains invalid symbols.`); + `Pool name "${item}" contains invalid symbols.` + ); return; } } - this._namesFieldNode.setCustomValidity(''); + this._namesFieldNode.setCustomValidity(""); } _evtPostsInput(e) { @@ -86,46 +90,50 @@ class PoolCreateView extends events.EventTarget { for (let item of list) { if (!regex.test(item)) { this._postsFieldNode.setCustomValidity( - `Pool ID "${item}" is not an integer.`); + `Pool ID "${item}" is not an integer.` + ); return; } } - this._postsFieldNode.setCustomValidity(''); + this._postsFieldNode.setCustomValidity(""); } _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - names: misc.splitByWhitespace(this._namesFieldNode.value), - category: this._categoryFieldNode.value, - description: this._descriptionFieldNode.value, - posts: misc.splitByWhitespace(this._postsFieldNode.value) - .map(i => parseInt(i)) - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + names: misc.splitByWhitespace(this._namesFieldNode.value), + category: this._categoryFieldNode.value, + description: this._descriptionFieldNode.value, + posts: misc + .splitByWhitespace(this._postsFieldNode.value) + .map((i) => parseInt(i)), + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _namesFieldNode() { - return this._formNode.querySelector('.names input'); + return this._formNode.querySelector(".names input"); } get _categoryFieldNode() { - return this._formNode.querySelector('.category select'); + return this._formNode.querySelector(".category select"); } get _descriptionFieldNode() { - return this._formNode.querySelector('.description textarea'); + return this._formNode.querySelector(".description textarea"); } get _postsFieldNode() { - return this._formNode.querySelector('.posts input'); + return this._formNode.querySelector(".posts input"); } } diff --git a/client/js/views/pool_delete_view.js b/client/js/views/pool_delete_view.js index d3707db..fa92a49 100644 --- a/client/js/views/pool_delete_view.js +++ b/client/js/views/pool_delete_view.js @@ -1,9 +1,9 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); -const template = views.getTemplate('pool-delete'); +const template = views.getTemplate("pool-delete"); class PoolDeleteView extends events.EventTarget { constructor(ctx) { @@ -13,7 +13,7 @@ class PoolDeleteView extends events.EventTarget { this._pool = ctx.pool; views.replaceContent(this._hostNode, template(ctx)); views.decorateValidator(this._formNode); - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } clearMessages() { @@ -38,15 +38,17 @@ class PoolDeleteView extends events.EventTarget { _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - pool: this._pool, - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + pool: this._pool, + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } } diff --git a/client/js/views/pool_edit_view.js b/client/js/views/pool_edit_view.js index 9c118b3..b30ab9b 100644 --- a/client/js/views/pool_edit_view.js +++ b/client/js/views/pool_edit_view.js @@ -1,12 +1,12 @@ -'use strict'; +"use strict"; -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 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'); +const template = views.getTemplate("pool-edit"); class PoolEditView extends events.EventTarget { constructor(ctx) { @@ -19,24 +19,26 @@ class PoolEditView extends events.EventTarget { views.decorateValidator(this._formNode); if (this._namesFieldNode) { - this._namesFieldNode.addEventListener( - 'input', e => this._evtNameInput(e)); + this._namesFieldNode.addEventListener("input", (e) => + this._evtNameInput(e) + ); } if (this._postsFieldNode) { - this._postsFieldNode.addEventListener( - 'input', e => this._evtPostsInput(e)); + this._postsFieldNode.addEventListener("input", (e) => + this._evtPostsInput(e) + ); } for (let node of this._formNode.querySelectorAll( - 'input, select, textarea, posts')) { - node.addEventListener( - 'change', e => { - this.dispatchEvent(new CustomEvent('change')); - }); + "input, select, textarea, posts" + )) { + node.addEventListener("change", (e) => { + this.dispatchEvent(new CustomEvent("change")); + }); } - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } clearMessages() { @@ -65,19 +67,21 @@ class PoolEditView extends events.EventTarget { if (!list.length) { this._namesFieldNode.setCustomValidity( - 'Pools must have at least one name.'); + "Pools must have at least one name." + ); return; } for (let item of list) { if (!regex.test(item)) { this._namesFieldNode.setCustomValidity( - `Pool name "${item}" contains invalid symbols.`); + `Pool name "${item}" contains invalid symbols.` + ); return; } } - this._namesFieldNode.setCustomValidity(''); + this._namesFieldNode.setCustomValidity(""); } _evtPostsInput(e) { @@ -87,57 +91,60 @@ class PoolEditView extends events.EventTarget { for (let item of list) { if (!regex.test(item)) { this._postsFieldNode.setCustomValidity( - `Pool ID "${item}" is not an integer.`); + `Pool ID "${item}" is not an integer.` + ); return; } } - this._postsFieldNode.setCustomValidity(''); + this._postsFieldNode.setCustomValidity(""); } _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - pool: this._pool, - - names: this._namesFieldNode ? - misc.splitByWhitespace(this._namesFieldNode.value) : - undefined, - - category: this._categoryFieldNode ? - this._categoryFieldNode.value : - undefined, - - description: this._descriptionFieldNode ? - this._descriptionFieldNode.value : - undefined, - - posts: this._postsFieldNode ? - misc.splitByWhitespace(this._postsFieldNode.value) : - undefined, - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + pool: this._pool, + + names: this._namesFieldNode + ? misc.splitByWhitespace(this._namesFieldNode.value) + : undefined, + + category: this._categoryFieldNode + ? this._categoryFieldNode.value + : undefined, + + description: this._descriptionFieldNode + ? this._descriptionFieldNode.value + : undefined, + + posts: this._postsFieldNode + ? misc.splitByWhitespace(this._postsFieldNode.value) + : undefined, + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _namesFieldNode() { - return this._formNode.querySelector('.names input'); + return this._formNode.querySelector(".names input"); } get _categoryFieldNode() { - return this._formNode.querySelector('.category select'); + return this._formNode.querySelector(".category select"); } get _descriptionFieldNode() { - return this._formNode.querySelector('.description textarea'); + return this._formNode.querySelector(".description textarea"); } get _postsFieldNode() { - return this._formNode.querySelector('.posts input'); + return this._formNode.querySelector(".posts input"); } } diff --git a/client/js/views/pool_merge_view.js b/client/js/views/pool_merge_view.js index 09c4e8d..f0ca15a 100644 --- a/client/js/views/pool_merge_view.js +++ b/client/js/views/pool_merge_view.js @@ -1,12 +1,11 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const api = require('../api.js'); -const views = require('../util/views.js'); -const PoolAutoCompleteControl = - require('../controls/pool_auto_complete_control.js'); +const events = require("../events.js"); +const api = require("../api.js"); +const views = require("../util/views.js"); +const PoolAutoCompleteControl = require("../controls/pool_auto_complete_control.js"); -const template = views.getTemplate('pool-merge'); +const template = views.getTemplate("pool-merge"); class PoolMergeView extends events.EventTarget { constructor(ctx) { @@ -23,15 +22,18 @@ class PoolMergeView extends events.EventTarget { this._autoCompleteControl = new PoolAutoCompleteControl( this._targetPoolFieldNode, { - confirm: pool => { + confirm: (pool) => { this._targetPoolId = pool.id; this._autoCompleteControl.replaceSelectedText( - pool.names[0], false); - } - }); + pool.names[0], + false + ); + }, + } + ); } - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } clearMessages() { @@ -56,24 +58,26 @@ class PoolMergeView extends events.EventTarget { _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - pool: this._pool, - targetPoolId: this._targetPoolId - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + pool: this._pool, + targetPoolId: this._targetPoolId, + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _targetPoolFieldNode() { - return this._formNode.querySelector('input[name=target-pool]'); + return this._formNode.querySelector("input[name=target-pool]"); } get _addAliasCheckboxNode() { - return this._formNode.querySelector('input[name=alias]'); + return this._formNode.querySelector("input[name=alias]"); } } diff --git a/client/js/views/pool_summary_view.js b/client/js/views/pool_summary_view.js index a5808cc..37ce1c2 100644 --- a/client/js/views/pool_summary_view.js +++ b/client/js/views/pool_summary_view.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const views = require('../util/views.js'); +const views = require("../util/views.js"); -const template = views.getTemplate('pool-summary'); +const template = views.getTemplate("pool-summary"); class PoolSummaryView { constructor(ctx) { diff --git a/client/js/views/pool_view.js b/client/js/views/pool_view.js index f296e90..12b7704 100644 --- a/client/js/views/pool_view.js +++ b/client/js/views/pool_view.js @@ -1,26 +1,26 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); -const misc = require('../util/misc.js'); -const PoolSummaryView = require('./pool_summary_view.js'); -const PoolEditView = require('./pool_edit_view.js'); -const PoolMergeView = require('./pool_merge_view.js'); -const PoolDeleteView = require('./pool_delete_view.js'); -const EmptyView = require('../views/empty_view.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); +const misc = require("../util/misc.js"); +const PoolSummaryView = require("./pool_summary_view.js"); +const PoolEditView = require("./pool_edit_view.js"); +const PoolMergeView = require("./pool_merge_view.js"); +const PoolDeleteView = require("./pool_delete_view.js"); +const EmptyView = require("../views/empty_view.js"); -const template = views.getTemplate('pool'); +const template = views.getTemplate("pool"); class PoolView extends events.EventTarget { constructor(ctx) { super(); this._ctx = ctx; - ctx.pool.addEventListener('change', e => this._evtChange(e)); - ctx.section = ctx.section || 'summary'; + ctx.pool.addEventListener("change", (e) => this._evtChange(e)); + ctx.section = ctx.section || "summary"; ctx.getPrettyPoolName = misc.getPrettyPoolName; - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); this._install(); } @@ -28,52 +28,54 @@ class PoolView extends events.EventTarget { const ctx = this._ctx; views.replaceContent(this._hostNode, template(ctx)); - for (let item of this._hostNode.querySelectorAll('[data-name]')) { + for (let item of this._hostNode.querySelectorAll("[data-name]")) { item.classList.toggle( - 'active', item.getAttribute('data-name') === ctx.section); - if (item.getAttribute('data-name') === ctx.section) { + "active", + item.getAttribute("data-name") === ctx.section + ); + if (item.getAttribute("data-name") === ctx.section) { item.parentNode.scrollLeft = item.getBoundingClientRect().left - - item.parentNode.getBoundingClientRect().left + item.parentNode.getBoundingClientRect().left; } } - ctx.hostNode = this._hostNode.querySelector('.pool-content-holder'); - if (ctx.section === 'edit') { + ctx.hostNode = this._hostNode.querySelector(".pool-content-holder"); + if (ctx.section === "edit") { if (!this._ctx.canEditAnything) { this._view = new EmptyView(); this._view.showError( - 'You don\'t have privileges to edit pools.'); + "You don't have privileges to edit pools." + ); } else { this._view = new PoolEditView(ctx); - events.proxyEvent(this._view, this, 'submit'); + events.proxyEvent(this._view, this, "submit"); } - - } else if (ctx.section === 'merge') { + } else if (ctx.section === "merge") { if (!this._ctx.canMerge) { this._view = new EmptyView(); this._view.showError( - 'You don\'t have privileges to merge pools.'); + "You don't have privileges to merge pools." + ); } else { this._view = new PoolMergeView(ctx); - events.proxyEvent(this._view, this, 'submit', 'merge'); + events.proxyEvent(this._view, this, "submit", "merge"); } - - } else if (ctx.section === 'delete') { + } else if (ctx.section === "delete") { if (!this._ctx.canDelete) { this._view = new EmptyView(); this._view.showError( - 'You don\'t have privileges to delete pools.'); + "You don't have privileges to delete pools." + ); } else { this._view = new PoolDeleteView(ctx); - events.proxyEvent(this._view, this, 'submit', 'delete'); + events.proxyEvent(this._view, this, "submit", "delete"); } - } else { this._view = new PoolSummaryView(ctx); } - events.proxyEvent(this._view, this, 'change'); + events.proxyEvent(this._view, this, "change"); views.syncScrollPosition(); } diff --git a/client/js/views/pools_header_view.js b/client/js/views/pools_header_view.js index 65bbe1b..cfc6c8b 100644 --- a/client/js/views/pools_header_view.js +++ b/client/js/views/pools_header_view.js @@ -1,13 +1,12 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const misc = require('../util/misc.js'); -const search = require('../util/search.js'); -const views = require('../util/views.js'); -const PoolAutoCompleteControl = - require('../controls/pool_auto_complete_control.js'); +const events = require("../events.js"); +const misc = require("../util/misc.js"); +const search = require("../util/search.js"); +const views = require("../util/views.js"); +const PoolAutoCompleteControl = require("../controls/pool_auto_complete_control.js"); -const template = views.getTemplate('pools-header'); +const template = views.getTemplate("pools-header"); class PoolsHeaderView extends events.EventTarget { constructor(ctx) { @@ -20,31 +19,41 @@ class PoolsHeaderView extends events.EventTarget { this._autoCompleteControl = new PoolAutoCompleteControl( this._queryInputNode, { - confirm: pool => this._autoCompleteControl.replaceSelectedText( - misc.escapeSearchTerm(pool.names[0]), true), - }); + confirm: (pool) => + this._autoCompleteControl.replaceSelectedText( + misc.escapeSearchTerm(pool.names[0]), + true + ), + } + ); } search.searchInputNodeFocusHelper(this._queryInputNode); - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _queryInputNode() { - return this._hostNode.querySelector('[name=search-text]'); + return this._hostNode.querySelector("[name=search-text]"); } _evtSubmit(e) { e.preventDefault(); this._queryInputNode.blur(); - this.dispatchEvent(new CustomEvent('navigate', {detail: {parameters: { - query: this._queryInputNode.value, - page: 1, - }}})); + this.dispatchEvent( + new CustomEvent("navigate", { + detail: { + parameters: { + query: this._queryInputNode.value, + page: 1, + }, + }, + }) + ); } } diff --git a/client/js/views/pools_page_view.js b/client/js/views/pools_page_view.js index 234d446..6230ef6 100644 --- a/client/js/views/pools_page_view.js +++ b/client/js/views/pools_page_view.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const views = require('../util/views.js'); +const views = require("../util/views.js"); -const template = views.getTemplate('pools-page'); +const template = views.getTemplate("pools-page"); class PoolsPageView { constructor(ctx) { diff --git a/client/js/views/post_detail_view.js b/client/js/views/post_detail_view.js index 14786d3..587c41f 100644 --- a/client/js/views/post_detail_view.js +++ b/client/js/views/post_detail_view.js @@ -1,21 +1,21 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); -const PostMergeView = require('./post_merge_view.js'); -const EmptyView = require('../views/empty_view.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); +const PostMergeView = require("./post_merge_view.js"); +const EmptyView = require("../views/empty_view.js"); -const template = views.getTemplate('post-detail'); +const template = views.getTemplate("post-detail"); class PostDetailView extends events.EventTarget { constructor(ctx) { super(); this._ctx = ctx; - ctx.post.addEventListener('change', e => this._evtChange(e)); - ctx.section = ctx.section || 'summary'; + ctx.post.addEventListener("change", (e) => this._evtChange(e)); + ctx.section = ctx.section || "summary"; - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); this._install(); } @@ -23,28 +23,30 @@ class PostDetailView extends events.EventTarget { const ctx = this._ctx; views.replaceContent(this._hostNode, template(ctx)); - for (let item of this._hostNode.querySelectorAll('[data-name]')) { + for (let item of this._hostNode.querySelectorAll("[data-name]")) { item.classList.toggle( - 'active', item.getAttribute('data-name') === ctx.section); - if (item.getAttribute('data-name') === ctx.section) { + "active", + item.getAttribute("data-name") === ctx.section + ); + if (item.getAttribute("data-name") === ctx.section) { item.parentNode.scrollLeft = item.getBoundingClientRect().left - - item.parentNode.getBoundingClientRect().left + item.parentNode.getBoundingClientRect().left; } } - ctx.hostNode = this._hostNode.querySelector('.post-content-holder'); - if (ctx.section === 'merge') { + ctx.hostNode = this._hostNode.querySelector(".post-content-holder"); + if (ctx.section === "merge") { if (!this._ctx.canMerge) { this._view = new EmptyView(); this._view.showError( - 'You don\'t have privileges to merge posts.'); + "You don't have privileges to merge posts." + ); } else { this._view = new PostMergeView(ctx); - events.proxyEvent(this._view, this, 'select'); - events.proxyEvent(this._view, this, 'submit', 'merge'); + events.proxyEvent(this._view, this, "select"); + events.proxyEvent(this._view, this, "submit", "merge"); } - } else { // this._view = new PostSummaryView(ctx); } diff --git a/client/js/views/post_main_view.js b/client/js/views/post_main_view.js index 6a7472c..73bb324 100644 --- a/client/js/views/post_main_view.js +++ b/client/js/views/post_main_view.js @@ -1,35 +1,33 @@ -'use strict'; - -const iosCorrectedInnerHeight = require('ios-inner-height'); -const router = require('../router.js'); -const views = require('../util/views.js'); -const uri = require('../util/uri.js'); -const keyboard = require('../util/keyboard.js'); -const Touch = require('../util/touch.js'); -const PostContentControl = require('../controls/post_content_control.js'); -const PostNotesOverlayControl = - require('../controls/post_notes_overlay_control.js'); -const PostReadonlySidebarControl = - require('../controls/post_readonly_sidebar_control.js'); -const PostEditSidebarControl = - require('../controls/post_edit_sidebar_control.js'); -const CommentControl = require('../controls/comment_control.js'); -const CommentListControl = require('../controls/comment_list_control.js'); - -const template = views.getTemplate('post-main'); +"use strict"; + +const iosCorrectedInnerHeight = require("ios-inner-height"); +const router = require("../router.js"); +const views = require("../util/views.js"); +const uri = require("../util/uri.js"); +const keyboard = require("../util/keyboard.js"); +const Touch = require("../util/touch.js"); +const PostContentControl = require("../controls/post_content_control.js"); +const PostNotesOverlayControl = require("../controls/post_notes_overlay_control.js"); +const PostReadonlySidebarControl = require("../controls/post_readonly_sidebar_control.js"); +const PostEditSidebarControl = require("../controls/post_edit_sidebar_control.js"); +const CommentControl = require("../controls/comment_control.js"); +const CommentListControl = require("../controls/comment_list_control.js"); + +const template = views.getTemplate("post-main"); class PostMainView { constructor(ctx) { - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); const sourceNode = template(ctx); - const postContainerNode = sourceNode.querySelector('.post-container'); - const sidebarNode = sourceNode.querySelector('.sidebar'); + const postContainerNode = sourceNode.querySelector(".post-container"); + const sidebarNode = sourceNode.querySelector(".sidebar"); views.replaceContent(this._hostNode, sourceNode); views.syncScrollPosition(); - const topNavigationNode = - document.body.querySelector('#top-navigation'); + const topNavigationNode = document.body.querySelector( + "#top-navigation" + ); this._postContentControl = new PostContentControl( postContainerNode, @@ -43,15 +41,17 @@ class PostMainView { margin, iosCorrectedInnerHeight() - topNavigationNode.getBoundingClientRect().height - - (margin * 2), + margin * 2, ]; - }); + } + ); this._postNotesOverlayControl = new PostNotesOverlayControl( - postContainerNode.querySelector('.post-overlay'), - ctx.post); + postContainerNode.querySelector(".post-overlay"), + ctx.post + ); - if (ctx.post.type === 'video' || ctx.post.type === 'flash') { + if (ctx.post.type === "video" || ctx.post.type === "flash") { this._postContentControl.disableOverlay(); } @@ -71,16 +71,16 @@ class PostMainView { } }; - keyboard.bind('e', () => { + keyboard.bind("e", () => { if (ctx.editMode) { - router.show(uri.formatClientLink('post', ctx.post.id)); + router.show(uri.formatClientLink("post", ctx.post.id)); } else { - router.show(uri.formatClientLink('post', ctx.post.id, 'edit')); + router.show(uri.formatClientLink("post", ctx.post.id, "edit")); } }); - keyboard.bind(['a', 'left'], showPreviousImage); - keyboard.bind(['d', 'right'], showNextImage); - keyboard.bind('del', e => { + keyboard.bind(["a", "left"], showPreviousImage); + keyboard.bind(["d", "right"], showNextImage); + keyboard.bind("del", (e) => { if (ctx.editMode) { this.sidebarControl._evtDeleteClick(e); } @@ -90,53 +90,65 @@ class PostMainView { postContainerNode, () => { if (!ctx.editMode) { - showPreviousImage() + showPreviousImage(); } }, () => { if (!ctx.editMode) { - showNextImage() + showNextImage(); } } - ) + ); } _installSidebar(ctx) { const sidebarContainerNode = document.querySelector( - '#content-holder .sidebar-container'); + "#content-holder .sidebar-container" + ); if (ctx.editMode) { this.sidebarControl = new PostEditSidebarControl( sidebarContainerNode, ctx.post, this._postContentControl, - this._postNotesOverlayControl); + this._postNotesOverlayControl + ); } else { this.sidebarControl = new PostReadonlySidebarControl( - sidebarContainerNode, ctx.post, this._postContentControl); + sidebarContainerNode, + ctx.post, + this._postContentControl + ); } } _installCommentForm() { const commentFormContainer = document.querySelector( - '#content-holder .comment-form-container'); + "#content-holder .comment-form-container" + ); if (!commentFormContainer) { return; } this.commentControl = new CommentControl( - commentFormContainer, null, true); + commentFormContainer, + null, + true + ); } _installComments(comments) { const commentsContainerNode = document.querySelector( - '#content-holder .comments-container'); + "#content-holder .comments-container" + ); if (!commentsContainerNode) { return; } this.commentListControl = new CommentListControl( - commentsContainerNode, comments); + commentsContainerNode, + comments + ); } } diff --git a/client/js/views/post_merge_view.js b/client/js/views/post_merge_view.js index 952e3a3..20924d3 100644 --- a/client/js/views/post_merge_view.js +++ b/client/js/views/post_merge_view.js @@ -1,11 +1,11 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); const KEY_RETURN = 13; -const template = views.getTemplate('post-merge'); -const sideTemplate = views.getTemplate('post-merge-side'); +const template = views.getTemplate("post-merge"); +const sideTemplate = views.getTemplate("post-merge-side"); class PostMergeView extends events.EventTarget { constructor(ctx) { @@ -23,7 +23,7 @@ class PostMergeView extends events.EventTarget { this._refreshLeftSide(); this._refreshRightSide(); - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } clearMessages() { @@ -52,47 +52,61 @@ class PostMergeView extends events.EventTarget { } _refreshLeftSide() { - this._refreshSide(this._leftPost, this._leftSideNode, 'left', false); + this._refreshSide(this._leftPost, this._leftSideNode, "left", false); } _refreshRightSide() { - this._refreshSide(this._rightPost, this._rightSideNode, 'right', true); + this._refreshSide(this._rightPost, this._rightSideNode, "right", true); } _refreshSide(post, sideNode, sideName, isEditable) { views.replaceContent( sideNode, - sideTemplate(Object.assign({}, this._ctx, {post: post, - name: sideName, - editable: isEditable}))); - - let postIdNode = sideNode.querySelector('input[type=text]'); - let searchButtonNode = sideNode.querySelector('input[type=button]'); + sideTemplate( + Object.assign({}, this._ctx, { + post: post, + name: sideName, + editable: isEditable, + }) + ) + ); + + let postIdNode = sideNode.querySelector("input[type=text]"); + let searchButtonNode = sideNode.querySelector("input[type=button]"); if (isEditable) { - postIdNode.addEventListener( - 'keydown', e => this._evtPostSearchFieldKeyDown(e)); - searchButtonNode.addEventListener( - 'click', e => this._evtPostSearchButtonClick(e, postIdNode)); + postIdNode.addEventListener("keydown", (e) => + this._evtPostSearchFieldKeyDown(e) + ); + searchButtonNode.addEventListener("click", (e) => + this._evtPostSearchButtonClick(e, postIdNode) + ); } } _evtSubmit(e) { e.preventDefault(); const checkedTargetPost = this._formNode.querySelector( - '.target-post :checked').value; + ".target-post :checked" + ).value; const checkedTargetPostContent = this._formNode.querySelector( - '.target-post-content :checked').value; - this.dispatchEvent(new CustomEvent('submit', { - detail: { - post: checkedTargetPost === 'left' ? - this._rightPost : - this._leftPost, - targetPost: checkedTargetPost === 'left' ? - this._leftPost : - this._rightPost, - useOldContent: checkedTargetPostContent !== checkedTargetPost, - }, - })); + ".target-post-content :checked" + ).value; + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + post: + checkedTargetPost === "left" + ? this._rightPost + : this._leftPost, + targetPost: + checkedTargetPost === "left" + ? this._leftPost + : this._rightPost, + useOldContent: + checkedTargetPostContent !== checkedTargetPost, + }, + }) + ); } _evtPostSearchFieldKeyDown(e) { @@ -102,33 +116,37 @@ class PostMergeView extends events.EventTarget { } e.target.blur(); e.preventDefault(); - this.dispatchEvent(new CustomEvent('select', { - detail: { - postId: e.target.value, - }, - })); + this.dispatchEvent( + new CustomEvent("select", { + detail: { + postId: e.target.value, + }, + }) + ); } _evtPostSearchButtonClick(e, textNode) { e.target.blur(); e.preventDefault(); - this.dispatchEvent(new CustomEvent('select', { - detail: { - postId: textNode.value, - }, - })); + this.dispatchEvent( + new CustomEvent("select", { + detail: { + postId: textNode.value, + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _leftSideNode() { - return this._hostNode.querySelector('.left-post-container'); + return this._hostNode.querySelector(".left-post-container"); } get _rightSideNode() { - return this._hostNode.querySelector('.right-post-container'); + return this._hostNode.querySelector(".right-post-container"); } } diff --git a/client/js/views/post_upload_view.js b/client/js/views/post_upload_view.js index f1d465f..7daf4fb 100644 --- a/client/js/views/post_upload_view.js +++ b/client/js/views/post_upload_view.js @@ -1,22 +1,24 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); -const FileDropperControl = require('../controls/file_dropper_control.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); +const FileDropperControl = require("../controls/file_dropper_control.js"); -const template = views.getTemplate('post-upload'); -const rowTemplate = views.getTemplate('post-upload-row'); +const template = views.getTemplate("post-upload"); +const rowTemplate = views.getTemplate("post-upload-row"); function _mimeTypeToPostType(mimeType) { - return { - 'application/x-shockwave-flash': 'flash', - 'image/gif': 'image', - 'image/jpeg': 'image', - 'image/png': 'image', - 'image/webp': 'image', - 'video/mp4': 'video', - 'video/webm': 'video', - }[mimeType] || 'unknown'; + return ( + { + "application/x-shockwave-flash": "flash", + "image/gif": "image", + "image/jpeg": "image", + "image/png": "image", + "image/webp": "image", + "video/mp4": "video", + "video/webm": "video", + }[mimeType] || "unknown" + ); } class Uploadable extends events.EventTarget { @@ -24,18 +26,17 @@ class Uploadable extends events.EventTarget { super(); this.lookalikes = []; this.lookalikesConfirmed = false; - this.safety = 'safe'; + this.safety = "safe"; this.flags = []; this.tags = []; this.relations = []; this.anonymous = false; } - destroy() { - } + destroy() {} get mimeType() { - return 'application/octet-stream'; + return "application/octet-stream"; } get type() { @@ -43,11 +44,11 @@ class Uploadable extends events.EventTarget { } get key() { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } get name() { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } } @@ -62,10 +63,11 @@ class File extends Uploadable { } else { let reader = new FileReader(); reader.readAsDataURL(file); - reader.addEventListener('load', e => { + reader.addEventListener("load", (e) => { this._previewUrl = e.target.result; this.dispatchEvent( - new CustomEvent('finish', {detail: {uploadable: this}})); + new CustomEvent("finish", { detail: { uploadable: this } }) + ); }); } } @@ -97,25 +99,25 @@ class Url extends Uploadable { constructor(url) { super(); this.url = url; - this.dispatchEvent(new CustomEvent('finish')); + this.dispatchEvent(new CustomEvent("finish")); } get mimeType() { let mime = { - 'swf': 'application/x-shockwave-flash', - 'jpg': 'image/jpeg', - 'png': 'image/png', - 'gif': 'image/gif', - 'webp': 'image/webp', - 'mp4': 'video/mp4', - 'webm': 'video/webm', + swf: "application/x-shockwave-flash", + jpg: "image/jpeg", + png: "image/png", + gif: "image/gif", + webp: "image/webp", + mp4: "video/mp4", + webm: "video/webm", }; for (let extension of Object.keys(mime)) { - if (this.url.toLowerCase().indexOf('.' + extension) !== -1) { + if (this.url.toLowerCase().indexOf("." + extension) !== -1) { return mime[extension]; } } - return 'unknown'; + return "unknown"; } get previewUrl() { @@ -135,7 +137,7 @@ class PostUploadView extends events.EventTarget { constructor(ctx) { super(); this._ctx = ctx; - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); views.replaceContent(this._hostNode, template()); views.syncScrollPosition(); @@ -143,40 +145,46 @@ class PostUploadView extends events.EventTarget { this._cancelButtonNode.disabled = true; this._uploadables = []; - this._uploadables.find = u => { - return this._uploadables.findIndex(u2 => u.key === u2.key); + this._uploadables.find = (u) => { + return this._uploadables.findIndex((u2) => u.key === u2.key); }; this._contentFileDropper = new FileDropperControl( this._contentInputNode, { extraText: - 'Allowed extensions: .jpg, .png, .gif, .webm, .mp4, .swf', + "Allowed extensions: .jpg, .png, .gif, .webm, .mp4, .swf", allowUrls: true, allowMultiple: true, lock: false, - }); - this._contentFileDropper.addEventListener( - 'fileadd', e => this._evtFilesAdded(e)); - this._contentFileDropper.addEventListener( - 'urladd', e => this._evtUrlsAdded(e)); - - this._cancelButtonNode.addEventListener( - 'click', e => this._evtCancelButtonClick(e)); - this._formNode.addEventListener('submit', e => this._evtFormSubmit(e)); - this._formNode.classList.add('inactive'); + } + ); + this._contentFileDropper.addEventListener("fileadd", (e) => + this._evtFilesAdded(e) + ); + this._contentFileDropper.addEventListener("urladd", (e) => + this._evtUrlsAdded(e) + ); + + this._cancelButtonNode.addEventListener("click", (e) => + this._evtCancelButtonClick(e) + ); + this._formNode.addEventListener("submit", (e) => + this._evtFormSubmit(e) + ); + this._formNode.classList.add("inactive"); } enableForm() { views.enableForm(this._formNode); this._cancelButtonNode.disabled = true; - this._formNode.classList.remove('uploading'); + this._formNode.classList.remove("uploading"); } disableForm() { views.disableForm(this._formNode); this._cancelButtonNode.disabled = false; - this._formNode.classList.add('uploading'); + this._formNode.classList.add("uploading"); } clearMessages() { @@ -201,7 +209,7 @@ class PostUploadView extends events.EventTarget { } addUploadables(uploadables) { - this._formNode.classList.remove('inactive'); + this._formNode.classList.remove("inactive"); let duplicatesFound = 0; for (let uploadable of uploadables) { if (this._uploadables.find(uploadable) !== -1) { @@ -209,20 +217,22 @@ class PostUploadView extends events.EventTarget { continue; } this._uploadables.push(uploadable); - this._emit('change'); + this._emit("change"); this._renderRowNode(uploadable); - uploadable.addEventListener( - 'finish', e => this._updateThumbnailNode(e.detail.uploadable)); + uploadable.addEventListener("finish", (e) => + this._updateThumbnailNode(e.detail.uploadable) + ); } if (duplicatesFound) { let message = null; if (duplicatesFound < uploadables.length) { - message = 'Some of the files were already added ' + - 'and have been skipped.'; + message = + "Some of the files were already added " + + "and have been skipped."; } else if (duplicatesFound === 1) { - message = 'This file was already added.'; + message = "This file was already added."; } else { - message = 'These files were already added.'; + message = "These files were already added."; } alert(message); } @@ -235,10 +245,10 @@ class PostUploadView extends events.EventTarget { uploadable.destroy(); uploadable.rowNode.parentNode.removeChild(uploadable.rowNode); this._uploadables.splice(this._uploadables.find(uploadable), 1); - this._emit('change'); + this._emit("change"); if (!this._uploadables.length) { - this._formNode.classList.add('inactive'); - this._submitButtonNode.value = 'Upload all'; + this._formNode.classList.add("inactive"); + this._submitButtonNode.value = "Upload all"; } } @@ -248,16 +258,16 @@ class PostUploadView extends events.EventTarget { } _evtFilesAdded(e) { - this.addUploadables(e.detail.files.map(file => new File(file))); + this.addUploadables(e.detail.files.map((file) => new File(file))); } _evtUrlsAdded(e) { - this.addUploadables(e.detail.urls.map(url => new Url(url))); + this.addUploadables(e.detail.urls.map((url) => new Url(url))); } _evtCancelButtonClick(e) { e.preventDefault(); - this._emit('cancel'); + this._emit("cancel"); } _evtFormSubmit(e) { @@ -265,19 +275,21 @@ class PostUploadView extends events.EventTarget { for (let uploadable of this._uploadables) { this._updateUploadableFromDom(uploadable); } - this._submitButtonNode.value = 'Resume upload'; - this._emit('submit'); + this._submitButtonNode.value = "Resume upload"; + this._emit("submit"); } _updateUploadableFromDom(uploadable) { const rowNode = uploadable.rowNode; - const safetyNode = rowNode.querySelector('.safety input:checked'); + const safetyNode = rowNode.querySelector(".safety input:checked"); if (safetyNode) { uploadable.safety = safetyNode.value; } - const anonymousNode = rowNode.querySelector('.anonymous input:checked'); + const anonymousNode = rowNode.querySelector( + ".anonymous input:checked" + ); if (anonymousNode) { uploadable.anonymous = true; } @@ -286,11 +298,14 @@ class PostUploadView extends events.EventTarget { uploadable.relations = []; for (let [i, lookalike] of uploadable.lookalikes.entries()) { let lookalikeNode = rowNode.querySelector( - `.lookalikes li:nth-child(${i + 1})`); - if (lookalikeNode.querySelector('[name=copy-tags]').checked) { - uploadable.tags = uploadable.tags.concat(lookalike.post.tagNames); + `.lookalikes li:nth-child(${i + 1})` + ); + if (lookalikeNode.querySelector("[name=copy-tags]").checked) { + uploadable.tags = uploadable.tags.concat( + lookalike.post.tagNames + ); } - if (lookalikeNode.querySelector('[name=add-relation]').checked) { + if (lookalikeNode.querySelector("[name=add-relation]").checked) { uploadable.relations.push(lookalike.post.id); } } @@ -317,78 +332,97 @@ class PostUploadView extends events.EventTarget { this._uploadables[index + delta] = uploadable1; if (delta === 1) { this._listNode.insertBefore( - uploadable2.rowNode, uploadable1.rowNode); + uploadable2.rowNode, + uploadable1.rowNode + ); } else { this._listNode.insertBefore( - uploadable1.rowNode, uploadable2.rowNode); + uploadable1.rowNode, + uploadable2.rowNode + ); } } } _emit(eventType) { this.dispatchEvent( - new CustomEvent( - eventType, - {detail: { + new CustomEvent(eventType, { + detail: { uploadables: this._uploadables, skipDuplicates: this._skipDuplicatesCheckboxNode.checked, - }})); + }, + }) + ); } _renderRowNode(uploadable) { - const rowNode = rowTemplate(Object.assign( - {}, this._ctx, {uploadable: uploadable})); + const rowNode = rowTemplate( + Object.assign({}, this._ctx, { uploadable: uploadable }) + ); if (uploadable.rowNode) { uploadable.rowNode.parentNode.replaceChild( - rowNode, uploadable.rowNode); + rowNode, + uploadable.rowNode + ); } else { this._listNode.appendChild(rowNode); } uploadable.rowNode = rowNode; - rowNode.querySelector('a.remove').addEventListener('click', - e => this._evtRemoveClick(e, uploadable)); - rowNode.querySelector('a.move-up').addEventListener('click', - e => this._evtMoveClick(e, uploadable, -1)); - rowNode.querySelector('a.move-down').addEventListener('click', - e => this._evtMoveClick(e, uploadable, 1)); + rowNode + .querySelector("a.remove") + .addEventListener("click", (e) => + this._evtRemoveClick(e, uploadable) + ); + rowNode + .querySelector("a.move-up") + .addEventListener("click", (e) => + this._evtMoveClick(e, uploadable, -1) + ); + rowNode + .querySelector("a.move-down") + .addEventListener("click", (e) => + this._evtMoveClick(e, uploadable, 1) + ); } _updateThumbnailNode(uploadable) { - const rowNode = rowTemplate(Object.assign( - {}, this._ctx, {uploadable: uploadable})); + const rowNode = rowTemplate( + Object.assign({}, this._ctx, { uploadable: uploadable }) + ); views.replaceContent( - uploadable.rowNode.querySelector('.thumbnail'), - rowNode.querySelector('.thumbnail').childNodes); + uploadable.rowNode.querySelector(".thumbnail"), + rowNode.querySelector(".thumbnail").childNodes + ); } get _uploading() { - return this._formNode.classList.contains('uploading'); + return this._formNode.classList.contains("uploading"); } get _listNode() { - return this._hostNode.querySelector('.uploadables-container'); + return this._hostNode.querySelector(".uploadables-container"); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _skipDuplicatesCheckboxNode() { - return this._hostNode.querySelector('form [name=skip-duplicates]'); + return this._hostNode.querySelector("form [name=skip-duplicates]"); } get _submitButtonNode() { - return this._hostNode.querySelector('form [type=submit]'); + return this._hostNode.querySelector("form [type=submit]"); } get _cancelButtonNode() { - return this._hostNode.querySelector('form .cancel'); + return this._hostNode.querySelector("form .cancel"); } get _contentInputNode() { - return this._formNode.querySelector('.dropper-container'); + return this._formNode.querySelector(".dropper-container"); } } diff --git a/client/js/views/posts_header_view.js b/client/js/views/posts_header_view.js index 0797fff..81546a7 100644 --- a/client/js/views/posts_header_view.js +++ b/client/js/views/posts_header_view.js @@ -1,53 +1,56 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const settings = require('../models/settings.js'); -const keyboard = require('../util/keyboard.js'); -const misc = require('../util/misc.js'); -const search = require('../util/search.js'); -const views = require('../util/views.js'); -const TagAutoCompleteControl = - require('../controls/tag_auto_complete_control.js'); +const events = require("../events.js"); +const settings = require("../models/settings.js"); +const keyboard = require("../util/keyboard.js"); +const misc = require("../util/misc.js"); +const search = require("../util/search.js"); +const views = require("../util/views.js"); +const TagAutoCompleteControl = require("../controls/tag_auto_complete_control.js"); -const template = views.getTemplate('posts-header'); +const template = views.getTemplate("posts-header"); class BulkEditor extends events.EventTarget { constructor(hostNode) { super(); this._hostNode = hostNode; - this._openLinkNode.addEventListener( - 'click', e => this._evtOpenLinkClick(e)); - this._closeLinkNode.addEventListener( - 'click', e => this._evtCloseLinkClick(e)); + this._openLinkNode.addEventListener("click", (e) => + this._evtOpenLinkClick(e) + ); + this._closeLinkNode.addEventListener("click", (e) => + this._evtCloseLinkClick(e) + ); } get opened() { - return this._hostNode.classList.contains('opened') && - !this._hostNode.classList.contains('hidden'); + return ( + this._hostNode.classList.contains("opened") && + !this._hostNode.classList.contains("hidden") + ); } get _openLinkNode() { - return this._hostNode.querySelector('.open'); + return this._hostNode.querySelector(".open"); } get _closeLinkNode() { - return this._hostNode.querySelector('.close'); + return this._hostNode.querySelector(".close"); } toggleOpen(state) { - this._hostNode.classList.toggle('opened', state); + this._hostNode.classList.toggle("opened", state); } toggleHide(state) { - this._hostNode.classList.toggle('hidden', state); + this._hostNode.classList.toggle("hidden", state); } _evtOpenLinkClick(e) { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } _evtCloseLinkClick(e) { - throw new Error('Not implemented'); + throw new Error("Not implemented"); } } @@ -55,13 +58,13 @@ class BulkSafetyEditor extends BulkEditor { _evtOpenLinkClick(e) { e.preventDefault(); this.toggleOpen(true); - this.dispatchEvent(new CustomEvent('open', {detail: {}})); + this.dispatchEvent(new CustomEvent("open", { detail: {} })); } _evtCloseLinkClick(e) { e.preventDefault(); this.toggleOpen(false); - this.dispatchEvent(new CustomEvent('close', {detail: {}})); + this.dispatchEvent(new CustomEvent("close", { detail: {} })); } } @@ -71,10 +74,16 @@ class BulkTagEditor extends BulkEditor { this._autoCompleteControl = new TagAutoCompleteControl( this._inputNode, { - confirm: tag => this._autoCompleteControl.replaceSelectedText( - tag.names[0], false), - }); - this._hostNode.addEventListener('submit', e => this._evtFormSubmit(e)); + confirm: (tag) => + this._autoCompleteControl.replaceSelectedText( + tag.names[0], + false + ), + } + ); + this._hostNode.addEventListener("submit", (e) => + this._evtFormSubmit(e) + ); } get value() { @@ -82,7 +91,7 @@ class BulkTagEditor extends BulkEditor { } get _inputNode() { - return this._hostNode.querySelector('input[name=tag]'); + return this._hostNode.querySelector("input[name=tag]"); } focus() { @@ -96,22 +105,22 @@ class BulkTagEditor extends BulkEditor { _evtFormSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', {detail: {}})); + this.dispatchEvent(new CustomEvent("submit", { detail: {} })); } _evtOpenLinkClick(e) { e.preventDefault(); this.toggleOpen(true); this.focus(); - this.dispatchEvent(new CustomEvent('open', {detail: {}})); + this.dispatchEvent(new CustomEvent("open", { detail: {} })); } _evtCloseLinkClick(e) { e.preventDefault(); - this._inputNode.value = ''; + this._inputNode.value = ""; this.toggleOpen(false); this.blur(); - this.dispatchEvent(new CustomEvent('close', {detail: {}})); + this.dispatchEvent(new CustomEvent("close", { detail: {} })); } } @@ -127,18 +136,25 @@ class PostsHeaderView extends events.EventTarget { this._autoCompleteControl = new TagAutoCompleteControl( this._queryInputNode, { - confirm: tag => this._autoCompleteControl.replaceSelectedText( - misc.escapeSearchTerm(tag.names[0]), true), - }); + confirm: (tag) => + this._autoCompleteControl.replaceSelectedText( + misc.escapeSearchTerm(tag.names[0]), + true + ), + } + ); - keyboard.bind('p', () => this._focusFirstPostNode()); + keyboard.bind("p", () => this._focusFirstPostNode()); search.searchInputNodeFocusHelper(this._queryInputNode); for (let safetyButtonNode of this._safetyButtonNodes) { - safetyButtonNode.addEventListener( - 'click', e => this._evtSafetyButtonClick(e)); + safetyButtonNode.addEventListener("click", (e) => + this._evtSafetyButtonClick(e) + ); } - this._formNode.addEventListener('submit', e => this._evtFormSubmit(e)); + this._formNode.addEventListener("submit", (e) => + this._evtFormSubmit(e) + ); this._bulkEditors = []; if (this._bulkEditTagsNode) { @@ -148,19 +164,20 @@ class PostsHeaderView extends events.EventTarget { if (this._bulkEditSafetyNode) { this._bulkSafetyEditor = new BulkSafetyEditor( - this._bulkEditSafetyNode); + this._bulkEditSafetyNode + ); this._bulkEditors.push(this._bulkSafetyEditor); } for (let editor of this._bulkEditors) { - editor.addEventListener('submit', e => { + editor.addEventListener("submit", (e) => { this._navigate(); }); - editor.addEventListener('open', e => { + editor.addEventListener("open", (e) => { this._hideBulkEditorsExcept(editor); this._navigate(); }); - editor.addEventListener('close', e => { + editor.addEventListener("close", (e) => { this._closeAndShowAllBulkEditors(); this._navigate(); }); @@ -174,23 +191,23 @@ class PostsHeaderView extends events.EventTarget { } get _formNode() { - return this._hostNode.querySelector('form.search'); + return this._hostNode.querySelector("form.search"); } get _safetyButtonNodes() { - return this._hostNode.querySelectorAll('form .safety'); + return this._hostNode.querySelectorAll("form .safety"); } get _queryInputNode() { - return this._hostNode.querySelector('form [name=search-text]'); + return this._hostNode.querySelector("form [name=search-text]"); } get _bulkEditTagsNode() { - return this._hostNode.querySelector('.bulk-edit-tags'); + return this._hostNode.querySelector(".bulk-edit-tags"); } get _bulkEditSafetyNode() { - return this._hostNode.querySelector('.bulk-edit-safety'); + return this._hostNode.querySelector(".bulk-edit-safety"); } _openBulkEditor(editor) { @@ -216,20 +233,23 @@ class PostsHeaderView extends events.EventTarget { _evtSafetyButtonClick(e, url) { e.preventDefault(); - e.target.classList.toggle('disabled'); - const safety = e.target.getAttribute('data-safety'); + e.target.classList.toggle("disabled"); + const safety = e.target.getAttribute("data-safety"); let browsingSettings = settings.get(); - browsingSettings.listPosts[safety] = - !browsingSettings.listPosts[safety]; + browsingSettings.listPosts[safety] = !browsingSettings.listPosts[ + safety + ]; settings.save(browsingSettings, true); this.dispatchEvent( - new CustomEvent( - 'navigate', { - detail: { - parameters: Object.assign( - {}, this._ctx.parameters, {tag: null, offset: 0}), - }, - })); + new CustomEvent("navigate", { + detail: { + parameters: Object.assign({}, this._ctx.parameters, { + tag: null, + offset: 0, + }), + }, + }) + ); } _evtFormSubmit(e) { @@ -239,27 +259,33 @@ class PostsHeaderView extends events.EventTarget { _navigate() { this._autoCompleteControl.hide(); - let parameters = {query: this._queryInputNode.value}; + let parameters = { query: this._queryInputNode.value }; // convert falsy values to an empty string "" so that we can correctly compare with the current query - const prevQuery = this._ctx.parameters.query ? this._ctx.parameters.query : ""; - parameters.offset = parameters.query === prevQuery ? this._ctx.parameters.offset : 0; + const prevQuery = this._ctx.parameters.query + ? this._ctx.parameters.query + : ""; + parameters.offset = + parameters.query === prevQuery ? this._ctx.parameters.offset : 0; if (this._bulkTagEditor && this._bulkTagEditor.opened) { parameters.tag = this._bulkTagEditor.value; this._bulkTagEditor.blur(); } else { parameters.tag = null; } - parameters.safety = ( - this._bulkSafetyEditor && - this._bulkSafetyEditor.opened ? '1' : null); + parameters.safety = + this._bulkSafetyEditor && this._bulkSafetyEditor.opened + ? "1" + : null; this.dispatchEvent( - new CustomEvent('navigate', {detail: {parameters: parameters}})); + new CustomEvent("navigate", { detail: { parameters: parameters } }) + ); } _focusFirstPostNode() { - const firstPostNode = - document.body.querySelector('.post-list li:first-child a'); + const firstPostNode = document.body.querySelector( + ".post-list li:first-child a" + ); if (firstPostNode) { firstPostNode.focus(); } diff --git a/client/js/views/posts_page_view.js b/client/js/views/posts_page_view.js index 84521e3..ba07a63 100644 --- a/client/js/views/posts_page_view.js +++ b/client/js/views/posts_page_view.js @@ -1,9 +1,9 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); -const template = views.getTemplate('posts-page'); +const template = views.getTemplate("posts-page"); class PostsPageView extends events.EventTarget { constructor(ctx) { @@ -15,26 +15,28 @@ class PostsPageView extends events.EventTarget { this._postIdToPost = {}; for (let post of ctx.response.results) { this._postIdToPost[post.id] = post; - post.addEventListener('change', e => this._evtPostChange(e)); + post.addEventListener("change", (e) => this._evtPostChange(e)); } this._postIdToListItemNode = {}; for (let listItemNode of this._listItemNodes) { - const postId = listItemNode.getAttribute('data-post-id'); + const postId = listItemNode.getAttribute("data-post-id"); const post = this._postIdToPost[postId]; this._postIdToListItemNode[postId] = listItemNode; const tagFlipperNode = this._getTagFlipperNode(listItemNode); if (tagFlipperNode) { - tagFlipperNode.addEventListener( - 'click', e => this._evtBulkEditTagsClick(e, post)); + tagFlipperNode.addEventListener("click", (e) => + this._evtBulkEditTagsClick(e, post) + ); } const safetyFlipperNode = this._getSafetyFlipperNode(listItemNode); if (safetyFlipperNode) { - for (let linkNode of safetyFlipperNode.querySelectorAll('a')) { - linkNode.addEventListener( - 'click', e => this._evtBulkEditSafetyClick(e, post)); + for (let linkNode of safetyFlipperNode.querySelectorAll("a")) { + linkNode.addEventListener("click", (e) => + this._evtBulkEditSafetyClick(e, post) + ); } } } @@ -43,21 +45,21 @@ class PostsPageView extends events.EventTarget { } get _listItemNodes() { - return this._hostNode.querySelectorAll('li'); + return this._hostNode.querySelectorAll("li"); } _getTagFlipperNode(listItemNode) { - return listItemNode.querySelector('.tag-flipper'); + return listItemNode.querySelector(".tag-flipper"); } _getSafetyFlipperNode(listItemNode) { - return listItemNode.querySelector('.safety-flipper'); + return listItemNode.querySelector(".safety-flipper"); } _evtPostChange(e) { const listItemNode = this._postIdToListItemNode[e.detail.post.id]; - for (let node of listItemNode.querySelectorAll('[data-disabled]')) { - node.removeAttribute('data-disabled'); + for (let node of listItemNode.querySelectorAll("[data-disabled]")) { + node.removeAttribute("data-disabled"); } this._syncBulkEditorsHighlights(); } @@ -65,35 +67,41 @@ class PostsPageView extends events.EventTarget { _evtBulkEditTagsClick(e, post) { e.preventDefault(); const linkNode = e.target; - if (linkNode.getAttribute('data-disabled')) { + if (linkNode.getAttribute("data-disabled")) { return; } - linkNode.setAttribute('data-disabled', true); + linkNode.setAttribute("data-disabled", true); this.dispatchEvent( new CustomEvent( - linkNode.classList.contains('tagged') ? 'untag' : 'tag', - {detail: {post: post}})); + linkNode.classList.contains("tagged") ? "untag" : "tag", + { + detail: { post: post }, + } + ) + ); } _evtBulkEditSafetyClick(e, post) { e.preventDefault(); const linkNode = e.target; - if (linkNode.getAttribute('data-disabled')) { + if (linkNode.getAttribute("data-disabled")) { return; } - const newSafety = linkNode.getAttribute('data-safety'); + const newSafety = linkNode.getAttribute("data-safety"); if (post.safety === newSafety) { return; } - linkNode.setAttribute('data-disabled', true); + linkNode.setAttribute("data-disabled", true); this.dispatchEvent( - new CustomEvent( - 'changeSafety', {detail: {post: post, safety: newSafety}})); + new CustomEvent("changeSafety", { + detail: { post: post, safety: newSafety }, + }) + ); } _syncBulkEditorsHighlights() { for (let listItemNode of this._listItemNodes) { - const postId = listItemNode.getAttribute('data-post-id'); + const postId = listItemNode.getAttribute("data-post-id"); const post = this._postIdToPost[postId]; const tagFlipperNode = this._getTagFlipperNode(listItemNode); @@ -102,14 +110,17 @@ class PostsPageView extends events.EventTarget { for (let tag of this._ctx.bulkEdit.tags) { tagged &= post.tags.isTaggedWith(tag); } - tagFlipperNode.classList.toggle('tagged', tagged); + tagFlipperNode.classList.toggle("tagged", tagged); } const safetyFlipperNode = this._getSafetyFlipperNode(listItemNode); if (safetyFlipperNode) { - for (let linkNode of safetyFlipperNode.querySelectorAll('a')) { - const safety = linkNode.getAttribute('data-safety'); - linkNode.classList.toggle('active', post.safety === safety); + for (let linkNode of safetyFlipperNode.querySelectorAll("a")) { + const safety = linkNode.getAttribute("data-safety"); + linkNode.classList.toggle( + "active", + post.safety === safety + ); } } } diff --git a/client/js/views/registration_view.js b/client/js/views/registration_view.js index 1db5fcb..0a08de2 100644 --- a/client/js/views/registration_view.js +++ b/client/js/views/registration_view.js @@ -1,22 +1,25 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const api = require('../api.js'); -const views = require('../util/views.js'); +const events = require("../events.js"); +const api = require("../api.js"); +const views = require("../util/views.js"); -const template = views.getTemplate('user-registration'); +const template = views.getTemplate("user-registration"); class RegistrationView extends events.EventTarget { constructor() { super(); - this._hostNode = document.getElementById('content-holder'); - views.replaceContent(this._hostNode, template({ - userNamePattern: api.getUserNameRegex(), - passwordPattern: api.getPasswordRegex(), - })); + this._hostNode = document.getElementById("content-holder"); + views.replaceContent( + this._hostNode, + template({ + userNamePattern: api.getUserNameRegex(), + passwordPattern: api.getPasswordRegex(), + }) + ); views.syncScrollPosition(); views.decorateValidator(this._formNode); - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } clearMessages() { @@ -37,29 +40,31 @@ class RegistrationView extends events.EventTarget { _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - name: this._userNameFieldNode.value, - password: this._passwordFieldNode.value, - email: this._emailFieldNode.value, - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + name: this._userNameFieldNode.value, + password: this._passwordFieldNode.value, + email: this._emailFieldNode.value, + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _userNameFieldNode() { - return this._formNode.querySelector('[name=name]'); + return this._formNode.querySelector("[name=name]"); } get _passwordFieldNode() { - return this._formNode.querySelector('[name=password]'); + return this._formNode.querySelector("[name=password]"); } get _emailFieldNode() { - return this._formNode.querySelector('[name=email]'); + return this._formNode.querySelector("[name=email]"); } } diff --git a/client/js/views/settings_view.js b/client/js/views/settings_view.js index 5ab7316..184028e 100644 --- a/client/js/views/settings_view.js +++ b/client/js/views/settings_view.js @@ -1,21 +1,23 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); -const template = views.getTemplate('settings'); +const template = views.getTemplate("settings"); class SettingsView extends events.EventTarget { constructor(ctx) { super(); - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); views.replaceContent( - this._hostNode, template({browsingSettings: ctx.settings})); + this._hostNode, + template({ browsingSettings: ctx.settings }) + ); views.syncScrollPosition(); views.decorateValidator(this._formNode); - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } clearMessages() { @@ -28,26 +30,32 @@ class SettingsView extends events.EventTarget { _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - upscaleSmallPosts: this._find('upscale-small-posts').checked, - endlessScroll: this._find('endless-scroll').checked, - keyboardShortcuts: this._find('keyboard-shortcuts').checked, - transparencyGrid: this._find('transparency-grid').checked, - tagSuggestions: this._find('tag-suggestions').checked, - autoplayVideos: this._find('autoplay-videos').checked, - postsPerPage: this._find('posts-per-page').value, - tagUnderscoresAsSpaces: this._find('tag-underscores-as-spaces').checked, - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + upscaleSmallPosts: this._find("upscale-small-posts") + .checked, + endlessScroll: this._find("endless-scroll").checked, + keyboardShortcuts: this._find("keyboard-shortcuts") + .checked, + transparencyGrid: this._find("transparency-grid").checked, + tagSuggestions: this._find("tag-suggestions").checked, + autoplayVideos: this._find("autoplay-videos").checked, + postsPerPage: this._find("posts-per-page").value, + tagUnderscoresAsSpaces: this._find( + "tag-underscores-as-spaces" + ).checked, + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } _find(nodeName) { - return this._formNode.querySelector('[name=' + nodeName + ']'); + return this._formNode.querySelector("[name=" + nodeName + "]"); } } diff --git a/client/js/views/snapshots_page_view.js b/client/js/views/snapshots_page_view.js index e7ea264..a665ea0 100644 --- a/client/js/views/snapshots_page_view.js +++ b/client/js/views/snapshots_page_view.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const views = require('../util/views.js'); +const views = require("../util/views.js"); -const template = views.getTemplate('snapshots-page'); +const template = views.getTemplate("snapshots-page"); function _extend(target, source) { target.push.apply(target, source); @@ -10,18 +10,18 @@ function _extend(target, source) { function _formatBasicChange(diff, text) { const lines = []; - if (diff.type === 'list change') { + if (diff.type === "list change") { const addedItems = diff.added; const removedItems = diff.removed; if (addedItems && addedItems.length) { - lines.push(`Added ${text} (${addedItems.join(', ')})`); + lines.push(`Added ${text} (${addedItems.join(", ")})`); } if (removedItems && removedItems.length) { - lines.push(`Removed ${text} (${removedItems.join(', ')})`); + lines.push(`Removed ${text} (${removedItems.join(", ")})`); } - } else if (diff.type === 'primitive change') { - const oldValue = diff['old-value']; - const newValue = diff['new-value']; + } else if (diff.type === "primitive change") { + const oldValue = diff["old-value"]; + const newValue = diff["new-value"]; lines.push(`Changed ${text} (${oldValue} → ${newValue})`); } else { lines.push(`Changed ${text}`); @@ -30,13 +30,13 @@ function _formatBasicChange(diff, text) { } function _makeResourceLink(type, id) { - if (type === 'post') { + if (type === "post") { return views.makePostLink(id, true); - } else if (type === 'tag') { + } else if (type === "tag") { return views.makeTagLink(id, true); - } else if (type === 'tag_category') { + } else if (type === "tag_category") { return 'category "' + id + '"'; - } else if (type === 'pool') { + } else if (type === "pool") { return views.makePoolLink(id, true); } } @@ -50,96 +50,102 @@ function _makeItemCreation(type, data) { let text = key[0].toUpperCase() + key.substr(1).toLowerCase(); if (Array.isArray(data[key])) { if (data[key].length) { - lines.push(`${text}: ${data[key].join(', ')}`); + lines.push(`${text}: ${data[key].join(", ")}`); } } else { lines.push(`${text}: ${data[key]}`); } } - return lines.join('
        '); + return lines.join("
        "); } function _makeItemModification(type, data) { const lines = []; const diff = data.value; - if (type === 'tag_category') { + if (type === "tag_category") { if (diff.name) { - _extend(lines, _formatBasicChange(diff.name, 'name')); + _extend(lines, _formatBasicChange(diff.name, "name")); } if (diff.color) { - _extend(lines, _formatBasicChange(diff.color, 'color')); + _extend(lines, _formatBasicChange(diff.color, "color")); } if (diff.default) { - _extend(lines, ['Made into default category']); + _extend(lines, ["Made into default category"]); } - - } else if (type === 'tag') { + } else if (type === "tag") { if (diff.names) { - _extend(lines, _formatBasicChange(diff.names, 'names')); + _extend(lines, _formatBasicChange(diff.names, "names")); } if (diff.category) { - _extend( - lines, _formatBasicChange(diff.category, 'category')); + _extend(lines, _formatBasicChange(diff.category, "category")); } if (diff.suggestions) { _extend( - lines, _formatBasicChange(diff.suggestions, 'suggestions')); + lines, + _formatBasicChange(diff.suggestions, "suggestions") + ); } if (diff.implications) { _extend( - lines, _formatBasicChange(diff.implications, 'implications')); + lines, + _formatBasicChange(diff.implications, "implications") + ); } - - } else if (type === 'post') { + } else if (type === "post") { if (diff.checksum) { - _extend(lines, ['Changed content']); + _extend(lines, ["Changed content"]); } if (diff.featured) { - _extend(lines, ['Featured on front page']); + _extend(lines, ["Featured on front page"]); } if (diff.source) { - _extend(lines, _formatBasicChange(diff.source, 'source')); + _extend(lines, _formatBasicChange(diff.source, "source")); } if (diff.safety) { - _extend(lines, _formatBasicChange(diff.safety, 'safety')); + _extend(lines, _formatBasicChange(diff.safety, "safety")); } if (diff.tags) { - _extend(lines, _formatBasicChange(diff.tags, 'tags')); + _extend(lines, _formatBasicChange(diff.tags, "tags")); } if (diff.relations) { - _extend(lines, _formatBasicChange(diff.relations, 'relations')); + _extend(lines, _formatBasicChange(diff.relations, "relations")); } if (diff.notes) { - _extend(lines, ['Changed notes']); + _extend(lines, ["Changed notes"]); } if (diff.flags) { - _extend(lines, ['Changed flags']); + _extend(lines, ["Changed flags"]); } - - } else if (type === 'pool') { + } else if (type === "pool") { if (diff.names) { - _extend(lines, _formatBasicChange(diff.names, 'names')); + _extend(lines, _formatBasicChange(diff.names, "names")); } if (diff.category) { - _extend( - lines, _formatBasicChange(diff.category, 'category')); + _extend(lines, _formatBasicChange(diff.category, "category")); } if (diff.posts) { - _extend( - lines, _formatBasicChange(diff.posts, 'posts')); + _extend(lines, _formatBasicChange(diff.posts, "posts")); } } - return lines.join('
        '); + return lines.join("
        "); } class SnapshotsPageView { constructor(ctx) { - views.replaceContent(ctx.hostNode, template(Object.assign({ - makeResourceLink: _makeResourceLink, - makeItemCreation: _makeItemCreation, - makeItemModification: _makeItemModification, - }, ctx))); + views.replaceContent( + ctx.hostNode, + template( + Object.assign( + { + makeResourceLink: _makeResourceLink, + makeItemCreation: _makeItemCreation, + makeItemModification: _makeItemModification, + }, + ctx + ) + ) + ); } } diff --git a/client/js/views/tag_categories_view.js b/client/js/views/tag_categories_view.js index 7e1000d..1f1a4da 100644 --- a/client/js/views/tag_categories_view.js +++ b/client/js/views/tag_categories_view.js @@ -1,17 +1,17 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); -const TagCategory = require('../models/tag_category.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); +const TagCategory = require("../models/tag_category.js"); -const template = views.getTemplate('tag-categories'); -const rowTemplate = views.getTemplate('tag-category-row'); +const template = views.getTemplate("tag-categories"); +const rowTemplate = views.getTemplate("tag-category-row"); class TagCategoriesView extends events.EventTarget { constructor(ctx) { super(); this._ctx = ctx; - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); views.replaceContent(this._hostNode, template(ctx)); views.syncScrollPosition(); @@ -31,18 +31,22 @@ class TagCategoriesView extends events.EventTarget { } if (this._addLinkNode) { - this._addLinkNode.addEventListener( - 'click', e => this._evtAddButtonClick(e)); + this._addLinkNode.addEventListener("click", (e) => + this._evtAddButtonClick(e) + ); } - ctx.tagCategories.addEventListener( - 'add', e => this._evtTagCategoryAdded(e)); + ctx.tagCategories.addEventListener("add", (e) => + this._evtTagCategoryAdded(e) + ); - ctx.tagCategories.addEventListener( - 'remove', e => this._evtTagCategoryDeleted(e)); + ctx.tagCategories.addEventListener("remove", (e) => + this._evtTagCategoryDeleted(e) + ); - this._formNode.addEventListener( - 'submit', e => this._evtSaveButtonClick(e, ctx)); + this._formNode.addEventListener("submit", (e) => + this._evtSaveButtonClick(e, ctx) + ); } enableForm() { @@ -66,44 +70,48 @@ class TagCategoriesView extends events.EventTarget { } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _tableBodyNode() { - return this._hostNode.querySelector('tbody'); + return this._hostNode.querySelector("tbody"); } get _addLinkNode() { - return this._hostNode.querySelector('a.add'); + return this._hostNode.querySelector("a.add"); } _addTagCategoryRowNode(tagCategory) { const rowNode = rowTemplate( - Object.assign( - {}, this._ctx, {tagCategory: tagCategory})); + Object.assign({}, this._ctx, { tagCategory: tagCategory }) + ); - const nameInput = rowNode.querySelector('.name input'); + const nameInput = rowNode.querySelector(".name input"); if (nameInput) { - nameInput.addEventListener( - 'change', e => this._evtNameChange(e, rowNode)); + nameInput.addEventListener("change", (e) => + this._evtNameChange(e, rowNode) + ); } - const colorInput = rowNode.querySelector('.color input'); + const colorInput = rowNode.querySelector(".color input"); if (colorInput) { - colorInput.addEventListener( - 'change', e => this._evtColorChange(e, rowNode)); + colorInput.addEventListener("change", (e) => + this._evtColorChange(e, rowNode) + ); } - const removeLinkNode = rowNode.querySelector('.remove a'); + const removeLinkNode = rowNode.querySelector(".remove a"); if (removeLinkNode) { - removeLinkNode.addEventListener( - 'click', e => this._evtDeleteButtonClick(e, rowNode)); + removeLinkNode.addEventListener("click", (e) => + this._evtDeleteButtonClick(e, rowNode) + ); } - const defaultLinkNode = rowNode.querySelector('.set-default a'); + const defaultLinkNode = rowNode.querySelector(".set-default a"); if (defaultLinkNode) { - defaultLinkNode.addEventListener( - 'click', e => this._evtSetDefaultButtonClick(e, rowNode)); + defaultLinkNode.addEventListener("click", (e) => + this._evtSetDefaultButtonClick(e, rowNode) + ); } this._tableBodyNode.appendChild(rowNode); @@ -141,7 +149,7 @@ class TagCategoriesView extends events.EventTarget { _evtDeleteButtonClick(e, rowNode, link) { e.preventDefault(); - if (e.target.classList.contains('inactive')) { + if (e.target.classList.contains("inactive")) { return; } this._ctx.tagCategories.remove(rowNode._tagCategory); @@ -150,16 +158,16 @@ class TagCategoriesView extends events.EventTarget { _evtSetDefaultButtonClick(e, rowNode) { e.preventDefault(); this._ctx.tagCategories.defaultCategory = rowNode._tagCategory; - const oldRowNode = rowNode.parentNode.querySelector('tr.default'); + const oldRowNode = rowNode.parentNode.querySelector("tr.default"); if (oldRowNode) { - oldRowNode.classList.remove('default'); + oldRowNode.classList.remove("default"); } - rowNode.classList.add('default'); + rowNode.classList.add("default"); } _evtSaveButtonClick(e, ctx) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit')); + this.dispatchEvent(new CustomEvent("submit")); } } diff --git a/client/js/views/tag_delete_view.js b/client/js/views/tag_delete_view.js index 4d27f15..4246ec3 100644 --- a/client/js/views/tag_delete_view.js +++ b/client/js/views/tag_delete_view.js @@ -1,9 +1,9 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); -const template = views.getTemplate('tag-delete'); +const template = views.getTemplate("tag-delete"); class TagDeleteView extends events.EventTarget { constructor(ctx) { @@ -13,7 +13,7 @@ class TagDeleteView extends events.EventTarget { this._tag = ctx.tag; views.replaceContent(this._hostNode, template(ctx)); views.decorateValidator(this._formNode); - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } clearMessages() { @@ -38,15 +38,17 @@ class TagDeleteView extends events.EventTarget { _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - tag: this._tag, - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + tag: this._tag, + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } } diff --git a/client/js/views/tag_edit_view.js b/client/js/views/tag_edit_view.js index bb4a382..58c1bc4 100644 --- a/client/js/views/tag_edit_view.js +++ b/client/js/views/tag_edit_view.js @@ -1,12 +1,12 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const api = require('../api.js'); -const misc = require('../util/misc.js'); -const views = require('../util/views.js'); -const TagInputControl = require('../controls/tag_input_control.js'); +const events = require("../events.js"); +const api = require("../api.js"); +const misc = require("../util/misc.js"); +const views = require("../util/views.js"); +const TagInputControl = require("../controls/tag_input_control.js"); -const template = views.getTemplate('tag-edit'); +const template = views.getTemplate("tag-edit"); class TagEditView extends events.EventTarget { constructor(ctx) { @@ -19,28 +19,33 @@ class TagEditView extends events.EventTarget { views.decorateValidator(this._formNode); if (this._namesFieldNode) { - this._namesFieldNode.addEventListener( - 'input', e => this._evtNameInput(e)); + this._namesFieldNode.addEventListener("input", (e) => + this._evtNameInput(e) + ); } if (this._implicationsFieldNode) { new TagInputControl( - this._implicationsFieldNode, this._tag.implications); + this._implicationsFieldNode, + this._tag.implications + ); } if (this._suggestionsFieldNode) { new TagInputControl( - this._suggestionsFieldNode, this._tag.suggestions); + this._suggestionsFieldNode, + this._tag.suggestions + ); } for (let node of this._formNode.querySelectorAll( - 'input, select, textarea')) { - node.addEventListener( - 'change', e => { - this.dispatchEvent(new CustomEvent('change')); - }); + "input, select, textarea" + )) { + node.addEventListener("change", (e) => { + this.dispatchEvent(new CustomEvent("change")); + }); } - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } clearMessages() { @@ -69,72 +74,80 @@ class TagEditView extends events.EventTarget { if (!list.length) { this._namesFieldNode.setCustomValidity( - 'Tags must have at least one name.'); + "Tags must have at least one name." + ); return; } for (let item of list) { if (!regex.test(item)) { this._namesFieldNode.setCustomValidity( - `Tag name "${item}" contains invalid symbols.`); + `Tag name "${item}" contains invalid symbols.` + ); return; } } - this._namesFieldNode.setCustomValidity(''); + this._namesFieldNode.setCustomValidity(""); } _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - tag: this._tag, - - names: this._namesFieldNode ? - misc.splitByWhitespace(this._namesFieldNode.value) : - undefined, - - category: this._categoryFieldNode ? - this._categoryFieldNode.value : - undefined, - - implications: this._implicationsFieldNode ? - misc.splitByWhitespace(this._implicationsFieldNode.value) : - undefined, - - suggestions: this._suggestionsFieldNode ? - misc.splitByWhitespace(this._suggestionsFieldNode.value) : - undefined, - - description: this._descriptionFieldNode ? - this._descriptionFieldNode.value : - undefined, - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + tag: this._tag, + + names: this._namesFieldNode + ? misc.splitByWhitespace(this._namesFieldNode.value) + : undefined, + + category: this._categoryFieldNode + ? this._categoryFieldNode.value + : undefined, + + implications: this._implicationsFieldNode + ? misc.splitByWhitespace( + this._implicationsFieldNode.value + ) + : undefined, + + suggestions: this._suggestionsFieldNode + ? misc.splitByWhitespace( + this._suggestionsFieldNode.value + ) + : undefined, + + description: this._descriptionFieldNode + ? this._descriptionFieldNode.value + : undefined, + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _namesFieldNode() { - return this._formNode.querySelector('.names input'); + return this._formNode.querySelector(".names input"); } get _categoryFieldNode() { - return this._formNode.querySelector('.category select'); + return this._formNode.querySelector(".category select"); } get _implicationsFieldNode() { - return this._formNode.querySelector('.implications input'); + return this._formNode.querySelector(".implications input"); } get _suggestionsFieldNode() { - return this._formNode.querySelector('.suggestions input'); + return this._formNode.querySelector(".suggestions input"); } get _descriptionFieldNode() { - return this._formNode.querySelector('.description textarea'); + return this._formNode.querySelector(".description textarea"); } } diff --git a/client/js/views/tag_merge_view.js b/client/js/views/tag_merge_view.js index e29183a..f823ca4 100644 --- a/client/js/views/tag_merge_view.js +++ b/client/js/views/tag_merge_view.js @@ -1,12 +1,11 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const api = require('../api.js'); -const views = require('../util/views.js'); -const TagAutoCompleteControl = - require('../controls/tag_auto_complete_control.js'); +const events = require("../events.js"); +const api = require("../api.js"); +const views = require("../util/views.js"); +const TagAutoCompleteControl = require("../controls/tag_auto_complete_control.js"); -const template = views.getTemplate('tag-merge'); +const template = views.getTemplate("tag-merge"); class TagMergeView extends events.EventTarget { constructor(ctx) { @@ -22,12 +21,16 @@ class TagMergeView extends events.EventTarget { this._autoCompleteControl = new TagAutoCompleteControl( this._targetTagFieldNode, { - confirm: tag => this._autoCompleteControl.replaceSelectedText( - tag.names[0], false), - }); + confirm: (tag) => + this._autoCompleteControl.replaceSelectedText( + tag.names[0], + false + ), + } + ); } - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } clearMessages() { @@ -52,25 +55,27 @@ class TagMergeView extends events.EventTarget { _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - tag: this._tag, - targetTagName: this._targetTagFieldNode.value, - addAlias: this._addAliasCheckboxNode.checked, - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + tag: this._tag, + targetTagName: this._targetTagFieldNode.value, + addAlias: this._addAliasCheckboxNode.checked, + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _targetTagFieldNode() { - return this._formNode.querySelector('input[name=target-tag]'); + return this._formNode.querySelector("input[name=target-tag]"); } get _addAliasCheckboxNode() { - return this._formNode.querySelector('input[name=alias]'); + return this._formNode.querySelector("input[name=alias]"); } } diff --git a/client/js/views/tag_summary_view.js b/client/js/views/tag_summary_view.js index 019e72b..11c6191 100644 --- a/client/js/views/tag_summary_view.js +++ b/client/js/views/tag_summary_view.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const views = require('../util/views.js'); +const views = require("../util/views.js"); -const template = views.getTemplate('tag-summary'); +const template = views.getTemplate("tag-summary"); class TagSummaryView { constructor(ctx) { diff --git a/client/js/views/tag_view.js b/client/js/views/tag_view.js index 2aaa999..e6c3776 100644 --- a/client/js/views/tag_view.js +++ b/client/js/views/tag_view.js @@ -1,26 +1,26 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); -const misc = require('../util/misc.js'); -const TagSummaryView = require('./tag_summary_view.js'); -const TagEditView = require('./tag_edit_view.js'); -const TagMergeView = require('./tag_merge_view.js'); -const TagDeleteView = require('./tag_delete_view.js'); -const EmptyView = require('../views/empty_view.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); +const misc = require("../util/misc.js"); +const TagSummaryView = require("./tag_summary_view.js"); +const TagEditView = require("./tag_edit_view.js"); +const TagMergeView = require("./tag_merge_view.js"); +const TagDeleteView = require("./tag_delete_view.js"); +const EmptyView = require("../views/empty_view.js"); -const template = views.getTemplate('tag'); +const template = views.getTemplate("tag"); class TagView extends events.EventTarget { constructor(ctx) { super(); this._ctx = ctx; - ctx.tag.addEventListener('change', e => this._evtChange(e)); - ctx.section = ctx.section || 'summary'; + ctx.tag.addEventListener("change", (e) => this._evtChange(e)); + ctx.section = ctx.section || "summary"; ctx.getPrettyTagName = misc.getPrettyTagName; - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); this._install(); } @@ -28,52 +28,54 @@ class TagView extends events.EventTarget { const ctx = this._ctx; views.replaceContent(this._hostNode, template(ctx)); - for (let item of this._hostNode.querySelectorAll('[data-name]')) { + for (let item of this._hostNode.querySelectorAll("[data-name]")) { item.classList.toggle( - 'active', item.getAttribute('data-name') === ctx.section); - if (item.getAttribute('data-name') === ctx.section) { + "active", + item.getAttribute("data-name") === ctx.section + ); + if (item.getAttribute("data-name") === ctx.section) { item.parentNode.scrollLeft = item.getBoundingClientRect().left - - item.parentNode.getBoundingClientRect().left + item.parentNode.getBoundingClientRect().left; } } - ctx.hostNode = this._hostNode.querySelector('.tag-content-holder'); - if (ctx.section === 'edit') { + ctx.hostNode = this._hostNode.querySelector(".tag-content-holder"); + if (ctx.section === "edit") { if (!this._ctx.canEditAnything) { this._view = new EmptyView(); this._view.showError( - 'You don\'t have privileges to edit tags.'); + "You don't have privileges to edit tags." + ); } else { this._view = new TagEditView(ctx); - events.proxyEvent(this._view, this, 'submit'); + events.proxyEvent(this._view, this, "submit"); } - - } else if (ctx.section === 'merge') { + } else if (ctx.section === "merge") { if (!this._ctx.canMerge) { this._view = new EmptyView(); this._view.showError( - 'You don\'t have privileges to merge tags.'); + "You don't have privileges to merge tags." + ); } else { this._view = new TagMergeView(ctx); - events.proxyEvent(this._view, this, 'submit', 'merge'); + events.proxyEvent(this._view, this, "submit", "merge"); } - - } else if (ctx.section === 'delete') { + } else if (ctx.section === "delete") { if (!this._ctx.canDelete) { this._view = new EmptyView(); this._view.showError( - 'You don\'t have privileges to delete tags.'); + "You don't have privileges to delete tags." + ); } else { this._view = new TagDeleteView(ctx); - events.proxyEvent(this._view, this, 'submit', 'delete'); + events.proxyEvent(this._view, this, "submit", "delete"); } - } else { this._view = new TagSummaryView(ctx); } - events.proxyEvent(this._view, this, 'change'); + events.proxyEvent(this._view, this, "change"); views.syncScrollPosition(); } diff --git a/client/js/views/tags_header_view.js b/client/js/views/tags_header_view.js index 425c811..6dd7626 100644 --- a/client/js/views/tags_header_view.js +++ b/client/js/views/tags_header_view.js @@ -1,13 +1,12 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const misc = require('../util/misc.js'); -const search = require('../util/search.js'); -const views = require('../util/views.js'); -const TagAutoCompleteControl = - require('../controls/tag_auto_complete_control.js'); +const events = require("../events.js"); +const misc = require("../util/misc.js"); +const search = require("../util/search.js"); +const views = require("../util/views.js"); +const TagAutoCompleteControl = require("../controls/tag_auto_complete_control.js"); -const template = views.getTemplate('tags-header'); +const template = views.getTemplate("tags-header"); class TagsHeaderView extends events.EventTarget { constructor(ctx) { @@ -20,31 +19,41 @@ class TagsHeaderView extends events.EventTarget { this._autoCompleteControl = new TagAutoCompleteControl( this._queryInputNode, { - confirm: tag => this._autoCompleteControl.replaceSelectedText( - misc.escapeSearchTerm(tag.names[0]), true), - }); + confirm: (tag) => + this._autoCompleteControl.replaceSelectedText( + misc.escapeSearchTerm(tag.names[0]), + true + ), + } + ); } search.searchInputNodeFocusHelper(this._queryInputNode); - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _queryInputNode() { - return this._hostNode.querySelector('[name=search-text]'); + return this._hostNode.querySelector("[name=search-text]"); } _evtSubmit(e) { e.preventDefault(); this._queryInputNode.blur(); - this.dispatchEvent(new CustomEvent('navigate', {detail: {parameters: { - query: this._queryInputNode.value, - page: 1, - }}})); + this.dispatchEvent( + new CustomEvent("navigate", { + detail: { + parameters: { + query: this._queryInputNode.value, + page: 1, + }, + }, + }) + ); } } diff --git a/client/js/views/tags_page_view.js b/client/js/views/tags_page_view.js index cd335a4..e2b2820 100644 --- a/client/js/views/tags_page_view.js +++ b/client/js/views/tags_page_view.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const views = require('../util/views.js'); +const views = require("../util/views.js"); -const template = views.getTemplate('tags-page'); +const template = views.getTemplate("tags-page"); class TagsPageView { constructor(ctx) { diff --git a/client/js/views/top_navigation_view.js b/client/js/views/top_navigation_view.js index 2f99187..efe75a9 100644 --- a/client/js/views/top_navigation_view.js +++ b/client/js/views/top_navigation_view.js @@ -1,24 +1,24 @@ -'use strict'; +"use strict"; -const views = require('../util/views.js'); +const views = require("../util/views.js"); -const template = views.getTemplate('top-navigation'); +const template = views.getTemplate("top-navigation"); class TopNavigationView { constructor() { - this._hostNode = document.getElementById('top-navigation-holder'); + this._hostNode = document.getElementById("top-navigation-holder"); } get _mobileNavigationToggleNode() { - return this._hostNode.querySelector('#mobile-navigation-toggle'); + return this._hostNode.querySelector("#mobile-navigation-toggle"); } get _navigationListNode() { - return this._hostNode.querySelector('nav > ul'); + return this._hostNode.querySelector("nav > ul"); } get _navigationLinkNodes() { - return this._navigationListNode.querySelectorAll('li > a'); + return this._navigationListNode.querySelectorAll("li > a"); } render(ctx) { @@ -28,28 +28,32 @@ class TopNavigationView { } activate(key) { - for (let itemNode of this._hostNode.querySelectorAll('[data-name]')) { + for (let itemNode of this._hostNode.querySelectorAll("[data-name]")) { itemNode.classList.toggle( - 'active', itemNode.getAttribute('data-name') === key); + "active", + itemNode.getAttribute("data-name") === key + ); } } _bindMobileNavigationEvents() { - this._mobileNavigationToggleNode.addEventListener( - 'click', e => this._mobileNavigationToggleClick(e)); + this._mobileNavigationToggleNode.addEventListener("click", (e) => + this._mobileNavigationToggleClick(e) + ); for (let navigationLinkNode of this._navigationLinkNodes) { - navigationLinkNode.addEventListener( - 'click', e => this._navigationLinkClick(e)); + navigationLinkNode.addEventListener("click", (e) => + this._navigationLinkClick(e) + ); } } _mobileNavigationToggleClick(e) { - this._navigationListNode.classList.toggle('opened'); + this._navigationListNode.classList.toggle("opened"); } _navigationLinkClick(e) { - this._navigationListNode.classList.remove('opened'); + this._navigationListNode.classList.remove("opened"); } } diff --git a/client/js/views/user_delete_view.js b/client/js/views/user_delete_view.js index bdaf9e6..37de52f 100644 --- a/client/js/views/user_delete_view.js +++ b/client/js/views/user_delete_view.js @@ -1,9 +1,9 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); -const template = views.getTemplate('user-delete'); +const template = views.getTemplate("user-delete"); class UserDeleteView extends events.EventTarget { constructor(ctx) { @@ -14,7 +14,7 @@ class UserDeleteView extends events.EventTarget { views.replaceContent(this._hostNode, template(ctx)); views.decorateValidator(this._formNode); - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } clearMessages() { @@ -39,16 +39,17 @@ class UserDeleteView extends events.EventTarget { _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - user: this._user, - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + user: this._user, + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('form'); - + return this._hostNode.querySelector("form"); } } diff --git a/client/js/views/user_edit_view.js b/client/js/views/user_edit_view.js index 317d8e1..4886726 100644 --- a/client/js/views/user_edit_view.js +++ b/client/js/views/user_edit_view.js @@ -1,18 +1,18 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const api = require('../api.js'); -const views = require('../util/views.js'); -const FileDropperControl = require('../controls/file_dropper_control.js'); +const events = require("../events.js"); +const api = require("../api.js"); +const views = require("../util/views.js"); +const FileDropperControl = require("../controls/file_dropper_control.js"); -const template = views.getTemplate('user-edit'); +const template = views.getTemplate("user-edit"); class UserEditView extends events.EventTarget { constructor(ctx) { super(); - ctx.userNamePattern = api.getUserNameRegex() + (/|^$/).source; - ctx.passwordPattern = api.getPasswordRegex() + (/|^$/).source; + ctx.userNamePattern = api.getUserNameRegex() + /|^$/.source; + ctx.passwordPattern = api.getPasswordRegex() + /|^$/.source; this._user = ctx.user; this._hostNode = ctx.hostNode; @@ -22,24 +22,26 @@ class UserEditView extends events.EventTarget { this._avatarContent = null; if (this._avatarContentInputNode) { this._avatarFileDropper = new FileDropperControl( - this._avatarContentInputNode, {lock: true}); - this._avatarFileDropper.addEventListener('fileadd', e => { + this._avatarContentInputNode, + { lock: true } + ); + this._avatarFileDropper.addEventListener("fileadd", (e) => { this._hostNode.querySelector( - '[name=avatar-style][value=manual]').checked = true; + "[name=avatar-style][value=manual]" + ).checked = true; this._avatarContent = e.detail.files[0]; }); } - for (let node of this._formNode.querySelectorAll('input, select')) { - node.addEventListener( - 'change', e => { - if (!e.target.classList.contains('anticomplete')) { - this.dispatchEvent(new CustomEvent('change')); - } - }); + for (let node of this._formNode.querySelectorAll("input, select")) { + node.addEventListener("change", (e) => { + if (!e.target.classList.contains("anticomplete")) { + this.dispatchEvent(new CustomEvent("change")); + } + }); } - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } clearMessages() { @@ -64,61 +66,63 @@ class UserEditView extends events.EventTarget { _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - user: this._user, - - name: this._userNameInputNode ? - this._userNameInputNode.value : - undefined, - - email: this._emailInputNode ? - this._emailInputNode.value : - undefined, - - rank: this._rankInputNode ? - this._rankInputNode.value : - undefined, - - avatarStyle: this._avatarStyleInputNode ? - this._avatarStyleInputNode.value : - undefined, - - password: this._passwordInputNode ? - this._passwordInputNode.value : - undefined, - - avatarContent: this._avatarContent, - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + user: this._user, + + name: this._userNameInputNode + ? this._userNameInputNode.value + : undefined, + + email: this._emailInputNode + ? this._emailInputNode.value + : undefined, + + rank: this._rankInputNode + ? this._rankInputNode.value + : undefined, + + avatarStyle: this._avatarStyleInputNode + ? this._avatarStyleInputNode.value + : undefined, + + password: this._passwordInputNode + ? this._passwordInputNode.value + : undefined, + + avatarContent: this._avatarContent, + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _rankInputNode() { - return this._formNode.querySelector('[name=rank]'); + return this._formNode.querySelector("[name=rank]"); } get _emailInputNode() { - return this._formNode.querySelector('[name=email]'); + return this._formNode.querySelector("[name=email]"); } get _userNameInputNode() { - return this._formNode.querySelector('[name=name]'); + return this._formNode.querySelector("[name=name]"); } get _passwordInputNode() { - return this._formNode.querySelector('[name=password]'); + return this._formNode.querySelector("[name=password]"); } get _avatarContentInputNode() { - return this._formNode.querySelector('#avatar-content'); + return this._formNode.querySelector("#avatar-content"); } get _avatarStyleInputNode() { - return this._formNode.querySelector('[name=avatar-style]:checked'); + return this._formNode.querySelector("[name=avatar-style]:checked"); } } diff --git a/client/js/views/user_summary_view.js b/client/js/views/user_summary_view.js index d8463f3..eab827f 100644 --- a/client/js/views/user_summary_view.js +++ b/client/js/views/user_summary_view.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const views = require('../util/views.js'); +const views = require("../util/views.js"); -const template = views.getTemplate('user-summary'); +const template = views.getTemplate("user-summary"); class UserSummaryView { constructor(ctx) { diff --git a/client/js/views/user_tokens_view.js b/client/js/views/user_tokens_view.js index 8e283bf..68707f0 100644 --- a/client/js/views/user_tokens_view.js +++ b/client/js/views/user_tokens_view.js @@ -1,9 +1,9 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); -const template = views.getTemplate('user-tokens'); +const template = views.getTemplate("user-tokens"); class UserTokenView extends events.EventTarget { constructor(ctx) { @@ -16,7 +16,7 @@ class UserTokenView extends events.EventTarget { views.replaceContent(this._hostNode, template(ctx)); views.decorateValidator(this._formNode); - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); this._decorateTokenForms(); this._decorateTokenNoteChangeLinks(); @@ -26,8 +26,9 @@ class UserTokenView extends events.EventTarget { this._tokenFormNodes = []; for (let i = 0; i < this._tokens.length; i++) { let formNode = this._hostNode.querySelector( - '.token[data-token-id=\"' + i + '\"]'); - formNode.addEventListener('submit', e => this._evtDelete(e)); + '.token[data-token-id="' + i + '"]' + ); + formNode.addEventListener("submit", (e) => this._evtDelete(e)); this._tokenFormNodes.push(formNode); } } @@ -35,9 +36,11 @@ class UserTokenView extends events.EventTarget { _decorateTokenNoteChangeLinks() { for (let i = 0; i < this._tokens.length; i++) { let linkNode = this._hostNode.querySelector( - '.token-change-note[data-token-id=\"' + i + '\"]'); - linkNode.addEventListener( - 'click', e => this._evtChangeNoteClick(e)); + '.token-change-note[data-token-id="' + i + '"]' + ); + linkNode.addEventListener("click", (e) => + this._evtChangeNoteClick(e) + ); } } @@ -69,65 +72,75 @@ class UserTokenView extends events.EventTarget { _evtDelete(e) { e.preventDefault(); - const userToken = this._tokens[parseInt( - e.target.getAttribute('data-token-id'))]; - this.dispatchEvent(new CustomEvent('delete', { - detail: { - user: this._user, - userToken: userToken, - }, - })); + const userToken = this._tokens[ + parseInt(e.target.getAttribute("data-token-id")) + ]; + this.dispatchEvent( + new CustomEvent("delete", { + detail: { + user: this._user, + userToken: userToken, + }, + }) + ); } _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - user: this._user, - - note: this._userTokenNoteInputNode ? - this._userTokenNoteInputNode.value : - undefined, - - expirationTime: - (this._userTokenExpirationTimeInputNode - && this._userTokenExpirationTimeInputNode.value) ? - new Date(this._userTokenExpirationTimeInputNode.value) - .toISOString() : - undefined, - }, - })); + this.dispatchEvent( + new CustomEvent("submit", { + detail: { + user: this._user, + + note: this._userTokenNoteInputNode + ? this._userTokenNoteInputNode.value + : undefined, + + expirationTime: + this._userTokenExpirationTimeInputNode && + this._userTokenExpirationTimeInputNode.value + ? new Date( + this._userTokenExpirationTimeInputNode.value + ).toISOString() + : undefined, + }, + }) + ); } _evtChangeNoteClick(e) { e.preventDefault(); const userToken = this._tokens[ - parseInt(e.target.getAttribute('data-token-id'))]; + parseInt(e.target.getAttribute("data-token-id")) + ]; const text = window.prompt( - 'Please enter the new name:', - userToken.note !== null ? userToken.note : undefined); + "Please enter the new name:", + userToken.note !== null ? userToken.note : undefined + ); if (!text) { return; } - this.dispatchEvent(new CustomEvent('update', { - detail: { - user: this._user, - userToken: userToken, - note: text ? text : undefined, - }, - })); + this.dispatchEvent( + new CustomEvent("update", { + detail: { + user: this._user, + userToken: userToken, + note: text ? text : undefined, + }, + }) + ); } get _formNode() { - return this._hostNode.querySelector('#create-token-form'); + return this._hostNode.querySelector("#create-token-form"); } get _userTokenNoteInputNode() { - return this._formNode.querySelector('.note input'); + return this._formNode.querySelector(".note input"); } get _userTokenExpirationTimeInputNode() { - return this._formNode.querySelector('.expirationTime input'); + return this._formNode.querySelector(".expirationTime input"); } } diff --git a/client/js/views/user_view.js b/client/js/views/user_view.js index d96ebe1..2eebfe9 100644 --- a/client/js/views/user_view.js +++ b/client/js/views/user_view.js @@ -1,24 +1,24 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const views = require('../util/views.js'); -const UserDeleteView = require('./user_delete_view.js'); -const UserTokensView = require('./user_tokens_view.js'); -const UserSummaryView = require('./user_summary_view.js'); -const UserEditView = require('./user_edit_view.js'); -const EmptyView = require('../views/empty_view.js'); +const events = require("../events.js"); +const views = require("../util/views.js"); +const UserDeleteView = require("./user_delete_view.js"); +const UserTokensView = require("./user_tokens_view.js"); +const UserSummaryView = require("./user_summary_view.js"); +const UserEditView = require("./user_edit_view.js"); +const EmptyView = require("../views/empty_view.js"); -const template = views.getTemplate('user'); +const template = views.getTemplate("user"); class UserView extends events.EventTarget { constructor(ctx) { super(); this._ctx = ctx; - ctx.user.addEventListener('change', e => this._evtChange(e)); - ctx.section = ctx.section || 'summary'; + ctx.user.addEventListener("change", (e) => this._evtChange(e)); + ctx.section = ctx.section || "summary"; - this._hostNode = document.getElementById('content-holder'); + this._hostNode = document.getElementById("content-holder"); this._install(); } @@ -26,52 +26,56 @@ class UserView extends events.EventTarget { const ctx = this._ctx; views.replaceContent(this._hostNode, template(ctx)); - for (let item of this._hostNode.querySelectorAll('[data-name]')) { + for (let item of this._hostNode.querySelectorAll("[data-name]")) { item.classList.toggle( - 'active', item.getAttribute('data-name') === ctx.section); - if (item.getAttribute('data-name') === ctx.section) { + "active", + item.getAttribute("data-name") === ctx.section + ); + if (item.getAttribute("data-name") === ctx.section) { item.parentNode.scrollLeft = item.getBoundingClientRect().left - - item.parentNode.getBoundingClientRect().left + item.parentNode.getBoundingClientRect().left; } } - ctx.hostNode = this._hostNode.querySelector('#user-content-holder'); - if (ctx.section === 'edit') { + ctx.hostNode = this._hostNode.querySelector("#user-content-holder"); + if (ctx.section === "edit") { if (!this._ctx.canEditAnything) { this._view = new EmptyView(); this._view.showError( - 'You don\'t have privileges to edit users.'); + "You don't have privileges to edit users." + ); } else { this._view = new UserEditView(ctx); - events.proxyEvent(this._view, this, 'submit'); + events.proxyEvent(this._view, this, "submit"); } - } else if (ctx.section === 'list-tokens') { + } else if (ctx.section === "list-tokens") { if (!this._ctx.canListTokens) { this._view = new EmptyView(); this._view.showError( - 'You don\'t have privileges to view user tokens.'); + "You don't have privileges to view user tokens." + ); } else { this._view = new UserTokensView(ctx); - events.proxyEvent(this._view, this, 'delete', 'delete-token'); - events.proxyEvent(this._view, this, 'submit', 'create-token'); - events.proxyEvent(this._view, this, 'update', 'update-token'); + events.proxyEvent(this._view, this, "delete", "delete-token"); + events.proxyEvent(this._view, this, "submit", "create-token"); + events.proxyEvent(this._view, this, "update", "update-token"); } - } else if (ctx.section === 'delete') { + } else if (ctx.section === "delete") { if (!this._ctx.canDelete) { this._view = new EmptyView(); this._view.showError( - 'You don\'t have privileges to delete users.'); + "You don't have privileges to delete users." + ); } else { this._view = new UserDeleteView(ctx); - events.proxyEvent(this._view, this, 'submit', 'delete'); + events.proxyEvent(this._view, this, "submit", "delete"); } - } else { this._view = new UserSummaryView(ctx); } - events.proxyEvent(this._view, this, 'change'); + events.proxyEvent(this._view, this, "change"); views.syncScrollPosition(); } diff --git a/client/js/views/users_header_view.js b/client/js/views/users_header_view.js index 08b7620..d6b7c6a 100644 --- a/client/js/views/users_header_view.js +++ b/client/js/views/users_header_view.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; -const events = require('../events.js'); -const search = require('../util/search.js'); -const views = require('../util/views.js'); +const events = require("../events.js"); +const search = require("../util/search.js"); +const views = require("../util/views.js"); -const template = views.getTemplate('users-header'); +const template = views.getTemplate("users-header"); class UsersHeaderView extends events.EventTarget { constructor(ctx) { @@ -15,23 +15,29 @@ class UsersHeaderView extends events.EventTarget { search.searchInputNodeFocusHelper(this._queryInputNode); - this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + this._formNode.addEventListener("submit", (e) => this._evtSubmit(e)); } get _formNode() { - return this._hostNode.querySelector('form'); + return this._hostNode.querySelector("form"); } get _queryInputNode() { - return this._formNode.querySelector('[name=search-text]'); + return this._formNode.querySelector("[name=search-text]"); } _evtSubmit(e) { e.preventDefault(); - this.dispatchEvent(new CustomEvent('navigate', {detail: {parameters: { - query: this._queryInputNode.value, - page: 1, - }}})); + this.dispatchEvent( + new CustomEvent("navigate", { + detail: { + parameters: { + query: this._queryInputNode.value, + page: 1, + }, + }, + }) + ); } } diff --git a/client/js/views/users_page_view.js b/client/js/views/users_page_view.js index f772d4e..2689433 100644 --- a/client/js/views/users_page_view.js +++ b/client/js/views/users_page_view.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const views = require('../util/views.js'); +const views = require("../util/views.js"); -const template = views.getTemplate('users-page'); +const template = views.getTemplate("users-page"); class UsersPageView { constructor(ctx) { diff --git a/server/.dockerignore b/server/.dockerignore index 0b6e9fc..cdfeafc 100644 --- a/server/.dockerignore +++ b/server/.dockerignore @@ -1,5 +1,6 @@ # Linter configs -setup.cfg +pyproject.toml +.flake8 # Python requirements files requirements.txt diff --git a/server/.flake8 b/server/.flake8 new file mode 100644 index 0000000..023ae12 --- /dev/null +++ b/server/.flake8 @@ -0,0 +1,5 @@ +[flake8] +filename = szurubooru/ +exclude = __pycache__ +ignore = F401, W503, W504, E203, E231 +max-line-length = 79 diff --git a/server/Dockerfile.test b/server/Dockerfile.test index d5b1133..16c6121 100644 --- a/server/Dockerfile.test +++ b/server/Dockerfile.test @@ -47,4 +47,4 @@ ENV POSTGRES_HOST=x \ COPY --chown=app:app ./ /opt/app/ ENTRYPOINT ["pytest", "--tb=short"] -CMD ["--cov-report=term-missing:skip-covered", "--cov=szurubooru", "szurubooru/"] +CMD ["szurubooru/"] diff --git a/server/hooks/test b/server/hooks/test index bf7776d..1edf701 100755 --- a/server/hooks/test +++ b/server/hooks/test @@ -1,8 +1,8 @@ #!/bin/sh set -e -docker build -f ${DOCKERFILE_PATH:-Dockerfile}.test -t ${IMAGE_NAME}-test . -docker run --rm -t ${IMAGE_NAME}-test -docker rmi ${IMAGE_NAME}-test +docker run --rm \ + -t $(docker build -f ${DOCKERFILE_PATH:-Dockerfile}.test -q .) \ + --color=no szurubooru/ exit $? diff --git a/server/pyproject.toml b/server/pyproject.toml new file mode 100644 index 0000000..ccf47fc --- /dev/null +++ b/server/pyproject.toml @@ -0,0 +1,10 @@ +[tool.black] +line-length = 79 + +[tool.isort] +known_first_party = ["szurubooru"] +known_third_party = ["PIL", "alembic", "coloredlogs", "freezegun", "nacl", "numpy", "pyrfc3339", "pytest", "pytz", "sqlalchemy", "yaml", "youtube_dl"] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true diff --git a/server/setup.cfg b/server/setup.cfg deleted file mode 100644 index 2179081..0000000 --- a/server/setup.cfg +++ /dev/null @@ -1,20 +0,0 @@ -[flake8] -filename = szurubooru/ -exclude = __pycache__ -ignore = F401, W503, W504 -max-line-length = 79 - -[mypy] -ignore_missing_imports = True -follow_imports = skip -disallow_untyped_calls = True -disallow_untyped_defs = True -check_untyped_defs = True -disallow_subclassing_any = False -warn_redundant_casts = True -warn_unused_ignores = True -strict_optional = True -strict_boolean = False - -[mypy-szurubooru.tests.*] -ignore_errors=True diff --git a/server/szuru-admin b/server/szuru-admin index 6433fde..7abc69c 100755 --- a/server/szuru-admin +++ b/server/szuru-admin @@ -13,8 +13,9 @@ from getpass import getpass from sys import stderr from szurubooru import config, db, errors, model -from szurubooru.func import files, images, \ - posts as postfuncs, users as userfuncs +from szurubooru.func import files, images +from szurubooru.func import posts as postfuncs +from szurubooru.func import users as userfuncs def reset_password(username: str) -> None: diff --git a/server/szurubooru/api/__init__.py b/server/szurubooru/api/__init__.py index 28f0e98..d9b7ecb 100644 --- a/server/szurubooru/api/__init__.py +++ b/server/szurubooru/api/__init__.py @@ -1,12 +1,12 @@ +import szurubooru.api.comment_api import szurubooru.api.info_api -import szurubooru.api.user_api -import szurubooru.api.user_token_api -import szurubooru.api.post_api -import szurubooru.api.tag_api -import szurubooru.api.tag_category_api +import szurubooru.api.password_reset_api import szurubooru.api.pool_api import szurubooru.api.pool_category_api -import szurubooru.api.comment_api -import szurubooru.api.password_reset_api +import szurubooru.api.post_api import szurubooru.api.snapshot_api +import szurubooru.api.tag_api +import szurubooru.api.tag_category_api import szurubooru.api.upload_api +import szurubooru.api.user_api +import szurubooru.api.user_token_api diff --git a/server/szurubooru/api/comment_api.py b/server/szurubooru/api/comment_api.py index cc8350a..d60d23e 100644 --- a/server/szurubooru/api/comment_api.py +++ b/server/szurubooru/api/comment_api.py @@ -1,44 +1,52 @@ -from typing import Dict from datetime import datetime -from szurubooru import search, rest, model -from szurubooru.func import ( - auth, comments, posts, scores, versions, serialization) +from typing import Dict +from szurubooru import model, rest, search +from szurubooru.func import ( + auth, + comments, + posts, + scores, + serialization, + versions, +) _search_executor = search.Executor(search.configs.CommentSearchConfig()) def _get_comment(params: Dict[str, str]) -> model.Comment: try: - comment_id = int(params['comment_id']) + comment_id = int(params["comment_id"]) except TypeError: raise comments.InvalidCommentIdError( - 'Invalid comment ID: %r.' % params['comment_id']) + "Invalid comment ID: %r." % params["comment_id"] + ) return comments.get_comment_by_id(comment_id) -def _serialize( - ctx: rest.Context, comment: model.Comment) -> rest.Response: +def _serialize(ctx: rest.Context, comment: model.Comment) -> rest.Response: return comments.serialize_comment( - comment, - ctx.user, - options=serialization.get_serialization_options(ctx)) + comment, ctx.user, options=serialization.get_serialization_options(ctx) + ) -@rest.routes.get('/comments/?') +@rest.routes.get("/comments/?") def get_comments( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'comments:list') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "comments:list") return _search_executor.execute_and_serialize( - ctx, lambda comment: _serialize(ctx, comment)) + ctx, lambda comment: _serialize(ctx, comment) + ) -@rest.routes.post('/comments/?') +@rest.routes.post("/comments/?") def create_comment( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'comments:create') - text = ctx.get_param_as_string('text') - post_id = ctx.get_param_as_int('postId') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "comments:create") + text = ctx.get_param_as_string("text") + post_id = ctx.get_param_as_int("postId") post = posts.get_post_by_id(post_id) comment = comments.create_comment(ctx.user, post, text) ctx.session.add(comment) @@ -46,53 +54,55 @@ def create_comment( return _serialize(ctx, comment) -@rest.routes.get('/comment/(?P[^/]+)/?') +@rest.routes.get("/comment/(?P[^/]+)/?") def get_comment(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'comments:view') + auth.verify_privilege(ctx.user, "comments:view") comment = _get_comment(params) return _serialize(ctx, comment) -@rest.routes.put('/comment/(?P[^/]+)/?') +@rest.routes.put("/comment/(?P[^/]+)/?") def update_comment(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: comment = _get_comment(params) versions.verify_version(comment, ctx) versions.bump_version(comment) - infix = 'own' if ctx.user.user_id == comment.user_id else 'any' - text = ctx.get_param_as_string('text') - auth.verify_privilege(ctx.user, 'comments:edit:%s' % infix) + infix = "own" if ctx.user.user_id == comment.user_id else "any" + text = ctx.get_param_as_string("text") + auth.verify_privilege(ctx.user, "comments:edit:%s" % infix) comments.update_comment_text(comment, text) comment.last_edit_time = datetime.utcnow() ctx.session.commit() return _serialize(ctx, comment) -@rest.routes.delete('/comment/(?P[^/]+)/?') +@rest.routes.delete("/comment/(?P[^/]+)/?") def delete_comment(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: comment = _get_comment(params) versions.verify_version(comment, ctx) - infix = 'own' if ctx.user.user_id == comment.user_id else 'any' - auth.verify_privilege(ctx.user, 'comments:delete:%s' % infix) + infix = "own" if ctx.user.user_id == comment.user_id else "any" + auth.verify_privilege(ctx.user, "comments:delete:%s" % infix) ctx.session.delete(comment) ctx.session.commit() return {} -@rest.routes.put('/comment/(?P[^/]+)/score/?') +@rest.routes.put("/comment/(?P[^/]+)/score/?") def set_comment_score( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'comments:score') - score = ctx.get_param_as_int('score') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "comments:score") + score = ctx.get_param_as_int("score") comment = _get_comment(params) scores.set_score(comment, ctx.user, score) ctx.session.commit() return _serialize(ctx, comment) -@rest.routes.delete('/comment/(?P[^/]+)/score/?') +@rest.routes.delete("/comment/(?P[^/]+)/score/?") def delete_comment_score( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'comments:score') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "comments:score") comment = _get_comment(params) scores.delete_score(comment, ctx.user) ctx.session.commit() diff --git a/server/szurubooru/api/info_api.py b/server/szurubooru/api/info_api.py index 1e2fd1d..757b09c 100644 --- a/server/szurubooru/api/info_api.py +++ b/server/szurubooru/api/info_api.py @@ -1,10 +1,10 @@ import os -from typing import Optional, Dict from datetime import datetime, timedelta +from typing import Dict, Optional + from szurubooru import config, rest from szurubooru.func import auth, posts, users, util - _cache_time = None # type: Optional[datetime] _cache_result = None # type: Optional[int] @@ -17,7 +17,7 @@ def _get_disk_usage() -> int: assert _cache_result is not None return _cache_result total_size = 0 - for dir_path, _, file_names in os.walk(config.config['data_dir']): + for dir_path, _, file_names in os.walk(config.config["data_dir"]): for file_name in file_names: file_path = os.path.join(dir_path, file_name) try: @@ -29,35 +29,38 @@ def _get_disk_usage() -> int: return total_size -@rest.routes.get('/info/?') -def get_info( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: +@rest.routes.get("/info/?") +def get_info(ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: post_feature = posts.try_get_current_post_feature() ret = { - 'postCount': posts.get_post_count(), - 'diskUsage': _get_disk_usage(), - 'serverTime': datetime.utcnow(), - 'config': { - 'name': config.config['name'], - 'userNameRegex': config.config['user_name_regex'], - 'passwordRegex': config.config['password_regex'], - 'tagNameRegex': config.config['tag_name_regex'], - 'tagCategoryNameRegex': config.config['tag_category_name_regex'], - 'defaultUserRank': config.config['default_rank'], - 'enableSafety': config.config['enable_safety'], - 'contactEmail': config.config['contact_email'], - 'canSendMails': bool(config.config['smtp']['host']), - 'privileges': - util.snake_case_to_lower_camel_case_keys( - config.config['privileges']), + "postCount": posts.get_post_count(), + "diskUsage": _get_disk_usage(), + "serverTime": datetime.utcnow(), + "config": { + "name": config.config["name"], + "userNameRegex": config.config["user_name_regex"], + "passwordRegex": config.config["password_regex"], + "tagNameRegex": config.config["tag_name_regex"], + "tagCategoryNameRegex": config.config["tag_category_name_regex"], + "defaultUserRank": config.config["default_rank"], + "enableSafety": config.config["enable_safety"], + "contactEmail": config.config["contact_email"], + "canSendMails": bool(config.config["smtp"]["host"]), + "privileges": util.snake_case_to_lower_camel_case_keys( + config.config["privileges"] + ), }, } - if auth.has_privilege(ctx.user, 'posts:view:featured'): - ret['featuredPost'] = ( + if auth.has_privilege(ctx.user, "posts:view:featured"): + ret["featuredPost"] = ( posts.serialize_post(post_feature.post, ctx.user) - if post_feature else None) - ret['featuringUser'] = ( + if post_feature + else None + ) + ret["featuringUser"] = ( users.serialize_user(post_feature.user, ctx.user) - if post_feature else None) - ret['featuringTime'] = post_feature.time if post_feature else None + if post_feature + else None + ) + ret["featuringTime"] = post_feature.time if post_feature else None return ret diff --git a/server/szurubooru/api/password_reset_api.py b/server/szurubooru/api/password_reset_api.py index 5296d23..e0e31b7 100644 --- a/server/szurubooru/api/password_reset_api.py +++ b/server/szurubooru/api/password_reset_api.py @@ -1,60 +1,65 @@ +from hashlib import md5 from typing import Dict + from szurubooru import config, errors, rest from szurubooru.func import auth, mailer, users, versions -from hashlib import md5 - -MAIL_SUBJECT = 'Password reset for {name}' +MAIL_SUBJECT = "Password reset for {name}" MAIL_BODY = ( - 'You (or someone else) requested to reset your password on {name}.\n' - 'If you wish to proceed, click this link: {url}\n' - 'Otherwise, please ignore this email.') + "You (or someone else) requested to reset your password on {name}.\n" + "If you wish to proceed, click this link: {url}\n" + "Otherwise, please ignore this email." +) -@rest.routes.get('/password-reset/(?P[^/]+)/?') +@rest.routes.get("/password-reset/(?P[^/]+)/?") def start_password_reset( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - user_name = params['user_name'] + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + user_name = params["user_name"] user = users.get_user_by_name_or_email(user_name) if not user.email: raise errors.ValidationError( - 'User %r hasn\'t supplied email. Cannot reset password.' % ( - user_name)) + "User %r hasn't supplied email. Cannot reset password." + % (user_name) + ) token = auth.generate_authentication_token(user) - if config.config['domain']: - url = config.config['domain'] - elif 'HTTP_ORIGIN' in ctx.env: - url = ctx.env['HTTP_ORIGIN'].rstrip('/') - elif 'HTTP_REFERER' in ctx.env: - url = ctx.env['HTTP_REFERER'].rstrip('/') + if config.config["domain"]: + url = config.config["domain"] + elif "HTTP_ORIGIN" in ctx.env: + url = ctx.env["HTTP_ORIGIN"].rstrip("/") + elif "HTTP_REFERER" in ctx.env: + url = ctx.env["HTTP_REFERER"].rstrip("/") else: - url = '' - url += '/password-reset/%s:%s' % (user.name, token) + url = "" + url += "/password-reset/%s:%s" % (user.name, token) mailer.send_mail( - config.config['smtp']['from'], + config.config["smtp"]["from"], user.email, - MAIL_SUBJECT.format(name=config.config['name']), - MAIL_BODY.format(name=config.config['name'], url=url)) + MAIL_SUBJECT.format(name=config.config["name"]), + MAIL_BODY.format(name=config.config["name"], url=url), + ) return {} def _hash(token: str) -> str: - return md5(token.encode('utf-8')).hexdigest() + return md5(token.encode("utf-8")).hexdigest() -@rest.routes.post('/password-reset/(?P[^/]+)/?') +@rest.routes.post("/password-reset/(?P[^/]+)/?") def finish_password_reset( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - user_name = params['user_name'] + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + user_name = params["user_name"] user = users.get_user_by_name_or_email(user_name) good_token = auth.generate_authentication_token(user) - token = ctx.get_param_as_string('token') + token = ctx.get_param_as_string("token") if _hash(token) != _hash(good_token): - raise errors.ValidationError('Invalid password reset token.') + raise errors.ValidationError("Invalid password reset token.") new_password = users.reset_user_password(user) versions.bump_version(user) ctx.session.commit() - return {'password': new_password} + return {"password": new_password} diff --git a/server/szurubooru/api/pool_api.py b/server/szurubooru/api/pool_api.py index 9627ed2..a2fb716 100644 --- a/server/szurubooru/api/pool_api.py +++ b/server/szurubooru/api/pool_api.py @@ -1,38 +1,42 @@ -from typing import Optional, List, Dict from datetime import datetime -from szurubooru import db, model, search, rest -from szurubooru.func import auth, pools, snapshots, serialization, versions +from typing import Dict, List, Optional +from szurubooru import db, model, rest, search +from szurubooru.func import auth, pools, serialization, snapshots, versions _search_executor = search.Executor(search.configs.PoolSearchConfig()) def _serialize(ctx: rest.Context, pool: model.Pool) -> rest.Response: return pools.serialize_pool( - pool, options=serialization.get_serialization_options(ctx)) + pool, options=serialization.get_serialization_options(ctx) + ) 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"]) -@rest.routes.get('/pools/?') +@rest.routes.get("/pools/?") def get_pools( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'pools:list') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "pools:list") return _search_executor.execute_and_serialize( - ctx, lambda pool: _serialize(ctx, pool)) + ctx, lambda pool: _serialize(ctx, pool) + ) -@rest.routes.post('/pool/?') +@rest.routes.post("/pool/?") def create_pool( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'pools:create') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "pools:create") - names = ctx.get_param_as_string_list('names') - category = ctx.get_param_as_string('category') - description = ctx.get_param_as_string('description', default='') - posts = ctx.get_param_as_int_list('posts', default=[]) + names = ctx.get_param_as_string_list("names") + category = ctx.get_param_as_string("category") + description = ctx.get_param_as_string("description", default="") + posts = ctx.get_param_as_int_list("posts", default=[]) pool = pools.create_pool(names, category, posts) pool.last_edit_time = datetime.utcnow() @@ -44,32 +48,34 @@ def create_pool( return _serialize(ctx, pool) -@rest.routes.get('/pool/(?P[^/]+)/?') +@rest.routes.get("/pool/(?P[^/]+)/?") def get_pool(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'pools:view') + auth.verify_privilege(ctx.user, "pools:view") pool = _get_pool(params) return _serialize(ctx, pool) -@rest.routes.put('/pool/(?P[^/]+)/?') +@rest.routes.put("/pool/(?P[^/]+)/?") def update_pool(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: pool = _get_pool(params) versions.verify_version(pool, ctx) versions.bump_version(pool) - if ctx.has_param('names'): - auth.verify_privilege(ctx.user, 'pools:edit:names') - pools.update_pool_names(pool, ctx.get_param_as_string_list('names')) - if ctx.has_param('category'): - auth.verify_privilege(ctx.user, 'pools:edit:category') + if ctx.has_param("names"): + auth.verify_privilege(ctx.user, "pools:edit:names") + pools.update_pool_names(pool, ctx.get_param_as_string_list("names")) + if ctx.has_param("category"): + auth.verify_privilege(ctx.user, "pools:edit:category") pools.update_pool_category_name( - pool, ctx.get_param_as_string('category')) - if ctx.has_param('description'): - auth.verify_privilege(ctx.user, 'pools:edit:description') + pool, ctx.get_param_as_string("category") + ) + if ctx.has_param("description"): + auth.verify_privilege(ctx.user, "pools:edit:description") pools.update_pool_description( - pool, ctx.get_param_as_string('description')) - if ctx.has_param('posts'): - auth.verify_privilege(ctx.user, 'pools:edit:posts') - posts = ctx.get_param_as_int_list('posts') + pool, ctx.get_param_as_string("description") + ) + 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() @@ -78,28 +84,29 @@ def update_pool(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: return _serialize(ctx, pool) -@rest.routes.delete('/pool/(?P[^/]+)/?') +@rest.routes.delete("/pool/(?P[^/]+)/?") def delete_pool(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: pool = _get_pool(params) versions.verify_version(pool, ctx) - auth.verify_privilege(ctx.user, 'pools:delete') + auth.verify_privilege(ctx.user, "pools:delete") snapshots.delete(pool, ctx.user) pools.delete(pool) ctx.session.commit() return {} -@rest.routes.post('/pool-merge/?') +@rest.routes.post("/pool-merge/?") def merge_pools( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - source_pool_id = ctx.get_param_as_string('remove') - target_pool_id = ctx.get_param_as_string('mergeTo') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + source_pool_id = ctx.get_param_as_string("remove") + target_pool_id = ctx.get_param_as_string("mergeTo") source_pool = pools.get_pool_by_id(source_pool_id) target_pool = pools.get_pool_by_id(target_pool_id) - versions.verify_version(source_pool, ctx, 'removeVersion') - versions.verify_version(target_pool, ctx, 'mergeToVersion') + versions.verify_version(source_pool, ctx, "removeVersion") + versions.verify_version(target_pool, ctx, "mergeToVersion") versions.bump_version(target_pool) - auth.verify_privilege(ctx.user, 'pools:merge') + auth.verify_privilege(ctx.user, "pools:merge") pools.merge_pools(source_pool, target_pool) snapshots.merge(source_pool, target_pool, ctx.user) ctx.session.commit() diff --git a/server/szurubooru/api/pool_category_api.py b/server/szurubooru/api/pool_category_api.py index f293724..9af41d4 100644 --- a/server/szurubooru/api/pool_category_api.py +++ b/server/szurubooru/api/pool_category_api.py @@ -1,31 +1,42 @@ from typing import Dict + from szurubooru import model, rest from szurubooru.func import ( - auth, pools, pool_categories, snapshots, serialization, versions) + auth, + pool_categories, + pools, + serialization, + snapshots, + versions, +) def _serialize( - ctx: rest.Context, category: model.PoolCategory) -> rest.Response: + ctx: rest.Context, category: model.PoolCategory +) -> rest.Response: return pool_categories.serialize_category( - category, options=serialization.get_serialization_options(ctx)) + category, options=serialization.get_serialization_options(ctx) + ) -@rest.routes.get('/pool-categories/?') +@rest.routes.get("/pool-categories/?") def get_pool_categories( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'pool_categories:list') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "pool_categories:list") categories = pool_categories.get_all_categories() return { - 'results': [_serialize(ctx, category) for category in categories], + "results": [_serialize(ctx, category) for category in categories], } -@rest.routes.post('/pool-categories/?') +@rest.routes.post("/pool-categories/?") def create_pool_category( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'pool_categories:create') - name = ctx.get_param_as_string('name') - color = ctx.get_param_as_string('color') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "pool_categories:create") + name = ctx.get_param_as_string("name") + color = ctx.get_param_as_string("color") category = pool_categories.create_category(name, color) ctx.session.add(category) ctx.session.flush() @@ -34,54 +45,63 @@ def create_pool_category( return _serialize(ctx, category) -@rest.routes.get('/pool-category/(?P[^/]+)/?') +@rest.routes.get("/pool-category/(?P[^/]+)/?") def get_pool_category( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'pool_categories:view') - category = pool_categories.get_category_by_name(params['category_name']) + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "pool_categories:view") + category = pool_categories.get_category_by_name(params["category_name"]) return _serialize(ctx, category) -@rest.routes.put('/pool-category/(?P[^/]+)/?') +@rest.routes.put("/pool-category/(?P[^/]+)/?") def update_pool_category( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: category = pool_categories.get_category_by_name( - params['category_name'], lock=True) + params["category_name"], lock=True + ) versions.verify_version(category, ctx) versions.bump_version(category) - if ctx.has_param('name'): - auth.verify_privilege(ctx.user, 'pool_categories:edit:name') + if ctx.has_param("name"): + auth.verify_privilege(ctx.user, "pool_categories:edit:name") pool_categories.update_category_name( - category, ctx.get_param_as_string('name')) - if ctx.has_param('color'): - auth.verify_privilege(ctx.user, 'pool_categories:edit:color') + category, ctx.get_param_as_string("name") + ) + if ctx.has_param("color"): + auth.verify_privilege(ctx.user, "pool_categories:edit:color") pool_categories.update_category_color( - category, ctx.get_param_as_string('color')) + category, ctx.get_param_as_string("color") + ) ctx.session.flush() snapshots.modify(category, ctx.user) ctx.session.commit() return _serialize(ctx, category) -@rest.routes.delete('/pool-category/(?P[^/]+)/?') +@rest.routes.delete("/pool-category/(?P[^/]+)/?") def delete_pool_category( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: category = pool_categories.get_category_by_name( - params['category_name'], lock=True) + params["category_name"], lock=True + ) versions.verify_version(category, ctx) - auth.verify_privilege(ctx.user, 'pool_categories:delete') + auth.verify_privilege(ctx.user, "pool_categories:delete") pool_categories.delete_category(category) snapshots.delete(category, ctx.user) ctx.session.commit() return {} -@rest.routes.put('/pool-category/(?P[^/]+)/default/?') +@rest.routes.put("/pool-category/(?P[^/]+)/default/?") def set_pool_category_as_default( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'pool_categories:set_default') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "pool_categories:set_default") category = pool_categories.get_category_by_name( - params['category_name'], lock=True) + params["category_name"], lock=True + ) pool_categories.set_default_category(category) ctx.session.flush() snapshots.modify(category, ctx.user) diff --git a/server/szurubooru/api/post_api.py b/server/szurubooru/api/post_api.py index 6d48fbd..bf40bc0 100644 --- a/server/szurubooru/api/post_api.py +++ b/server/szurubooru/api/post_api.py @@ -1,10 +1,18 @@ -from typing import Optional, Dict, List from datetime import datetime -from szurubooru import db, model, errors, rest, search -from szurubooru.func import ( - auth, tags, posts, snapshots, favorites, scores, - serialization, versions, mime) +from typing import Dict, List, Optional +from szurubooru import db, errors, model, rest, search +from szurubooru.func import ( + auth, + favorites, + mime, + posts, + scores, + serialization, + snapshots, + tags, + versions, +) _search_executor_config = search.configs.PostSearchConfig() _search_executor = search.Executor(_search_executor_config) @@ -12,10 +20,11 @@ _search_executor = search.Executor(_search_executor_config) def _get_post_id(params: Dict[str, str]) -> int: try: - return int(params['post_id']) + return int(params["post_id"]) except TypeError: raise posts.InvalidPostIdError( - 'Invalid post ID: %r.' % params['post_id']) + "Invalid post ID: %r." % params["post_id"] + ) def _get_post(params: Dict[str, str]) -> model.Post: @@ -23,56 +32,62 @@ def _get_post(params: Dict[str, str]) -> model.Post: def _serialize_post( - ctx: rest.Context, post: Optional[model.Post]) -> rest.Response: + ctx: rest.Context, post: Optional[model.Post] +) -> rest.Response: return posts.serialize_post( - post, - ctx.user, - options=serialization.get_serialization_options(ctx)) + post, ctx.user, options=serialization.get_serialization_options(ctx) + ) -@rest.routes.get('/posts/?') +@rest.routes.get("/posts/?") def get_posts( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:list') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:list") _search_executor_config.user = ctx.user return _search_executor.execute_and_serialize( - ctx, lambda post: _serialize_post(ctx, post)) + ctx, lambda post: _serialize_post(ctx, post) + ) -@rest.routes.post('/posts/?') +@rest.routes.post("/posts/?") def create_post( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - anonymous = ctx.get_param_as_bool('anonymous', default=False) + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + anonymous = ctx.get_param_as_bool("anonymous", default=False) if anonymous: - auth.verify_privilege(ctx.user, 'posts:create:anonymous') + auth.verify_privilege(ctx.user, "posts:create:anonymous") else: - auth.verify_privilege(ctx.user, 'posts:create:identified') + auth.verify_privilege(ctx.user, "posts:create:identified") content = ctx.get_file( - 'content', + "content", use_video_downloader=auth.has_privilege( - ctx.user, 'uploads:use_downloader')) - tag_names = ctx.get_param_as_string_list('tags', default=[]) - safety = ctx.get_param_as_string('safety') - source = ctx.get_param_as_string('source', default='') - if ctx.has_param('contentUrl') and not source: - source = ctx.get_param_as_string('contentUrl', default='') - relations = ctx.get_param_as_int_list('relations', default=[]) - notes = ctx.get_param_as_list('notes', default=[]) + ctx.user, "uploads:use_downloader" + ), + ) + tag_names = ctx.get_param_as_string_list("tags", default=[]) + safety = ctx.get_param_as_string("safety") + source = ctx.get_param_as_string("source", default="") + if ctx.has_param("contentUrl") and not source: + source = ctx.get_param_as_string("contentUrl", default="") + relations = ctx.get_param_as_int_list("relations", default=[]) + notes = ctx.get_param_as_list("notes", default=[]) flags = ctx.get_param_as_string_list( - 'flags', - default=posts.get_default_flags(content)) + "flags", default=posts.get_default_flags(content) + ) post, new_tags = posts.create_post( - content, tag_names, None if anonymous else ctx.user) + content, tag_names, None if anonymous else ctx.user + ) if len(new_tags): - auth.verify_privilege(ctx.user, 'tags:create') + auth.verify_privilege(ctx.user, "tags:create") posts.update_post_safety(post, safety) posts.update_post_source(post, source) posts.update_post_relations(post, relations) posts.update_post_notes(post, notes) posts.update_post_flags(post, flags) - if ctx.has_file('thumbnail'): - posts.update_post_thumbnail(post, ctx.get_file('thumbnail')) + if ctx.has_file("thumbnail"): + posts.update_post_thumbnail(post, ctx.get_file("thumbnail")) ctx.session.add(post) ctx.session.flush() create_snapshots_for_post(post, new_tags, None if anonymous else ctx.user) @@ -81,68 +96,75 @@ def create_post( create_snapshots_for_post( alternate_post, alternate_post_new_tags, - None if anonymous else ctx.user) + None if anonymous else ctx.user, + ) ctx.session.commit() return _serialize_post(ctx, post) def create_snapshots_for_post( - post: model.Post, - new_tags: List[model.Tag], - user: Optional[model.User]): + post: model.Post, new_tags: List[model.Tag], user: Optional[model.User] +): snapshots.create(post, user) for tag in new_tags: snapshots.create(tag, user) -@rest.routes.get('/post/(?P[^/]+)/?') +@rest.routes.get("/post/(?P[^/]+)/?") def get_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:view') + auth.verify_privilege(ctx.user, "posts:view") post = _get_post(params) return _serialize_post(ctx, post) -@rest.routes.put('/post/(?P[^/]+)/?') +@rest.routes.put("/post/(?P[^/]+)/?") def update_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: post = _get_post(params) versions.verify_version(post, ctx) versions.bump_version(post) - if ctx.has_file('content'): - auth.verify_privilege(ctx.user, 'posts:edit:content') + if ctx.has_file("content"): + auth.verify_privilege(ctx.user, "posts:edit:content") posts.update_post_content( post, - ctx.get_file('content', use_video_downloader=auth.has_privilege( - ctx.user, 'uploads:use_downloader'))) - if ctx.has_param('tags'): - auth.verify_privilege(ctx.user, 'posts:edit:tags') + ctx.get_file( + "content", + use_video_downloader=auth.has_privilege( + ctx.user, "uploads:use_downloader" + ), + ), + ) + if ctx.has_param("tags"): + auth.verify_privilege(ctx.user, "posts:edit:tags") new_tags = posts.update_post_tags( - post, ctx.get_param_as_string_list('tags')) + post, ctx.get_param_as_string_list("tags") + ) if len(new_tags): - auth.verify_privilege(ctx.user, 'tags:create') + auth.verify_privilege(ctx.user, "tags:create") db.session.flush() for tag in new_tags: snapshots.create(tag, ctx.user) - if ctx.has_param('safety'): - auth.verify_privilege(ctx.user, 'posts:edit:safety') - posts.update_post_safety(post, ctx.get_param_as_string('safety')) - if ctx.has_param('source'): - auth.verify_privilege(ctx.user, 'posts:edit:source') - posts.update_post_source(post, ctx.get_param_as_string('source')) - elif ctx.has_param('contentUrl'): - posts.update_post_source(post, ctx.get_param_as_string('contentUrl')) - if ctx.has_param('relations'): - auth.verify_privilege(ctx.user, 'posts:edit:relations') + if ctx.has_param("safety"): + auth.verify_privilege(ctx.user, "posts:edit:safety") + posts.update_post_safety(post, ctx.get_param_as_string("safety")) + if ctx.has_param("source"): + auth.verify_privilege(ctx.user, "posts:edit:source") + posts.update_post_source(post, ctx.get_param_as_string("source")) + elif ctx.has_param("contentUrl"): + posts.update_post_source(post, ctx.get_param_as_string("contentUrl")) + if ctx.has_param("relations"): + auth.verify_privilege(ctx.user, "posts:edit:relations") posts.update_post_relations( - post, ctx.get_param_as_int_list('relations')) - if ctx.has_param('notes'): - auth.verify_privilege(ctx.user, 'posts:edit:notes') - posts.update_post_notes(post, ctx.get_param_as_list('notes')) - if ctx.has_param('flags'): - auth.verify_privilege(ctx.user, 'posts:edit:flags') - posts.update_post_flags(post, ctx.get_param_as_string_list('flags')) - if ctx.has_file('thumbnail'): - auth.verify_privilege(ctx.user, 'posts:edit:thumbnail') - posts.update_post_thumbnail(post, ctx.get_file('thumbnail')) + post, ctx.get_param_as_int_list("relations") + ) + if ctx.has_param("notes"): + auth.verify_privilege(ctx.user, "posts:edit:notes") + posts.update_post_notes(post, ctx.get_param_as_list("notes")) + if ctx.has_param("flags"): + auth.verify_privilege(ctx.user, "posts:edit:flags") + posts.update_post_flags(post, ctx.get_param_as_string_list("flags")) + if ctx.has_file("thumbnail"): + auth.verify_privilege(ctx.user, "posts:edit:thumbnail") + posts.update_post_thumbnail(post, ctx.get_file("thumbnail")) post.last_edit_time = datetime.utcnow() ctx.session.flush() snapshots.modify(post, ctx.user) @@ -150,9 +172,9 @@ def update_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: return _serialize_post(ctx, post) -@rest.routes.delete('/post/(?P[^/]+)/?') +@rest.routes.delete("/post/(?P[^/]+)/?") def delete_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:delete') + auth.verify_privilege(ctx.user, "posts:delete") post = _get_post(params) versions.verify_version(post, ctx) snapshots.delete(post, ctx.user) @@ -161,103 +183,113 @@ def delete_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: return {} -@rest.routes.post('/post-merge/?') +@rest.routes.post("/post-merge/?") def merge_posts( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - source_post_id = ctx.get_param_as_int('remove') - target_post_id = ctx.get_param_as_int('mergeTo') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + source_post_id = ctx.get_param_as_int("remove") + target_post_id = ctx.get_param_as_int("mergeTo") source_post = posts.get_post_by_id(source_post_id) target_post = posts.get_post_by_id(target_post_id) - replace_content = ctx.get_param_as_bool('replaceContent') - versions.verify_version(source_post, ctx, 'removeVersion') - versions.verify_version(target_post, ctx, 'mergeToVersion') + replace_content = ctx.get_param_as_bool("replaceContent") + versions.verify_version(source_post, ctx, "removeVersion") + versions.verify_version(target_post, ctx, "mergeToVersion") versions.bump_version(target_post) - auth.verify_privilege(ctx.user, 'posts:merge') + auth.verify_privilege(ctx.user, "posts:merge") posts.merge_posts(source_post, target_post, replace_content) snapshots.merge(source_post, target_post, ctx.user) ctx.session.commit() return _serialize_post(ctx, target_post) -@rest.routes.get('/featured-post/?') +@rest.routes.get("/featured-post/?") def get_featured_post( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:view:featured') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:view:featured") post = posts.try_get_featured_post() return _serialize_post(ctx, post) -@rest.routes.post('/featured-post/?') +@rest.routes.post("/featured-post/?") def set_featured_post( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:feature') - post_id = ctx.get_param_as_int('id') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:feature") + post_id = ctx.get_param_as_int("id") post = posts.get_post_by_id(post_id) featured_post = posts.try_get_featured_post() if featured_post and featured_post.post_id == post.post_id: raise posts.PostAlreadyFeaturedError( - 'Post %r is already featured.' % post_id) + "Post %r is already featured." % post_id + ) posts.feature_post(post, ctx.user) snapshots.modify(post, ctx.user) ctx.session.commit() return _serialize_post(ctx, post) -@rest.routes.put('/post/(?P[^/]+)/score/?') +@rest.routes.put("/post/(?P[^/]+)/score/?") def set_post_score(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:score') + auth.verify_privilege(ctx.user, "posts:score") post = _get_post(params) - score = ctx.get_param_as_int('score') + score = ctx.get_param_as_int("score") scores.set_score(post, ctx.user, score) ctx.session.commit() return _serialize_post(ctx, post) -@rest.routes.delete('/post/(?P[^/]+)/score/?') +@rest.routes.delete("/post/(?P[^/]+)/score/?") def delete_post_score( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:score') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:score") post = _get_post(params) scores.delete_score(post, ctx.user) ctx.session.commit() return _serialize_post(ctx, post) -@rest.routes.post('/post/(?P[^/]+)/favorite/?') +@rest.routes.post("/post/(?P[^/]+)/favorite/?") def add_post_to_favorites( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:favorite') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:favorite") post = _get_post(params) favorites.set_favorite(post, ctx.user) ctx.session.commit() return _serialize_post(ctx, post) -@rest.routes.delete('/post/(?P[^/]+)/favorite/?') +@rest.routes.delete("/post/(?P[^/]+)/favorite/?") def delete_post_from_favorites( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:favorite') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:favorite") post = _get_post(params) favorites.unset_favorite(post, ctx.user) ctx.session.commit() return _serialize_post(ctx, post) -@rest.routes.get('/post/(?P[^/]+)/around/?') +@rest.routes.get("/post/(?P[^/]+)/around/?") def get_posts_around( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:list') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:list") _search_executor_config.user = ctx.user post_id = _get_post_id(params) return _search_executor.get_around_and_serialize( - ctx, post_id, lambda post: _serialize_post(ctx, post)) + ctx, post_id, lambda post: _serialize_post(ctx, post) + ) -@rest.routes.post('/posts/reverse-search/?') +@rest.routes.post("/posts/reverse-search/?") def get_posts_by_image( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:reverse_search') - content = ctx.get_file('content') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:reverse_search") + content = ctx.get_file("content") try: lookalikes = posts.search_by_image(content) @@ -265,14 +297,11 @@ def get_posts_by_image( lookalikes = [] return { - 'exactPost': - _serialize_post(ctx, posts.search_by_image_exact(content)), - 'similarPosts': - [ - { - 'distance': distance, - 'post': _serialize_post(ctx, post), - } - for distance, post in lookalikes - ], + "exactPost": _serialize_post( + ctx, posts.search_by_image_exact(content) + ), + "similarPosts": [ + {"distance": distance, "post": _serialize_post(ctx, post),} + for distance, post in lookalikes + ], } diff --git a/server/szurubooru/api/snapshot_api.py b/server/szurubooru/api/snapshot_api.py index 469be7f..87012a2 100644 --- a/server/szurubooru/api/snapshot_api.py +++ b/server/szurubooru/api/snapshot_api.py @@ -1,14 +1,16 @@ from typing import Dict -from szurubooru import search, rest -from szurubooru.func import auth, snapshots +from szurubooru import rest, search +from szurubooru.func import auth, snapshots _search_executor = search.Executor(search.configs.SnapshotSearchConfig()) -@rest.routes.get('/snapshots/?') +@rest.routes.get("/snapshots/?") def get_snapshots( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'snapshots:list') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "snapshots:list") return _search_executor.execute_and_serialize( - ctx, lambda snapshot: snapshots.serialize_snapshot(snapshot, ctx.user)) + ctx, lambda snapshot: snapshots.serialize_snapshot(snapshot, ctx.user) + ) diff --git a/server/szurubooru/api/tag_api.py b/server/szurubooru/api/tag_api.py index f9a15e7..6b4c807 100644 --- a/server/szurubooru/api/tag_api.py +++ b/server/szurubooru/api/tag_api.py @@ -1,19 +1,20 @@ -from typing import Optional, List, Dict from datetime import datetime -from szurubooru import db, model, search, rest -from szurubooru.func import auth, tags, snapshots, serialization, versions +from typing import Dict, List, Optional +from szurubooru import db, model, rest, search +from szurubooru.func import auth, serialization, snapshots, tags, versions _search_executor = search.Executor(search.configs.TagSearchConfig()) def _serialize(ctx: rest.Context, tag: model.Tag) -> rest.Response: return tags.serialize_tag( - tag, options=serialization.get_serialization_options(ctx)) + tag, options=serialization.get_serialization_options(ctx) + ) def _get_tag(params: Dict[str, str]) -> model.Tag: - return tags.get_tag_by_name(params['tag_name']) + return tags.get_tag_by_name(params["tag_name"]) def _create_if_needed(tag_names: List[str], user: model.User) -> None: @@ -21,29 +22,31 @@ def _create_if_needed(tag_names: List[str], user: model.User) -> None: return _existing_tags, new_tags = tags.get_or_create_tags_by_names(tag_names) if len(new_tags): - auth.verify_privilege(user, 'tags:create') + auth.verify_privilege(user, "tags:create") db.session.flush() for tag in new_tags: snapshots.create(tag, user) -@rest.routes.get('/tags/?') +@rest.routes.get("/tags/?") def get_tags(ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'tags:list') + auth.verify_privilege(ctx.user, "tags:list") return _search_executor.execute_and_serialize( - ctx, lambda tag: _serialize(ctx, tag)) + ctx, lambda tag: _serialize(ctx, tag) + ) -@rest.routes.post('/tags/?') +@rest.routes.post("/tags/?") def create_tag( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'tags:create') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "tags:create") - names = ctx.get_param_as_string_list('names') - category = ctx.get_param_as_string('category') - description = ctx.get_param_as_string('description', default='') - suggestions = ctx.get_param_as_string_list('suggestions', default=[]) - implications = ctx.get_param_as_string_list('implications', default=[]) + names = ctx.get_param_as_string_list("names") + category = ctx.get_param_as_string("category") + description = ctx.get_param_as_string("description", 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) _create_if_needed(implications, ctx.user) @@ -57,37 +60,37 @@ def create_tag( return _serialize(ctx, tag) -@rest.routes.get('/tag/(?P.+)') +@rest.routes.get("/tag/(?P.+)") def get_tag(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'tags:view') + auth.verify_privilege(ctx.user, "tags:view") tag = _get_tag(params) return _serialize(ctx, tag) -@rest.routes.put('/tag/(?P.+)') +@rest.routes.put("/tag/(?P.+)") def update_tag(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: tag = _get_tag(params) versions.verify_version(tag, ctx) versions.bump_version(tag) - if ctx.has_param('names'): - auth.verify_privilege(ctx.user, 'tags:edit:names') - tags.update_tag_names(tag, ctx.get_param_as_string_list('names')) - if ctx.has_param('category'): - auth.verify_privilege(ctx.user, 'tags:edit:category') - tags.update_tag_category_name( - tag, ctx.get_param_as_string('category')) - if ctx.has_param('description'): - auth.verify_privilege(ctx.user, 'tags:edit:description') + if ctx.has_param("names"): + auth.verify_privilege(ctx.user, "tags:edit:names") + tags.update_tag_names(tag, ctx.get_param_as_string_list("names")) + if ctx.has_param("category"): + auth.verify_privilege(ctx.user, "tags:edit:category") + tags.update_tag_category_name(tag, ctx.get_param_as_string("category")) + if ctx.has_param("description"): + auth.verify_privilege(ctx.user, "tags:edit:description") tags.update_tag_description( - tag, ctx.get_param_as_string('description')) - if ctx.has_param('suggestions'): - auth.verify_privilege(ctx.user, 'tags:edit:suggestions') - suggestions = ctx.get_param_as_string_list('suggestions') + tag, ctx.get_param_as_string("description") + ) + if ctx.has_param("suggestions"): + auth.verify_privilege(ctx.user, "tags:edit:suggestions") + suggestions = ctx.get_param_as_string_list("suggestions") _create_if_needed(suggestions, ctx.user) tags.update_tag_suggestions(tag, suggestions) - if ctx.has_param('implications'): - auth.verify_privilege(ctx.user, 'tags:edit:implications') - implications = ctx.get_param_as_string_list('implications') + if ctx.has_param("implications"): + auth.verify_privilege(ctx.user, "tags:edit:implications") + implications = ctx.get_param_as_string_list("implications") _create_if_needed(implications, ctx.user) tags.update_tag_implications(tag, implications) tag.last_edit_time = datetime.utcnow() @@ -97,44 +100,45 @@ def update_tag(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: return _serialize(ctx, tag) -@rest.routes.delete('/tag/(?P.+)') +@rest.routes.delete("/tag/(?P.+)") def delete_tag(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: tag = _get_tag(params) versions.verify_version(tag, ctx) - auth.verify_privilege(ctx.user, 'tags:delete') + auth.verify_privilege(ctx.user, "tags:delete") snapshots.delete(tag, ctx.user) tags.delete(tag) ctx.session.commit() return {} -@rest.routes.post('/tag-merge/?') +@rest.routes.post("/tag-merge/?") def merge_tags( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - source_tag_name = ctx.get_param_as_string('remove') - target_tag_name = ctx.get_param_as_string('mergeTo') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + source_tag_name = ctx.get_param_as_string("remove") + target_tag_name = ctx.get_param_as_string("mergeTo") source_tag = tags.get_tag_by_name(source_tag_name) target_tag = tags.get_tag_by_name(target_tag_name) - versions.verify_version(source_tag, ctx, 'removeVersion') - versions.verify_version(target_tag, ctx, 'mergeToVersion') + versions.verify_version(source_tag, ctx, "removeVersion") + versions.verify_version(target_tag, ctx, "mergeToVersion") versions.bump_version(target_tag) - auth.verify_privilege(ctx.user, 'tags:merge') + auth.verify_privilege(ctx.user, "tags:merge") tags.merge_tags(source_tag, target_tag) snapshots.merge(source_tag, target_tag, ctx.user) ctx.session.commit() return _serialize(ctx, target_tag) -@rest.routes.get('/tag-siblings/(?P.+)') +@rest.routes.get("/tag-siblings/(?P.+)") def get_tag_siblings( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'tags:view') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "tags:view") tag = _get_tag(params) result = tags.get_tag_siblings(tag) serialized_siblings = [] for sibling, occurrences in result: - serialized_siblings.append({ - 'tag': _serialize(ctx, sibling), - 'occurrences': occurrences - }) - return {'results': serialized_siblings} + serialized_siblings.append( + {"tag": _serialize(ctx, sibling), "occurrences": occurrences} + ) + return {"results": serialized_siblings} diff --git a/server/szurubooru/api/tag_category_api.py b/server/szurubooru/api/tag_category_api.py index 07da999..498289a 100644 --- a/server/szurubooru/api/tag_category_api.py +++ b/server/szurubooru/api/tag_category_api.py @@ -1,31 +1,42 @@ from typing import Dict + from szurubooru import model, rest from szurubooru.func import ( - auth, tags, tag_categories, snapshots, serialization, versions) + auth, + serialization, + snapshots, + tag_categories, + tags, + versions, +) def _serialize( - ctx: rest.Context, category: model.TagCategory) -> rest.Response: + ctx: rest.Context, category: model.TagCategory +) -> rest.Response: return tag_categories.serialize_category( - category, options=serialization.get_serialization_options(ctx)) + category, options=serialization.get_serialization_options(ctx) + ) -@rest.routes.get('/tag-categories/?') +@rest.routes.get("/tag-categories/?") def get_tag_categories( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'tag_categories:list') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "tag_categories:list") categories = tag_categories.get_all_categories() return { - 'results': [_serialize(ctx, category) for category in categories], + "results": [_serialize(ctx, category) for category in categories], } -@rest.routes.post('/tag-categories/?') +@rest.routes.post("/tag-categories/?") def create_tag_category( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'tag_categories:create') - name = ctx.get_param_as_string('name') - color = ctx.get_param_as_string('color') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "tag_categories:create") + name = ctx.get_param_as_string("name") + color = ctx.get_param_as_string("color") category = tag_categories.create_category(name, color) ctx.session.add(category) ctx.session.flush() @@ -34,54 +45,63 @@ def create_tag_category( return _serialize(ctx, category) -@rest.routes.get('/tag-category/(?P[^/]+)/?') +@rest.routes.get("/tag-category/(?P[^/]+)/?") def get_tag_category( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'tag_categories:view') - category = tag_categories.get_category_by_name(params['category_name']) + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "tag_categories:view") + category = tag_categories.get_category_by_name(params["category_name"]) return _serialize(ctx, category) -@rest.routes.put('/tag-category/(?P[^/]+)/?') +@rest.routes.put("/tag-category/(?P[^/]+)/?") def update_tag_category( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: category = tag_categories.get_category_by_name( - params['category_name'], lock=True) + params["category_name"], lock=True + ) versions.verify_version(category, ctx) versions.bump_version(category) - if ctx.has_param('name'): - auth.verify_privilege(ctx.user, 'tag_categories:edit:name') + if ctx.has_param("name"): + auth.verify_privilege(ctx.user, "tag_categories:edit:name") tag_categories.update_category_name( - category, ctx.get_param_as_string('name')) - if ctx.has_param('color'): - auth.verify_privilege(ctx.user, 'tag_categories:edit:color') + category, ctx.get_param_as_string("name") + ) + if ctx.has_param("color"): + auth.verify_privilege(ctx.user, "tag_categories:edit:color") tag_categories.update_category_color( - category, ctx.get_param_as_string('color')) + category, ctx.get_param_as_string("color") + ) ctx.session.flush() snapshots.modify(category, ctx.user) ctx.session.commit() return _serialize(ctx, category) -@rest.routes.delete('/tag-category/(?P[^/]+)/?') +@rest.routes.delete("/tag-category/(?P[^/]+)/?") def delete_tag_category( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: category = tag_categories.get_category_by_name( - params['category_name'], lock=True) + params["category_name"], lock=True + ) versions.verify_version(category, ctx) - auth.verify_privilege(ctx.user, 'tag_categories:delete') + auth.verify_privilege(ctx.user, "tag_categories:delete") tag_categories.delete_category(category) snapshots.delete(category, ctx.user) ctx.session.commit() return {} -@rest.routes.put('/tag-category/(?P[^/]+)/default/?') +@rest.routes.put("/tag-category/(?P[^/]+)/default/?") def set_tag_category_as_default( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'tag_categories:set_default') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "tag_categories:set_default") category = tag_categories.get_category_by_name( - params['category_name'], lock=True) + params["category_name"], lock=True + ) tag_categories.set_default_category(category) ctx.session.flush() snapshots.modify(category, ctx.user) diff --git a/server/szurubooru/api/upload_api.py b/server/szurubooru/api/upload_api.py index 16b4640..3b7bca8 100644 --- a/server/szurubooru/api/upload_api.py +++ b/server/szurubooru/api/upload_api.py @@ -1,16 +1,20 @@ from typing import Dict + from szurubooru import rest from szurubooru.func import auth, file_uploads -@rest.routes.post('/uploads/?') +@rest.routes.post("/uploads/?") def create_temporary_file( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'uploads:create') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "uploads:create") content = ctx.get_file( - 'content', + "content", allow_tokens=False, use_video_downloader=auth.has_privilege( - ctx.user, 'uploads:use_downloader')) + ctx.user, "uploads:use_downloader" + ), + ) token = file_uploads.save(content) - return {'token': token} + return {"token": token} diff --git a/server/szurubooru/api/user_api.py b/server/szurubooru/api/user_api.py index 5e14fab..a6196cb 100644 --- a/server/szurubooru/api/user_api.py +++ b/server/szurubooru/api/user_api.py @@ -1,97 +1,102 @@ from typing import Any, Dict -from szurubooru import model, search, rest -from szurubooru.func import auth, users, serialization, versions +from szurubooru import model, rest, search +from szurubooru.func import auth, serialization, users, versions _search_executor = search.Executor(search.configs.UserSearchConfig()) def _serialize( - ctx: rest.Context, user: model.User, **kwargs: Any) -> rest.Response: + ctx: rest.Context, user: model.User, **kwargs: Any +) -> rest.Response: return users.serialize_user( user, ctx.user, options=serialization.get_serialization_options(ctx), - **kwargs) + **kwargs + ) -@rest.routes.get('/users/?') +@rest.routes.get("/users/?") def get_users( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'users:list') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "users:list") return _search_executor.execute_and_serialize( - ctx, lambda user: _serialize(ctx, user)) + ctx, lambda user: _serialize(ctx, user) + ) -@rest.routes.post('/users/?') +@rest.routes.post("/users/?") def create_user( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: if ctx.user.user_id is None: - auth.verify_privilege(ctx.user, 'users:create:self') + auth.verify_privilege(ctx.user, "users:create:self") else: - auth.verify_privilege(ctx.user, 'users:create:any') + auth.verify_privilege(ctx.user, "users:create:any") - name = ctx.get_param_as_string('name') - password = ctx.get_param_as_string('password') - email = ctx.get_param_as_string('email', default='') + name = ctx.get_param_as_string("name") + password = ctx.get_param_as_string("password") + email = ctx.get_param_as_string("email", default="") user = users.create_user(name, password, email) - if ctx.has_param('rank'): - users.update_user_rank(user, ctx.get_param_as_string('rank'), ctx.user) - if ctx.has_param('avatarStyle'): + if ctx.has_param("rank"): + users.update_user_rank(user, ctx.get_param_as_string("rank"), ctx.user) + if ctx.has_param("avatarStyle"): users.update_user_avatar( user, - ctx.get_param_as_string('avatarStyle'), - ctx.get_file('avatar', default=b'')) + ctx.get_param_as_string("avatarStyle"), + ctx.get_file("avatar", default=b""), + ) ctx.session.add(user) ctx.session.commit() return _serialize(ctx, user, force_show_email=True) -@rest.routes.get('/user/(?P[^/]+)/?') +@rest.routes.get("/user/(?P[^/]+)/?") def get_user(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - user = users.get_user_by_name(params['user_name']) + user = users.get_user_by_name(params["user_name"]) if ctx.user.user_id != user.user_id: - auth.verify_privilege(ctx.user, 'users:view') + auth.verify_privilege(ctx.user, "users:view") return _serialize(ctx, user) -@rest.routes.put('/user/(?P[^/]+)/?') +@rest.routes.put("/user/(?P[^/]+)/?") def update_user(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - user = users.get_user_by_name(params['user_name']) + user = users.get_user_by_name(params["user_name"]) versions.verify_version(user, ctx) versions.bump_version(user) - infix = 'self' if ctx.user.user_id == user.user_id else 'any' - if ctx.has_param('name'): - auth.verify_privilege(ctx.user, 'users:edit:%s:name' % infix) - users.update_user_name(user, ctx.get_param_as_string('name')) - if ctx.has_param('password'): - auth.verify_privilege(ctx.user, 'users:edit:%s:pass' % infix) - users.update_user_password( - user, ctx.get_param_as_string('password')) - if ctx.has_param('email'): - auth.verify_privilege(ctx.user, 'users:edit:%s:email' % infix) - users.update_user_email(user, ctx.get_param_as_string('email')) - if ctx.has_param('rank'): - auth.verify_privilege(ctx.user, 'users:edit:%s:rank' % infix) - users.update_user_rank( - user, ctx.get_param_as_string('rank'), ctx.user) - if ctx.has_param('avatarStyle'): - auth.verify_privilege(ctx.user, 'users:edit:%s:avatar' % infix) + infix = "self" if ctx.user.user_id == user.user_id else "any" + if ctx.has_param("name"): + auth.verify_privilege(ctx.user, "users:edit:%s:name" % infix) + users.update_user_name(user, ctx.get_param_as_string("name")) + if ctx.has_param("password"): + auth.verify_privilege(ctx.user, "users:edit:%s:pass" % infix) + users.update_user_password(user, ctx.get_param_as_string("password")) + if ctx.has_param("email"): + auth.verify_privilege(ctx.user, "users:edit:%s:email" % infix) + users.update_user_email(user, ctx.get_param_as_string("email")) + if ctx.has_param("rank"): + auth.verify_privilege(ctx.user, "users:edit:%s:rank" % infix) + users.update_user_rank(user, ctx.get_param_as_string("rank"), ctx.user) + if ctx.has_param("avatarStyle"): + auth.verify_privilege(ctx.user, "users:edit:%s:avatar" % infix) users.update_user_avatar( user, - ctx.get_param_as_string('avatarStyle'), - ctx.get_file('avatar', default=b'')) + ctx.get_param_as_string("avatarStyle"), + ctx.get_file("avatar", default=b""), + ) ctx.session.commit() return _serialize(ctx, user) -@rest.routes.delete('/user/(?P[^/]+)/?') +@rest.routes.delete("/user/(?P[^/]+)/?") def delete_user(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - user = users.get_user_by_name(params['user_name']) + user = users.get_user_by_name(params["user_name"]) versions.verify_version(user, ctx) - infix = 'self' if ctx.user.user_id == user.user_id else 'any' - auth.verify_privilege(ctx.user, 'users:delete:%s' % infix) + infix = "self" if ctx.user.user_id == user.user_id else "any" + auth.verify_privilege(ctx.user, "users:delete:%s" % infix) ctx.session.delete(user) ctx.session.commit() return {} diff --git a/server/szurubooru/api/user_token_api.py b/server/szurubooru/api/user_token_api.py index 7739823..772f97a 100644 --- a/server/szurubooru/api/user_token_api.py +++ b/server/szurubooru/api/user_token_api.py @@ -1,82 +1,90 @@ from typing import Dict + from szurubooru import model, rest -from szurubooru.func import auth, users, user_tokens, serialization, versions +from szurubooru.func import auth, serialization, user_tokens, users, versions def _serialize( - ctx: rest.Context, user_token: model.UserToken) -> rest.Response: + ctx: rest.Context, user_token: model.UserToken +) -> rest.Response: return user_tokens.serialize_user_token( user_token, ctx.user, - options=serialization.get_serialization_options(ctx)) + options=serialization.get_serialization_options(ctx), + ) -@rest.routes.get('/user-tokens/(?P[^/]+)/?') +@rest.routes.get("/user-tokens/(?P[^/]+)/?") def get_user_tokens( - ctx: rest.Context, params: Dict[str, str] = {}) -> rest.Response: - user = users.get_user_by_name(params['user_name']) - infix = 'self' if ctx.user.user_id == user.user_id else 'any' - auth.verify_privilege(ctx.user, 'user_tokens:list:%s' % infix) + ctx: rest.Context, params: Dict[str, str] = {} +) -> rest.Response: + user = users.get_user_by_name(params["user_name"]) + infix = "self" if ctx.user.user_id == user.user_id else "any" + auth.verify_privilege(ctx.user, "user_tokens:list:%s" % infix) user_token_list = user_tokens.get_user_tokens(user) - return { - 'results': [_serialize(ctx, token) for token in user_token_list] - } + return {"results": [_serialize(ctx, token) for token in user_token_list]} -@rest.routes.post('/user-token/(?P[^/]+)/?') +@rest.routes.post("/user-token/(?P[^/]+)/?") def create_user_token( - ctx: rest.Context, params: Dict[str, str] = {}) -> rest.Response: - user = users.get_user_by_name(params['user_name']) - infix = 'self' if ctx.user.user_id == user.user_id else 'any' - auth.verify_privilege(ctx.user, 'user_tokens:create:%s' % infix) - enabled = ctx.get_param_as_bool('enabled', True) + ctx: rest.Context, params: Dict[str, str] = {} +) -> rest.Response: + user = users.get_user_by_name(params["user_name"]) + infix = "self" if ctx.user.user_id == user.user_id else "any" + auth.verify_privilege(ctx.user, "user_tokens:create:%s" % infix) + enabled = ctx.get_param_as_bool("enabled", True) user_token = user_tokens.create_user_token(user, enabled) - if ctx.has_param('note'): - note = ctx.get_param_as_string('note') + if ctx.has_param("note"): + note = ctx.get_param_as_string("note") user_tokens.update_user_token_note(user_token, note) - if ctx.has_param('expirationTime'): - expiration_time = ctx.get_param_as_string('expirationTime') + if ctx.has_param("expirationTime"): + expiration_time = ctx.get_param_as_string("expirationTime") user_tokens.update_user_token_expiration_time( - user_token, expiration_time) + user_token, expiration_time + ) ctx.session.add(user_token) ctx.session.commit() return _serialize(ctx, user_token) -@rest.routes.put('/user-token/(?P[^/]+)/(?P[^/]+)/?') +@rest.routes.put("/user-token/(?P[^/]+)/(?P[^/]+)/?") def update_user_token( - ctx: rest.Context, params: Dict[str, str] = {}) -> rest.Response: - user = users.get_user_by_name(params['user_name']) - infix = 'self' if ctx.user.user_id == user.user_id else 'any' - auth.verify_privilege(ctx.user, 'user_tokens:edit:%s' % infix) - user_token = user_tokens.get_by_user_and_token(user, params['user_token']) + ctx: rest.Context, params: Dict[str, str] = {} +) -> rest.Response: + user = users.get_user_by_name(params["user_name"]) + infix = "self" if ctx.user.user_id == user.user_id else "any" + auth.verify_privilege(ctx.user, "user_tokens:edit:%s" % infix) + user_token = user_tokens.get_by_user_and_token(user, params["user_token"]) versions.verify_version(user_token, ctx) versions.bump_version(user_token) - if ctx.has_param('enabled'): - auth.verify_privilege(ctx.user, 'user_tokens:edit:%s' % infix) + if ctx.has_param("enabled"): + auth.verify_privilege(ctx.user, "user_tokens:edit:%s" % infix) user_tokens.update_user_token_enabled( - user_token, ctx.get_param_as_bool('enabled')) - if ctx.has_param('note'): - auth.verify_privilege(ctx.user, 'user_tokens:edit:%s' % infix) - note = ctx.get_param_as_string('note') + user_token, ctx.get_param_as_bool("enabled") + ) + if ctx.has_param("note"): + auth.verify_privilege(ctx.user, "user_tokens:edit:%s" % infix) + note = ctx.get_param_as_string("note") user_tokens.update_user_token_note(user_token, note) - if ctx.has_param('expirationTime'): - auth.verify_privilege(ctx.user, 'user_tokens:edit:%s' % infix) - expiration_time = ctx.get_param_as_string('expirationTime') + if ctx.has_param("expirationTime"): + auth.verify_privilege(ctx.user, "user_tokens:edit:%s" % infix) + expiration_time = ctx.get_param_as_string("expirationTime") user_tokens.update_user_token_expiration_time( - user_token, expiration_time) + user_token, expiration_time + ) user_tokens.update_user_token_edit_time(user_token) ctx.session.commit() return _serialize(ctx, user_token) -@rest.routes.delete('/user-token/(?P[^/]+)/(?P[^/]+)/?') +@rest.routes.delete("/user-token/(?P[^/]+)/(?P[^/]+)/?") def delete_user_token( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - user = users.get_user_by_name(params['user_name']) - infix = 'self' if ctx.user.user_id == user.user_id else 'any' - auth.verify_privilege(ctx.user, 'user_tokens:delete:%s' % infix) - user_token = user_tokens.get_by_user_and_token(user, params['user_token']) + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + user = users.get_user_by_name(params["user_name"]) + infix = "self" if ctx.user.user_id == user.user_id else "any" + auth.verify_privilege(ctx.user, "user_tokens:delete:%s" % infix) + user_token = user_tokens.get_by_user_and_token(user, params["user_token"]) if user_token is not None: ctx.session.delete(user_token) ctx.session.commit() diff --git a/server/szurubooru/config.py b/server/szurubooru/config.py index 72a24b6..a0fc3e7 100644 --- a/server/szurubooru/config.py +++ b/server/szurubooru/config.py @@ -1,9 +1,10 @@ -from typing import Dict import logging import os +from typing import Dict + import yaml -from szurubooru import errors +from szurubooru import errors logger = logging.getLogger(__name__) @@ -21,21 +22,22 @@ def _merge(left: Dict, right: Dict) -> Dict: def _docker_config() -> Dict: - for key in ['POSTGRES_USER', 'POSTGRES_PASSWORD', 'POSTGRES_HOST']: + for key in ["POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_HOST"]: if not os.getenv(key, False): raise errors.ConfigError(f'Environment variable "{key}" not set') return { - 'debug': True, - 'show_sql': int(os.getenv('LOG_SQL', 0)), - 'data_url': os.getenv('DATA_URL', 'data/'), - 'data_dir': '/data/', - 'database': 'postgres://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s' % { - 'user': os.getenv('POSTGRES_USER'), - 'pass': os.getenv('POSTGRES_PASSWORD'), - 'host': os.getenv('POSTGRES_HOST'), - 'port': int(os.getenv('POSTGRES_PORT', 5432)), - 'db': os.getenv('POSTGRES_DB', os.getenv('POSTGRES_USER')) - } + "debug": True, + "show_sql": int(os.getenv("LOG_SQL", 0)), + "data_url": os.getenv("DATA_URL", "data/"), + "data_dir": "/data/", + "database": "postgres://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s" + % { + "user": os.getenv("POSTGRES_USER"), + "pass": os.getenv("POSTGRES_PASSWORD"), + "host": os.getenv("POSTGRES_HOST"), + "port": int(os.getenv("POSTGRES_PORT", 5432)), + "db": os.getenv("POSTGRES_DB", os.getenv("POSTGRES_USER")), + }, } @@ -45,13 +47,14 @@ def _file_config(filename: str) -> Dict: def _read_config() -> Dict: - ret = _file_config('config.yaml.dist') - if os.path.isfile('config.yaml'): - ret = _merge(ret, _file_config('config.yaml')) - elif os.path.isdir('config.yaml'): + ret = _file_config("config.yaml.dist") + if os.path.isfile("config.yaml"): + ret = _merge(ret, _file_config("config.yaml")) + elif os.path.isdir("config.yaml"): logger.warning( - '\'config.yaml\' should be a file, not a directory, skipping') - if os.path.exists('/.dockerenv'): + "'config.yaml' should be a file, not a directory, skipping" + ) + if os.path.exists("/.dockerenv"): ret = _merge(ret, _docker_config()) return ret diff --git a/server/szurubooru/db.py b/server/szurubooru/db.py index 03bfaff..ed59a70 100644 --- a/server/szurubooru/db.py +++ b/server/szurubooru/db.py @@ -1,12 +1,13 @@ -from typing import Any import threading +from typing import Any + import sqlalchemy as sa import sqlalchemy.orm -from szurubooru import config +from szurubooru import config _data = threading.local() -_engine = sa.create_engine(config.config['database']) # type: Any +_engine = sa.create_engine(config.config["database"]) # type: Any _sessionmaker = sa.orm.sessionmaker(bind=_engine, autoflush=False) # type: Any session = sa.orm.scoped_session(_sessionmaker) # type: Any @@ -30,7 +31,7 @@ def get_query_count() -> int: def _bump_query_count() -> None: - _data.query_count = getattr(_data, 'query_count', 0) + 1 + _data.query_count = getattr(_data, "query_count", 0) + 1 -sa.event.listen(_engine, 'after_execute', lambda *args: _bump_query_count()) +sa.event.listen(_engine, "after_execute", lambda *args: _bump_query_count()) diff --git a/server/szurubooru/errors.py b/server/szurubooru/errors.py index beeb469..9106f04 100644 --- a/server/szurubooru/errors.py +++ b/server/szurubooru/errors.py @@ -3,9 +3,10 @@ from typing import Dict class BaseError(RuntimeError): def __init__( - self, - message: str = 'Unknown error', - extra_fields: Dict[str, str] = None) -> None: + self, + message: str = "Unknown error", + extra_fields: Dict[str, str] = None, + ) -> None: super().__init__(message) self.extra_fields = extra_fields diff --git a/server/szurubooru/facade.py b/server/szurubooru/facade.py index d8d0b63..f1a9a07 100644 --- a/server/szurubooru/facade.py +++ b/server/szurubooru/facade.py @@ -1,109 +1,114 @@ -import os -import time import logging +import os import threading -from typing import Callable, Any, Type +import time +from typing import Any, Callable, Type import coloredlogs import sqlalchemy as sa import sqlalchemy.orm.exc -from szurubooru import config, db, errors, rest -from szurubooru.func.posts import update_all_post_signatures + +from szurubooru import api, config, db, errors, middleware, rest from szurubooru.func.file_uploads import purge_old_uploads -from szurubooru import api, middleware +from szurubooru.func.posts import update_all_post_signatures def _map_error( - ex: Exception, - target_class: Type[rest.errors.BaseHttpError], - title: str) -> rest.errors.BaseHttpError: + ex: Exception, target_class: Type[rest.errors.BaseHttpError], title: str +) -> rest.errors.BaseHttpError: return target_class( name=type(ex).__name__, title=title, description=str(ex), - extra_fields=getattr(ex, 'extra_fields', {})) + extra_fields=getattr(ex, "extra_fields", {}), + ) def _on_auth_error(ex: Exception) -> None: - raise _map_error(ex, rest.errors.HttpForbidden, 'Authentication error') + raise _map_error(ex, rest.errors.HttpForbidden, "Authentication error") def _on_validation_error(ex: Exception) -> None: - raise _map_error(ex, rest.errors.HttpBadRequest, 'Validation error') + raise _map_error(ex, rest.errors.HttpBadRequest, "Validation error") def _on_search_error(ex: Exception) -> None: - raise _map_error(ex, rest.errors.HttpBadRequest, 'Search error') + raise _map_error(ex, rest.errors.HttpBadRequest, "Search error") def _on_integrity_error(ex: Exception) -> None: - raise _map_error(ex, rest.errors.HttpConflict, 'Integrity violation') + raise _map_error(ex, rest.errors.HttpConflict, "Integrity violation") def _on_not_found_error(ex: Exception) -> None: - raise _map_error(ex, rest.errors.HttpNotFound, 'Not found') + raise _map_error(ex, rest.errors.HttpNotFound, "Not found") def _on_processing_error(ex: Exception) -> None: - raise _map_error(ex, rest.errors.HttpBadRequest, 'Processing error') + raise _map_error(ex, rest.errors.HttpBadRequest, "Processing error") def _on_third_party_error(ex: Exception) -> None: raise _map_error( - ex, - rest.errors.HttpInternalServerError, - 'Server configuration error') + ex, rest.errors.HttpInternalServerError, "Server configuration error" + ) def _on_stale_data_error(_ex: Exception) -> None: raise rest.errors.HttpConflict( - name='IntegrityError', - title='Integrity violation', + name="IntegrityError", + title="Integrity violation", description=( - 'Someone else modified this in the meantime. ' - 'Please try again.')) + "Someone else modified this in the meantime. " "Please try again." + ), + ) def validate_config() -> None: - ''' + """ Check whether config doesn't contain errors that might prove lethal at runtime. - ''' + """ from szurubooru.func.auth import RANK_MAP - for privilege, rank in config.config['privileges'].items(): + + for privilege, rank in config.config["privileges"].items(): if rank not in RANK_MAP.values(): raise errors.ConfigError( - 'Rank %r for privilege %r is missing' % (rank, privilege)) - if config.config['default_rank'] not in RANK_MAP.values(): + "Rank %r for privilege %r is missing" % (rank, privilege) + ) + if config.config["default_rank"] not in RANK_MAP.values(): raise errors.ConfigError( - 'Default rank %r is not on the list of known ranks' % ( - config.config['default_rank'])) + "Default rank %r is not on the list of known ranks" + % (config.config["default_rank"]) + ) - for key in ['data_url', 'data_dir']: + for key in ["data_url", "data_dir"]: if not config.config[key]: raise errors.ConfigError( - 'Service is not configured: %r is missing' % key) + "Service is not configured: %r is missing" % key + ) - if not os.path.isabs(config.config['data_dir']): - raise errors.ConfigError( - 'data_dir must be an absolute path') + if not os.path.isabs(config.config["data_dir"]): + raise errors.ConfigError("data_dir must be an absolute path") - if not config.config['database']: - raise errors.ConfigError('Database is not configured') + if not config.config["database"]: + raise errors.ConfigError("Database is not configured") - if config.config['smtp']['host']: - if not config.config['smtp']['port']: - raise errors.ConfigError( - 'SMTP host is set but port is not set') - if not config.config['smtp']['user']: + if config.config["smtp"]["host"]: + if not config.config["smtp"]["port"]: + raise errors.ConfigError("SMTP host is set but port is not set") + if not config.config["smtp"]["user"]: raise errors.ConfigError( - 'SMTP host is set but username is not set') - if not config.config['smtp']['pass']: + "SMTP host is set but username is not set" + ) + if not config.config["smtp"]["pass"]: raise errors.ConfigError( - 'SMTP host is set but password is not set') - if not config.config['smtp']['from']: + "SMTP host is set but password is not set" + ) + if not config.config["smtp"]["from"]: raise errors.ConfigError( - 'From address must be set to use mail-based password reset') + "From address must be set to use mail-based password reset" + ) def purge_old_uploads_daemon() -> None: @@ -116,13 +121,13 @@ def purge_old_uploads_daemon() -> None: def create_app() -> Callable[[Any, Any], Any]: - ''' Create a WSGI compatible App object. ''' + """ Create a WSGI compatible App object. """ validate_config() - coloredlogs.install(fmt='[%(asctime)-15s] %(name)s %(message)s') - if config.config['debug']: - logging.getLogger('szurubooru').setLevel(logging.INFO) - if config.config['show_sql']: - logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) + coloredlogs.install(fmt="[%(asctime)-15s] %(name)s %(message)s") + if config.config["debug"]: + logging.getLogger("szurubooru").setLevel(logging.INFO) + if config.config["show_sql"]: + logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) purge_thread = threading.Thread(target=purge_old_uploads_daemon) purge_thread.daemon = True diff --git a/server/szurubooru/func/auth.py b/server/szurubooru/func/auth.py index 504fe61..d013775 100644 --- a/server/szurubooru/func/auth.py +++ b/server/szurubooru/func/auth.py @@ -1,60 +1,67 @@ -from typing import Tuple, Optional import hashlib import random import uuid from collections import OrderedDict from datetime import datetime +from typing import Optional, Tuple + from nacl import pwhash from nacl.exceptions import InvalidkeyError -from szurubooru import config, db, model, errors -from szurubooru.func import util +from szurubooru import config, db, errors, model +from szurubooru.func import util -RANK_MAP = OrderedDict([ - (model.User.RANK_ANONYMOUS, 'anonymous'), - (model.User.RANK_RESTRICTED, 'restricted'), - (model.User.RANK_REGULAR, 'regular'), - (model.User.RANK_POWER, 'power'), - (model.User.RANK_MODERATOR, 'moderator'), - (model.User.RANK_ADMINISTRATOR, 'administrator'), - (model.User.RANK_NOBODY, 'nobody'), -]) +RANK_MAP = OrderedDict( + [ + (model.User.RANK_ANONYMOUS, "anonymous"), + (model.User.RANK_RESTRICTED, "restricted"), + (model.User.RANK_REGULAR, "regular"), + (model.User.RANK_POWER, "power"), + (model.User.RANK_MODERATOR, "moderator"), + (model.User.RANK_ADMINISTRATOR, "administrator"), + (model.User.RANK_NOBODY, "nobody"), + ] +) def get_password_hash(salt: str, password: str) -> Tuple[str, int]: - ''' Retrieve argon2id password hash. ''' - return pwhash.argon2id.str( - (config.config['secret'] + salt + password).encode('utf8') - ).decode('utf8'), 3 + """ Retrieve argon2id password hash. """ + return ( + pwhash.argon2id.str( + (config.config["secret"] + salt + password).encode("utf8") + ).decode("utf8"), + 3, + ) def get_sha256_legacy_password_hash( - salt: str, password: str) -> Tuple[str, int]: - ''' Retrieve old-style sha256 password hash. ''' + salt: str, password: str +) -> Tuple[str, int]: + """ Retrieve old-style sha256 password hash. """ digest = hashlib.sha256() - digest.update(config.config['secret'].encode('utf8')) - digest.update(salt.encode('utf8')) - digest.update(password.encode('utf8')) + digest.update(config.config["secret"].encode("utf8")) + digest.update(salt.encode("utf8")) + digest.update(password.encode("utf8")) return digest.hexdigest(), 2 def get_sha1_legacy_password_hash(salt: str, password: str) -> Tuple[str, int]: - ''' Retrieve old-style sha1 password hash. ''' + """ Retrieve old-style sha1 password hash. """ digest = hashlib.sha1() - digest.update(b'1A2/$_4xVa') - digest.update(salt.encode('utf8')) - digest.update(password.encode('utf8')) + digest.update(b"1A2/$_4xVa") + digest.update(salt.encode("utf8")) + digest.update(password.encode("utf8")) return digest.hexdigest(), 1 def create_password() -> str: alphabet = { - 'c': list('bcdfghijklmnpqrstvwxyz'), - 'v': list('aeiou'), - 'n': list('0123456789'), + "c": list("bcdfghijklmnpqrstvwxyz"), + "v": list("aeiou"), + "n": list("0123456789"), } - pattern = 'cvcvnncvcv' - return ''.join(random.choice(alphabet[type]) for type in list(pattern)) + pattern = "cvcvnncvcv" + return "".join(random.choice(alphabet[type]) for type in list(pattern)) def is_valid_password(user: model.User, password: str) -> bool: @@ -63,12 +70,13 @@ def is_valid_password(user: model.User, password: str) -> bool: try: return pwhash.verify( - user.password_hash.encode('utf8'), - (config.config['secret'] + salt + password).encode('utf8')) + user.password_hash.encode("utf8"), + (config.config["secret"] + salt + password).encode("utf8"), + ) except InvalidkeyError: possible_hashes = [ get_sha256_legacy_password_hash(salt, password)[0], - get_sha1_legacy_password_hash(salt, password)[0] + get_sha1_legacy_password_hash(salt, password)[0], ] if valid_hash in possible_hashes: # Convert the user password hash to the new hash @@ -82,16 +90,18 @@ def is_valid_password(user: model.User, password: str) -> bool: def is_valid_token(user_token: Optional[model.UserToken]) -> bool: - ''' + """ Token must be enabled and if it has an expiration, it must be greater than now. - ''' + """ if user_token is None: return False if not user_token.enabled: return False - if (user_token.expiration_time is not None - and user_token.expiration_time < datetime.utcnow()): + if ( + user_token.expiration_time is not None + and user_token.expiration_time < datetime.utcnow() + ): return False return True @@ -99,26 +109,27 @@ def is_valid_token(user_token: Optional[model.UserToken]) -> bool: def has_privilege(user: model.User, privilege_name: str) -> bool: assert user all_ranks = list(RANK_MAP.keys()) - assert privilege_name in config.config['privileges'] + assert privilege_name in config.config["privileges"] assert user.rank in all_ranks minimal_rank = util.flip(RANK_MAP)[ - config.config['privileges'][privilege_name]] - good_ranks = all_ranks[all_ranks.index(minimal_rank):] + config.config["privileges"][privilege_name] + ] + good_ranks = all_ranks[all_ranks.index(minimal_rank) :] return user.rank in good_ranks def verify_privilege(user: model.User, privilege_name: str) -> None: assert user if not has_privilege(user, privilege_name): - raise errors.AuthError('Insufficient privileges to do this.') + raise errors.AuthError("Insufficient privileges to do this.") def generate_authentication_token(user: model.User) -> str: - ''' Generate nonguessable challenge (e.g. links in password reminder). ''' + """ Generate nonguessable challenge (e.g. links in password reminder). """ assert user digest = hashlib.md5() - digest.update(config.config['secret'].encode('utf8')) - digest.update(user.password_salt.encode('utf8')) + digest.update(config.config["secret"].encode("utf8")) + digest.update(user.password_salt.encode("utf8")) return digest.hexdigest() diff --git a/server/szurubooru/func/cache.py b/server/szurubooru/func/cache.py index 01e4659..65e99e1 100644 --- a/server/szurubooru/func/cache.py +++ b/server/szurubooru/func/cache.py @@ -1,5 +1,5 @@ -from typing import Any, List, Dict from datetime import datetime +from typing import Any, Dict, List class LruCacheItem: @@ -18,12 +18,11 @@ class LruCache: def insert_item(self, item: LruCacheItem) -> None: if item.key in self.hash: item_index = next( - i - for i, v in enumerate(self.item_list) - if v.key == item.key) + i for i, v in enumerate(self.item_list) if v.key == item.key + ) self.item_list[:] = ( - self.item_list[:item_index] + - self.item_list[item_index + 1:]) + self.item_list[:item_index] + self.item_list[item_index + 1 :] + ) self.item_list.insert(0, item) else: if len(self.item_list) > self.length: diff --git a/server/szurubooru/func/comments.py b/server/szurubooru/func/comments.py index 9f88283..5eb7c8e 100644 --- a/server/szurubooru/func/comments.py +++ b/server/szurubooru/func/comments.py @@ -1,7 +1,8 @@ from datetime import datetime -from typing import Any, Optional, List, Dict, Callable -from szurubooru import db, model, errors, rest -from szurubooru.func import users, scores, serialization +from typing import Any, Callable, Dict, List, Optional + +from szurubooru import db, errors, model, rest +from szurubooru.func import scores, serialization, users class InvalidCommentIdError(errors.ValidationError): @@ -23,15 +24,15 @@ class CommentSerializer(serialization.BaseSerializer): def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'id': self.serialize_id, - 'user': self.serialize_user, - 'postId': self.serialize_post_id, - 'version': self.serialize_version, - 'text': self.serialize_text, - 'creationTime': self.serialize_creation_time, - 'lastEditTime': self.serialize_last_edit_time, - 'score': self.serialize_score, - 'ownScore': self.serialize_own_score, + "id": self.serialize_id, + "user": self.serialize_user, + "postId": self.serialize_post_id, + "version": self.serialize_version, + "text": self.serialize_text, + "creationTime": self.serialize_creation_time, + "lastEditTime": self.serialize_last_edit_time, + "score": self.serialize_score, + "ownScore": self.serialize_own_score, } def serialize_id(self) -> Any: @@ -63,9 +64,8 @@ class CommentSerializer(serialization.BaseSerializer): def serialize_comment( - comment: model.Comment, - auth_user: model.User, - options: List[str] = []) -> rest.Response: + comment: model.Comment, auth_user: model.User, options: List[str] = [] +) -> rest.Response: if comment is None: return None return CommentSerializer(comment, auth_user).serialize(options) @@ -74,21 +74,22 @@ def serialize_comment( def try_get_comment_by_id(comment_id: int) -> Optional[model.Comment]: comment_id = int(comment_id) return ( - db.session - .query(model.Comment) + db.session.query(model.Comment) .filter(model.Comment.comment_id == comment_id) - .one_or_none()) + .one_or_none() + ) def get_comment_by_id(comment_id: int) -> model.Comment: comment = try_get_comment_by_id(comment_id) if comment: return comment - raise CommentNotFoundError('Comment %r not found.' % comment_id) + raise CommentNotFoundError("Comment %r not found." % comment_id) def create_comment( - user: model.User, post: model.Post, text: str) -> model.Comment: + user: model.User, post: model.Post, text: str +) -> model.Comment: comment = model.Comment() comment.user = user comment.post = post @@ -100,5 +101,5 @@ def create_comment( def update_comment_text(comment: model.Comment, text: str) -> None: assert comment if not text: - raise EmptyCommentTextError('Comment text cannot be empty.') + raise EmptyCommentTextError("Comment text cannot be empty.") comment.text = text diff --git a/server/szurubooru/func/diff.py b/server/szurubooru/func/diff.py index 90014f7..3282ebb 100644 --- a/server/szurubooru/func/diff.py +++ b/server/szurubooru/func/diff.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Any +from typing import Any, Dict, List def get_list_diff(old: List[Any], new: List[Any]) -> Any: @@ -16,8 +16,11 @@ def get_list_diff(old: List[Any], new: List[Any]) -> Any: equal = False added.append(item) - return None if equal else { - 'type': 'list change', 'added': added, 'removed': removed} + return ( + None + if equal + else {"type": "list change", "added": added, "removed": removed} + ) def get_dict_diff(old: Dict[str, Any], new: Dict[str, Any]) -> Any: @@ -40,23 +43,20 @@ def get_dict_diff(old: Dict[str, Any], new: Dict[str, Any]) -> Any: else: equal = False value[key] = { - 'type': 'primitive change', - 'old-value': old[key], - 'new-value': new[key], + "type": "primitive change", + "old-value": old[key], + "new-value": new[key], } else: equal = False - value[key] = { - 'type': 'deleted property', - 'value': old[key] - } + value[key] = {"type": "deleted property", "value": old[key]} for key in new.keys(): if key not in old: equal = False value[key] = { - 'type': 'added property', - 'value': new[key], + "type": "added property", + "value": new[key], } - return None if equal else {'type': 'object change', 'value': value} + return None if equal else {"type": "object change", "value": value} diff --git a/server/szurubooru/func/favorites.py b/server/szurubooru/func/favorites.py index f567bfa..a012537 100644 --- a/server/szurubooru/func/favorites.py +++ b/server/szurubooru/func/favorites.py @@ -1,6 +1,7 @@ -from typing import Any, Optional, Callable, Tuple from datetime import datetime -from szurubooru import db, model, errors +from typing import Any, Callable, Optional, Tuple + +from szurubooru import db, errors, model class InvalidFavoriteTargetError(errors.ValidationError): @@ -8,10 +9,11 @@ class InvalidFavoriteTargetError(errors.ValidationError): def _get_table_info( - entity: model.Base) -> Tuple[model.Base, Callable[[model.Base], Any]]: + entity: model.Base, +) -> Tuple[model.Base, Callable[[model.Base], Any]]: assert entity resource_type, _, _ = model.util.get_resource_info(entity) - if resource_type == 'post': + if resource_type == "post": return model.PostFavorite, lambda table: table.post_id raise InvalidFavoriteTargetError() @@ -38,6 +40,7 @@ def unset_favorite(entity: model.Base, user: Optional[model.User]) -> None: def set_favorite(entity: model.Base, user: Optional[model.User]) -> None: from szurubooru.func import scores + assert entity assert user try: diff --git a/server/szurubooru/func/file_uploads.py b/server/szurubooru/func/file_uploads.py index e7f93d8..800397d 100644 --- a/server/szurubooru/func/file_uploads.py +++ b/server/szurubooru/func/file_uploads.py @@ -1,25 +1,25 @@ -from typing import Optional from datetime import datetime, timedelta -from szurubooru.func import files, util +from typing import Optional +from szurubooru.func import files, util MAX_MINUTES = 60 def _get_path(checksum: str) -> str: - return 'temporary-uploads/%s.dat' % checksum + return "temporary-uploads/%s.dat" % checksum def purge_old_uploads() -> None: now = datetime.now() - for file in files.scan('temporary-uploads'): + for file in files.scan("temporary-uploads"): file_time = datetime.fromtimestamp(file.stat().st_ctime) if now - file_time > timedelta(minutes=MAX_MINUTES): - files.delete('temporary-uploads/%s' % file.name) + files.delete("temporary-uploads/%s" % file.name) def get(checksum: str) -> Optional[bytes]: - return files.get('temporary-uploads/%s.dat' % checksum) + return files.get("temporary-uploads/%s.dat" % checksum) def save(content: bytes) -> str: diff --git a/server/szurubooru/func/files.py b/server/szurubooru/func/files.py index fa9f36f..6a89826 100644 --- a/server/szurubooru/func/files.py +++ b/server/szurubooru/func/files.py @@ -1,10 +1,11 @@ -from typing import Any, Optional, List import os +from typing import Any, List, Optional + from szurubooru import config def _get_full_path(path: str) -> str: - return os.path.join(config.config['data_dir'], path) + return os.path.join(config.config["data_dir"], path) def delete(path: str) -> None: @@ -31,12 +32,12 @@ def get(path: str) -> Optional[bytes]: full_path = _get_full_path(path) if not os.path.exists(full_path): return None - with open(full_path, 'rb') as handle: + with open(full_path, "rb") as handle: return handle.read() def save(path: str, content: bytes) -> None: full_path = _get_full_path(path) os.makedirs(os.path.dirname(full_path), exist_ok=True) - with open(full_path, 'wb') as handle: + with open(full_path, "wb") as handle: handle.write(content) diff --git a/server/szurubooru/func/image_hash.py b/server/szurubooru/func/image_hash.py index 771302d..fc7d141 100644 --- a/server/szurubooru/func/image_hash.py +++ b/server/szurubooru/func/image_hash.py @@ -1,12 +1,13 @@ import logging -from io import BytesIO -from datetime import datetime -from typing import Any, Optional, Tuple, Set, List, Callable import math +from datetime import datetime +from io import BytesIO +from typing import Any, Callable, List, Optional, Set, Tuple + import numpy as np from PIL import Image -from szurubooru import config, errors +from szurubooru import config, errors logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ logger = logging.getLogger(__name__) LOWER_PERCENTILE = 5 UPPER_PERCENTILE = 95 -IDENTICAL_TOLERANCE = 2 / 255. +IDENTICAL_TOLERANCE = 2 / 255.0 DISTANCE_CUTOFF = 0.45 N_LEVELS = 2 N = 9 @@ -38,68 +39,74 @@ NpMatrix = np.ndarray def _preprocess_image(content: bytes) -> NpMatrix: try: img = Image.open(BytesIO(content)) - return np.asarray(img.convert('L'), dtype=np.uint8) + return np.asarray(img.convert("L"), dtype=np.uint8) except IOError: raise errors.ProcessingError( - 'Unable to generate a signature hash ' - 'for this image.') + "Unable to generate a signature hash " "for this image." + ) def _crop_image( - image: NpMatrix, - lower_percentile: float, - upper_percentile: float) -> Window: + image: NpMatrix, lower_percentile: float, upper_percentile: float +) -> Window: rw = np.cumsum(np.sum(np.abs(np.diff(image, axis=1)), axis=1)) cw = np.cumsum(np.sum(np.abs(np.diff(image, axis=0)), axis=0)) upper_column_limit = np.searchsorted( - cw, np.percentile(cw, upper_percentile), side='left') + cw, np.percentile(cw, upper_percentile), side="left" + ) lower_column_limit = np.searchsorted( - cw, np.percentile(cw, lower_percentile), side='right') + cw, np.percentile(cw, lower_percentile), side="right" + ) upper_row_limit = np.searchsorted( - rw, np.percentile(rw, upper_percentile), side='left') + rw, np.percentile(rw, upper_percentile), side="left" + ) lower_row_limit = np.searchsorted( - rw, np.percentile(rw, lower_percentile), side='right') + rw, np.percentile(rw, lower_percentile), side="right" + ) if lower_row_limit > upper_row_limit: - lower_row_limit = int(lower_percentile / 100. * image.shape[0]) - upper_row_limit = int(upper_percentile / 100. * image.shape[0]) + lower_row_limit = int(lower_percentile / 100.0 * image.shape[0]) + upper_row_limit = int(upper_percentile / 100.0 * image.shape[0]) if lower_column_limit > upper_column_limit: - lower_column_limit = int(lower_percentile / 100. * image.shape[1]) - upper_column_limit = int(upper_percentile / 100. * image.shape[1]) + lower_column_limit = int(lower_percentile / 100.0 * image.shape[1]) + upper_column_limit = int(upper_percentile / 100.0 * image.shape[1]) return ( (lower_row_limit, upper_row_limit), - (lower_column_limit, upper_column_limit)) + (lower_column_limit, upper_column_limit), + ) def _normalize_and_threshold( - diff_array: NpMatrix, - identical_tolerance: float, - n_levels: int) -> None: + diff_array: NpMatrix, identical_tolerance: float, n_levels: int +) -> None: mask = np.abs(diff_array) < identical_tolerance - diff_array[mask] = 0. + diff_array[mask] = 0.0 if np.all(mask): return positive_cutoffs = np.percentile( - diff_array[diff_array > 0.], np.linspace(0, 100, n_levels + 1)) + diff_array[diff_array > 0.0], np.linspace(0, 100, n_levels + 1) + ) negative_cutoffs = np.percentile( - diff_array[diff_array < 0.], np.linspace(100, 0, n_levels + 1)) + diff_array[diff_array < 0.0], np.linspace(100, 0, n_levels + 1) + ) for level, interval in enumerate( - positive_cutoffs[i:i + 2] - for i in range(positive_cutoffs.shape[0] - 1)): + positive_cutoffs[i : i + 2] + for i in range(positive_cutoffs.shape[0] - 1) + ): diff_array[ - (diff_array >= interval[0]) & (diff_array <= interval[1])] = \ - level + 1 + (diff_array >= interval[0]) & (diff_array <= interval[1]) + ] = (level + 1) for level, interval in enumerate( - negative_cutoffs[i:i + 2] - for i in range(negative_cutoffs.shape[0] - 1)): + negative_cutoffs[i : i + 2] + for i in range(negative_cutoffs.shape[0] - 1) + ): diff_array[ - (diff_array <= interval[0]) & (diff_array >= interval[1])] = \ - -(level + 1) + (diff_array <= interval[0]) & (diff_array >= interval[1]) + ] = -(level + 1) def _compute_grid_points( - image: NpMatrix, - n: float, - window: Window = None) -> Tuple[NpMatrix, NpMatrix]: + image: NpMatrix, n: float, window: Window = None +) -> Tuple[NpMatrix, NpMatrix]: if window is None: window = ((0, image.shape[0]), (0, image.shape[1])) x_coords = np.linspace(window[0][0], window[0][1], n + 2, dtype=int)[1:-1] @@ -108,12 +115,10 @@ def _compute_grid_points( def _compute_mean_level( - image: NpMatrix, - x_coords: NpMatrix, - y_coords: NpMatrix, - p: Optional[float]) -> NpMatrix: + image: NpMatrix, x_coords: NpMatrix, y_coords: NpMatrix, p: Optional[float] +) -> NpMatrix: if p is None: - p = max([2.0, int(0.5 + min(image.shape) / 20.)]) + p = max([2.0, int(0.5 + min(image.shape) / 20.0)]) avg_grey = np.zeros((x_coords.shape[0], y_coords.shape[0])) for i, x in enumerate(x_coords): lower_x_lim = int(max([x - p / 2, 0])) @@ -122,7 +127,8 @@ def _compute_mean_level( lower_y_lim = int(max([y - p / 2, 0])) upper_y_lim = int(min([lower_y_lim + p, image.shape[1]])) avg_grey[i, j] = np.mean( - image[lower_x_lim:upper_x_lim, lower_y_lim:upper_y_lim]) + image[lower_x_lim:upper_x_lim, lower_y_lim:upper_y_lim] + ) return avg_grey @@ -132,59 +138,82 @@ def _compute_differentials(grey_level_matrix: NpMatrix) -> NpMatrix: ( np.diff(grey_level_matrix), ( - np.zeros(grey_level_matrix.shape[0]) - .reshape((grey_level_matrix.shape[0], 1)) - ) - ), axis=1) + np.zeros(grey_level_matrix.shape[0]).reshape( + (grey_level_matrix.shape[0], 1) + ) + ), + ), + axis=1, + ) down_neighbors = -np.concatenate( ( np.diff(grey_level_matrix, axis=0), ( - np.zeros(grey_level_matrix.shape[1]) - .reshape((1, grey_level_matrix.shape[1])) - ) - )) + np.zeros(grey_level_matrix.shape[1]).reshape( + (1, grey_level_matrix.shape[1]) + ) + ), + ) + ) left_neighbors = -np.concatenate( - (right_neighbors[:, -1:], right_neighbors[:, :-1]), axis=1) + (right_neighbors[:, -1:], right_neighbors[:, :-1]), axis=1 + ) up_neighbors = -np.concatenate((down_neighbors[-1:], down_neighbors[:-1])) diagonals = np.arange( - -grey_level_matrix.shape[0] + 1, grey_level_matrix.shape[0]) - upper_left_neighbors = sum([ - np.diagflat(np.insert(np.diff(np.diag(grey_level_matrix, i)), 0, 0), i) - for i in diagonals]) - upper_right_neighbors = sum([ - np.diagflat(np.insert(np.diff(np.diag(flipped, i)), 0, 0), i) - for i in diagonals]) + -grey_level_matrix.shape[0] + 1, grey_level_matrix.shape[0] + ) + upper_left_neighbors = sum( + [ + np.diagflat( + np.insert(np.diff(np.diag(grey_level_matrix, i)), 0, 0), i + ) + for i in diagonals + ] + ) + upper_right_neighbors = sum( + [ + np.diagflat(np.insert(np.diff(np.diag(flipped, i)), 0, 0), i) + for i in diagonals + ] + ) lower_right_neighbors = -np.pad( - upper_left_neighbors[1:, 1:], (0, 1), mode='constant') + upper_left_neighbors[1:, 1:], (0, 1), mode="constant" + ) lower_left_neighbors = -np.pad( - upper_right_neighbors[1:, 1:], (0, 1), mode='constant') - return np.dstack(np.array([ - upper_left_neighbors, - up_neighbors, - np.fliplr(upper_right_neighbors), - left_neighbors, - right_neighbors, - np.fliplr(lower_left_neighbors), - down_neighbors, - lower_right_neighbors])) + upper_right_neighbors[1:, 1:], (0, 1), mode="constant" + ) + return np.dstack( + np.array( + [ + upper_left_neighbors, + up_neighbors, + np.fliplr(upper_right_neighbors), + left_neighbors, + right_neighbors, + np.fliplr(lower_left_neighbors), + down_neighbors, + lower_right_neighbors, + ] + ) + ) def _words_to_int(word_array: NpMatrix) -> List[int]: width = word_array.shape[1] - coding_vector = 3**np.arange(width) + coding_vector = 3 ** np.arange(width) return np.dot(word_array + 1, coding_vector).astype(int).tolist() def _get_words(array: NpMatrix, k: int, n: int) -> NpMatrix: - word_positions = np.linspace( - 0, array.shape[0], n, endpoint=False).astype('int') + word_positions = np.linspace(0, array.shape[0], n, endpoint=False).astype( + "int" + ) assert k <= array.shape[0] assert word_positions.shape[0] <= array.shape[0] - words = np.zeros((n, k)).astype('int8') + words = np.zeros((n, k)).astype("int8") for i, pos in enumerate(word_positions): if pos + k <= array.shape[0]: - words[i] = array[pos:pos + k] + words[i] = array[pos : pos + k] else: temp = array[pos:].copy() temp.resize(k, refcheck=False) @@ -199,16 +228,17 @@ def generate_signature(content: bytes) -> NpMatrix: image_limits = _crop_image( im_array, lower_percentile=LOWER_PERCENTILE, - upper_percentile=UPPER_PERCENTILE) + upper_percentile=UPPER_PERCENTILE, + ) x_coords, y_coords = _compute_grid_points( - im_array, n=N, window=image_limits) + im_array, n=N, window=image_limits + ) avg_grey = _compute_mean_level(im_array, x_coords, y_coords, p=P) diff_matrix = _compute_differentials(avg_grey) _normalize_and_threshold( - diff_matrix, - identical_tolerance=IDENTICAL_TOLERANCE, - n_levels=N_LEVELS) - return np.ravel(diff_matrix).astype('int8') + diff_matrix, identical_tolerance=IDENTICAL_TOLERANCE, n_levels=N_LEVELS + ) + return np.ravel(diff_matrix).astype("int8") def generate_words(signature: NpMatrix) -> List[int]: @@ -216,9 +246,8 @@ def generate_words(signature: NpMatrix) -> List[int]: def normalized_distance( - target_array: Any, - vec: NpMatrix, - nan_value: float = 1.0) -> List[float]: + target_array: Any, vec: NpMatrix, nan_value: float = 1.0 +) -> List[float]: target_array = np.array(target_array).astype(int) vec = vec.astype(int) topvec = np.linalg.norm(vec - target_array, axis=1) @@ -230,7 +259,7 @@ def normalized_distance( def pack_signature(signature: NpMatrix) -> bytes: - ''' + """ Serializes the signature vector for efficient storage in a database. Shifts the range of the signature vector from [-N_LEVELS,+N_LEVELS] @@ -241,24 +270,38 @@ def pack_signature(signature: NpMatrix) -> bytes: This is then converted into a more packed array consisting of uint32 elements (for SIG_CHUNK_BITS = 32). - ''' - coding_vector = np.flipud(SIG_BASE**np.arange(SIG_CHUNK_WIDTH)) - return np.array([ - np.dot(x, coding_vector) for x in - np.reshape(signature + N_LEVELS, (-1, SIG_CHUNK_WIDTH)) - ]).astype(f'uint{SIG_CHUNK_BITS}').tobytes() + """ + coding_vector = np.flipud(SIG_BASE ** np.arange(SIG_CHUNK_WIDTH)) + return ( + np.array( + [ + np.dot(x, coding_vector) + for x in np.reshape( + signature + N_LEVELS, (-1, SIG_CHUNK_WIDTH) + ) + ] + ) + .astype(f"uint{SIG_CHUNK_BITS}") + .tobytes() + ) def unpack_signature(packed: bytes) -> NpMatrix: - ''' + """ Deserializes the signature vector once recieved from the database. Functions as an inverse transformation of pack_signature() - ''' - return np.ravel(np.array([ - [ - int(digit) - N_LEVELS for digit in - np.base_repr(e, base=SIG_BASE).zfill(SIG_CHUNK_WIDTH) - ] for e in - np.frombuffer(packed, dtype=f'uint{SIG_CHUNK_BITS}') - ]).astype('int8')) + """ + return np.ravel( + np.array( + [ + [ + int(digit) - N_LEVELS + for digit in np.base_repr(e, base=SIG_BASE).zfill( + SIG_CHUNK_WIDTH + ) + ] + for e in np.frombuffer(packed, dtype=f"uint{SIG_CHUNK_BITS}") + ] + ).astype("int8") + ) diff --git a/server/szurubooru/func/images.py b/server/szurubooru/func/images.py index 1a96dcc..440fc06 100644 --- a/server/szurubooru/func/images.py +++ b/server/szurubooru/func/images.py @@ -1,14 +1,14 @@ -from typing import List -import logging import json -import shlex -import subprocess +import logging import math import re +import shlex +import subprocess +from typing import List + from szurubooru import errors from szurubooru.func import mime, util - logger = logging.getLogger(__name__) @@ -19,97 +19,139 @@ class Image: @property def width(self) -> int: - return self.info['streams'][0]['width'] + return self.info["streams"][0]["width"] @property def height(self) -> int: - return self.info['streams'][0]['height'] + return self.info["streams"][0]["height"] @property def frames(self) -> int: - return self.info['streams'][0]['nb_read_frames'] + return self.info["streams"][0]["nb_read_frames"] def resize_fill(self, width: int, height: int) -> None: width_greater = self.width > self.height width, height = (-1, height) if width_greater else (width, -1) cli = [ - '-i', '{path}', - '-f', 'image2', - '-filter:v', "scale='{width}:{height}'".format( - width=width, height=height), - '-map', '0:v:0', - '-vframes', '1', - '-vcodec', 'png', - '-', + "-i", + "{path}", + "-f", + "image2", + "-filter:v", + "scale='{width}:{height}'".format(width=width, height=height), + "-map", + "0:v:0", + "-vframes", + "1", + "-vcodec", + "png", + "-", ] - if 'duration' in self.info['format'] \ - and self.info['format']['format_name'] != 'swf': - duration = float(self.info['format']['duration']) + if ( + "duration" in self.info["format"] + and self.info["format"]["format_name"] != "swf" + ): + duration = float(self.info["format"]["duration"]) if duration > 3: - cli = [ - '-ss', - '%d' % math.floor(duration * 0.3), - ] + cli + cli = ["-ss", "%d" % math.floor(duration * 0.3),] + cli content = self._execute(cli, ignore_error_if_data=True) if not content: - raise errors.ProcessingError('Error while resizing image.') + raise errors.ProcessingError("Error while resizing image.") self.content = content self._reload_info() def to_png(self) -> bytes: - return self._execute([ - '-i', '{path}', - '-f', 'image2', - '-map', '0:v:0', - '-vframes', '1', - '-vcodec', 'png', - '-', - ]) + return self._execute( + [ + "-i", + "{path}", + "-f", + "image2", + "-map", + "0:v:0", + "-vframes", + "1", + "-vcodec", + "png", + "-", + ] + ) def to_jpeg(self) -> bytes: - return self._execute([ - '-f', 'lavfi', - '-i', 'color=white:s=%dx%d' % (self.width, self.height), - '-i', '{path}', - '-f', 'image2', - '-filter_complex', 'overlay', - '-map', '0:v:0', - '-vframes', '1', - '-vcodec', 'mjpeg', - '-', - ]) + return self._execute( + [ + "-f", + "lavfi", + "-i", + "color=white:s=%dx%d" % (self.width, self.height), + "-i", + "{path}", + "-f", + "image2", + "-filter_complex", + "overlay", + "-map", + "0:v:0", + "-vframes", + "1", + "-vcodec", + "mjpeg", + "-", + ] + ) def to_webm(self) -> bytes: - with util.create_temp_file_path(suffix='.log') as phase_log_path: + with util.create_temp_file_path(suffix=".log") as phase_log_path: # Pass 1 - self._execute([ - '-i', '{path}', - '-pass', '1', - '-passlogfile', phase_log_path, - '-vcodec', 'libvpx-vp9', - '-crf', '4', - '-b:v', '2500K', - '-acodec', 'libvorbis', - '-f', 'webm', - '-y', '/dev/null' - ]) + self._execute( + [ + "-i", + "{path}", + "-pass", + "1", + "-passlogfile", + phase_log_path, + "-vcodec", + "libvpx-vp9", + "-crf", + "4", + "-b:v", + "2500K", + "-acodec", + "libvorbis", + "-f", + "webm", + "-y", + "/dev/null", + ] + ) # Pass 2 - return self._execute([ - '-i', '{path}', - '-pass', '2', - '-passlogfile', phase_log_path, - '-vcodec', 'libvpx-vp9', - '-crf', '4', - '-b:v', '2500K', - '-acodec', 'libvorbis', - '-f', 'webm', - '-' - ]) + return self._execute( + [ + "-i", + "{path}", + "-pass", + "2", + "-passlogfile", + phase_log_path, + "-vcodec", + "libvpx-vp9", + "-crf", + "4", + "-b:v", + "2500K", + "-acodec", + "libvorbis", + "-f", + "webm", + "-", + ] + ) def to_mp4(self) -> bytes: - with util.create_temp_file_path(suffix='.dat') as mp4_temp_path: + with util.create_temp_file_path(suffix=".dat") as mp4_temp_path: width = self.width height = self.height altered_dimensions = False @@ -123,97 +165,138 @@ class Image: altered_dimensions = True args = [ - '-i', '{path}', - '-vcodec', 'libx264', - '-preset', 'slow', - '-crf', '22', - '-b:v', '200K', - '-profile:v', 'main', - '-pix_fmt', 'yuv420p', - '-acodec', 'aac', - '-f', 'mp4' + "-i", + "{path}", + "-vcodec", + "libx264", + "-preset", + "slow", + "-crf", + "22", + "-b:v", + "200K", + "-profile:v", + "main", + "-pix_fmt", + "yuv420p", + "-acodec", + "aac", + "-f", + "mp4", ] if altered_dimensions: - args += ['-filter:v', 'scale=\'%d:%d\'' % (width, height)] + args += ["-filter:v", "scale='%d:%d'" % (width, height)] - self._execute(args + ['-y', mp4_temp_path]) + self._execute(args + ["-y", mp4_temp_path]) - with open(mp4_temp_path, 'rb') as mp4_temp: + with open(mp4_temp_path, "rb") as mp4_temp: return mp4_temp.read() def check_for_sound(self) -> bool: - audioinfo = json.loads(self._execute([ - '-i', '{path}', - '-of', 'json', - '-select_streams', 'a', - '-show_streams', - ], program='ffprobe').decode('utf-8')) - assert 'streams' in audioinfo - if len(audioinfo['streams']) < 1: + audioinfo = json.loads( + self._execute( + [ + "-i", + "{path}", + "-of", + "json", + "-select_streams", + "a", + "-show_streams", + ], + program="ffprobe", + ).decode("utf-8") + ) + assert "streams" in audioinfo + if len(audioinfo["streams"]) < 1: return False - log = self._execute([ - '-hide_banner', - '-progress', '-', - '-i', '{path}', - '-af', 'volumedetect', - '-max_muxing_queue_size', '99999', - '-vn', '-sn', - '-f', 'null', - '-y', '/dev/null', - ], get_logs=True).decode('utf-8', errors='replace') - log_match = re.search(r'.*volumedetect.*mean_volume: (.*) dB', log) + log = self._execute( + [ + "-hide_banner", + "-progress", + "-", + "-i", + "{path}", + "-af", + "volumedetect", + "-max_muxing_queue_size", + "99999", + "-vn", + "-sn", + "-f", + "null", + "-y", + "/dev/null", + ], + get_logs=True, + ).decode("utf-8", errors="replace") + log_match = re.search(r".*volumedetect.*mean_volume: (.*) dB", log) if not log_match or not log_match.groups(): raise errors.ProcessingError( - 'A problem occured when trying to check for audio') + "A problem occured when trying to check for audio" + ) meanvol = float(log_match.groups()[0]) # -91.0 dB is the minimum for 16-bit audio, assume sound if > -80.0 dB return meanvol > -80.0 def _execute( - self, - cli: List[str], - program: str = 'ffmpeg', - ignore_error_if_data: bool = False, - get_logs: bool = False) -> bytes: + self, + cli: List[str], + program: str = "ffmpeg", + ignore_error_if_data: bool = False, + get_logs: bool = False, + ) -> bytes: extension = mime.get_extension(mime.get_mime_type(self.content)) assert extension - with util.create_temp_file(suffix='.' + extension) as handle: + with util.create_temp_file(suffix="." + extension) as handle: handle.write(self.content) handle.flush() - cli = [program, '-loglevel', '32' if get_logs else '24'] + cli + cli = [program, "-loglevel", "32" if get_logs else "24"] + cli cli = [part.format(path=handle.name) for part in cli] proc = subprocess.Popen( cli, stdout=subprocess.PIPE, stdin=subprocess.PIPE, - stderr=subprocess.PIPE) + stderr=subprocess.PIPE, + ) out, err = proc.communicate(input=self.content) if proc.returncode != 0: logger.warning( - 'Failed to execute ffmpeg command (cli=%r, err=%r)', - ' '.join(shlex.quote(arg) for arg in cli), - err) - if ((len(out) > 0 and not ignore_error_if_data) - or len(out) == 0): + "Failed to execute ffmpeg command (cli=%r, err=%r)", + " ".join(shlex.quote(arg) for arg in cli), + err, + ) + if (len(out) > 0 and not ignore_error_if_data) or len( + out + ) == 0: raise errors.ProcessingError( - 'Error while processing image.\n' - + err.decode('utf-8')) + "Error while processing image.\n" + err.decode("utf-8") + ) return err if get_logs else out def _reload_info(self) -> None: - self.info = json.loads(self._execute([ - '-i', '{path}', - '-of', 'json', - '-select_streams', 'v', - '-show_format', - '-show_streams', - ], program='ffprobe').decode('utf-8')) - assert 'format' in self.info - assert 'streams' in self.info - if len(self.info['streams']) < 1: - logger.warning('The video contains no video streams.') + self.info = json.loads( + self._execute( + [ + "-i", + "{path}", + "-of", + "json", + "-select_streams", + "v", + "-show_format", + "-show_streams", + ], + program="ffprobe", + ).decode("utf-8") + ) + assert "format" in self.info + assert "streams" in self.info + if len(self.info["streams"]) < 1: + logger.warning("The video contains no video streams.") raise errors.ProcessingError( - 'The video contains no video streams.') + "The video contains no video streams." + ) diff --git a/server/szurubooru/func/mailer.py b/server/szurubooru/func/mailer.py index 76682f1..c4cf9db 100644 --- a/server/szurubooru/func/mailer.py +++ b/server/szurubooru/func/mailer.py @@ -1,16 +1,18 @@ -import smtplib import email.mime.text +import smtplib + from szurubooru import config def send_mail(sender: str, recipient: str, subject: str, body: str) -> None: msg = email.mime.text.MIMEText(body) - msg['Subject'] = subject - msg['From'] = sender - msg['To'] = recipient + msg["Subject"] = subject + msg["From"] = sender + msg["To"] = recipient smtp = smtplib.SMTP( - config.config['smtp']['host'], int(config.config['smtp']['port'])) - smtp.login(config.config['smtp']['user'], config.config['smtp']['pass']) + config.config["smtp"]["host"], int(config.config["smtp"]["port"]) + ) + smtp.login(config.config["smtp"]["user"], config.config["smtp"]["pass"]) smtp.send_message(msg) smtp.quit() diff --git a/server/szurubooru/func/mime.py b/server/szurubooru/func/mime.py index afab817..5f6279b 100644 --- a/server/szurubooru/func/mime.py +++ b/server/szurubooru/func/mime.py @@ -4,60 +4,66 @@ from typing import Optional def get_mime_type(content: bytes) -> str: if not content: - return 'application/octet-stream' + return "application/octet-stream" - if content[0:3] in (b'CWS', b'FWS', b'ZWS'): - return 'application/x-shockwave-flash' + if content[0:3] in (b"CWS", b"FWS", b"ZWS"): + return "application/x-shockwave-flash" - if content[0:3] == b'\xFF\xD8\xFF': - return 'image/jpeg' + if content[0:3] == b"\xFF\xD8\xFF": + return "image/jpeg" - if content[0:6] == b'\x89PNG\x0D\x0A': - return 'image/png' + if content[0:6] == b"\x89PNG\x0D\x0A": + return "image/png" - if content[0:6] in (b'GIF87a', b'GIF89a'): - return 'image/gif' + if content[0:6] in (b"GIF87a", b"GIF89a"): + return "image/gif" - if content[8:12] == b'WEBP': - return 'image/webp' + if content[8:12] == b"WEBP": + return "image/webp" - if content[0:4] == b'\x1A\x45\xDF\xA3': - return 'video/webm' + if content[0:4] == b"\x1A\x45\xDF\xA3": + return "video/webm" - if content[4:12] in (b'ftypisom', b'ftypiso5', b'ftypmp42'): - return 'video/mp4' + if content[4:12] in (b"ftypisom", b"ftypiso5", b"ftypmp42"): + return "video/mp4" - return 'application/octet-stream' + return "application/octet-stream" def get_extension(mime_type: str) -> Optional[str]: extension_map = { - 'application/x-shockwave-flash': 'swf', - 'image/gif': 'gif', - 'image/jpeg': 'jpg', - 'image/png': 'png', - 'image/webp': 'webp', - 'video/mp4': 'mp4', - 'video/webm': 'webm', - 'application/octet-stream': 'dat', + "application/x-shockwave-flash": "swf", + "image/gif": "gif", + "image/jpeg": "jpg", + "image/png": "png", + "image/webp": "webp", + "video/mp4": "mp4", + "video/webm": "webm", + "application/octet-stream": "dat", } - return extension_map.get((mime_type or '').strip().lower(), None) + return extension_map.get((mime_type or "").strip().lower(), None) def is_flash(mime_type: str) -> bool: - return mime_type.lower() == 'application/x-shockwave-flash' + return mime_type.lower() == "application/x-shockwave-flash" def is_video(mime_type: str) -> bool: - return mime_type.lower() in ('application/ogg', 'video/mp4', 'video/webm') + return mime_type.lower() in ("application/ogg", "video/mp4", "video/webm") def is_image(mime_type: str) -> bool: return mime_type.lower() in ( - 'image/jpeg', 'image/png', 'image/gif', 'image/webp') + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + ) def is_animated_gif(content: bytes) -> bool: - pattern = b'\x21\xF9\x04[\x00-\xFF]{4}\x00[\x2C\x21]' - return get_mime_type(content) == 'image/gif' \ + pattern = b"\x21\xF9\x04[\x00-\xFF]{4}\x00[\x2C\x21]" + return ( + get_mime_type(content) == "image/gif" and len(re.findall(pattern, content)) > 1 + ) diff --git a/server/szurubooru/func/net.py b/server/szurubooru/func/net.py index 8110340..ddac65c 100644 --- a/server/szurubooru/func/net.py +++ b/server/szurubooru/func/net.py @@ -1,12 +1,13 @@ import logging -import urllib.request import os +import urllib.request from tempfile import NamedTemporaryFile -from szurubooru import config, errors -from szurubooru.func import mime, util + from youtube_dl import YoutubeDL from youtube_dl.utils import YoutubeDLError +from szurubooru import config, errors +from szurubooru.func import mime, util logger = logging.getLogger(__name__) @@ -14,41 +15,46 @@ logger = logging.getLogger(__name__) def download(url: str, use_video_downloader: bool = False) -> bytes: assert url request = urllib.request.Request(url) - if config.config['user_agent']: - request.add_header('User-Agent', config.config['user_agent']) - request.add_header('Referer', url) + if config.config["user_agent"]: + request.add_header("User-Agent", config.config["user_agent"]) + request.add_header("Referer", url) try: with urllib.request.urlopen(request) as handle: content = handle.read() except Exception as ex: - raise errors.ProcessingError('Error downloading %s (%s)' % (url, ex)) - if (use_video_downloader and - mime.get_mime_type(content) == 'application/octet-stream'): + raise errors.ProcessingError("Error downloading %s (%s)" % (url, ex)) + if ( + use_video_downloader + and mime.get_mime_type(content) == "application/octet-stream" + ): return _youtube_dl_wrapper(url) return content def _youtube_dl_wrapper(url: str) -> bytes: outpath = os.path.join( - config.config['data_dir'], - 'temporary-uploads', - 'youtubedl-' + util.get_sha1(url)[0:8] + '.dat') + config.config["data_dir"], + "temporary-uploads", + "youtubedl-" + util.get_sha1(url)[0:8] + ".dat", + ) options = { - 'ignoreerrors': False, - 'format': 'best[ext=webm]/best[ext=mp4]/best[ext=flv]', - 'logger': logger, - 'max_filesize': config.config['max_dl_filesize'], - 'max_downloads': 1, - 'outtmpl': outpath, + "ignoreerrors": False, + "format": "best[ext=webm]/best[ext=mp4]/best[ext=flv]", + "logger": logger, + "max_filesize": config.config["max_dl_filesize"], + "max_downloads": 1, + "outtmpl": outpath, } try: with YoutubeDL(options) as ydl: ydl.extract_info(url, download=True) - with open(outpath, 'rb') as f: + with open(outpath, "rb") as f: return f.read() except YoutubeDLError as ex: raise errors.ThirdPartyError( - 'Error downloading video %s (%s)' % (url, ex)) + "Error downloading video %s (%s)" % (url, ex) + ) except FileNotFoundError: raise errors.ThirdPartyError( - 'Error downloading video %s (file could not be saved)' % (url)) + "Error downloading video %s (file could not be saved)" % (url) + ) diff --git a/server/szurubooru/func/pool_categories.py b/server/szurubooru/func/pool_categories.py index 83305de..c13857c 100644 --- a/server/szurubooru/func/pool_categories.py +++ b/server/szurubooru/func/pool_categories.py @@ -1,11 +1,12 @@ import re -from typing import Any, Optional, Dict, List, Callable +from typing import Any, Callable, Dict, List, Optional + import sqlalchemy as sa -from szurubooru import config, db, model, errors, rest -from szurubooru.func import util, serialization, cache +from szurubooru import config, db, errors, model, rest +from szurubooru.func import cache, serialization, util -DEFAULT_CATEGORY_NAME_CACHE_KEY = 'default-pool-category' +DEFAULT_CATEGORY_NAME_CACHE_KEY = "default-pool-category" class PoolCategoryNotFoundError(errors.NotFoundError): @@ -29,10 +30,11 @@ class InvalidPoolCategoryColorError(errors.ValidationError): def _verify_name_validity(name: str) -> None: - name_regex = config.config['pool_category_name_regex'] + name_regex = config.config["pool_category_name_regex"] if not re.match(name_regex, name): raise InvalidPoolCategoryNameError( - 'Name must satisfy regex %r.' % name_regex) + "Name must satisfy regex %r." % name_regex + ) class PoolCategorySerializer(serialization.BaseSerializer): @@ -41,11 +43,11 @@ class PoolCategorySerializer(serialization.BaseSerializer): def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'name': self.serialize_name, - 'version': self.serialize_version, - 'color': self.serialize_color, - 'usages': self.serialize_usages, - 'default': self.serialize_default, + "name": self.serialize_name, + "version": self.serialize_version, + "color": self.serialize_color, + "usages": self.serialize_usages, + "default": self.serialize_default, } def serialize_name(self) -> Any: @@ -65,8 +67,8 @@ class PoolCategorySerializer(serialization.BaseSerializer): def serialize_category( - category: Optional[model.PoolCategory], - options: List[str] = []) -> Optional[rest.Response]: + category: Optional[model.PoolCategory], options: List[str] = [] +) -> Optional[rest.Response]: if not category: return None return PoolCategorySerializer(category).serialize(options) @@ -84,18 +86,21 @@ def create_category(name: str, color: str) -> model.PoolCategory: def update_category_name(category: model.PoolCategory, name: str) -> None: assert category if not name: - raise InvalidPoolCategoryNameError('Name cannot be empty.') + raise InvalidPoolCategoryNameError("Name cannot be empty.") expr = sa.func.lower(model.PoolCategory.name) == name.lower() if category.pool_category_id: expr = expr & ( - model.PoolCategory.pool_category_id != category.pool_category_id) + model.PoolCategory.pool_category_id != category.pool_category_id + ) already_exists = ( - db.session.query(model.PoolCategory).filter(expr).count() > 0) + db.session.query(model.PoolCategory).filter(expr).count() > 0 + ) if already_exists: raise PoolCategoryAlreadyExistsError( - 'A category with this name already exists.') + "A category with this name already exists." + ) if util.value_exceeds_column_size(name, model.PoolCategory.name): - raise InvalidPoolCategoryNameError('Name is too long.') + raise InvalidPoolCategoryNameError("Name is too long.") _verify_name_validity(name) category.name = name cache.remove(DEFAULT_CATEGORY_NAME_CACHE_KEY) @@ -104,20 +109,20 @@ def update_category_name(category: model.PoolCategory, name: str) -> None: def update_category_color(category: model.PoolCategory, color: str) -> None: assert category if not color: - raise InvalidPoolCategoryColorError('Color cannot be empty.') - if not re.match(r'^#?[0-9a-z]+$', color): - raise InvalidPoolCategoryColorError('Invalid color.') + raise InvalidPoolCategoryColorError("Color cannot be empty.") + if not re.match(r"^#?[0-9a-z]+$", color): + raise InvalidPoolCategoryColorError("Invalid color.") if util.value_exceeds_column_size(color, model.PoolCategory.color): - raise InvalidPoolCategoryColorError('Color is too long.') + raise InvalidPoolCategoryColorError("Color is too long.") category.color = color def try_get_category_by_name( - name: str, lock: bool = False) -> Optional[model.PoolCategory]: - query = ( - db.session - .query(model.PoolCategory) - .filter(sa.func.lower(model.PoolCategory.name) == name.lower())) + name: str, lock: bool = False +) -> Optional[model.PoolCategory]: + query = db.session.query(model.PoolCategory).filter( + sa.func.lower(model.PoolCategory.name) == name.lower() + ) if lock: query = query.with_for_update() return query.one_or_none() @@ -126,7 +131,7 @@ def try_get_category_by_name( def get_category_by_name(name: str, lock: bool = False) -> model.PoolCategory: category = try_get_category_by_name(name, lock) if not category: - raise PoolCategoryNotFoundError('Pool category %r not found.' % name) + raise PoolCategoryNotFoundError("Pool category %r not found." % name) return category @@ -135,26 +140,28 @@ def get_all_category_names() -> List[str]: def get_all_categories() -> List[model.PoolCategory]: - return db.session.query(model.PoolCategory).order_by( - model.PoolCategory.name.asc()).all() + return ( + db.session.query(model.PoolCategory) + .order_by(model.PoolCategory.name.asc()) + .all() + ) def try_get_default_category( - lock: bool = False) -> Optional[model.PoolCategory]: - query = ( - db.session - .query(model.PoolCategory) - .filter(model.PoolCategory.default)) + lock: bool = False, +) -> Optional[model.PoolCategory]: + query = db.session.query(model.PoolCategory).filter( + model.PoolCategory.default + ) if lock: query = query.with_for_update() category = query.first() # if for some reason (e.g. as a result of migration) there's no default # category, get the first record available. if not category: - query = ( - db.session - .query(model.PoolCategory) - .order_by(model.PoolCategory.pool_category_id.asc())) + query = db.session.query(model.PoolCategory).order_by( + model.PoolCategory.pool_category_id.asc() + ) if lock: query = query.with_for_update() category = query.first() @@ -164,7 +171,7 @@ def try_get_default_category( def get_default_category(lock: bool = False) -> model.PoolCategory: category = try_get_default_category(lock) if not category: - raise PoolCategoryNotFoundError('No pool category created yet.') + raise PoolCategoryNotFoundError("No pool category created yet.") return category @@ -191,9 +198,10 @@ def set_default_category(category: model.PoolCategory) -> None: def delete_category(category: model.PoolCategory) -> None: assert category if len(get_all_category_names()) == 1: - raise PoolCategoryIsInUseError('Cannot delete the last category.') + raise PoolCategoryIsInUseError("Cannot delete the last category.") 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.') + "Pool category has some usages and cannot be deleted. " + + "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 4acf9df..8bd5090 100644 --- a/server/szurubooru/func/pools.py +++ b/server/szurubooru/func/pools.py @@ -1,9 +1,11 @@ import re -from typing import Any, Optional, Tuple, List, Dict, Callable from datetime import datetime +from typing import Any, Callable, Dict, List, Optional, Tuple + import sqlalchemy as sa -from szurubooru import config, db, model, errors, rest -from szurubooru.func import util, pool_categories, posts, serialization + +from szurubooru import config, db, errors, model, rest +from szurubooru.func import pool_categories, posts, serialization, util class PoolNotFoundError(errors.NotFoundError): @@ -44,10 +46,10 @@ class InvalidPoolNonexistentPostError(errors.ValidationError): def _verify_name_validity(name: str) -> None: if util.value_exceeds_column_size(name, model.PoolName.name): - raise InvalidPoolNameError('Name is too long.') - name_regex = config.config['pool_name_regex'] + raise InvalidPoolNameError("Name is too long.") + name_regex = config.config["pool_name_regex"] if not re.match(name_regex, name): - raise InvalidPoolNameError('Name must satisfy regex %r.' % name_regex) + raise InvalidPoolNameError("Name must satisfy regex %r." % name_regex) def _get_names(pool: model.Pool) -> List[str]: @@ -60,7 +62,8 @@ def _lower_list(names: List[str]) -> List[str]: def _check_name_intersection( - names1: List[str], names2: List[str], case_sensitive: bool) -> bool: + names1: List[str], names2: List[str], case_sensitive: bool +) -> bool: if not case_sensitive: names1 = _lower_list(names1) names2 = _lower_list(names2) @@ -85,7 +88,8 @@ def sort_pools(pools: List[model.Pool]) -> List[model.Pool]: key=lambda pool: ( default_category_name == pool.category.name, pool.category.name, - pool.names[0].name) + pool.names[0].name, + ), ) @@ -95,15 +99,15 @@ class PoolSerializer(serialization.BaseSerializer): def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'id': self.serialize_id, - 'names': self.serialize_names, - 'category': self.serialize_category, - 'version': self.serialize_version, - 'description': self.serialize_description, - 'creationTime': self.serialize_creation_time, - 'lastEditTime': self.serialize_last_edit_time, - 'postCount': self.serialize_post_count, - 'posts': self.serialize_posts + "id": self.serialize_id, + "names": self.serialize_names, + "category": self.serialize_category, + "version": self.serialize_version, + "description": self.serialize_description, + "creationTime": self.serialize_creation_time, + "lastEditTime": self.serialize_last_edit_time, + "postCount": self.serialize_post_count, + "posts": self.serialize_posts, } def serialize_id(self) -> Any: @@ -132,7 +136,8 @@ class PoolSerializer(serialization.BaseSerializer): def serialize_posts(self) -> Any: return [ - post for post in [ + post + for post in [ posts.serialize_micro_post(rel, None) for rel in self.pool.posts ] @@ -140,7 +145,8 @@ class PoolSerializer(serialization.BaseSerializer): def serialize_pool( - pool: model.Pool, options: List[str] = []) -> Optional[rest.Response]: + pool: model.Pool, options: List[str] = [] +) -> Optional[rest.Response]: if not pool: return None return PoolSerializer(pool).serialize(options) @@ -148,32 +154,32 @@ def serialize_pool( def try_get_pool_by_id(pool_id: int) -> Optional[model.Pool]: return ( - db.session - .query(model.Pool) + db.session.query(model.Pool) .filter(model.Pool.pool_id == pool_id) - .one_or_none()) + .one_or_none() + ) def get_pool_by_id(pool_id: int) -> model.Pool: pool = try_get_pool_by_id(pool_id) if not pool: - raise PoolNotFoundError('Pool %r not found.' % pool_id) + raise PoolNotFoundError("Pool %r not found." % pool_id) return pool def try_get_pool_by_name(name: str) -> Optional[model.Pool]: return ( - db.session - .query(model.Pool) + db.session.query(model.Pool) .join(model.PoolName) .filter(sa.func.lower(model.PoolName.name) == name.lower()) - .one_or_none()) + .one_or_none() + ) def get_pool_by_name(name: str) -> model.Pool: pool = try_get_pool_by_name(name) if not pool: - raise PoolNotFoundError('Pool %r not found.' % name) + raise PoolNotFoundError("Pool %r not found." % name) return pool @@ -187,12 +193,16 @@ def get_pools_by_names(names: List[str]) -> List[model.Pool]: .filter( sa.sql.or_( sa.func.lower(model.PoolName.name) == name.lower() - for name in names)) - .all()) + for name in names + ) + ) + .all() + ) def get_or_create_pools_by_names( - names: List[str]) -> Tuple[List[model.Pool], List[model.Pool]]: + names: List[str], +) -> Tuple[List[model.Pool], List[model.Pool]]: names = util.icase_unique(names) existing_pools = get_pools_by_names(names) new_pools = [] @@ -201,14 +211,14 @@ def get_or_create_pools_by_names( found = False for existing_pool in existing_pools: if _check_name_intersection( - _get_names(existing_pool), [name], False): + _get_names(existing_pool), [name], False + ): found = True break if not found: new_pool = create_pool( - names=[name], - category_name=pool_category_name, - post_ids=[]) + names=[name], category_name=pool_category_name, post_ids=[] + ) db.session.add(new_pool) new_pools.append(new_pool) return existing_pools, new_pools @@ -223,20 +233,19 @@ def merge_pools(source_pool: model.Pool, target_pool: model.Pool) -> None: assert source_pool assert target_pool if source_pool.pool_id == target_pool.pool_id: - raise InvalidPoolRelationError('Cannot merge pool with itself.') + raise InvalidPoolRelationError("Cannot merge pool with itself.") 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 = ( - sa.sql.expression.update(alias1) - .where(alias1.pool_id == source_pool_id)) - update_stmt = ( - update_stmt - .where( - ~sa.exists() - .where(alias1.post_id == alias2.post_id) - .where(alias2.pool_id == target_pool_id))) + update_stmt = sa.sql.expression.update(alias1).where( + alias1.pool_id == source_pool_id + ) + update_stmt = update_stmt.where( + ~sa.exists() + .where(alias1.post_id == alias2.post_id) + .where(alias2.pool_id == target_pool_id) + ) update_stmt = update_stmt.values(pool_id=target_pool_id) db.session.execute(update_stmt) @@ -245,9 +254,8 @@ def merge_pools(source_pool: model.Pool, target_pool: model.Pool) -> None: def create_pool( - names: List[str], - category_name: str, - post_ids: List[int]) -> model.Pool: + names: List[str], category_name: str, post_ids: List[int] +) -> model.Pool: pool = model.Pool() pool.creation_time = datetime.utcnow() update_pool_names(pool, names) @@ -266,7 +274,7 @@ def update_pool_names(pool: model.Pool, names: List[str]) -> None: assert pool names = util.icase_unique([name for name in names if name]) if not len(names): - raise InvalidPoolNameError('At least one name must be specified.') + raise InvalidPoolNameError("At least one name must be specified.") for name in names: _verify_name_validity(name) @@ -279,7 +287,8 @@ def update_pool_names(pool: model.Pool, names: List[str]) -> None: existing_pools = db.session.query(model.PoolName).filter(expr).all() if len(existing_pools): raise PoolAlreadyExistsError( - 'One of names is already used by another pool.') + "One of names is already used by another pool." + ) # remove unwanted items for pool_name in pool.names[:]: @@ -300,7 +309,7 @@ def update_pool_names(pool: model.Pool, names: List[str]) -> None: def update_pool_description(pool: model.Pool, description: str) -> None: assert pool 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 @@ -308,14 +317,15 @@ def update_pool_posts(pool: model.Pool, post_ids: List[int]) -> None: assert pool dupes = _duplicates(post_ids) if len(dupes) > 0: - dupes = ', '.join(list(str(x) for x in dupes)) - raise InvalidPoolDuplicateError('Duplicate post(s) in pool: ' + dupes) + 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) if len(post_ids) != len(ret): missing = set(post_ids) - set(post.post_id for post in ret) - missing = ', '.join(list(str(x) for x in missing)) + missing = ", ".join(list(str(x) for x in missing)) raise InvalidPoolNonexistentPostError( - 'The following posts do not exist: ' + missing) + "The following posts do not exist: " + missing + ) pool.posts.clear() for post in ret: pool.posts.append(post) diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index e18656a..67996b6 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -1,21 +1,34 @@ -import logging import hmac -from typing import Any, Optional, Tuple, List, Dict, Callable +import logging from datetime import datetime +from typing import Any, Callable, Dict, List, Optional, Tuple + import sqlalchemy as sa -from szurubooru import config, db, model, errors, rest -from szurubooru.func import ( - users, scores, comments, tags, pools, util, - mime, images, files, image_hash, serialization, snapshots) +from szurubooru import config, db, errors, model, rest +from szurubooru.func import ( + comments, + files, + image_hash, + images, + mime, + pools, + scores, + serialization, + snapshots, + tags, + users, + util, +) logger = logging.getLogger(__name__) EMPTY_PIXEL = ( - b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00' - b'\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x01\x00\x2c\x00\x00\x00\x00' - b'\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b') + b"\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00" + b"\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x01\x00\x2c\x00\x00\x00\x00" + b"\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b" +) class PostNotFoundError(errors.NotFoundError): @@ -29,11 +42,12 @@ class PostAlreadyFeaturedError(errors.ValidationError): class PostAlreadyUploadedError(errors.ValidationError): def __init__(self, other_post: model.Post) -> None: super().__init__( - 'Post already uploaded (%d)' % other_post.post_id, + "Post already uploaded (%d)" % other_post.post_id, { - 'otherPostUrl': get_post_content_url(other_post), - 'otherPostId': other_post.post_id, - }) + "otherPostUrl": get_post_content_url(other_post), + "otherPostId": other_post.post_id, + }, + ) class InvalidPostIdError(errors.ValidationError): @@ -65,75 +79,82 @@ class InvalidPostFlagError(errors.ValidationError): SAFETY_MAP = { - model.Post.SAFETY_SAFE: 'safe', - model.Post.SAFETY_SKETCHY: 'sketchy', - model.Post.SAFETY_UNSAFE: 'unsafe', + model.Post.SAFETY_SAFE: "safe", + model.Post.SAFETY_SKETCHY: "sketchy", + model.Post.SAFETY_UNSAFE: "unsafe", } TYPE_MAP = { - model.Post.TYPE_IMAGE: 'image', - model.Post.TYPE_ANIMATION: 'animation', - model.Post.TYPE_VIDEO: 'video', - model.Post.TYPE_FLASH: 'flash', + model.Post.TYPE_IMAGE: "image", + model.Post.TYPE_ANIMATION: "animation", + model.Post.TYPE_VIDEO: "video", + model.Post.TYPE_FLASH: "flash", } FLAG_MAP = { - model.Post.FLAG_LOOP: 'loop', - model.Post.FLAG_SOUND: 'sound', + model.Post.FLAG_LOOP: "loop", + model.Post.FLAG_SOUND: "sound", } def get_post_security_hash(id: int) -> str: return hmac.new( - config.config['secret'].encode('utf8'), - msg=str(id).encode('utf-8'), - digestmod='md5').hexdigest()[0:16] + config.config["secret"].encode("utf8"), + msg=str(id).encode("utf-8"), + digestmod="md5", + ).hexdigest()[0:16] def get_post_content_url(post: model.Post) -> str: assert post - return '%s/posts/%d_%s.%s' % ( - config.config['data_url'].rstrip('/'), + return "%s/posts/%d_%s.%s" % ( + config.config["data_url"].rstrip("/"), post.post_id, get_post_security_hash(post.post_id), - mime.get_extension(post.mime_type) or 'dat') + mime.get_extension(post.mime_type) or "dat", + ) def get_post_thumbnail_url(post: model.Post) -> str: assert post - return '%s/generated-thumbnails/%d_%s.jpg' % ( - config.config['data_url'].rstrip('/'), + return "%s/generated-thumbnails/%d_%s.jpg" % ( + config.config["data_url"].rstrip("/"), post.post_id, - get_post_security_hash(post.post_id)) + get_post_security_hash(post.post_id), + ) def get_post_content_path(post: model.Post) -> str: assert post assert post.post_id - return 'posts/%d_%s.%s' % ( + return "posts/%d_%s.%s" % ( post.post_id, get_post_security_hash(post.post_id), - mime.get_extension(post.mime_type) or 'dat') + mime.get_extension(post.mime_type) or "dat", + ) def get_post_thumbnail_path(post: model.Post) -> str: assert post - return 'generated-thumbnails/%d_%s.jpg' % ( + return "generated-thumbnails/%d_%s.jpg" % ( post.post_id, - get_post_security_hash(post.post_id)) + get_post_security_hash(post.post_id), + ) def get_post_thumbnail_backup_path(post: model.Post) -> str: assert post - return 'posts/custom-thumbnails/%d_%s.dat' % ( - post.post_id, get_post_security_hash(post.post_id)) + return "posts/custom-thumbnails/%d_%s.dat" % ( + post.post_id, + get_post_security_hash(post.post_id), + ) def serialize_note(note: model.PostNote) -> rest.Response: assert note return { - 'polygon': note.polygon, - 'text': note.text, + "polygon": note.polygon, + "text": note.text, } @@ -144,39 +165,39 @@ class PostSerializer(serialization.BaseSerializer): def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'id': self.serialize_id, - 'version': self.serialize_version, - 'creationTime': self.serialize_creation_time, - 'lastEditTime': self.serialize_last_edit_time, - 'safety': self.serialize_safety, - 'source': self.serialize_source, - 'type': self.serialize_type, - 'mimeType': self.serialize_mime, - 'checksum': self.serialize_checksum, - 'fileSize': self.serialize_file_size, - 'canvasWidth': self.serialize_canvas_width, - 'canvasHeight': self.serialize_canvas_height, - 'contentUrl': self.serialize_content_url, - 'thumbnailUrl': self.serialize_thumbnail_url, - 'flags': self.serialize_flags, - 'tags': self.serialize_tags, - 'relations': self.serialize_relations, - 'user': self.serialize_user, - 'score': self.serialize_score, - 'ownScore': self.serialize_own_score, - 'ownFavorite': self.serialize_own_favorite, - 'tagCount': self.serialize_tag_count, - 'favoriteCount': self.serialize_favorite_count, - 'commentCount': self.serialize_comment_count, - 'noteCount': self.serialize_note_count, - 'relationCount': self.serialize_relation_count, - 'featureCount': self.serialize_feature_count, - 'lastFeatureTime': self.serialize_last_feature_time, - 'favoritedBy': self.serialize_favorited_by, - 'hasCustomThumbnail': self.serialize_has_custom_thumbnail, - 'notes': self.serialize_notes, - 'comments': self.serialize_comments, - 'pools': self.serialize_pools, + "id": self.serialize_id, + "version": self.serialize_version, + "creationTime": self.serialize_creation_time, + "lastEditTime": self.serialize_last_edit_time, + "safety": self.serialize_safety, + "source": self.serialize_source, + "type": self.serialize_type, + "mimeType": self.serialize_mime, + "checksum": self.serialize_checksum, + "fileSize": self.serialize_file_size, + "canvasWidth": self.serialize_canvas_width, + "canvasHeight": self.serialize_canvas_height, + "contentUrl": self.serialize_content_url, + "thumbnailUrl": self.serialize_thumbnail_url, + "flags": self.serialize_flags, + "tags": self.serialize_tags, + "relations": self.serialize_relations, + "user": self.serialize_user, + "score": self.serialize_score, + "ownScore": self.serialize_own_score, + "ownFavorite": self.serialize_own_favorite, + "tagCount": self.serialize_tag_count, + "favoriteCount": self.serialize_favorite_count, + "commentCount": self.serialize_comment_count, + "noteCount": self.serialize_note_count, + "relationCount": self.serialize_relation_count, + "featureCount": self.serialize_feature_count, + "lastFeatureTime": self.serialize_last_feature_time, + "favoritedBy": self.serialize_favorited_by, + "hasCustomThumbnail": self.serialize_has_custom_thumbnail, + "notes": self.serialize_notes, + "comments": self.serialize_comments, + "pools": self.serialize_pools, } def serialize_id(self) -> Any: @@ -227,21 +248,24 @@ class PostSerializer(serialization.BaseSerializer): def serialize_tags(self) -> Any: return [ { - 'names': [name.name for name in tag.names], - 'category': tag.category.name, - 'usages': tag.post_count, + "names": [name.name for name in tag.names], + "category": tag.category.name, + "usages": tag.post_count, } - for tag in tags.sort_tags(self.post.tags)] + for tag in tags.sort_tags(self.post.tags) + ] def serialize_relations(self) -> Any: return sorted( { - post['id']: post + post["id"]: post for post in [ serialize_micro_post(rel, self.auth_user) - for rel in self.post.relations] + for rel in self.post.relations + ] }.values(), - key=lambda post: post['id']) + key=lambda post: post["id"], + ) def serialize_user(self) -> Any: return users.serialize_micro_user(self.post.user, self.auth_user) @@ -253,10 +277,16 @@ class PostSerializer(serialization.BaseSerializer): return scores.get_score(self.post, self.auth_user) def serialize_own_favorite(self) -> Any: - return len([ - user for user in self.post.favorited_by - if user.user_id == self.auth_user.user_id] - ) > 0 + return ( + len( + [ + user + for user in self.post.favorited_by + if user.user_id == self.auth_user.user_id + ] + ) + > 0 + ) def serialize_tag_count(self) -> Any: return self.post.tag_count @@ -291,36 +321,40 @@ class PostSerializer(serialization.BaseSerializer): def serialize_notes(self) -> Any: return sorted( [serialize_note(note) for note in self.post.notes], - key=lambda x: x['polygon']) + key=lambda x: x["polygon"], + ) def serialize_comments(self) -> Any: return [ comments.serialize_comment(comment, self.auth_user) for comment in sorted( - self.post.comments, - key=lambda comment: comment.creation_time)] + self.post.comments, key=lambda comment: comment.creation_time + ) + ] def serialize_pools(self) -> List[Any]: return [ pools.serialize_pool(pool) for pool in sorted( - self.post.pools, - key=lambda pool: pool.creation_time)] + self.post.pools, key=lambda pool: pool.creation_time + ) + ] def serialize_post( - post: Optional[model.Post], - auth_user: model.User, - options: List[str] = []) -> Optional[rest.Response]: + post: Optional[model.Post], auth_user: model.User, options: List[str] = [] +) -> Optional[rest.Response]: if not post: return None return PostSerializer(post, auth_user).serialize(options) def serialize_micro_post( - post: model.Post, auth_user: model.User) -> Optional[rest.Response]: + post: model.Post, auth_user: model.User +) -> Optional[rest.Response]: return serialize_post( - post, auth_user=auth_user, options=['id', 'thumbnailUrl']) + post, auth_user=auth_user, options=["id", "thumbnailUrl"] + ) def get_post_count() -> int: @@ -329,16 +363,16 @@ def get_post_count() -> int: def try_get_post_by_id(post_id: int) -> Optional[model.Post]: return ( - db.session - .query(model.Post) + db.session.query(model.Post) .filter(model.Post.post_id == post_id) - .one_or_none()) + .one_or_none() + ) def get_post_by_id(post_id: int) -> model.Post: post = try_get_post_by_id(post_id) if not post: - raise PostNotFoundError('Post %r not found.' % post_id) + raise PostNotFoundError("Post %r not found." % post_id) return post @@ -347,23 +381,19 @@ def get_posts_by_ids(ids: List[int]) -> List[model.Post]: 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) - } + .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 - .query(model.PostFeature) + db.session.query(model.PostFeature) .order_by(model.PostFeature.time.desc()) - .first()) + .first() + ) def try_get_featured_post() -> Optional[model.Post]: @@ -372,18 +402,17 @@ def try_get_featured_post() -> Optional[model.Post]: def create_post( - content: bytes, - tag_names: List[str], - user: Optional[model.User]) -> Tuple[model.Post, List[model.Tag]]: + content: bytes, tag_names: List[str], user: Optional[model.User] +) -> Tuple[model.Post, List[model.Tag]]: post = model.Post() post.safety = model.Post.SAFETY_SAFE post.user = user post.creation_time = datetime.utcnow() post.flags = [] - post.type = '' - post.checksum = '' - post.mime_type = '' + post.type = "" + post.checksum = "" + post.mime_type = "" update_post_content(post, content) new_tags = update_post_tags(post, tag_names) @@ -397,34 +426,38 @@ def update_post_safety(post: model.Post, safety: str) -> None: safety = util.flip(SAFETY_MAP).get(safety, None) if not safety: raise InvalidPostSafetyError( - 'Safety can be either of %r.' % list(SAFETY_MAP.values())) + "Safety can be either of %r." % list(SAFETY_MAP.values()) + ) post.safety = safety def update_post_source(post: model.Post, source: Optional[str]) -> None: assert post if util.value_exceeds_column_size(source, model.Post.source): - raise InvalidPostSourceError('Source is too long.') + raise InvalidPostSourceError("Source is too long.") post.source = source or None -@sa.events.event.listens_for(model.Post, 'after_insert') +@sa.events.event.listens_for(model.Post, "after_insert") def _after_post_insert( - _mapper: Any, _connection: Any, post: model.Post) -> None: + _mapper: Any, _connection: Any, post: model.Post +) -> None: _sync_post_content(post) -@sa.events.event.listens_for(model.Post, 'after_update') +@sa.events.event.listens_for(model.Post, "after_update") def _after_post_update( - _mapper: Any, _connection: Any, post: model.Post) -> None: + _mapper: Any, _connection: Any, post: model.Post +) -> None: _sync_post_content(post) -@sa.events.event.listens_for(model.Post, 'before_delete') +@sa.events.event.listens_for(model.Post, "before_delete") def _before_post_delete( - _mapper: Any, _connection: Any, post: model.Post) -> None: + _mapper: Any, _connection: Any, post: model.Post +) -> None: if post.post_id: - if config.config['delete_source_files']: + if config.config["delete_source_files"]: files.delete(get_post_content_path(post)) files.delete(get_post_thumbnail_path(post)) @@ -432,50 +465,50 @@ def _before_post_delete( def _sync_post_content(post: model.Post) -> None: regenerate_thumb = False - if hasattr(post, '__content'): - content = getattr(post, '__content') + if hasattr(post, "__content"): + content = getattr(post, "__content") files.save(get_post_content_path(post), content) - delattr(post, '__content') + delattr(post, "__content") regenerate_thumb = True - if hasattr(post, '__thumbnail'): - if getattr(post, '__thumbnail'): + if hasattr(post, "__thumbnail"): + if getattr(post, "__thumbnail"): files.save( get_post_thumbnail_backup_path(post), - getattr(post, '__thumbnail')) + getattr(post, "__thumbnail"), + ) else: files.delete(get_post_thumbnail_backup_path(post)) - delattr(post, '__thumbnail') + delattr(post, "__thumbnail") regenerate_thumb = True if regenerate_thumb: generate_post_thumbnail(post) -def generate_alternate_formats(post: model.Post, content: bytes) \ - -> List[Tuple[model.Post, List[model.Tag]]]: +def generate_alternate_formats( + post: model.Post, content: bytes +) -> List[Tuple[model.Post, List[model.Tag]]]: assert post assert content new_posts = [] if mime.is_animated_gif(content): tag_names = [tag.first_name for tag in post.tags] - if config.config['convert']['gif']['to_mp4']: + if config.config["convert"]["gif"]["to_mp4"]: mp4_post, new_tags = create_post( - images.Image(content).to_mp4(), - tag_names, - post.user) - update_post_flags(mp4_post, ['loop']) + images.Image(content).to_mp4(), tag_names, post.user + ) + update_post_flags(mp4_post, ["loop"]) update_post_safety(mp4_post, post.safety) update_post_source(mp4_post, post.source) new_posts += [(mp4_post, new_tags)] - if config.config['convert']['gif']['to_webm']: + if config.config["convert"]["gif"]["to_webm"]: webm_post, new_tags = create_post( - images.Image(content).to_webm(), - tag_names, - post.user) - update_post_flags(webm_post, ['loop']) + images.Image(content).to_webm(), tag_names, post.user + ) + update_post_flags(webm_post, ["loop"]) update_post_safety(webm_post, post.safety) update_post_source(webm_post, post.source) new_posts += [(webm_post, new_tags)] @@ -502,10 +535,11 @@ def get_default_flags(content: bytes) -> List[str]: def purge_post_signature(post: model.Post) -> None: - (db.session - .query(model.PostSignature) + ( + db.session.query(model.PostSignature) .filter(model.PostSignature.post_id == post.post_id) - .delete()) + .delete() + ) def generate_post_signature(post: model.Post, content: bytes) -> None: @@ -514,30 +548,36 @@ def generate_post_signature(post: model.Post, content: bytes) -> None: packed_signature = image_hash.pack_signature(unpacked_signature) words = image_hash.generate_words(unpacked_signature) - db.session.add(model.PostSignature( - post=post, signature=packed_signature, words=words)) + db.session.add( + model.PostSignature( + post=post, signature=packed_signature, words=words + ) + ) except errors.ProcessingError: - if not config.config['allow_broken_uploads']: + if not config.config["allow_broken_uploads"]: raise InvalidPostContentError( - 'Unable to generate image hash data.') + "Unable to generate image hash data." + ) def update_all_post_signatures() -> None: posts_to_hash = ( - db.session - .query(model.Post) + db.session.query(model.Post) .filter( - (model.Post.type == model.Post.TYPE_IMAGE) | - (model.Post.type == model.Post.TYPE_ANIMATION)) + (model.Post.type == model.Post.TYPE_IMAGE) + | (model.Post.type == model.Post.TYPE_ANIMATION) + ) .filter(model.Post.signature == None) # noqa: E711 .order_by(model.Post.post_id.asc()) - .all()) + .all() + ) for post in posts_to_hash: try: generate_post_signature( - post, files.get(get_post_content_path(post))) + post, files.get(get_post_content_path(post)) + ) db.session.commit() - logger.info('Hashed Post %d', post.post_id) + logger.info("Hashed Post %d", post.post_id) except Exception as ex: logger.exception(ex) @@ -545,7 +585,7 @@ def update_all_post_signatures() -> None: def update_post_content(post: model.Post, content: Optional[bytes]) -> None: assert post if not content: - raise InvalidPostContentError('Post content missing.') + raise InvalidPostContentError("Post content missing.") update_signature = False post.mime_type = mime.get_mime_type(content) @@ -561,18 +601,21 @@ def update_post_content(post: model.Post, content: Optional[bytes]) -> None: post.type = model.Post.TYPE_VIDEO else: raise InvalidPostContentError( - 'Unhandled file type: %r' % post.mime_type) + "Unhandled file type: %r" % post.mime_type + ) post.checksum = util.get_sha1(content) other_post = ( - db.session - .query(model.Post) + db.session.query(model.Post) .filter(model.Post.checksum == post.checksum) .filter(model.Post.post_id != post.post_id) - .one_or_none()) - if other_post \ - and other_post.post_id \ - and other_post.post_id != post.post_id: + .one_or_none() + ) + if ( + other_post + and other_post.post_id + and other_post.post_id != post.post_id + ): raise PostAlreadyUploadedError(other_post) if update_signature: @@ -585,27 +628,29 @@ def update_post_content(post: model.Post, content: Optional[bytes]) -> None: post.canvas_width = image.width post.canvas_height = image.height except errors.ProcessingError: - if not config.config['allow_broken_uploads']: - raise InvalidPostContentError( - 'Unable to process image metadata') + if not config.config["allow_broken_uploads"]: + raise InvalidPostContentError("Unable to process image metadata") else: post.canvas_width = None post.canvas_height = None - if (post.canvas_width is not None and post.canvas_width <= 0) \ - or (post.canvas_height is not None and post.canvas_height <= 0): - if not config.config['allow_broken_uploads']: + if (post.canvas_width is not None and post.canvas_width <= 0) or ( + post.canvas_height is not None and post.canvas_height <= 0 + ): + if not config.config["allow_broken_uploads"]: raise InvalidPostContentError( - 'Invalid image dimensions returned during processing') + "Invalid image dimensions returned during processing" + ) else: post.canvas_width = None post.canvas_height = None - setattr(post, '__content', content) + setattr(post, "__content", content) def update_post_thumbnail( - post: model.Post, content: Optional[bytes] = None) -> None: + post: model.Post, content: Optional[bytes] = None +) -> None: assert post - setattr(post, '__thumbnail', content) + setattr(post, "__thumbnail", content) def generate_post_thumbnail(post: model.Post) -> None: @@ -618,15 +663,17 @@ def generate_post_thumbnail(post: model.Post) -> None: assert content image = images.Image(content) image.resize_fill( - int(config.config['thumbnails']['post_width']), - int(config.config['thumbnails']['post_height'])) + int(config.config["thumbnails"]["post_width"]), + int(config.config["thumbnails"]["post_height"]), + ) files.save(get_post_thumbnail_path(post), image.to_jpeg()) except errors.ProcessingError: files.save(get_post_thumbnail_path(post), EMPTY_PIXEL) def update_post_tags( - post: model.Post, tag_names: List[str]) -> List[model.Tag]: + post: model.Post, tag_names: List[str] +) -> List[model.Tag]: assert post existing_tags, new_tags = tags.get_or_create_tags_by_names(tag_names) post.tags = existing_tags + new_tags @@ -638,22 +685,21 @@ def update_post_relations(post: model.Post, new_post_ids: List[int]) -> None: try: new_post_ids = [int(id) for id in new_post_ids] except ValueError: - raise InvalidPostRelationError( - 'A relation must be numeric post ID.') + raise InvalidPostRelationError("A relation must be numeric post ID.") old_posts = post.relations old_post_ids = [int(p.post_id) for p in old_posts] if new_post_ids: new_posts = ( - db.session - .query(model.Post) + db.session.query(model.Post) .filter(model.Post.post_id.in_(new_post_ids)) - .all()) + .all() + ) else: new_posts = [] if len(new_posts) != len(new_post_ids): - raise InvalidPostRelationError('One of relations does not exist.') + raise InvalidPostRelationError("One of relations does not exist.") if post.post_id in new_post_ids: - raise InvalidPostRelationError('Post cannot relate to itself.') + raise InvalidPostRelationError("Post cannot relate to itself.") relations_to_del = [p for p in old_posts if p.post_id not in new_post_ids] relations_to_add = [p for p in new_posts if p.post_id not in old_post_ids] @@ -669,37 +715,44 @@ def update_post_notes(post: model.Post, notes: Any) -> None: assert post post.notes = [] for note in notes: - for field in ('polygon', 'text'): + for field in ("polygon", "text"): if field not in note: - raise InvalidPostNoteError('Note is missing %r field.' % field) - if not note['text']: - raise InvalidPostNoteError('A note\'s text cannot be empty.') - if not isinstance(note['polygon'], (list, tuple)): + raise InvalidPostNoteError("Note is missing %r field." % field) + if not note["text"]: + raise InvalidPostNoteError("A note's text cannot be empty.") + if not isinstance(note["polygon"], (list, tuple)): raise InvalidPostNoteError( - 'A note\'s polygon must be a list of points.') - if len(note['polygon']) < 3: + "A note's polygon must be a list of points." + ) + if len(note["polygon"]) < 3: raise InvalidPostNoteError( - 'A note\'s polygon must have at least 3 points.') - for point in note['polygon']: + "A note's polygon must have at least 3 points." + ) + for point in note["polygon"]: if not isinstance(point, (list, tuple)): raise InvalidPostNoteError( - 'A note\'s polygon point must be a list of length 2.') + "A note's polygon point must be a list of length 2." + ) if len(point) != 2: raise InvalidPostNoteError( - 'A point in note\'s polygon must have two coordinates.') + "A point in note's polygon must have two coordinates." + ) try: pos_x = float(point[0]) pos_y = float(point[1]) if not 0 <= pos_x <= 1 or not 0 <= pos_y <= 1: raise InvalidPostNoteError( - 'All points must fit in the image (0..1 range).') + "All points must fit in the image (0..1 range)." + ) except ValueError: raise InvalidPostNoteError( - 'A point in note\'s polygon must be numeric.') - if util.value_exceeds_column_size(note['text'], model.PostNote.text): - raise InvalidPostNoteError('Note text is too long.') + "A point in note's polygon must be numeric." + ) + if util.value_exceeds_column_size(note["text"], model.PostNote.text): + raise InvalidPostNoteError("Note text is too long.") post.notes.append( - model.PostNote(polygon=note['polygon'], text=str(note['text']))) + model.PostNote(polygon=note["polygon"], text=str(note["text"])) + ) def update_post_flags(post: model.Post, flags: List[str]) -> None: @@ -709,7 +762,8 @@ def update_post_flags(post: model.Post, flags: List[str]) -> None: flag = util.flip(FLAG_MAP).get(flag, None) if not flag: raise InvalidPostFlagError( - 'Flag must be one of %r.' % list(FLAG_MAP.values())) + "Flag must be one of %r." % list(FLAG_MAP.values()) + ) target_flags.append(flag) post.flags = target_flags @@ -729,32 +783,31 @@ def delete(post: model.Post) -> None: def merge_posts( - source_post: model.Post, - target_post: model.Post, - replace_content: bool) -> None: + source_post: model.Post, target_post: model.Post, replace_content: bool +) -> None: assert source_post assert target_post if source_post.post_id == target_post.post_id: - raise InvalidPostRelationError('Cannot merge post with itself.') + raise InvalidPostRelationError("Cannot merge post with itself.") def merge_tables( - table: model.Base, - anti_dup_func: Optional[Callable[[model.Base, model.Base], bool]], - source_post_id: int, - target_post_id: int) -> None: + table: model.Base, + anti_dup_func: Optional[Callable[[model.Base, model.Base], bool]], + source_post_id: int, + target_post_id: int, + ) -> None: alias1 = table alias2 = sa.orm.util.aliased(table) - update_stmt = ( - sa.sql.expression.update(alias1) - .where(alias1.post_id == source_post_id)) + update_stmt = sa.sql.expression.update(alias1).where( + alias1.post_id == source_post_id + ) if anti_dup_func is not None: - update_stmt = ( - update_stmt - .where( - ~sa.exists() - .where(anti_dup_func(alias1, alias2)) - .where(alias2.post_id == target_post_id))) + update_stmt = update_stmt.where( + ~sa.exists() + .where(anti_dup_func(alias1, alias2)) + .where(alias2.post_id == target_post_id) + ) update_stmt = update_stmt.values(post_id=target_post_id) db.session.execute(update_stmt) @@ -764,21 +817,24 @@ def merge_posts( model.PostTag, lambda alias1, alias2: alias1.tag_id == alias2.tag_id, source_post_id, - target_post_id) + target_post_id, + ) def merge_scores(source_post_id: int, target_post_id: int) -> None: merge_tables( model.PostScore, lambda alias1, alias2: alias1.user_id == alias2.user_id, source_post_id, - target_post_id) + target_post_id, + ) def merge_favorites(source_post_id: int, target_post_id: int) -> None: merge_tables( model.PostFavorite, lambda alias1, alias2: alias1.user_id == alias2.user_id, source_post_id, - target_post_id) + target_post_id, + ) def merge_comments(source_post_id: int, target_post_id: int) -> None: merge_tables(model.Comment, None, source_post_id, target_post_id) @@ -793,8 +849,10 @@ def merge_posts( .where( ~sa.exists() .where(alias2.child_id == alias1.child_id) - .where(alias2.parent_id == target_post_id)) - .values(parent_id=target_post_id)) + .where(alias2.parent_id == target_post_id) + ) + .values(parent_id=target_post_id) + ) db.session.execute(update_stmt) update_stmt = ( @@ -804,8 +862,10 @@ def merge_posts( .where( ~sa.exists() .where(alias2.parent_id == alias1.parent_id) - .where(alias2.child_id == target_post_id)) - .values(child_id=target_post_id)) + .where(alias2.child_id == target_post_id) + ) + .values(child_id=target_post_id) + ) db.session.execute(update_stmt) merge_tags(source_post.post_id, target_post.post_id) @@ -837,44 +897,49 @@ def merge_posts( def search_by_image_exact(image_content: bytes) -> Optional[model.Post]: checksum = util.get_sha1(image_content) return ( - db.session - .query(model.Post) + db.session.query(model.Post) .filter(model.Post.checksum == checksum) - .one_or_none()) + .one_or_none() + ) def search_by_image(image_content: bytes) -> List[Tuple[float, model.Post]]: query_signature = image_hash.generate_signature(image_content) query_words = image_hash.generate_words(query_signature) - ''' + """ The unnest function is used here to expand one row containing the 'words' array into multiple rows each containing a singular word. Documentation of the unnest function can be found here: https://www.postgresql.org/docs/9.2/functions-array.html - ''' + """ - dbquery = ''' + dbquery = """ SELECT s.post_id, s.signature, count(a.query) AS score FROM post_signature AS s, unnest(s.words, :q) AS a(word, query) WHERE a.word = a.query GROUP BY s.post_id ORDER BY score DESC LIMIT 100; - ''' - - candidates = db.session.execute(dbquery, {'q': query_words}) - data = tuple(zip(*[ - (post_id, image_hash.unpack_signature(packedsig)) - for post_id, packedsig, score in candidates - ])) + """ + + candidates = db.session.execute(dbquery, {"q": query_words}) + data = tuple( + zip( + *[ + (post_id, image_hash.unpack_signature(packedsig)) + for post_id, packedsig, score in candidates + ] + ) + ) if data: candidate_post_ids, sigarray = data distances = image_hash.normalized_distance(sigarray, query_signature) return [ (distance, try_get_post_by_id(candidate_post_id)) - for candidate_post_id, distance - in zip(candidate_post_ids, distances) + for candidate_post_id, distance in zip( + candidate_post_ids, distances + ) if distance < image_hash.DISTANCE_CUTOFF ] else: diff --git a/server/szurubooru/func/scores.py b/server/szurubooru/func/scores.py index 615fd98..b095f48 100644 --- a/server/szurubooru/func/scores.py +++ b/server/szurubooru/func/scores.py @@ -1,6 +1,7 @@ import datetime -from typing import Any, Tuple, Callable -from szurubooru import db, model, errors +from typing import Any, Callable, Tuple + +from szurubooru import db, errors, model class InvalidScoreTargetError(errors.ValidationError): @@ -12,12 +13,13 @@ class InvalidScoreValueError(errors.ValidationError): def _get_table_info( - entity: model.Base) -> Tuple[model.Base, Callable[[model.Base], Any]]: + entity: model.Base, +) -> Tuple[model.Base, Callable[[model.Base], Any]]: assert entity resource_type, _, _ = model.util.get_resource_info(entity) - if resource_type == 'post': + if resource_type == "post": return model.PostScore, lambda table: table.post_id - elif resource_type == 'comment': + elif resource_type == "comment": return model.CommentScore, lambda table: table.comment_id raise InvalidScoreTargetError() @@ -40,16 +42,17 @@ def get_score(entity: model.Base, user: model.User) -> int: assert user table, get_column = _get_table_info(entity) row = ( - db.session - .query(table.score) + db.session.query(table.score) .filter(get_column(table) == get_column(entity)) .filter(table.user_id == user.user_id) - .one_or_none()) + .one_or_none() + ) return row[0] if row else 0 def set_score(entity: model.Base, user: model.User, score: int) -> None: from szurubooru.func import favorites + assert entity assert user if not score: @@ -61,7 +64,8 @@ def set_score(entity: model.Base, user: model.User, score: int) -> None: return if score not in (-1, 1): raise InvalidScoreValueError( - 'Score %r is invalid. Valid scores: %r.' % (score, (-1, 1))) + "Score %r is invalid. Valid scores: %r." % (score, (-1, 1)) + ) score_entity = _get_score_entity(entity, user) if score_entity: score_entity.score = score diff --git a/server/szurubooru/func/serialization.py b/server/szurubooru/func/serialization.py index 699fb47..d2fadc0 100644 --- a/server/szurubooru/func/serialization.py +++ b/server/szurubooru/func/serialization.py @@ -1,9 +1,10 @@ -from typing import Any, List, Dict, Callable -from szurubooru import model, rest, errors +from typing import Any, Callable, Dict, List + +from szurubooru import errors, model, rest def get_serialization_options(ctx: rest.Context) -> List[str]: - return ctx.get_param_as_list('fields', default=[]) + return ctx.get_param_as_list("fields", default=[]) class BaseSerializer: @@ -17,8 +18,9 @@ class BaseSerializer: for key in options: if key not in field_factories: raise errors.ValidationError( - 'Invalid key: %r. Valid keys: %r.' % ( - key, list(sorted(field_factories.keys())))) + "Invalid key: %r. Valid keys: %r." + % (key, list(sorted(field_factories.keys()))) + ) factory = field_factories[key] ret[key] = factory() return ret diff --git a/server/szurubooru/func/snapshots.py b/server/szurubooru/func/snapshots.py index 38d42a9..b34b702 100644 --- a/server/szurubooru/func/snapshots.py +++ b/server/szurubooru/func/snapshots.py @@ -1,6 +1,8 @@ -from typing import Any, Optional, Dict, Callable from datetime import datetime +from typing import Any, Callable, Dict, Optional + import sqlalchemy as sa + from szurubooru import db, model from szurubooru.func import diff, users @@ -8,86 +10,95 @@ from szurubooru.func import diff, users def get_tag_category_snapshot(category: model.TagCategory) -> Dict[str, Any]: assert category return { - 'name': category.name, - 'color': category.color, - 'default': True if category.default else False, + "name": category.name, + "color": category.color, + "default": True if category.default else False, } def get_tag_snapshot(tag: model.Tag) -> Dict[str, Any]: assert tag return { - 'names': [tag_name.name for tag_name in tag.names], - 'category': tag.category.name, - 'suggestions': sorted(rel.first_name for rel in tag.suggestions), - 'implications': sorted(rel.first_name for rel in tag.implications), + "names": [tag_name.name for tag_name in tag.names], + "category": tag.category.name, + "suggestions": sorted(rel.first_name for rel in tag.suggestions), + "implications": sorted(rel.first_name for rel in tag.implications), } def get_pool_category_snapshot(category: model.PoolCategory) -> Dict[str, Any]: assert category return { - 'name': category.name, - 'color': category.color, - 'default': True if category.default else False, + "name": category.name, + "color": category.color, + "default": True if category.default else False, } def get_pool_snapshot(pool: model.Pool) -> Dict[str, Any]: assert pool return { - 'names': [pool_name.name for pool_name in pool.names], - 'category': pool.category.name, - 'posts': [post.post_id for post in pool.posts] + "names": [pool_name.name for pool_name in pool.names], + "category": pool.category.name, + "posts": [post.post_id for post in pool.posts], } def get_post_snapshot(post: model.Post) -> Dict[str, Any]: assert post return { - 'source': post.source, - 'safety': post.safety, - 'checksum': post.checksum, - 'flags': post.flags, - 'featured': post.is_featured, - 'tags': sorted([tag.first_name for tag in post.tags]), - 'relations': sorted([rel.post_id for rel in post.relations]), - 'notes': sorted([{ - 'polygon': [[point[0], point[1]] for point in note.polygon], - 'text': note.text, - } for note in post.notes], key=lambda x: x['polygon']), + "source": post.source, + "safety": post.safety, + "checksum": post.checksum, + "flags": post.flags, + "featured": post.is_featured, + "tags": sorted([tag.first_name for tag in post.tags]), + "relations": sorted([rel.post_id for rel in post.relations]), + "notes": sorted( + [ + { + "polygon": [ + [point[0], point[1]] for point in note.polygon + ], + "text": note.text, + } + for note in post.notes + ], + key=lambda x: x["polygon"], + ), } _snapshot_factories = { # lambdas allow mocking target functions in the tests - 'tag_category': lambda entity: get_tag_category_snapshot(entity), - 'tag': lambda entity: get_tag_snapshot(entity), - 'post': lambda entity: get_post_snapshot(entity), - 'pool_category': lambda entity: get_pool_category_snapshot(entity), - 'pool': lambda entity: get_pool_snapshot(entity), + "tag_category": lambda entity: get_tag_category_snapshot(entity), + "tag": lambda entity: get_tag_snapshot(entity), + "post": lambda entity: get_post_snapshot(entity), + "pool_category": lambda entity: get_pool_category_snapshot(entity), + "pool": lambda entity: get_pool_snapshot(entity), } # type: Dict[model.Base, Callable[[model.Base], Dict[str ,Any]]] def serialize_snapshot( - snapshot: model.Snapshot, auth_user: model.User) -> Dict[str, Any]: + snapshot: model.Snapshot, auth_user: model.User +) -> Dict[str, Any]: assert snapshot return { - 'operation': snapshot.operation, - 'type': snapshot.resource_type, - 'id': snapshot.resource_name, - 'user': users.serialize_micro_user(snapshot.user, auth_user), - 'data': snapshot.data, - 'time': snapshot.creation_time, + "operation": snapshot.operation, + "type": snapshot.resource_type, + "id": snapshot.resource_name, + "user": users.serialize_micro_user(snapshot.user, auth_user), + "data": snapshot.data, + "time": snapshot.creation_time, } def _create( - operation: str, - entity: model.Base, - auth_user: Optional[model.User]) -> model.Snapshot: - resource_type, resource_pkey, resource_name = ( - model.util.get_resource_info(entity)) + operation: str, entity: model.Base, auth_user: Optional[model.User] +) -> model.Snapshot: + resource_type, resource_pkey, resource_name = model.util.get_resource_info( + entity + ) snapshot = model.Snapshot() snapshot.creation_time = datetime.utcnow() @@ -114,10 +125,11 @@ def modify(entity: model.Base, auth_user: Optional[model.User]) -> None: ( cls for cls in model.Base._decl_class_registry.values() - if hasattr(cls, '__table__') + if hasattr(cls, "__table__") and cls.__table__.fullname == entity.__table__.fullname ), - None) + None, + ) assert table snapshot = _create(model.Snapshot.OPERATION_MODIFIED, entity, auth_user) @@ -125,7 +137,7 @@ def modify(entity: model.Base, auth_user: Optional[model.User]) -> None: detached_session = sa.orm.sessionmaker(bind=db.session.get_bind())() detached_entity = detached_session.query(table).get(snapshot.resource_pkey) - assert detached_entity, 'Entity not found in DB, have you committed it?' + assert detached_entity, "Entity not found in DB, have you committed it?" detached_snapshot = snapshot_factory(detached_entity) detached_session.close() @@ -146,14 +158,19 @@ def delete(entity: model.Base, auth_user: Optional[model.User]) -> None: def merge( - source_entity: model.Base, - target_entity: model.Base, - auth_user: Optional[model.User]) -> None: + source_entity: model.Base, + target_entity: model.Base, + auth_user: Optional[model.User], +) -> None: assert source_entity assert target_entity snapshot = _create( - model.Snapshot.OPERATION_MERGED, source_entity, auth_user) - resource_type, _resource_pkey, resource_name = ( - model.util.get_resource_info(target_entity)) + model.Snapshot.OPERATION_MERGED, source_entity, auth_user + ) + ( + resource_type, + _resource_pkey, + resource_name, + ) = model.util.get_resource_info(target_entity) snapshot.data = [resource_type, resource_name] db.session.add(snapshot) diff --git a/server/szurubooru/func/tag_categories.py b/server/szurubooru/func/tag_categories.py index ab0fc32..bbf7297 100644 --- a/server/szurubooru/func/tag_categories.py +++ b/server/szurubooru/func/tag_categories.py @@ -1,11 +1,12 @@ import re -from typing import Any, Optional, Dict, List, Callable +from typing import Any, Callable, Dict, List, Optional + import sqlalchemy as sa -from szurubooru import config, db, model, errors, rest -from szurubooru.func import util, serialization, cache +from szurubooru import config, db, errors, model, rest +from szurubooru.func import cache, serialization, util -DEFAULT_CATEGORY_NAME_CACHE_KEY = 'default-tag-category' +DEFAULT_CATEGORY_NAME_CACHE_KEY = "default-tag-category" class TagCategoryNotFoundError(errors.NotFoundError): @@ -29,10 +30,11 @@ class InvalidTagCategoryColorError(errors.ValidationError): def _verify_name_validity(name: str) -> None: - name_regex = config.config['tag_category_name_regex'] + name_regex = config.config["tag_category_name_regex"] if not re.match(name_regex, name): raise InvalidTagCategoryNameError( - 'Name must satisfy regex %r.' % name_regex) + "Name must satisfy regex %r." % name_regex + ) class TagCategorySerializer(serialization.BaseSerializer): @@ -41,11 +43,11 @@ class TagCategorySerializer(serialization.BaseSerializer): def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'name': self.serialize_name, - 'version': self.serialize_version, - 'color': self.serialize_color, - 'usages': self.serialize_usages, - 'default': self.serialize_default, + "name": self.serialize_name, + "version": self.serialize_version, + "color": self.serialize_color, + "usages": self.serialize_usages, + "default": self.serialize_default, } def serialize_name(self) -> Any: @@ -65,8 +67,8 @@ class TagCategorySerializer(serialization.BaseSerializer): def serialize_category( - category: Optional[model.TagCategory], - options: List[str] = []) -> Optional[rest.Response]: + category: Optional[model.TagCategory], options: List[str] = [] +) -> Optional[rest.Response]: if not category: return None return TagCategorySerializer(category).serialize(options) @@ -84,18 +86,21 @@ def create_category(name: str, color: str) -> model.TagCategory: def update_category_name(category: model.TagCategory, name: str) -> None: assert category if not name: - raise InvalidTagCategoryNameError('Name cannot be empty.') + raise InvalidTagCategoryNameError("Name cannot be empty.") expr = sa.func.lower(model.TagCategory.name) == name.lower() if category.tag_category_id: expr = expr & ( - model.TagCategory.tag_category_id != category.tag_category_id) + model.TagCategory.tag_category_id != category.tag_category_id + ) already_exists = ( - db.session.query(model.TagCategory).filter(expr).count() > 0) + db.session.query(model.TagCategory).filter(expr).count() > 0 + ) if already_exists: raise TagCategoryAlreadyExistsError( - 'A category with this name already exists.') + "A category with this name already exists." + ) if util.value_exceeds_column_size(name, model.TagCategory.name): - raise InvalidTagCategoryNameError('Name is too long.') + raise InvalidTagCategoryNameError("Name is too long.") _verify_name_validity(name) category.name = name cache.remove(DEFAULT_CATEGORY_NAME_CACHE_KEY) @@ -104,20 +109,20 @@ def update_category_name(category: model.TagCategory, name: str) -> None: def update_category_color(category: model.TagCategory, color: str) -> None: assert category if not color: - raise InvalidTagCategoryColorError('Color cannot be empty.') - if not re.match(r'^#?[0-9a-z]+$', color): - raise InvalidTagCategoryColorError('Invalid color.') + raise InvalidTagCategoryColorError("Color cannot be empty.") + if not re.match(r"^#?[0-9a-z]+$", color): + raise InvalidTagCategoryColorError("Invalid color.") if util.value_exceeds_column_size(color, model.TagCategory.color): - raise InvalidTagCategoryColorError('Color is too long.') + raise InvalidTagCategoryColorError("Color is too long.") category.color = color def try_get_category_by_name( - name: str, lock: bool = False) -> Optional[model.TagCategory]: - query = ( - db.session - .query(model.TagCategory) - .filter(sa.func.lower(model.TagCategory.name) == name.lower())) + name: str, lock: bool = False +) -> Optional[model.TagCategory]: + query = db.session.query(model.TagCategory).filter( + sa.func.lower(model.TagCategory.name) == name.lower() + ) if lock: query = query.with_for_update() return query.one_or_none() @@ -126,7 +131,7 @@ def try_get_category_by_name( def get_category_by_name(name: str, lock: bool = False) -> model.TagCategory: category = try_get_category_by_name(name, lock) if not category: - raise TagCategoryNotFoundError('Tag category %r not found.' % name) + raise TagCategoryNotFoundError("Tag category %r not found." % name) return category @@ -135,26 +140,28 @@ def get_all_category_names() -> List[str]: def get_all_categories() -> List[model.TagCategory]: - return db.session.query(model.TagCategory).order_by( - model.TagCategory.name.asc()).all() + return ( + db.session.query(model.TagCategory) + .order_by(model.TagCategory.name.asc()) + .all() + ) def try_get_default_category( - lock: bool = False) -> Optional[model.TagCategory]: - query = ( - db.session - .query(model.TagCategory) - .filter(model.TagCategory.default)) + lock: bool = False, +) -> Optional[model.TagCategory]: + query = db.session.query(model.TagCategory).filter( + model.TagCategory.default + ) if lock: query = query.with_for_update() category = query.first() # if for some reason (e.g. as a result of migration) there's no default # category, get the first record available. if not category: - query = ( - db.session - .query(model.TagCategory) - .order_by(model.TagCategory.tag_category_id.asc())) + query = db.session.query(model.TagCategory).order_by( + model.TagCategory.tag_category_id.asc() + ) if lock: query = query.with_for_update() category = query.first() @@ -164,7 +171,7 @@ def try_get_default_category( def get_default_category(lock: bool = False) -> model.TagCategory: category = try_get_default_category(lock) if not category: - raise TagCategoryNotFoundError('No tag category created yet.') + raise TagCategoryNotFoundError("No tag category created yet.") return category @@ -191,9 +198,10 @@ def set_default_category(category: model.TagCategory) -> None: def delete_category(category: model.TagCategory) -> None: assert category if len(get_all_category_names()) == 1: - raise TagCategoryIsInUseError('Cannot delete the last category.') + raise TagCategoryIsInUseError("Cannot delete the last category.") if (category.tag_count or 0) > 0: raise TagCategoryIsInUseError( - 'Tag category has some usages and cannot be deleted. ' + - 'Please remove this category from relevant tags first..') + "Tag category has some usages and cannot be deleted. " + + "Please remove this category from relevant tags first.." + ) db.session.delete(category) diff --git a/server/szurubooru/func/tags.py b/server/szurubooru/func/tags.py index 60eda50..e15a78c 100644 --- a/server/szurubooru/func/tags.py +++ b/server/szurubooru/func/tags.py @@ -1,9 +1,11 @@ import re -from typing import Any, Optional, Tuple, List, Dict, Callable from datetime import datetime +from typing import Any, Callable, Dict, List, Optional, Tuple + import sqlalchemy as sa -from szurubooru import config, db, model, errors, rest -from szurubooru.func import util, tag_categories, serialization + +from szurubooru import config, db, errors, model, rest +from szurubooru.func import serialization, tag_categories, util class TagNotFoundError(errors.NotFoundError): @@ -36,10 +38,10 @@ class InvalidTagDescriptionError(errors.ValidationError): def _verify_name_validity(name: str) -> None: if util.value_exceeds_column_size(name, model.TagName.name): - raise InvalidTagNameError('Name is too long.') - name_regex = config.config['tag_name_regex'] + raise InvalidTagNameError("Name is too long.") + name_regex = config.config["tag_name_regex"] if not re.match(name_regex, name): - raise InvalidTagNameError('Name must satisfy regex %r.' % name_regex) + raise InvalidTagNameError("Name must satisfy regex %r." % name_regex) def _get_names(tag: model.Tag) -> List[str]: @@ -52,7 +54,8 @@ def _lower_list(names: List[str]) -> List[str]: def _check_name_intersection( - names1: List[str], names2: List[str], case_sensitive: bool) -> bool: + names1: List[str], names2: List[str], case_sensitive: bool +) -> bool: if not case_sensitive: names1 = _lower_list(names1) names2 = _lower_list(names2) @@ -66,15 +69,16 @@ def sort_tags(tags: List[model.Tag]) -> List[model.Tag]: key=lambda tag: ( default_category_name == tag.category.name, tag.category.name, - tag.names[0].name) + tag.names[0].name, + ), ) def serialize_relation(tag): return { - 'names': [tag_name.name for tag_name in tag.names], - 'category': tag.category.name, - 'usages': tag.post_count, + "names": [tag_name.name for tag_name in tag.names], + "category": tag.category.name, + "usages": tag.post_count, } @@ -84,15 +88,15 @@ class TagSerializer(serialization.BaseSerializer): def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'names': self.serialize_names, - 'category': self.serialize_category, - 'version': self.serialize_version, - 'description': self.serialize_description, - 'creationTime': self.serialize_creation_time, - 'lastEditTime': self.serialize_last_edit_time, - 'usages': self.serialize_usages, - 'suggestions': self.serialize_suggestions, - 'implications': self.serialize_implications, + "names": self.serialize_names, + "category": self.serialize_category, + "version": self.serialize_version, + "description": self.serialize_description, + "creationTime": self.serialize_creation_time, + "lastEditTime": self.serialize_last_edit_time, + "usages": self.serialize_usages, + "suggestions": self.serialize_suggestions, + "implications": self.serialize_implications, } def serialize_names(self) -> Any: @@ -119,16 +123,19 @@ class TagSerializer(serialization.BaseSerializer): def serialize_suggestions(self) -> Any: return [ serialize_relation(relation) - for relation in sort_tags(self.tag.suggestions)] + for relation in sort_tags(self.tag.suggestions) + ] def serialize_implications(self) -> Any: return [ serialize_relation(relation) - for relation in sort_tags(self.tag.implications)] + for relation in sort_tags(self.tag.implications) + ] def serialize_tag( - tag: model.Tag, options: List[str] = []) -> Optional[rest.Response]: + tag: model.Tag, options: List[str] = [] +) -> Optional[rest.Response]: if not tag: return None return TagSerializer(tag).serialize(options) @@ -136,17 +143,17 @@ def serialize_tag( def try_get_tag_by_name(name: str) -> Optional[model.Tag]: return ( - db.session - .query(model.Tag) + db.session.query(model.Tag) .join(model.TagName) .filter(sa.func.lower(model.TagName.name) == name.lower()) - .one_or_none()) + .one_or_none() + ) def get_tag_by_name(name: str) -> model.Tag: tag = try_get_tag_by_name(name) if not tag: - raise TagNotFoundError('Tag %r not found.' % name) + raise TagNotFoundError("Tag %r not found." % name) return tag @@ -160,12 +167,16 @@ def get_tags_by_names(names: List[str]) -> List[model.Tag]: .filter( sa.sql.or_( sa.func.lower(model.TagName.name) == name.lower() - for name in names)) - .all()) + for name in names + ) + ) + .all() + ) def get_or_create_tags_by_names( - names: List[str]) -> Tuple[List[model.Tag], List[model.Tag]]: + names: List[str], +) -> Tuple[List[model.Tag], List[model.Tag]]: names = util.icase_unique(names) existing_tags = get_tags_by_names(names) new_tags = [] @@ -174,7 +185,8 @@ def get_or_create_tags_by_names( found = False for existing_tag in existing_tags: if _check_name_intersection( - _get_names(existing_tag), [name], False): + _get_names(existing_tag), [name], False + ): found = True break if not found: @@ -182,7 +194,8 @@ def get_or_create_tags_by_names( names=[name], category_name=tag_category_name, suggestions=[], - implications=[]) + implications=[], + ) db.session.add(new_tag) new_tags.append(new_tag) return existing_tags, new_tags @@ -194,8 +207,7 @@ def get_tag_siblings(tag: model.Tag) -> List[model.Tag]: pt_alias1 = sa.orm.aliased(model.PostTag) pt_alias2 = sa.orm.aliased(model.PostTag) result = ( - db.session - .query(tag_alias, sa.func.count(pt_alias2.post_id)) + db.session.query(tag_alias, sa.func.count(pt_alias2.post_id)) .join(pt_alias1, pt_alias1.tag_id == tag_alias.tag_id) .join(pt_alias2, pt_alias2.post_id == pt_alias1.post_id) .filter(pt_alias2.tag_id == tag.tag_id) @@ -203,18 +215,23 @@ def get_tag_siblings(tag: model.Tag) -> List[model.Tag]: .group_by(tag_alias.tag_id) .order_by(sa.func.count(pt_alias2.post_id).desc()) .order_by(tag_alias.first_name) - .limit(50)) + .limit(50) + ) return result def delete(source_tag: model.Tag) -> None: assert source_tag db.session.execute( - sa.sql.expression.delete(model.TagSuggestion) - .where(model.TagSuggestion.child_id == source_tag.tag_id)) + sa.sql.expression.delete(model.TagSuggestion).where( + model.TagSuggestion.child_id == source_tag.tag_id + ) + ) db.session.execute( - sa.sql.expression.delete(model.TagImplication) - .where(model.TagImplication.child_id == source_tag.tag_id)) + sa.sql.expression.delete(model.TagImplication).where( + model.TagImplication.child_id == source_tag.tag_id + ) + ) db.session.delete(source_tag) @@ -222,25 +239,25 @@ def merge_tags(source_tag: model.Tag, target_tag: model.Tag) -> None: assert source_tag assert target_tag if source_tag.tag_id == target_tag.tag_id: - raise InvalidTagRelationError('Cannot merge tag with itself.') + raise InvalidTagRelationError("Cannot merge tag with itself.") def merge_posts(source_tag_id: int, target_tag_id: int) -> None: alias1 = model.PostTag alias2 = sa.orm.util.aliased(model.PostTag) - update_stmt = ( - sa.sql.expression.update(alias1) - .where(alias1.tag_id == source_tag_id)) - update_stmt = ( - update_stmt - .where( - ~sa.exists() - .where(alias1.post_id == alias2.post_id) - .where(alias2.tag_id == target_tag_id))) + update_stmt = sa.sql.expression.update(alias1).where( + alias1.tag_id == source_tag_id + ) + update_stmt = update_stmt.where( + ~sa.exists() + .where(alias1.post_id == alias2.post_id) + .where(alias2.tag_id == target_tag_id) + ) update_stmt = update_stmt.values(tag_id=target_tag_id) db.session.execute(update_stmt) def merge_relations( - table: model.Base, source_tag_id: int, target_tag_id: int) -> None: + table: model.Base, source_tag_id: int, target_tag_id: int + ) -> None: alias1 = table alias2 = sa.orm.util.aliased(table) update_stmt = ( @@ -250,8 +267,10 @@ def merge_tags(source_tag: model.Tag, target_tag: model.Tag) -> None: .where( ~sa.exists() .where(alias2.child_id == alias1.child_id) - .where(alias2.parent_id == target_tag_id)) - .values(parent_id=target_tag_id)) + .where(alias2.parent_id == target_tag_id) + ) + .values(parent_id=target_tag_id) + ) db.session.execute(update_stmt) update_stmt = ( @@ -261,8 +280,10 @@ def merge_tags(source_tag: model.Tag, target_tag: model.Tag) -> None: .where( ~sa.exists() .where(alias2.parent_id == alias1.parent_id) - .where(alias2.child_id == target_tag_id)) - .values(child_id=target_tag_id)) + .where(alias2.child_id == target_tag_id) + ) + .values(child_id=target_tag_id) + ) db.session.execute(update_stmt) def merge_suggestions(source_tag_id: int, target_tag_id: int) -> None: @@ -278,10 +299,11 @@ def merge_tags(source_tag: model.Tag, target_tag: model.Tag) -> None: def create_tag( - names: List[str], - category_name: str, - suggestions: List[str], - implications: List[str]) -> model.Tag: + names: List[str], + category_name: str, + suggestions: List[str], + implications: List[str], +) -> model.Tag: tag = model.Tag() tag.creation_time = datetime.utcnow() update_tag_names(tag, names) @@ -301,7 +323,7 @@ def update_tag_names(tag: model.Tag, names: List[str]) -> None: assert tag names = util.icase_unique([name for name in names if name]) if not len(names): - raise InvalidTagNameError('At least one name must be specified.') + raise InvalidTagNameError("At least one name must be specified.") for name in names: _verify_name_validity(name) @@ -314,7 +336,8 @@ def update_tag_names(tag: model.Tag, names: List[str]) -> None: existing_tags = db.session.query(model.TagName).filter(expr).all() if len(existing_tags): raise TagAlreadyExistsError( - 'One of names is already used by another tag.') + "One of names is already used by another tag." + ) # remove unwanted items for tag_name in tag.names[:]: @@ -336,7 +359,7 @@ def update_tag_names(tag: model.Tag, names: List[str]) -> None: def update_tag_implications(tag: model.Tag, relations: List[str]) -> None: assert tag if _check_name_intersection(_get_names(tag), relations, False): - raise InvalidTagRelationError('Tag cannot imply itself.') + raise InvalidTagRelationError("Tag cannot imply itself.") tag.implications = get_tags_by_names(relations) @@ -344,12 +367,12 @@ def update_tag_implications(tag: model.Tag, relations: List[str]) -> None: def update_tag_suggestions(tag: model.Tag, relations: List[str]) -> None: assert tag if _check_name_intersection(_get_names(tag), relations, False): - raise InvalidTagRelationError('Tag cannot suggest itself.') + raise InvalidTagRelationError("Tag cannot suggest itself.") tag.suggestions = get_tags_by_names(relations) def update_tag_description(tag: model.Tag, description: str) -> None: assert tag if util.value_exceeds_column_size(description, model.Tag.description): - raise InvalidTagDescriptionError('Description is too long.') + raise InvalidTagDescriptionError("Description is too long.") tag.description = description or None diff --git a/server/szurubooru/func/user_tokens.py b/server/szurubooru/func/user_tokens.py index c0f4bad..8d977e4 100644 --- a/server/szurubooru/func/user_tokens.py +++ b/server/szurubooru/func/user_tokens.py @@ -1,8 +1,10 @@ from datetime import datetime -from typing import Any, Optional, List, Dict, Callable -from pyrfc3339 import parser as rfc3339_parser +from typing import Any, Callable, Dict, List, Optional + import pytz -from szurubooru import db, model, rest, errors +from pyrfc3339 import parser as rfc3339_parser + +from szurubooru import db, errors, model, rest from szurubooru.func import auth, serialization, users, util @@ -16,23 +18,22 @@ class InvalidNoteError(errors.ValidationError): class UserTokenSerializer(serialization.BaseSerializer): def __init__( - self, - user_token: model.UserToken, - auth_user: model.User) -> None: + self, user_token: model.UserToken, auth_user: model.User + ) -> None: self.user_token = user_token self.auth_user = auth_user def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'user': self.serialize_user, - 'token': self.serialize_token, - 'note': self.serialize_note, - 'enabled': self.serialize_enabled, - 'expirationTime': self.serialize_expiration_time, - 'creationTime': self.serialize_creation_time, - 'lastEditTime': self.serialize_last_edit_time, - 'lastUsageTime': self.serialize_last_usage_time, - 'version': self.serialize_version, + "user": self.serialize_user, + "token": self.serialize_token, + "note": self.serialize_note, + "enabled": self.serialize_enabled, + "expirationTime": self.serialize_expiration_time, + "creationTime": self.serialize_creation_time, + "lastEditTime": self.serialize_last_edit_time, + "lastUsageTime": self.serialize_last_usage_time, + "version": self.serialize_version, } def serialize_user(self) -> Any: @@ -64,31 +65,31 @@ class UserTokenSerializer(serialization.BaseSerializer): def serialize_user_token( - user_token: Optional[model.UserToken], - auth_user: model.User, - options: List[str] = []) -> Optional[rest.Response]: + user_token: Optional[model.UserToken], + auth_user: model.User, + options: List[str] = [], +) -> Optional[rest.Response]: if not user_token: return None return UserTokenSerializer(user_token, auth_user).serialize(options) -def get_by_user_and_token( - user: model.User, token: str) -> model.UserToken: +def get_by_user_and_token(user: model.User, token: str) -> model.UserToken: return ( - db.session - .query(model.UserToken) + db.session.query(model.UserToken) .filter(model.UserToken.user_id == user.user_id) .filter(model.UserToken.token == token) - .one_or_none()) + .one_or_none() + ) def get_user_tokens(user: model.User) -> List[model.UserToken]: assert user return ( - db.session - .query(model.UserToken) + db.session.query(model.UserToken) .filter(model.UserToken.user_id == user.user_id) - .all()) + .all() + ) def create_user_token(user: model.User, enabled: bool) -> model.UserToken: @@ -103,7 +104,8 @@ def create_user_token(user: model.User, enabled: bool) -> model.UserToken: def update_user_token_enabled( - user_token: model.UserToken, enabled: bool) -> None: + user_token: model.UserToken, enabled: bool +) -> None: assert user_token user_token.enabled = enabled update_user_token_edit_time(user_token) @@ -115,28 +117,30 @@ def update_user_token_edit_time(user_token: model.UserToken) -> None: def update_user_token_expiration_time( - user_token: model.UserToken, expiration_time_str: str) -> None: + user_token: model.UserToken, expiration_time_str: str +) -> None: assert user_token try: expiration_time = rfc3339_parser.parse(expiration_time_str, utc=True) expiration_time = expiration_time.astimezone(pytz.UTC) if expiration_time < datetime.utcnow().replace(tzinfo=pytz.UTC): raise InvalidExpirationError( - 'Expiration cannot happen in the past') + "Expiration cannot happen in the past" + ) user_token.expiration_time = expiration_time update_user_token_edit_time(user_token) except ValueError: raise InvalidExpirationError( - 'Expiration is in an invalid format {}'.format( - expiration_time_str)) + "Expiration is in an invalid format {}".format(expiration_time_str) + ) def update_user_token_note(user_token: model.UserToken, note: str) -> None: assert user_token - note = note.strip() if note is not None else '' + note = note.strip() if note is not None else "" note = None if len(note) == 0 else note if util.value_exceeds_column_size(note, model.UserToken.note): - raise InvalidNoteError('Note is too long.') + raise InvalidNoteError("Note is too long.") user_token.note = note update_user_token_edit_time(user_token) diff --git a/server/szurubooru/func/users.py b/server/szurubooru/func/users.py index e5946dc..5cbe3cc 100644 --- a/server/szurubooru/func/users.py +++ b/server/szurubooru/func/users.py @@ -1,9 +1,11 @@ -from datetime import datetime -from typing import Any, Optional, Union, List, Dict, Callable import re +from datetime import datetime +from typing import Any, Callable, Dict, List, Optional, Union + import sqlalchemy as sa -from szurubooru import config, db, model, errors, rest -from szurubooru.func import auth, util, serialization, files, images + +from szurubooru import config, db, errors, model, rest +from szurubooru.func import auth, files, images, serialization, util class UserNotFoundError(errors.NotFoundError): @@ -35,36 +37,41 @@ class InvalidAvatarError(errors.ValidationError): def get_avatar_path(user_name: str) -> str: - return 'avatars/' + user_name.lower() + '.png' + return "avatars/" + user_name.lower() + ".png" def get_avatar_url(user: model.User) -> str: assert user if user.avatar_style == user.AVATAR_GRAVATAR: assert user.email or user.name - return 'https://gravatar.com/avatar/%s?d=retro&s=%d' % ( + return "https://gravatar.com/avatar/%s?d=retro&s=%d" % ( util.get_md5((user.email or user.name).lower()), - config.config['thumbnails']['avatar_width']) + config.config["thumbnails"]["avatar_width"], + ) assert user.name - return '%s/avatars/%s.png' % ( - config.config['data_url'].rstrip('/'), user.name.lower()) + return "%s/avatars/%s.png" % ( + config.config["data_url"].rstrip("/"), + user.name.lower(), + ) def get_email( - user: model.User, - auth_user: model.User, - force_show_email: bool) -> Union[bool, str]: + user: model.User, auth_user: model.User, force_show_email: bool +) -> Union[bool, str]: assert user assert auth_user - if not force_show_email \ - and auth_user.user_id != user.user_id \ - and not auth.has_privilege(auth_user, 'users:edit:any:email'): + if ( + not force_show_email + and auth_user.user_id != user.user_id + and not auth.has_privilege(auth_user, "users:edit:any:email") + ): return False return user.email def get_liked_post_count( - user: model.User, auth_user: model.User) -> Union[bool, int]: + user: model.User, auth_user: model.User +) -> Union[bool, int]: assert user assert auth_user if auth_user.user_id != user.user_id: @@ -73,7 +80,8 @@ def get_liked_post_count( def get_disliked_post_count( - user: model.User, auth_user: model.User) -> Union[bool, int]: + user: model.User, auth_user: model.User +) -> Union[bool, int]: assert user assert auth_user if auth_user.user_id != user.user_id: @@ -83,29 +91,30 @@ def get_disliked_post_count( class UserSerializer(serialization.BaseSerializer): def __init__( - self, - user: model.User, - auth_user: model.User, - force_show_email: bool = False) -> None: + self, + user: model.User, + auth_user: model.User, + force_show_email: bool = False, + ) -> None: self.user = user self.auth_user = auth_user self.force_show_email = force_show_email def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'name': self.serialize_name, - 'creationTime': self.serialize_creation_time, - 'lastLoginTime': self.serialize_last_login_time, - 'version': self.serialize_version, - 'rank': self.serialize_rank, - 'avatarStyle': self.serialize_avatar_style, - 'avatarUrl': self.serialize_avatar_url, - 'commentCount': self.serialize_comment_count, - 'uploadedPostCount': self.serialize_uploaded_post_count, - 'favoritePostCount': self.serialize_favorite_post_count, - 'likedPostCount': self.serialize_liked_post_count, - 'dislikedPostCount': self.serialize_disliked_post_count, - 'email': self.serialize_email, + "name": self.serialize_name, + "creationTime": self.serialize_creation_time, + "lastLoginTime": self.serialize_last_login_time, + "version": self.serialize_version, + "rank": self.serialize_rank, + "avatarStyle": self.serialize_avatar_style, + "avatarUrl": self.serialize_avatar_url, + "commentCount": self.serialize_comment_count, + "uploadedPostCount": self.serialize_uploaded_post_count, + "favoritePostCount": self.serialize_favorite_post_count, + "likedPostCount": self.serialize_liked_post_count, + "dislikedPostCount": self.serialize_disliked_post_count, + "email": self.serialize_email, } def serialize_name(self) -> Any: @@ -149,20 +158,22 @@ class UserSerializer(serialization.BaseSerializer): def serialize_user( - user: Optional[model.User], - auth_user: model.User, - options: List[str] = [], - force_show_email: bool = False) -> Optional[rest.Response]: + user: Optional[model.User], + auth_user: model.User, + options: List[str] = [], + force_show_email: bool = False, +) -> Optional[rest.Response]: if not user: return None return UserSerializer(user, auth_user, force_show_email).serialize(options) def serialize_micro_user( - user: Optional[model.User], - auth_user: model.User) -> Optional[rest.Response]: + user: Optional[model.User], auth_user: model.User +) -> Optional[rest.Response]: return serialize_user( - user, auth_user=auth_user, options=['name', 'avatarUrl']) + user, auth_user=auth_user, options=["name", "avatarUrl"] + ) def get_user_count() -> int: @@ -171,33 +182,34 @@ def get_user_count() -> int: def try_get_user_by_name(name: str) -> Optional[model.User]: return ( - db.session - .query(model.User) + db.session.query(model.User) .filter(sa.func.lower(model.User.name) == sa.func.lower(name)) - .one_or_none()) + .one_or_none() + ) def get_user_by_name(name: str) -> model.User: user = try_get_user_by_name(name) if not user: - raise UserNotFoundError('User %r not found.' % name) + raise UserNotFoundError("User %r not found." % name) return user def try_get_user_by_name_or_email(name_or_email: str) -> Optional[model.User]: return ( - db.session - .query(model.User) + db.session.query(model.User) .filter( - (sa.func.lower(model.User.name) == sa.func.lower(name_or_email)) | - (sa.func.lower(model.User.email) == sa.func.lower(name_or_email))) - .one_or_none()) + (sa.func.lower(model.User.name) == sa.func.lower(name_or_email)) + | (sa.func.lower(model.User.email) == sa.func.lower(name_or_email)) + ) + .one_or_none() + ) def get_user_by_name_or_email(name_or_email: str) -> model.User: user = try_get_user_by_name_or_email(name_or_email) if not user: - raise UserNotFoundError('User %r not found.' % name_or_email) + raise UserNotFoundError("User %r not found." % name_or_email) return user @@ -207,7 +219,7 @@ def create_user(name: str, password: str, email: str) -> model.User: update_user_password(user, password) update_user_email(user, email) if get_user_count() > 0: - user.rank = util.flip(auth.RANK_MAP)[config.config['default_rank']] + user.rank = util.flip(auth.RANK_MAP)[config.config["default_rank"]] else: user.rank = model.User.RANK_ADMINISTRATOR user.creation_time = datetime.utcnow() @@ -218,17 +230,18 @@ def create_user(name: str, password: str, email: str) -> model.User: def update_user_name(user: model.User, name: str) -> None: assert user if not name: - raise InvalidUserNameError('Name cannot be empty.') + raise InvalidUserNameError("Name cannot be empty.") if util.value_exceeds_column_size(name, model.User.name): - raise InvalidUserNameError('User name is too long.') + raise InvalidUserNameError("User name is too long.") name = name.strip() - name_regex = config.config['user_name_regex'] + name_regex = config.config["user_name_regex"] if not re.match(name_regex, name): raise InvalidUserNameError( - 'User name %r must satisfy regex %r.' % (name, name_regex)) + "User name %r must satisfy regex %r." % (name, name_regex) + ) other_user = try_get_user_by_name(name) if other_user and other_user.user_id != user.user_id: - raise UserAlreadyExistsError('User %r already exists.' % name) + raise UserAlreadyExistsError("User %r already exists." % name) if user.name and files.has(get_avatar_path(user.name)): files.move(get_avatar_path(user.name), get_avatar_path(name)) user.name = name @@ -237,14 +250,16 @@ def update_user_name(user: model.User, name: str) -> None: def update_user_password(user: model.User, password: str) -> None: assert user if not password: - raise InvalidPasswordError('Password cannot be empty.') - password_regex = config.config['password_regex'] + raise InvalidPasswordError("Password cannot be empty.") + password_regex = config.config["password_regex"] if not re.match(password_regex, password): raise InvalidPasswordError( - 'Password must satisfy regex %r.' % password_regex) + "Password must satisfy regex %r." % password_regex + ) user.password_salt = auth.create_password() password_hash, revision = auth.get_password_hash( - user.password_salt, password) + user.password_salt, password + ) user.password_hash = password_hash user.password_revision = revision @@ -253,53 +268,56 @@ def update_user_email(user: model.User, email: str) -> None: assert user email = email.strip() if util.value_exceeds_column_size(email, model.User.email): - raise InvalidEmailError('Email is too long.') + raise InvalidEmailError("Email is too long.") if not util.is_valid_email(email): - raise InvalidEmailError('E-mail is invalid.') + raise InvalidEmailError("E-mail is invalid.") user.email = email or None def update_user_rank( - user: model.User, rank: str, auth_user: model.User) -> None: + user: model.User, rank: str, auth_user: model.User +) -> None: assert user if not rank: - raise InvalidRankError('Rank cannot be empty.') + raise InvalidRankError("Rank cannot be empty.") rank = util.flip(auth.RANK_MAP).get(rank.strip(), None) all_ranks = list(auth.RANK_MAP.values()) if not rank: - raise InvalidRankError( - 'Rank can be either of %r.' % all_ranks) + raise InvalidRankError("Rank can be either of %r." % all_ranks) if rank in (model.User.RANK_ANONYMOUS, model.User.RANK_NOBODY): - raise InvalidRankError('Rank %r cannot be used.' % auth.RANK_MAP[rank]) - if all_ranks.index(auth_user.rank) \ - < all_ranks.index(rank) and get_user_count() > 0: - raise errors.AuthError('Trying to set higher rank than your own.') + raise InvalidRankError("Rank %r cannot be used." % auth.RANK_MAP[rank]) + if ( + all_ranks.index(auth_user.rank) < all_ranks.index(rank) + and get_user_count() > 0 + ): + raise errors.AuthError("Trying to set higher rank than your own.") user.rank = rank def update_user_avatar( - user: model.User, - avatar_style: str, - avatar_content: Optional[bytes] = None) -> None: + user: model.User, avatar_style: str, avatar_content: Optional[bytes] = None +) -> None: assert user - if avatar_style == 'gravatar': + if avatar_style == "gravatar": user.avatar_style = user.AVATAR_GRAVATAR - elif avatar_style == 'manual': + elif avatar_style == "manual": user.avatar_style = user.AVATAR_MANUAL - avatar_path = 'avatars/' + user.name.lower() + '.png' + avatar_path = "avatars/" + user.name.lower() + ".png" if not avatar_content: if files.has(avatar_path): return - raise InvalidAvatarError('Avatar content missing.') + raise InvalidAvatarError("Avatar content missing.") image = images.Image(avatar_content) image.resize_fill( - int(config.config['thumbnails']['avatar_width']), - int(config.config['thumbnails']['avatar_height'])) + int(config.config["thumbnails"]["avatar_width"]), + int(config.config["thumbnails"]["avatar_height"]), + ) files.save(avatar_path, image.to_png()) else: raise InvalidAvatarError( - 'Avatar style %r is invalid. Valid avatar styles: %r.' % ( - avatar_style, ['gravatar', 'manual'])) + "Avatar style %r is invalid. Valid avatar styles: %r." + % (avatar_style, ["gravatar", "manual"]) + ) def bump_user_login_time(user: model.User) -> None: @@ -312,7 +330,8 @@ def reset_user_password(user: model.User) -> str: password = auth.create_password() user.password_salt = auth.create_password() password_hash, revision = auth.get_password_hash( - user.password_salt, password) + user.password_salt, password + ) user.password_hash = password_hash user.password_revision = revision return password diff --git a/server/szurubooru/func/util.py b/server/szurubooru/func/util.py index 1bf34fa..f839136 100644 --- a/server/szurubooru/func/util.py +++ b/server/szurubooru/func/util.py @@ -1,29 +1,32 @@ -import os import hashlib +import os import re import tempfile -from typing import Any, Optional, Union, Tuple, List, Dict, Generator, TypeVar -from datetime import datetime, timedelta from contextlib import contextmanager -from szurubooru import errors +from datetime import datetime, timedelta +from typing import Any, Dict, Generator, List, Optional, Tuple, TypeVar, Union +from szurubooru import errors -T = TypeVar('T') +T = TypeVar("T") def snake_case_to_lower_camel_case(text: str) -> str: - components = text.split('_') - return components[0].lower() + \ - ''.join(word[0].upper() + word[1:].lower() for word in components[1:]) + components = text.split("_") + return components[0].lower() + "".join( + word[0].upper() + word[1:].lower() for word in components[1:] + ) def snake_case_to_upper_train_case(text: str) -> str: - return '-'.join( - word[0].upper() + word[1:].lower() for word in text.split('_')) + return "-".join( + word[0].upper() + word[1:].lower() for word in text.split("_") + ) def snake_case_to_lower_camel_case_keys( - source: Dict[str, Any]) -> Dict[str, Any]: + source: Dict[str, Any] +) -> Dict[str, Any]: target = {} for key, value in source.items(): target[snake_case_to_lower_camel_case(key)] = value @@ -35,7 +38,7 @@ def create_temp_file(**kwargs: Any) -> Generator: (descriptor, path) = tempfile.mkstemp(**kwargs) os.close(descriptor) try: - with open(path, 'r+b') as handle: + with open(path, "r+b") as handle: yield handle finally: os.remove(path) @@ -61,7 +64,7 @@ def unalias_dict(source: List[Tuple[List[str], T]]) -> Dict[str, T]: def get_md5(source: Union[str, bytes]) -> str: if not isinstance(source, bytes): - source = source.encode('utf-8') + source = source.encode("utf-8") md5 = hashlib.md5() md5.update(source) return md5.hexdigest() @@ -69,7 +72,7 @@ def get_md5(source: Union[str, bytes]) -> str: def get_sha1(source: Union[str, bytes]) -> str: if not isinstance(source, bytes): - source = source.encode('utf-8') + source = source.encode("utf-8") sha1 = hashlib.sha1() sha1.update(source) return sha1.hexdigest() @@ -80,12 +83,13 @@ def flip(source: Dict[Any, Any]) -> Dict[Any, Any]: def is_valid_email(email: Optional[str]) -> bool: - ''' Return whether given email address is valid or empty. ''' - return not email or re.match(r'^[^@]*@[^@]*\.[^@]*$', email) is not None + """ Return whether given email address is valid or empty. """ + return not email or re.match(r"^[^@]*@[^@]*\.[^@]*$", email) is not None class dotdict(dict): - ''' dot.notation access to dictionary attributes. ''' + """ dot.notation access to dictionary attributes. """ + def __getattr__(self, attr: str) -> Any: return self.get(attr) @@ -94,51 +98,54 @@ class dotdict(dict): def parse_time_range(value: str) -> Tuple[datetime, datetime]: - ''' Return tuple containing min/max time for given text representation. ''' + """ Return tuple containing min/max time for given text representation. """ one_day = timedelta(days=1) one_second = timedelta(seconds=1) almost_one_day = one_day - one_second value = value.lower() if not value: - raise errors.ValidationError('Empty date format.') + raise errors.ValidationError("Empty date format.") - if value == 'today': + if value == "today": now = datetime.utcnow() return ( datetime(now.year, now.month, now.day, 0, 0, 0), - datetime(now.year, now.month, now.day, 0, 0, 0) + almost_one_day + datetime(now.year, now.month, now.day, 0, 0, 0) + almost_one_day, ) - if value == 'yesterday': + if value == "yesterday": now = datetime.utcnow() return ( datetime(now.year, now.month, now.day, 0, 0, 0) - one_day, - datetime(now.year, now.month, now.day, 0, 0, 0) - one_second) + datetime(now.year, now.month, now.day, 0, 0, 0) - one_second, + ) - match = re.match(r'^(\d{4})$', value) + match = re.match(r"^(\d{4})$", value) if match: year = int(match.group(1)) return (datetime(year, 1, 1), datetime(year + 1, 1, 1) - one_second) - match = re.match(r'^(\d{4})-(\d{1,2})$', value) + match = re.match(r"^(\d{4})-(\d{1,2})$", value) if match: year = int(match.group(1)) month = int(match.group(2)) return ( datetime(year, month, 1), - datetime(year, month + 1, 1) - one_second) + datetime(year, month + 1, 1) - one_second, + ) - match = re.match(r'^(\d{4})-(\d{1,2})-(\d{1,2})$', value) + match = re.match(r"^(\d{4})-(\d{1,2})-(\d{1,2})$", value) if match: year = int(match.group(1)) month = int(match.group(2)) day = int(match.group(3)) return ( datetime(year, month, day), - datetime(year, month, day + 1) - one_second) + datetime(year, month, day + 1) - one_second, + ) - raise errors.ValidationError('Invalid date format: %r.' % value) + raise errors.ValidationError("Invalid date format: %r." % value) def icase_unique(source: List[str]) -> List[str]: @@ -168,4 +175,4 @@ def get_column_size(column: Any) -> Optional[int]: def chunks(source_list: List[Any], part_size: int) -> Generator: for i in range(0, len(source_list), part_size): - yield source_list[i:i + part_size] + yield source_list[i : i + part_size] diff --git a/server/szurubooru/func/versions.py b/server/szurubooru/func/versions.py index 6e5a367..790b05b 100644 --- a/server/szurubooru/func/versions.py +++ b/server/szurubooru/func/versions.py @@ -1,16 +1,16 @@ -from szurubooru import errors, rest, model +from szurubooru import errors, model, rest def verify_version( - entity: model.Base, - context: rest.Context, - field_name: str = 'version') -> None: + entity: model.Base, context: rest.Context, field_name: str = "version" +) -> None: actual_version = context.get_param_as_int(field_name) expected_version = entity.version if actual_version != expected_version: raise errors.IntegrityError( - 'Someone else modified this in the meantime. ' + - 'Please try again.') + "Someone else modified this in the meantime. " + + "Please try again." + ) def bump_version(entity: model.Base) -> None: diff --git a/server/szurubooru/middleware/__init__.py b/server/szurubooru/middleware/__init__.py index c5a90d8..7177564 100644 --- a/server/szurubooru/middleware/__init__.py +++ b/server/szurubooru/middleware/__init__.py @@ -1,4 +1,4 @@ -''' Various hooks that get executed for each request. ''' +""" Various hooks that get executed for each request. """ import szurubooru.middleware.authenticator import szurubooru.middleware.cache_purger diff --git a/server/szurubooru/middleware/authenticator.py b/server/szurubooru/middleware/authenticator.py index 4340ec9..e73b235 100644 --- a/server/szurubooru/middleware/authenticator.py +++ b/server/szurubooru/middleware/authenticator.py @@ -1,55 +1,66 @@ import base64 from typing import Optional, Tuple -from szurubooru import model, errors, rest -from szurubooru.func import auth, users, user_tokens + +from szurubooru import errors, model, rest +from szurubooru.func import auth, user_tokens, users from szurubooru.rest.errors import HttpBadRequest def _authenticate_basic_auth(username: str, password: str) -> model.User: - ''' Try to authenticate user. Throw AuthError for invalid users. ''' + """ Try to authenticate user. Throw AuthError for invalid users. """ user = users.get_user_by_name(username) if not auth.is_valid_password(user, password): - raise errors.AuthError('Invalid password.') + raise errors.AuthError("Invalid password.") return user def _authenticate_token( - username: str, token: str) -> Tuple[model.User, model.UserToken]: - ''' Try to authenticate user. Throw AuthError for invalid users. ''' + username: str, token: str +) -> Tuple[model.User, model.UserToken]: + """ Try to authenticate user. Throw AuthError for invalid users. """ user = users.get_user_by_name(username) user_token = user_tokens.get_by_user_and_token(user, token) if not auth.is_valid_token(user_token): - raise errors.AuthError('Invalid token.') + raise errors.AuthError("Invalid token.") return user, user_token def _get_user(ctx: rest.Context, bump_login: bool) -> Optional[model.User]: - if not ctx.has_header('Authorization'): + if not ctx.has_header("Authorization"): return None auth_token = None try: - auth_type, credentials = ctx.get_header('Authorization').split(' ', 1) - if auth_type.lower() == 'basic': - username, password = base64.decodebytes( - credentials.encode('ascii')).decode('utf8').split(':', 1) + auth_type, credentials = ctx.get_header("Authorization").split(" ", 1) + if auth_type.lower() == "basic": + username, password = ( + base64.decodebytes(credentials.encode("ascii")) + .decode("utf8") + .split(":", 1) + ) auth_user = _authenticate_basic_auth(username, password) - elif auth_type.lower() == 'token': - username, token = base64.decodebytes( - credentials.encode('ascii')).decode('utf8').split(':', 1) + elif auth_type.lower() == "token": + username, token = ( + base64.decodebytes(credentials.encode("ascii")) + .decode("utf8") + .split(":", 1) + ) auth_user, auth_token = _authenticate_token(username, token) else: raise HttpBadRequest( - 'ValidationError', - 'Only basic or token HTTP authentication is supported.') + "ValidationError", + "Only basic or token HTTP authentication is supported.", + ) except ValueError as err: msg = ( - 'Authorization header values are not properly formed. ' - 'Supplied header {0}. Got error: {1}') + "Authorization header values are not properly formed. " + "Supplied header {0}. Got error: {1}" + ) raise HttpBadRequest( - 'ValidationError', - msg.format(ctx.get_header('Authorization'), str(err))) + "ValidationError", + msg.format(ctx.get_header("Authorization"), str(err)), + ) if bump_login and auth_user.user_id: users.bump_user_login_time(auth_user) @@ -61,8 +72,8 @@ def _get_user(ctx: rest.Context, bump_login: bool) -> Optional[model.User]: def process_request(ctx: rest.Context) -> None: - ''' Bind the user to request. Update last login time if needed. ''' - bump_login = ctx.get_param_as_bool('bump-login', default=False) + """ Bind the user to request. Update last login time if needed. """ + bump_login = ctx.get_param_as_bool("bump-login", default=False) auth_user = _get_user(ctx, bump_login) if auth_user: ctx.user = auth_user diff --git a/server/szurubooru/middleware/cache_purger.py b/server/szurubooru/middleware/cache_purger.py index d83fb84..e3d05a7 100644 --- a/server/szurubooru/middleware/cache_purger.py +++ b/server/szurubooru/middleware/cache_purger.py @@ -5,5 +5,5 @@ from szurubooru.rest import middleware @middleware.pre_hook def process_request(ctx: rest.Context) -> None: - if ctx.method != 'GET': + if ctx.method != "GET": cache.purge() diff --git a/server/szurubooru/middleware/request_logger.py b/server/szurubooru/middleware/request_logger.py index 54e40e4..79fffbd 100644 --- a/server/szurubooru/middleware/request_logger.py +++ b/server/szurubooru/middleware/request_logger.py @@ -1,8 +1,8 @@ import logging + from szurubooru import db, rest from szurubooru.rest import middleware - logger = logging.getLogger(__name__) @@ -14,8 +14,9 @@ def process_request(_ctx: rest.Context) -> None: @middleware.post_hook def process_response(ctx: rest.Context) -> None: logger.info( - '%s %s (user=%s, queries=%d)', + "%s %s (user=%s, queries=%d)", ctx.method, ctx.url, ctx.user.name, - db.get_query_count()) + db.get_query_count(), + ) diff --git a/server/szurubooru/migrations/env.py b/server/szurubooru/migrations/env.py index f0a06dd..cd4f6ad 100644 --- a/server/szurubooru/migrations/env.py +++ b/server/szurubooru/migrations/env.py @@ -1,29 +1,40 @@ +""" +Alembic setup and configuration script + +isort:skip_file +""" + + +import logging.config import os import sys +from time import sleep import alembic import sqlalchemy as sa -import logging.config -from time import sleep + +# fmt: off # make szurubooru module importable dir_to_self = os.path.dirname(os.path.realpath(__file__)) sys.path.append(os.path.join(dir_to_self, *[os.pardir] * 2)) -import szurubooru.model.base # noqa: E402 import szurubooru.config # noqa: E402 +import szurubooru.model.base # noqa: E402 +# fmt: on + alembic_config = alembic.context.config logging.config.fileConfig(alembic_config.config_file_name) szuru_config = szurubooru.config.config -alembic_config.set_main_option('sqlalchemy.url', szuru_config['database']) +alembic_config.set_main_option("sqlalchemy.url", szuru_config["database"]) target_metadata = szurubooru.model.Base.metadata def run_migrations_offline(): - ''' + """ Run migrations in 'offline' mode. This configures the context with just a URL @@ -33,29 +44,31 @@ def run_migrations_offline(): Calls to context.execute() here emit the given string to the script output. - ''' - url = alembic_config.get_main_option('sqlalchemy.url') + """ + url = alembic_config.get_main_option("sqlalchemy.url") alembic.context.configure( url=url, target_metadata=target_metadata, literal_binds=True, - compare_type=True) + compare_type=True, + ) with alembic.context.begin_transaction(): alembic.context.run_migrations() def run_migrations_online(): - ''' + """ Run migrations in 'online' mode. In this scenario we need to create an Engine and associate a connection with the context. - ''' + """ connectable = sa.engine_from_config( alembic_config.get_section(alembic_config.config_ini_section), - prefix='sqlalchemy.', - poolclass=sa.pool.NullPool) + prefix="sqlalchemy.", + poolclass=sa.pool.NullPool, + ) def connect_with_timeout(connectable, timeout=45): dt = 5 @@ -70,7 +83,8 @@ def run_migrations_online(): alembic.context.configure( connection=connection, target_metadata=target_metadata, - compare_type=True) + compare_type=True, + ) with alembic.context.begin_transaction(): alembic.context.run_migrations() diff --git a/server/szurubooru/migrations/script.py.mako b/server/szurubooru/migrations/script.py.mako index 13adc51..f065447 100644 --- a/server/szurubooru/migrations/script.py.mako +++ b/server/szurubooru/migrations/script.py.mako @@ -7,6 +7,7 @@ Created at: ${create_date} import sqlalchemy as sa from alembic import op + ${imports if imports else ""} revision = ${repr(up_revision)} diff --git a/server/szurubooru/migrations/versions/00cb3a2734db_create_tag_tables.py b/server/szurubooru/migrations/versions/00cb3a2734db_create_tag_tables.py index 77d7641..a817044 100644 --- a/server/szurubooru/migrations/versions/00cb3a2734db_create_tag_tables.py +++ b/server/szurubooru/migrations/versions/00cb3a2734db_create_tag_tables.py @@ -1,65 +1,70 @@ -''' +""" Create tag tables Revision ID: 00cb3a2734db Created at: 2016-04-15 23:15:36.255429 -''' +""" import sqlalchemy as sa from alembic import op -revision = '00cb3a2734db' -down_revision = 'e5c1216a8503' +revision = "00cb3a2734db" +down_revision = "e5c1216a8503" branch_labels = None depends_on = None def upgrade(): op.create_table( - 'tag_category', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.Unicode(length=32), nullable=False), - sa.Column('color', sa.Unicode(length=32), nullable=False), - sa.PrimaryKeyConstraint('id')) + "tag_category", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.Unicode(length=32), nullable=False), + sa.Column("color", sa.Unicode(length=32), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'tag', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('category_id', sa.Integer(), nullable=False), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('last_edit_time', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['category_id'], ['tag_category.id']), - sa.PrimaryKeyConstraint('id')) + "tag", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("category_id", sa.Integer(), nullable=False), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("last_edit_time", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(["category_id"], ["tag_category.id"]), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'tag_name', - sa.Column('tag_name_id', sa.Integer(), nullable=False), - sa.Column('tag_id', sa.Integer(), nullable=False), - sa.Column('name', sa.Unicode(length=64), nullable=False), - sa.ForeignKeyConstraint(['tag_id'], ['tag.id']), - sa.PrimaryKeyConstraint('tag_name_id'), - sa.UniqueConstraint('name')) + "tag_name", + sa.Column("tag_name_id", sa.Integer(), nullable=False), + sa.Column("tag_id", sa.Integer(), nullable=False), + sa.Column("name", sa.Unicode(length=64), nullable=False), + sa.ForeignKeyConstraint(["tag_id"], ["tag.id"]), + sa.PrimaryKeyConstraint("tag_name_id"), + sa.UniqueConstraint("name"), + ) op.create_table( - 'tag_implication', - sa.Column('parent_id', sa.Integer(), nullable=False), - sa.Column('child_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['parent_id'], ['tag.id']), - sa.ForeignKeyConstraint(['child_id'], ['tag.id']), - sa.PrimaryKeyConstraint('parent_id', 'child_id')) + "tag_implication", + sa.Column("parent_id", sa.Integer(), nullable=False), + sa.Column("child_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["parent_id"], ["tag.id"]), + sa.ForeignKeyConstraint(["child_id"], ["tag.id"]), + sa.PrimaryKeyConstraint("parent_id", "child_id"), + ) op.create_table( - 'tag_suggestion', - sa.Column('parent_id', sa.Integer(), nullable=False), - sa.Column('child_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['parent_id'], ['tag.id']), - sa.ForeignKeyConstraint(['child_id'], ['tag.id']), - sa.PrimaryKeyConstraint('parent_id', 'child_id')) + "tag_suggestion", + sa.Column("parent_id", sa.Integer(), nullable=False), + sa.Column("child_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["parent_id"], ["tag.id"]), + sa.ForeignKeyConstraint(["child_id"], ["tag.id"]), + sa.PrimaryKeyConstraint("parent_id", "child_id"), + ) def downgrade(): - op.drop_table('tag_suggestion') - op.drop_table('tag_implication') - op.drop_table('tag_name') - op.drop_table('tag') - op.drop_table('tag_category') + op.drop_table("tag_suggestion") + op.drop_table("tag_implication") + op.drop_table("tag_name") + op.drop_table("tag") + op.drop_table("tag_category") diff --git a/server/szurubooru/migrations/versions/02ef5f73f4ab_add_hashes_to_post_file_names.py b/server/szurubooru/migrations/versions/02ef5f73f4ab_add_hashes_to_post_file_names.py index 1e09e34..fb4f09c 100644 --- a/server/szurubooru/migrations/versions/02ef5f73f4ab_add_hashes_to_post_file_names.py +++ b/server/szurubooru/migrations/versions/02ef5f73f4ab_add_hashes_to_post_file_names.py @@ -1,43 +1,45 @@ -''' +""" Add hashes to post file names Revision ID: 02ef5f73f4ab Created at: 2017-08-24 13:30:46.766928 -''' +""" import os import re + from szurubooru.func import files, posts -revision = '02ef5f73f4ab' -down_revision = '5f00af3004a4' +revision = "02ef5f73f4ab" +down_revision = "5f00af3004a4" branch_labels = None depends_on = None def upgrade(): - for name in ['posts', 'posts/custom-thumbnails', 'generated-thumbnails']: + for name in ["posts", "posts/custom-thumbnails", "generated-thumbnails"]: for entry in list(files.scan(name)): - match = re.match(r'^(?P\d+)\.(?P\w+)$', entry.name) + match = re.match(r"^(?P\d+)\.(?P\w+)$", entry.name) if match: - post_id = int(match.group('name')) + post_id = int(match.group("name")) security_hash = posts.get_post_security_hash(post_id) - ext = match.group('ext') - new_name = '%s_%s.%s' % (post_id, security_hash, ext) + ext = match.group("ext") + new_name = "%s_%s.%s" % (post_id, security_hash, ext) new_path = os.path.join(os.path.dirname(entry.path), new_name) os.rename(entry.path, new_path) def downgrade(): - for name in ['posts', 'posts/custom-thumbnails', 'generated-thumbnails']: + for name in ["posts", "posts/custom-thumbnails", "generated-thumbnails"]: for entry in list(files.scan(name)): match = re.match( - r'^(?P\d+)_(?P[0-9A-Fa-f]+)\.(?P\w+)$', - entry.name) + r"^(?P\d+)_(?P[0-9A-Fa-f]+)\.(?P\w+)$", + entry.name, + ) if match: - post_id = int(match.group('name')) - security_hash = match.group('hash') # noqa: F841 - ext = match.group('ext') - new_name = '%s.%s' % (post_id, ext) + post_id = int(match.group("name")) + security_hash = match.group("hash") # noqa: F841 + ext = match.group("ext") + new_name = "%s.%s" % (post_id, ext) new_path = os.path.join(os.path.dirname(entry.path), new_name) os.rename(entry.path, new_path) diff --git a/server/szurubooru/migrations/versions/055d0e048fb3_add_default_column_to_tag_categories.py b/server/szurubooru/migrations/versions/055d0e048fb3_add_default_column_to_tag_categories.py index 1ced159..0b4efb8 100644 --- a/server/szurubooru/migrations/versions/055d0e048fb3_add_default_column_to_tag_categories.py +++ b/server/szurubooru/migrations/versions/055d0e048fb3_add_default_column_to_tag_categories.py @@ -1,28 +1,30 @@ -''' +""" Add default column to tag categories Revision ID: 055d0e048fb3 Created at: 2016-05-22 18:12:58.149678 -''' +""" import sqlalchemy as sa from alembic import op -revision = '055d0e048fb3' -down_revision = '49ab4e1139ef' +revision = "055d0e048fb3" +down_revision = "49ab4e1139ef" branch_labels = None depends_on = None def upgrade(): op.add_column( - 'tag_category', sa.Column('default', sa.Boolean(), nullable=True)) + "tag_category", sa.Column("default", sa.Boolean(), nullable=True) + ) op.execute( - sa.table('tag_category', sa.column('default')) + sa.table("tag_category", sa.column("default")) .update() - .values(default=False)) - op.alter_column('tag_category', 'default', nullable=False) + .values(default=False) + ) + op.alter_column("tag_category", "default", nullable=False) def downgrade(): - op.drop_column('tag_category', 'default') + op.drop_column("tag_category", "default") diff --git a/server/szurubooru/migrations/versions/1cd4c7b22846_change_flags_column_to_string.py b/server/szurubooru/migrations/versions/1cd4c7b22846_change_flags_column_to_string.py index ce01751..ef9c5f3 100644 --- a/server/szurubooru/migrations/versions/1cd4c7b22846_change_flags_column_to_string.py +++ b/server/szurubooru/migrations/versions/1cd4c7b22846_change_flags_column_to_string.py @@ -1,61 +1,54 @@ -''' +""" Change flags column to string Revision ID: 1cd4c7b22846 Created at: 2018-09-21 19:37:27.686568 -''' +""" import sqlalchemy as sa from alembic import op -revision = '1cd4c7b22846' -down_revision = 'a39c7f98a7fa' +revision = "1cd4c7b22846" +down_revision = "a39c7f98a7fa" branch_labels = None depends_on = None def upgrade(): conn = op.get_bind() - op.alter_column('post', 'flags', new_column_name='oldflags') - op.add_column('post', sa.Column( - 'flags', sa.Unicode(200), default='', nullable=True)) + op.alter_column("post", "flags", new_column_name="oldflags") + op.add_column( + "post", sa.Column("flags", sa.Unicode(200), default="", nullable=True) + ) posts = sa.Table( - 'post', + "post", sa.MetaData(), - sa.Column('id', sa.Integer, primary_key=True), - sa.Column('flags', sa.Unicode(200), default='', nullable=True), - sa.Column('oldflags', sa.PickleType(), nullable=True), + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("flags", sa.Unicode(200), default="", nullable=True), + sa.Column("oldflags", sa.PickleType(), nullable=True), ) for row in conn.execute(posts.select()): - newflag = ','.join(row.oldflags) if row.oldflags else '' + newflag = ",".join(row.oldflags) if row.oldflags else "" conn.execute( - posts.update().where( - posts.c.id == row.id - ).values( - flags=newflag - ) + posts.update().where(posts.c.id == row.id).values(flags=newflag) ) - op.drop_column('post', 'oldflags') + op.drop_column("post", "oldflags") def downgrade(): conn = op.get_bind() - op.alter_column('post', 'flags', new_column_name='oldflags') - op.add_column('post', sa.Column('flags', sa.PickleType(), nullable=True)) + op.alter_column("post", "flags", new_column_name="oldflags") + op.add_column("post", sa.Column("flags", sa.PickleType(), nullable=True)) posts = sa.Table( - 'post', + "post", sa.MetaData(), - sa.Column('id', sa.Integer, primary_key=True), - sa.Column('flags', sa.PickleType(), nullable=True), - sa.Column('oldflags', sa.Unicode(200), default='', nullable=True), + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("flags", sa.PickleType(), nullable=True), + sa.Column("oldflags", sa.Unicode(200), default="", nullable=True), ) for row in conn.execute(posts.select()): - newflag = [x for x in row.oldflags.split(',') if x] + newflag = [x for x in row.oldflags.split(",") if x] conn.execute( - posts.update().where( - posts.c.id == row.id - ).values( - flags=newflag - ) + posts.update().where(posts.c.id == row.id).values(flags=newflag) ) - op.drop_column('post', 'oldflags') + op.drop_column("post", "oldflags") diff --git a/server/szurubooru/migrations/versions/1e280b5d5df1_longer_tag_names.py b/server/szurubooru/migrations/versions/1e280b5d5df1_longer_tag_names.py index 1770291..5a79103 100644 --- a/server/szurubooru/migrations/versions/1e280b5d5df1_longer_tag_names.py +++ b/server/szurubooru/migrations/versions/1e280b5d5df1_longer_tag_names.py @@ -1,43 +1,50 @@ -''' +""" Longer tag names Revision ID: 1e280b5d5df1 Created at: 2020-03-15 18:57:12.901148 -''' +""" import sqlalchemy as sa from alembic import op - -revision = '1e280b5d5df1' -down_revision = '52d6ea6584b8' +revision = "1e280b5d5df1" +down_revision = "52d6ea6584b8" branch_labels = None depends_on = None def upgrade(): op.alter_column( - 'tag_name', 'name', + "tag_name", + "name", type_=sa.Unicode(128), existing_type=sa.Unicode(64), - existing_nullable=False) + existing_nullable=False, + ) op.alter_column( - 'snapshot', 'resource_name', + "snapshot", + "resource_name", type_=sa.Unicode(128), existing_type=sa.Unicode(64), - existing_nullable=False) + existing_nullable=False, + ) def downgrade(): op.alter_column( - 'tag_name', 'name', + "tag_name", + "name", type_=sa.Unicode(64), existing_type=sa.Unicode(128), - existing_nullable=False) + existing_nullable=False, + ) op.alter_column( - 'snapshot', 'resource_name', + "snapshot", + "resource_name", type_=sa.Unicode(64), existing_type=sa.Unicode(128), - existing_nullable=False) + existing_nullable=False, + ) diff --git a/server/szurubooru/migrations/versions/23abaf4a0a4b_add_mime_type_to_posts.py b/server/szurubooru/migrations/versions/23abaf4a0a4b_add_mime_type_to_posts.py index e18119b..c8c4031 100644 --- a/server/szurubooru/migrations/versions/23abaf4a0a4b_add_mime_type_to_posts.py +++ b/server/szurubooru/migrations/versions/23abaf4a0a4b_add_mime_type_to_posts.py @@ -1,23 +1,24 @@ -''' +""" Add mime type to posts Revision ID: 23abaf4a0a4b Created at: 2016-05-02 00:02:33.024885 -''' +""" import sqlalchemy as sa from alembic import op -revision = '23abaf4a0a4b' -down_revision = 'ed6dd16a30f3' +revision = "23abaf4a0a4b" +down_revision = "ed6dd16a30f3" branch_labels = None depends_on = None def upgrade(): op.add_column( - 'post', sa.Column('mime-type', sa.Unicode(length=32), nullable=False)) + "post", sa.Column("mime-type", sa.Unicode(length=32), nullable=False) + ) def downgrade(): - op.drop_column('post', 'mime-type') + op.drop_column("post", "mime-type") diff --git a/server/szurubooru/migrations/versions/336a76ec1338_create_post_tables.py b/server/szurubooru/migrations/versions/336a76ec1338_create_post_tables.py index b767c98..aea6cc7 100644 --- a/server/szurubooru/migrations/versions/336a76ec1338_create_post_tables.py +++ b/server/szurubooru/migrations/versions/336a76ec1338_create_post_tables.py @@ -1,64 +1,67 @@ -''' +""" Create post tables Revision ID: 336a76ec1338 Created at: 2016-04-19 12:06:08.649503 -''' +""" import sqlalchemy as sa from alembic import op -revision = '336a76ec1338' -down_revision = '00cb3a2734db' +revision = "336a76ec1338" +down_revision = "00cb3a2734db" branch_labels = None depends_on = None def upgrade(): op.create_table( - 'post', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('last_edit_time', sa.DateTime(), nullable=True), - sa.Column('safety', sa.Unicode(length=32), nullable=False), - sa.Column('type', sa.Unicode(length=32), nullable=False), - sa.Column('checksum', sa.Unicode(length=64), nullable=False), - sa.Column('source', sa.Unicode(length=200), nullable=True), - sa.Column('file_size', sa.Integer(), nullable=True), - sa.Column('image_width', sa.Integer(), nullable=True), - sa.Column('image_height', sa.Integer(), nullable=True), - sa.Column('flags', sa.Integer(), nullable=False), - sa.Column('auto_fav_count', sa.Integer(), nullable=False), - sa.Column('auto_score', sa.Integer(), nullable=False), - sa.Column('auto_feature_count', sa.Integer(), nullable=False), - sa.Column('auto_comment_count', sa.Integer(), nullable=False), - sa.Column('auto_note_count', sa.Integer(), nullable=False), - sa.Column('auto_fav_time', sa.Integer(), nullable=False), - sa.Column('auto_feature_time', sa.Integer(), nullable=False), - sa.Column('auto_comment_creation_time', sa.Integer(), nullable=False), - sa.Column('auto_comment_edit_time', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.PrimaryKeyConstraint('id')) + "post", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("last_edit_time", sa.DateTime(), nullable=True), + sa.Column("safety", sa.Unicode(length=32), nullable=False), + sa.Column("type", sa.Unicode(length=32), nullable=False), + sa.Column("checksum", sa.Unicode(length=64), nullable=False), + sa.Column("source", sa.Unicode(length=200), nullable=True), + sa.Column("file_size", sa.Integer(), nullable=True), + sa.Column("image_width", sa.Integer(), nullable=True), + sa.Column("image_height", sa.Integer(), nullable=True), + sa.Column("flags", sa.Integer(), nullable=False), + sa.Column("auto_fav_count", sa.Integer(), nullable=False), + sa.Column("auto_score", sa.Integer(), nullable=False), + sa.Column("auto_feature_count", sa.Integer(), nullable=False), + sa.Column("auto_comment_count", sa.Integer(), nullable=False), + sa.Column("auto_note_count", sa.Integer(), nullable=False), + sa.Column("auto_fav_time", sa.Integer(), nullable=False), + sa.Column("auto_feature_time", sa.Integer(), nullable=False), + sa.Column("auto_comment_creation_time", sa.Integer(), nullable=False), + sa.Column("auto_comment_edit_time", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'post_relation', - sa.Column('parent_id', sa.Integer(), nullable=False), - sa.Column('child_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['child_id'], ['post.id']), - sa.ForeignKeyConstraint(['parent_id'], ['post.id']), - sa.PrimaryKeyConstraint('parent_id', 'child_id')) + "post_relation", + sa.Column("parent_id", sa.Integer(), nullable=False), + sa.Column("child_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["child_id"], ["post.id"]), + sa.ForeignKeyConstraint(["parent_id"], ["post.id"]), + sa.PrimaryKeyConstraint("parent_id", "child_id"), + ) op.create_table( - 'post_tag', - sa.Column('post_id', sa.Integer(), nullable=False), - sa.Column('tag_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['post_id'], ['post.id']), - sa.ForeignKeyConstraint(['tag_id'], ['tag.id']), - sa.PrimaryKeyConstraint('post_id', 'tag_id')) + "post_tag", + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("tag_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.ForeignKeyConstraint(["tag_id"], ["tag.id"]), + sa.PrimaryKeyConstraint("post_id", "tag_id"), + ) def downgrade(): - op.drop_table('post_tag') - op.drop_table('post_relation') - op.drop_table('post') + op.drop_table("post_tag") + op.drop_table("post_relation") + op.drop_table("post") diff --git a/server/szurubooru/migrations/versions/3c1f0316fa7f_resize_post_columns.py b/server/szurubooru/migrations/versions/3c1f0316fa7f_resize_post_columns.py index 417803e..17e30d5 100644 --- a/server/szurubooru/migrations/versions/3c1f0316fa7f_resize_post_columns.py +++ b/server/szurubooru/migrations/versions/3c1f0316fa7f_resize_post_columns.py @@ -1,38 +1,34 @@ -''' +""" resize post columns Revision ID: 3c1f0316fa7f Created at: 2019-07-27 22:29:33.874837 -''' +""" import sqlalchemy as sa from alembic import op -revision = '3c1f0316fa7f' -down_revision = '1cd4c7b22846' +revision = "3c1f0316fa7f" +down_revision = "1cd4c7b22846" branch_labels = None depends_on = None def upgrade(): op.alter_column( - 'post', 'flags', - type_=sa.Unicode(32), - existing_type=sa.Unicode(200)) + "post", "flags", type_=sa.Unicode(32), existing_type=sa.Unicode(200) + ) op.alter_column( - 'post', 'source', - type_=sa.Unicode(2048), - existing_type=sa.Unicode(200)) + "post", "source", type_=sa.Unicode(2048), existing_type=sa.Unicode(200) + ) def downgrade(): op.alter_column( - 'post', 'flags', - type_=sa.Unicode(200), - existing_type=sa.Unicode(32)) + "post", "flags", type_=sa.Unicode(200), existing_type=sa.Unicode(32) + ) op.alter_column( - 'post', 'source', - type_=sa.Unicode(200), - existing_type=sa.Unicode(2048)) + "post", "source", type_=sa.Unicode(200), existing_type=sa.Unicode(2048) + ) diff --git a/server/szurubooru/migrations/versions/46cd5229839b_add_snapshot_resource_repr.py b/server/szurubooru/migrations/versions/46cd5229839b_add_snapshot_resource_repr.py index 0a46fbc..7620e90 100644 --- a/server/szurubooru/migrations/versions/46cd5229839b_add_snapshot_resource_repr.py +++ b/server/szurubooru/migrations/versions/46cd5229839b_add_snapshot_resource_repr.py @@ -1,24 +1,25 @@ -''' +""" Add snapshot resource_repr column Revision ID: 46cd5229839b Created at: 2016-04-21 19:00:48.087069 -''' +""" import sqlalchemy as sa from alembic import op -revision = '46cd5229839b' -down_revision = '565e01e3cf6d' +revision = "46cd5229839b" +down_revision = "565e01e3cf6d" branch_labels = None depends_on = None def upgrade(): op.add_column( - 'snapshot', - sa.Column('resource_repr', sa.Unicode(length=64), nullable=False)) + "snapshot", + sa.Column("resource_repr", sa.Unicode(length=64), nullable=False), + ) def downgrade(): - op.drop_column('snapshot', 'resource_repr') + op.drop_column("snapshot", "resource_repr") diff --git a/server/szurubooru/migrations/versions/46df355634dc_add_comment_tables.py b/server/szurubooru/migrations/versions/46df355634dc_add_comment_tables.py index 49971fe..c2a0092 100644 --- a/server/szurubooru/migrations/versions/46df355634dc_add_comment_tables.py +++ b/server/szurubooru/migrations/versions/46df355634dc_add_comment_tables.py @@ -1,43 +1,45 @@ -''' +""" Add comment tables Revision ID: 46df355634dc Created at: 2016-04-24 09:02:05.008648 -''' +""" import sqlalchemy as sa from alembic import op -revision = '46df355634dc' -down_revision = '84bd402f15f0' +revision = "46df355634dc" +down_revision = "84bd402f15f0" branch_labels = None depends_on = None def upgrade(): op.create_table( - 'comment', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('post_id', sa.Integer(), nullable=False), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('last_edit_time', sa.DateTime(), nullable=True), - sa.Column('text', sa.UnicodeText(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.ForeignKeyConstraint(['post_id'], ['post.id']), - sa.PrimaryKeyConstraint('id')) + "comment", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("last_edit_time", sa.DateTime(), nullable=True), + sa.Column("text", sa.UnicodeText(), nullable=True), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'comment_score', - sa.Column('comment_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('time', sa.DateTime(), nullable=False), - sa.Column('score', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['comment_id'], ['comment.id']), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.PrimaryKeyConstraint('comment_id', 'user_id')) + "comment_score", + sa.Column("comment_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("time", sa.DateTime(), nullable=False), + sa.Column("score", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["comment_id"], ["comment.id"]), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.PrimaryKeyConstraint("comment_id", "user_id"), + ) def downgrade(): - op.drop_table('comment_score') - op.drop_table('comment') + op.drop_table("comment_score") + op.drop_table("comment") diff --git a/server/szurubooru/migrations/versions/49ab4e1139ef_create_indexes.py b/server/szurubooru/migrations/versions/49ab4e1139ef_create_indexes.py index b18e410..73a0ad1 100644 --- a/server/szurubooru/migrations/versions/49ab4e1139ef_create_indexes.py +++ b/server/szurubooru/migrations/versions/49ab4e1139ef_create_indexes.py @@ -1,71 +1,74 @@ -''' +""" Create indexes Revision ID: 49ab4e1139ef Created at: 2016-05-09 09:38:28.078936 -''' +""" import sqlalchemy as sa from alembic import op -revision = '49ab4e1139ef' -down_revision = '23abaf4a0a4b' +revision = "49ab4e1139ef" +down_revision = "23abaf4a0a4b" branch_labels = None depends_on = None def upgrade(): for index_name, table_name, column_name in [ - ('ix_comment_post_id', 'comment', 'post_id'), - ('ix_comment_user_id', 'comment', 'user_id'), - ('ix_comment_score_user_id', 'comment_score', 'user_id'), - ('ix_post_user_id', 'post', 'user_id'), - ('ix_post_favorite_post_id', 'post_favorite', 'post_id'), - ('ix_post_favorite_user_id', 'post_favorite', 'user_id'), - ('ix_post_feature_post_id', 'post_feature', 'post_id'), - ('ix_post_feature_user_id', 'post_feature', 'user_id'), - ('ix_post_note_post_id', 'post_note', 'post_id'), - ('ix_post_relation_child_id', 'post_relation', 'child_id'), - ('ix_post_relation_parent_id', 'post_relation', 'parent_id'), - ('ix_post_score_post_id', 'post_score', 'post_id'), - ('ix_post_score_user_id', 'post_score', 'user_id'), - ('ix_post_tag_post_id', 'post_tag', 'post_id'), - ('ix_post_tag_tag_id', 'post_tag', 'tag_id'), - ('ix_snapshot_resource_id', 'snapshot', 'resource_id'), - ('ix_snapshot_resource_type', 'snapshot', 'resource_type'), - ('ix_tag_category_id', 'tag', 'category_id'), - ('ix_tag_implication_child_id', 'tag_implication', 'child_id'), - ('ix_tag_implication_parent_id', 'tag_implication', 'parent_id'), - ('ix_tag_name_tag_id', 'tag_name', 'tag_id'), - ('ix_tag_suggestion_child_id', 'tag_suggestion', 'child_id'), - ('ix_tag_suggestion_parent_id', 'tag_suggestion', 'parent_id')]: + ("ix_comment_post_id", "comment", "post_id"), + ("ix_comment_user_id", "comment", "user_id"), + ("ix_comment_score_user_id", "comment_score", "user_id"), + ("ix_post_user_id", "post", "user_id"), + ("ix_post_favorite_post_id", "post_favorite", "post_id"), + ("ix_post_favorite_user_id", "post_favorite", "user_id"), + ("ix_post_feature_post_id", "post_feature", "post_id"), + ("ix_post_feature_user_id", "post_feature", "user_id"), + ("ix_post_note_post_id", "post_note", "post_id"), + ("ix_post_relation_child_id", "post_relation", "child_id"), + ("ix_post_relation_parent_id", "post_relation", "parent_id"), + ("ix_post_score_post_id", "post_score", "post_id"), + ("ix_post_score_user_id", "post_score", "user_id"), + ("ix_post_tag_post_id", "post_tag", "post_id"), + ("ix_post_tag_tag_id", "post_tag", "tag_id"), + ("ix_snapshot_resource_id", "snapshot", "resource_id"), + ("ix_snapshot_resource_type", "snapshot", "resource_type"), + ("ix_tag_category_id", "tag", "category_id"), + ("ix_tag_implication_child_id", "tag_implication", "child_id"), + ("ix_tag_implication_parent_id", "tag_implication", "parent_id"), + ("ix_tag_name_tag_id", "tag_name", "tag_id"), + ("ix_tag_suggestion_child_id", "tag_suggestion", "child_id"), + ("ix_tag_suggestion_parent_id", "tag_suggestion", "parent_id"), + ]: op.create_index( - op.f(index_name), table_name, [column_name], unique=False) + op.f(index_name), table_name, [column_name], unique=False + ) def downgrade(): for index_name, table_name in [ - ('ix_tag_suggestion_parent_id', 'tag_suggestion'), - ('ix_tag_suggestion_child_id', 'tag_suggestion'), - ('ix_tag_name_tag_id', 'tag_name'), - ('ix_tag_implication_parent_id', 'tag_implication'), - ('ix_tag_implication_child_id', 'tag_implication'), - ('ix_tag_category_id', 'tag'), - ('ix_snapshot_resource_type', 'snapshot'), - ('ix_snapshot_resource_id', 'snapshot'), - ('ix_post_tag_tag_id', 'post_tag'), - ('ix_post_tag_post_id', 'post_tag'), - ('ix_post_score_user_id', 'post_score'), - ('ix_post_score_post_id', 'post_score'), - ('ix_post_relation_parent_id', 'post_relation'), - ('ix_post_relation_child_id', 'post_relation'), - ('ix_post_note_post_id', 'post_note'), - ('ix_post_feature_user_id', 'post_feature'), - ('ix_post_feature_post_id', 'post_feature'), - ('ix_post_favorite_user_id', 'post_favorite'), - ('ix_post_favorite_post_id', 'post_favorite'), - ('ix_post_user_id', 'post'), - ('ix_comment_score_user_id', 'comment_score'), - ('ix_comment_user_id', 'comment'), - ('ix_comment_post_id', 'comment')]: + ("ix_tag_suggestion_parent_id", "tag_suggestion"), + ("ix_tag_suggestion_child_id", "tag_suggestion"), + ("ix_tag_name_tag_id", "tag_name"), + ("ix_tag_implication_parent_id", "tag_implication"), + ("ix_tag_implication_child_id", "tag_implication"), + ("ix_tag_category_id", "tag"), + ("ix_snapshot_resource_type", "snapshot"), + ("ix_snapshot_resource_id", "snapshot"), + ("ix_post_tag_tag_id", "post_tag"), + ("ix_post_tag_post_id", "post_tag"), + ("ix_post_score_user_id", "post_score"), + ("ix_post_score_post_id", "post_score"), + ("ix_post_relation_parent_id", "post_relation"), + ("ix_post_relation_child_id", "post_relation"), + ("ix_post_note_post_id", "post_note"), + ("ix_post_feature_user_id", "post_feature"), + ("ix_post_feature_post_id", "post_feature"), + ("ix_post_favorite_user_id", "post_favorite"), + ("ix_post_favorite_post_id", "post_favorite"), + ("ix_post_user_id", "post"), + ("ix_comment_score_user_id", "comment_score"), + ("ix_comment_user_id", "comment"), + ("ix_comment_post_id", "comment"), + ]: op.drop_index(op.f(index_name), table_name=table_name) diff --git a/server/szurubooru/migrations/versions/4a020f1d271a_rename_snapshot_columns.py b/server/szurubooru/migrations/versions/4a020f1d271a_rename_snapshot_columns.py index e1c98ca..957bede 100644 --- a/server/szurubooru/migrations/versions/4a020f1d271a_rename_snapshot_columns.py +++ b/server/szurubooru/migrations/versions/4a020f1d271a_rename_snapshot_columns.py @@ -1,54 +1,57 @@ -''' +""" Rename snapshot columns Revision ID: 4a020f1d271a Created at: 2016-08-16 09:25:38.350861 -''' +""" import sqlalchemy as sa from alembic import op - -revision = '4a020f1d271a' -down_revision = '840b460c5613' +revision = "4a020f1d271a" +down_revision = "840b460c5613" branch_labels = None depends_on = None def upgrade(): op.add_column( - 'snapshot', - sa.Column('resource_name', sa.Unicode(length=64), nullable=False)) + "snapshot", + sa.Column("resource_name", sa.Unicode(length=64), nullable=False), + ) op.add_column( - 'snapshot', - sa.Column('resource_pkey', sa.Integer(), nullable=False)) + "snapshot", sa.Column("resource_pkey", sa.Integer(), nullable=False) + ) op.create_index( - op.f('ix_snapshot_resource_pkey'), - 'snapshot', - ['resource_pkey'], - unique=False) - op.drop_index('ix_snapshot_resource_id', table_name='snapshot') - op.drop_column('snapshot', 'resource_id') - op.drop_column('snapshot', 'resource_repr') + op.f("ix_snapshot_resource_pkey"), + "snapshot", + ["resource_pkey"], + unique=False, + ) + op.drop_index("ix_snapshot_resource_id", table_name="snapshot") + op.drop_column("snapshot", "resource_id") + op.drop_column("snapshot", "resource_repr") def downgrade(): op.add_column( - 'snapshot', + "snapshot", sa.Column( - 'resource_repr', + "resource_repr", sa.VARCHAR(length=64), autoincrement=False, - nullable=False)) + nullable=False, + ), + ) op.add_column( - 'snapshot', + "snapshot", sa.Column( - 'resource_id', - sa.INTEGER(), - autoincrement=False, - nullable=False)) + "resource_id", sa.INTEGER(), autoincrement=False, nullable=False + ), + ) op.create_index( - 'ix_snapshot_resource_id', 'snapshot', ['resource_id'], unique=False) - op.drop_index(op.f('ix_snapshot_resource_pkey'), table_name='snapshot') - op.drop_column('snapshot', 'resource_pkey') - op.drop_column('snapshot', 'resource_name') + "ix_snapshot_resource_id", "snapshot", ["resource_id"], unique=False + ) + op.drop_index(op.f("ix_snapshot_resource_pkey"), table_name="snapshot") + op.drop_column("snapshot", "resource_pkey") + op.drop_column("snapshot", "resource_name") diff --git a/server/szurubooru/migrations/versions/4c526f869323_add_description_to_tags.py b/server/szurubooru/migrations/versions/4c526f869323_add_description_to_tags.py index f53866f..443bc3c 100644 --- a/server/szurubooru/migrations/versions/4c526f869323_add_description_to_tags.py +++ b/server/szurubooru/migrations/versions/4c526f869323_add_description_to_tags.py @@ -1,23 +1,24 @@ -''' +""" Add description to tags Revision ID: 4c526f869323 Created at: 2016-06-21 17:56:34.979741 -''' +""" import sqlalchemy as sa from alembic import op -revision = '4c526f869323' -down_revision = '055d0e048fb3' +revision = "4c526f869323" +down_revision = "055d0e048fb3" branch_labels = None depends_on = None def upgrade(): op.add_column( - 'tag', sa.Column('description', sa.UnicodeText(), nullable=True)) + "tag", sa.Column("description", sa.UnicodeText(), nullable=True) + ) def downgrade(): - op.drop_column('tag', 'description') + op.drop_column("tag", "description") diff --git a/server/szurubooru/migrations/versions/52d6ea6584b8_generate_post_signature_table.py b/server/szurubooru/migrations/versions/52d6ea6584b8_generate_post_signature_table.py index 5a86573..e544f80 100644 --- a/server/szurubooru/migrations/versions/52d6ea6584b8_generate_post_signature_table.py +++ b/server/szurubooru/migrations/versions/52d6ea6584b8_generate_post_signature_table.py @@ -1,16 +1,15 @@ -''' +""" Generate post signature table Revision ID: 52d6ea6584b8 Created at: 2020-03-07 17:03:40.193512 -''' +""" import sqlalchemy as sa from alembic import op - -revision = '52d6ea6584b8' -down_revision = '3c1f0316fa7f' +revision = "52d6ea6584b8" +down_revision = "3c1f0316fa7f" branch_labels = None depends_on = None @@ -18,13 +17,14 @@ depends_on = None def upgrade(): ArrayType = sa.dialects.postgresql.ARRAY(sa.Integer, dimensions=1) op.create_table( - 'post_signature', - sa.Column('post_id', sa.Integer(), nullable=False), - sa.Column('signature', sa.LargeBinary(), nullable=False), - sa.Column('words', ArrayType, nullable=False), - sa.ForeignKeyConstraint(['post_id'], ['post.id']), - sa.PrimaryKeyConstraint('post_id')) + "post_signature", + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("signature", sa.LargeBinary(), nullable=False), + sa.Column("words", ArrayType, nullable=False), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.PrimaryKeyConstraint("post_id"), + ) def downgrade(): - op.drop_table('post_signature') + op.drop_table("post_signature") diff --git a/server/szurubooru/migrations/versions/54de8acc6cef_add_default_pool_category.py b/server/szurubooru/migrations/versions/54de8acc6cef_add_default_pool_category.py index eaa68f2..73bf342 100644 --- a/server/szurubooru/migrations/versions/54de8acc6cef_add_default_pool_category.py +++ b/server/szurubooru/migrations/versions/54de8acc6cef_add_default_pool_category.py @@ -1,16 +1,15 @@ -''' +""" add default pool category Revision ID: 54de8acc6cef Created at: 2020-05-03 14:57:46.825766 -''' +""" import sqlalchemy as sa from alembic import op - -revision = '54de8acc6cef' -down_revision = '6a2f424ec9d2' +revision = "54de8acc6cef" +down_revision = "6a2f424ec9d2" branch_labels = None depends_on = None @@ -19,18 +18,18 @@ Base = sa.ext.declarative.declarative_base() class PoolCategory(Base): - __tablename__ = 'pool_category' - __table_args__ = {'extend_existing': True} + __tablename__ = "pool_category" + __table_args__ = {"extend_existing": True} - pool_category_id = sa.Column('id', sa.Integer, primary_key=True) - version = sa.Column('version', sa.Integer, nullable=False) - name = sa.Column('name', sa.Unicode(32), nullable=False) - color = sa.Column('color', sa.Unicode(32), nullable=False) - default = sa.Column('default', sa.Boolean, nullable=False) + pool_category_id = sa.Column("id", sa.Integer, primary_key=True) + version = sa.Column("version", sa.Integer, nullable=False) + name = sa.Column("name", sa.Unicode(32), nullable=False) + color = sa.Column("color", sa.Unicode(32), nullable=False) + default = sa.Column("default", sa.Boolean, nullable=False) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } @@ -38,8 +37,8 @@ def upgrade(): session = sa.orm.session.Session(bind=op.get_bind()) if session.query(PoolCategory).count() == 0: category = PoolCategory() - category.name = 'default' - category.color = 'default' + category.name = "default" + category.color = "default" category.version = 1 category.default = True session.add(category) @@ -49,13 +48,13 @@ def upgrade(): def downgrade(): session = sa.orm.session.Session(bind=op.get_bind()) default_category = ( - session - .query(PoolCategory) - .filter(PoolCategory.name == 'default') - .filter(PoolCategory.color == 'default') + session.query(PoolCategory) + .filter(PoolCategory.name == "default") + .filter(PoolCategory.color == "default") .filter(PoolCategory.version == 1) .filter(PoolCategory.default == 1) - .one_or_none()) + .one_or_none() + ) if default_category: session.delete(default_category) session.commit() diff --git a/server/szurubooru/migrations/versions/565e01e3cf6d_create_snapshot_table.py b/server/szurubooru/migrations/versions/565e01e3cf6d_create_snapshot_table.py index 475fe96..a4fb89f 100644 --- a/server/szurubooru/migrations/versions/565e01e3cf6d_create_snapshot_table.py +++ b/server/szurubooru/migrations/versions/565e01e3cf6d_create_snapshot_table.py @@ -1,32 +1,33 @@ -''' +""" Create snapshot table Revision ID: 565e01e3cf6d Created at: 2016-04-19 12:07:58.372426 -''' +""" import sqlalchemy as sa from alembic import op -revision = '565e01e3cf6d' -down_revision = '336a76ec1338' +revision = "565e01e3cf6d" +down_revision = "336a76ec1338" branch_labels = None depends_on = None def upgrade(): op.create_table( - 'snapshot', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('resource_type', sa.Unicode(length=32), nullable=False), - sa.Column('resource_id', sa.Integer(), nullable=False), - sa.Column('operation', sa.Unicode(length=16), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('data', sa.PickleType(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.PrimaryKeyConstraint('id')) + "snapshot", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("resource_type", sa.Unicode(length=32), nullable=False), + sa.Column("resource_id", sa.Integer(), nullable=False), + sa.Column("operation", sa.Unicode(length=16), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("data", sa.PickleType(), nullable=True), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.PrimaryKeyConstraint("id"), + ) def downgrade(): - op.drop_table('snapshot') + op.drop_table("snapshot") diff --git a/server/szurubooru/migrations/versions/5f00af3004a4_add_default_tag_category.py b/server/szurubooru/migrations/versions/5f00af3004a4_add_default_tag_category.py index e097e67..716a04e 100644 --- a/server/szurubooru/migrations/versions/5f00af3004a4_add_default_tag_category.py +++ b/server/szurubooru/migrations/versions/5f00af3004a4_add_default_tag_category.py @@ -1,18 +1,17 @@ -''' +""" Add default tag category Revision ID: 5f00af3004a4 Created at: 2017-02-02 20:06:13.336380 -''' +""" import sqlalchemy as sa -from alembic import op import sqlalchemy.ext.declarative import sqlalchemy.orm.session +from alembic import op - -revision = '5f00af3004a4' -down_revision = '9837fc981ec7' +revision = "5f00af3004a4" +down_revision = "9837fc981ec7" branch_labels = None depends_on = None @@ -21,18 +20,18 @@ Base = sa.ext.declarative.declarative_base() class TagCategory(Base): - __tablename__ = 'tag_category' - __table_args__ = {'extend_existing': True} + __tablename__ = "tag_category" + __table_args__ = {"extend_existing": True} - tag_category_id = sa.Column('id', sa.Integer, primary_key=True) - version = sa.Column('version', sa.Integer, nullable=False) - name = sa.Column('name', sa.Unicode(32), nullable=False) - color = sa.Column('color', sa.Unicode(32), nullable=False) - default = sa.Column('default', sa.Boolean, nullable=False) + tag_category_id = sa.Column("id", sa.Integer, primary_key=True) + version = sa.Column("version", sa.Integer, nullable=False) + name = sa.Column("name", sa.Unicode(32), nullable=False) + color = sa.Column("color", sa.Unicode(32), nullable=False) + default = sa.Column("default", sa.Boolean, nullable=False) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } @@ -40,8 +39,8 @@ def upgrade(): session = sa.orm.session.Session(bind=op.get_bind()) if session.query(TagCategory).count() == 0: category = TagCategory() - category.name = 'default' - category.color = 'default' + category.name = "default" + category.color = "default" category.version = 1 category.default = True session.add(category) @@ -51,13 +50,13 @@ def upgrade(): def downgrade(): session = sa.orm.session.Session(bind=op.get_bind()) default_category = ( - session - .query(TagCategory) - .filter(TagCategory.name == 'default') - .filter(TagCategory.color == 'default') + session.query(TagCategory) + .filter(TagCategory.name == "default") + .filter(TagCategory.color == "default") .filter(TagCategory.version == 1) .filter(TagCategory.default == 1) - .one_or_none()) + .one_or_none() + ) if default_category: session.delete(default_category) session.commit() diff --git a/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py b/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py index 18a0d7a..197373d 100644 --- a/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py +++ b/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py @@ -1,64 +1,67 @@ -''' +""" create pool tables Revision ID: 6a2f424ec9d2 Created at: 2020-05-03 14:47:59.136410 -''' +""" import sqlalchemy as sa from alembic import op - -revision = '6a2f424ec9d2' -down_revision = '1e280b5d5df1' +revision = "6a2f424ec9d2" +down_revision = "1e280b5d5df1" branch_labels = None depends_on = None def upgrade(): op.create_table( - 'pool_category', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('version', sa.Integer(), nullable=False, default=1), - sa.Column('name', sa.Unicode(length=32), nullable=False), - sa.Column('color', sa.Unicode(length=32), nullable=False), - sa.Column('default', sa.Boolean(), nullable=False, default=False), - sa.PrimaryKeyConstraint('id')) + "pool_category", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("version", sa.Integer(), nullable=False, default=1), + sa.Column("name", sa.Unicode(length=32), nullable=False), + sa.Column("color", sa.Unicode(length=32), nullable=False), + sa.Column("default", sa.Boolean(), nullable=False, default=False), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'pool', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('version', sa.Integer(), nullable=False, default=1), - sa.Column('description', sa.UnicodeText(), nullable=True), - sa.Column('category_id', sa.Integer(), nullable=False), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('last_edit_time', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['category_id'], ['pool_category.id']), - sa.PrimaryKeyConstraint('id')) + "pool", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("version", sa.Integer(), nullable=False, default=1), + sa.Column("description", sa.UnicodeText(), nullable=True), + sa.Column("category_id", sa.Integer(), nullable=False), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("last_edit_time", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(["category_id"], ["pool_category.id"]), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'pool_name', - sa.Column('pool_name_id', sa.Integer(), nullable=False), - sa.Column('pool_id', sa.Integer(), nullable=False), - sa.Column('name', sa.Unicode(length=256), nullable=False), - sa.Column('ord', sa.Integer(), nullable=False, index=True), - sa.ForeignKeyConstraint(['pool_id'], ['pool.id']), - sa.PrimaryKeyConstraint('pool_name_id'), - sa.UniqueConstraint('name')) + "pool_name", + sa.Column("pool_name_id", sa.Integer(), nullable=False), + sa.Column("pool_id", sa.Integer(), nullable=False), + sa.Column("name", sa.Unicode(length=256), nullable=False), + sa.Column("ord", sa.Integer(), nullable=False, index=True), + sa.ForeignKeyConstraint(["pool_id"], ["pool.id"]), + 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, index=True), - 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')) + "pool_post", + sa.Column("pool_id", sa.Integer(), nullable=False), + sa.Column("post_id", sa.Integer(), nullable=False, index=True), + 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') + 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/migrations/versions/7f6baf38c27c_add_versions.py b/server/szurubooru/migrations/versions/7f6baf38c27c_add_versions.py index 2236026..24056c0 100644 --- a/server/szurubooru/migrations/versions/7f6baf38c27c_add_versions.py +++ b/server/szurubooru/migrations/versions/7f6baf38c27c_add_versions.py @@ -1,31 +1,30 @@ -''' +""" Add entity versions Revision ID: 7f6baf38c27c Created at: 2016-08-06 22:26:58.111763 -''' +""" import sqlalchemy as sa from alembic import op -revision = '7f6baf38c27c' -down_revision = '4c526f869323' +revision = "7f6baf38c27c" +down_revision = "4c526f869323" branch_labels = None depends_on = None -tables = ['tag_category', 'tag', 'user', 'post', 'comment'] +tables = ["tag_category", "tag", "user", "post", "comment"] def upgrade(): for table in tables: - op.add_column(table, sa.Column('version', sa.Integer(), nullable=True)) + op.add_column(table, sa.Column("version", sa.Integer(), nullable=True)) op.execute( - sa.table(table, sa.column('version')) - .update() - .values(version=1)) - op.alter_column(table, 'version', nullable=False) + sa.table(table, sa.column("version")).update().values(version=1) + ) + op.alter_column(table, "version", nullable=False) def downgrade(): for table in tables: - op.drop_column(table, 'version') + op.drop_column(table, "version") diff --git a/server/szurubooru/migrations/versions/840b460c5613_fix_foreignkeys.py b/server/szurubooru/migrations/versions/840b460c5613_fix_foreignkeys.py index 8821dd8..b2b5df5 100644 --- a/server/szurubooru/migrations/versions/840b460c5613_fix_foreignkeys.py +++ b/server/szurubooru/migrations/versions/840b460c5613_fix_foreignkeys.py @@ -1,33 +1,36 @@ -''' +""" Fix ForeignKey constraint definitions Revision ID: 840b460c5613 Created at: 2016-08-15 18:39:30.909867 -''' +""" import sqlalchemy as sa from alembic import op - -revision = '840b460c5613' -down_revision = '7f6baf38c27c' +revision = "840b460c5613" +down_revision = "7f6baf38c27c" branch_labels = None depends_on = None def upgrade(): - op.drop_constraint('post_user_id_fkey', 'post', type_='foreignkey') - op.drop_constraint('snapshot_user_id_fkey', 'snapshot', type_='foreignkey') + op.drop_constraint("post_user_id_fkey", "post", type_="foreignkey") + op.drop_constraint("snapshot_user_id_fkey", "snapshot", type_="foreignkey") op.create_foreign_key( - None, 'post', 'user', ['user_id'], ['id'], ondelete='SET NULL') + None, "post", "user", ["user_id"], ["id"], ondelete="SET NULL" + ) op.create_foreign_key( - None, 'snapshot', 'user', ['user_id'], ['id'], ondelete='set null') + None, "snapshot", "user", ["user_id"], ["id"], ondelete="set null" + ) def downgrade(): - op.drop_constraint(None, 'snapshot', type_='foreignkey') - op.drop_constraint(None, 'post', type_='foreignkey') + op.drop_constraint(None, "snapshot", type_="foreignkey") + op.drop_constraint(None, "post", type_="foreignkey") op.create_foreign_key( - 'snapshot_user_id_fkey', 'snapshot', 'user', ['user_id'], ['id']) + "snapshot_user_id_fkey", "snapshot", "user", ["user_id"], ["id"] + ) op.create_foreign_key( - 'post_user_id_fkey', 'post', 'user', ['user_id'], ['id']) + "post_user_id_fkey", "post", "user", ["user_id"], ["id"] + ) diff --git a/server/szurubooru/migrations/versions/84bd402f15f0_change_flags_column_type.py b/server/szurubooru/migrations/versions/84bd402f15f0_change_flags_column_type.py index 7236641..d495b82 100644 --- a/server/szurubooru/migrations/versions/84bd402f15f0_change_flags_column_type.py +++ b/server/szurubooru/migrations/versions/84bd402f15f0_change_flags_column_type.py @@ -1,26 +1,27 @@ -''' +""" Change flags column type Revision ID: 84bd402f15f0 Created at: 2016-04-22 20:48:32.386159 -''' +""" import sqlalchemy as sa from alembic import op -revision = '84bd402f15f0' -down_revision = '9587de88a84b' +revision = "84bd402f15f0" +down_revision = "9587de88a84b" branch_labels = None depends_on = None def upgrade(): - op.drop_column('post', 'flags') - op.add_column('post', sa.Column('flags', sa.PickleType(), nullable=True)) + op.drop_column("post", "flags") + op.add_column("post", sa.Column("flags", sa.PickleType(), nullable=True)) def downgrade(): - op.drop_column('post', 'flags') + op.drop_column("post", "flags") op.add_column( - 'post', - sa.Column('flags', sa.Integer(), autoincrement=False, nullable=False)) + "post", + sa.Column("flags", sa.Integer(), autoincrement=False, nullable=False), + ) diff --git a/server/szurubooru/migrations/versions/9587de88a84b_create_aux_post_tables.py b/server/szurubooru/migrations/versions/9587de88a84b_create_aux_post_tables.py index eddec24..46647cf 100644 --- a/server/szurubooru/migrations/versions/9587de88a84b_create_aux_post_tables.py +++ b/server/szurubooru/migrations/versions/9587de88a84b_create_aux_post_tables.py @@ -1,61 +1,65 @@ -''' +""" Create auxilliary post tables Revision ID: 9587de88a84b Created at: 2016-04-22 17:42:57.697229 -''' +""" import sqlalchemy as sa from alembic import op -revision = '9587de88a84b' -down_revision = '46cd5229839b' +revision = "9587de88a84b" +down_revision = "46cd5229839b" branch_labels = None depends_on = None def upgrade(): op.create_table( - 'post_favorite', - sa.Column('post_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('time', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['post_id'], ['post.id']), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.PrimaryKeyConstraint('post_id', 'user_id')) + "post_favorite", + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("time", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.PrimaryKeyConstraint("post_id", "user_id"), + ) op.create_table( - 'post_feature', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('post_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('time', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['post_id'], ['post.id']), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.PrimaryKeyConstraint('id')) + "post_feature", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("time", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'post_note', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('post_id', sa.Integer(), nullable=False), - sa.Column('text', sa.UnicodeText(), nullable=False), - sa.Column('polygon', sa.PickleType(), nullable=False), - sa.ForeignKeyConstraint(['post_id'], ['post.id']), - sa.PrimaryKeyConstraint('id')) + "post_note", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("text", sa.UnicodeText(), nullable=False), + sa.Column("polygon", sa.PickleType(), nullable=False), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'post_score', - sa.Column('post_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('time', sa.DateTime(), nullable=False), - sa.Column('score', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['post_id'], ['post.id']), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.PrimaryKeyConstraint('post_id', 'user_id')) + "post_score", + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("time", sa.DateTime(), nullable=False), + sa.Column("score", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.PrimaryKeyConstraint("post_id", "user_id"), + ) def downgrade(): - op.drop_table('post_score') - op.drop_table('post_note') - op.drop_table('post_feature') - op.drop_table('post_favorite') + op.drop_table("post_score") + op.drop_table("post_note") + op.drop_table("post_feature") + op.drop_table("post_favorite") diff --git a/server/szurubooru/migrations/versions/9837fc981ec7_add_order_to_tag_names.py b/server/szurubooru/migrations/versions/9837fc981ec7_add_order_to_tag_names.py index 39f9edf..d1e74e9 100644 --- a/server/szurubooru/migrations/versions/9837fc981ec7_add_order_to_tag_names.py +++ b/server/szurubooru/migrations/versions/9837fc981ec7_add_order_to_tag_names.py @@ -1,17 +1,16 @@ -''' +""" Add order to tag names Revision ID: 9837fc981ec7 Created at: 2016-08-28 19:03:59.831527 -''' +""" import sqlalchemy as sa -from alembic import op import sqlalchemy.ext.declarative +from alembic import op - -revision = '9837fc981ec7' -down_revision = '4a020f1d271a' +revision = "9837fc981ec7" +down_revision = "4a020f1d271a" branch_labels = None depends_on = None @@ -20,21 +19,20 @@ Base = sa.ext.declarative.declarative_base() class TagName(Base): - __tablename__ = 'tag_name' - __table_args__ = {'extend_existing': True} + __tablename__ = "tag_name" + __table_args__ = {"extend_existing": True} - tag_name_id = sa.Column('tag_name_id', sa.Integer, primary_key=True) - ord = sa.Column('ord', sa.Integer, nullable=False, index=True) + tag_name_id = sa.Column("tag_name_id", sa.Integer, primary_key=True) + ord = sa.Column("ord", sa.Integer, nullable=False, index=True) def upgrade(): - op.add_column('tag_name', sa.Column('ord', sa.Integer(), nullable=True)) + op.add_column("tag_name", sa.Column("ord", sa.Integer(), nullable=True)) op.execute(TagName.__table__.update().values(ord=TagName.tag_name_id)) - op.alter_column('tag_name', 'ord', nullable=False) - op.create_index( - op.f('ix_tag_name_ord'), 'tag_name', ['ord'], unique=False) + op.alter_column("tag_name", "ord", nullable=False) + op.create_index(op.f("ix_tag_name_ord"), "tag_name", ["ord"], unique=False) def downgrade(): - op.drop_index(op.f('ix_tag_name_ord'), table_name='tag_name') - op.drop_column('tag_name', 'ord') + op.drop_index(op.f("ix_tag_name_ord"), table_name="tag_name") + op.drop_column("tag_name", "ord") diff --git a/server/szurubooru/migrations/versions/9ef1a1643c2a_update_user_table_for_hardened_passwords.py b/server/szurubooru/migrations/versions/9ef1a1643c2a_update_user_table_for_hardened_passwords.py index 3805772..b8763a7 100644 --- a/server/szurubooru/migrations/versions/9ef1a1643c2a_update_user_table_for_hardened_passwords.py +++ b/server/szurubooru/migrations/versions/9ef1a1643c2a_update_user_table_for_hardened_passwords.py @@ -1,19 +1,18 @@ -''' +""" Alter the password_hash field to work with larger output. Particularly libsodium output for greater password security. Revision ID: 9ef1a1643c2a Created at: 2018-02-24 23:00:32.848575 -''' +""" import sqlalchemy as sa import sqlalchemy.ext.declarative import sqlalchemy.orm.session from alembic import op - -revision = '9ef1a1643c2a' -down_revision = '02ef5f73f4ab' +revision = "9ef1a1643c2a" +down_revision = "02ef5f73f4ab" branch_labels = None depends_on = None @@ -21,43 +20,46 @@ Base = sa.ext.declarative.declarative_base() class User(Base): - __tablename__ = 'user' + __tablename__ = "user" - AVATAR_GRAVATAR = 'gravatar' + AVATAR_GRAVATAR = "gravatar" - user_id = sa.Column('id', sa.Integer, primary_key=True) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - last_login_time = sa.Column('last_login_time', sa.DateTime) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - name = sa.Column('name', sa.Unicode(50), nullable=False, unique=True) - password_hash = sa.Column('password_hash', sa.Unicode(128), nullable=False) - password_salt = sa.Column('password_salt', sa.Unicode(32)) + user_id = sa.Column("id", sa.Integer, primary_key=True) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_login_time = sa.Column("last_login_time", sa.DateTime) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + name = sa.Column("name", sa.Unicode(50), nullable=False, unique=True) + password_hash = sa.Column("password_hash", sa.Unicode(128), nullable=False) + password_salt = sa.Column("password_salt", sa.Unicode(32)) password_revision = sa.Column( - 'password_revision', sa.SmallInteger, default=0, nullable=False) - email = sa.Column('email', sa.Unicode(64), nullable=True) - rank = sa.Column('rank', sa.Unicode(32), nullable=False) + "password_revision", sa.SmallInteger, default=0, nullable=False + ) + email = sa.Column("email", sa.Unicode(64), nullable=True) + rank = sa.Column("rank", sa.Unicode(32), nullable=False) avatar_style = sa.Column( - 'avatar_style', sa.Unicode(32), nullable=False, - default=AVATAR_GRAVATAR) + "avatar_style", sa.Unicode(32), nullable=False, default=AVATAR_GRAVATAR + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } def upgrade(): op.alter_column( - 'user', - 'password_hash', + "user", + "password_hash", existing_type=sa.VARCHAR(length=64), type_=sa.Unicode(length=128), - existing_nullable=False) - op.add_column('user', sa.Column( - 'password_revision', - sa.SmallInteger(), - nullable=True, - default=0)) + existing_nullable=False, + ) + op.add_column( + "user", + sa.Column( + "password_revision", sa.SmallInteger(), nullable=True, default=0 + ), + ) session = sa.orm.session.Session(bind=op.get_bind()) if session.query(User).count() >= 0: @@ -73,17 +75,16 @@ def upgrade(): session.commit() op.alter_column( - 'user', - 'password_revision', - existing_nullable=True, - nullable=False) + "user", "password_revision", existing_nullable=True, nullable=False + ) def downgrade(): op.alter_column( - 'user', - 'password_hash', + "user", + "password_hash", existing_type=sa.Unicode(length=128), type_=sa.VARCHAR(length=64), - existing_nullable=False) - op.drop_column('user', 'password_revision') + existing_nullable=False, + ) + op.drop_column("user", "password_revision") diff --git a/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py b/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py index 899eaa7..57dda5c 100644 --- a/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py +++ b/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py @@ -1,39 +1,40 @@ -''' +""" Added a user_token table for API authorization Revision ID: a39c7f98a7fa Created at: 2018-02-25 01:31:27.345595 -''' +""" import sqlalchemy as sa from alembic import op - -revision = 'a39c7f98a7fa' -down_revision = '9ef1a1643c2a' +revision = "a39c7f98a7fa" +down_revision = "9ef1a1643c2a" branch_labels = None depends_on = None def upgrade(): op.create_table( - 'user_token', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('token', sa.Unicode(length=36), nullable=False), - sa.Column('note', sa.Unicode(length=128), nullable=True), - sa.Column('enabled', sa.Boolean(), nullable=False), - sa.Column('expiration_time', sa.DateTime(), nullable=True), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('last_edit_time', sa.DateTime(), nullable=True), - sa.Column('last_usage_time', sa.DateTime(), nullable=True), - sa.Column('version', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id')) + "user_token", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("token", sa.Unicode(length=36), nullable=False), + sa.Column("note", sa.Unicode(length=128), nullable=True), + sa.Column("enabled", sa.Boolean(), nullable=False), + sa.Column("expiration_time", sa.DateTime(), nullable=True), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("last_edit_time", sa.DateTime(), nullable=True), + sa.Column("last_usage_time", sa.DateTime(), nullable=True), + sa.Column("version", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) op.create_index( - op.f('ix_user_token_user_id'), 'user_token', ['user_id'], unique=False) + op.f("ix_user_token_user_id"), "user_token", ["user_id"], unique=False + ) def downgrade(): - op.drop_index(op.f('ix_user_token_user_id'), table_name='user_token') - op.drop_table('user_token') + op.drop_index(op.f("ix_user_token_user_id"), table_name="user_token") + op.drop_table("user_token") diff --git a/server/szurubooru/migrations/versions/e5c1216a8503_create_user_table.py b/server/szurubooru/migrations/versions/e5c1216a8503_create_user_table.py index a84e31b..672d4eb 100644 --- a/server/szurubooru/migrations/versions/e5c1216a8503_create_user_table.py +++ b/server/szurubooru/migrations/versions/e5c1216a8503_create_user_table.py @@ -1,14 +1,14 @@ -''' +""" Create user table Revision ID: e5c1216a8503 Created at: 2016-03-20 15:53:25.030415 -''' +""" import sqlalchemy as sa from alembic import op -revision = 'e5c1216a8503' +revision = "e5c1216a8503" down_revision = None branch_labels = None depends_on = None @@ -16,19 +16,20 @@ depends_on = None def upgrade(): op.create_table( - 'user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.Unicode(length=50), nullable=False), - sa.Column('password_hash', sa.Unicode(length=64), nullable=False), - sa.Column('password_salt', sa.Unicode(length=32), nullable=True), - sa.Column('email', sa.Unicode(length=64), nullable=True), - sa.Column('rank', sa.Unicode(length=32), nullable=False), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('last_login_time', sa.DateTime()), - sa.Column('avatar_style', sa.Unicode(length=32), nullable=False), - sa.PrimaryKeyConstraint('id')) - op.create_unique_constraint('uq_user_name', 'user', ['name']) + "user", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.Unicode(length=50), nullable=False), + sa.Column("password_hash", sa.Unicode(length=64), nullable=False), + sa.Column("password_salt", sa.Unicode(length=32), nullable=True), + sa.Column("email", sa.Unicode(length=64), nullable=True), + sa.Column("rank", sa.Unicode(length=32), nullable=False), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("last_login_time", sa.DateTime()), + sa.Column("avatar_style", sa.Unicode(length=32), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_unique_constraint("uq_user_name", "user", ["name"]) def downgrade(): - op.drop_table('user') + op.drop_table("user") diff --git a/server/szurubooru/migrations/versions/ed6dd16a30f3_delete_post_columns.py b/server/szurubooru/migrations/versions/ed6dd16a30f3_delete_post_columns.py index dffc6c0..3b36c6a 100644 --- a/server/szurubooru/migrations/versions/ed6dd16a30f3_delete_post_columns.py +++ b/server/szurubooru/migrations/versions/ed6dd16a30f3_delete_post_columns.py @@ -1,48 +1,49 @@ -''' +""" Delete post columns Revision ID: ed6dd16a30f3 Created at: 2016-04-24 16:29:25.309154 -''' +""" import sqlalchemy as sa from alembic import op -revision = 'ed6dd16a30f3' -down_revision = '46df355634dc' +revision = "ed6dd16a30f3" +down_revision = "46df355634dc" branch_labels = None depends_on = None def upgrade(): for column_name in [ - 'auto_comment_edit_time', - 'auto_fav_count', - 'auto_comment_creation_time', - 'auto_feature_count', - 'auto_comment_count', - 'auto_score', - 'auto_fav_time', - 'auto_feature_time', - 'auto_note_count']: - op.drop_column('post', column_name) + "auto_comment_edit_time", + "auto_fav_count", + "auto_comment_creation_time", + "auto_feature_count", + "auto_comment_count", + "auto_score", + "auto_fav_time", + "auto_feature_time", + "auto_note_count", + ]: + op.drop_column("post", column_name) def downgrade(): for column_name in [ - 'auto_note_count', - 'auto_feature_time', - 'auto_fav_time', - 'auto_score', - 'auto_comment_count', - 'auto_feature_count', - 'auto_comment_creation_time', - 'auto_fav_count', - 'auto_comment_edit_time']: + "auto_note_count", + "auto_feature_time", + "auto_fav_time", + "auto_score", + "auto_comment_count", + "auto_feature_count", + "auto_comment_creation_time", + "auto_fav_count", + "auto_comment_edit_time", + ]: op.add_column( - 'post', + "post", sa.Column( - column_name, - sa.INTEGER(), - autoincrement=False, - nullable=False)) + column_name, sa.INTEGER(), autoincrement=False, nullable=False + ), + ) diff --git a/server/szurubooru/model/__init__.py b/server/szurubooru/model/__init__.py index 4f6cb2a..21a178e 100644 --- a/server/szurubooru/model/__init__.py +++ b/server/szurubooru/model/__init__.py @@ -1,18 +1,19 @@ +import szurubooru.model.util from szurubooru.model.base import Base -from szurubooru.model.user import User, UserToken -from szurubooru.model.tag_category import TagCategory -from szurubooru.model.tag import Tag, TagName, TagSuggestion, TagImplication +from szurubooru.model.comment import Comment, CommentScore +from szurubooru.model.pool import Pool, PoolName, PoolPost +from szurubooru.model.pool_category import PoolCategory from szurubooru.model.post import ( Post, - PostTag, - PostRelation, PostFavorite, - PostScore, - PostNote, PostFeature, - PostSignature) -from szurubooru.model.pool import Pool, PoolName, PoolPost -from szurubooru.model.pool_category import PoolCategory -from szurubooru.model.comment import Comment, CommentScore + PostNote, + PostRelation, + PostScore, + PostSignature, + PostTag, +) from szurubooru.model.snapshot import Snapshot -import szurubooru.model.util +from szurubooru.model.tag import Tag, TagImplication, TagName, TagSuggestion +from szurubooru.model.tag_category import TagCategory +from szurubooru.model.user import User, UserToken diff --git a/server/szurubooru/model/base.py b/server/szurubooru/model/base.py index 00ea8e1..860e542 100644 --- a/server/szurubooru/model/base.py +++ b/server/szurubooru/model/base.py @@ -1,4 +1,3 @@ from sqlalchemy.ext.declarative import declarative_base - Base = declarative_base() diff --git a/server/szurubooru/model/comment.py b/server/szurubooru/model/comment.py index 17b76a0..e64961e 100644 --- a/server/szurubooru/model/comment.py +++ b/server/szurubooru/model/comment.py @@ -1,58 +1,65 @@ import sqlalchemy as sa + from szurubooru.db import get_session from szurubooru.model.base import Base class CommentScore(Base): - __tablename__ = 'comment_score' + __tablename__ = "comment_score" comment_id = sa.Column( - 'comment_id', + "comment_id", sa.Integer, - sa.ForeignKey('comment.id'), + sa.ForeignKey("comment.id"), nullable=False, - primary_key=True) + primary_key=True, + ) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id'), + sa.ForeignKey("user.id"), nullable=False, primary_key=True, - index=True) - time = sa.Column('time', sa.DateTime, nullable=False) - score = sa.Column('score', sa.Integer, nullable=False) + index=True, + ) + time = sa.Column("time", sa.DateTime, nullable=False) + score = sa.Column("score", sa.Integer, nullable=False) - comment = sa.orm.relationship('Comment') + comment = sa.orm.relationship("Comment") user = sa.orm.relationship( - 'User', - backref=sa.orm.backref('comment_scores', cascade='all, delete-orphan')) + "User", + backref=sa.orm.backref("comment_scores", cascade="all, delete-orphan"), + ) class Comment(Base): - __tablename__ = 'comment' + __tablename__ = "comment" - comment_id = sa.Column('id', sa.Integer, primary_key=True) + comment_id = sa.Column("id", sa.Integer, primary_key=True) post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), nullable=False, - index=True) + index=True, + ) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id'), + sa.ForeignKey("user.id"), nullable=True, - index=True) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - last_edit_time = sa.Column('last_edit_time', sa.DateTime) - text = sa.Column('text', sa.UnicodeText, default=None) + index=True, + ) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_edit_time = sa.Column("last_edit_time", sa.DateTime) + text = sa.Column("text", sa.UnicodeText, default=None) - user = sa.orm.relationship('User') - post = sa.orm.relationship('Post') + user = sa.orm.relationship("User") + post = sa.orm.relationship("Post") scores = sa.orm.relationship( - 'CommentScore', cascade='all, delete-orphan', lazy='joined') + "CommentScore", cascade="all, delete-orphan", lazy="joined" + ) @property def score(self) -> int: @@ -60,9 +67,11 @@ class Comment(Base): get_session() .query(sa.sql.expression.func.sum(CommentScore.score)) .filter(CommentScore.comment_id == self.comment_id) - .one()[0] or 0) + .one()[0] + or 0 + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } diff --git a/server/szurubooru/model/pool.py b/server/szurubooru/model/pool.py index 6fc8361..3dcdd35 100644 --- a/server/szurubooru/model/pool.py +++ b/server/szurubooru/model/pool.py @@ -1,21 +1,23 @@ import sqlalchemy as sa -from sqlalchemy.ext.orderinglist import ordering_list from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.ext.orderinglist import ordering_list + from szurubooru.model.base import Base class PoolName(Base): - __tablename__ = 'pool_name' + __tablename__ = "pool_name" - pool_name_id = sa.Column('pool_name_id', sa.Integer, primary_key=True) + pool_name_id = sa.Column("pool_name_id", sa.Integer, primary_key=True) pool_id = sa.Column( - 'pool_id', + "pool_id", sa.Integer, - sa.ForeignKey('pool.id'), + sa.ForeignKey("pool.id"), nullable=False, - index=True) - name = sa.Column('name', sa.Unicode(128), nullable=False, unique=True) - order = sa.Column('ord', sa.Integer, nullable=False, index=True) + index=True, + ) + name = sa.Column("name", sa.Unicode(128), nullable=False, unique=True) + order = sa.Column("ord", sa.Integer, nullable=False, index=True) def __init__(self, name: str, order: int) -> None: self.name = name @@ -23,69 +25,76 @@ class PoolName(Base): class PoolPost(Base): - __tablename__ = 'pool_post' + __tablename__ = "pool_post" pool_id = sa.Column( - 'pool_id', + "pool_id", sa.Integer, - sa.ForeignKey('pool.id'), + sa.ForeignKey("pool.id"), nullable=False, primary_key=True, - index=True) + index=True, + ) post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), nullable=False, primary_key=True, - index=True) - order = sa.Column('ord', sa.Integer, nullable=False, index=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', back_populates='_pools') + pool = sa.orm.relationship("Pool", back_populates="_posts") + post = sa.orm.relationship("Post", back_populates="_pools") def __init__(self, post) -> None: self.post_id = post.post_id class Pool(Base): - __tablename__ = 'pool' + __tablename__ = "pool" - pool_id = sa.Column('id', sa.Integer, primary_key=True) + pool_id = sa.Column("id", sa.Integer, primary_key=True) category_id = sa.Column( - 'category_id', + "category_id", sa.Integer, - sa.ForeignKey('pool_category.id'), + sa.ForeignKey("pool_category.id"), nullable=False, - index=True) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - last_edit_time = sa.Column('last_edit_time', sa.DateTime) - description = sa.Column('description', sa.UnicodeText, default=None) - - category = sa.orm.relationship('PoolCategory', lazy='joined') + index=True, + ) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_edit_time = sa.Column("last_edit_time", sa.DateTime) + description = sa.Column("description", sa.UnicodeText, default=None) + + category = sa.orm.relationship("PoolCategory", lazy="joined") names = sa.orm.relationship( - 'PoolName', - cascade='all,delete-orphan', - lazy='joined', - order_by='PoolName.order') + "PoolName", + cascade="all,delete-orphan", + lazy="joined", + order_by="PoolName.order", + ) _posts = sa.orm.relationship( - 'PoolPost', - cascade='all,delete-orphan', - lazy='joined', - back_populates='pool', - order_by='PoolPost.order', - collection_class=ordering_list('order')) - posts = association_proxy('_posts', 'post') + "PoolPost", + cascade="all,delete-orphan", + lazy="joined", + back_populates="pool", + 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(PoolPost.post_id)]) + [sa.sql.expression.func.count(PoolPost.post_id)] + ) .where(PoolPost.pool_id == pool_id) .as_scalar() ), - deferred=True) + deferred=True, + ) first_name = sa.orm.column_property( ( @@ -95,9 +104,10 @@ class Pool(Base): .limit(1) .as_scalar() ), - deferred=True) + deferred=True, + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } diff --git a/server/szurubooru/model/pool_category.py b/server/szurubooru/model/pool_category.py index a54e5a6..f527d2d 100644 --- a/server/szurubooru/model/pool_category.py +++ b/server/szurubooru/model/pool_category.py @@ -1,29 +1,34 @@ from typing import Optional + import sqlalchemy as sa + from szurubooru.model.base import Base from szurubooru.model.pool import Pool class PoolCategory(Base): - __tablename__ = 'pool_category' + __tablename__ = "pool_category" - pool_category_id = sa.Column('id', sa.Integer, primary_key=True) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - name = sa.Column('name', sa.Unicode(32), nullable=False) + pool_category_id = sa.Column("id", sa.Integer, primary_key=True) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + name = sa.Column("name", sa.Unicode(32), nullable=False) color = sa.Column( - 'color', sa.Unicode(32), nullable=False, default='#000000') - default = sa.Column('default', sa.Boolean, nullable=False, default=False) + "color", sa.Unicode(32), nullable=False, default="#000000" + ) + default = sa.Column("default", sa.Boolean, nullable=False, default=False) def __init__(self, name: Optional[str] = None) -> None: self.name = name pool_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count('Pool.pool_id')]) + [sa.sql.expression.func.count("Pool.pool_id")] + ) .where(Pool.category_id == pool_category_id) - .correlate_except(sa.table('Pool'))) + .correlate_except(sa.table("Pool")) + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } diff --git a/server/szurubooru/model/post.py b/server/szurubooru/model/post.py index 11da866..9e06a74 100644 --- a/server/szurubooru/model/post.py +++ b/server/szurubooru/model/post.py @@ -1,122 +1,135 @@ from typing import List + import sqlalchemy as sa -from szurubooru.model.base import Base -from szurubooru.model.comment import Comment -from szurubooru.model.pool import PoolPost from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.orderinglist import ordering_list +from szurubooru.model.base import Base +from szurubooru.model.comment import Comment +from szurubooru.model.pool import PoolPost + class PostFeature(Base): - __tablename__ = 'post_feature' + __tablename__ = "post_feature" - post_feature_id = sa.Column('id', sa.Integer, primary_key=True) + post_feature_id = sa.Column("id", sa.Integer, primary_key=True) post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), nullable=False, - index=True) + index=True, + ) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id'), + sa.ForeignKey("user.id"), nullable=False, - index=True) - time = sa.Column('time', sa.DateTime, nullable=False) + index=True, + ) + time = sa.Column("time", sa.DateTime, nullable=False) - post = sa.orm.relationship('Post') # type: Post + post = sa.orm.relationship("Post") # type: Post user = sa.orm.relationship( - 'User', - backref=sa.orm.backref( - 'post_features', cascade='all, delete-orphan')) + "User", + backref=sa.orm.backref("post_features", cascade="all, delete-orphan"), + ) class PostScore(Base): - __tablename__ = 'post_score' + __tablename__ = "post_score" post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), primary_key=True, nullable=False, - index=True) + index=True, + ) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id'), + sa.ForeignKey("user.id"), primary_key=True, nullable=False, - index=True) - time = sa.Column('time', sa.DateTime, nullable=False) - score = sa.Column('score', sa.Integer, nullable=False) + index=True, + ) + time = sa.Column("time", sa.DateTime, nullable=False) + score = sa.Column("score", sa.Integer, nullable=False) - post = sa.orm.relationship('Post') + post = sa.orm.relationship("Post") user = sa.orm.relationship( - 'User', - backref=sa.orm.backref('post_scores', cascade='all, delete-orphan')) + "User", + backref=sa.orm.backref("post_scores", cascade="all, delete-orphan"), + ) class PostFavorite(Base): - __tablename__ = 'post_favorite' + __tablename__ = "post_favorite" post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), primary_key=True, nullable=False, - index=True) + index=True, + ) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id'), + sa.ForeignKey("user.id"), primary_key=True, nullable=False, - index=True) - time = sa.Column('time', sa.DateTime, nullable=False) + index=True, + ) + time = sa.Column("time", sa.DateTime, nullable=False) - post = sa.orm.relationship('Post') + post = sa.orm.relationship("Post") user = sa.orm.relationship( - 'User', - backref=sa.orm.backref('post_favorites', cascade='all, delete-orphan')) + "User", + backref=sa.orm.backref("post_favorites", cascade="all, delete-orphan"), + ) class PostNote(Base): - __tablename__ = 'post_note' + __tablename__ = "post_note" - post_note_id = sa.Column('id', sa.Integer, primary_key=True) + post_note_id = sa.Column("id", sa.Integer, primary_key=True) post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), nullable=False, - index=True) - polygon = sa.Column('polygon', sa.PickleType, nullable=False) - text = sa.Column('text', sa.UnicodeText, nullable=False) + index=True, + ) + polygon = sa.Column("polygon", sa.PickleType, nullable=False) + text = sa.Column("text", sa.UnicodeText, nullable=False) - post = sa.orm.relationship('Post') + post = sa.orm.relationship("Post") class PostRelation(Base): - __tablename__ = 'post_relation' + __tablename__ = "post_relation" parent_id = sa.Column( - 'parent_id', + "parent_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), primary_key=True, nullable=False, - index=True) + index=True, + ) child_id = sa.Column( - 'child_id', + "child_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), primary_key=True, nullable=False, - index=True) + index=True, + ) def __init__(self, parent_id: int, child_id: int) -> None: self.parent_id = parent_id @@ -124,22 +137,24 @@ class PostRelation(Base): class PostTag(Base): - __tablename__ = 'post_tag' + __tablename__ = "post_tag" post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), primary_key=True, nullable=False, - index=True) + index=True, + ) tag_id = sa.Column( - 'tag_id', + "tag_id", sa.Integer, - sa.ForeignKey('tag.id'), + sa.ForeignKey("tag.id"), primary_key=True, nullable=False, - index=True) + index=True, + ) def __init__(self, post_id: int, tag_id: int) -> None: self.post_id = post_id @@ -147,105 +162,119 @@ class PostTag(Base): class PostSignature(Base): - __tablename__ = 'post_signature' + __tablename__ = "post_signature" post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), primary_key=True, nullable=False, - index=True) - signature = sa.Column('signature', sa.LargeBinary, nullable=False) + index=True, + ) + signature = sa.Column("signature", sa.LargeBinary, nullable=False) words = sa.Column( - 'words', + "words", sa.dialects.postgresql.ARRAY(sa.Integer, dimensions=1), nullable=False, - index=True) + index=True, + ) - post = sa.orm.relationship('Post') + post = sa.orm.relationship("Post") class Post(Base): - __tablename__ = 'post' + __tablename__ = "post" - SAFETY_SAFE = 'safe' - SAFETY_SKETCHY = 'sketchy' - SAFETY_UNSAFE = 'unsafe' + SAFETY_SAFE = "safe" + SAFETY_SKETCHY = "sketchy" + SAFETY_UNSAFE = "unsafe" - TYPE_IMAGE = 'image' - TYPE_ANIMATION = 'animation' - TYPE_VIDEO = 'video' - TYPE_FLASH = 'flash' + TYPE_IMAGE = "image" + TYPE_ANIMATION = "animation" + TYPE_VIDEO = "video" + TYPE_FLASH = "flash" - FLAG_LOOP = 'loop' - FLAG_SOUND = 'sound' + FLAG_LOOP = "loop" + FLAG_SOUND = "sound" # basic meta - post_id = sa.Column('id', sa.Integer, primary_key=True) + post_id = sa.Column("id", sa.Integer, primary_key=True) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id', ondelete='SET NULL'), + sa.ForeignKey("user.id", ondelete="SET NULL"), nullable=True, - index=True) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - last_edit_time = sa.Column('last_edit_time', sa.DateTime) - safety = sa.Column('safety', sa.Unicode(32), nullable=False) - source = sa.Column('source', sa.Unicode(2048)) - flags_string = sa.Column('flags', sa.Unicode(32), default='') + index=True, + ) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_edit_time = sa.Column("last_edit_time", sa.DateTime) + safety = sa.Column("safety", sa.Unicode(32), nullable=False) + source = sa.Column("source", sa.Unicode(2048)) + flags_string = sa.Column("flags", sa.Unicode(32), default="") # content description - type = sa.Column('type', sa.Unicode(32), nullable=False) - checksum = sa.Column('checksum', sa.Unicode(64), nullable=False) - file_size = sa.Column('file_size', sa.Integer) - canvas_width = sa.Column('image_width', sa.Integer) - canvas_height = sa.Column('image_height', sa.Integer) - mime_type = sa.Column('mime-type', sa.Unicode(32), nullable=False) + type = sa.Column("type", sa.Unicode(32), nullable=False) + checksum = sa.Column("checksum", sa.Unicode(64), nullable=False) + file_size = sa.Column("file_size", sa.Integer) + canvas_width = sa.Column("image_width", sa.Integer) + canvas_height = sa.Column("image_height", sa.Integer) + mime_type = sa.Column("mime-type", sa.Unicode(32), nullable=False) # foreign tables - user = sa.orm.relationship('User') - tags = sa.orm.relationship('Tag', backref='posts', secondary='post_tag') + user = sa.orm.relationship("User") + tags = sa.orm.relationship("Tag", backref="posts", secondary="post_tag") signature = sa.orm.relationship( - 'PostSignature', + "PostSignature", uselist=False, - cascade='all, delete, delete-orphan', - lazy='joined') + cascade="all, delete, delete-orphan", + lazy="joined", + ) relations = sa.orm.relationship( - 'Post', - secondary='post_relation', + "Post", + secondary="post_relation", primaryjoin=post_id == PostRelation.parent_id, - secondaryjoin=post_id == PostRelation.child_id, lazy='joined', - backref='related_by') + secondaryjoin=post_id == PostRelation.child_id, + lazy="joined", + backref="related_by", + ) features = sa.orm.relationship( - 'PostFeature', cascade='all, delete-orphan', lazy='joined') + "PostFeature", cascade="all, delete-orphan", lazy="joined" + ) scores = sa.orm.relationship( - 'PostScore', cascade='all, delete-orphan', lazy='joined') + "PostScore", cascade="all, delete-orphan", lazy="joined" + ) favorited_by = sa.orm.relationship( - 'PostFavorite', cascade='all, delete-orphan', lazy='joined') + "PostFavorite", cascade="all, delete-orphan", lazy="joined" + ) notes = sa.orm.relationship( - 'PostNote', cascade='all, delete-orphan', lazy='joined') - comments = sa.orm.relationship('Comment', cascade='all, delete-orphan') + "PostNote", cascade="all, delete-orphan", lazy="joined" + ) + comments = sa.orm.relationship("Comment", cascade="all, delete-orphan") _pools = sa.orm.relationship( - 'PoolPost', - cascade='all,delete-orphan', - lazy='select', - order_by='PoolPost.order', - back_populates='post') - pools = association_proxy('_pools', 'pool') + "PoolPost", + cascade="all,delete-orphan", + lazy="select", + order_by="PoolPost.order", + back_populates="post", + ) + pools = association_proxy("_pools", "pool") # dynamic columns tag_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(PostTag.tag_id)]) + [sa.sql.expression.func.count(PostTag.tag_id)] + ) .where(PostTag.post_id == post_id) - .correlate_except(PostTag)) + .correlate_except(PostTag) + ) canvas_area = sa.orm.column_property(canvas_width * canvas_height) canvas_aspect_ratio = sa.orm.column_property( - sa.sql.expression.func.cast(canvas_width, sa.Float) / - sa.sql.expression.func.cast(canvas_height, sa.Float)) + sa.sql.expression.func.cast(canvas_width, sa.Float) + / sa.sql.expression.func.cast(canvas_height, sa.Float) + ) @property def is_featured(self) -> bool: @@ -253,81 +282,106 @@ class Post(Base): sa.orm.object_session(self) .query(PostFeature) .order_by(PostFeature.time.desc()) - .first()) + .first() + ) return featured_post and featured_post.post_id == self.post_id @hybrid_property def flags(self) -> List[str]: - return sorted([x for x in self.flags_string.split(',') if x]) + return sorted([x for x in self.flags_string.split(",") if x]) @flags.setter def flags(self, data: List[str]) -> None: - self.flags_string = ','.join([x for x in data if x]) + self.flags_string = ",".join([x for x in data if x]) score = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.coalesce( - sa.sql.expression.func.sum(PostScore.score), 0)]) + [ + sa.sql.expression.func.coalesce( + sa.sql.expression.func.sum(PostScore.score), 0 + ) + ] + ) .where(PostScore.post_id == post_id) - .correlate_except(PostScore)) + .correlate_except(PostScore) + ) favorite_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(PostFavorite.post_id)]) + [sa.sql.expression.func.count(PostFavorite.post_id)] + ) .where(PostFavorite.post_id == post_id) - .correlate_except(PostFavorite)) + .correlate_except(PostFavorite) + ) last_favorite_time = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.max(PostFavorite.time)]) + [sa.sql.expression.func.max(PostFavorite.time)] + ) .where(PostFavorite.post_id == post_id) - .correlate_except(PostFavorite)) + .correlate_except(PostFavorite) + ) feature_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(PostFeature.post_id)]) + [sa.sql.expression.func.count(PostFeature.post_id)] + ) .where(PostFeature.post_id == post_id) - .correlate_except(PostFeature)) + .correlate_except(PostFeature) + ) last_feature_time = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.max(PostFeature.time)]) + [sa.sql.expression.func.max(PostFeature.time)] + ) .where(PostFeature.post_id == post_id) - .correlate_except(PostFeature)) + .correlate_except(PostFeature) + ) comment_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(Comment.post_id)]) + [sa.sql.expression.func.count(Comment.post_id)] + ) .where(Comment.post_id == post_id) - .correlate_except(Comment)) + .correlate_except(Comment) + ) last_comment_creation_time = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.max(Comment.creation_time)]) + [sa.sql.expression.func.max(Comment.creation_time)] + ) .where(Comment.post_id == post_id) - .correlate_except(Comment)) + .correlate_except(Comment) + ) last_comment_edit_time = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.max(Comment.last_edit_time)]) + [sa.sql.expression.func.max(Comment.last_edit_time)] + ) .where(Comment.post_id == post_id) - .correlate_except(Comment)) + .correlate_except(Comment) + ) note_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(PostNote.post_id)]) + [sa.sql.expression.func.count(PostNote.post_id)] + ) .where(PostNote.post_id == post_id) - .correlate_except(PostNote)) + .correlate_except(PostNote) + ) relation_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(PostRelation.child_id)]) + [sa.sql.expression.func.count(PostRelation.child_id)] + ) .where( - (PostRelation.parent_id == post_id) | - (PostRelation.child_id == post_id)) - .correlate_except(PostRelation)) + (PostRelation.parent_id == post_id) + | (PostRelation.child_id == post_id) + ) + .correlate_except(PostRelation) + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } diff --git a/server/szurubooru/model/snapshot.py b/server/szurubooru/model/snapshot.py index 7ee3ba6..d4e3fc8 100644 --- a/server/szurubooru/model/snapshot.py +++ b/server/szurubooru/model/snapshot.py @@ -1,29 +1,32 @@ import sqlalchemy as sa + from szurubooru.model.base import Base class Snapshot(Base): - __tablename__ = 'snapshot' + __tablename__ = "snapshot" - OPERATION_CREATED = 'created' - OPERATION_MODIFIED = 'modified' - OPERATION_DELETED = 'deleted' - OPERATION_MERGED = 'merged' + OPERATION_CREATED = "created" + OPERATION_MODIFIED = "modified" + OPERATION_DELETED = "deleted" + OPERATION_MERGED = "merged" - snapshot_id = sa.Column('id', sa.Integer, primary_key=True) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - operation = sa.Column('operation', sa.Unicode(16), nullable=False) + snapshot_id = sa.Column("id", sa.Integer, primary_key=True) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + operation = sa.Column("operation", sa.Unicode(16), nullable=False) resource_type = sa.Column( - 'resource_type', sa.Unicode(32), nullable=False, index=True) + "resource_type", sa.Unicode(32), nullable=False, index=True + ) resource_pkey = sa.Column( - 'resource_pkey', sa.Integer, nullable=False, index=True) - resource_name = sa.Column( - 'resource_name', sa.Unicode(128), nullable=False) + "resource_pkey", sa.Integer, nullable=False, index=True + ) + resource_name = sa.Column("resource_name", sa.Unicode(128), nullable=False) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id', ondelete='set null'), - nullable=True) - data = sa.Column('data', sa.PickleType) + sa.ForeignKey("user.id", ondelete="set null"), + nullable=True, + ) + data = sa.Column("data", sa.PickleType) - user = sa.orm.relationship('User') + user = sa.orm.relationship("User") diff --git a/server/szurubooru/model/tag.py b/server/szurubooru/model/tag.py index 2af87e7..61dbf83 100644 --- a/server/szurubooru/model/tag.py +++ b/server/szurubooru/model/tag.py @@ -1,25 +1,28 @@ import sqlalchemy as sa + from szurubooru.model.base import Base from szurubooru.model.post import PostTag class TagSuggestion(Base): - __tablename__ = 'tag_suggestion' + __tablename__ = "tag_suggestion" parent_id = sa.Column( - 'parent_id', + "parent_id", sa.Integer, - sa.ForeignKey('tag.id'), + sa.ForeignKey("tag.id"), nullable=False, primary_key=True, - index=True) + index=True, + ) child_id = sa.Column( - 'child_id', + "child_id", sa.Integer, - sa.ForeignKey('tag.id'), + sa.ForeignKey("tag.id"), nullable=False, primary_key=True, - index=True) + index=True, + ) def __init__(self, parent_id: int, child_id: int) -> None: self.parent_id = parent_id @@ -27,22 +30,24 @@ class TagSuggestion(Base): class TagImplication(Base): - __tablename__ = 'tag_implication' + __tablename__ = "tag_implication" parent_id = sa.Column( - 'parent_id', + "parent_id", sa.Integer, - sa.ForeignKey('tag.id'), + sa.ForeignKey("tag.id"), nullable=False, primary_key=True, - index=True) + index=True, + ) child_id = sa.Column( - 'child_id', + "child_id", sa.Integer, - sa.ForeignKey('tag.id'), + sa.ForeignKey("tag.id"), nullable=False, primary_key=True, - index=True) + index=True, + ) def __init__(self, parent_id: int, child_id: int) -> None: self.parent_id = parent_id @@ -50,17 +55,18 @@ class TagImplication(Base): class TagName(Base): - __tablename__ = 'tag_name' + __tablename__ = "tag_name" - tag_name_id = sa.Column('tag_name_id', sa.Integer, primary_key=True) + tag_name_id = sa.Column("tag_name_id", sa.Integer, primary_key=True) tag_id = sa.Column( - 'tag_id', + "tag_id", sa.Integer, - sa.ForeignKey('tag.id'), + sa.ForeignKey("tag.id"), nullable=False, - index=True) - name = sa.Column('name', sa.Unicode(128), nullable=False, unique=True) - order = sa.Column('ord', sa.Integer, nullable=False, index=True) + index=True, + ) + name = sa.Column("name", sa.Unicode(128), nullable=False, unique=True) + order = sa.Column("ord", sa.Integer, nullable=False, index=True) def __init__(self, name: str, order: int) -> None: self.name = name @@ -68,44 +74,50 @@ class TagName(Base): class Tag(Base): - __tablename__ = 'tag' + __tablename__ = "tag" - tag_id = sa.Column('id', sa.Integer, primary_key=True) + tag_id = sa.Column("id", sa.Integer, primary_key=True) category_id = sa.Column( - 'category_id', + "category_id", sa.Integer, - sa.ForeignKey('tag_category.id'), + sa.ForeignKey("tag_category.id"), nullable=False, - index=True) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - last_edit_time = sa.Column('last_edit_time', sa.DateTime) - description = sa.Column('description', sa.UnicodeText, default=None) - - category = sa.orm.relationship('TagCategory', lazy='joined') + index=True, + ) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_edit_time = sa.Column("last_edit_time", sa.DateTime) + description = sa.Column("description", sa.UnicodeText, default=None) + + category = sa.orm.relationship("TagCategory", lazy="joined") names = sa.orm.relationship( - 'TagName', - cascade='all,delete-orphan', - lazy='joined', - order_by='TagName.order') + "TagName", + cascade="all,delete-orphan", + lazy="joined", + order_by="TagName.order", + ) suggestions = sa.orm.relationship( - 'Tag', - secondary='tag_suggestion', + "Tag", + secondary="tag_suggestion", primaryjoin=tag_id == TagSuggestion.parent_id, secondaryjoin=tag_id == TagSuggestion.child_id, - lazy='joined') + lazy="joined", + ) implications = sa.orm.relationship( - 'Tag', - secondary='tag_implication', + "Tag", + secondary="tag_implication", primaryjoin=tag_id == TagImplication.parent_id, secondaryjoin=tag_id == TagImplication.child_id, - lazy='joined') + lazy="joined", + ) post_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(PostTag.post_id)]) + [sa.sql.expression.func.count(PostTag.post_id)] + ) .where(PostTag.tag_id == tag_id) - .correlate_except(PostTag)) + .correlate_except(PostTag) + ) first_name = sa.orm.column_property( ( @@ -115,27 +127,32 @@ class Tag(Base): .limit(1) .as_scalar() ), - deferred=True) + deferred=True, + ) suggestion_count = sa.orm.column_property( ( sa.sql.expression.select( - [sa.sql.expression.func.count(TagSuggestion.child_id)]) + [sa.sql.expression.func.count(TagSuggestion.child_id)] + ) .where(TagSuggestion.parent_id == tag_id) .as_scalar() ), - deferred=True) + deferred=True, + ) implication_count = sa.orm.column_property( ( sa.sql.expression.select( - [sa.sql.expression.func.count(TagImplication.child_id)]) + [sa.sql.expression.func.count(TagImplication.child_id)] + ) .where(TagImplication.parent_id == tag_id) .as_scalar() ), - deferred=True) + deferred=True, + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } diff --git a/server/szurubooru/model/tag_category.py b/server/szurubooru/model/tag_category.py index 2c961ed..1faf74c 100644 --- a/server/szurubooru/model/tag_category.py +++ b/server/szurubooru/model/tag_category.py @@ -1,28 +1,32 @@ from typing import Optional + import sqlalchemy as sa + from szurubooru.model.base import Base from szurubooru.model.tag import Tag class TagCategory(Base): - __tablename__ = 'tag_category' + __tablename__ = "tag_category" - tag_category_id = sa.Column('id', sa.Integer, primary_key=True) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - name = sa.Column('name', sa.Unicode(32), nullable=False) + tag_category_id = sa.Column("id", sa.Integer, primary_key=True) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + name = sa.Column("name", sa.Unicode(32), nullable=False) color = sa.Column( - 'color', sa.Unicode(32), nullable=False, default='#000000') - default = sa.Column('default', sa.Boolean, nullable=False, default=False) + "color", sa.Unicode(32), nullable=False, default="#000000" + ) + default = sa.Column("default", sa.Boolean, nullable=False, default=False) def __init__(self, name: Optional[str] = None) -> None: self.name = name tag_count = sa.orm.column_property( - sa.sql.expression.select([sa.sql.expression.func.count('Tag.tag_id')]) + sa.sql.expression.select([sa.sql.expression.func.count("Tag.tag_id")]) .where(Tag.category_id == tag_category_id) - .correlate_except(sa.table('Tag'))) + .correlate_except(sa.table("Tag")) + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } diff --git a/server/szurubooru/model/user.py b/server/szurubooru/model/user.py index 2d599e8..41a9b30 100644 --- a/server/szurubooru/model/user.py +++ b/server/szurubooru/model/user.py @@ -1,110 +1,123 @@ import sqlalchemy as sa + from szurubooru.model.base import Base -from szurubooru.model.post import Post, PostScore, PostFavorite from szurubooru.model.comment import Comment +from szurubooru.model.post import Post, PostFavorite, PostScore class User(Base): - __tablename__ = 'user' - - AVATAR_GRAVATAR = 'gravatar' - AVATAR_MANUAL = 'manual' - - RANK_ANONYMOUS = 'anonymous' - RANK_RESTRICTED = 'restricted' - RANK_REGULAR = 'regular' - RANK_POWER = 'power' - RANK_MODERATOR = 'moderator' - RANK_ADMINISTRATOR = 'administrator' - RANK_NOBODY = 'nobody' # unattainable, used for privileges - - user_id = sa.Column('id', sa.Integer, primary_key=True) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - last_login_time = sa.Column('last_login_time', sa.DateTime) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - name = sa.Column('name', sa.Unicode(50), nullable=False, unique=True) - password_hash = sa.Column('password_hash', sa.Unicode(128), nullable=False) - password_salt = sa.Column('password_salt', sa.Unicode(32)) + __tablename__ = "user" + + AVATAR_GRAVATAR = "gravatar" + AVATAR_MANUAL = "manual" + + RANK_ANONYMOUS = "anonymous" + RANK_RESTRICTED = "restricted" + RANK_REGULAR = "regular" + RANK_POWER = "power" + RANK_MODERATOR = "moderator" + RANK_ADMINISTRATOR = "administrator" + RANK_NOBODY = "nobody" # unattainable, used for privileges + + user_id = sa.Column("id", sa.Integer, primary_key=True) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_login_time = sa.Column("last_login_time", sa.DateTime) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + name = sa.Column("name", sa.Unicode(50), nullable=False, unique=True) + password_hash = sa.Column("password_hash", sa.Unicode(128), nullable=False) + password_salt = sa.Column("password_salt", sa.Unicode(32)) password_revision = sa.Column( - 'password_revision', sa.SmallInteger, default=0, nullable=False) - email = sa.Column('email', sa.Unicode(64), nullable=True) - rank = sa.Column('rank', sa.Unicode(32), nullable=False) + "password_revision", sa.SmallInteger, default=0, nullable=False + ) + email = sa.Column("email", sa.Unicode(64), nullable=True) + rank = sa.Column("rank", sa.Unicode(32), nullable=False) avatar_style = sa.Column( - 'avatar_style', sa.Unicode(32), nullable=False, - default=AVATAR_GRAVATAR) + "avatar_style", sa.Unicode(32), nullable=False, default=AVATAR_GRAVATAR + ) - comments = sa.orm.relationship('Comment') + comments = sa.orm.relationship("Comment") @property def post_count(self) -> int: from szurubooru.db import session + return ( - session - .query(sa.sql.expression.func.sum(1)) + session.query(sa.sql.expression.func.sum(1)) .filter(Post.user_id == self.user_id) - .one()[0] or 0) + .one()[0] + or 0 + ) @property def comment_count(self) -> int: from szurubooru.db import session + return ( - session - .query(sa.sql.expression.func.sum(1)) + session.query(sa.sql.expression.func.sum(1)) .filter(Comment.user_id == self.user_id) - .one()[0] or 0) + .one()[0] + or 0 + ) @property def favorite_post_count(self) -> int: from szurubooru.db import session + return ( - session - .query(sa.sql.expression.func.sum(1)) + session.query(sa.sql.expression.func.sum(1)) .filter(PostFavorite.user_id == self.user_id) - .one()[0] or 0) + .one()[0] + or 0 + ) @property def liked_post_count(self) -> int: from szurubooru.db import session + return ( - session - .query(sa.sql.expression.func.sum(1)) + session.query(sa.sql.expression.func.sum(1)) .filter(PostScore.user_id == self.user_id) .filter(PostScore.score == 1) - .one()[0] or 0) + .one()[0] + or 0 + ) @property def disliked_post_count(self) -> int: from szurubooru.db import session + return ( - session - .query(sa.sql.expression.func.sum(1)) + session.query(sa.sql.expression.func.sum(1)) .filter(PostScore.user_id == self.user_id) .filter(PostScore.score == -1) - .one()[0] or 0) + .one()[0] + or 0 + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } class UserToken(Base): - __tablename__ = 'user_token' + __tablename__ = "user_token" - user_token_id = sa.Column('id', sa.Integer, primary_key=True) + user_token_id = sa.Column("id", sa.Integer, primary_key=True) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id', ondelete='CASCADE'), + sa.ForeignKey("user.id", ondelete="CASCADE"), nullable=False, - index=True) - token = sa.Column('token', sa.Unicode(36), nullable=False) - note = sa.Column('note', sa.Unicode(128), nullable=True) - enabled = sa.Column('enabled', sa.Boolean, nullable=False, default=True) - expiration_time = sa.Column('expiration_time', sa.DateTime, nullable=True) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - last_edit_time = sa.Column('last_edit_time', sa.DateTime) - last_usage_time = sa.Column('last_usage_time', sa.DateTime) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - - user = sa.orm.relationship('User') + index=True, + ) + token = sa.Column("token", sa.Unicode(36), nullable=False) + note = sa.Column("note", sa.Unicode(128), nullable=True) + enabled = sa.Column("enabled", sa.Boolean, nullable=False, default=True) + expiration_time = sa.Column("expiration_time", sa.DateTime, nullable=True) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_edit_time = sa.Column("last_edit_time", sa.DateTime) + last_usage_time = sa.Column("last_usage_time", sa.DateTime) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + + user = sa.orm.relationship("User") diff --git a/server/szurubooru/model/util.py b/server/szurubooru/model/util.py index ab86100..bece9e9 100644 --- a/server/szurubooru/model/util.py +++ b/server/szurubooru/model/util.py @@ -1,17 +1,19 @@ -from typing import Tuple, Any, Dict, Callable, Union, Optional +from typing import Any, Callable, Dict, Optional, Tuple, Union + import sqlalchemy as sa + from szurubooru.model.base import Base from szurubooru.model.user import User def get_resource_info(entity: Base) -> Tuple[Any, Any, Union[str, int]]: serializers = { - 'tag': lambda tag: tag.first_name, - 'tag_category': lambda category: category.name, - 'comment': lambda comment: comment.comment_id, - 'post': lambda post: post.post_id, - 'pool': lambda pool: pool.pool_id, - 'pool_category': lambda category: category.name, + "tag": lambda tag: tag.first_name, + "tag_category": lambda category: category.name, + "comment": lambda comment: comment.comment_id, + "post": lambda post: post.post_id, + "pool": lambda pool: pool.pool_id, + "pool_category": lambda category: category.name, } # type: Dict[str, Callable[[Base], Any]] resource_type = entity.__table__.name @@ -31,14 +33,15 @@ def get_resource_info(entity: Base) -> Tuple[Any, Any, Union[str, int]]: def get_aux_entity( - session: Any, - get_table_info: Callable[[Base], Tuple[Base, Callable[[Base], Any]]], - entity: Base, - user: User) -> Optional[Base]: + session: Any, + get_table_info: Callable[[Base], Tuple[Base, Callable[[Base], Any]]], + entity: Base, + user: User, +) -> Optional[Base]: table, get_column = get_table_info(entity) return ( - session - .query(table) + session.query(table) .filter(get_column(table) == get_column(entity)) .filter(table.user_id == user.user_id) - .one_or_none()) + .one_or_none() + ) diff --git a/server/szurubooru/rest/__init__.py b/server/szurubooru/rest/__init__.py index d6b3ef2..6db22e2 100644 --- a/server/szurubooru/rest/__init__.py +++ b/server/szurubooru/rest/__init__.py @@ -1,3 +1,3 @@ +import szurubooru.rest.routes from szurubooru.rest.app import application from szurubooru.rest.context import Context, Response -import szurubooru.rest.routes diff --git a/server/szurubooru/rest/app.py b/server/szurubooru/rest/app.py index 8c9efba..a6f10fb 100644 --- a/server/szurubooru/rest/app.py +++ b/server/szurubooru/rest/app.py @@ -1,20 +1,21 @@ -import urllib.parse import cgi import json import re -from typing import Dict, Any, Callable, Tuple +import urllib.parse from datetime import datetime +from typing import Any, Callable, Dict, Tuple + from szurubooru import db from szurubooru.func import util -from szurubooru.rest import errors, middleware, routes, context +from szurubooru.rest import context, errors, middleware, routes def _json_serializer(obj: Any) -> str: - ''' JSON serializer for objects not serializable by default JSON code ''' + """ JSON serializer for objects not serializable by default JSON code """ if isinstance(obj, datetime): - serial = obj.isoformat('T') + 'Z' + serial = obj.isoformat("T") + "Z" return serial - raise TypeError('Type not serializable') + raise TypeError("Type not serializable") def _dump_json(obj: Any) -> str: @@ -24,71 +25,75 @@ def _dump_json(obj: Any) -> str: def _get_headers(env: Dict[str, Any]) -> Dict[str, str]: headers = {} # type: Dict[str, str] for key, value in env.items(): - if key.startswith('HTTP_'): + if key.startswith("HTTP_"): key = util.snake_case_to_upper_train_case(key[5:]) headers[key] = value return headers def _create_context(env: Dict[str, Any]) -> context.Context: - method = env['REQUEST_METHOD'] - path = '/' + env['PATH_INFO'].lstrip('/') - path = path.encode('latin-1').decode('utf-8') # PEP-3333 + method = env["REQUEST_METHOD"] + path = "/" + env["PATH_INFO"].lstrip("/") + path = path.encode("latin-1").decode("utf-8") # PEP-3333 headers = _get_headers(env) files = {} - params = dict(urllib.parse.parse_qsl(env.get('QUERY_STRING', ''))) + params = dict(urllib.parse.parse_qsl(env.get("QUERY_STRING", ""))) - if 'multipart' in env.get('CONTENT_TYPE', ''): - form = cgi.FieldStorage(fp=env['wsgi.input'], environ=env) + if "multipart" in env.get("CONTENT_TYPE", ""): + form = cgi.FieldStorage(fp=env["wsgi.input"], environ=env) if not form.list: raise errors.HttpBadRequest( - 'ValidationError', 'No files attached.') - body = form.getvalue('metadata') + "ValidationError", "No files attached." + ) + body = form.getvalue("metadata") for key in form: files[key] = form.getvalue(key) else: - body = env['wsgi.input'].read() + body = env["wsgi.input"].read() if body: try: if isinstance(body, bytes): - body = body.decode('utf-8') + body = body.decode("utf-8") for key, value in json.loads(body).items(): params[key] = value except (ValueError, UnicodeDecodeError): raise errors.HttpBadRequest( - 'ValidationError', - 'Could not decode the request body. The JSON ' - 'was incorrect or was not encoded as UTF-8.') + "ValidationError", + "Could not decode the request body. The JSON " + "was incorrect or was not encoded as UTF-8.", + ) return context.Context(env, method, path, headers, params, files) def application( - env: Dict[str, Any], - start_response: Callable[[str, Any], Any]) -> Tuple[bytes]: + env: Dict[str, Any], start_response: Callable[[str, Any], Any] +) -> Tuple[bytes]: try: ctx = _create_context(env) - if 'application/json' not in ctx.get_header('Accept'): + if "application/json" not in ctx.get_header("Accept"): raise errors.HttpNotAcceptable( - 'ValidationError', - 'This API only supports JSON responses.') + "ValidationError", "This API only supports JSON responses." + ) for url, allowed_methods in routes.routes.items(): match = re.fullmatch(url, ctx.url) if match: if ctx.method not in allowed_methods: raise errors.HttpMethodNotAllowed( - 'ValidationError', - 'Allowed methods: %r' % allowed_methods) + "ValidationError", + "Allowed methods: %r" % allowed_methods, + ) handler = allowed_methods[ctx.method] break else: raise errors.HttpNotFound( - 'ValidationError', - 'Requested path ' + ctx.url + ' was not found.') + "ValidationError", + "Requested path " + ctx.url + " was not found.", + ) try: ctx.session = db.session() @@ -106,8 +111,8 @@ def application( finally: db.session.remove() - start_response('200', [('content-type', 'application/json')]) - return (_dump_json(response).encode('utf-8'),) + start_response("200", [("content-type", "application/json")]) + return (_dump_json(response).encode("utf-8"),) except Exception as ex: for exception_type, ex_handler in errors.error_handlers.items(): @@ -117,14 +122,15 @@ def application( except errors.BaseHttpError as ex: start_response( - '%d %s' % (ex.code, ex.reason), - [('content-type', 'application/json')]) + "%d %s" % (ex.code, ex.reason), + [("content-type", "application/json")], + ) blob = { - 'name': ex.name, - 'title': ex.title, - 'description': ex.description, + "name": ex.name, + "title": ex.title, + "description": ex.description, } if ex.extra_fields is not None: for key, value in ex.extra_fields.items(): blob[key] = value - return (_dump_json(blob).encode('utf-8'),) + return (_dump_json(blob).encode("utf-8"),) diff --git a/server/szurubooru/rest/context.py b/server/szurubooru/rest/context.py index 3e90243..40ba0bc 100644 --- a/server/szurubooru/rest/context.py +++ b/server/szurubooru/rest/context.py @@ -1,7 +1,7 @@ -from typing import Any, Union, List, Dict, Optional, cast -from szurubooru import model, errors -from szurubooru.func import net, file_uploads +from typing import Any, Dict, List, Optional, Union, cast +from szurubooru import errors, model +from szurubooru.func import file_uploads, net MISSING = object() Request = Dict[str, Any] @@ -10,13 +10,14 @@ Response = Optional[Dict[str, Any]] class Context: def __init__( - self, - env: Dict[str, Any], - method: str, - url: str, - headers: Dict[str, str] = None, - params: Request = None, - files: Dict[str, bytes] = None) -> None: + self, + env: Dict[str, Any], + method: str, + url: str, + headers: Dict[str, str] = None, + params: Request = None, + files: Dict[str, bytes] = None, + ) -> None: self.env = env self.method = method self.url = url @@ -26,7 +27,7 @@ class Context: self.user = model.User() self.user.name = None - self.user.rank = 'anonymous' + self.user.rank = "anonymous" self.session = None # type: Any @@ -34,100 +35,106 @@ class Context: return name in self._headers def get_header(self, name: str) -> str: - return self._headers.get(name, '') + return self._headers.get(name, "") def has_file(self, name: str, allow_tokens: bool = True) -> bool: return ( - name in self._files or - name + 'Url' in self._params or - (allow_tokens and name + 'Token' in self._params)) + name in self._files + or name + "Url" in self._params + or (allow_tokens and name + "Token" in self._params) + ) def get_file( - self, - name: str, - default: Union[object, bytes] = MISSING, - use_video_downloader: bool = False, - allow_tokens: bool = True) -> bytes: + self, + name: str, + default: Union[object, bytes] = MISSING, + use_video_downloader: bool = False, + allow_tokens: bool = True, + ) -> bytes: if name in self._files and self._files[name]: return self._files[name] - if name + 'Url' in self._params: + if name + "Url" in self._params: return net.download( - self._params[name + 'Url'], - use_video_downloader=use_video_downloader) + self._params[name + "Url"], + use_video_downloader=use_video_downloader, + ) - if allow_tokens and name + 'Token' in self._params: - ret = file_uploads.get(self._params[name + 'Token']) + if allow_tokens and name + "Token" in self._params: + ret = file_uploads.get(self._params[name + "Token"]) if ret: return ret elif default is not MISSING: raise errors.MissingOrExpiredRequiredFileError( - 'Required file %r is missing or has expired.' % name) + "Required file %r is missing or has expired." % name + ) if default is not MISSING: return cast(bytes, default) raise errors.MissingRequiredFileError( - 'Required file %r is missing.' % name) + "Required file %r is missing." % name + ) def has_param(self, name: str) -> bool: return name in self._params def get_param_as_list( - self, - name: str, - default: Union[object, List[Any]] = MISSING) -> List[Any]: + self, name: str, default: Union[object, List[Any]] = MISSING + ) -> List[Any]: if name not in self._params: if default is not MISSING: return cast(List[Any], default) raise errors.MissingRequiredParameterError( - 'Required parameter %r is missing.' % name) + "Required parameter %r is missing." % name + ) value = self._params[name] if type(value) is str: - if ',' in value: - return value.split(',') + if "," in value: + return value.split(",") return [value] if type(value) is list: return value raise errors.InvalidParameterError( - 'Parameter %r must be a list.' % name) + "Parameter %r must be a list." % name + ) def get_param_as_int_list( - self, - name: str, - default: Union[object, List[int]] = MISSING) -> List[int]: + self, name: str, default: Union[object, List[int]] = MISSING + ) -> List[int]: ret = self.get_param_as_list(name, default) for item in ret: if type(item) is not int: raise errors.InvalidParameterError( - 'Parameter %r must be a list of integer values.' % name) + "Parameter %r must be a list of integer values." % name + ) return ret def get_param_as_string_list( - self, - name: str, - default: Union[object, List[str]] = MISSING) -> List[str]: + self, name: str, default: Union[object, List[str]] = MISSING + ) -> List[str]: ret = self.get_param_as_list(name, default) for item in ret: if type(item) is not str: raise errors.InvalidParameterError( - 'Parameter %r must be a list of string values.' % name) + "Parameter %r must be a list of string values." % name + ) return ret def get_param_as_string( - self, - name: str, - default: Union[object, str] = MISSING) -> str: + self, name: str, default: Union[object, str] = MISSING + ) -> str: if name not in self._params: if default is not MISSING: return cast(str, default) raise errors.MissingRequiredParameterError( - 'Required parameter %r is missing.' % name) + "Required parameter %r is missing." % name + ) value = self._params[name] try: if value is None: - return '' + return "" if type(value) is list: - return ','.join(value) + return ",".join(value) if type(value) is int or type(value) is float: return str(value) if type(value) is str: @@ -135,51 +142,58 @@ class Context: except TypeError: pass raise errors.InvalidParameterError( - 'Parameter %r must be a string value.' % name) + "Parameter %r must be a string value." % name + ) def get_param_as_int( - self, - name: str, - default: Union[object, int] = MISSING, - min: Optional[int] = None, - max: Optional[int] = None) -> int: + self, + name: str, + default: Union[object, int] = MISSING, + min: Optional[int] = None, + max: Optional[int] = None, + ) -> int: if name not in self._params: if default is not MISSING: return cast(int, default) raise errors.MissingRequiredParameterError( - 'Required parameter %r is missing.' % name) + "Required parameter %r is missing." % name + ) value = self._params[name] try: value = int(value) if min is not None and value < min: raise errors.InvalidParameterError( - 'Parameter %r must be at least %r.' % (name, min)) + "Parameter %r must be at least %r." % (name, min) + ) if max is not None and value > max: raise errors.InvalidParameterError( - 'Parameter %r may not exceed %r.' % (name, max)) + "Parameter %r may not exceed %r." % (name, max) + ) return value except (ValueError, TypeError): pass raise errors.InvalidParameterError( - 'Parameter %r must be an integer value.' % name) + "Parameter %r must be an integer value." % name + ) def get_param_as_bool( - self, - name: str, - default: Union[object, bool] = MISSING) -> bool: + self, name: str, default: Union[object, bool] = MISSING + ) -> bool: if name not in self._params: if default is not MISSING: return cast(bool, default) raise errors.MissingRequiredParameterError( - 'Required parameter %r is missing.' % name) + "Required parameter %r is missing." % name + ) value = self._params[name] try: value = str(value).lower() except TypeError: pass - if value in ['1', 'y', 'yes', 'yeah', 'yep', 'yup', 't', 'true']: + if value in ["1", "y", "yes", "yeah", "yep", "yup", "t", "true"]: return True - if value in ['0', 'n', 'no', 'nope', 'f', 'false']: + if value in ["0", "n", "no", "nope", "f", "false"]: return False raise errors.InvalidParameterError( - 'Parameter %r must be a boolean value.' % name) + "Parameter %r must be a boolean value." % name + ) diff --git a/server/szurubooru/rest/errors.py b/server/szurubooru/rest/errors.py index 45dc615..446b757 100644 --- a/server/szurubooru/rest/errors.py +++ b/server/szurubooru/rest/errors.py @@ -1,19 +1,19 @@ -from typing import Optional, Callable, Type, Dict - +from typing import Callable, Dict, Optional, Type error_handlers = {} class BaseHttpError(RuntimeError): code = -1 - reason = '' + reason = "" def __init__( - self, - name: str, - description: str, - title: Optional[str] = None, - extra_fields: Optional[Dict[str, str]] = None) -> None: + self, + name: str, + description: str, + title: Optional[str] = None, + extra_fields: Optional[Dict[str, str]] = None, + ) -> None: super().__init__() # error name for programmers self.name = name @@ -27,40 +27,40 @@ class BaseHttpError(RuntimeError): class HttpBadRequest(BaseHttpError): code = 400 - reason = 'Bad Request' + reason = "Bad Request" class HttpForbidden(BaseHttpError): code = 403 - reason = 'Forbidden' + reason = "Forbidden" class HttpNotFound(BaseHttpError): code = 404 - reason = 'Not Found' + reason = "Not Found" class HttpNotAcceptable(BaseHttpError): code = 406 - reason = 'Not Acceptable' + reason = "Not Acceptable" class HttpConflict(BaseHttpError): code = 409 - reason = 'Conflict' + reason = "Conflict" class HttpMethodNotAllowed(BaseHttpError): code = 405 - reason = 'Method Not Allowed' + reason = "Method Not Allowed" class HttpInternalServerError(BaseHttpError): code = 500 - reason = 'Internal Server Error' + reason = "Internal Server Error" def handle( - exception_type: Type[Exception], - handler: Callable[[Exception], None]) -> None: + exception_type: Type[Exception], handler: Callable[[Exception], None] +) -> None: error_handlers[exception_type] = handler diff --git a/server/szurubooru/rest/middleware.py b/server/szurubooru/rest/middleware.py index 18b6b46..2936abf 100644 --- a/server/szurubooru/rest/middleware.py +++ b/server/szurubooru/rest/middleware.py @@ -1,6 +1,6 @@ -from typing import List, Callable -from szurubooru.rest.context import Context +from typing import Callable, List +from szurubooru.rest.context import Context pre_hooks = [] # type: List[Callable[[Context], None]] post_hooks = [] # type: List[Callable[[Context], None]] diff --git a/server/szurubooru/rest/routes.py b/server/szurubooru/rest/routes.py index 93e124e..b0946fb 100644 --- a/server/szurubooru/rest/routes.py +++ b/server/szurubooru/rest/routes.py @@ -1,7 +1,7 @@ -from typing import Callable, Dict from collections import defaultdict -from szurubooru.rest.context import Context, Response +from typing import Callable, Dict +from szurubooru.rest.context import Context, Response RouteHandler = Callable[[Context, Dict[str, str]], Response] routes = defaultdict(dict) # type: Dict[str, Dict[str, RouteHandler]] @@ -9,27 +9,31 @@ routes = defaultdict(dict) # type: Dict[str, Dict[str, RouteHandler]] def get(url: str) -> Callable[[RouteHandler], RouteHandler]: def wrapper(handler: RouteHandler) -> RouteHandler: - routes[url]['GET'] = handler + routes[url]["GET"] = handler return handler + return wrapper def put(url: str) -> Callable[[RouteHandler], RouteHandler]: def wrapper(handler: RouteHandler) -> RouteHandler: - routes[url]['PUT'] = handler + routes[url]["PUT"] = handler return handler + return wrapper def post(url: str) -> Callable[[RouteHandler], RouteHandler]: def wrapper(handler: RouteHandler) -> RouteHandler: - routes[url]['POST'] = handler + routes[url]["POST"] = handler return handler + return wrapper def delete(url: str) -> Callable[[RouteHandler], RouteHandler]: def wrapper(handler: RouteHandler) -> RouteHandler: - routes[url]['DELETE'] = handler + routes[url]["DELETE"] = handler return handler + return wrapper diff --git a/server/szurubooru/search/__init__.py b/server/szurubooru/search/__init__.py index 919475f..6ad1920 100644 --- a/server/szurubooru/search/__init__.py +++ b/server/szurubooru/search/__init__.py @@ -1,2 +1,2 @@ -from szurubooru.search.executor import Executor import szurubooru.search.configs +from szurubooru.search.executor import Executor diff --git a/server/szurubooru/search/configs/__init__.py b/server/szurubooru/search/configs/__init__.py index c6a3ea4..c721813 100644 --- a/server/szurubooru/search/configs/__init__.py +++ b/server/szurubooru/search/configs/__init__.py @@ -1,6 +1,6 @@ -from .user_search_config import UserSearchConfig -from .tag_search_config import TagSearchConfig -from .post_search_config import PostSearchConfig -from .snapshot_search_config import SnapshotSearchConfig from .comment_search_config import CommentSearchConfig from .pool_search_config import PoolSearchConfig +from .post_search_config import PostSearchConfig +from .snapshot_search_config import SnapshotSearchConfig +from .tag_search_config import TagSearchConfig +from .user_search_config import UserSearchConfig diff --git a/server/szurubooru/search/configs/base_search_config.py b/server/szurubooru/search/configs/base_search_config.py index 0cb814d..d60f361 100644 --- a/server/szurubooru/search/configs/base_search_config.py +++ b/server/szurubooru/search/configs/base_search_config.py @@ -1,5 +1,6 @@ -from typing import Optional, Tuple, Dict, Callable -from szurubooru.search import tokens, criteria +from typing import Callable, Dict, Optional, Tuple + +from szurubooru.search import criteria, tokens from szurubooru.search.query import SearchQuery from szurubooru.search.typing import SaColumn, SaQuery diff --git a/server/szurubooru/search/configs/comment_search_config.py b/server/szurubooru/search/configs/comment_search_config.py index 8b15446..1145e51 100644 --- a/server/szurubooru/search/configs/comment_search_config.py +++ b/server/szurubooru/search/configs/comment_search_config.py @@ -1,10 +1,14 @@ -from typing import Tuple, Dict +from typing import Dict, Tuple + import sqlalchemy as sa + from szurubooru import db, model -from szurubooru.search.typing import SaColumn, SaQuery from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import ( - BaseSearchConfig, Filter) + BaseSearchConfig, + Filter, +) +from szurubooru.search.typing import SaColumn, SaQuery class CommentSearchConfig(BaseSearchConfig): @@ -27,36 +31,42 @@ class CommentSearchConfig(BaseSearchConfig): @property def named_filters(self) -> Dict[str, Filter]: return { - 'id': search_util.create_num_filter(model.Comment.comment_id), - 'post': search_util.create_num_filter(model.Comment.post_id), - 'user': search_util.create_str_filter(model.User.name), - 'author': search_util.create_str_filter(model.User.name), - 'text': search_util.create_str_filter(model.Comment.text), - 'creation-date': - search_util.create_date_filter(model.Comment.creation_time), - 'creation-time': - search_util.create_date_filter(model.Comment.creation_time), - 'last-edit-date': - search_util.create_date_filter(model.Comment.last_edit_time), - 'last-edit-time': - search_util.create_date_filter(model.Comment.last_edit_time), - 'edit-date': - search_util.create_date_filter(model.Comment.last_edit_time), - 'edit-time': - search_util.create_date_filter(model.Comment.last_edit_time), + "id": search_util.create_num_filter(model.Comment.comment_id), + "post": search_util.create_num_filter(model.Comment.post_id), + "user": search_util.create_str_filter(model.User.name), + "author": search_util.create_str_filter(model.User.name), + "text": search_util.create_str_filter(model.Comment.text), + "creation-date": search_util.create_date_filter( + model.Comment.creation_time + ), + "creation-time": search_util.create_date_filter( + model.Comment.creation_time + ), + "last-edit-date": search_util.create_date_filter( + model.Comment.last_edit_time + ), + "last-edit-time": search_util.create_date_filter( + model.Comment.last_edit_time + ), + "edit-date": search_util.create_date_filter( + model.Comment.last_edit_time + ), + "edit-time": search_util.create_date_filter( + model.Comment.last_edit_time + ), } @property def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: return { - 'random': (sa.sql.expression.func.random(), self.SORT_NONE), - 'user': (model.User.name, self.SORT_ASC), - 'author': (model.User.name, self.SORT_ASC), - 'post': (model.Comment.post_id, self.SORT_DESC), - 'creation-date': (model.Comment.creation_time, self.SORT_DESC), - 'creation-time': (model.Comment.creation_time, self.SORT_DESC), - 'last-edit-date': (model.Comment.last_edit_time, self.SORT_DESC), - 'last-edit-time': (model.Comment.last_edit_time, self.SORT_DESC), - 'edit-date': (model.Comment.last_edit_time, self.SORT_DESC), - 'edit-time': (model.Comment.last_edit_time, self.SORT_DESC), + "random": (sa.sql.expression.func.random(), self.SORT_NONE), + "user": (model.User.name, self.SORT_ASC), + "author": (model.User.name, self.SORT_ASC), + "post": (model.Comment.post_id, self.SORT_DESC), + "creation-date": (model.Comment.creation_time, self.SORT_DESC), + "creation-time": (model.Comment.creation_time, self.SORT_DESC), + "last-edit-date": (model.Comment.last_edit_time, self.SORT_DESC), + "last-edit-time": (model.Comment.last_edit_time, self.SORT_DESC), + "edit-date": (model.Comment.last_edit_time, self.SORT_DESC), + "edit-time": (model.Comment.last_edit_time, self.SORT_DESC), } diff --git a/server/szurubooru/search/configs/pool_search_config.py b/server/szurubooru/search/configs/pool_search_config.py index ddc325a..88b30a6 100644 --- a/server/szurubooru/search/configs/pool_search_config.py +++ b/server/szurubooru/search/configs/pool_search_config.py @@ -1,24 +1,27 @@ -from typing import Tuple, Dict +from typing import Dict, Tuple + import sqlalchemy as sa + from szurubooru import db, model from szurubooru.func import util -from szurubooru.search.typing import SaColumn, SaQuery from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import ( - BaseSearchConfig, Filter) + BaseSearchConfig, + Filter, +) +from szurubooru.search.typing import SaColumn, SaQuery class PoolSearchConfig(BaseSearchConfig): def create_filter_query(self, _disable_eager_loads: bool) -> SaQuery: strategy = ( - sa.orm.lazyload - if _disable_eager_loads - else sa.orm.subqueryload) + sa.orm.lazyload if _disable_eager_loads else sa.orm.subqueryload + ) return ( db.session.query(model.Pool) .join(model.PoolCategory) - .options( - strategy(model.Pool.names))) + .options(strategy(model.Pool.names)) + ) def create_count_query(self, _disable_eager_loads: bool) -> SaQuery: return db.session.query(model.Pool) @@ -35,75 +38,74 @@ class PoolSearchConfig(BaseSearchConfig): model.Pool.pool_id, model.PoolName.pool_id, model.PoolName.name, - search_util.create_str_filter) + search_util.create_str_filter, + ) @property def named_filters(self) -> Dict[str, Filter]: - return util.unalias_dict([ - ( - ['name'], - search_util.create_subquery_filter( - model.Pool.pool_id, - model.PoolName.pool_id, - model.PoolName.name, - search_util.create_str_filter) - ), - - ( - ['category'], - search_util.create_subquery_filter( - model.Pool.category_id, - model.PoolCategory.pool_category_id, - model.PoolCategory.name, - search_util.create_str_filter) - ), - - ( - ['creation-date', 'creation-time'], - search_util.create_date_filter(model.Pool.creation_time) - ), - - ( - ['last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'], - search_util.create_date_filter(model.Pool.last_edit_time) - ), - - ( - ['post-count'], - search_util.create_num_filter(model.Pool.post_count) - ), - ]) + return util.unalias_dict( + [ + ( + ["name"], + search_util.create_subquery_filter( + model.Pool.pool_id, + model.PoolName.pool_id, + model.PoolName.name, + search_util.create_str_filter, + ), + ), + ( + ["category"], + search_util.create_subquery_filter( + model.Pool.category_id, + model.PoolCategory.pool_category_id, + model.PoolCategory.name, + search_util.create_str_filter, + ), + ), + ( + ["creation-date", "creation-time"], + search_util.create_date_filter(model.Pool.creation_time), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + search_util.create_date_filter(model.Pool.last_edit_time), + ), + ( + ["post-count"], + search_util.create_num_filter(model.Pool.post_count), + ), + ] + ) @property def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: - return util.unalias_dict([ - ( - ['random'], - (sa.sql.expression.func.random(), self.SORT_NONE) - ), - - ( - ['name'], - (model.Pool.first_name, self.SORT_ASC) - ), - - ( - ['category'], - (model.PoolCategory.name, self.SORT_ASC) - ), - - ( - ['creation-date', 'creation-time'], - (model.Pool.creation_time, self.SORT_DESC) - ), - - ( - ['last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'], - (model.Pool.last_edit_time, self.SORT_DESC) - ), - - ( - ['post-count'], - (model.Pool.post_count, self.SORT_DESC) - ), - ]) + return util.unalias_dict( + [ + ( + ["random"], + (sa.sql.expression.func.random(), self.SORT_NONE), + ), + (["name"], (model.Pool.first_name, self.SORT_ASC)), + (["category"], (model.PoolCategory.name, self.SORT_ASC)), + ( + ["creation-date", "creation-time"], + (model.Pool.creation_time, self.SORT_DESC), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + (model.Pool.last_edit_time, self.SORT_DESC), + ), + (["post-count"], (model.Pool.post_count, self.SORT_DESC)), + ] + ) diff --git a/server/szurubooru/search/configs/post_search_config.py b/server/szurubooru/search/configs/post_search_config.py index dcf0255..fc98e55 100644 --- a/server/szurubooru/search/configs/post_search_config.py +++ b/server/szurubooru/search/configs/post_search_config.py @@ -1,85 +1,91 @@ -from typing import Any, Optional, Tuple, Dict +from typing import Any, Dict, Optional, Tuple + import sqlalchemy as sa -from szurubooru import db, model, errors + +from szurubooru import db, errors, model from szurubooru.func import util from szurubooru.search import criteria, tokens -from szurubooru.search.typing import SaColumn, SaQuery -from szurubooru.search.query import SearchQuery from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import ( - BaseSearchConfig, Filter) + BaseSearchConfig, + Filter, +) +from szurubooru.search.query import SearchQuery +from szurubooru.search.typing import SaColumn, SaQuery def _type_transformer(value: str) -> str: available_values = { - 'image': model.Post.TYPE_IMAGE, - 'animation': model.Post.TYPE_ANIMATION, - 'animated': model.Post.TYPE_ANIMATION, - 'anim': model.Post.TYPE_ANIMATION, - 'gif': model.Post.TYPE_ANIMATION, - 'video': model.Post.TYPE_VIDEO, - 'webm': model.Post.TYPE_VIDEO, - 'flash': model.Post.TYPE_FLASH, - 'swf': model.Post.TYPE_FLASH, + "image": model.Post.TYPE_IMAGE, + "animation": model.Post.TYPE_ANIMATION, + "animated": model.Post.TYPE_ANIMATION, + "anim": model.Post.TYPE_ANIMATION, + "gif": model.Post.TYPE_ANIMATION, + "video": model.Post.TYPE_VIDEO, + "webm": model.Post.TYPE_VIDEO, + "flash": model.Post.TYPE_FLASH, + "swf": model.Post.TYPE_FLASH, } return search_util.enum_transformer(available_values, value) def _safety_transformer(value: str) -> str: available_values = { - 'safe': model.Post.SAFETY_SAFE, - 'sketchy': model.Post.SAFETY_SKETCHY, - 'questionable': model.Post.SAFETY_SKETCHY, - 'unsafe': model.Post.SAFETY_UNSAFE, + "safe": model.Post.SAFETY_SAFE, + "sketchy": model.Post.SAFETY_SKETCHY, + "questionable": model.Post.SAFETY_SKETCHY, + "unsafe": model.Post.SAFETY_UNSAFE, } return search_util.enum_transformer(available_values, value) def _flag_transformer(value: str) -> str: available_values = { - 'loop': model.Post.FLAG_LOOP, - 'sound': model.Post.FLAG_SOUND, + "loop": model.Post.FLAG_LOOP, + "sound": model.Post.FLAG_SOUND, } - return '%' + search_util.enum_transformer(available_values, value) + '%' + return "%" + search_util.enum_transformer(available_values, value) + "%" def _source_transformer(value: str) -> str: - return search_util.wildcard_transformer('*' + value + '*') + return search_util.wildcard_transformer("*" + value + "*") def _create_score_filter(score: int) -> Filter: def wrapper( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, + criterion: Optional[criteria.BaseCriterion], + negated: bool, + ) -> SaQuery: assert criterion - if not getattr(criterion, 'internal', False): + if not getattr(criterion, "internal", False): raise errors.SearchError( - 'Votes cannot be seen publicly. Did you mean %r?' - % 'special:liked') + "Votes cannot be seen publicly. Did you mean %r?" + % "special:liked" + ) user_alias = sa.orm.aliased(model.User) score_alias = sa.orm.aliased(model.PostScore) expr = score_alias.score == score expr = expr & search_util.apply_str_criterion_to_column( - user_alias.name, criterion) + user_alias.name, criterion + ) if negated: expr = ~expr ret = ( - query - .join(score_alias, score_alias.post_id == model.Post.post_id) + query.join(score_alias, score_alias.post_id == model.Post.post_id) .join(user_alias, user_alias.user_id == score_alias.user_id) - .filter(expr)) + .filter(expr) + ) return ret + return wrapper def _user_filter( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool +) -> SaQuery: assert criterion - if isinstance(criterion, criteria.PlainCriterion) \ - and not criterion.value: + if isinstance(criterion, criteria.PlainCriterion) and not criterion.value: expr = model.Post.user_id == None # noqa: E711 if negated: expr = ~expr @@ -88,31 +94,32 @@ def _user_filter( model.Post.user_id, model.User.user_id, model.User.name, - search_util.create_str_filter)(query, criterion, negated) + search_util.create_str_filter, + )(query, criterion, negated) def _note_filter( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool +) -> SaQuery: assert criterion return search_util.create_subquery_filter( model.Post.post_id, model.PostNote.post_id, model.PostNote.text, - 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: + 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) + search_util.create_num_filter, + )(query, criterion, negated) class PostSearchConfig(BaseSearchConfig): @@ -122,51 +129,52 @@ class PostSearchConfig(BaseSearchConfig): def on_search_query_parsed(self, search_query: SearchQuery) -> SaQuery: new_special_tokens = [] for token in search_query.special_tokens: - if token.value in ('fav', 'liked', 'disliked'): + if token.value in ("fav", "liked", "disliked"): assert self.user - if self.user.rank == 'anonymous': + if self.user.rank == "anonymous": raise errors.SearchError( - 'Must be logged in to use this feature.') + "Must be logged in to use this feature." + ) criterion = criteria.PlainCriterion( - original_text=self.user.name, - value=self.user.name) - setattr(criterion, 'internal', True) + original_text=self.user.name, value=self.user.name + ) + setattr(criterion, "internal", True) search_query.named_tokens.append( tokens.NamedToken( name=token.value, criterion=criterion, - negated=token.negated)) + negated=token.negated, + ) + ) else: new_special_tokens.append(token) search_query.special_tokens = new_special_tokens def create_around_query(self) -> SaQuery: - return db.session.query(model.Post).options(sa.orm.lazyload('*')) + return db.session.query(model.Post).options(sa.orm.lazyload("*")) def create_filter_query(self, disable_eager_loads: bool) -> SaQuery: strategy = ( - sa.orm.lazyload - if disable_eager_loads - else sa.orm.subqueryload) - return ( - db.session.query(model.Post) - .options( - sa.orm.lazyload('*'), - # use config optimized for official client - # sa.orm.defer(model.Post.score), - # sa.orm.defer(model.Post.favorite_count), - # sa.orm.defer(model.Post.comment_count), - sa.orm.defer(model.Post.last_favorite_time), - sa.orm.defer(model.Post.feature_count), - sa.orm.defer(model.Post.last_feature_time), - sa.orm.defer(model.Post.last_comment_creation_time), - sa.orm.defer(model.Post.last_comment_edit_time), - sa.orm.defer(model.Post.note_count), - sa.orm.defer(model.Post.tag_count), - strategy(model.Post.tags).subqueryload(model.Tag.names), - strategy(model.Post.tags).defer(model.Tag.post_count), - strategy(model.Post.tags).lazyload(model.Tag.implications), - strategy(model.Post.tags).lazyload(model.Tag.suggestions))) + sa.orm.lazyload if disable_eager_loads else sa.orm.subqueryload + ) + return db.session.query(model.Post).options( + sa.orm.lazyload("*"), + # use config optimized for official client + # sa.orm.defer(model.Post.score), + # sa.orm.defer(model.Post.favorite_count), + # sa.orm.defer(model.Post.comment_count), + sa.orm.defer(model.Post.last_favorite_time), + sa.orm.defer(model.Post.feature_count), + sa.orm.defer(model.Post.last_feature_time), + sa.orm.defer(model.Post.last_comment_creation_time), + sa.orm.defer(model.Post.last_comment_edit_time), + sa.orm.defer(model.Post.note_count), + sa.orm.defer(model.Post.tag_count), + strategy(model.Post.tags).subqueryload(model.Tag.names), + strategy(model.Post.tags).defer(model.Tag.post_count), + strategy(model.Post.tags).lazyload(model.Tag.implications), + strategy(model.Post.tags).lazyload(model.Tag.suggestions), + ) def create_count_query(self, _disable_eager_loads: bool) -> SaQuery: return db.session.query(model.Post) @@ -185,309 +193,256 @@ class PostSearchConfig(BaseSearchConfig): model.PostTag.post_id, model.TagName.name, search_util.create_str_filter, - lambda subquery: subquery.join(model.Tag).join(model.TagName)) + lambda subquery: subquery.join(model.Tag).join(model.TagName), + ) @property def named_filters(self) -> Dict[str, Filter]: - return util.unalias_dict([ - ( - ['id'], - search_util.create_num_filter(model.Post.post_id) - ), - - ( - ['tag'], - search_util.create_subquery_filter( - model.Post.post_id, - model.PostTag.post_id, - model.TagName.name, - search_util.create_str_filter, - lambda subquery: - subquery.join(model.Tag).join(model.TagName)) - ), - - ( - ['score'], - search_util.create_num_filter(model.Post.score) - ), - - ( - ['uploader', 'upload', 'submit'], - _user_filter - ), - - ( - ['comment'], - search_util.create_subquery_filter( - model.Post.post_id, - model.Comment.post_id, - model.User.name, - search_util.create_str_filter, - lambda subquery: subquery.join(model.User)) - ), - - ( - ['fav'], - search_util.create_subquery_filter( - model.Post.post_id, - model.PostFavorite.post_id, - model.User.name, - search_util.create_str_filter, - lambda subquery: subquery.join(model.User)) - ), - - ( - ['liked'], - _create_score_filter(1) - ), - ( - ['disliked'], - _create_score_filter(-1) - ), - - ( - ['source'], - search_util.create_str_filter( - model.Post.source, _source_transformer) - ), - - ( - ['tag-count'], - search_util.create_num_filter(model.Post.tag_count) - ), - - ( - ['comment-count'], - search_util.create_num_filter(model.Post.comment_count) - ), - - ( - ['fav-count'], - search_util.create_num_filter(model.Post.favorite_count) - ), - - ( - ['note-count'], - search_util.create_num_filter(model.Post.note_count) - ), - - ( - ['relation-count'], - search_util.create_num_filter(model.Post.relation_count) - ), - - ( - ['feature-count'], - search_util.create_num_filter(model.Post.feature_count) - ), - - ( - ['type'], - search_util.create_str_filter( - model.Post.type, _type_transformer) - ), - - ( - ['content-checksum'], - search_util.create_str_filter(model.Post.checksum) - ), - - ( - ['file-size'], - search_util.create_num_filter(model.Post.file_size) - ), - - ( - ['image-width', 'width'], - search_util.create_num_filter(model.Post.canvas_width) - ), - - ( - ['image-height', 'height'], - search_util.create_num_filter(model.Post.canvas_height) - ), - - ( - ['image-area', 'area'], - search_util.create_num_filter(model.Post.canvas_area) - ), - - ( - ['image-aspect-ratio', 'image-ar', 'aspect-ratio', 'ar'], - search_util.create_num_filter( - model.Post.canvas_aspect_ratio, - transformer=search_util.float_transformer) - ), - - ( - ['creation-date', 'creation-time', 'date', 'time'], - search_util.create_date_filter(model.Post.creation_time) - ), - - ( - ['last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'], - search_util.create_date_filter(model.Post.last_edit_time) - ), - - ( - ['comment-date', 'comment-time'], - search_util.create_date_filter( - model.Post.last_comment_creation_time) - ), - - ( - ['fav-date', 'fav-time'], - search_util.create_date_filter(model.Post.last_favorite_time) - ), - - ( - ['feature-date', 'feature-time'], - search_util.create_date_filter(model.Post.last_feature_time) - ), - - ( - ['safety', 'rating'], - search_util.create_str_filter( - model.Post.safety, _safety_transformer) - ), - - ( - ['note-text'], - _note_filter - ), - - ( - ['flag'], - search_util.create_str_filter( - model.Post.flags_string, _flag_transformer) - ), - - ( - ['pool'], - _pool_filter - ), - ]) + return util.unalias_dict( + [ + (["id"], search_util.create_num_filter(model.Post.post_id)), + ( + ["tag"], + search_util.create_subquery_filter( + model.Post.post_id, + model.PostTag.post_id, + model.TagName.name, + search_util.create_str_filter, + lambda subquery: subquery.join(model.Tag).join( + model.TagName + ), + ), + ), + (["score"], search_util.create_num_filter(model.Post.score)), + (["uploader", "upload", "submit"], _user_filter), + ( + ["comment"], + search_util.create_subquery_filter( + model.Post.post_id, + model.Comment.post_id, + model.User.name, + search_util.create_str_filter, + lambda subquery: subquery.join(model.User), + ), + ), + ( + ["fav"], + search_util.create_subquery_filter( + model.Post.post_id, + model.PostFavorite.post_id, + model.User.name, + search_util.create_str_filter, + lambda subquery: subquery.join(model.User), + ), + ), + (["liked"], _create_score_filter(1)), + (["disliked"], _create_score_filter(-1)), + ( + ["source"], + search_util.create_str_filter( + model.Post.source, _source_transformer + ), + ), + ( + ["tag-count"], + search_util.create_num_filter(model.Post.tag_count), + ), + ( + ["comment-count"], + search_util.create_num_filter(model.Post.comment_count), + ), + ( + ["fav-count"], + search_util.create_num_filter(model.Post.favorite_count), + ), + ( + ["note-count"], + search_util.create_num_filter(model.Post.note_count), + ), + ( + ["relation-count"], + search_util.create_num_filter(model.Post.relation_count), + ), + ( + ["feature-count"], + search_util.create_num_filter(model.Post.feature_count), + ), + ( + ["type"], + search_util.create_str_filter( + model.Post.type, _type_transformer + ), + ), + ( + ["content-checksum"], + search_util.create_str_filter(model.Post.checksum), + ), + ( + ["file-size"], + search_util.create_num_filter(model.Post.file_size), + ), + ( + ["image-width", "width"], + search_util.create_num_filter(model.Post.canvas_width), + ), + ( + ["image-height", "height"], + search_util.create_num_filter(model.Post.canvas_height), + ), + ( + ["image-area", "area"], + search_util.create_num_filter(model.Post.canvas_area), + ), + ( + ["image-aspect-ratio", "image-ar", "aspect-ratio", "ar"], + search_util.create_num_filter( + model.Post.canvas_aspect_ratio, + transformer=search_util.float_transformer, + ), + ), + ( + ["creation-date", "creation-time", "date", "time"], + search_util.create_date_filter(model.Post.creation_time), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + search_util.create_date_filter(model.Post.last_edit_time), + ), + ( + ["comment-date", "comment-time"], + search_util.create_date_filter( + model.Post.last_comment_creation_time + ), + ), + ( + ["fav-date", "fav-time"], + search_util.create_date_filter( + model.Post.last_favorite_time + ), + ), + ( + ["feature-date", "feature-time"], + search_util.create_date_filter( + model.Post.last_feature_time + ), + ), + ( + ["safety", "rating"], + search_util.create_str_filter( + model.Post.safety, _safety_transformer + ), + ), + (["note-text"], _note_filter), + ( + ["flag"], + search_util.create_str_filter( + model.Post.flags_string, _flag_transformer + ), + ), + (["pool"], _pool_filter), + ] + ) @property def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: - return util.unalias_dict([ - ( - ['random'], - (sa.sql.expression.func.random(), self.SORT_NONE) - ), - - ( - ['id'], - (model.Post.post_id, self.SORT_DESC) - ), - - ( - ['score'], - (model.Post.score, self.SORT_DESC) - ), - - ( - ['tag-count'], - (model.Post.tag_count, self.SORT_DESC) - ), - - ( - ['comment-count'], - (model.Post.comment_count, self.SORT_DESC) - ), - - ( - ['fav-count'], - (model.Post.favorite_count, self.SORT_DESC) - ), - - ( - ['note-count'], - (model.Post.note_count, self.SORT_DESC) - ), - - ( - ['relation-count'], - (model.Post.relation_count, self.SORT_DESC) - ), - - ( - ['feature-count'], - (model.Post.feature_count, self.SORT_DESC) - ), - - ( - ['file-size'], - (model.Post.file_size, self.SORT_DESC) - ), - - ( - ['image-width', 'width'], - (model.Post.canvas_width, self.SORT_DESC) - ), - - ( - ['image-height', 'height'], - (model.Post.canvas_height, self.SORT_DESC) - ), - - ( - ['image-area', 'area'], - (model.Post.canvas_area, self.SORT_DESC) - ), - - ( - ['creation-date', 'creation-time', 'date', 'time'], - (model.Post.creation_time, self.SORT_DESC) - ), - - ( - ['last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'], - (model.Post.last_edit_time, self.SORT_DESC) - ), - - ( - ['comment-date', 'comment-time'], - (model.Post.last_comment_creation_time, self.SORT_DESC) - ), - - ( - ['fav-date', 'fav-time'], - (model.Post.last_favorite_time, self.SORT_DESC) - ), - - ( - ['feature-date', 'feature-time'], - (model.Post.last_feature_time, self.SORT_DESC) - ), - ]) + return util.unalias_dict( + [ + ( + ["random"], + (sa.sql.expression.func.random(), self.SORT_NONE), + ), + (["id"], (model.Post.post_id, self.SORT_DESC)), + (["score"], (model.Post.score, self.SORT_DESC)), + (["tag-count"], (model.Post.tag_count, self.SORT_DESC)), + ( + ["comment-count"], + (model.Post.comment_count, self.SORT_DESC), + ), + (["fav-count"], (model.Post.favorite_count, self.SORT_DESC)), + (["note-count"], (model.Post.note_count, self.SORT_DESC)), + ( + ["relation-count"], + (model.Post.relation_count, self.SORT_DESC), + ), + ( + ["feature-count"], + (model.Post.feature_count, self.SORT_DESC), + ), + (["file-size"], (model.Post.file_size, self.SORT_DESC)), + ( + ["image-width", "width"], + (model.Post.canvas_width, self.SORT_DESC), + ), + ( + ["image-height", "height"], + (model.Post.canvas_height, self.SORT_DESC), + ), + ( + ["image-area", "area"], + (model.Post.canvas_area, self.SORT_DESC), + ), + ( + ["creation-date", "creation-time", "date", "time"], + (model.Post.creation_time, self.SORT_DESC), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + (model.Post.last_edit_time, self.SORT_DESC), + ), + ( + ["comment-date", "comment-time"], + (model.Post.last_comment_creation_time, self.SORT_DESC), + ), + ( + ["fav-date", "fav-time"], + (model.Post.last_favorite_time, self.SORT_DESC), + ), + ( + ["feature-date", "feature-time"], + (model.Post.last_feature_time, self.SORT_DESC), + ), + ] + ) @property def special_filters(self) -> Dict[str, Filter]: return { # handled by parser - 'fav': self.noop_filter, - 'liked': self.noop_filter, - 'disliked': self.noop_filter, - 'tumbleweed': self.tumbleweed_filter, + "fav": self.noop_filter, + "liked": self.noop_filter, + "disliked": self.noop_filter, + "tumbleweed": self.tumbleweed_filter, } def noop_filter( - self, - query: SaQuery, - _criterion: Optional[criteria.BaseCriterion], - _negated: bool) -> SaQuery: + self, + query: SaQuery, + _criterion: Optional[criteria.BaseCriterion], + _negated: bool, + ) -> SaQuery: return query def tumbleweed_filter( - self, - query: SaQuery, - _criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + self, + query: SaQuery, + _criterion: Optional[criteria.BaseCriterion], + negated: bool, + ) -> SaQuery: expr = ( (model.Post.comment_count == 0) & (model.Post.favorite_count == 0) - & (model.Post.score == 0)) + & (model.Post.score == 0) + ) if negated: expr = ~expr return query.filter(expr) diff --git a/server/szurubooru/search/configs/snapshot_search_config.py b/server/szurubooru/search/configs/snapshot_search_config.py index 0fdb69d..df722d2 100644 --- a/server/szurubooru/search/configs/snapshot_search_config.py +++ b/server/szurubooru/search/configs/snapshot_search_config.py @@ -1,9 +1,12 @@ from typing import Dict + from szurubooru import db, model -from szurubooru.search.typing import SaQuery from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import ( - BaseSearchConfig, Filter) + BaseSearchConfig, + Filter, +) +from szurubooru.search.typing import SaQuery class SnapshotSearchConfig(BaseSearchConfig): @@ -22,16 +25,18 @@ class SnapshotSearchConfig(BaseSearchConfig): @property def named_filters(self) -> Dict[str, Filter]: return { - 'type': - search_util.create_str_filter(model.Snapshot.resource_type), - 'id': - search_util.create_str_filter(model.Snapshot.resource_name), - 'date': - search_util.create_date_filter(model.Snapshot.creation_time), - 'time': - search_util.create_date_filter(model.Snapshot.creation_time), - 'operation': - search_util.create_str_filter(model.Snapshot.operation), - 'user': - search_util.create_str_filter(model.User.name), + "type": search_util.create_str_filter( + model.Snapshot.resource_type + ), + "id": search_util.create_str_filter(model.Snapshot.resource_name), + "date": search_util.create_date_filter( + model.Snapshot.creation_time + ), + "time": search_util.create_date_filter( + model.Snapshot.creation_time + ), + "operation": search_util.create_str_filter( + model.Snapshot.operation + ), + "user": search_util.create_str_filter(model.User.name), } diff --git a/server/szurubooru/search/configs/tag_search_config.py b/server/szurubooru/search/configs/tag_search_config.py index db3b4b2..5d41603 100644 --- a/server/szurubooru/search/configs/tag_search_config.py +++ b/server/szurubooru/search/configs/tag_search_config.py @@ -1,19 +1,22 @@ -from typing import Tuple, Dict +from typing import Dict, Tuple + import sqlalchemy as sa + from szurubooru import db, model from szurubooru.func import util -from szurubooru.search.typing import SaColumn, SaQuery from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import ( - BaseSearchConfig, Filter) + BaseSearchConfig, + Filter, +) +from szurubooru.search.typing import SaColumn, SaQuery class TagSearchConfig(BaseSearchConfig): def create_filter_query(self, _disable_eager_loads: bool) -> SaQuery: strategy = ( - sa.orm.lazyload - if _disable_eager_loads - else sa.orm.subqueryload) + sa.orm.lazyload if _disable_eager_loads else sa.orm.subqueryload + ) return ( db.session.query(model.Tag) .join(model.TagCategory) @@ -24,7 +27,9 @@ class TagSearchConfig(BaseSearchConfig): sa.orm.defer(model.Tag.post_count), strategy(model.Tag.names), strategy(model.Tag.suggestions).joinedload(model.Tag.names), - strategy(model.Tag.implications).joinedload(model.Tag.names))) + strategy(model.Tag.implications).joinedload(model.Tag.names), + ) + ) def create_count_query(self, _disable_eager_loads: bool) -> SaQuery: return db.session.query(model.Tag) @@ -41,95 +46,93 @@ class TagSearchConfig(BaseSearchConfig): model.Tag.tag_id, model.TagName.tag_id, model.TagName.name, - search_util.create_str_filter) + search_util.create_str_filter, + ) @property def named_filters(self) -> Dict[str, Filter]: - return util.unalias_dict([ - ( - ['name'], - search_util.create_subquery_filter( - model.Tag.tag_id, - model.TagName.tag_id, - model.TagName.name, - search_util.create_str_filter) - ), - - ( - ['category'], - search_util.create_subquery_filter( - model.Tag.category_id, - model.TagCategory.tag_category_id, - model.TagCategory.name, - search_util.create_str_filter) - ), - - ( - ['creation-date', 'creation-time'], - search_util.create_date_filter(model.Tag.creation_time) - ), - - ( - ['last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'], - search_util.create_date_filter(model.Tag.last_edit_time) - ), - - ( - ['usage-count', 'post-count', 'usages'], - search_util.create_num_filter(model.Tag.post_count) - ), - - ( - ['suggestion-count'], - search_util.create_num_filter(model.Tag.suggestion_count) - ), - - ( - ['implication-count'], - search_util.create_num_filter(model.Tag.implication_count) - ), - ]) + return util.unalias_dict( + [ + ( + ["name"], + search_util.create_subquery_filter( + model.Tag.tag_id, + model.TagName.tag_id, + model.TagName.name, + search_util.create_str_filter, + ), + ), + ( + ["category"], + search_util.create_subquery_filter( + model.Tag.category_id, + model.TagCategory.tag_category_id, + model.TagCategory.name, + search_util.create_str_filter, + ), + ), + ( + ["creation-date", "creation-time"], + search_util.create_date_filter(model.Tag.creation_time), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + search_util.create_date_filter(model.Tag.last_edit_time), + ), + ( + ["usage-count", "post-count", "usages"], + search_util.create_num_filter(model.Tag.post_count), + ), + ( + ["suggestion-count"], + search_util.create_num_filter(model.Tag.suggestion_count), + ), + ( + ["implication-count"], + search_util.create_num_filter(model.Tag.implication_count), + ), + ] + ) @property def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: - return util.unalias_dict([ - ( - ['random'], - (sa.sql.expression.func.random(), self.SORT_NONE) - ), - - ( - ['name'], - (model.Tag.first_name, self.SORT_ASC) - ), - - ( - ['category'], - (model.TagCategory.name, self.SORT_ASC) - ), - - ( - ['creation-date', 'creation-time'], - (model.Tag.creation_time, self.SORT_DESC) - ), - - ( - ['last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'], - (model.Tag.last_edit_time, self.SORT_DESC) - ), - - ( - ['usage-count', 'post-count', 'usages'], - (model.Tag.post_count, self.SORT_DESC) - ), - - ( - ['suggestion-count'], - (model.Tag.suggestion_count, self.SORT_DESC) - ), - - ( - ['implication-count'], - (model.Tag.implication_count, self.SORT_DESC) - ), - ]) + return util.unalias_dict( + [ + ( + ["random"], + (sa.sql.expression.func.random(), self.SORT_NONE), + ), + (["name"], (model.Tag.first_name, self.SORT_ASC)), + (["category"], (model.TagCategory.name, self.SORT_ASC)), + ( + ["creation-date", "creation-time"], + (model.Tag.creation_time, self.SORT_DESC), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + (model.Tag.last_edit_time, self.SORT_DESC), + ), + ( + ["usage-count", "post-count", "usages"], + (model.Tag.post_count, self.SORT_DESC), + ), + ( + ["suggestion-count"], + (model.Tag.suggestion_count, self.SORT_DESC), + ), + ( + ["implication-count"], + (model.Tag.implication_count, self.SORT_DESC), + ), + ] + ) diff --git a/server/szurubooru/search/configs/user_search_config.py b/server/szurubooru/search/configs/user_search_config.py index 6453400..bbf4034 100644 --- a/server/szurubooru/search/configs/user_search_config.py +++ b/server/szurubooru/search/configs/user_search_config.py @@ -1,10 +1,14 @@ -from typing import Tuple, Dict +from typing import Dict, Tuple + import sqlalchemy as sa + from szurubooru import db, model -from szurubooru.search.typing import SaColumn, SaQuery from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import ( - BaseSearchConfig, Filter) + BaseSearchConfig, + Filter, +) +from szurubooru.search.typing import SaColumn, SaQuery class UserSearchConfig(BaseSearchConfig): @@ -27,31 +31,36 @@ class UserSearchConfig(BaseSearchConfig): @property def named_filters(self) -> Dict[str, Filter]: return { - 'name': - search_util.create_str_filter(model.User.name), - 'creation-date': - search_util.create_date_filter(model.User.creation_time), - 'creation-time': - search_util.create_date_filter(model.User.creation_time), - 'last-login-date': - search_util.create_date_filter(model.User.last_login_time), - 'last-login-time': - search_util.create_date_filter(model.User.last_login_time), - 'login-date': - search_util.create_date_filter(model.User.last_login_time), - 'login-time': - search_util.create_date_filter(model.User.last_login_time), + "name": search_util.create_str_filter(model.User.name), + "creation-date": search_util.create_date_filter( + model.User.creation_time + ), + "creation-time": search_util.create_date_filter( + model.User.creation_time + ), + "last-login-date": search_util.create_date_filter( + model.User.last_login_time + ), + "last-login-time": search_util.create_date_filter( + model.User.last_login_time + ), + "login-date": search_util.create_date_filter( + model.User.last_login_time + ), + "login-time": search_util.create_date_filter( + model.User.last_login_time + ), } @property def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: return { - 'random': (sa.sql.expression.func.random(), self.SORT_NONE), - 'name': (model.User.name, self.SORT_ASC), - 'creation-date': (model.User.creation_time, self.SORT_DESC), - 'creation-time': (model.User.creation_time, self.SORT_DESC), - 'last-login-date': (model.User.last_login_time, self.SORT_DESC), - 'last-login-time': (model.User.last_login_time, self.SORT_DESC), - 'login-date': (model.User.last_login_time, self.SORT_DESC), - 'login-time': (model.User.last_login_time, self.SORT_DESC), + "random": (sa.sql.expression.func.random(), self.SORT_NONE), + "name": (model.User.name, self.SORT_ASC), + "creation-date": (model.User.creation_time, self.SORT_DESC), + "creation-time": (model.User.creation_time, self.SORT_DESC), + "last-login-date": (model.User.last_login_time, self.SORT_DESC), + "last-login-time": (model.User.last_login_time, self.SORT_DESC), + "login-date": (model.User.last_login_time, self.SORT_DESC), + "login-time": (model.User.last_login_time, self.SORT_DESC), } diff --git a/server/szurubooru/search/configs/util.py b/server/szurubooru/search/configs/util.py index ee201a2..58e6ebe 100644 --- a/server/szurubooru/search/configs/util.py +++ b/server/szurubooru/search/configs/util.py @@ -1,33 +1,36 @@ -from typing import Any, Optional, Union, Dict, Callable +from typing import Any, Callable, Dict, Optional, Union + import sqlalchemy as sa + from szurubooru import db, errors from szurubooru.func import util from szurubooru.search import criteria -from szurubooru.search.typing import SaColumn, SaQuery from szurubooru.search.configs.base_search_config import Filter - +from szurubooru.search.typing import SaColumn, SaQuery Number = Union[int, float] -WILDCARD = '(--wildcard--)' # something unlikely to be used by the users +WILDCARD = "(--wildcard--)" # something unlikely to be used by the users def unescape(text: str, make_wildcards_special: bool = False) -> str: - output = '' + output = "" i = 0 while i < len(text): - if text[i] == '\\': + if text[i] == "\\": try: char = text[i + 1] i += 1 except IndexError: raise errors.SearchError( - 'Unterminated escape sequence (did you forget to escape ' - 'the ending backslash?)') - if char not in '*\\:-.,': + "Unterminated escape sequence (did you forget to escape " + "the ending backslash?)" + ) + if char not in "*\\:-.,": raise errors.SearchError( - 'Unknown escape sequence (did you forget to escape ' - 'the backslash?)') - elif text[i] == '*' and make_wildcards_special: + "Unknown escape sequence (did you forget to escape " + "the backslash?)" + ) + elif text[i] == "*" and make_wildcards_special: char = WILDCARD else: char = text[i] @@ -39,10 +42,11 @@ def unescape(text: str, make_wildcards_special: bool = False) -> str: def wildcard_transformer(value: str) -> str: return ( unescape(value, make_wildcards_special=True) - .replace('\\', '\\\\') - .replace('%', '\\%') - .replace('_', '\\_') - .replace(WILDCARD, '%')) + .replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_") + .replace(WILDCARD, "%") + ) def enum_transformer(available_values: Dict[str, Any], value: str) -> str: @@ -50,8 +54,9 @@ def enum_transformer(available_values: Dict[str, Any], value: str) -> str: return available_values[unescape(value.lower())] except KeyError: raise errors.SearchError( - 'Invalid value: %r. Possible values: %r.' % ( - value, list(sorted(available_values.keys())))) + "Invalid value: %r. Possible values: %r." + % (value, list(sorted(available_values.keys()))) + ) def integer_transformer(value: str) -> int: @@ -59,7 +64,7 @@ def integer_transformer(value: str) -> int: def float_transformer(value: str) -> float: - for sep in list('/:'): + for sep in list("/:"): if sep in value: a, b = value.split(sep, 1) return float(unescape(a)) / float(unescape(b)) @@ -67,9 +72,10 @@ def float_transformer(value: str) -> float: def apply_num_criterion_to_column( - column: Any, - criterion: criteria.BaseCriterion, - transformer: Callable[[str], Number] = integer_transformer) -> SaQuery: + column: Any, + criterion: criteria.BaseCriterion, + transformer: Callable[[str], Number] = integer_transformer, +) -> SaQuery: try: if isinstance(criterion, criteria.PlainCriterion): expr = column == transformer(criterion.value) @@ -80,7 +86,8 @@ def apply_num_criterion_to_column( if criterion.min_value and criterion.max_value: expr = column.between( transformer(criterion.min_value), - transformer(criterion.max_value)) + transformer(criterion.max_value), + ) elif criterion.min_value: expr = column >= transformer(criterion.min_value) elif criterion.max_value: @@ -89,29 +96,33 @@ def apply_num_criterion_to_column( assert False except ValueError: raise errors.SearchError( - 'Criterion value %r must be a number.' % (criterion,)) + "Criterion value %r must be a number." % (criterion,) + ) return expr def create_num_filter( - column: Any, - transformer: Callable[[str], Number] = integer_transformer) -> SaQuery: + column: Any, transformer: Callable[[str], Number] = integer_transformer +) -> SaQuery: def wrapper( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, + criterion: Optional[criteria.BaseCriterion], + negated: bool, + ) -> SaQuery: assert criterion expr = apply_num_criterion_to_column(column, criterion, transformer) if negated: expr = ~expr return query.filter(expr) + return wrapper def apply_str_criterion_to_column( - column: SaColumn, - criterion: criteria.BaseCriterion, - transformer: Callable[[str], str] = wildcard_transformer) -> SaQuery: + column: SaColumn, + criterion: criteria.BaseCriterion, + transformer: Callable[[str], str] = wildcard_transformer, +) -> SaQuery: if isinstance(criterion, criteria.PlainCriterion): expr = column.ilike(transformer(criterion.value)) elif isinstance(criterion, criteria.ArrayCriterion): @@ -120,30 +131,34 @@ def apply_str_criterion_to_column( expr = expr | column.ilike(transformer(value)) elif isinstance(criterion, criteria.RangedCriterion): raise errors.SearchError( - 'Ranged criterion is invalid in this context. ' - 'Did you forget to escape the dots?') + "Ranged criterion is invalid in this context. " + "Did you forget to escape the dots?" + ) else: assert False return expr def create_str_filter( - column: SaColumn, - transformer: Callable[[str], str] = wildcard_transformer) -> Filter: + column: SaColumn, transformer: Callable[[str], str] = wildcard_transformer +) -> Filter: def wrapper( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, + criterion: Optional[criteria.BaseCriterion], + negated: bool, + ) -> SaQuery: assert criterion expr = apply_str_criterion_to_column(column, criterion, transformer) if negated: expr = ~expr return query.filter(expr) + return wrapper def apply_date_criterion_to_column( - column: SaQuery, criterion: criteria.BaseCriterion) -> SaQuery: + column: SaQuery, criterion: criteria.BaseCriterion +) -> SaQuery: if isinstance(criterion, criteria.PlainCriterion): min_date, max_date = util.parse_time_range(criterion.value) expr = column.between(min_date, max_date) @@ -171,36 +186,40 @@ def apply_date_criterion_to_column( def create_date_filter(column: SaColumn) -> Filter: def wrapper( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, + criterion: Optional[criteria.BaseCriterion], + negated: bool, + ) -> SaQuery: assert criterion expr = apply_date_criterion_to_column(column, criterion) if negated: expr = ~expr return query.filter(expr) + return wrapper def create_subquery_filter( - left_id_column: SaColumn, - right_id_column: SaColumn, - filter_column: SaColumn, - filter_factory: SaColumn, - subquery_decorator: Callable[[SaQuery], None] = None) -> Filter: + left_id_column: SaColumn, + right_id_column: SaColumn, + filter_column: SaColumn, + filter_factory: SaColumn, + subquery_decorator: Callable[[SaQuery], None] = None, +) -> Filter: filter_func = filter_factory(filter_column) def wrapper( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, + criterion: Optional[criteria.BaseCriterion], + negated: bool, + ) -> SaQuery: assert criterion - subquery = db.session.query(right_id_column.label('foreign_id')) + subquery = db.session.query(right_id_column.label("foreign_id")) if subquery_decorator: subquery = subquery_decorator(subquery) - subquery = subquery.options(sa.orm.lazyload('*')) + subquery = subquery.options(sa.orm.lazyload("*")) subquery = filter_func(subquery, criterion, False) - subquery = subquery.subquery('t') + subquery = subquery.subquery("t") expression = left_id_column.in_(subquery) if negated: expression = ~expression diff --git a/server/szurubooru/search/criteria.py b/server/szurubooru/search/criteria.py index 4512b0f..633c6f2 100644 --- a/server/szurubooru/search/criteria.py +++ b/server/szurubooru/search/criteria.py @@ -1,4 +1,5 @@ -from typing import Optional, List +from typing import List, Optional + from szurubooru.search.typing import SaQuery @@ -12,16 +13,17 @@ class BaseCriterion: class RangedCriterion(BaseCriterion): def __init__( - self, - original_text: str, - min_value: Optional[str], - max_value: Optional[str]) -> None: + self, + original_text: str, + min_value: Optional[str], + max_value: Optional[str], + ) -> None: super().__init__(original_text) self.min_value = min_value self.max_value = max_value def __hash__(self) -> int: - return hash(('range', self.min_value, self.max_value)) + return hash(("range", self.min_value, self.max_value)) class PlainCriterion(BaseCriterion): @@ -39,4 +41,4 @@ class ArrayCriterion(BaseCriterion): self.values = values def __hash__(self) -> int: - return hash(tuple(['array'] + self.values)) + return hash(tuple(["array"] + self.values)) diff --git a/server/szurubooru/search/executor.py b/server/szurubooru/search/executor.py index 10b34b1..a5ef962 100644 --- a/server/szurubooru/search/executor.py +++ b/server/szurubooru/search/executor.py @@ -1,11 +1,13 @@ -from typing import Union, Tuple, List, Dict, Callable +from typing import Callable, Dict, List, Tuple, Union + import sqlalchemy as sa -from szurubooru import db, model, errors, rest + +from szurubooru import db, errors, model, rest from szurubooru.func import cache -from szurubooru.search import tokens, parser -from szurubooru.search.typing import SaQuery -from szurubooru.search.query import SearchQuery +from szurubooru.search import parser, tokens from szurubooru.search.configs.base_search_config import BaseSearchConfig +from szurubooru.search.query import SearchQuery +from szurubooru.search.typing import SaQuery def _format_dict_keys(source: Dict) -> List[str]: @@ -25,61 +27,59 @@ def _get_order(order: str, default_order: str) -> Union[bool, str]: class Executor: - ''' + """ Class for search parsing and execution. Handles plaintext parsing and delegates sqlalchemy filter decoration to SearchConfig instances. - ''' + """ def __init__(self, search_config: BaseSearchConfig) -> None: self.config = search_config self.parser = parser.Parser() def get_around( - self, - query_text: str, - entity_id: int) -> Tuple[model.Base, model.Base]: + self, query_text: str, entity_id: int + ) -> Tuple[model.Base, model.Base]: search_query = self.parser.parse(query_text) self.config.on_search_query_parsed(search_query) - filter_query = ( - self.config - .create_around_query() - .options(sa.orm.lazyload('*'))) + filter_query = self.config.create_around_query().options( + sa.orm.lazyload("*") + ) filter_query = self._prepare_db_query( - filter_query, search_query, False) + filter_query, search_query, False + ) prev_filter_query = ( - filter_query - .filter(self.config.id_column > entity_id) + filter_query.filter(self.config.id_column > entity_id) .order_by(None) .order_by(sa.func.abs(self.config.id_column - entity_id).asc()) - .limit(1)) + .limit(1) + ) next_filter_query = ( - filter_query - .filter(self.config.id_column < entity_id) + filter_query.filter(self.config.id_column < entity_id) .order_by(None) .order_by(sa.func.abs(self.config.id_column - entity_id).asc()) - .limit(1)) + .limit(1) + ) return ( prev_filter_query.one_or_none(), - next_filter_query.one_or_none()) + next_filter_query.one_or_none(), + ) def get_around_and_serialize( self, ctx: rest.Context, entity_id: int, - serializer: Callable[[model.Base], rest.Response] + serializer: Callable[[model.Base], rest.Response], ) -> rest.Response: entities = self.get_around( - ctx.get_param_as_string('query', default=''), entity_id) + ctx.get_param_as_string("query", default=""), entity_id + ) return { - 'prev': serializer(entities[0]), - 'next': serializer(entities[1]), + "prev": serializer(entities[0]), + "next": serializer(entities[1]), } def execute( - self, - query_text: str, - offset: int, - limit: int + self, query_text: str, offset: int, limit: int ) -> Tuple[int, List[model.Base]]: search_query = self.parser.parse(query_text) self.config.on_search_query_parsed(search_query) @@ -90,7 +90,7 @@ class Executor: disable_eager_loads = False for token in search_query.sort_tokens: - if token.name == 'random': + if token.name == "random": disable_eager_loads = True key = (id(self.config), hash(search_query), offset, limit) @@ -98,22 +98,16 @@ class Executor: return cache.get(key) filter_query = self.config.create_filter_query(disable_eager_loads) - filter_query = filter_query.options(sa.orm.lazyload('*')) + filter_query = filter_query.options(sa.orm.lazyload("*")) filter_query = self._prepare_db_query(filter_query, search_query, True) - entities = ( - filter_query - .offset(offset) - .limit(limit) - .all()) + entities = filter_query.offset(offset).limit(limit).all() count_query = self.config.create_count_query(disable_eager_loads) - count_query = count_query.options(sa.orm.lazyload('*')) + count_query = count_query.options(sa.orm.lazyload("*")) count_query = self._prepare_db_query(count_query, search_query, False) - count_statement = ( - count_query - .statement - .with_only_columns([sa.func.count()]) - .order_by(None)) + count_statement = count_query.statement.with_only_columns( + [sa.func.count()] + ).order_by(None) count = db.session.execute(count_statement).scalar() ret = (count, entities) @@ -123,61 +117,73 @@ class Executor: def execute_and_serialize( self, ctx: rest.Context, - serializer: Callable[[model.Base], rest.Response] + serializer: Callable[[model.Base], rest.Response], ) -> rest.Response: - query = ctx.get_param_as_string('query', default='') - offset = ctx.get_param_as_int('offset', default=0, min=0) - limit = ctx.get_param_as_int('limit', default=100, min=1, max=100) + query = ctx.get_param_as_string("query", default="") + offset = ctx.get_param_as_int("offset", default=0, min=0) + limit = ctx.get_param_as_int("limit", default=100, min=1, max=100) count, entities = self.execute(query, offset, limit) return { - 'query': query, - 'offset': offset, - 'limit': limit, - 'total': count, - 'results': list([serializer(entity) for entity in entities]), + "query": query, + "offset": offset, + "limit": limit, + "total": count, + "results": list([serializer(entity) for entity in entities]), } def _prepare_db_query( - self, - db_query: SaQuery, - search_query: SearchQuery, - use_sort: bool) -> SaQuery: + self, db_query: SaQuery, search_query: SearchQuery, use_sort: bool + ) -> SaQuery: for anon_token in search_query.anonymous_tokens: if not self.config.anonymous_filter: raise errors.SearchError( - 'Anonymous tokens are not valid in this context.') + "Anonymous tokens are not valid in this context." + ) db_query = self.config.anonymous_filter( - db_query, anon_token.criterion, anon_token.negated) + db_query, anon_token.criterion, anon_token.negated + ) for named_token in search_query.named_tokens: if named_token.name not in self.config.named_filters: raise errors.SearchError( - 'Unknown named token: %r. Available named tokens: %r.' % ( + "Unknown named token: %r. Available named tokens: %r." + % ( named_token.name, - _format_dict_keys(self.config.named_filters))) + _format_dict_keys(self.config.named_filters), + ) + ) db_query = self.config.named_filters[named_token.name]( - db_query, named_token.criterion, named_token.negated) + db_query, named_token.criterion, named_token.negated + ) for sp_token in search_query.special_tokens: if sp_token.value not in self.config.special_filters: raise errors.SearchError( - 'Unknown special token: %r. ' - 'Available special tokens: %r.' % ( + "Unknown special token: %r. " + "Available special tokens: %r." + % ( sp_token.value, - _format_dict_keys(self.config.special_filters))) + _format_dict_keys(self.config.special_filters), + ) + ) db_query = self.config.special_filters[sp_token.value]( - db_query, None, sp_token.negated) + db_query, None, sp_token.negated + ) if use_sort: for sort_token in search_query.sort_tokens: if sort_token.name not in self.config.sort_columns: raise errors.SearchError( - 'Unknown sort token: %r. ' - 'Available sort tokens: %r.' % ( + "Unknown sort token: %r. " + "Available sort tokens: %r." + % ( sort_token.name, - _format_dict_keys(self.config.sort_columns))) - column, default_order = ( - self.config.sort_columns[sort_token.name]) + _format_dict_keys(self.config.sort_columns), + ) + ) + column, default_order = self.config.sort_columns[ + sort_token.name + ] order = _get_order(sort_token.order, default_order) if order == sort_token.SORT_ASC: db_query = db_query.order_by(column.asc()) diff --git a/server/szurubooru/search/parser.py b/server/szurubooru/search/parser.py index 79cf968..d5e0f2f 100644 --- a/server/szurubooru/search/parser.py +++ b/server/szurubooru/search/parser.py @@ -1,21 +1,23 @@ import re + from szurubooru import errors from szurubooru.search import criteria, tokens -from szurubooru.search.query import SearchQuery from szurubooru.search.configs import util +from szurubooru.search.query import SearchQuery def _create_criterion( - original_value: str, value: str) -> criteria.BaseCriterion: - if re.search(r'(? criteria.BaseCriterion: + if re.search(r"(? tokens.AnonymousToken: def _parse_named(key: str, value: str, negated: bool) -> tokens.NamedToken: original_value = value - if key.endswith('-min'): + if key.endswith("-min"): key = key[:-4] - value += '..' - elif key.endswith('-max'): + value += ".." + elif key.endswith("-max"): key = key[:-4] - value = '..' + value + value = ".." + value criterion = _create_criterion(original_value, value) return tokens.NamedToken(key, criterion, negated) @@ -42,32 +44,27 @@ def _parse_special(value: str, negated: bool) -> tokens.SpecialToken: def _parse_sort(value: str, negated: bool) -> tokens.SortToken: - if value.count(',') == 0: + if value.count(",") == 0: order_str = None - elif value.count(',') == 1: - value, order_str = value.split(',') + elif value.count(",") == 1: + value, order_str = value.split(",") else: - raise errors.SearchError('Too many commas in sort style token.') + raise errors.SearchError("Too many commas in sort style token.") try: order = { - 'asc': tokens.SortToken.SORT_ASC, - 'desc': tokens.SortToken.SORT_DESC, - '': tokens.SortToken.SORT_DEFAULT, + "asc": tokens.SortToken.SORT_ASC, + "desc": tokens.SortToken.SORT_DESC, + "": tokens.SortToken.SORT_DEFAULT, None: tokens.SortToken.SORT_DEFAULT, }[order_str] except KeyError: - raise errors.SearchError( - 'Unknown search direction: %r.' % order_str) + raise errors.SearchError("Unknown search direction: %r." % order_str) if negated: order = { - tokens.SortToken.SORT_ASC: - tokens.SortToken.SORT_DESC, - tokens.SortToken.SORT_DESC: - tokens.SortToken.SORT_ASC, - tokens.SortToken.SORT_DEFAULT: - tokens.SortToken.SORT_NEGATED_DEFAULT, - tokens.SortToken.SORT_NEGATED_DEFAULT: - tokens.SortToken.SORT_DEFAULT, + tokens.SortToken.SORT_ASC: tokens.SortToken.SORT_DESC, + tokens.SortToken.SORT_DESC: tokens.SortToken.SORT_ASC, + tokens.SortToken.SORT_DEFAULT: tokens.SortToken.SORT_NEGATED_DEFAULT, # noqa: E501 + tokens.SortToken.SORT_NEGATED_DEFAULT: tokens.SortToken.SORT_DEFAULT, # noqa: E501 }[order] return tokens.SortToken(value, order) @@ -75,29 +72,27 @@ def _parse_sort(value: str, negated: bool) -> tokens.SortToken: class Parser: def parse(self, query_text: str) -> SearchQuery: query = SearchQuery() - for chunk in re.split(r'\s+', (query_text or '').lower()): + for chunk in re.split(r"\s+", (query_text or "").lower()): if not chunk: continue negated = False - if chunk[0] == '-': + if chunk[0] == "-": chunk = chunk[1:] negated = True if not chunk: - raise errors.SearchError('Empty negated token.') - match = re.match(r'^(.*?)(? None: @@ -10,8 +11,11 @@ class SearchQuery: self.sort_tokens = [] # type: List[tokens.SortToken] def __hash__(self) -> int: - return hash(( - tuple(self.anonymous_tokens), - tuple(self.named_tokens), - tuple(self.special_tokens), - tuple(self.sort_tokens))) + return hash( + ( + tuple(self.anonymous_tokens), + tuple(self.named_tokens), + tuple(self.special_tokens), + tuple(self.sort_tokens), + ) + ) diff --git a/server/szurubooru/search/tokens.py b/server/szurubooru/search/tokens.py index 0cd7fd7..9f4eeed 100644 --- a/server/szurubooru/search/tokens.py +++ b/server/szurubooru/search/tokens.py @@ -12,7 +12,8 @@ class AnonymousToken: class NamedToken(AnonymousToken): def __init__( - self, name: str, criterion: BaseCriterion, negated: bool) -> None: + self, name: str, criterion: BaseCriterion, negated: bool + ) -> None: super().__init__(criterion, negated) self.name = name @@ -21,11 +22,11 @@ class NamedToken(AnonymousToken): class SortToken: - SORT_DESC = 'desc' - SORT_ASC = 'asc' - SORT_NONE = '' - SORT_DEFAULT = 'default' - SORT_NEGATED_DEFAULT = 'negated default' + SORT_DESC = "desc" + SORT_ASC = "asc" + SORT_NONE = "" + SORT_DEFAULT = "default" + SORT_NEGATED_DEFAULT = "negated default" def __init__(self, name: str, order: str) -> None: self.name = name diff --git a/server/szurubooru/search/typing.py b/server/szurubooru/search/typing.py index ebb1b30..686c2cb 100644 --- a/server/szurubooru/search/typing.py +++ b/server/szurubooru/search/typing.py @@ -1,6 +1,5 @@ from typing import Any, Callable - SaColumn = Any SaQuery = Any SaQueryFactory = Callable[[], SaQuery] diff --git a/server/szurubooru/tests/api/test_comment_creating.py b/server/szurubooru/tests/api/test_comment_creating.py index ad24366..3f9479b 100644 --- a/server/szurubooru/tests/api/test_comment_creating.py +++ b/server/szurubooru/tests/api/test_comment_creating.py @@ -1,70 +1,76 @@ from datetime import datetime from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import comments, posts @pytest.fixture(autouse=True) def inject_config(config_injector): config_injector( - {'privileges': {'comments:create': model.User.RANK_REGULAR}}) + {"privileges": {"comments:create": model.User.RANK_REGULAR}} + ) def test_creating_comment( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): post = post_factory() user = user_factory(rank=model.User.RANK_REGULAR) db.session.add_all([post, user]) db.session.flush() - with patch('szurubooru.func.comments.serialize_comment'), \ - fake_datetime('1997-01-01'): - comments.serialize_comment.return_value = 'serialized comment' + with patch("szurubooru.func.comments.serialize_comment"), fake_datetime( + "1997-01-01" + ): + comments.serialize_comment.return_value = "serialized comment" result = api.comment_api.create_comment( context_factory( - params={'text': 'input', 'postId': post.post_id}, - user=user)) - assert result == 'serialized comment' + params={"text": "input", "postId": post.post_id}, user=user + ) + ) + assert result == "serialized comment" comment = db.session.query(model.Comment).one() - assert comment.text == 'input' + assert comment.text == "input" assert comment.creation_time == datetime(1997, 1, 1) assert comment.last_edit_time is None assert comment.user and comment.user.user_id == user.user_id assert comment.post and comment.post.post_id == post.post_id -@pytest.mark.parametrize('params', [ - {'text': None}, - {'text': ''}, - {'text': [None]}, - {'text': ['']}, -]) +@pytest.mark.parametrize( + "params", [{"text": None}, {"text": ""}, {"text": [None]}, {"text": [""]},] +) def test_trying_to_pass_invalid_params( - user_factory, post_factory, context_factory, params): + user_factory, post_factory, context_factory, params +): post = post_factory() user = user_factory(rank=model.User.RANK_REGULAR) db.session.add_all([post, user]) db.session.flush() - real_params = {'text': 'input', 'postId': post.post_id} + real_params = {"text": "input", "postId": post.post_id} for key, value in params.items(): real_params[key] = value with pytest.raises(errors.ValidationError): api.comment_api.create_comment( - context_factory(params=real_params, user=user)) + context_factory(params=real_params, user=user) + ) -@pytest.mark.parametrize('field', ['text', 'postId']) +@pytest.mark.parametrize("field", ["text", "postId"]) def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): params = { - 'text': 'input', - 'postId': 1, + "text": "input", + "postId": 1, } del params[field] with pytest.raises(errors.ValidationError): api.comment_api.create_comment( context_factory( - params={}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={}, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) def test_trying_to_comment_non_existing(user_factory, context_factory): @@ -73,13 +79,14 @@ def test_trying_to_comment_non_existing(user_factory, context_factory): db.session.flush() with pytest.raises(posts.PostNotFoundError): api.comment_api.create_comment( - context_factory( - params={'text': 'bad', 'postId': 5}, user=user)) + context_factory(params={"text": "bad", "postId": 5}, user=user) + ) def test_trying_to_create_without_privileges(user_factory, context_factory): with pytest.raises(errors.AuthError): api.comment_api.create_comment( context_factory( - params={}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={}, user=user_factory(rank=model.User.RANK_ANONYMOUS) + ) + ) diff --git a/server/szurubooru/tests/api/test_comment_deleting.py b/server/szurubooru/tests/api/test_comment_deleting.py index e1d1baa..71df1ff 100644 --- a/server/szurubooru/tests/api/test_comment_deleting.py +++ b/server/szurubooru/tests/api/test_comment_deleting.py @@ -1,16 +1,19 @@ import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import comments @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'comments:delete:own': model.User.RANK_REGULAR, - 'comments:delete:any': model.User.RANK_MODERATOR, - }, - }) + config_injector( + { + "privileges": { + "comments:delete:own": model.User.RANK_REGULAR, + "comments:delete:any": model.User.RANK_MODERATOR, + }, + } + ) def test_deleting_own_comment(user_factory, comment_factory, context_factory): @@ -19,27 +22,31 @@ def test_deleting_own_comment(user_factory, comment_factory, context_factory): db.session.add(comment) db.session.commit() result = api.comment_api.delete_comment( - context_factory(params={'version': 1}, user=user), - {'comment_id': comment.comment_id}) + context_factory(params={"version": 1}, user=user), + {"comment_id": comment.comment_id}, + ) assert result == {} assert db.session.query(model.Comment).count() == 0 def test_deleting_someones_else_comment( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): user1 = user_factory(rank=model.User.RANK_REGULAR) user2 = user_factory(rank=model.User.RANK_MODERATOR) comment = comment_factory(user=user1) db.session.add(comment) db.session.commit() api.comment_api.delete_comment( - context_factory(params={'version': 1}, user=user2), - {'comment_id': comment.comment_id}) + context_factory(params={"version": 1}, user=user2), + {"comment_id": comment.comment_id}, + ) assert db.session.query(model.Comment).count() == 0 def test_trying_to_delete_someones_else_comment_without_privileges( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): user1 = user_factory(rank=model.User.RANK_REGULAR) user2 = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user1) @@ -47,8 +54,9 @@ def test_trying_to_delete_someones_else_comment_without_privileges( db.session.commit() with pytest.raises(errors.AuthError): api.comment_api.delete_comment( - context_factory(params={'version': 1}, user=user2), - {'comment_id': comment.comment_id}) + context_factory(params={"version": 1}, user=user2), + {"comment_id": comment.comment_id}, + ) assert db.session.query(model.Comment).count() == 1 @@ -56,6 +64,8 @@ def test_trying_to_delete_non_existing(user_factory, context_factory): with pytest.raises(comments.CommentNotFoundError): api.comment_api.delete_comment( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'comment_id': 1}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"comment_id": 1}, + ) diff --git a/server/szurubooru/tests/api/test_comment_rating.py b/server/szurubooru/tests/api/test_comment_rating.py index aae5e24..efb4bc2 100644 --- a/server/szurubooru/tests/api/test_comment_rating.py +++ b/server/szurubooru/tests/api/test_comment_rating.py @@ -1,116 +1,134 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import comments @pytest.fixture(autouse=True) def inject_config(config_injector): config_injector( - {'privileges': {'comments:score': model.User.RANK_REGULAR}}) + {"privileges": {"comments:score": model.User.RANK_REGULAR}} + ) def test_simple_rating( - user_factory, comment_factory, context_factory, fake_datetime): + user_factory, comment_factory, context_factory, fake_datetime +): user = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user) db.session.add(comment) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'), \ - fake_datetime('1997-12-01'): - comments.serialize_comment.return_value = 'serialized comment' + with patch("szurubooru.func.comments.serialize_comment"), fake_datetime( + "1997-12-01" + ): + comments.serialize_comment.return_value = "serialized comment" result = api.comment_api.set_comment_score( - context_factory(params={'score': 1}, user=user), - {'comment_id': comment.comment_id}) - assert result == 'serialized comment' + context_factory(params={"score": 1}, user=user), + {"comment_id": comment.comment_id}, + ) + assert result == "serialized comment" assert db.session.query(model.CommentScore).count() == 1 assert comment is not None assert comment.score == 1 def test_updating_rating( - user_factory, comment_factory, context_factory, fake_datetime): + user_factory, comment_factory, context_factory, fake_datetime +): user = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user) db.session.add(comment) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.comments.serialize_comment"): + with fake_datetime("1997-12-01"): api.comment_api.set_comment_score( - context_factory(params={'score': 1}, user=user), - {'comment_id': comment.comment_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user), + {"comment_id": comment.comment_id}, + ) + with fake_datetime("1997-12-02"): api.comment_api.set_comment_score( - context_factory(params={'score': -1}, user=user), - {'comment_id': comment.comment_id}) + context_factory(params={"score": -1}, user=user), + {"comment_id": comment.comment_id}, + ) comment = db.session.query(model.Comment).one() assert db.session.query(model.CommentScore).count() == 1 assert comment.score == -1 def test_updating_rating_to_zero( - user_factory, comment_factory, context_factory, fake_datetime): + user_factory, comment_factory, context_factory, fake_datetime +): user = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user) db.session.add(comment) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.comments.serialize_comment"): + with fake_datetime("1997-12-01"): api.comment_api.set_comment_score( - context_factory(params={'score': 1}, user=user), - {'comment_id': comment.comment_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user), + {"comment_id": comment.comment_id}, + ) + with fake_datetime("1997-12-02"): api.comment_api.set_comment_score( - context_factory(params={'score': 0}, user=user), - {'comment_id': comment.comment_id}) + context_factory(params={"score": 0}, user=user), + {"comment_id": comment.comment_id}, + ) comment = db.session.query(model.Comment).one() assert db.session.query(model.CommentScore).count() == 0 assert comment.score == 0 def test_deleting_rating( - user_factory, comment_factory, context_factory, fake_datetime): + user_factory, comment_factory, context_factory, fake_datetime +): user = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user) db.session.add(comment) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.comments.serialize_comment"): + with fake_datetime("1997-12-01"): api.comment_api.set_comment_score( - context_factory(params={'score': 1}, user=user), - {'comment_id': comment.comment_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user), + {"comment_id": comment.comment_id}, + ) + with fake_datetime("1997-12-02"): api.comment_api.delete_comment_score( - context_factory(user=user), - {'comment_id': comment.comment_id}) + context_factory(user=user), {"comment_id": comment.comment_id} + ) comment = db.session.query(model.Comment).one() assert db.session.query(model.CommentScore).count() == 0 assert comment.score == 0 def test_ratings_from_multiple_users( - user_factory, comment_factory, context_factory, fake_datetime): + user_factory, comment_factory, context_factory, fake_datetime +): user1 = user_factory(rank=model.User.RANK_REGULAR) user2 = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory() db.session.add_all([user1, user2, comment]) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.comments.serialize_comment"): + with fake_datetime("1997-12-01"): api.comment_api.set_comment_score( - context_factory(params={'score': 1}, user=user1), - {'comment_id': comment.comment_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user1), + {"comment_id": comment.comment_id}, + ) + with fake_datetime("1997-12-02"): api.comment_api.set_comment_score( - context_factory(params={'score': -1}, user=user2), - {'comment_id': comment.comment_id}) + context_factory(params={"score": -1}, user=user2), + {"comment_id": comment.comment_id}, + ) comment = db.session.query(model.Comment).one() assert db.session.query(model.CommentScore).count() == 2 assert comment.score == 0 def test_trying_to_omit_mandatory_field( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): user = user_factory() comment = comment_factory(user=user) db.session.add(comment) @@ -118,26 +136,32 @@ def test_trying_to_omit_mandatory_field( with pytest.raises(errors.ValidationError): api.comment_api.set_comment_score( context_factory(params={}, user=user), - {'comment_id': comment.comment_id}) + {"comment_id": comment.comment_id}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(comments.CommentNotFoundError): api.comment_api.set_comment_score( context_factory( - params={'score': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'comment_id': 5}) + params={"score": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"comment_id": 5}, + ) def test_trying_to_rate_without_privileges( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): comment = comment_factory() db.session.add(comment) db.session.commit() with pytest.raises(errors.AuthError): api.comment_api.set_comment_score( context_factory( - params={'score': 1}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'comment_id': comment.comment_id}) + params={"score": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"comment_id": comment.comment_id}, + ) diff --git a/server/szurubooru/tests/api/test_comment_retrieving.py b/server/szurubooru/tests/api/test_comment_retrieving.py index 5c846bb..404af76 100644 --- a/server/szurubooru/tests/api/test_comment_retrieving.py +++ b/server/szurubooru/tests/api/test_comment_retrieving.py @@ -1,72 +1,83 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import comments @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'comments:list': model.User.RANK_REGULAR, - 'comments:view': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "comments:list": model.User.RANK_REGULAR, + "comments:view": model.User.RANK_REGULAR, + }, + } + ) def test_retrieving_multiple(user_factory, comment_factory, context_factory): - comment1 = comment_factory(text='text 1') - comment2 = comment_factory(text='text 2') + comment1 = comment_factory(text="text 1") + comment2 = comment_factory(text="text 2") db.session.add_all([comment1, comment2]) db.session.flush() - with patch('szurubooru.func.comments.serialize_comment'): - comments.serialize_comment.return_value = 'serialized comment' + with patch("szurubooru.func.comments.serialize_comment"): + comments.serialize_comment.return_value = "serialized comment" result = api.comment_api.get_comments( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) assert result == { - 'query': '', - 'offset': 0, - 'limit': 100, - 'total': 2, - 'results': ['serialized comment', 'serialized comment'], + "query": "", + "offset": 0, + "limit": 100, + "total": 2, + "results": ["serialized comment", "serialized comment"], } def test_trying_to_retrieve_multiple_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.comment_api.get_comments( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) def test_retrieving_single(user_factory, comment_factory, context_factory): - comment = comment_factory(text='dummy text') + comment = comment_factory(text="dummy text") db.session.add(comment) db.session.flush() - with patch('szurubooru.func.comments.serialize_comment'): - comments.serialize_comment.return_value = 'serialized comment' + with patch("szurubooru.func.comments.serialize_comment"): + comments.serialize_comment.return_value = "serialized comment" result = api.comment_api.get_comment( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR)), - {'comment_id': comment.comment_id}) - assert result == 'serialized comment' + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"comment_id": comment.comment_id}, + ) + assert result == "serialized comment" def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): with pytest.raises(comments.CommentNotFoundError): api.comment_api.get_comment( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR)), - {'comment_id': 5}) + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"comment_id": 5}, + ) def test_trying_to_retrieve_single_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.comment_api.get_comment( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'comment_id': 5}) + {"comment_id": 5}, + ) diff --git a/server/szurubooru/tests/api/test_comment_updating.py b/server/szurubooru/tests/api/test_comment_updating.py index 761b1ce..e7c4fbe 100644 --- a/server/szurubooru/tests/api/test_comment_updating.py +++ b/server/szurubooru/tests/api/test_comment_updating.py @@ -1,84 +1,97 @@ from datetime import datetime from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import comments @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'comments:edit:own': model.User.RANK_REGULAR, - 'comments:edit:any': model.User.RANK_MODERATOR, - }, - }) + config_injector( + { + "privileges": { + "comments:edit:own": model.User.RANK_REGULAR, + "comments:edit:any": model.User.RANK_MODERATOR, + }, + } + ) def test_simple_updating( - user_factory, comment_factory, context_factory, fake_datetime): + user_factory, comment_factory, context_factory, fake_datetime +): user = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user) db.session.add(comment) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'), \ - fake_datetime('1997-12-01'): - comments.serialize_comment.return_value = 'serialized comment' + with patch("szurubooru.func.comments.serialize_comment"), fake_datetime( + "1997-12-01" + ): + comments.serialize_comment.return_value = "serialized comment" result = api.comment_api.update_comment( context_factory( - params={'text': 'new text', 'version': 1}, user=user), - {'comment_id': comment.comment_id}) - assert result == 'serialized comment' + params={"text": "new text", "version": 1}, user=user + ), + {"comment_id": comment.comment_id}, + ) + assert result == "serialized comment" assert comment.last_edit_time == datetime(1997, 12, 1) -@pytest.mark.parametrize('params,expected_exception', [ - ({'text': None}, comments.EmptyCommentTextError), - ({'text': ''}, comments.EmptyCommentTextError), - ({'text': []}, comments.EmptyCommentTextError), - ({'text': [None]}, errors.ValidationError), - ({'text': ['']}, comments.EmptyCommentTextError), -]) +@pytest.mark.parametrize( + "params,expected_exception", + [ + ({"text": None}, comments.EmptyCommentTextError), + ({"text": ""}, comments.EmptyCommentTextError), + ({"text": []}, comments.EmptyCommentTextError), + ({"text": [None]}, errors.ValidationError), + ({"text": [""]}, comments.EmptyCommentTextError), + ], +) def test_trying_to_pass_invalid_params( - user_factory, - comment_factory, - context_factory, - params, - expected_exception): + user_factory, comment_factory, context_factory, params, expected_exception +): user = user_factory() comment = comment_factory(user=user) db.session.add(comment) db.session.commit() with pytest.raises(expected_exception): api.comment_api.update_comment( - context_factory( - params={**params, **{'version': 1}}, user=user), - {'comment_id': comment.comment_id}) + context_factory(params={**params, **{"version": 1}}, user=user), + {"comment_id": comment.comment_id}, + ) def test_trying_to_omit_mandatory_field( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): user = user_factory() comment = comment_factory(user=user) db.session.add(comment) db.session.commit() with pytest.raises(errors.ValidationError): api.comment_api.update_comment( - context_factory(params={'version': 1}, user=user), - {'comment_id': comment.comment_id}) + context_factory(params={"version": 1}, user=user), + {"comment_id": comment.comment_id}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(comments.CommentNotFoundError): api.comment_api.update_comment( context_factory( - params={'text': 'new text'}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'comment_id': 5}) + params={"text": "new text"}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"comment_id": 5}, + ) def test_trying_to_update_someones_comment_without_privileges( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): user = user_factory(rank=model.User.RANK_REGULAR) user2 = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user) @@ -87,19 +100,24 @@ def test_trying_to_update_someones_comment_without_privileges( with pytest.raises(errors.AuthError): api.comment_api.update_comment( context_factory( - params={'text': 'new text', 'version': 1}, user=user2), - {'comment_id': comment.comment_id}) + params={"text": "new text", "version": 1}, user=user2 + ), + {"comment_id": comment.comment_id}, + ) def test_updating_someones_comment_with_privileges( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): user = user_factory(rank=model.User.RANK_REGULAR) user2 = user_factory(rank=model.User.RANK_MODERATOR) comment = comment_factory(user=user) db.session.add(comment) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'): + with patch("szurubooru.func.comments.serialize_comment"): api.comment_api.update_comment( context_factory( - params={'text': 'new text', 'version': 1}, user=user2), - {'comment_id': comment.comment_id}) + params={"text": "new text", "version": 1}, user=user2 + ), + {"comment_id": comment.comment_id}, + ) diff --git a/server/szurubooru/tests/api/test_info.py b/server/szurubooru/tests/api/test_info.py index cd15727..57bf7a3 100644 --- a/server/szurubooru/tests/api/test_info.py +++ b/server/szurubooru/tests/api/test_info.py @@ -1,88 +1,94 @@ from datetime import datetime + from szurubooru import api, db, model def test_info_api( - tmpdir, config_injector, context_factory, post_factory, user_factory, - fake_datetime): - directory = tmpdir.mkdir('data') - directory.join('test.txt').write('abc') + tmpdir, + config_injector, + context_factory, + post_factory, + user_factory, + fake_datetime, +): + directory = tmpdir.mkdir("data") + directory.join("test.txt").write("abc") auth_user = user_factory(rank=model.User.RANK_REGULAR) anon_user = user_factory(rank=model.User.RANK_ANONYMOUS) - config_injector({ - 'name': 'test installation', - 'contact_email': 'test@example.com', - 'enable_safety': True, - 'data_dir': str(directory), - 'user_name_regex': '1', - 'password_regex': '2', - 'tag_name_regex': '3', - 'tag_category_name_regex': '4', - 'default_rank': '5', - 'privileges': { - 'test_key1': 'test_value1', - 'test_key2': 'test_value2', - 'posts:view:featured': 'regular', - }, - 'smtp': { - 'host': 'example.com', + config_injector( + { + "name": "test installation", + "contact_email": "test@example.com", + "enable_safety": True, + "data_dir": str(directory), + "user_name_regex": "1", + "password_regex": "2", + "tag_name_regex": "3", + "tag_category_name_regex": "4", + "default_rank": "5", + "privileges": { + "test_key1": "test_value1", + "test_key2": "test_value2", + "posts:view:featured": "regular", + }, + "smtp": {"host": "example.com",}, } - }) + ) db.session.add_all([post_factory(), post_factory()]) db.session.flush() expected_config_key = { - 'name': 'test installation', - 'contactEmail': 'test@example.com', - 'enableSafety': True, - 'userNameRegex': '1', - 'passwordRegex': '2', - 'tagNameRegex': '3', - 'tagCategoryNameRegex': '4', - 'defaultUserRank': '5', - 'privileges': { - 'testKey1': 'test_value1', - 'testKey2': 'test_value2', - 'posts:view:featured': 'regular', + "name": "test installation", + "contactEmail": "test@example.com", + "enableSafety": True, + "userNameRegex": "1", + "passwordRegex": "2", + "tagNameRegex": "3", + "tagCategoryNameRegex": "4", + "defaultUserRank": "5", + "privileges": { + "testKey1": "test_value1", + "testKey2": "test_value2", + "posts:view:featured": "regular", }, - 'canSendMails': True + "canSendMails": True, } - with fake_datetime('2016-01-01 13:00'): + with fake_datetime("2016-01-01 13:00"): assert api.info_api.get_info(context_factory(user=auth_user)) == { - 'postCount': 2, - 'diskUsage': 3, - 'featuredPost': None, - 'featuringTime': None, - 'featuringUser': None, - 'serverTime': datetime(2016, 1, 1, 13, 0), - 'config': expected_config_key, + "postCount": 2, + "diskUsage": 3, + "featuredPost": None, + "featuringTime": None, + "featuringUser": None, + "serverTime": datetime(2016, 1, 1, 13, 0), + "config": expected_config_key, } - directory.join('test2.txt').write('abc') - with fake_datetime('2016-01-03 12:59'): + directory.join("test2.txt").write("abc") + with fake_datetime("2016-01-03 12:59"): assert api.info_api.get_info(context_factory(user=auth_user)) == { - 'postCount': 2, - 'diskUsage': 3, # still 3 - it's cached - 'featuredPost': None, - 'featuringTime': None, - 'featuringUser': None, - 'serverTime': datetime(2016, 1, 3, 12, 59), - 'config': expected_config_key, + "postCount": 2, + "diskUsage": 3, # still 3 - it's cached + "featuredPost": None, + "featuringTime": None, + "featuringUser": None, + "serverTime": datetime(2016, 1, 3, 12, 59), + "config": expected_config_key, } - with fake_datetime('2016-01-03 13:01'): + with fake_datetime("2016-01-03 13:01"): assert api.info_api.get_info(context_factory(user=auth_user)) == { - 'postCount': 2, - 'diskUsage': 6, # cache expired - 'featuredPost': None, - 'featuringTime': None, - 'featuringUser': None, - 'serverTime': datetime(2016, 1, 3, 13, 1), - 'config': expected_config_key, + "postCount": 2, + "diskUsage": 6, # cache expired + "featuredPost": None, + "featuringTime": None, + "featuringUser": None, + "serverTime": datetime(2016, 1, 3, 13, 1), + "config": expected_config_key, } - with fake_datetime('2016-01-03 13:01'): + with fake_datetime("2016-01-03 13:01"): assert api.info_api.get_info(context_factory(user=anon_user)) == { - 'postCount': 2, - 'diskUsage': 6, # cache expired - 'serverTime': datetime(2016, 1, 3, 13, 1), - 'config': expected_config_key, + "postCount": 2, + "diskUsage": 6, # cache expired + "serverTime": datetime(2016, 1, 3, 13, 1), + "config": expected_config_key, } diff --git a/server/szurubooru/tests/api/test_password_reset.py b/server/szurubooru/tests/api/test_password_reset.py index 29917a0..28e4c12 100644 --- a/server/szurubooru/tests/api/test_password_reset.py +++ b/server/szurubooru/tests/api/test_password_reset.py @@ -1,87 +1,112 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import auth, mailer @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'secret': 'x', - 'domain': 'http://example.com', - 'name': 'Test instance', - 'smtp': { - 'from': 'noreply@example.com', - }, - }) + config_injector( + { + "secret": "x", + "domain": "http://example.com", + "name": "Test instance", + "smtp": {"from": "noreply@example.com",}, + } + ) def test_reset_sending_email(context_factory, user_factory): - db.session.add(user_factory( - name='u1', rank=model.User.RANK_REGULAR, email='user@example.com')) + db.session.add( + user_factory( + name="u1", rank=model.User.RANK_REGULAR, email="user@example.com" + ) + ) db.session.flush() - for initiating_user in ['u1', 'user@example.com']: - with patch('szurubooru.func.mailer.send_mail'): - assert api.password_reset_api.start_password_reset( - context_factory(), {'user_name': initiating_user}) == {} + for initiating_user in ["u1", "user@example.com"]: + with patch("szurubooru.func.mailer.send_mail"): + assert ( + api.password_reset_api.start_password_reset( + context_factory(), {"user_name": initiating_user} + ) + == {} + ) mailer.send_mail.assert_called_once_with( - 'noreply@example.com', - 'user@example.com', - 'Password reset for Test instance', - 'You (or someone else) requested to reset your password ' + - 'on Test instance.\nIf you wish to proceed, click this l' + - 'ink: http://example.com/password-reset/u1:4ac0be176fb36' + - '4f13ee6b634c43220e2\nOtherwise, please ignore this email.') + "noreply@example.com", + "user@example.com", + "Password reset for Test instance", + "You (or someone else) requested to reset your password " + + "on Test instance.\nIf you wish to proceed, click this l" + + "ink: http://example.com/password-reset/u1:4ac0be176fb36" + + "4f13ee6b634c43220e2\nOtherwise, please ignore this email.", + ) def test_trying_to_reset_non_existing(context_factory): with pytest.raises(errors.NotFoundError): api.password_reset_api.start_password_reset( - context_factory(), {'user_name': 'u1'}) + context_factory(), {"user_name": "u1"} + ) def test_trying_to_reset_without_email(context_factory, user_factory): db.session.add( - user_factory(name='u1', rank=model.User.RANK_REGULAR, email=None)) + user_factory(name="u1", rank=model.User.RANK_REGULAR, email=None) + ) db.session.flush() with pytest.raises(errors.ValidationError): api.password_reset_api.start_password_reset( - context_factory(), {'user_name': 'u1'}) + context_factory(), {"user_name": "u1"} + ) def test_confirming_with_good_token(context_factory, user_factory): user = user_factory( - name='u1', rank=model.User.RANK_REGULAR, email='user@example.com') + name="u1", rank=model.User.RANK_REGULAR, email="user@example.com" + ) old_hash = user.password_hash db.session.add(user) db.session.flush() context = context_factory( - params={'token': '4ac0be176fb364f13ee6b634c43220e2'}) + params={"token": "4ac0be176fb364f13ee6b634c43220e2"} + ) result = api.password_reset_api.finish_password_reset( - context, {'user_name': 'u1'}) + context, {"user_name": "u1"} + ) assert user.password_hash != old_hash - assert auth.is_valid_password(user, result['password']) is True + assert auth.is_valid_password(user, result["password"]) is True def test_trying_to_confirm_non_existing(context_factory): with pytest.raises(errors.NotFoundError): api.password_reset_api.finish_password_reset( - context_factory(), {'user_name': 'u1'}) + context_factory(), {"user_name": "u1"} + ) def test_trying_to_confirm_without_token(context_factory, user_factory): - db.session.add(user_factory( - name='u1', rank=model.User.RANK_REGULAR, email='user@example.com')) + db.session.add( + user_factory( + name="u1", rank=model.User.RANK_REGULAR, email="user@example.com" + ) + ) db.session.flush() with pytest.raises(errors.ValidationError): api.password_reset_api.finish_password_reset( - context_factory(params={}), {'user_name': 'u1'}) + context_factory(params={}), {"user_name": "u1"} + ) def test_trying_to_confirm_with_bad_token(context_factory, user_factory): - db.session.add(user_factory( - name='u1', rank=model.User.RANK_REGULAR, email='user@example.com')) + db.session.add( + user_factory( + name="u1", rank=model.User.RANK_REGULAR, email="user@example.com" + ) + ) db.session.flush() with pytest.raises(errors.ValidationError): api.password_reset_api.finish_password_reset( - context_factory(params={'token': 'bad'}), {'user_name': 'u1'}) + context_factory(params={"token": "bad"}), {"user_name": "u1"} + ) diff --git a/server/szurubooru/tests/api/test_pool_category_creating.py b/server/szurubooru/tests/api/test_pool_category_creating.py index 5e932a6..c13c1ec 100644 --- a/server/szurubooru/tests/api/test_pool_category_creating.py +++ b/server/szurubooru/tests/api/test_pool_category_creating.py @@ -1,6 +1,8 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import pool_categories, snapshots @@ -10,51 +12,60 @@ def _update_category_name(category, name): @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': {'pool_categories:create': model.User.RANK_REGULAR}, - }) + config_injector( + {"privileges": {"pool_categories:create": model.User.RANK_REGULAR},} + ) def test_creating_category( - pool_category_factory, user_factory, context_factory): + pool_category_factory, user_factory, context_factory +): auth_user = user_factory(rank=model.User.RANK_REGULAR) - category = pool_category_factory(name='meta') + category = pool_category_factory(name="meta") db.session.add(category) - with patch('szurubooru.func.pool_categories.create_category'), \ - patch('szurubooru.func.pool_categories.serialize_category'), \ - patch('szurubooru.func.pool_categories.update_category_name'), \ - patch('szurubooru.func.snapshots.create'): + with patch("szurubooru.func.pool_categories.create_category"), patch( + "szurubooru.func.pool_categories.serialize_category" + ), patch("szurubooru.func.pool_categories.update_category_name"), patch( + "szurubooru.func.snapshots.create" + ): pool_categories.create_category.return_value = category - pool_categories.update_category_name.side_effect = \ + pool_categories.update_category_name.side_effect = ( _update_category_name - pool_categories.serialize_category.return_value = 'serialized category' + ) + pool_categories.serialize_category.return_value = "serialized category" result = api.pool_category_api.create_pool_category( context_factory( - params={'name': 'meta', 'color': 'black'}, user=auth_user)) - assert result == 'serialized category' + params={"name": "meta", "color": "black"}, user=auth_user + ) + ) + assert result == "serialized category" pool_categories.create_category.assert_called_once_with( - 'meta', 'black') + "meta", "black" + ) snapshots.create.assert_called_once_with(category, auth_user) -@pytest.mark.parametrize('field', ['name', 'color']) +@pytest.mark.parametrize("field", ["name", "color"]) def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): params = { - 'name': 'meta', - 'color': 'black', + "name": "meta", + "color": "black", } del params[field] with pytest.raises(errors.ValidationError): api.pool_category_api.create_pool_category( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) def test_trying_to_create_without_privileges(user_factory, context_factory): with pytest.raises(errors.AuthError): api.pool_category_api.create_pool_category( context_factory( - params={'name': 'meta', 'color': 'black'}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"name": "meta", "color": "black"}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_pool_category_deleting.py b/server/szurubooru/tests/api/test_pool_category_deleting.py index 72853ac..cdbdcfe 100644 --- a/server/szurubooru/tests/api/test_pool_category_deleting.py +++ b/server/szurubooru/tests/api/test_pool_category_deleting.py @@ -1,76 +1,89 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import pool_categories, snapshots @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': {'pool_categories:delete': model.User.RANK_REGULAR}, - }) + config_injector( + {"privileges": {"pool_categories:delete": model.User.RANK_REGULAR},} + ) def test_deleting(user_factory, pool_category_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) - category = pool_category_factory(name='category') - db.session.add(pool_category_factory(name='root')) + category = pool_category_factory(name="category") + db.session.add(pool_category_factory(name="root")) db.session.add(category) db.session.flush() - with patch('szurubooru.func.snapshots.delete'): + with patch("szurubooru.func.snapshots.delete"): result = api.pool_category_api.delete_pool_category( - context_factory(params={'version': 1}, user=auth_user), - {'category_name': 'category'}) + context_factory(params={"version": 1}, user=auth_user), + {"category_name": "category"}, + ) assert result == {} assert db.session.query(model.PoolCategory).count() == 1 - assert db.session.query(model.PoolCategory).one().name == 'root' + assert db.session.query(model.PoolCategory).one().name == "root" snapshots.delete.assert_called_once_with(category, auth_user) def test_trying_to_delete_used( - user_factory, pool_category_factory, pool_factory, context_factory): - category = pool_category_factory(name='category') + user_factory, pool_category_factory, pool_factory, context_factory +): + category = pool_category_factory(name="category") db.session.add(category) db.session.flush() - pool = pool_factory(names=['pool'], category=category) + pool = pool_factory(names=["pool"], category=category) db.session.add(pool) db.session.commit() with pytest.raises(pool_categories.PoolCategoryIsInUseError): api.pool_category_api.delete_pool_category( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'category'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "category"}, + ) assert db.session.query(model.PoolCategory).count() == 1 def test_trying_to_delete_last( - user_factory, pool_category_factory, context_factory): - db.session.add(pool_category_factory(name='root')) + user_factory, pool_category_factory, context_factory +): + db.session.add(pool_category_factory(name="root")) db.session.commit() with pytest.raises(pool_categories.PoolCategoryIsInUseError): api.pool_category_api.delete_pool_category( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'root'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "root"}, + ) def test_trying_to_delete_non_existing(user_factory, context_factory): with pytest.raises(pool_categories.PoolCategoryNotFoundError): api.pool_category_api.delete_pool_category( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'bad'}) + {"category_name": "bad"}, + ) def test_trying_to_delete_without_privileges( - user_factory, pool_category_factory, context_factory): - db.session.add(pool_category_factory(name='category')) + user_factory, pool_category_factory, context_factory +): + db.session.add(pool_category_factory(name="category")) db.session.commit() with pytest.raises(errors.AuthError): api.pool_category_api.delete_pool_category( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'category_name': 'category'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"category_name": "category"}, + ) assert db.session.query(model.PoolCategory).count() == 1 diff --git a/server/szurubooru/tests/api/test_pool_category_retrieving.py b/server/szurubooru/tests/api/test_pool_category_retrieving.py index 4a467c0..ea837c4 100644 --- a/server/szurubooru/tests/api/test_pool_category_retrieving.py +++ b/server/szurubooru/tests/api/test_pool_category_retrieving.py @@ -1,43 +1,49 @@ import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import pool_categories @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'pool_categories:list': model.User.RANK_REGULAR, - 'pool_categories:view': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "pool_categories:list": model.User.RANK_REGULAR, + "pool_categories:view": model.User.RANK_REGULAR, + }, + } + ) def test_retrieving_multiple( - user_factory, pool_category_factory, context_factory): - db.session.add_all([ - pool_category_factory(name='c1'), - pool_category_factory(name='c2'), - ]) + user_factory, pool_category_factory, context_factory +): + db.session.add_all( + [pool_category_factory(name="c1"), pool_category_factory(name="c2"),] + ) db.session.flush() result = api.pool_category_api.get_pool_categories( - context_factory(user=user_factory(rank=model.User.RANK_REGULAR))) - assert [cat['name'] for cat in result['results']] == ['c1', 'c2'] + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)) + ) + assert [cat["name"] for cat in result["results"]] == ["c1", "c2"] def test_retrieving_single( - user_factory, pool_category_factory, context_factory): - db.session.add(pool_category_factory(name='cat')) + user_factory, pool_category_factory, context_factory +): + db.session.add(pool_category_factory(name="cat")) db.session.flush() result = api.pool_category_api.get_pool_category( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'cat'}) + {"category_name": "cat"}, + ) assert result == { - 'name': 'cat', - 'color': 'dummy', - 'usages': 0, - 'default': False, - 'version': 1, + "name": "cat", + "color": "dummy", + "usages": 0, + "default": False, + "version": 1, } @@ -45,12 +51,15 @@ def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): with pytest.raises(pool_categories.PoolCategoryNotFoundError): api.pool_category_api.get_pool_category( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': '-'}) + {"category_name": "-"}, + ) def test_trying_to_retrieve_single_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.pool_category_api.get_pool_category( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'category_name': '-'}) + {"category_name": "-"}, + ) diff --git a/server/szurubooru/tests/api/test_pool_category_updating.py b/server/szurubooru/tests/api/test_pool_category_updating.py index 028c520..d934e60 100644 --- a/server/szurubooru/tests/api/test_pool_category_updating.py +++ b/server/szurubooru/tests/api/test_pool_category_updating.py @@ -1,6 +1,8 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import pool_categories, snapshots @@ -10,101 +12,117 @@ def _update_category_name(category, name): @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'pool_categories:edit:name': model.User.RANK_REGULAR, - 'pool_categories:edit:color': model.User.RANK_REGULAR, - 'pool_categories:set_default': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "pool_categories:edit:name": model.User.RANK_REGULAR, + "pool_categories:edit:color": model.User.RANK_REGULAR, + "pool_categories:set_default": model.User.RANK_REGULAR, + }, + } + ) def test_simple_updating(user_factory, pool_category_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) - category = pool_category_factory(name='name', color='black') + category = pool_category_factory(name="name", color="black") db.session.add(category) db.session.flush() - with patch('szurubooru.func.pool_categories.serialize_category'), \ - patch('szurubooru.func.pool_categories.update_category_name'), \ - patch('szurubooru.func.pool_categories.update_category_color'), \ - patch('szurubooru.func.snapshots.modify'): - pool_categories.update_category_name.side_effect = \ + with patch("szurubooru.func.pool_categories.serialize_category"), patch( + "szurubooru.func.pool_categories.update_category_name" + ), patch("szurubooru.func.pool_categories.update_category_color"), patch( + "szurubooru.func.snapshots.modify" + ): + pool_categories.update_category_name.side_effect = ( _update_category_name - pool_categories.serialize_category.return_value = 'serialized category' + ) + pool_categories.serialize_category.return_value = "serialized category" result = api.pool_category_api.update_pool_category( context_factory( - params={'name': 'changed', 'color': 'white', 'version': 1}, - user=auth_user), - {'category_name': 'name'}) - assert result == 'serialized category' + params={"name": "changed", "color": "white", "version": 1}, + user=auth_user, + ), + {"category_name": "name"}, + ) + assert result == "serialized category" pool_categories.update_category_name.assert_called_once_with( - category, 'changed') + category, "changed" + ) pool_categories.update_category_color.assert_called_once_with( - category, 'white') + category, "white" + ) snapshots.modify.assert_called_once_with(category, auth_user) -@pytest.mark.parametrize('field', ['name', 'color']) +@pytest.mark.parametrize("field", ["name", "color"]) def test_omitting_optional_field( - user_factory, pool_category_factory, context_factory, field): - db.session.add(pool_category_factory(name='name', color='black')) + user_factory, pool_category_factory, context_factory, field +): + db.session.add(pool_category_factory(name="name", color="black")) db.session.commit() params = { - 'name': 'changed', - 'color': 'white', + "name": "changed", + "color": "white", } del params[field] - with patch('szurubooru.func.pool_categories.serialize_category'), \ - patch('szurubooru.func.pool_categories.update_category_name'): + with patch("szurubooru.func.pool_categories.serialize_category"), patch( + "szurubooru.func.pool_categories.update_category_name" + ): api.pool_category_api.update_pool_category( context_factory( - params={**params, **{'version': 1}}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'name'}) + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "name"}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(pool_categories.PoolCategoryNotFoundError): api.pool_category_api.update_pool_category( context_factory( - params={'name': ['dummy']}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'bad'}) + params={"name": ["dummy"]}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "bad"}, + ) -@pytest.mark.parametrize('params', [ - {'name': 'whatever'}, - {'color': 'whatever'}, -]) +@pytest.mark.parametrize( + "params", [{"name": "whatever"}, {"color": "whatever"},] +) def test_trying_to_update_without_privileges( - user_factory, pool_category_factory, context_factory, params): - db.session.add(pool_category_factory(name='dummy')) + user_factory, pool_category_factory, context_factory, params +): + db.session.add(pool_category_factory(name="dummy")) db.session.commit() with pytest.raises(errors.AuthError): api.pool_category_api.update_pool_category( context_factory( - params={**params, **{'version': 1}}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'category_name': 'dummy'}) + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"category_name": "dummy"}, + ) def test_set_as_default(user_factory, pool_category_factory, context_factory): - category = pool_category_factory(name='name', color='black') + category = pool_category_factory(name="name", color="black") db.session.add(category) db.session.commit() - with patch('szurubooru.func.pool_categories.serialize_category'), \ - patch('szurubooru.func.pool_categories.set_default_category'): - pool_categories.update_category_name.side_effect = \ + with patch("szurubooru.func.pool_categories.serialize_category"), patch( + "szurubooru.func.pool_categories.set_default_category" + ): + pool_categories.update_category_name.side_effect = ( _update_category_name - pool_categories.serialize_category.return_value = 'serialized category' + ) + pool_categories.serialize_category.return_value = "serialized category" result = api.pool_category_api.set_pool_category_as_default( context_factory( - params={ - 'name': 'changed', - 'color': 'white', - 'version': 1, - }, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'name'}) - assert result == 'serialized category' + params={"name": "changed", "color": "white", "version": 1,}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "name"}, + ) + assert result == "serialized category" pool_categories.set_default_category.assert_called_once_with(category) diff --git a/server/szurubooru/tests/api/test_pool_creating.py b/server/szurubooru/tests/api/test_pool_creating.py index 5eeb2e2..a9f1473 100644 --- a/server/szurubooru/tests/api/test_pool_creating.py +++ b/server/szurubooru/tests/api/test_pool_creating.py @@ -1,82 +1,95 @@ from unittest.mock import patch + import pytest -from szurubooru import api, model, errors + +from szurubooru import api, errors, model from szurubooru.func import pools, posts, snapshots @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'pools:create': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"pools:create": model.User.RANK_REGULAR}}) def test_creating_simple_pools(pool_factory, user_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) pool = pool_factory() - with patch('szurubooru.func.pools.create_pool'), \ - patch('szurubooru.func.pools.get_or_create_pools_by_names'), \ - patch('szurubooru.func.pools.serialize_pool'), \ - patch('szurubooru.func.snapshots.create'): + with patch("szurubooru.func.pools.create_pool"), patch( + "szurubooru.func.pools.get_or_create_pools_by_names" + ), patch("szurubooru.func.pools.serialize_pool"), patch( + "szurubooru.func.snapshots.create" + ): posts.get_posts_by_ids.return_value = ([], []) pools.create_pool.return_value = pool - pools.serialize_pool.return_value = 'serialized pool' + pools.serialize_pool.return_value = "serialized pool" result = api.pool_api.create_pool( context_factory( params={ - 'names': ['pool1', 'pool2'], - 'category': 'default', - 'description': 'desc', - 'posts': [1, 2], + "names": ["pool1", "pool2"], + "category": "default", + "description": "desc", + "posts": [1, 2], }, - user=auth_user)) - assert result == 'serialized pool' + user=auth_user, + ) + ) + assert result == "serialized pool" pools.create_pool.assert_called_once_with( - ['pool1', 'pool2'], 'default', [1, 2]) + ["pool1", "pool2"], "default", [1, 2] + ) snapshots.create.assert_called_once_with(pool, auth_user) -@pytest.mark.parametrize('field', ['names', 'category']) +@pytest.mark.parametrize("field", ["names", "category"]) def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): params = { - 'names': ['pool1', 'pool2'], - 'category': 'default', - 'description': 'desc', - 'posts': [], + "names": ["pool1", "pool2"], + "category": "default", + "description": "desc", + "posts": [], } del params[field] with pytest.raises(errors.ValidationError): api.pool_api.create_pool( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) -@pytest.mark.parametrize('field', ['description', 'posts']) +@pytest.mark.parametrize("field", ["description", "posts"]) def test_omitting_optional_field( - pool_factory, user_factory, context_factory, field): + pool_factory, user_factory, context_factory, field +): params = { - 'names': ['pool1', 'pool2'], - 'category': 'default', - 'description': 'desc', - 'posts': [], + "names": ["pool1", "pool2"], + "category": "default", + "description": "desc", + "posts": [], } del params[field] - with patch('szurubooru.func.pools.create_pool'), \ - patch('szurubooru.func.pools.serialize_pool'): + with patch("szurubooru.func.pools.create_pool"), patch( + "szurubooru.func.pools.serialize_pool" + ): pools.create_pool.return_value = pool_factory() api.pool_api.create_pool( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) def test_trying_to_create_pool_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.pool_api.create_pool( context_factory( params={ - 'names': ['pool'], - 'category': 'default', - 'posts': [], + "names": ["pool"], + "category": "default", + "posts": [], }, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_pool_deleting.py b/server/szurubooru/tests/api/test_pool_deleting.py index e29656d..387aca1 100644 --- a/server/szurubooru/tests/api/test_pool_deleting.py +++ b/server/szurubooru/tests/api/test_pool_deleting.py @@ -1,12 +1,14 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import pools, snapshots @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'pools:delete': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"pools:delete": model.User.RANK_REGULAR}}) def test_deleting(user_factory, pool_factory, context_factory): @@ -14,17 +16,19 @@ def test_deleting(user_factory, pool_factory, context_factory): pool = pool_factory(id=1) db.session.add(pool) db.session.commit() - with patch('szurubooru.func.snapshots.delete'): + with patch("szurubooru.func.snapshots.delete"): result = api.pool_api.delete_pool( - context_factory(params={'version': 1}, user=auth_user), - {'pool_id': 1}) + context_factory(params={"version": 1}, user=auth_user), + {"pool_id": 1}, + ) assert result == {} assert db.session.query(model.Pool).count() == 0 snapshots.delete.assert_called_once_with(pool, auth_user) def test_deleting_used( - user_factory, pool_factory, context_factory, post_factory): + user_factory, pool_factory, context_factory, post_factory +): pool = pool_factory(id=1) post = post_factory(id=1) pool.posts.append(post) @@ -32,9 +36,11 @@ def test_deleting_used( db.session.commit() api.pool_api.delete_pool( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'pool_id': 1}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"pool_id": 1}, + ) db.session.refresh(post) assert db.session.query(model.Pool).count() == 0 assert db.session.query(model.PoolPost).count() == 0 @@ -45,17 +51,21 @@ def test_trying_to_delete_non_existing(user_factory, context_factory): with pytest.raises(pools.PoolNotFoundError): api.pool_api.delete_pool( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'pool_id': 9999}) + {"pool_id": 9999}, + ) def test_trying_to_delete_without_privileges( - user_factory, pool_factory, context_factory): + user_factory, pool_factory, context_factory +): db.session.add(pool_factory(id=1)) db.session.commit() with pytest.raises(errors.AuthError): api.pool_api.delete_pool( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'pool_id': 1}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"pool_id": 1}, + ) assert db.session.query(model.Pool).count() == 1 diff --git a/server/szurubooru/tests/api/test_pool_merging.py b/server/szurubooru/tests/api/test_pool_merging.py index dc462d2..d0e5532 100644 --- a/server/szurubooru/tests/api/test_pool_merging.py +++ b/server/szurubooru/tests/api/test_pool_merging.py @@ -1,12 +1,14 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import pools, snapshots @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'pools:merge': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"pools:merge": model.User.RANK_REGULAR}}) def test_merging(user_factory, pool_factory, context_factory, post_factory): @@ -23,76 +25,88 @@ def test_merging(user_factory, pool_factory, context_factory, post_factory): db.session.commit() assert source_pool.post_count == 1 assert target_pool.post_count == 0 - with patch('szurubooru.func.pools.serialize_pool'), \ - patch('szurubooru.func.pools.merge_pools'), \ - patch('szurubooru.func.snapshots.merge'): + with patch("szurubooru.func.pools.serialize_pool"), patch( + "szurubooru.func.pools.merge_pools" + ), patch("szurubooru.func.snapshots.merge"): api.pool_api.merge_pools( context_factory( params={ - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': 1, - 'mergeTo': 2, + "removeVersion": 1, + "mergeToVersion": 1, + "remove": 1, + "mergeTo": 2, }, - user=auth_user)) + user=auth_user, + ) + ) pools.merge_pools.called_once_with(source_pool, target_pool) snapshots.merge.assert_called_once_with( - source_pool, target_pool, auth_user) + source_pool, target_pool, auth_user + ) @pytest.mark.parametrize( - 'field', ['remove', 'mergeTo', 'removeVersion', 'mergeToVersion']) + "field", ["remove", "mergeTo", "removeVersion", "mergeToVersion"] +) def test_trying_to_omit_mandatory_field( - user_factory, pool_factory, context_factory, field): - db.session.add_all([ - pool_factory(id=1), - pool_factory(id=2), - ]) + user_factory, pool_factory, context_factory, field +): + db.session.add_all( + [pool_factory(id=1), pool_factory(id=2),] + ) db.session.commit() params = { - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': 1, - 'mergeTo': 2, + "removeVersion": 1, + "mergeToVersion": 1, + "remove": 1, + "mergeTo": 2, } del params[field] with pytest.raises(errors.ValidationError): api.pool_api.merge_pools( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) def test_trying_to_merge_non_existing( - user_factory, pool_factory, context_factory): + user_factory, pool_factory, context_factory +): db.session.add(pool_factory(id=1)) db.session.commit() with pytest.raises(pools.PoolNotFoundError): api.pool_api.merge_pools( context_factory( - params={'remove': 1, 'mergeTo': 9999}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"remove": 1, "mergeTo": 9999}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) with pytest.raises(pools.PoolNotFoundError): api.pool_api.merge_pools( context_factory( - params={'remove': 9999, 'mergeTo': 1}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"remove": 9999, "mergeTo": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) def test_trying_to_merge_without_privileges( - user_factory, pool_factory, context_factory): - db.session.add_all([ - pool_factory(id=1), - pool_factory(id=2), - ]) + user_factory, pool_factory, context_factory +): + db.session.add_all( + [pool_factory(id=1), pool_factory(id=2),] + ) db.session.commit() with pytest.raises(errors.AuthError): api.pool_api.merge_pools( context_factory( params={ - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': 1, - 'mergeTo': 2, + "removeVersion": 1, + "mergeToVersion": 1, + "remove": 1, + "mergeTo": 2, }, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_pool_retrieving.py b/server/szurubooru/tests/api/test_pool_retrieving.py index e48565a..688dfa7 100644 --- a/server/szurubooru/tests/api/test_pool_retrieving.py +++ b/server/szurubooru/tests/api/test_pool_retrieving.py @@ -1,17 +1,21 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import pools @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'pools:list': model.User.RANK_REGULAR, - 'pools:view': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "pools:list": model.User.RANK_REGULAR, + "pools:view": model.User.RANK_REGULAR, + }, + } + ) def test_retrieving_multiple(user_factory, pool_factory, context_factory): @@ -19,54 +23,60 @@ def test_retrieving_multiple(user_factory, pool_factory, context_factory): pool2 = pool_factory(id=2) db.session.add_all([pool2, pool1]) db.session.flush() - with patch('szurubooru.func.pools.serialize_pool'): - pools.serialize_pool.return_value = 'serialized pool' + with patch("szurubooru.func.pools.serialize_pool"): + pools.serialize_pool.return_value = "serialized pool" result = api.pool_api.get_pools( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) assert result == { - 'query': '', - 'offset': 0, - 'limit': 100, - 'total': 2, - 'results': ['serialized pool', 'serialized pool'], + "query": "", + "offset": 0, + "limit": 100, + "total": 2, + "results": ["serialized pool", "serialized pool"], } def test_trying_to_retrieve_multiple_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.pool_api.get_pools( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) def test_retrieving_single(user_factory, pool_factory, context_factory): db.session.add(pool_factory(id=1)) db.session.flush() - with patch('szurubooru.func.pools.serialize_pool'): - pools.serialize_pool.return_value = 'serialized pool' + with patch("szurubooru.func.pools.serialize_pool"): + pools.serialize_pool.return_value = "serialized pool" result = api.pool_api.get_pool( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR)), - {'pool_id': 1}) - assert result == 'serialized pool' + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"pool_id": 1}, + ) + assert result == "serialized pool" def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): with pytest.raises(pools.PoolNotFoundError): api.pool_api.get_pool( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR)), - {'pool_id': 1}) + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"pool_id": 1}, + ) def test_trying_to_retrieve_single_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.pool_api.get_pool( - context_factory( - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'pool_id': 1}) + context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), + {"pool_id": 1}, + ) diff --git a/server/szurubooru/tests/api/test_pool_updating.py b/server/szurubooru/tests/api/test_pool_updating.py index bd6b71c..aa2c09a 100644 --- a/server/szurubooru/tests/api/test_pool_updating.py +++ b/server/szurubooru/tests/api/test_pool_updating.py @@ -1,131 +1,150 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import pools, posts, snapshots @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'pools:create': model.User.RANK_REGULAR, - 'pools:edit:names': model.User.RANK_REGULAR, - 'pools:edit:category': model.User.RANK_REGULAR, - 'pools:edit:description': model.User.RANK_REGULAR, - 'pools:edit:posts': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "pools:create": model.User.RANK_REGULAR, + "pools:edit:names": model.User.RANK_REGULAR, + "pools:edit:category": model.User.RANK_REGULAR, + "pools:edit:description": model.User.RANK_REGULAR, + "pools:edit:posts": model.User.RANK_REGULAR, + }, + } + ) def test_simple_updating(user_factory, pool_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) - pool = pool_factory(id=1, names=['pool1', 'pool2']) + pool = pool_factory(id=1, names=["pool1", "pool2"]) db.session.add(pool) db.session.commit() - with patch('szurubooru.func.pools.create_pool'), \ - patch('szurubooru.func.posts.get_posts_by_ids'), \ - patch('szurubooru.func.pools.update_pool_names'), \ - patch('szurubooru.func.pools.update_pool_category_name'), \ - patch('szurubooru.func.pools.update_pool_description'), \ - patch('szurubooru.func.pools.update_pool_posts'), \ - patch('szurubooru.func.pools.serialize_pool'), \ - patch('szurubooru.func.snapshots.modify'): + with patch("szurubooru.func.pools.create_pool"), patch( + "szurubooru.func.posts.get_posts_by_ids" + ), patch("szurubooru.func.pools.update_pool_names"), patch( + "szurubooru.func.pools.update_pool_category_name" + ), patch( + "szurubooru.func.pools.update_pool_description" + ), patch( + "szurubooru.func.pools.update_pool_posts" + ), patch( + "szurubooru.func.pools.serialize_pool" + ), patch( + "szurubooru.func.snapshots.modify" + ): posts.get_posts_by_ids.return_value = ([], []) - pools.serialize_pool.return_value = 'serialized pool' + pools.serialize_pool.return_value = "serialized pool" result = api.pool_api.update_pool( context_factory( params={ - 'version': 1, - 'names': ['pool3'], - 'category': 'series', - 'description': 'desc', - 'posts': [1, 2] + "version": 1, + "names": ["pool3"], + "category": "series", + "description": "desc", + "posts": [1, 2], }, - user=auth_user), - {'pool_id': 1}) - assert result == 'serialized pool' + user=auth_user, + ), + {"pool_id": 1}, + ) + assert result == "serialized pool" pools.create_pool.assert_not_called() - pools.update_pool_names.assert_called_once_with(pool, ['pool3']) - pools.update_pool_category_name.assert_called_once_with(pool, 'series') - pools.update_pool_description.assert_called_once_with(pool, 'desc') + pools.update_pool_names.assert_called_once_with(pool, ["pool3"]) + pools.update_pool_category_name.assert_called_once_with(pool, "series") + pools.update_pool_description.assert_called_once_with(pool, "desc") pools.update_pool_posts.assert_called_once_with(pool, [1, 2]) pools.serialize_pool.assert_called_once_with(pool, options=[]) snapshots.modify.assert_called_once_with(pool, auth_user) @pytest.mark.parametrize( - 'field', [ - 'names', - 'category', - 'description', - 'posts', - ]) + "field", ["names", "category", "description", "posts",] +) def test_omitting_optional_field( - user_factory, pool_factory, context_factory, field): + user_factory, pool_factory, context_factory, field +): db.session.add(pool_factory(id=1)) db.session.commit() params = { - 'names': ['pool1', 'pool2'], - 'category': 'default', - 'description': 'desc', - 'posts': [], + "names": ["pool1", "pool2"], + "category": "default", + "description": "desc", + "posts": [], } del params[field] - with patch('szurubooru.func.pools.create_pool'), \ - patch('szurubooru.func.pools.update_pool_names'), \ - patch('szurubooru.func.pools.update_pool_category_name'), \ - patch('szurubooru.func.pools.serialize_pool'): + with patch("szurubooru.func.pools.create_pool"), patch( + "szurubooru.func.pools.update_pool_names" + ), patch("szurubooru.func.pools.update_pool_category_name"), patch( + "szurubooru.func.pools.serialize_pool" + ): api.pool_api.update_pool( context_factory( - params={**params, **{'version': 1}}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'pool_id': 1}) + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"pool_id": 1}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(pools.PoolNotFoundError): api.pool_api.update_pool( context_factory( - params={'names': ['dummy']}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'pool_id': 9999}) + params={"names": ["dummy"]}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"pool_id": 9999}, + ) -@pytest.mark.parametrize('params', [ - {'names': ['whatever']}, - {'category': 'whatever'}, - {'posts': [1]}, -]) +@pytest.mark.parametrize( + "params", + [{"names": ["whatever"]}, {"category": "whatever"}, {"posts": [1]},], +) def test_trying_to_update_without_privileges( - user_factory, pool_factory, context_factory, params): + user_factory, pool_factory, context_factory, params +): db.session.add(pool_factory(id=1)) db.session.commit() with pytest.raises(errors.AuthError): api.pool_api.update_pool( context_factory( - params={**params, **{'version': 1}}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'pool_id': 1}) + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"pool_id": 1}, + ) def test_trying_to_create_pools_without_privileges( - config_injector, context_factory, pool_factory, user_factory): + config_injector, context_factory, pool_factory, user_factory +): pool = pool_factory(id=1) db.session.add(pool) db.session.commit() config_injector( { - 'privileges': { - 'pools:create': model.User.RANK_ADMINISTRATOR, - 'pools:edit:posts': model.User.RANK_REGULAR, + "privileges": { + "pools:create": model.User.RANK_ADMINISTRATOR, + "pools:edit:posts": model.User.RANK_REGULAR, }, - 'delete_source_files': False, - }) - with patch('szurubooru.func.posts.get_posts_by_ids'): - posts.get_posts_by_ids.return_value = ([], ['new-post']) + "delete_source_files": False, + } + ) + with patch("szurubooru.func.posts.get_posts_by_ids"): + posts.get_posts_by_ids.return_value = ([], ["new-post"]) with pytest.raises(errors.AuthError): api.pool_api.create_pool( context_factory( - params={'posts': [1, 2], 'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'pool_id': 1}) + params={"posts": [1, 2], "version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"pool_id": 1}, + ) diff --git a/server/szurubooru/tests/api/test_post_creating.py b/server/szurubooru/tests/api/test_post_creating.py index dc4d51c..4e05cf1 100644 --- a/server/szurubooru/tests/api/test_post_creating.py +++ b/server/szurubooru/tests/api/test_post_creating.py @@ -1,67 +1,79 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import posts, tags, snapshots, net + +from szurubooru import api, db, errors, model +from szurubooru.func import net, posts, snapshots, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'posts:create:anonymous': model.User.RANK_REGULAR, - 'posts:create:identified': model.User.RANK_REGULAR, - 'tags:create': model.User.RANK_REGULAR, - 'uploads:use_downloader': model.User.RANK_REGULAR, - }, - 'allow_broken_uploads': False, - }) - - -def test_creating_minimal_posts( - context_factory, post_factory, user_factory): + config_injector( + { + "privileges": { + "posts:create:anonymous": model.User.RANK_REGULAR, + "posts:create:identified": model.User.RANK_REGULAR, + "tags:create": model.User.RANK_REGULAR, + "uploads:use_downloader": model.User.RANK_REGULAR, + }, + "allow_broken_uploads": False, + } + ) + + +def test_creating_minimal_posts(context_factory, post_factory, user_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_safety'), \ - patch('szurubooru.func.posts.update_post_source'), \ - patch('szurubooru.func.posts.update_post_relations'), \ - patch('szurubooru.func.posts.update_post_notes'), \ - patch('szurubooru.func.posts.update_post_flags'), \ - patch('szurubooru.func.posts.update_post_thumbnail'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.snapshots.create'): + with patch("szurubooru.func.posts.create_post"), patch( + "szurubooru.func.posts.update_post_safety" + ), patch("szurubooru.func.posts.update_post_source"), patch( + "szurubooru.func.posts.update_post_relations" + ), patch( + "szurubooru.func.posts.update_post_notes" + ), patch( + "szurubooru.func.posts.update_post_flags" + ), patch( + "szurubooru.func.posts.update_post_thumbnail" + ), patch( + "szurubooru.func.posts.serialize_post" + ), patch( + "szurubooru.func.snapshots.create" + ): posts.create_post.return_value = (post, []) - posts.serialize_post.return_value = 'serialized post' + posts.serialize_post.return_value = "serialized post" result = api.post_api.create_post( context_factory( - params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - }, + params={"safety": "safe", "tags": ["tag1", "tag2"],}, files={ - 'content': 'post-content', - 'thumbnail': 'post-thumbnail', + "content": "post-content", + "thumbnail": "post-thumbnail", }, - user=auth_user)) + user=auth_user, + ) + ) - assert result == 'serialized post' + assert result == "serialized post" posts.create_post.assert_called_once_with( - 'post-content', ['tag1', 'tag2'], auth_user) + "post-content", ["tag1", "tag2"], auth_user + ) posts.update_post_thumbnail.assert_called_once_with( - post, 'post-thumbnail') - posts.update_post_safety.assert_called_once_with(post, 'safe') - posts.update_post_source.assert_called_once_with(post, '') + post, "post-thumbnail" + ) + posts.update_post_safety.assert_called_once_with(post, "safe") + posts.update_post_source.assert_called_once_with(post, "") posts.update_post_relations.assert_called_once_with(post, []) posts.update_post_notes.assert_called_once_with(post, []) posts.update_post_flags.assert_called_once_with(post, []) posts.update_post_thumbnail.assert_called_once_with( - post, 'post-thumbnail') + post, "post-thumbnail" + ) posts.serialize_post.assert_called_once_with( - post, auth_user, options=[]) + post, auth_user, options=[] + ) snapshots.create.assert_called_once_with(post, auth_user) @@ -71,241 +83,289 @@ def test_creating_full_posts(context_factory, post_factory, user_factory): db.session.add(post) db.session.flush() - with patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_safety'), \ - patch('szurubooru.func.posts.update_post_source'), \ - patch('szurubooru.func.posts.update_post_relations'), \ - patch('szurubooru.func.posts.update_post_notes'), \ - patch('szurubooru.func.posts.update_post_flags'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.snapshots.create'): + with patch("szurubooru.func.posts.create_post"), patch( + "szurubooru.func.posts.update_post_safety" + ), patch("szurubooru.func.posts.update_post_source"), patch( + "szurubooru.func.posts.update_post_relations" + ), patch( + "szurubooru.func.posts.update_post_notes" + ), patch( + "szurubooru.func.posts.update_post_flags" + ), patch( + "szurubooru.func.posts.serialize_post" + ), patch( + "szurubooru.func.snapshots.create" + ): posts.create_post.return_value = (post, []) - posts.serialize_post.return_value = 'serialized post' + posts.serialize_post.return_value = "serialized post" result = api.post_api.create_post( context_factory( params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - 'relations': [1, 2], - 'source': 'source', - 'notes': ['note1', 'note2'], - 'flags': ['flag1', 'flag2'], + "safety": "safe", + "tags": ["tag1", "tag2"], + "relations": [1, 2], + "source": "source", + "notes": ["note1", "note2"], + "flags": ["flag1", "flag2"], }, - files={ - 'content': 'post-content', - }, - user=auth_user)) + files={"content": "post-content",}, + user=auth_user, + ) + ) - assert result == 'serialized post' + assert result == "serialized post" posts.create_post.assert_called_once_with( - 'post-content', ['tag1', 'tag2'], auth_user) - posts.update_post_safety.assert_called_once_with(post, 'safe') - posts.update_post_source.assert_called_once_with(post, 'source') + "post-content", ["tag1", "tag2"], auth_user + ) + posts.update_post_safety.assert_called_once_with(post, "safe") + posts.update_post_source.assert_called_once_with(post, "source") posts.update_post_relations.assert_called_once_with(post, [1, 2]) posts.update_post_notes.assert_called_once_with( - post, ['note1', 'note2']) + post, ["note1", "note2"] + ) posts.update_post_flags.assert_called_once_with( - post, ['flag1', 'flag2']) + post, ["flag1", "flag2"] + ) posts.serialize_post.assert_called_once_with( - post, auth_user, options=[]) + post, auth_user, options=[] + ) snapshots.create.assert_called_once_with(post, auth_user) def test_anonymous_uploads( - config_injector, context_factory, post_factory, user_factory): + config_injector, context_factory, post_factory, user_factory +): auth_user = user_factory(rank=model.User.RANK_REGULAR) post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_source'): - config_injector({ - 'privileges': { - 'posts:create:anonymous': model.User.RANK_REGULAR, - 'uploads:use_downloader': model.User.RANK_POWER, - }, - }) + with patch("szurubooru.func.posts.serialize_post"), patch( + "szurubooru.func.posts.create_post" + ), patch("szurubooru.func.posts.update_post_source"): + config_injector( + { + "privileges": { + "posts:create:anonymous": model.User.RANK_REGULAR, + "uploads:use_downloader": model.User.RANK_POWER, + }, + } + ) posts.create_post.return_value = [post, []] api.post_api.create_post( context_factory( params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - 'anonymous': 'True', + "safety": "safe", + "tags": ["tag1", "tag2"], + "anonymous": "True", }, - files={ - 'content': 'post-content', - }, - user=auth_user)) + files={"content": "post-content",}, + user=auth_user, + ) + ) posts.create_post.assert_called_once_with( - 'post-content', ['tag1', 'tag2'], None) + "post-content", ["tag1", "tag2"], None + ) def test_creating_from_url_saves_source( - config_injector, context_factory, post_factory, user_factory): + config_injector, context_factory, post_factory, user_factory +): auth_user = user_factory(rank=model.User.RANK_REGULAR) post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.net.download'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_source'): - config_injector({ - 'privileges': { - 'posts:create:identified': model.User.RANK_REGULAR, - 'uploads:use_downloader': model.User.RANK_POWER, - }, - }) - net.download.return_value = b'content' + with patch("szurubooru.func.net.download"), patch( + "szurubooru.func.posts.serialize_post" + ), patch("szurubooru.func.posts.create_post"), patch( + "szurubooru.func.posts.update_post_source" + ): + config_injector( + { + "privileges": { + "posts:create:identified": model.User.RANK_REGULAR, + "uploads:use_downloader": model.User.RANK_POWER, + }, + } + ) + net.download.return_value = b"content" posts.create_post.return_value = [post, []] api.post_api.create_post( context_factory( params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - 'contentUrl': 'example.com', + "safety": "safe", + "tags": ["tag1", "tag2"], + "contentUrl": "example.com", }, - user=auth_user)) + user=auth_user, + ) + ) net.download.assert_called_once_with( - 'example.com', use_video_downloader=False) + "example.com", use_video_downloader=False + ) posts.create_post.assert_called_once_with( - b'content', ['tag1', 'tag2'], auth_user) - posts.update_post_source.assert_called_once_with(post, 'example.com') + b"content", ["tag1", "tag2"], auth_user + ) + posts.update_post_source.assert_called_once_with(post, "example.com") def test_creating_from_url_with_source_specified( - config_injector, context_factory, post_factory, user_factory): + config_injector, context_factory, post_factory, user_factory +): auth_user = user_factory(rank=model.User.RANK_REGULAR) post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.net.download'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_source'): - config_injector({ - 'privileges': { - 'posts:create:identified': model.User.RANK_REGULAR, - 'uploads:use_downloader': model.User.RANK_REGULAR, - }, - }) - net.download.return_value = b'content' + with patch("szurubooru.func.net.download"), patch( + "szurubooru.func.posts.serialize_post" + ), patch("szurubooru.func.posts.create_post"), patch( + "szurubooru.func.posts.update_post_source" + ): + config_injector( + { + "privileges": { + "posts:create:identified": model.User.RANK_REGULAR, + "uploads:use_downloader": model.User.RANK_REGULAR, + }, + } + ) + net.download.return_value = b"content" posts.create_post.return_value = [post, []] api.post_api.create_post( context_factory( params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - 'contentUrl': 'example.com', - 'source': 'example2.com', + "safety": "safe", + "tags": ["tag1", "tag2"], + "contentUrl": "example.com", + "source": "example2.com", }, - user=auth_user)) + user=auth_user, + ) + ) net.download.assert_called_once_with( - 'example.com', use_video_downloader=True) + "example.com", use_video_downloader=True + ) posts.create_post.assert_called_once_with( - b'content', ['tag1', 'tag2'], auth_user) - posts.update_post_source.assert_called_once_with(post, 'example2.com') + b"content", ["tag1", "tag2"], auth_user + ) + posts.update_post_source.assert_called_once_with(post, "example2.com") -@pytest.mark.parametrize('field', ['safety']) +@pytest.mark.parametrize("field", ["safety"]) def test_trying_to_omit_mandatory_field(context_factory, user_factory, field): params = { - 'safety': 'safe', + "safety": "safe", } del params[field] with pytest.raises(errors.MissingRequiredParameterError): api.post_api.create_post( context_factory( params=params, - files={'content': '...'}, - user=user_factory(rank=model.User.RANK_REGULAR))) + files={"content": "..."}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) @pytest.mark.parametrize( - 'field', ['tags', 'relations', 'source', 'notes', 'flags']) + "field", ["tags", "relations", "source", "notes", "flags"] +) def test_omitting_optional_field( - field, context_factory, post_factory, user_factory): + field, context_factory, post_factory, user_factory +): auth_user = user_factory(rank=model.User.RANK_REGULAR) post = post_factory() db.session.add(post) db.session.flush() params = { - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - 'relations': [1, 2], - 'source': 'source', - 'notes': ['note1', 'note2'], - 'flags': ['flag1', 'flag2'], + "safety": "safe", + "tags": ["tag1", "tag2"], + "relations": [1, 2], + "source": "source", + "notes": ["note1", "note2"], + "flags": ["flag1", "flag2"], } del params[field] - with patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_safety'), \ - patch('szurubooru.func.posts.update_post_source'), \ - patch('szurubooru.func.posts.update_post_relations'), \ - patch('szurubooru.func.posts.update_post_notes'), \ - patch('szurubooru.func.posts.update_post_flags'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.snapshots.create'): + with patch("szurubooru.func.posts.create_post"), patch( + "szurubooru.func.posts.update_post_safety" + ), patch("szurubooru.func.posts.update_post_source"), patch( + "szurubooru.func.posts.update_post_relations" + ), patch( + "szurubooru.func.posts.update_post_notes" + ), patch( + "szurubooru.func.posts.update_post_flags" + ), patch( + "szurubooru.func.posts.serialize_post" + ), patch( + "szurubooru.func.snapshots.create" + ): posts.create_post.return_value = (post, []) - posts.serialize_post.return_value = 'serialized post' + posts.serialize_post.return_value = "serialized post" result = api.post_api.create_post( context_factory( params=params, - files={'content': 'post-content'}, - user=auth_user)) - assert result == 'serialized post' + files={"content": "post-content"}, + user=auth_user, + ) + ) + assert result == "serialized post" def test_errors_not_spending_ids( - config_injector, tmpdir, context_factory, read_asset, user_factory): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'data_url': 'example.com', - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'privileges': { - 'posts:create:identified': model.User.RANK_REGULAR, - 'uploads:use_downloader': model.User.RANK_POWER, - }, - 'secret': 'test', - }) + config_injector, tmpdir, context_factory, read_asset, user_factory +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "data_url": "example.com", + "thumbnails": {"post_width": 300, "post_height": 300,}, + "privileges": { + "posts:create:identified": model.User.RANK_REGULAR, + "uploads:use_downloader": model.User.RANK_POWER, + }, + "secret": "test", + } + ) auth_user = user_factory(rank=model.User.RANK_REGULAR) # successful request - with patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.update_post_tags'): + with patch("szurubooru.func.posts.serialize_post"), patch( + "szurubooru.func.posts.update_post_tags" + ): posts.serialize_post.side_effect = lambda post, *_, **__: post.post_id post1_id = api.post_api.create_post( context_factory( - params={'safety': 'safe', 'tags': []}, - files={'content': read_asset('png.png')}, - user=auth_user)) + params={"safety": "safe", "tags": []}, + files={"content": read_asset("png.png")}, + user=auth_user, + ) + ) # erroreous request (duplicate post) with pytest.raises(posts.PostAlreadyUploadedError): api.post_api.create_post( context_factory( - params={'safety': 'safe', 'tags': []}, - files={'content': read_asset('png.png')}, - user=auth_user)) + params={"safety": "safe", "tags": []}, + files={"content": read_asset("png.png")}, + user=auth_user, + ) + ) # successful request - with patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.update_post_tags'): + with patch("szurubooru.func.posts.serialize_post"), patch( + "szurubooru.func.posts.update_post_tags" + ): posts.serialize_post.side_effect = lambda post, *_, **__: post.post_id post2_id = api.post_api.create_post( context_factory( - params={'safety': 'safe', 'tags': []}, - files={'content': read_asset('jpeg.jpg')}, - user=auth_user)) + params={"safety": "safe", "tags": []}, + files={"content": read_asset("jpeg.jpg")}, + user=auth_user, + ) + ) assert post1_id > 0 assert post2_id > 0 @@ -316,42 +376,45 @@ def test_trying_to_omit_content(context_factory, user_factory): with pytest.raises(errors.MissingRequiredFileError): api.post_api.create_post( context_factory( - params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - }, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"safety": "safe", "tags": ["tag1", "tag2"],}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) def test_trying_to_create_post_without_privileges( - context_factory, user_factory): + context_factory, user_factory +): with pytest.raises(errors.AuthError): - api.post_api.create_post(context_factory( - params='whatever', - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + api.post_api.create_post( + context_factory( + params="whatever", + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) def test_trying_to_create_tags_without_privileges( - config_injector, context_factory, user_factory): - config_injector({ - 'privileges': { - 'posts:create:anonymous': model.User.RANK_REGULAR, - 'posts:create:identified': model.User.RANK_REGULAR, - 'tags:create': model.User.RANK_ADMINISTRATOR, - 'uploads:use_downloader': model.User.RANK_POWER, - }, - }) - with pytest.raises(errors.AuthError), \ - patch('szurubooru.func.posts.update_post_content'), \ - patch('szurubooru.func.posts.update_post_tags'): - posts.update_post_tags.return_value = ['new-tag'] + config_injector, context_factory, user_factory +): + config_injector( + { + "privileges": { + "posts:create:anonymous": model.User.RANK_REGULAR, + "posts:create:identified": model.User.RANK_REGULAR, + "tags:create": model.User.RANK_ADMINISTRATOR, + "uploads:use_downloader": model.User.RANK_POWER, + }, + } + ) + with pytest.raises(errors.AuthError), patch( + "szurubooru.func.posts.update_post_content" + ), patch("szurubooru.func.posts.update_post_tags"): + posts.update_post_tags.return_value = ["new-tag"] api.post_api.create_post( context_factory( - params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - }, - files={ - 'content': posts.EMPTY_PIXEL, - }, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"safety": "safe", "tags": ["tag1", "tag2"],}, + files={"content": posts.EMPTY_PIXEL,}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) diff --git a/server/szurubooru/tests/api/test_post_deleting.py b/server/szurubooru/tests/api/test_post_deleting.py index bb5f9ce..fd2cb75 100644 --- a/server/szurubooru/tests/api/test_post_deleting.py +++ b/server/szurubooru/tests/api/test_post_deleting.py @@ -1,19 +1,21 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import posts, snapshots @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'secret': 'secret', - 'data_dir': '', - 'delete_source_files': False, - 'privileges': { - 'posts:delete': model.User.RANK_REGULAR + config_injector( + { + "secret": "secret", + "data_dir": "", + "delete_source_files": False, + "privileges": {"posts:delete": model.User.RANK_REGULAR}, } - }) + ) def test_deleting(user_factory, post_factory, context_factory): @@ -21,10 +23,11 @@ def test_deleting(user_factory, post_factory, context_factory): post = post_factory(id=1) db.session.add(post) db.session.flush() - with patch('szurubooru.func.snapshots.delete'): + with patch("szurubooru.func.snapshots.delete"): result = api.post_api.delete_post( - context_factory(params={'version': 1}, user=auth_user), - {'post_id': 1}) + context_factory(params={"version": 1}, user=auth_user), + {"post_id": 1}, + ) assert result == {} assert db.session.query(model.Post).count() == 0 snapshots.delete.assert_called_once_with(post, auth_user) @@ -34,15 +37,18 @@ def test_trying_to_delete_non_existing(user_factory, context_factory): with pytest.raises(posts.PostNotFoundError): api.post_api.delete_post( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': 999}) + {"post_id": 999}, + ) def test_trying_to_delete_without_privileges( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): db.session.add(post_factory(id=1)) db.session.commit() with pytest.raises(errors.AuthError): api.post_api.delete_post( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'post_id': 1}) + {"post_id": 1}, + ) assert db.session.query(model.Post).count() == 1 diff --git a/server/szurubooru/tests/api/test_post_favoriting.py b/server/szurubooru/tests/api/test_post_favoriting.py index ce91a02..dc92bd9 100644 --- a/server/szurubooru/tests/api/test_post_favoriting.py +++ b/server/szurubooru/tests/api/test_post_favoriting.py @@ -1,29 +1,34 @@ from datetime import datetime from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import posts @pytest.fixture(autouse=True) def inject_config(config_injector): config_injector( - {'privileges': {'posts:favorite': model.User.RANK_REGULAR}}) + {"privileges": {"posts:favorite": model.User.RANK_REGULAR}} + ) def test_adding_to_favorites( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): post = post_factory() db.session.add(post) db.session.commit() assert post.score == 0 - with patch('szurubooru.func.posts.serialize_post'), \ - fake_datetime('1997-12-01'): - posts.serialize_post.return_value = 'serialized post' + with patch("szurubooru.func.posts.serialize_post"), fake_datetime( + "1997-12-01" + ): + posts.serialize_post.return_value = "serialized post" result = api.post_api.add_post_to_favorites( - context_factory(user=user_factory()), - {'post_id': post.post_id}) - assert result == 'serialized post' + context_factory(user=user_factory()), {"post_id": post.post_id} + ) + assert result == "serialized post" post = db.session.query(model.Post).one() assert db.session.query(model.PostFavorite).count() == 1 assert post is not None @@ -32,22 +37,23 @@ def test_adding_to_favorites( def test_removing_from_favorites( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user = user_factory() post = post_factory() db.session.add(post) db.session.commit() assert post.score == 0 - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.add_post_to_favorites( - context_factory(user=user), - {'post_id': post.post_id}) + context_factory(user=user), {"post_id": post.post_id} + ) assert post.score == 1 - with fake_datetime('1997-12-02'): + with fake_datetime("1997-12-02"): api.post_api.delete_post_from_favorites( - context_factory(user=user), - {'post_id': post.post_id}) + context_factory(user=user), {"post_id": post.post_id} + ) post = db.session.query(model.Post).one() assert post.score == 1 assert db.session.query(model.PostFavorite).count() == 0 @@ -55,65 +61,68 @@ def test_removing_from_favorites( def test_favoriting_twice( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user = user_factory() post = post_factory() db.session.add(post) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.add_post_to_favorites( - context_factory(user=user), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(user=user), {"post_id": post.post_id} + ) + with fake_datetime("1997-12-02"): api.post_api.add_post_to_favorites( - context_factory(user=user), - {'post_id': post.post_id}) + context_factory(user=user), {"post_id": post.post_id} + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostFavorite).count() == 1 assert post.favorite_count == 1 def test_removing_twice( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user = user_factory() post = post_factory() db.session.add(post) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.add_post_to_favorites( - context_factory(user=user), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(user=user), {"post_id": post.post_id} + ) + with fake_datetime("1997-12-02"): api.post_api.delete_post_from_favorites( - context_factory(user=user), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(user=user), {"post_id": post.post_id} + ) + with fake_datetime("1997-12-02"): api.post_api.delete_post_from_favorites( - context_factory(user=user), - {'post_id': post.post_id}) + context_factory(user=user), {"post_id": post.post_id} + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostFavorite).count() == 0 assert post.favorite_count == 0 def test_favorites_from_multiple_users( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user1 = user_factory() user2 = user_factory() post = post_factory() db.session.add_all([user1, user2, post]) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.add_post_to_favorites( - context_factory(user=user1), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(user=user1), {"post_id": post.post_id} + ) + with fake_datetime("1997-12-02"): api.post_api.add_post_to_favorites( - context_factory(user=user2), - {'post_id': post.post_id}) + context_factory(user=user2), {"post_id": post.post_id} + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostFavorite).count() == 2 assert post.favorite_count == 2 @@ -123,16 +132,18 @@ def test_favorites_from_multiple_users( def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(posts.PostNotFoundError): api.post_api.add_post_to_favorites( - context_factory(user=user_factory()), - {'post_id': 5}) + context_factory(user=user_factory()), {"post_id": 5} + ) def test_trying_to_rate_without_privileges( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): post = post_factory() db.session.add(post) db.session.commit() with pytest.raises(errors.AuthError): api.post_api.add_post_to_favorites( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'post_id': post.post_id}) + {"post_id": post.post_id}, + ) diff --git a/server/szurubooru/tests/api/test_post_featuring.py b/server/szurubooru/tests/api/test_post_featuring.py index 6e9e756..e1f8cef 100644 --- a/server/szurubooru/tests/api/test_post_featuring.py +++ b/server/szurubooru/tests/api/test_post_featuring.py @@ -1,18 +1,22 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import posts, snapshots @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'posts:feature': model.User.RANK_REGULAR, - 'posts:view': model.User.RANK_REGULAR, - 'posts:view:featured': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "posts:feature": model.User.RANK_REGULAR, + "posts:view": model.User.RANK_REGULAR, + "posts:view:featured": model.User.RANK_REGULAR, + }, + } + ) def test_featuring(user_factory, post_factory, context_factory): @@ -21,64 +25,76 @@ def test_featuring(user_factory, post_factory, context_factory): db.session.add(post) db.session.flush() assert not posts.get_post_by_id(1).is_featured - with patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.snapshots.modify'): - posts.serialize_post.return_value = 'serialized post' + with patch("szurubooru.func.posts.serialize_post"), patch( + "szurubooru.func.snapshots.modify" + ): + posts.serialize_post.return_value = "serialized post" result = api.post_api.set_featured_post( - context_factory(params={'id': 1}, user=auth_user)) - assert result == 'serialized post' + context_factory(params={"id": 1}, user=auth_user) + ) + assert result == "serialized post" assert posts.try_get_featured_post() is not None assert posts.try_get_featured_post().post_id == 1 assert posts.get_post_by_id(1).is_featured result = api.post_api.get_featured_post( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR))) - assert result == 'serialized post' + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)) + ) + assert result == "serialized post" snapshots.modify.assert_called_once_with(post, auth_user) def test_trying_to_omit_required_parameter(user_factory, context_factory): with pytest.raises(errors.MissingRequiredParameterError): api.post_api.set_featured_post( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR))) + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)) + ) def test_trying_to_feature_the_same_post_twice( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): db.session.add(post_factory(id=1)) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): + with patch("szurubooru.func.posts.serialize_post"): api.post_api.set_featured_post( context_factory( - params={'id': 1}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"id": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) with pytest.raises(posts.PostAlreadyFeaturedError): api.post_api.set_featured_post( context_factory( - params={'id': 1}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"id": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) def test_featuring_one_post_after_another( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): db.session.add(post_factory(id=1)) db.session.add(post_factory(id=2)) db.session.commit() assert posts.try_get_featured_post() is None assert not posts.get_post_by_id(1).is_featured assert not posts.get_post_by_id(2).is_featured - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997"): api.post_api.set_featured_post( context_factory( - params={'id': 1}, - user=user_factory(rank=model.User.RANK_REGULAR))) - with fake_datetime('1998'): + params={"id": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) + with fake_datetime("1998"): api.post_api.set_featured_post( context_factory( - params={'id': 2}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"id": 2}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) assert posts.try_get_featured_post() is not None assert posts.try_get_featured_post().post_id == 2 assert not posts.get_post_by_id(1).is_featured @@ -89,18 +105,21 @@ def test_trying_to_feature_non_existing(user_factory, context_factory): with pytest.raises(posts.PostNotFoundError): api.post_api.set_featured_post( context_factory( - params={'id': 1}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"id": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) -def test_trying_to_retrieve_without_privileges( - user_factory, context_factory): +def test_trying_to_retrieve_without_privileges(user_factory, context_factory): with pytest.raises(errors.AuthError): api.post_api.get_featured_post( - context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS))) + context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)) + ) def test_trying_to_feature_without_privileges(user_factory, context_factory): with pytest.raises(errors.AuthError): api.post_api.set_featured_post( - context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS))) + context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)) + ) diff --git a/server/szurubooru/tests/api/test_post_merging.py b/server/szurubooru/tests/api/test_post_merging.py index eb8464f..cdcb0af 100644 --- a/server/szurubooru/tests/api/test_post_merging.py +++ b/server/szurubooru/tests/api/test_post_merging.py @@ -1,12 +1,14 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import posts, snapshots @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'posts:merge': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"posts:merge": model.User.RANK_REGULAR}}) def test_merging(user_factory, context_factory, post_factory): @@ -15,66 +17,78 @@ def test_merging(user_factory, context_factory, post_factory): target_post = post_factory() db.session.add_all([source_post, target_post]) db.session.flush() - with patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.merge_posts'), \ - patch('szurubooru.func.snapshots.merge'): + with patch("szurubooru.func.posts.serialize_post"), patch( + "szurubooru.func.posts.merge_posts" + ), patch("szurubooru.func.snapshots.merge"): api.post_api.merge_posts( context_factory( params={ - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': source_post.post_id, - 'mergeTo': target_post.post_id, - 'replaceContent': False, + "removeVersion": 1, + "mergeToVersion": 1, + "remove": source_post.post_id, + "mergeTo": target_post.post_id, + "replaceContent": False, }, - user=auth_user)) + user=auth_user, + ) + ) posts.merge_posts.called_once_with(source_post, target_post) snapshots.merge.assert_called_once_with( - source_post, target_post, auth_user) + source_post, target_post, auth_user + ) @pytest.mark.parametrize( - 'field', ['remove', 'mergeTo', 'removeVersion', 'mergeToVersion']) + "field", ["remove", "mergeTo", "removeVersion", "mergeToVersion"] +) def test_trying_to_omit_mandatory_field( - user_factory, post_factory, context_factory, field): + user_factory, post_factory, context_factory, field +): source_post = post_factory() target_post = post_factory() db.session.add_all([source_post, target_post]) db.session.commit() params = { - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': source_post.post_id, - 'mergeTo': target_post.post_id, - 'replaceContent': False, + "removeVersion": 1, + "mergeToVersion": 1, + "remove": source_post.post_id, + "mergeTo": target_post.post_id, + "replaceContent": False, } del params[field] with pytest.raises(errors.ValidationError): api.post_api.merge_posts( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) def test_trying_to_merge_non_existing( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): post = post_factory() db.session.add(post) db.session.commit() with pytest.raises(posts.PostNotFoundError): api.post_api.merge_posts( context_factory( - params={'remove': post.post_id, 'mergeTo': 999}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"remove": post.post_id, "mergeTo": 999}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) with pytest.raises(posts.PostNotFoundError): api.post_api.merge_posts( context_factory( - params={'remove': 999, 'mergeTo': post.post_id}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"remove": 999, "mergeTo": post.post_id}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) def test_trying_to_merge_without_privileges( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): source_post = post_factory() target_post = post_factory() db.session.add_all([source_post, target_post]) @@ -83,10 +97,12 @@ def test_trying_to_merge_without_privileges( api.post_api.merge_posts( context_factory( params={ - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': source_post.post_id, - 'mergeTo': target_post.post_id, - 'replaceContent': False, + "removeVersion": 1, + "mergeToVersion": 1, + "remove": source_post.post_id, + "mergeTo": target_post.post_id, + "replaceContent": False, }, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_post_rating.py b/server/szurubooru/tests/api/test_post_rating.py index 0fca2f5..2db17db 100644 --- a/server/szurubooru/tests/api/test_post_rating.py +++ b/server/szurubooru/tests/api/test_post_rating.py @@ -1,27 +1,31 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import posts @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'posts:score': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"posts:score": model.User.RANK_REGULAR}}) def test_simple_rating( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): post = post_factory() db.session.add(post) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'), \ - fake_datetime('1997-12-01'): - posts.serialize_post.return_value = 'serialized post' + with patch("szurubooru.func.posts.serialize_post"), fake_datetime( + "1997-12-01" + ): + posts.serialize_post.return_value = "serialized post" result = api.post_api.set_post_score( - context_factory( - params={'score': 1}, user=user_factory()), - {'post_id': post.post_id}) - assert result == 'serialized post' + context_factory(params={"score": 1}, user=user_factory()), + {"post_id": post.post_id}, + ) + assert result == "serialized post" post = db.session.query(model.Post).one() assert db.session.query(model.PostScore).count() == 1 assert post is not None @@ -29,112 +33,129 @@ def test_simple_rating( def test_updating_rating( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user = user_factory() post = post_factory() db.session.add(post) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.set_post_score( - context_factory(params={'score': 1}, user=user), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user), + {"post_id": post.post_id}, + ) + with fake_datetime("1997-12-02"): api.post_api.set_post_score( - context_factory(params={'score': -1}, user=user), - {'post_id': post.post_id}) + context_factory(params={"score": -1}, user=user), + {"post_id": post.post_id}, + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostScore).count() == 1 assert post.score == -1 def test_updating_rating_to_zero( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user = user_factory() post = post_factory() db.session.add(post) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.set_post_score( - context_factory(params={'score': 1}, user=user), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user), + {"post_id": post.post_id}, + ) + with fake_datetime("1997-12-02"): api.post_api.set_post_score( - context_factory(params={'score': 0}, user=user), - {'post_id': post.post_id}) + context_factory(params={"score": 0}, user=user), + {"post_id": post.post_id}, + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostScore).count() == 0 assert post.score == 0 def test_deleting_rating( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user = user_factory() post = post_factory() db.session.add(post) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.set_post_score( - context_factory(params={'score': 1}, user=user), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user), + {"post_id": post.post_id}, + ) + with fake_datetime("1997-12-02"): api.post_api.delete_post_score( - context_factory(user=user), - {'post_id': post.post_id}) + context_factory(user=user), {"post_id": post.post_id} + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostScore).count() == 0 assert post.score == 0 def test_ratings_from_multiple_users( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user1 = user_factory() user2 = user_factory() post = post_factory() db.session.add_all([user1, user2, post]) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.set_post_score( - context_factory(params={'score': 1}, user=user1), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user1), + {"post_id": post.post_id}, + ) + with fake_datetime("1997-12-02"): api.post_api.set_post_score( - context_factory(params={'score': -1}, user=user2), - {'post_id': post.post_id}) + context_factory(params={"score": -1}, user=user2), + {"post_id": post.post_id}, + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostScore).count() == 2 assert post.score == 0 def test_trying_to_omit_mandatory_field( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): post = post_factory() db.session.add(post) db.session.commit() with pytest.raises(errors.ValidationError): api.post_api.set_post_score( context_factory(params={}, user=user_factory()), - {'post_id': post.post_id}) + {"post_id": post.post_id}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(posts.PostNotFoundError): api.post_api.set_post_score( - context_factory(params={'score': 1}, user=user_factory()), - {'post_id': 5}) + context_factory(params={"score": 1}, user=user_factory()), + {"post_id": 5}, + ) def test_trying_to_rate_without_privileges( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): post = post_factory() db.session.add(post) db.session.commit() with pytest.raises(errors.AuthError): api.post_api.set_post_score( context_factory( - params={'score': 1}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'post_id': post.post_id}) + params={"score": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"post_id": post.post_id}, + ) diff --git a/server/szurubooru/tests/api/test_post_retrieving.py b/server/szurubooru/tests/api/test_post_retrieving.py index 7270547..8f2c371 100644 --- a/server/szurubooru/tests/api/test_post_retrieving.py +++ b/server/szurubooru/tests/api/test_post_retrieving.py @@ -1,18 +1,22 @@ from datetime import datetime from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import posts @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'posts:list': model.User.RANK_REGULAR, - 'posts:view': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "posts:list": model.User.RANK_REGULAR, + "posts:view": model.User.RANK_REGULAR, + }, + } + ) def test_retrieving_multiple(user_factory, post_factory, context_factory): @@ -20,18 +24,20 @@ def test_retrieving_multiple(user_factory, post_factory, context_factory): post2 = post_factory(id=2) db.session.add_all([post1, post2]) db.session.flush() - with patch('szurubooru.func.posts.serialize_post'): - posts.serialize_post.return_value = 'serialized post' + with patch("szurubooru.func.posts.serialize_post"): + posts.serialize_post.return_value = "serialized post" result = api.post_api.get_posts( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) assert result == { - 'query': '', - 'offset': 0, - 'limit': 100, - 'total': 2, - 'results': ['serialized post', 'serialized post'], + "query": "", + "offset": 0, + "limit": 100, + "total": 2, + "results": ["serialized post", "serialized post"], } @@ -39,68 +45,81 @@ def test_using_special_tokens(user_factory, post_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) post1 = post_factory(id=1) post2 = post_factory(id=2) - post1.favorited_by = [model.PostFavorite( - user=auth_user, time=datetime.utcnow())] + post1.favorited_by = [ + model.PostFavorite(user=auth_user, time=datetime.utcnow()) + ] db.session.add_all([post1, post2, auth_user]) db.session.flush() - with patch('szurubooru.func.posts.serialize_post'): - posts.serialize_post.side_effect = lambda post, *_args, **_kwargs: \ - 'serialized post %d' % post.post_id + with patch("szurubooru.func.posts.serialize_post"): + posts.serialize_post.side_effect = ( + lambda post, *_args, **_kwargs: "serialized post %d" % post.post_id + ) result = api.post_api.get_posts( context_factory( - params={'query': 'special:fav', 'offset': 0}, - user=auth_user)) + params={"query": "special:fav", "offset": 0}, user=auth_user + ) + ) assert result == { - 'query': 'special:fav', - 'offset': 0, - 'limit': 100, - 'total': 1, - 'results': ['serialized post 1'], + "query": "special:fav", + "offset": 0, + "limit": 100, + "total": 1, + "results": ["serialized post 1"], } def test_trying_to_use_special_tokens_without_logging_in( - user_factory, context_factory, config_injector): - config_injector({ - 'privileges': {'posts:list': 'anonymous'}, - }) + user_factory, context_factory, config_injector +): + config_injector( + {"privileges": {"posts:list": "anonymous"},} + ) with pytest.raises(errors.SearchError): api.post_api.get_posts( context_factory( - params={'query': 'special:fav', 'offset': 0}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"query": "special:fav", "offset": 0}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) def test_trying_to_retrieve_multiple_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.post_api.get_posts( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) def test_retrieving_single(user_factory, post_factory, context_factory): db.session.add(post_factory(id=1)) db.session.flush() - with patch('szurubooru.func.posts.serialize_post'): - posts.serialize_post.return_value = 'serialized post' + with patch("szurubooru.func.posts.serialize_post"): + posts.serialize_post.return_value = "serialized post" result = api.post_api.get_post( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': 1}) - assert result == 'serialized post' + {"post_id": 1}, + ) + assert result == "serialized post" def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): with pytest.raises(posts.PostNotFoundError): api.post_api.get_post( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': 999}) + {"post_id": 999}, + ) def test_trying_to_retrieve_single_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.post_api.get_post( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'post_id': 999}) + {"post_id": 999}, + ) diff --git a/server/szurubooru/tests/api/test_post_updating.py b/server/szurubooru/tests/api/test_post_updating.py index 887f54f..e4a606d 100644 --- a/server/szurubooru/tests/api/test_post_updating.py +++ b/server/szurubooru/tests/api/test_post_updating.py @@ -1,177 +1,222 @@ from datetime import datetime from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import posts, tags, snapshots, net + +from szurubooru import api, db, errors, model +from szurubooru.func import net, posts, snapshots, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'posts:edit:tags': model.User.RANK_REGULAR, - 'posts:edit:content': model.User.RANK_REGULAR, - 'posts:edit:safety': model.User.RANK_REGULAR, - 'posts:edit:source': model.User.RANK_REGULAR, - 'posts:edit:relations': model.User.RANK_REGULAR, - 'posts:edit:notes': model.User.RANK_REGULAR, - 'posts:edit:flags': model.User.RANK_REGULAR, - 'posts:edit:thumbnail': model.User.RANK_REGULAR, - 'tags:create': model.User.RANK_MODERATOR, - 'uploads:use_downloader': model.User.RANK_REGULAR, - }, - 'allow_broken_uploads': False, - }) + config_injector( + { + "privileges": { + "posts:edit:tags": model.User.RANK_REGULAR, + "posts:edit:content": model.User.RANK_REGULAR, + "posts:edit:safety": model.User.RANK_REGULAR, + "posts:edit:source": model.User.RANK_REGULAR, + "posts:edit:relations": model.User.RANK_REGULAR, + "posts:edit:notes": model.User.RANK_REGULAR, + "posts:edit:flags": model.User.RANK_REGULAR, + "posts:edit:thumbnail": model.User.RANK_REGULAR, + "tags:create": model.User.RANK_MODERATOR, + "uploads:use_downloader": model.User.RANK_REGULAR, + }, + "allow_broken_uploads": False, + } + ) def test_post_updating( - context_factory, post_factory, user_factory, fake_datetime): + context_factory, post_factory, user_factory, fake_datetime +): auth_user = user_factory(rank=model.User.RANK_REGULAR) post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_tags'), \ - patch('szurubooru.func.posts.update_post_content'), \ - patch('szurubooru.func.posts.update_post_thumbnail'), \ - patch('szurubooru.func.posts.update_post_safety'), \ - patch('szurubooru.func.posts.update_post_source'), \ - patch('szurubooru.func.posts.update_post_relations'), \ - patch('szurubooru.func.posts.update_post_notes'), \ - patch('szurubooru.func.posts.update_post_flags'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.snapshots.modify'), \ - fake_datetime('1997-01-01'): - posts.serialize_post.return_value = 'serialized post' + with patch("szurubooru.func.posts.create_post"), patch( + "szurubooru.func.posts.update_post_tags" + ), patch("szurubooru.func.posts.update_post_content"), patch( + "szurubooru.func.posts.update_post_thumbnail" + ), patch( + "szurubooru.func.posts.update_post_safety" + ), patch( + "szurubooru.func.posts.update_post_source" + ), patch( + "szurubooru.func.posts.update_post_relations" + ), patch( + "szurubooru.func.posts.update_post_notes" + ), patch( + "szurubooru.func.posts.update_post_flags" + ), patch( + "szurubooru.func.posts.serialize_post" + ), patch( + "szurubooru.func.snapshots.modify" + ), fake_datetime( + "1997-01-01" + ): + posts.serialize_post.return_value = "serialized post" result = api.post_api.update_post( context_factory( params={ - 'version': 1, - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - 'relations': [1, 2], - 'source': 'source', - 'notes': ['note1', 'note2'], - 'flags': ['flag1', 'flag2'], + "version": 1, + "safety": "safe", + "tags": ["tag1", "tag2"], + "relations": [1, 2], + "source": "source", + "notes": ["note1", "note2"], + "flags": ["flag1", "flag2"], }, files={ - 'content': 'post-content', - 'thumbnail': 'post-thumbnail', + "content": "post-content", + "thumbnail": "post-thumbnail", }, - user=auth_user), - {'post_id': post.post_id}) + user=auth_user, + ), + {"post_id": post.post_id}, + ) - assert result == 'serialized post' + assert result == "serialized post" posts.create_post.assert_not_called() - posts.update_post_tags.assert_called_once_with(post, ['tag1', 'tag2']) - posts.update_post_content.assert_called_once_with(post, 'post-content') + posts.update_post_tags.assert_called_once_with(post, ["tag1", "tag2"]) + posts.update_post_content.assert_called_once_with(post, "post-content") posts.update_post_thumbnail.assert_called_once_with( - post, 'post-thumbnail') - posts.update_post_safety.assert_called_once_with(post, 'safe') - posts.update_post_source.assert_called_once_with(post, 'source') + post, "post-thumbnail" + ) + posts.update_post_safety.assert_called_once_with(post, "safe") + posts.update_post_source.assert_called_once_with(post, "source") posts.update_post_relations.assert_called_once_with(post, [1, 2]) posts.update_post_notes.assert_called_once_with( - post, ['note1', 'note2']) + post, ["note1", "note2"] + ) posts.update_post_flags.assert_called_once_with( - post, ['flag1', 'flag2']) + post, ["flag1", "flag2"] + ) posts.serialize_post.assert_called_once_with( - post, auth_user, options=[]) + post, auth_user, options=[] + ) snapshots.modify.assert_called_once_with(post, auth_user) assert post.last_edit_time == datetime(1997, 1, 1) def test_uploading_from_url_saves_source( - context_factory, post_factory, user_factory): + context_factory, post_factory, user_factory +): post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.net.download'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.update_post_content'), \ - patch('szurubooru.func.posts.update_post_source'), \ - patch('szurubooru.func.snapshots.modify'): - net.download.return_value = b'content' + with patch("szurubooru.func.net.download"), patch( + "szurubooru.func.posts.serialize_post" + ), patch("szurubooru.func.posts.update_post_content"), patch( + "szurubooru.func.posts.update_post_source" + ), patch( + "szurubooru.func.snapshots.modify" + ): + net.download.return_value = b"content" api.post_api.update_post( context_factory( - params={'contentUrl': 'example.com', 'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': post.post_id}) + params={"contentUrl": "example.com", "version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"post_id": post.post_id}, + ) net.download.assert_called_once_with( - 'example.com', use_video_downloader=True) - posts.update_post_content.assert_called_once_with(post, b'content') - posts.update_post_source.assert_called_once_with(post, 'example.com') + "example.com", use_video_downloader=True + ) + posts.update_post_content.assert_called_once_with(post, b"content") + posts.update_post_source.assert_called_once_with(post, "example.com") def test_uploading_from_url_with_source_specified( - context_factory, post_factory, user_factory): + context_factory, post_factory, user_factory +): post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.net.download'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.update_post_content'), \ - patch('szurubooru.func.posts.update_post_source'), \ - patch('szurubooru.func.snapshots.modify'): - net.download.return_value = b'content' + with patch("szurubooru.func.net.download"), patch( + "szurubooru.func.posts.serialize_post" + ), patch("szurubooru.func.posts.update_post_content"), patch( + "szurubooru.func.posts.update_post_source" + ), patch( + "szurubooru.func.snapshots.modify" + ): + net.download.return_value = b"content" api.post_api.update_post( context_factory( params={ - 'contentUrl': 'example.com', - 'source': 'example2.com', - 'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': post.post_id}) + "contentUrl": "example.com", + "source": "example2.com", + "version": 1, + }, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"post_id": post.post_id}, + ) net.download.assert_called_once_with( - 'example.com', use_video_downloader=True) - posts.update_post_content.assert_called_once_with(post, b'content') - posts.update_post_source.assert_called_once_with(post, 'example2.com') + "example.com", use_video_downloader=True + ) + posts.update_post_content.assert_called_once_with(post, b"content") + posts.update_post_source.assert_called_once_with(post, "example2.com") def test_trying_to_update_non_existing(context_factory, user_factory): with pytest.raises(posts.PostNotFoundError): api.post_api.update_post( context_factory( - params='whatever', - user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': 1}) - - -@pytest.mark.parametrize('files,params', [ - ({}, {'tags': '...'}), - ({}, {'safety': '...'}), - ({}, {'source': '...'}), - ({}, {'relations': '...'}), - ({}, {'notes': '...'}), - ({}, {'flags': '...'}), - ({'content': '...'}, {}), - ({'thumbnail': '...'}, {}), -]) + params="whatever", + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"post_id": 1}, + ) + + +@pytest.mark.parametrize( + "files,params", + [ + ({}, {"tags": "..."}), + ({}, {"safety": "..."}), + ({}, {"source": "..."}), + ({}, {"relations": "..."}), + ({}, {"notes": "..."}), + ({}, {"flags": "..."}), + ({"content": "..."}, {}), + ({"thumbnail": "..."}, {}), + ], +) def test_trying_to_update_field_without_privileges( - context_factory, post_factory, user_factory, files, params): + context_factory, post_factory, user_factory, files, params +): post = post_factory() db.session.add(post) db.session.flush() with pytest.raises(errors.AuthError): api.post_api.update_post( context_factory( - params={**params, **{'version': 1}}, + params={**params, **{"version": 1}}, files=files, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'post_id': post.post_id}) + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"post_id": post.post_id}, + ) def test_trying_to_create_tags_without_privileges( - context_factory, post_factory, user_factory): + context_factory, post_factory, user_factory +): post = post_factory() db.session.add(post) db.session.flush() - with pytest.raises(errors.AuthError), \ - patch('szurubooru.func.posts.update_post_tags'): - posts.update_post_tags.return_value = ['new-tag'] + with pytest.raises(errors.AuthError), patch( + "szurubooru.func.posts.update_post_tags" + ): + posts.update_post_tags.return_value = ["new-tag"] api.post_api.update_post( context_factory( - params={'tags': ['tag1', 'tag2'], 'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': post.post_id}) + params={"tags": ["tag1", "tag2"], "version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"post_id": post.post_id}, + ) diff --git a/server/szurubooru/tests/api/test_snapshot_retrieving.py b/server/szurubooru/tests/api/test_snapshot_retrieving.py index 41f0beb..ec24e82 100644 --- a/server/szurubooru/tests/api/test_snapshot_retrieving.py +++ b/server/szurubooru/tests/api/test_snapshot_retrieving.py @@ -1,24 +1,26 @@ from datetime import datetime + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model def snapshot_factory(): snapshot = model.Snapshot() snapshot.creation_time = datetime(1999, 1, 1) - snapshot.resource_type = 'dummy' + snapshot.resource_type = "dummy" snapshot.resource_pkey = 1 - snapshot.resource_name = 'dummy' - snapshot.operation = 'added' - snapshot.data = '{}' + snapshot.resource_name = "dummy" + snapshot.operation = "added" + snapshot.data = "{}" return snapshot @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': {'snapshots:list': model.User.RANK_REGULAR}, - }) + config_injector( + {"privileges": {"snapshots:list": model.User.RANK_REGULAR},} + ) def test_retrieving_multiple(user_factory, context_factory): @@ -28,19 +30,24 @@ def test_retrieving_multiple(user_factory, context_factory): db.session.flush() result = api.snapshot_api.get_snapshots( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_REGULAR))) - assert result['query'] == '' - assert result['offset'] == 0 - assert result['limit'] == 100 - assert result['total'] == 2 - assert len(result['results']) == 2 + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) + assert result["query"] == "" + assert result["offset"] == 0 + assert result["limit"] == 100 + assert result["total"] == 2 + assert len(result["results"]) == 2 def test_trying_to_retrieve_multiple_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.snapshot_api.get_snapshots( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_tag_category_creating.py b/server/szurubooru/tests/api/test_tag_category_creating.py index 47e8405..78f3068 100644 --- a/server/szurubooru/tests/api/test_tag_category_creating.py +++ b/server/szurubooru/tests/api/test_tag_category_creating.py @@ -1,7 +1,9 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import tag_categories, tags, snapshots + +from szurubooru import api, db, errors, model +from szurubooru.func import snapshots, tag_categories, tags def _update_category_name(category, name): @@ -10,49 +12,56 @@ def _update_category_name(category, name): @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': {'tag_categories:create': model.User.RANK_REGULAR}, - }) + config_injector( + {"privileges": {"tag_categories:create": model.User.RANK_REGULAR},} + ) def test_creating_category( - tag_category_factory, user_factory, context_factory): + tag_category_factory, user_factory, context_factory +): auth_user = user_factory(rank=model.User.RANK_REGULAR) - category = tag_category_factory(name='meta') + category = tag_category_factory(name="meta") db.session.add(category) - with patch('szurubooru.func.tag_categories.create_category'), \ - patch('szurubooru.func.tag_categories.serialize_category'), \ - patch('szurubooru.func.tag_categories.update_category_name'), \ - patch('szurubooru.func.snapshots.create'): + with patch("szurubooru.func.tag_categories.create_category"), patch( + "szurubooru.func.tag_categories.serialize_category" + ), patch("szurubooru.func.tag_categories.update_category_name"), patch( + "szurubooru.func.snapshots.create" + ): tag_categories.create_category.return_value = category tag_categories.update_category_name.side_effect = _update_category_name - tag_categories.serialize_category.return_value = 'serialized category' + tag_categories.serialize_category.return_value = "serialized category" result = api.tag_category_api.create_tag_category( context_factory( - params={'name': 'meta', 'color': 'black'}, user=auth_user)) - assert result == 'serialized category' - tag_categories.create_category.assert_called_once_with('meta', 'black') + params={"name": "meta", "color": "black"}, user=auth_user + ) + ) + assert result == "serialized category" + tag_categories.create_category.assert_called_once_with("meta", "black") snapshots.create.assert_called_once_with(category, auth_user) -@pytest.mark.parametrize('field', ['name', 'color']) +@pytest.mark.parametrize("field", ["name", "color"]) def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): params = { - 'name': 'meta', - 'color': 'black', + "name": "meta", + "color": "black", } del params[field] with pytest.raises(errors.ValidationError): api.tag_category_api.create_tag_category( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) def test_trying_to_create_without_privileges(user_factory, context_factory): with pytest.raises(errors.AuthError): api.tag_category_api.create_tag_category( context_factory( - params={'name': 'meta', 'color': 'black'}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"name": "meta", "color": "black"}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_tag_category_deleting.py b/server/szurubooru/tests/api/test_tag_category_deleting.py index 2bee513..2e6be64 100644 --- a/server/szurubooru/tests/api/test_tag_category_deleting.py +++ b/server/szurubooru/tests/api/test_tag_category_deleting.py @@ -1,76 +1,89 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import tag_categories, tags, snapshots + +from szurubooru import api, db, errors, model +from szurubooru.func import snapshots, tag_categories, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': {'tag_categories:delete': model.User.RANK_REGULAR}, - }) + config_injector( + {"privileges": {"tag_categories:delete": model.User.RANK_REGULAR},} + ) def test_deleting(user_factory, tag_category_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) - category = tag_category_factory(name='category') - db.session.add(tag_category_factory(name='root')) + category = tag_category_factory(name="category") + db.session.add(tag_category_factory(name="root")) db.session.add(category) db.session.flush() - with patch('szurubooru.func.snapshots.delete'): + with patch("szurubooru.func.snapshots.delete"): result = api.tag_category_api.delete_tag_category( - context_factory(params={'version': 1}, user=auth_user), - {'category_name': 'category'}) + context_factory(params={"version": 1}, user=auth_user), + {"category_name": "category"}, + ) assert result == {} assert db.session.query(model.TagCategory).count() == 1 - assert db.session.query(model.TagCategory).one().name == 'root' + assert db.session.query(model.TagCategory).one().name == "root" snapshots.delete.assert_called_once_with(category, auth_user) def test_trying_to_delete_used( - user_factory, tag_category_factory, tag_factory, context_factory): - category = tag_category_factory(name='category') + user_factory, tag_category_factory, tag_factory, context_factory +): + category = tag_category_factory(name="category") db.session.add(category) db.session.flush() - tag = tag_factory(names=['tag'], category=category) + tag = tag_factory(names=["tag"], category=category) db.session.add(tag) db.session.commit() with pytest.raises(tag_categories.TagCategoryIsInUseError): api.tag_category_api.delete_tag_category( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'category'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "category"}, + ) assert db.session.query(model.TagCategory).count() == 1 def test_trying_to_delete_last( - user_factory, tag_category_factory, context_factory): - db.session.add(tag_category_factory(name='root')) + user_factory, tag_category_factory, context_factory +): + db.session.add(tag_category_factory(name="root")) db.session.commit() with pytest.raises(tag_categories.TagCategoryIsInUseError): api.tag_category_api.delete_tag_category( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'root'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "root"}, + ) def test_trying_to_delete_non_existing(user_factory, context_factory): with pytest.raises(tag_categories.TagCategoryNotFoundError): api.tag_category_api.delete_tag_category( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'bad'}) + {"category_name": "bad"}, + ) def test_trying_to_delete_without_privileges( - user_factory, tag_category_factory, context_factory): - db.session.add(tag_category_factory(name='category')) + user_factory, tag_category_factory, context_factory +): + db.session.add(tag_category_factory(name="category")) db.session.commit() with pytest.raises(errors.AuthError): api.tag_category_api.delete_tag_category( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'category_name': 'category'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"category_name": "category"}, + ) assert db.session.query(model.TagCategory).count() == 1 diff --git a/server/szurubooru/tests/api/test_tag_category_retrieving.py b/server/szurubooru/tests/api/test_tag_category_retrieving.py index 0b98d74..9c2c6fb 100644 --- a/server/szurubooru/tests/api/test_tag_category_retrieving.py +++ b/server/szurubooru/tests/api/test_tag_category_retrieving.py @@ -1,43 +1,49 @@ import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import tag_categories @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'tag_categories:list': model.User.RANK_REGULAR, - 'tag_categories:view': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "tag_categories:list": model.User.RANK_REGULAR, + "tag_categories:view": model.User.RANK_REGULAR, + }, + } + ) def test_retrieving_multiple( - user_factory, tag_category_factory, context_factory): - db.session.add_all([ - tag_category_factory(name='c1'), - tag_category_factory(name='c2'), - ]) + user_factory, tag_category_factory, context_factory +): + db.session.add_all( + [tag_category_factory(name="c1"), tag_category_factory(name="c2"),] + ) db.session.flush() result = api.tag_category_api.get_tag_categories( - context_factory(user=user_factory(rank=model.User.RANK_REGULAR))) - assert [cat['name'] for cat in result['results']] == ['c1', 'c2'] + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)) + ) + assert [cat["name"] for cat in result["results"]] == ["c1", "c2"] def test_retrieving_single( - user_factory, tag_category_factory, context_factory): - db.session.add(tag_category_factory(name='cat')) + user_factory, tag_category_factory, context_factory +): + db.session.add(tag_category_factory(name="cat")) db.session.flush() result = api.tag_category_api.get_tag_category( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'cat'}) + {"category_name": "cat"}, + ) assert result == { - 'name': 'cat', - 'color': 'dummy', - 'usages': 0, - 'default': False, - 'version': 1, + "name": "cat", + "color": "dummy", + "usages": 0, + "default": False, + "version": 1, } @@ -45,12 +51,15 @@ def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): with pytest.raises(tag_categories.TagCategoryNotFoundError): api.tag_category_api.get_tag_category( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': '-'}) + {"category_name": "-"}, + ) def test_trying_to_retrieve_single_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.tag_category_api.get_tag_category( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'category_name': '-'}) + {"category_name": "-"}, + ) diff --git a/server/szurubooru/tests/api/test_tag_category_updating.py b/server/szurubooru/tests/api/test_tag_category_updating.py index 24a9f6e..0d59390 100644 --- a/server/szurubooru/tests/api/test_tag_category_updating.py +++ b/server/szurubooru/tests/api/test_tag_category_updating.py @@ -1,7 +1,9 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import tag_categories, tags, snapshots + +from szurubooru import api, db, errors, model +from szurubooru.func import snapshots, tag_categories, tags def _update_category_name(category, name): @@ -10,99 +12,113 @@ def _update_category_name(category, name): @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'tag_categories:edit:name': model.User.RANK_REGULAR, - 'tag_categories:edit:color': model.User.RANK_REGULAR, - 'tag_categories:set_default': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "tag_categories:edit:name": model.User.RANK_REGULAR, + "tag_categories:edit:color": model.User.RANK_REGULAR, + "tag_categories:set_default": model.User.RANK_REGULAR, + }, + } + ) def test_simple_updating(user_factory, tag_category_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) - category = tag_category_factory(name='name', color='black') + category = tag_category_factory(name="name", color="black") db.session.add(category) db.session.flush() - with patch('szurubooru.func.tag_categories.serialize_category'), \ - patch('szurubooru.func.tag_categories.update_category_name'), \ - patch('szurubooru.func.tag_categories.update_category_color'), \ - patch('szurubooru.func.snapshots.modify'): + with patch("szurubooru.func.tag_categories.serialize_category"), patch( + "szurubooru.func.tag_categories.update_category_name" + ), patch("szurubooru.func.tag_categories.update_category_color"), patch( + "szurubooru.func.snapshots.modify" + ): tag_categories.update_category_name.side_effect = _update_category_name - tag_categories.serialize_category.return_value = 'serialized category' + tag_categories.serialize_category.return_value = "serialized category" result = api.tag_category_api.update_tag_category( context_factory( - params={'name': 'changed', 'color': 'white', 'version': 1}, - user=auth_user), - {'category_name': 'name'}) - assert result == 'serialized category' + params={"name": "changed", "color": "white", "version": 1}, + user=auth_user, + ), + {"category_name": "name"}, + ) + assert result == "serialized category" tag_categories.update_category_name.assert_called_once_with( - category, 'changed') + category, "changed" + ) tag_categories.update_category_color.assert_called_once_with( - category, 'white') + category, "white" + ) snapshots.modify.assert_called_once_with(category, auth_user) -@pytest.mark.parametrize('field', ['name', 'color']) +@pytest.mark.parametrize("field", ["name", "color"]) def test_omitting_optional_field( - user_factory, tag_category_factory, context_factory, field): - db.session.add(tag_category_factory(name='name', color='black')) + user_factory, tag_category_factory, context_factory, field +): + db.session.add(tag_category_factory(name="name", color="black")) db.session.commit() params = { - 'name': 'changed', - 'color': 'white', + "name": "changed", + "color": "white", } del params[field] - with patch('szurubooru.func.tag_categories.serialize_category'), \ - patch('szurubooru.func.tag_categories.update_category_name'): + with patch("szurubooru.func.tag_categories.serialize_category"), patch( + "szurubooru.func.tag_categories.update_category_name" + ): api.tag_category_api.update_tag_category( context_factory( - params={**params, **{'version': 1}}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'name'}) + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "name"}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(tag_categories.TagCategoryNotFoundError): api.tag_category_api.update_tag_category( context_factory( - params={'name': ['dummy']}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'bad'}) + params={"name": ["dummy"]}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "bad"}, + ) -@pytest.mark.parametrize('params', [ - {'name': 'whatever'}, - {'color': 'whatever'}, -]) +@pytest.mark.parametrize( + "params", [{"name": "whatever"}, {"color": "whatever"},] +) def test_trying_to_update_without_privileges( - user_factory, tag_category_factory, context_factory, params): - db.session.add(tag_category_factory(name='dummy')) + user_factory, tag_category_factory, context_factory, params +): + db.session.add(tag_category_factory(name="dummy")) db.session.commit() with pytest.raises(errors.AuthError): api.tag_category_api.update_tag_category( context_factory( - params={**params, **{'version': 1}}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'category_name': 'dummy'}) + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"category_name": "dummy"}, + ) def test_set_as_default(user_factory, tag_category_factory, context_factory): - category = tag_category_factory(name='name', color='black') + category = tag_category_factory(name="name", color="black") db.session.add(category) db.session.commit() - with patch('szurubooru.func.tag_categories.serialize_category'), \ - patch('szurubooru.func.tag_categories.set_default_category'): + with patch("szurubooru.func.tag_categories.serialize_category"), patch( + "szurubooru.func.tag_categories.set_default_category" + ): tag_categories.update_category_name.side_effect = _update_category_name - tag_categories.serialize_category.return_value = 'serialized category' + tag_categories.serialize_category.return_value = "serialized category" result = api.tag_category_api.set_tag_category_as_default( context_factory( - params={ - 'name': 'changed', - 'color': 'white', - 'version': 1, - }, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'name'}) - assert result == 'serialized category' + params={"name": "changed", "color": "white", "version": 1,}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "name"}, + ) + assert result == "serialized category" tag_categories.set_default_category.assert_called_once_with(category) diff --git a/server/szurubooru/tests/api/test_tag_creating.py b/server/szurubooru/tests/api/test_tag_creating.py index 4cee710..648b317 100644 --- a/server/szurubooru/tests/api/test_tag_creating.py +++ b/server/szurubooru/tests/api/test_tag_creating.py @@ -1,84 +1,97 @@ from unittest.mock import patch + import pytest -from szurubooru import api, model, errors -from szurubooru.func import tags, snapshots + +from szurubooru import api, errors, model +from szurubooru.func import snapshots, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'tags:create': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"tags:create": model.User.RANK_REGULAR}}) def test_creating_simple_tags(tag_factory, user_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) tag = tag_factory() - with patch('szurubooru.func.tags.create_tag'), \ - patch('szurubooru.func.tags.get_or_create_tags_by_names'), \ - patch('szurubooru.func.tags.serialize_tag'), \ - patch('szurubooru.func.snapshots.create'): + with patch("szurubooru.func.tags.create_tag"), patch( + "szurubooru.func.tags.get_or_create_tags_by_names" + ), patch("szurubooru.func.tags.serialize_tag"), patch( + "szurubooru.func.snapshots.create" + ): tags.get_or_create_tags_by_names.return_value = ([], []) tags.create_tag.return_value = tag - tags.serialize_tag.return_value = 'serialized tag' + tags.serialize_tag.return_value = "serialized tag" result = api.tag_api.create_tag( context_factory( params={ - 'names': ['tag1', 'tag2'], - 'category': 'meta', - 'description': 'desc', - 'suggestions': ['sug1', 'sug2'], - 'implications': ['imp1', 'imp2'], + "names": ["tag1", "tag2"], + "category": "meta", + "description": "desc", + "suggestions": ["sug1", "sug2"], + "implications": ["imp1", "imp2"], }, - user=auth_user)) - assert result == 'serialized tag' + user=auth_user, + ) + ) + assert result == "serialized tag" tags.create_tag.assert_called_once_with( - ['tag1', 'tag2'], 'meta', ['sug1', 'sug2'], ['imp1', 'imp2']) + ["tag1", "tag2"], "meta", ["sug1", "sug2"], ["imp1", "imp2"] + ) snapshots.create.assert_called_once_with(tag, auth_user) -@pytest.mark.parametrize('field', ['names', 'category']) +@pytest.mark.parametrize("field", ["names", "category"]) def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): params = { - 'names': ['tag1', 'tag2'], - 'category': 'meta', - 'suggestions': [], - 'implications': [], + "names": ["tag1", "tag2"], + "category": "meta", + "suggestions": [], + "implications": [], } del params[field] with pytest.raises(errors.ValidationError): api.tag_api.create_tag( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) -@pytest.mark.parametrize('field', ['implications', 'suggestions']) +@pytest.mark.parametrize("field", ["implications", "suggestions"]) def test_omitting_optional_field( - tag_factory, user_factory, context_factory, field): + tag_factory, user_factory, context_factory, field +): params = { - 'names': ['tag1', 'tag2'], - 'category': 'meta', - 'suggestions': [], - 'implications': [], + "names": ["tag1", "tag2"], + "category": "meta", + "suggestions": [], + "implications": [], } del params[field] - with patch('szurubooru.func.tags.create_tag'), \ - patch('szurubooru.func.tags.serialize_tag'): + with patch("szurubooru.func.tags.create_tag"), patch( + "szurubooru.func.tags.serialize_tag" + ): tags.create_tag.return_value = tag_factory() api.tag_api.create_tag( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) def test_trying_to_create_tag_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.tag_api.create_tag( context_factory( params={ - 'names': ['tag'], - 'category': 'meta', - 'suggestions': ['tag'], - 'implications': [], + "names": ["tag"], + "category": "meta", + "suggestions": ["tag"], + "implications": [], }, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_tag_deleting.py b/server/szurubooru/tests/api/test_tag_deleting.py index a0367f2..b45ef55 100644 --- a/server/szurubooru/tests/api/test_tag_deleting.py +++ b/server/szurubooru/tests/api/test_tag_deleting.py @@ -1,40 +1,46 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import tags, snapshots + +from szurubooru import api, db, errors, model +from szurubooru.func import snapshots, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'tags:delete': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"tags:delete": model.User.RANK_REGULAR}}) def test_deleting(user_factory, tag_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) - tag = tag_factory(names=['tag']) + tag = tag_factory(names=["tag"]) db.session.add(tag) db.session.commit() - with patch('szurubooru.func.snapshots.delete'): + with patch("szurubooru.func.snapshots.delete"): result = api.tag_api.delete_tag( - context_factory(params={'version': 1}, user=auth_user), - {'tag_name': 'tag'}) + context_factory(params={"version": 1}, user=auth_user), + {"tag_name": "tag"}, + ) assert result == {} assert db.session.query(model.Tag).count() == 0 snapshots.delete.assert_called_once_with(tag, auth_user) def test_deleting_used( - user_factory, tag_factory, context_factory, post_factory): - tag = tag_factory(names=['tag']) + user_factory, tag_factory, context_factory, post_factory +): + tag = tag_factory(names=["tag"]) post = post_factory() post.tags.append(tag) db.session.add_all([tag, post]) db.session.commit() api.tag_api.delete_tag( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"tag_name": "tag"}, + ) db.session.refresh(post) assert db.session.query(model.Tag).count() == 0 assert post.tags == [] @@ -44,17 +50,21 @@ def test_trying_to_delete_non_existing(user_factory, context_factory): with pytest.raises(tags.TagNotFoundError): api.tag_api.delete_tag( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'bad'}) + {"tag_name": "bad"}, + ) def test_trying_to_delete_without_privileges( - user_factory, tag_factory, context_factory): - db.session.add(tag_factory(names=['tag'])) + user_factory, tag_factory, context_factory +): + db.session.add(tag_factory(names=["tag"])) db.session.commit() with pytest.raises(errors.AuthError): api.tag_api.delete_tag( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'tag_name': 'tag'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"tag_name": "tag"}, + ) assert db.session.query(model.Tag).count() == 1 diff --git a/server/szurubooru/tests/api/test_tag_merging.py b/server/szurubooru/tests/api/test_tag_merging.py index 671e2e4..be7e887 100644 --- a/server/szurubooru/tests/api/test_tag_merging.py +++ b/server/szurubooru/tests/api/test_tag_merging.py @@ -1,18 +1,20 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import tags, snapshots + +from szurubooru import api, db, errors, model +from szurubooru.func import snapshots, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'tags:merge': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"tags:merge": model.User.RANK_REGULAR}}) def test_merging(user_factory, tag_factory, context_factory, post_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) db.session.add_all([source_tag, target_tag]) db.session.flush() assert source_tag.post_count == 0 @@ -23,76 +25,88 @@ def test_merging(user_factory, tag_factory, context_factory, post_factory): db.session.commit() assert source_tag.post_count == 1 assert target_tag.post_count == 0 - with patch('szurubooru.func.tags.serialize_tag'), \ - patch('szurubooru.func.tags.merge_tags'), \ - patch('szurubooru.func.snapshots.merge'): + with patch("szurubooru.func.tags.serialize_tag"), patch( + "szurubooru.func.tags.merge_tags" + ), patch("szurubooru.func.snapshots.merge"): api.tag_api.merge_tags( context_factory( params={ - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': 'source', - 'mergeTo': 'target', + "removeVersion": 1, + "mergeToVersion": 1, + "remove": "source", + "mergeTo": "target", }, - user=auth_user)) + user=auth_user, + ) + ) tags.merge_tags.called_once_with(source_tag, target_tag) snapshots.merge.assert_called_once_with( - source_tag, target_tag, auth_user) + source_tag, target_tag, auth_user + ) @pytest.mark.parametrize( - 'field', ['remove', 'mergeTo', 'removeVersion', 'mergeToVersion']) + "field", ["remove", "mergeTo", "removeVersion", "mergeToVersion"] +) def test_trying_to_omit_mandatory_field( - user_factory, tag_factory, context_factory, field): - db.session.add_all([ - tag_factory(names=['source']), - tag_factory(names=['target']), - ]) + user_factory, tag_factory, context_factory, field +): + db.session.add_all( + [tag_factory(names=["source"]), tag_factory(names=["target"]),] + ) db.session.commit() params = { - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': 'source', - 'mergeTo': 'target', + "removeVersion": 1, + "mergeToVersion": 1, + "remove": "source", + "mergeTo": "target", } del params[field] with pytest.raises(errors.ValidationError): api.tag_api.merge_tags( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) def test_trying_to_merge_non_existing( - user_factory, tag_factory, context_factory): - db.session.add(tag_factory(names=['good'])) + user_factory, tag_factory, context_factory +): + db.session.add(tag_factory(names=["good"])) db.session.commit() with pytest.raises(tags.TagNotFoundError): api.tag_api.merge_tags( context_factory( - params={'remove': 'good', 'mergeTo': 'bad'}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"remove": "good", "mergeTo": "bad"}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) with pytest.raises(tags.TagNotFoundError): api.tag_api.merge_tags( context_factory( - params={'remove': 'bad', 'mergeTo': 'good'}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"remove": "bad", "mergeTo": "good"}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) def test_trying_to_merge_without_privileges( - user_factory, tag_factory, context_factory): - db.session.add_all([ - tag_factory(names=['source']), - tag_factory(names=['target']), - ]) + user_factory, tag_factory, context_factory +): + db.session.add_all( + [tag_factory(names=["source"]), tag_factory(names=["target"]),] + ) db.session.commit() with pytest.raises(errors.AuthError): api.tag_api.merge_tags( context_factory( params={ - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': 'source', - 'mergeTo': 'target', + "removeVersion": 1, + "mergeToVersion": 1, + "remove": "source", + "mergeTo": "target", }, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_tag_retrieving.py b/server/szurubooru/tests/api/test_tag_retrieving.py index c720b6e..8b51de5 100644 --- a/server/szurubooru/tests/api/test_tag_retrieving.py +++ b/server/szurubooru/tests/api/test_tag_retrieving.py @@ -1,72 +1,82 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'tags:list': model.User.RANK_REGULAR, - 'tags:view': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "tags:list": model.User.RANK_REGULAR, + "tags:view": model.User.RANK_REGULAR, + }, + } + ) def test_retrieving_multiple(user_factory, tag_factory, context_factory): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) db.session.add_all([tag2, tag1]) db.session.flush() - with patch('szurubooru.func.tags.serialize_tag'): - tags.serialize_tag.return_value = 'serialized tag' + with patch("szurubooru.func.tags.serialize_tag"): + tags.serialize_tag.return_value = "serialized tag" result = api.tag_api.get_tags( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) assert result == { - 'query': '', - 'offset': 0, - 'limit': 100, - 'total': 2, - 'results': ['serialized tag', 'serialized tag'], + "query": "", + "offset": 0, + "limit": 100, + "total": 2, + "results": ["serialized tag", "serialized tag"], } def test_trying_to_retrieve_multiple_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.tag_api.get_tags( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) def test_retrieving_single(user_factory, tag_factory, context_factory): - db.session.add(tag_factory(names=['tag'])) + db.session.add(tag_factory(names=["tag"])) db.session.flush() - with patch('szurubooru.func.tags.serialize_tag'): - tags.serialize_tag.return_value = 'serialized tag' + with patch("szurubooru.func.tags.serialize_tag"): + tags.serialize_tag.return_value = "serialized tag" result = api.tag_api.get_tag( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag'}) - assert result == 'serialized tag' + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"tag_name": "tag"}, + ) + assert result == "serialized tag" def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): with pytest.raises(tags.TagNotFoundError): api.tag_api.get_tag( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': '-'}) + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"tag_name": "-"}, + ) def test_trying_to_retrieve_single_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.tag_api.get_tag( - context_factory( - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'tag_name': '-'}) + context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), + {"tag_name": "-"}, + ) diff --git a/server/szurubooru/tests/api/test_tag_siblings_retrieving.py b/server/szurubooru/tests/api/test_tag_siblings_retrieving.py index fc2f5aa..26f6666 100644 --- a/server/szurubooru/tests/api/test_tag_siblings_retrieving.py +++ b/server/szurubooru/tests/api/test_tag_siblings_retrieving.py @@ -1,38 +1,38 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'tags:view': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"tags:view": model.User.RANK_REGULAR}}) def test_get_tag_siblings(user_factory, tag_factory, context_factory): - db.session.add(tag_factory(names=['tag'])) + db.session.add(tag_factory(names=["tag"])) db.session.flush() - with patch('szurubooru.func.tags.serialize_tag'), \ - patch('szurubooru.func.tags.get_tag_siblings'): - tags.serialize_tag.side_effect = lambda tag, *args, **kwargs: \ - 'serialized tag %s' % tag.names[0].name + with patch("szurubooru.func.tags.serialize_tag"), patch( + "szurubooru.func.tags.get_tag_siblings" + ): + tags.serialize_tag.side_effect = ( + lambda tag, *args, **kwargs: "serialized tag %s" + % tag.names[0].name + ) tags.get_tag_siblings.return_value = [ - (tag_factory(names=['sib1']), 1), - (tag_factory(names=['sib2']), 3), + (tag_factory(names=["sib1"]), 1), + (tag_factory(names=["sib2"]), 3), ] result = api.tag_api.get_tag_siblings( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag'}) + {"tag_name": "tag"}, + ) assert result == { - 'results': [ - { - 'tag': 'serialized tag sib1', - 'occurrences': 1, - }, - { - 'tag': 'serialized tag sib2', - 'occurrences': 3, - }, + "results": [ + {"tag": "serialized tag sib1", "occurrences": 1,}, + {"tag": "serialized tag sib2", "occurrences": 3,}, ], } @@ -41,11 +41,13 @@ def test_trying_to_retrieve_non_existing(user_factory, context_factory): with pytest.raises(tags.TagNotFoundError): api.tag_api.get_tag_siblings( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': '-'}) + {"tag_name": "-"}, + ) def test_trying_to_retrieve_without_privileges(user_factory, context_factory): with pytest.raises(errors.AuthError): api.tag_api.get_tag_siblings( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'tag_name': '-'}) + {"tag_name": "-"}, + ) diff --git a/server/szurubooru/tests/api/test_tag_updating.py b/server/szurubooru/tests/api/test_tag_updating.py index 0b8aa70..e2b0e00 100644 --- a/server/szurubooru/tests/api/test_tag_updating.py +++ b/server/szurubooru/tests/api/test_tag_updating.py @@ -1,144 +1,175 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import tags, snapshots + +from szurubooru import api, db, errors, model +from szurubooru.func import snapshots, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'tags:create': model.User.RANK_REGULAR, - 'tags:edit:names': model.User.RANK_REGULAR, - 'tags:edit:category': model.User.RANK_REGULAR, - 'tags:edit:description': model.User.RANK_REGULAR, - 'tags:edit:suggestions': model.User.RANK_REGULAR, - 'tags:edit:implications': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "tags:create": model.User.RANK_REGULAR, + "tags:edit:names": model.User.RANK_REGULAR, + "tags:edit:category": model.User.RANK_REGULAR, + "tags:edit:description": model.User.RANK_REGULAR, + "tags:edit:suggestions": model.User.RANK_REGULAR, + "tags:edit:implications": model.User.RANK_REGULAR, + }, + } + ) def test_simple_updating(user_factory, tag_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) - tag = tag_factory(names=['tag1', 'tag2']) + tag = tag_factory(names=["tag1", "tag2"]) db.session.add(tag) db.session.commit() - with patch('szurubooru.func.tags.create_tag'), \ - patch('szurubooru.func.tags.get_or_create_tags_by_names'), \ - patch('szurubooru.func.tags.update_tag_names'), \ - patch('szurubooru.func.tags.update_tag_category_name'), \ - patch('szurubooru.func.tags.update_tag_description'), \ - patch('szurubooru.func.tags.update_tag_suggestions'), \ - patch('szurubooru.func.tags.update_tag_implications'), \ - patch('szurubooru.func.tags.serialize_tag'), \ - patch('szurubooru.func.snapshots.modify'): + with patch("szurubooru.func.tags.create_tag"), patch( + "szurubooru.func.tags.get_or_create_tags_by_names" + ), patch("szurubooru.func.tags.update_tag_names"), patch( + "szurubooru.func.tags.update_tag_category_name" + ), patch( + "szurubooru.func.tags.update_tag_description" + ), patch( + "szurubooru.func.tags.update_tag_suggestions" + ), patch( + "szurubooru.func.tags.update_tag_implications" + ), patch( + "szurubooru.func.tags.serialize_tag" + ), patch( + "szurubooru.func.snapshots.modify" + ): tags.get_or_create_tags_by_names.return_value = ([], []) - tags.serialize_tag.return_value = 'serialized tag' + tags.serialize_tag.return_value = "serialized tag" result = api.tag_api.update_tag( context_factory( params={ - 'version': 1, - 'names': ['tag3'], - 'category': 'character', - 'description': 'desc', - 'suggestions': ['sug1', 'sug2'], - 'implications': ['imp1', 'imp2'], + "version": 1, + "names": ["tag3"], + "category": "character", + "description": "desc", + "suggestions": ["sug1", "sug2"], + "implications": ["imp1", "imp2"], }, - user=auth_user), - {'tag_name': 'tag1'}) - assert result == 'serialized tag' + user=auth_user, + ), + {"tag_name": "tag1"}, + ) + assert result == "serialized tag" tags.create_tag.assert_not_called() - tags.update_tag_names.assert_called_once_with(tag, ['tag3']) - tags.update_tag_category_name.assert_called_once_with(tag, 'character') - tags.update_tag_description.assert_called_once_with(tag, 'desc') + tags.update_tag_names.assert_called_once_with(tag, ["tag3"]) + tags.update_tag_category_name.assert_called_once_with(tag, "character") + tags.update_tag_description.assert_called_once_with(tag, "desc") tags.update_tag_suggestions.assert_called_once_with( - tag, ['sug1', 'sug2']) + tag, ["sug1", "sug2"] + ) tags.update_tag_implications.assert_called_once_with( - tag, ['imp1', 'imp2']) + tag, ["imp1", "imp2"] + ) tags.serialize_tag.assert_called_once_with(tag, options=[]) snapshots.modify.assert_called_once_with(tag, auth_user) @pytest.mark.parametrize( - 'field', [ - 'names', - 'category', - 'description', - 'implications', - 'suggestions', - ]) + "field", + ["names", "category", "description", "implications", "suggestions",], +) def test_omitting_optional_field( - user_factory, tag_factory, context_factory, field): - db.session.add(tag_factory(names=['tag'])) + user_factory, tag_factory, context_factory, field +): + db.session.add(tag_factory(names=["tag"])) db.session.commit() params = { - 'names': ['tag1', 'tag2'], - 'category': 'meta', - 'description': 'desc', - 'suggestions': [], - 'implications': [], + "names": ["tag1", "tag2"], + "category": "meta", + "description": "desc", + "suggestions": [], + "implications": [], } del params[field] - with patch('szurubooru.func.tags.create_tag'), \ - patch('szurubooru.func.tags.update_tag_names'), \ - patch('szurubooru.func.tags.update_tag_category_name'), \ - patch('szurubooru.func.tags.serialize_tag'): + with patch("szurubooru.func.tags.create_tag"), patch( + "szurubooru.func.tags.update_tag_names" + ), patch("szurubooru.func.tags.update_tag_category_name"), patch( + "szurubooru.func.tags.serialize_tag" + ): api.tag_api.update_tag( context_factory( - params={**params, **{'version': 1}}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag'}) + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"tag_name": "tag"}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(tags.TagNotFoundError): api.tag_api.update_tag( context_factory( - params={'names': ['dummy']}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag1'}) + params={"names": ["dummy"]}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"tag_name": "tag1"}, + ) -@pytest.mark.parametrize('params', [ - {'names': 'whatever'}, - {'category': 'whatever'}, - {'suggestions': ['whatever']}, - {'implications': ['whatever']}, -]) +@pytest.mark.parametrize( + "params", + [ + {"names": "whatever"}, + {"category": "whatever"}, + {"suggestions": ["whatever"]}, + {"implications": ["whatever"]}, + ], +) def test_trying_to_update_without_privileges( - user_factory, tag_factory, context_factory, params): - db.session.add(tag_factory(names=['tag'])) + user_factory, tag_factory, context_factory, params +): + db.session.add(tag_factory(names=["tag"])) db.session.commit() with pytest.raises(errors.AuthError): api.tag_api.update_tag( context_factory( - params={**params, **{'version': 1}}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'tag_name': 'tag'}) + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"tag_name": "tag"}, + ) def test_trying_to_create_tags_without_privileges( - config_injector, context_factory, tag_factory, user_factory): - tag = tag_factory(names=['tag']) + config_injector, context_factory, tag_factory, user_factory +): + tag = tag_factory(names=["tag"]) db.session.add(tag) db.session.commit() - config_injector({'privileges': { - 'tags:create': model.User.RANK_ADMINISTRATOR, - 'tags:edit:suggestions': model.User.RANK_REGULAR, - 'tags:edit:implications': model.User.RANK_REGULAR, - }}) - with patch('szurubooru.func.tags.get_or_create_tags_by_names'): - tags.get_or_create_tags_by_names.return_value = ([], ['new-tag']) + config_injector( + { + "privileges": { + "tags:create": model.User.RANK_ADMINISTRATOR, + "tags:edit:suggestions": model.User.RANK_REGULAR, + "tags:edit:implications": model.User.RANK_REGULAR, + } + } + ) + with patch("szurubooru.func.tags.get_or_create_tags_by_names"): + tags.get_or_create_tags_by_names.return_value = ([], ["new-tag"]) with pytest.raises(errors.AuthError): api.tag_api.update_tag( context_factory( - params={'suggestions': ['tag1', 'tag2'], 'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag'}) + params={"suggestions": ["tag1", "tag2"], "version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"tag_name": "tag"}, + ) db.session.rollback() with pytest.raises(errors.AuthError): api.tag_api.update_tag( context_factory( - params={'implications': ['tag1', 'tag2'], 'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag'}) + params={"implications": ["tag1", "tag2"], "version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"tag_name": "tag"}, + ) diff --git a/server/szurubooru/tests/api/test_user_creating.py b/server/szurubooru/tests/api/test_user_creating.py index 699bfef..d55e1f7 100644 --- a/server/szurubooru/tests/api/test_user_creating.py +++ b/server/szurubooru/tests/api/test_user_creating.py @@ -1,87 +1,104 @@ from unittest.mock import patch + import pytest -from szurubooru import api, model, errors + +from szurubooru import api, errors, model from szurubooru.func import users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'users:create:self': 'regular'}}) + config_injector({"privileges": {"users:create:self": "regular"}}) def test_creating_user(user_factory, context_factory, fake_datetime): user = user_factory() - with patch('szurubooru.func.users.create_user'), \ - patch('szurubooru.func.users.update_user_name'), \ - patch('szurubooru.func.users.update_user_password'), \ - patch('szurubooru.func.users.update_user_email'), \ - patch('szurubooru.func.users.update_user_rank'), \ - patch('szurubooru.func.users.update_user_avatar'), \ - patch('szurubooru.func.users.serialize_user'), \ - fake_datetime('1969-02-12'): - users.serialize_user.return_value = 'serialized user' + with patch("szurubooru.func.users.create_user"), patch( + "szurubooru.func.users.update_user_name" + ), patch("szurubooru.func.users.update_user_password"), patch( + "szurubooru.func.users.update_user_email" + ), patch( + "szurubooru.func.users.update_user_rank" + ), patch( + "szurubooru.func.users.update_user_avatar" + ), patch( + "szurubooru.func.users.serialize_user" + ), fake_datetime( + "1969-02-12" + ): + users.serialize_user.return_value = "serialized user" users.create_user.return_value = user result = api.user_api.create_user( context_factory( params={ - 'name': 'chewie1', - 'email': 'asd@asd.asd', - 'password': 'oks', - 'rank': 'moderator', - 'avatarStyle': 'manual', + "name": "chewie1", + "email": "asd@asd.asd", + "password": "oks", + "rank": "moderator", + "avatarStyle": "manual", }, - files={'avatar': b'...'}, - user=user_factory(rank=model.User.RANK_REGULAR))) - assert result == 'serialized user' + files={"avatar": b"..."}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) + assert result == "serialized user" users.create_user.assert_called_once_with( - 'chewie1', 'oks', 'asd@asd.asd') + "chewie1", "oks", "asd@asd.asd" + ) assert not users.update_user_name.called assert not users.update_user_password.called assert not users.update_user_email.called - users.update_user_rank.called_once_with(user, 'moderator') - users.update_user_avatar.called_once_with(user, 'manual', b'...') + users.update_user_rank.called_once_with(user, "moderator") + users.update_user_avatar.called_once_with(user, "manual", b"...") -@pytest.mark.parametrize('field', ['name', 'password']) +@pytest.mark.parametrize("field", ["name", "password"]) def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): params = { - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', + "name": "chewie", + "email": "asd@asd.asd", + "password": "oks", } user = user_factory() auth_user = user_factory(rank=model.User.RANK_REGULAR) del params[field] - with patch('szurubooru.func.users.create_user'), \ - pytest.raises(errors.MissingRequiredParameterError): + with patch("szurubooru.func.users.create_user"), pytest.raises( + errors.MissingRequiredParameterError + ): users.create_user.return_value = user api.user_api.create_user( - context_factory(params=params, user=auth_user)) + context_factory(params=params, user=auth_user) + ) -@pytest.mark.parametrize('field', ['rank', 'email', 'avatarStyle']) +@pytest.mark.parametrize("field", ["rank", "email", "avatarStyle"]) def test_omitting_optional_field(user_factory, context_factory, field): params = { - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', - 'rank': 'moderator', - 'avatarStyle': 'gravatar', + "name": "chewie", + "email": "asd@asd.asd", + "password": "oks", + "rank": "moderator", + "avatarStyle": "gravatar", } del params[field] user = user_factory() auth_user = user_factory(rank=model.User.RANK_MODERATOR) - with patch('szurubooru.func.users.create_user'), \ - patch('szurubooru.func.users.update_user_avatar'), \ - patch('szurubooru.func.users.serialize_user'): + with patch("szurubooru.func.users.create_user"), patch( + "szurubooru.func.users.update_user_avatar" + ), patch("szurubooru.func.users.serialize_user"): users.create_user.return_value = user api.user_api.create_user( - context_factory(params=params, user=auth_user)) + context_factory(params=params, user=auth_user) + ) def test_trying_to_create_user_without_privileges( - context_factory, user_factory): + context_factory, user_factory +): with pytest.raises(errors.AuthError): - api.user_api.create_user(context_factory( - params='whatever', - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + api.user_api.create_user( + context_factory( + params="whatever", + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_user_deleting.py b/server/szurubooru/tests/api/test_user_deleting.py index 2bd53e2..6ab3f1d 100644 --- a/server/szurubooru/tests/api/test_user_deleting.py +++ b/server/szurubooru/tests/api/test_user_deleting.py @@ -1,50 +1,55 @@ import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'users:delete:self': model.User.RANK_REGULAR, - 'users:delete:any': model.User.RANK_MODERATOR, - }, - }) + config_injector( + { + "privileges": { + "users:delete:self": model.User.RANK_REGULAR, + "users:delete:any": model.User.RANK_MODERATOR, + }, + } + ) def test_deleting_oneself(user_factory, context_factory): - user = user_factory(name='u', rank=model.User.RANK_REGULAR) + user = user_factory(name="u", rank=model.User.RANK_REGULAR) db.session.add(user) db.session.commit() result = api.user_api.delete_user( - context_factory( - params={'version': 1}, user=user), {'user_name': 'u'}) + context_factory(params={"version": 1}, user=user), {"user_name": "u"} + ) assert result == {} assert db.session.query(model.User).count() == 0 def test_deleting_someone_else(user_factory, context_factory): - user1 = user_factory(name='u1', rank=model.User.RANK_REGULAR) - user2 = user_factory(name='u2', rank=model.User.RANK_MODERATOR) + user1 = user_factory(name="u1", rank=model.User.RANK_REGULAR) + user2 = user_factory(name="u2", rank=model.User.RANK_MODERATOR) db.session.add_all([user1, user2]) db.session.commit() api.user_api.delete_user( - context_factory( - params={'version': 1}, user=user2), {'user_name': 'u1'}) + context_factory(params={"version": 1}, user=user2), {"user_name": "u1"} + ) assert db.session.query(model.User).count() == 1 def test_trying_to_delete_someone_else_without_privileges( - user_factory, context_factory): - user1 = user_factory(name='u1', rank=model.User.RANK_REGULAR) - user2 = user_factory(name='u2', rank=model.User.RANK_REGULAR) + user_factory, context_factory +): + user1 = user_factory(name="u1", rank=model.User.RANK_REGULAR) + user2 = user_factory(name="u2", rank=model.User.RANK_REGULAR) db.session.add_all([user1, user2]) db.session.commit() with pytest.raises(errors.AuthError): api.user_api.delete_user( - context_factory( - params={'version': 1}, user=user2), {'user_name': 'u1'}) + context_factory(params={"version": 1}, user=user2), + {"user_name": "u1"}, + ) assert db.session.query(model.User).count() == 2 @@ -52,6 +57,8 @@ def test_trying_to_delete_non_existing(user_factory, context_factory): with pytest.raises(users.UserNotFoundError): api.user_api.delete_user( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'user_name': 'bad'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"user_name": "bad"}, + ) diff --git a/server/szurubooru/tests/api/test_user_retrieving.py b/server/szurubooru/tests/api/test_user_retrieving.py index 2e797e8..b18b4d5 100644 --- a/server/szurubooru/tests/api/test_user_retrieving.py +++ b/server/szurubooru/tests/api/test_user_retrieving.py @@ -1,73 +1,86 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'users:list': model.User.RANK_REGULAR, - 'users:view': model.User.RANK_REGULAR, - 'users:edit:any:email': model.User.RANK_MODERATOR, - }, - }) + config_injector( + { + "privileges": { + "users:list": model.User.RANK_REGULAR, + "users:view": model.User.RANK_REGULAR, + "users:edit:any:email": model.User.RANK_MODERATOR, + }, + } + ) def test_retrieving_multiple(user_factory, context_factory): - user1 = user_factory(name='u1', rank=model.User.RANK_MODERATOR) - user2 = user_factory(name='u2', rank=model.User.RANK_MODERATOR) + user1 = user_factory(name="u1", rank=model.User.RANK_MODERATOR) + user2 = user_factory(name="u2", rank=model.User.RANK_MODERATOR) db.session.add_all([user1, user2]) db.session.flush() - with patch('szurubooru.func.users.serialize_user'): - users.serialize_user.return_value = 'serialized user' + with patch("szurubooru.func.users.serialize_user"): + users.serialize_user.return_value = "serialized user" result = api.user_api.get_users( context_factory( - params={'query': '', 'page': 1}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"query": "", "page": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) assert result == { - 'query': '', - 'offset': 0, - 'limit': 100, - 'total': 2, - 'results': ['serialized user', 'serialized user'], + "query": "", + "offset": 0, + "limit": 100, + "total": 2, + "results": ["serialized user", "serialized user"], } def test_trying_to_retrieve_multiple_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.user_api.get_users( context_factory( - params={'query': '', 'page': 1}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"query": "", "page": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) def test_retrieving_single(user_factory, context_factory): - user = user_factory(name='u1', rank=model.User.RANK_REGULAR) + user = user_factory(name="u1", rank=model.User.RANK_REGULAR) auth_user = user_factory(rank=model.User.RANK_REGULAR) db.session.add(user) db.session.flush() - with patch('szurubooru.func.users.serialize_user'): - users.serialize_user.return_value = 'serialized user' + with patch("szurubooru.func.users.serialize_user"): + users.serialize_user.return_value = "serialized user" result = api.user_api.get_user( - context_factory(user=auth_user), {'user_name': 'u1'}) - assert result == 'serialized user' + context_factory(user=auth_user), {"user_name": "u1"} + ) + assert result == "serialized user" def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) with pytest.raises(users.UserNotFoundError): api.user_api.get_user( - context_factory(user=auth_user), {'user_name': '-'}) + context_factory(user=auth_user), {"user_name": "-"} + ) def test_trying_to_retrieve_single_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): auth_user = user_factory(rank=model.User.RANK_ANONYMOUS) - db.session.add(user_factory(name='u1', rank=model.User.RANK_REGULAR)) + db.session.add(user_factory(name="u1", rank=model.User.RANK_REGULAR)) db.session.flush() with pytest.raises(errors.AuthError): api.user_api.get_user( - context_factory(user=auth_user), {'user_name': 'u1'}) + context_factory(user=auth_user), {"user_name": "u1"} + ) diff --git a/server/szurubooru/tests/api/test_user_token_creating.py b/server/szurubooru/tests/api/test_user_token_creating.py index f550f63..bce41ec 100644 --- a/server/szurubooru/tests/api/test_user_token_creating.py +++ b/server/szurubooru/tests/api/test_user_token_creating.py @@ -1,29 +1,33 @@ from unittest.mock import patch + import pytest + from szurubooru import api from szurubooru.func import user_tokens, users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'user_tokens:create:self': 'regular'}}) + config_injector({"privileges": {"user_tokens:create:self": "regular"}}) def test_creating_user_token( - user_token_factory, context_factory, fake_datetime): + user_token_factory, context_factory, fake_datetime +): user_token = user_token_factory() - with patch('szurubooru.func.user_tokens.create_user_token'), \ - patch('szurubooru.func.user_tokens.serialize_user_token'), \ - patch('szurubooru.func.users.get_user_by_name'), \ - fake_datetime('1969-02-12'): + with patch("szurubooru.func.user_tokens.create_user_token"), patch( + "szurubooru.func.user_tokens.serialize_user_token" + ), patch("szurubooru.func.users.get_user_by_name"), fake_datetime( + "1969-02-12" + ): users.get_user_by_name.return_value = user_token.user - user_tokens.serialize_user_token.return_value = 'serialized user token' + user_tokens.serialize_user_token.return_value = "serialized user token" user_tokens.create_user_token.return_value = user_token result = api.user_token_api.create_user_token( context_factory(user=user_token.user), - { - 'user_name': user_token.user.name - }) - assert result == 'serialized user token' + {"user_name": user_token.user.name}, + ) + assert result == "serialized user token" user_tokens.create_user_token.assert_called_once_with( - user_token.user, True) + user_token.user, True + ) diff --git a/server/szurubooru/tests/api/test_user_token_deleting.py b/server/szurubooru/tests/api/test_user_token_deleting.py index 8534152..19f93fa 100644 --- a/server/szurubooru/tests/api/test_user_token_deleting.py +++ b/server/szurubooru/tests/api/test_user_token_deleting.py @@ -1,30 +1,35 @@ from unittest.mock import patch + import pytest + from szurubooru import api, db from szurubooru.func import user_tokens, users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'user_tokens:delete:self': 'regular'}}) + config_injector({"privileges": {"user_tokens:delete:self": "regular"}}) def test_deleting_user_token( - user_token_factory, context_factory, fake_datetime): + user_token_factory, context_factory, fake_datetime +): user_token = user_token_factory() db.session.add(user_token) db.session.commit() - with patch('szurubooru.func.user_tokens.get_by_user_and_token'), \ - patch('szurubooru.func.users.get_user_by_name'), \ - fake_datetime('1969-02-12'): + with patch("szurubooru.func.user_tokens.get_by_user_and_token"), patch( + "szurubooru.func.users.get_user_by_name" + ), fake_datetime("1969-02-12"): users.get_user_by_name.return_value = user_token.user user_tokens.get_by_user_and_token.return_value = user_token result = api.user_token_api.delete_user_token( context_factory(user=user_token.user), { - 'user_name': user_token.user.name, - 'user_token': user_token.token - }) + "user_name": user_token.user.name, + "user_token": user_token.token, + }, + ) assert result == {} user_tokens.get_by_user_and_token.assert_called_once_with( - user_token.user, user_token.token) + user_token.user, user_token.token + ) diff --git a/server/szurubooru/tests/api/test_user_token_retrieving.py b/server/szurubooru/tests/api/test_user_token_retrieving.py index 01b2534..e3351b4 100644 --- a/server/szurubooru/tests/api/test_user_token_retrieving.py +++ b/server/szurubooru/tests/api/test_user_token_retrieving.py @@ -1,31 +1,37 @@ from unittest.mock import patch + import pytest + from szurubooru import api from szurubooru.func import user_tokens, users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'user_tokens:list:self': 'regular'}}) + config_injector({"privileges": {"user_tokens:list:self": "regular"}}) def test_retrieving_user_tokens( - user_token_factory, context_factory, fake_datetime): + user_token_factory, context_factory, fake_datetime +): user_token1 = user_token_factory() user_token2 = user_token_factory(user=user_token1.user) user_token3 = user_token_factory(user=user_token1.user) - with patch('szurubooru.func.user_tokens.get_user_tokens'), \ - patch('szurubooru.func.user_tokens.serialize_user_token'), \ - patch('szurubooru.func.users.get_user_by_name'), \ - fake_datetime('1969-02-12'): + with patch("szurubooru.func.user_tokens.get_user_tokens"), patch( + "szurubooru.func.user_tokens.serialize_user_token" + ), patch("szurubooru.func.users.get_user_by_name"), fake_datetime( + "1969-02-12" + ): users.get_user_by_name.return_value = user_token1.user - user_tokens.serialize_user_token.return_value = 'serialized user token' - user_tokens.get_user_tokens.return_value = [user_token1, user_token2, - user_token3] + user_tokens.serialize_user_token.return_value = "serialized user token" + user_tokens.get_user_tokens.return_value = [ + user_token1, + user_token2, + user_token3, + ] result = api.user_token_api.get_user_tokens( context_factory(user=user_token1.user), - { - 'user_name': user_token1.user.name - }) - assert result == {'results': ['serialized user token'] * 3} + {"user_name": user_token1.user.name}, + ) + assert result == {"results": ["serialized user token"] * 3} user_tokens.get_user_tokens.assert_called_once_with(user_token1.user) diff --git a/server/szurubooru/tests/api/test_user_token_updating.py b/server/szurubooru/tests/api/test_user_token_updating.py index bf725a3..6bc5e11 100644 --- a/server/szurubooru/tests/api/test_user_token_updating.py +++ b/server/szurubooru/tests/api/test_user_token_updating.py @@ -1,42 +1,49 @@ from unittest.mock import patch + import pytest + from szurubooru import api, db from szurubooru.func import user_tokens, users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'user_tokens:edit:self': 'regular'}}) + config_injector({"privileges": {"user_tokens:edit:self": "regular"}}) def test_edit_user_token(user_token_factory, context_factory, fake_datetime): user_token = user_token_factory() db.session.add(user_token) db.session.commit() - with patch('szurubooru.func.user_tokens.get_by_user_and_token'), \ - patch('szurubooru.func.user_tokens.update_user_token_enabled'), \ - patch('szurubooru.func.user_tokens.update_user_token_edit_time'), \ - patch('szurubooru.func.user_tokens.serialize_user_token'), \ - patch('szurubooru.func.users.get_user_by_name'), \ - fake_datetime('1969-02-12'): + with patch("szurubooru.func.user_tokens.get_by_user_and_token"), patch( + "szurubooru.func.user_tokens.update_user_token_enabled" + ), patch("szurubooru.func.user_tokens.update_user_token_edit_time"), patch( + "szurubooru.func.user_tokens.serialize_user_token" + ), patch( + "szurubooru.func.users.get_user_by_name" + ), fake_datetime( + "1969-02-12" + ): users.get_user_by_name.return_value = user_token.user - user_tokens.serialize_user_token.return_value = 'serialized user token' + user_tokens.serialize_user_token.return_value = "serialized user token" user_tokens.get_by_user_and_token.return_value = user_token result = api.user_token_api.update_user_token( context_factory( - params={ - 'version': user_token.version, - 'enabled': False, - }, - user=user_token.user), + params={"version": user_token.version, "enabled": False,}, + user=user_token.user, + ), { - 'user_name': user_token.user.name, - 'user_token': user_token.token - }) - assert result == 'serialized user token' + "user_name": user_token.user.name, + "user_token": user_token.token, + }, + ) + assert result == "serialized user token" user_tokens.get_by_user_and_token.assert_called_once_with( - user_token.user, user_token.token) + user_token.user, user_token.token + ) user_tokens.update_user_token_enabled.assert_called_once_with( - user_token, False) + user_token, False + ) user_tokens.update_user_token_edit_time.assert_called_once_with( - user_token) + user_token + ) diff --git a/server/szurubooru/tests/api/test_user_updating.py b/server/szurubooru/tests/api/test_user_updating.py index af75049..08ccc78 100644 --- a/server/szurubooru/tests/api/test_user_updating.py +++ b/server/szurubooru/tests/api/test_user_updating.py @@ -1,125 +1,147 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'users:edit:self:name': model.User.RANK_REGULAR, - 'users:edit:self:pass': model.User.RANK_REGULAR, - 'users:edit:self:email': model.User.RANK_REGULAR, - 'users:edit:self:rank': model.User.RANK_MODERATOR, - 'users:edit:self:avatar': model.User.RANK_MODERATOR, - 'users:edit:any:name': model.User.RANK_MODERATOR, - 'users:edit:any:pass': model.User.RANK_MODERATOR, - 'users:edit:any:email': model.User.RANK_MODERATOR, - 'users:edit:any:rank': model.User.RANK_ADMINISTRATOR, - 'users:edit:any:avatar': model.User.RANK_ADMINISTRATOR, - }, - }) + config_injector( + { + "privileges": { + "users:edit:self:name": model.User.RANK_REGULAR, + "users:edit:self:pass": model.User.RANK_REGULAR, + "users:edit:self:email": model.User.RANK_REGULAR, + "users:edit:self:rank": model.User.RANK_MODERATOR, + "users:edit:self:avatar": model.User.RANK_MODERATOR, + "users:edit:any:name": model.User.RANK_MODERATOR, + "users:edit:any:pass": model.User.RANK_MODERATOR, + "users:edit:any:email": model.User.RANK_MODERATOR, + "users:edit:any:rank": model.User.RANK_ADMINISTRATOR, + "users:edit:any:avatar": model.User.RANK_ADMINISTRATOR, + }, + } + ) def test_updating_user(context_factory, user_factory): - user = user_factory(name='u1', rank=model.User.RANK_ADMINISTRATOR) + user = user_factory(name="u1", rank=model.User.RANK_ADMINISTRATOR) auth_user = user_factory(rank=model.User.RANK_ADMINISTRATOR) db.session.add(user) db.session.flush() - with patch('szurubooru.func.users.create_user'), \ - patch('szurubooru.func.users.update_user_name'), \ - patch('szurubooru.func.users.update_user_password'), \ - patch('szurubooru.func.users.update_user_email'), \ - patch('szurubooru.func.users.update_user_rank'), \ - patch('szurubooru.func.users.update_user_avatar'), \ - patch('szurubooru.func.users.serialize_user'): - users.serialize_user.return_value = 'serialized user' + with patch("szurubooru.func.users.create_user"), patch( + "szurubooru.func.users.update_user_name" + ), patch("szurubooru.func.users.update_user_password"), patch( + "szurubooru.func.users.update_user_email" + ), patch( + "szurubooru.func.users.update_user_rank" + ), patch( + "szurubooru.func.users.update_user_avatar" + ), patch( + "szurubooru.func.users.serialize_user" + ): + users.serialize_user.return_value = "serialized user" result = api.user_api.update_user( context_factory( params={ - 'version': 1, - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', - 'rank': 'moderator', - 'avatarStyle': 'manual', - }, - files={ - 'avatar': b'...', + "version": 1, + "name": "chewie", + "email": "asd@asd.asd", + "password": "oks", + "rank": "moderator", + "avatarStyle": "manual", }, - user=auth_user), - {'user_name': 'u1'}) + files={"avatar": b"...",}, + user=auth_user, + ), + {"user_name": "u1"}, + ) - assert result == 'serialized user' + assert result == "serialized user" users.create_user.assert_not_called() - users.update_user_name.assert_called_once_with(user, 'chewie') - users.update_user_password.assert_called_once_with(user, 'oks') - users.update_user_email.assert_called_once_with(user, 'asd@asd.asd') + users.update_user_name.assert_called_once_with(user, "chewie") + users.update_user_password.assert_called_once_with(user, "oks") + users.update_user_email.assert_called_once_with(user, "asd@asd.asd") users.update_user_rank.assert_called_once_with( - user, 'moderator', auth_user) + user, "moderator", auth_user + ) users.update_user_avatar.assert_called_once_with( - user, 'manual', b'...') + user, "manual", b"..." + ) users.serialize_user.assert_called_once_with( - user, auth_user, options=[]) + user, auth_user, options=[] + ) @pytest.mark.parametrize( - 'field', ['name', 'email', 'password', 'rank', 'avatarStyle']) + "field", ["name", "email", "password", "rank", "avatarStyle"] +) def test_omitting_optional_field(user_factory, context_factory, field): - user = user_factory(name='u1', rank=model.User.RANK_ADMINISTRATOR) + user = user_factory(name="u1", rank=model.User.RANK_ADMINISTRATOR) db.session.add(user) db.session.flush() params = { - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', - 'rank': 'moderator', - 'avatarStyle': 'gravatar', + "name": "chewie", + "email": "asd@asd.asd", + "password": "oks", + "rank": "moderator", + "avatarStyle": "gravatar", } del params[field] - with patch('szurubooru.func.users.create_user'), \ - patch('szurubooru.func.users.update_user_name'), \ - patch('szurubooru.func.users.update_user_password'), \ - patch('szurubooru.func.users.update_user_email'), \ - patch('szurubooru.func.users.update_user_rank'), \ - patch('szurubooru.func.users.update_user_avatar'), \ - patch('szurubooru.func.users.serialize_user'): + with patch("szurubooru.func.users.create_user"), patch( + "szurubooru.func.users.update_user_name" + ), patch("szurubooru.func.users.update_user_password"), patch( + "szurubooru.func.users.update_user_email" + ), patch( + "szurubooru.func.users.update_user_rank" + ), patch( + "szurubooru.func.users.update_user_avatar" + ), patch( + "szurubooru.func.users.serialize_user" + ): api.user_api.update_user( context_factory( - params={**params, **{'version': 1}}, - files={'avatar': b'...'}, - user=user), - {'user_name': 'u1'}) + params={**params, **{"version": 1}}, + files={"avatar": b"..."}, + user=user, + ), + {"user_name": "u1"}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): - user = user_factory(name='u1', rank=model.User.RANK_ADMINISTRATOR) + user = user_factory(name="u1", rank=model.User.RANK_ADMINISTRATOR) db.session.add(user) db.session.flush() with pytest.raises(users.UserNotFoundError): api.user_api.update_user( - context_factory(user=user), {'user_name': 'u2'}) + context_factory(user=user), {"user_name": "u2"} + ) -@pytest.mark.parametrize('params', [ - {'name': 'whatever'}, - {'email': 'whatever'}, - {'rank': 'whatever'}, - {'password': 'whatever'}, - {'avatarStyle': 'whatever'}, -]) +@pytest.mark.parametrize( + "params", + [ + {"name": "whatever"}, + {"email": "whatever"}, + {"rank": "whatever"}, + {"password": "whatever"}, + {"avatarStyle": "whatever"}, + ], +) def test_trying_to_update_field_without_privileges( - user_factory, context_factory, params): - user1 = user_factory(name='u1', rank=model.User.RANK_REGULAR) - user2 = user_factory(name='u2', rank=model.User.RANK_REGULAR) + user_factory, context_factory, params +): + user1 = user_factory(name="u1", rank=model.User.RANK_REGULAR) + user2 = user_factory(name="u2", rank=model.User.RANK_REGULAR) db.session.add_all([user1, user2]) db.session.flush() with pytest.raises(errors.AuthError): api.user_api.update_user( - context_factory( - params={**params, **{'version': 1}}, - user=user1), - {'user_name': user2.name}) + context_factory(params={**params, **{"version": 1}}, user=user1), + {"user_name": user2.name}, + ) diff --git a/server/szurubooru/tests/conftest.py b/server/szurubooru/tests/conftest.py index c776601..6e127bc 100644 --- a/server/szurubooru/tests/conftest.py +++ b/server/szurubooru/tests/conftest.py @@ -2,17 +2,19 @@ import contextlib import os import random import string -from unittest.mock import patch from datetime import datetime -import pytest +from unittest.mock import patch + import freezegun +import pytest import sqlalchemy as sa + from szurubooru import config, db, model, rest def get_unique_name(): alphabet = string.ascii_letters + string.digits - return ''.join(random.choice(alphabet) for _ in range(8)) + return "".join(random.choice(alphabet) for _ in range(8)) @pytest.fixture @@ -23,21 +25,24 @@ def fake_datetime(): freezer.start() yield freezer.stop() + return injector -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def query_logger(pytestconfig): if pytestconfig.option.verbose > 0: import logging import coloredlogs + coloredlogs.install( - fmt='[%(asctime)-15s] %(name)s %(message)s', isatty=True) + fmt="[%(asctime)-15s] %(name)s %(message)s", isatty=True + ) logging.basicConfig() - logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) + logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) -@pytest.yield_fixture(scope='function', autouse=True) +@pytest.yield_fixture(scope="function", autouse=True) def session(query_logger, postgresql_db): db.session = postgresql_db.session postgresql_db.create_table(*model.Base.metadata.sorted_tables) @@ -51,15 +56,17 @@ def session(query_logger, postgresql_db): def context_factory(session): def factory(params=None, files=None, user=None, headers=None): ctx = rest.Context( - env={'HTTP_ORIGIN': 'http://example.com'}, + env={"HTTP_ORIGIN": "http://example.com"}, method=None, url=None, headers=headers or {}, params=params or {}, - files=files or {}) + files=files or {}, + ) ctx.session = session ctx.user = user or model.User() return ctx + return factory @@ -67,58 +74,64 @@ def context_factory(session): def config_injector(): def injector(new_config_content): config.config = new_config_content + return injector @pytest.fixture def user_factory(): def factory( - name=None, - rank=model.User.RANK_REGULAR, - email='dummy', - password_salt=None, - password_hash=None): + name=None, + rank=model.User.RANK_REGULAR, + email="dummy", + password_salt=None, + password_hash=None, + ): user = model.User() user.name = name or get_unique_name() - user.password_salt = password_salt or 'dummy' - user.password_hash = password_hash or 'dummy' + user.password_salt = password_salt or "dummy" + user.password_hash = password_hash or "dummy" user.email = email user.rank = rank user.creation_time = datetime(1997, 1, 1) user.avatar_style = model.User.AVATAR_GRAVATAR return user + return factory @pytest.fixture def user_token_factory(user_factory): def factory( - user=None, - token=None, - expiration_time=None, - enabled=None, - creation_time=None): + user=None, + token=None, + expiration_time=None, + enabled=None, + creation_time=None, + ): if user is None: user = user_factory() db.session.add(user) user_token = model.UserToken() user_token.user = user - user_token.token = token or 'dummy' + user_token.token = token or "dummy" user_token.expiration_time = expiration_time user_token.enabled = enabled if enabled is not None else True user_token.creation_time = creation_time or datetime(1997, 1, 1) return user_token + return factory @pytest.fixture def tag_category_factory(): - def factory(name=None, color='dummy', default=False): + def factory(name=None, color="dummy", default=False): category = model.TagCategory() category.name = name or get_unique_name() category.color = color category.default = default return category + return factory @@ -135,31 +148,34 @@ def tag_factory(): tag.category = category tag.creation_time = datetime(1996, 1, 1) return tag + return factory @pytest.fixture def post_factory(): def factory( - id=None, - safety=model.Post.SAFETY_SAFE, - type=model.Post.TYPE_IMAGE, - checksum='...'): + id=None, + safety=model.Post.SAFETY_SAFE, + type=model.Post.TYPE_IMAGE, + checksum="...", + ): post = model.Post() post.post_id = id post.safety = safety post.type = type post.checksum = checksum post.flags = [] - post.mime_type = 'application/octet-stream' + post.mime_type = "application/octet-stream" post.creation_time = datetime(1996, 1, 1) return post + return factory @pytest.fixture def comment_factory(user_factory, post_factory): - def factory(user=None, post=None, text='dummy', time=None): + def factory(user=None, post=None, text="dummy", time=None): if not user: user = user_factory() db.session.add(user) @@ -172,6 +188,7 @@ def comment_factory(user_factory, post_factory): comment.text = text comment.creation_time = time or datetime(1996, 1, 1) return comment + return factory @@ -183,7 +200,9 @@ def post_score_factory(user_factory, post_factory): if post is None: post = post_factory() return model.PostScore( - post=post, user=user, score=score, time=datetime(1999, 1, 1)) + post=post, user=user, score=score, time=datetime(1999, 1, 1) + ) + return factory @@ -195,25 +214,29 @@ def post_favorite_factory(user_factory, post_factory): if post is None: post = post_factory() return model.PostFavorite( - post=post, user=user, time=datetime(1999, 1, 1)) + post=post, user=user, time=datetime(1999, 1, 1) + ) + return factory @pytest.fixture def pool_category_factory(): - def factory(name=None, color='dummy', default=False): + def factory(name=None, color="dummy", default=False): category = model.PoolCategory() category.name = name or get_unique_name() category.color = color category.default = default return category + return factory @pytest.fixture def pool_factory(): def factory( - id=None, names=None, description=None, category=None, time=None): + id=None, names=None, description=None, category=None, time=None + ): if not category: category = model.PoolCategory(get_unique_name()) db.session.add(category) @@ -226,6 +249,7 @@ def pool_factory(): pool.category = category pool.creation_time = time or datetime(1996, 1, 1) return pool + return factory @@ -243,13 +267,15 @@ def pool_post_factory(pool_factory, post_factory): pool_post.post = post pool_post.order = order or 0 return pool_post + return factory @pytest.fixture def read_asset(): def get(path): - path = os.path.join(os.path.dirname(__file__), 'assets', path) - with open(path, 'rb') as handle: + path = os.path.join(os.path.dirname(__file__), "assets", path) + with open(path, "rb") as handle: return handle.read() + return get diff --git a/server/szurubooru/tests/func/test_auth.py b/server/szurubooru/tests/func/test_auth.py index 6dc79bb..2141d3c 100644 --- a/server/szurubooru/tests/func/test_auth.py +++ b/server/szurubooru/tests/func/test_auth.py @@ -1,41 +1,44 @@ from datetime import datetime, timedelta + import pytest + from szurubooru.func import auth @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'secret': 'testSecret'}) + config_injector({"secret": "testSecret"}) def test_get_password_hash(): - salt, password = ('testSalt', 'pass') + salt, password = ("testSalt", "pass") result, revision = auth.get_password_hash(salt, password) assert result assert revision == 3 hash_parts = list( - filter(lambda e: e is not None and e != '', result.split('$'))) + filter(lambda e: e is not None and e != "", result.split("$")) + ) assert len(hash_parts) == 5 - assert hash_parts[0] == 'argon2id' + assert hash_parts[0] == "argon2id" def test_get_sha256_legacy_password_hash(): - salt, password = ('testSalt', 'pass') + salt, password = ("testSalt", "pass") result, revision = auth.get_sha256_legacy_password_hash(salt, password) - hash = '2031ac9631353ac9303719a7f808a24f79aa1d71712c98523e4bb4cce579428a' + hash = "2031ac9631353ac9303719a7f808a24f79aa1d71712c98523e4bb4cce579428a" assert result == hash assert revision == 2 def test_get_sha1_legacy_password_hash(): - salt, password = ('testSalt', 'pass') + salt, password = ("testSalt", "pass") result, revision = auth.get_sha1_legacy_password_hash(salt, password) - assert result == '1eb1f953d9be303a1b54627e903e6124cfb1245b' + assert result == "1eb1f953d9be303a1b54627e903e6124cfb1245b" assert revision == 1 def test_is_valid_password_auto_upgrades_user_password_hash(user_factory): - salt, password = ('testSalt', 'pass') + salt, password = ("testSalt", "pass") hash, revision = auth.get_sha256_legacy_password_hash(salt, password) user = user_factory(password_salt=salt, password_hash=hash) result = auth.is_valid_password(user, password) @@ -50,7 +53,7 @@ def test_is_valid_token(user_token_factory): def test_expired_token_is_invalid(user_token_factory): - past_expiration = (datetime.utcnow() - timedelta(minutes=30)) + past_expiration = datetime.utcnow() - timedelta(minutes=30) user_token = user_token_factory(expiration_time=past_expiration) assert not auth.is_valid_token(user_token) diff --git a/server/szurubooru/tests/func/test_comments.py b/server/szurubooru/tests/func/test_comments.py index f1e5d0f..ef5ca4f 100644 --- a/server/szurubooru/tests/func/test_comments.py +++ b/server/szurubooru/tests/func/test_comments.py @@ -1,34 +1,36 @@ -from unittest.mock import patch from datetime import datetime +from unittest.mock import patch + import pytest + from szurubooru import db from szurubooru.func import comments, users def test_serialize_user(user_factory, comment_factory): - with patch('szurubooru.func.users.get_avatar_url'): - users.get_avatar_url.return_value = 'https://example.com/avatar.png' - comment = comment_factory(user=user_factory(name='dummy')) + with patch("szurubooru.func.users.get_avatar_url"): + users.get_avatar_url.return_value = "https://example.com/avatar.png" + comment = comment_factory(user=user_factory(name="dummy")) comment.comment_id = 77 comment.creation_time = datetime(1997, 1, 1) comment.last_edit_time = datetime(1998, 1, 1) - comment.text = 'text' + comment.text = "text" db.session.add(comment) db.session.flush() auth_user = user_factory() assert comments.serialize_comment(comment, auth_user) == { - 'id': comment.comment_id, - 'postId': comment.post.post_id, - 'creationTime': datetime(1997, 1, 1, 0, 0), - 'lastEditTime': datetime(1998, 1, 1, 0, 0), - 'score': 0, - 'ownScore': 0, - 'text': 'text', - 'user': { - 'name': 'dummy', - 'avatarUrl': 'https://example.com/avatar.png', + "id": comment.comment_id, + "postId": comment.post.post_id, + "creationTime": datetime(1997, 1, 1, 0, 0), + "lastEditTime": datetime(1998, 1, 1, 0, 0), + "score": 0, + "ownScore": 0, + "text": "text", + "user": { + "name": "dummy", + "avatarUrl": "https://example.com/avatar.png", }, - 'version': 1, + "version": 1, } @@ -53,13 +55,14 @@ def test_create_comment(user_factory, post_factory, fake_datetime): user = user_factory() post = post_factory() db.session.add_all([user, post]) - with patch('szurubooru.func.comments.update_comment_text'), \ - fake_datetime('1997-01-01'): - comment = comments.create_comment(user, post, 'text') + with patch("szurubooru.func.comments.update_comment_text"), fake_datetime( + "1997-01-01" + ): + comment = comments.create_comment(user, post, "text") assert comment.creation_time == datetime(1997, 1, 1) assert comment.user == user assert comment.post == post - comments.update_comment_text.assert_called_once_with(comment, 'text') + comments.update_comment_text.assert_called_once_with(comment, "text") def test_update_comment_text_with_emptry_string(comment_factory): @@ -70,5 +73,5 @@ def test_update_comment_text_with_emptry_string(comment_factory): def test_update_comment_text(comment_factory): comment = comment_factory() - comments.update_comment_text(comment, 'text') - assert comment.text == 'text' + comments.update_comment_text(comment, "text") + assert comment.text == "text" diff --git a/server/szurubooru/tests/func/test_diff.py b/server/szurubooru/tests/func/test_diff.py index 0134a3f..5ea5780 100644 --- a/server/szurubooru/tests/func/test_diff.py +++ b/server/szurubooru/tests/func/test_diff.py @@ -1,275 +1,222 @@ import pytest -from szurubooru.func import diff - - -@pytest.mark.parametrize('old,new,expected', [ - ( - [], [], None, - ), - - ( - [], - ['added'], - {'type': 'list change', 'added': ['added'], 'removed': []}, - ), - ( - ['removed'], - [], - {'type': 'list change', 'added': [], 'removed': ['removed']}, - ), - - ( - ['untouched'], - ['untouched'], - None, - ), +from szurubooru.func import diff - ( - ['untouched'], - ['untouched', 'added'], - {'type': 'list change', 'added': ['added'], 'removed': []}, - ), - ( - ['untouched', 'removed'], - ['untouched'], - {'type': 'list change', 'added': [], 'removed': ['removed']}, - ), -]) +@pytest.mark.parametrize( + "old,new,expected", + [ + ([], [], None,), + ( + [], + ["added"], + {"type": "list change", "added": ["added"], "removed": []}, + ), + ( + ["removed"], + [], + {"type": "list change", "added": [], "removed": ["removed"]}, + ), + (["untouched"], ["untouched"], None,), + ( + ["untouched"], + ["untouched", "added"], + {"type": "list change", "added": ["added"], "removed": []}, + ), + ( + ["untouched", "removed"], + ["untouched"], + {"type": "list change", "added": [], "removed": ["removed"]}, + ), + ], +) def test_get_list_diff(old, new, expected): assert diff.get_list_diff(old, new) == expected -@pytest.mark.parametrize('old,new,expected', [ - ( - {}, {}, None, - ), - - ( - {'removed key': 'removed value'}, - {}, - { - 'type': 'object change', - 'value': +@pytest.mark.parametrize( + "old,new,expected", + [ + ({}, {}, None,), + ( + {"removed key": "removed value"}, + {}, { - 'removed key': - { - 'type': 'deleted property', - 'value': 'removed value', + "type": "object change", + "value": { + "removed key": { + "type": "deleted property", + "value": "removed value", + }, }, }, - }, - ), - - ( - {}, - {'added key': 'added value'}, - { - 'type': 'object change', - 'value': + ), + ( + {}, + {"added key": "added value"}, { - 'added key': - { - 'type': 'added property', - 'value': 'added value', + "type": "object change", + "value": { + "added key": { + "type": "added property", + "value": "added value", + }, }, }, - }, - ), - - ( - {'key': 'old value'}, - {'key': 'new value'}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": "old value"}, + {"key": "new value"}, { - 'key': - { - 'type': 'primitive change', - 'old-value': 'old value', - 'new-value': 'new value', + "type": "object change", + "value": { + "key": { + "type": "primitive change", + "old-value": "old value", + "new-value": "new value", + }, }, }, - }, - ), - - ( - {'key': 'untouched'}, - {'key': 'untouched'}, - None, - ), - - ( - {'key': 'untouched', 'removed key': 'removed value'}, - {'key': 'untouched'}, - { - 'type': 'object change', - 'value': + ), + ({"key": "untouched"}, {"key": "untouched"}, None,), + ( + {"key": "untouched", "removed key": "removed value"}, + {"key": "untouched"}, { - 'removed key': - { - 'type': 'deleted property', - 'value': 'removed value', + "type": "object change", + "value": { + "removed key": { + "type": "deleted property", + "value": "removed value", + }, }, }, - }, - ), - - ( - {'key': 'untouched'}, - {'key': 'untouched', 'added key': 'added value'}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": "untouched"}, + {"key": "untouched", "added key": "added value"}, { - 'added key': - { - 'type': 'added property', - 'value': 'added value', + "type": "object change", + "value": { + "added key": { + "type": "added property", + "value": "added value", + }, }, }, - }, - ), - - ( - {'key': 'untouched', 'changed key': 'old value'}, - {'key': 'untouched', 'changed key': 'new value'}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": "untouched", "changed key": "old value"}, + {"key": "untouched", "changed key": "new value"}, { - 'changed key': - { - 'type': 'primitive change', - 'old-value': 'old value', - 'new-value': 'new value', + "type": "object change", + "value": { + "changed key": { + "type": "primitive change", + "old-value": "old value", + "new-value": "new value", + }, }, }, - }, - ), - - ( - {'key': {'subkey': 'old value'}}, - {'key': {'subkey': 'new value'}}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": {"subkey": "old value"}}, + {"key": {"subkey": "new value"}}, { - 'key': - { - 'type': 'object change', - 'value': - { - 'subkey': - { - 'type': 'primitive change', - 'old-value': 'old value', - 'new-value': 'new value', + "type": "object change", + "value": { + "key": { + "type": "object change", + "value": { + "subkey": { + "type": "primitive change", + "old-value": "old value", + "new-value": "new value", + }, }, }, }, }, - }, - ), - - ( - {'key': {}}, - {'key': {'subkey': 'removed value'}}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": {}}, + {"key": {"subkey": "removed value"}}, { - 'key': - { - 'type': 'object change', - 'value': - { - 'subkey': - { - 'type': 'added property', - 'value': 'removed value', + "type": "object change", + "value": { + "key": { + "type": "object change", + "value": { + "subkey": { + "type": "added property", + "value": "removed value", + }, }, }, }, }, - }, - ), - - ( - {'key': {'subkey': 'removed value'}}, - {'key': {}}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": {"subkey": "removed value"}}, + {"key": {}}, { - 'key': - { - 'type': 'object change', - 'value': - { - 'subkey': - { - 'type': 'deleted property', - 'value': 'removed value', + "type": "object change", + "value": { + "key": { + "type": "object change", + "value": { + "subkey": { + "type": "deleted property", + "value": "removed value", + }, }, }, }, }, - }, - ), - - ( - {'key': ['old value']}, - {'key': ['new value']}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": ["old value"]}, + {"key": ["new value"]}, { - 'key': - { - 'type': 'list change', - 'added': ['new value'], - 'removed': ['old value'], + "type": "object change", + "value": { + "key": { + "type": "list change", + "added": ["new value"], + "removed": ["old value"], + }, }, }, - }, - ), - - ( - {'key': []}, - {'key': ['new value']}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": []}, + {"key": ["new value"]}, { - 'key': - { - 'type': 'list change', - 'added': ['new value'], - 'removed': [], + "type": "object change", + "value": { + "key": { + "type": "list change", + "added": ["new value"], + "removed": [], + }, }, }, - }, - ), - - ( - {'key': ['removed value']}, - {'key': []}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": ["removed value"]}, + {"key": []}, { - 'key': - { - 'type': 'list change', - 'added': [], - 'removed': ['removed value'], + "type": "object change", + "value": { + "key": { + "type": "list change", + "added": [], + "removed": ["removed value"], + }, }, }, - }, - ), -]) + ), + ], +) def test_get_dict_diff(old, new, expected): assert diff.get_dict_diff(old, new) == expected diff --git a/server/szurubooru/tests/func/test_image_hash.py b/server/szurubooru/tests/func/test_image_hash.py index 13bfae8..e7028b6 100644 --- a/server/szurubooru/tests/func/test_image_hash.py +++ b/server/szurubooru/tests/func/test_image_hash.py @@ -1,16 +1,19 @@ import pytest -from szurubooru.func import image_hash from numpy import array_equal +from szurubooru.func import image_hash + def test_signature_functions(read_asset, config_injector): - sig1 = image_hash.generate_signature(read_asset('jpeg.jpg')) - sig2 = image_hash.generate_signature(read_asset('jpeg-similar.jpg')) + sig1 = image_hash.generate_signature(read_asset("jpeg.jpg")) + sig2 = image_hash.generate_signature(read_asset("jpeg-similar.jpg")) sig1_repacked = image_hash.unpack_signature( - image_hash.pack_signature(sig1)) + image_hash.pack_signature(sig1) + ) sig2_repacked = image_hash.unpack_signature( - image_hash.pack_signature(sig2)) + image_hash.pack_signature(sig2) + ) assert array_equal(sig1, sig1_repacked) assert array_equal(sig2, sig2_repacked) diff --git a/server/szurubooru/tests/func/test_mime.py b/server/szurubooru/tests/func/test_mime.py index 821ed20..4eb9b16 100644 --- a/server/szurubooru/tests/func/test_mime.py +++ b/server/szurubooru/tests/func/test_mime.py @@ -1,78 +1,94 @@ import pytest + from szurubooru.func import mime -@pytest.mark.parametrize('input_path,expected_mime_type', [ - ('mp4.mp4', 'video/mp4'), - ('webm.webm', 'video/webm'), - ('flash.swf', 'application/x-shockwave-flash'), - ('png.png', 'image/png'), - ('jpeg.jpg', 'image/jpeg'), - ('gif.gif', 'image/gif'), - ('webp.webp', 'image/webp'), - ('text.txt', 'application/octet-stream'), -]) +@pytest.mark.parametrize( + "input_path,expected_mime_type", + [ + ("mp4.mp4", "video/mp4"), + ("webm.webm", "video/webm"), + ("flash.swf", "application/x-shockwave-flash"), + ("png.png", "image/png"), + ("jpeg.jpg", "image/jpeg"), + ("gif.gif", "image/gif"), + ("webp.webp", "image/webp"), + ("text.txt", "application/octet-stream"), + ], +) def test_get_mime_type(read_asset, input_path, expected_mime_type): assert mime.get_mime_type(read_asset(input_path)) == expected_mime_type def test_get_mime_type_for_empty_file(): - assert mime.get_mime_type(b'') == 'application/octet-stream' + assert mime.get_mime_type(b"") == "application/octet-stream" -@pytest.mark.parametrize('mime_type,expected_extension', [ - ('video/mp4', 'mp4'), - ('video/webm', 'webm'), - ('application/x-shockwave-flash', 'swf'), - ('image/png', 'png'), - ('image/jpeg', 'jpg'), - ('image/gif', 'gif'), - ('image/webp', 'webp'), - ('application/octet-stream', 'dat'), -]) +@pytest.mark.parametrize( + "mime_type,expected_extension", + [ + ("video/mp4", "mp4"), + ("video/webm", "webm"), + ("application/x-shockwave-flash", "swf"), + ("image/png", "png"), + ("image/jpeg", "jpg"), + ("image/gif", "gif"), + ("image/webp", "webp"), + ("application/octet-stream", "dat"), + ], +) def test_get_extension(mime_type, expected_extension): assert mime.get_extension(mime_type) == expected_extension -@pytest.mark.parametrize('input_mime_type,expected_state', [ - ('application/x-shockwave-flash', True), - ('APPLICATION/X-SHOCKWAVE-FLASH', True), - ('application/x-shockwave', False), -]) +@pytest.mark.parametrize( + "input_mime_type,expected_state", + [ + ("application/x-shockwave-flash", True), + ("APPLICATION/X-SHOCKWAVE-FLASH", True), + ("application/x-shockwave", False), + ], +) def test_is_flash(input_mime_type, expected_state): assert mime.is_flash(input_mime_type) == expected_state -@pytest.mark.parametrize('input_mime_type,expected_state', [ - ('video/webm', True), - ('VIDEO/WEBM', True), - ('video/mp4', True), - ('VIDEO/MP4', True), - ('video/anything_else', False), - ('application/ogg', True), - ('not a video', False), -]) +@pytest.mark.parametrize( + "input_mime_type,expected_state", + [ + ("video/webm", True), + ("VIDEO/WEBM", True), + ("video/mp4", True), + ("VIDEO/MP4", True), + ("video/anything_else", False), + ("application/ogg", True), + ("not a video", False), + ], +) def test_is_video(input_mime_type, expected_state): assert mime.is_video(input_mime_type) == expected_state -@pytest.mark.parametrize('input_mime_type,expected_state', [ - ('image/gif', True), - ('image/png', True), - ('image/jpeg', True), - ('IMAGE/GIF', True), - ('IMAGE/PNG', True), - ('IMAGE/JPEG', True), - ('image/anything_else', False), - ('not an image', False), -]) +@pytest.mark.parametrize( + "input_mime_type,expected_state", + [ + ("image/gif", True), + ("image/png", True), + ("image/jpeg", True), + ("IMAGE/GIF", True), + ("IMAGE/PNG", True), + ("IMAGE/JPEG", True), + ("image/anything_else", False), + ("not an image", False), + ], +) def test_is_image(input_mime_type, expected_state): assert mime.is_image(input_mime_type) == expected_state -@pytest.mark.parametrize('input_path,expected_state', [ - ('gif.gif', False), - ('gif-animated.gif', True), -]) +@pytest.mark.parametrize( + "input_path,expected_state", + [("gif.gif", False), ("gif-animated.gif", True),], +) def test_is_animated_gif(read_asset, input_path, expected_state): assert mime.is_animated_gif(read_asset(input_path)) == expected_state diff --git a/server/szurubooru/tests/func/test_net.py b/server/szurubooru/tests/func/test_net.py index 7fe14b6..58e1628 100644 --- a/server/szurubooru/tests/func/test_net.py +++ b/server/szurubooru/tests/func/test_net.py @@ -1,4 +1,5 @@ import pytest + from szurubooru import errors from szurubooru.func import net from szurubooru.func.util import get_sha1 @@ -6,82 +7,95 @@ from szurubooru.func.util import get_sha1 @pytest.fixture(autouse=True) def inject_config(tmpdir, config_injector): - config_injector({ - 'user_agent': None, - 'max_dl_filesize': 1.0E+6, - 'data_dir': str(tmpdir.mkdir('data')), - }) + config_injector( + { + "user_agent": None, + "max_dl_filesize": 1.0e6, + "data_dir": str(tmpdir.mkdir("data")), + } + ) def test_download(): - url = 'http://info.cern.ch/hypertext/WWW/TheProject.html' + url = "http://info.cern.ch/hypertext/WWW/TheProject.html" expected_content = ( - b'
        \nThe World Wide Web project\n\n
        \n\n

        World Wide Web

        The WorldWideWeb' + - b' (W3) is a wide-area\nhypermedia information retrieval\ninitiative aiming to give universal\na' + - b'ccess to a large universe of documents.

        \nEverything there is ' + - b'online about\nW3 is linked directly or indirectly\nto this docum' + - b'ent, including an executive\nsum' + - b'mary of the project, Mailing lists\n, Policy , November\'s W3 news ,\nFrequently Ask' + - b'ed Questions .\n

        \n
        What\'s out there?\n
        Pointers to the\nworld\'s ' + - b'online information, subjects\n, W3 servers, etc.\n
        Help\n
        on the browser you are using\n
        Software Products\n
        A list of W' + - b'3 project\ncomponents and their current state.\n(e.g. Line Mode ,X11 Viola , NeXTStep\n, Servers , Tools , Mail robot ,<' + - b'A\nNAME=52 HREF="Status.html#57">\nLibrary )\n
        Technical\n
        Details of protocols' + - b', formats,\nprogram internals etc\n
        Bibliography\n
        Paper documentation\non W3 a' + - b'nd references.\n
        People\n<' + - b'DD> A list of some people involved\nin the project.\n
        History\n
        A summary of the hist' + - b'ory\nof the project.\n
        How ca' + - b'n I help ?\n
        If you would like\nto support the web..\nGetting code\n
        Getti' + - b'ng the code by\nanonymous FTP , etc.\n
        \n\n') + b'
        \nThe World Wide Web project\n\n
        \n\n

        World Wide Web

        The WorldWideWeb' + + b' (W3) is a wide-area\nhypermedia information retrieval\ninitiative aiming to give universal\na" + + b"ccess to a large universe of documents.

        \nEverything there is " + + b"online about\nW3 is linked directly or indirectly\nto this docum" + + b'ent, including an executive\nsum' + + b'mary of the project, Mailing lists\n, Policy , November\'s W3 news ,\nFrequently Ask' + + b'ed Questions .\n

        \n
        What's out there?\n
        Pointers to the\nworld's " + + b'online information, subjects\n, W3 servers, etc.\n
        Help\n
        on the browser you are using\n
        Software Products\n
        A list of W' + + b"3 project\ncomponents and their current state.\n(e.g. Line Mode ,X11 Viola , NeXTStep\n, Servers , Tools , Mail robot ,<' + + b'A\nNAME=52 HREF="Status.html#57">\nLibrary )\n
        Technical\n
        Details of protocols' + + b', formats,\nprogram internals etc\n
        Bibliography\n
        Paper documentation\non W3 a' + + b'nd references.\n
        People\n<' + + b"DD> A list of some people involved\nin the project.\n
        History\n
        A summary of the hist' + + b'ory\nof the project.\n
        How ca' + + b"n I help ?\n
        If you would like\nto support the web..\nGetting code\n
        Getti' + + b'ng the code by\nanonymous FTP , etc.\n
        \n\n' + ) actual_content = net.download(url) assert actual_content == expected_content -@pytest.mark.parametrize('url', [ - 'https://samples.ffmpeg.org/MPEG-4/video.mp4', -]) +@pytest.mark.parametrize( + "url", ["https://samples.ffmpeg.org/MPEG-4/video.mp4",] +) def test_too_large_download(url): - pytest.xfail('Download limit not implemented yet') + pytest.xfail("Download limit not implemented yet") with pytest.raises(errors.ProcessingError): net.download(url) -@pytest.mark.parametrize('url,expected_sha1', [ - ('https://www.youtube.com/watch?v=C0DPdy98e4c', - '365af1c8f59c6865e1a84c6e13e3e25ff89e0ba1'), - ('https://gfycat.com/immaterialchillyiberianmole', - '953000e81d7bd1da95ce264f872e7b6c4a6484be'), -]) +@pytest.mark.parametrize( + "url,expected_sha1", + [ + ( + "https://www.youtube.com/watch?v=C0DPdy98e4c", + "365af1c8f59c6865e1a84c6e13e3e25ff89e0ba1", + ), + ( + "https://gfycat.com/immaterialchillyiberianmole", + "953000e81d7bd1da95ce264f872e7b6c4a6484be", + ), + ], +) def test_video_download(url, expected_sha1): actual_content = net.download(url, use_video_downloader=True) assert get_sha1(actual_content) == expected_sha1 -@pytest.mark.parametrize('url', [ - 'https://samples.ffmpeg.org/flac/short.flac', # not a video - 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', # video too large -]) +@pytest.mark.parametrize( + "url", + [ + "https://samples.ffmpeg.org/flac/short.flac", # not a video + "https://www.youtube.com/watch?v=dQw4w9WgXcQ", # video too large + ], +) def test_failed_video_download(url): with pytest.raises(errors.ThirdPartyError): net.download(url, use_video_downloader=True) diff --git a/server/szurubooru/tests/func/test_posts.py b/server/szurubooru/tests/func/test_posts.py index ed00879..acedf6f 100644 --- a/server/szurubooru/tests/func/test_posts.py +++ b/server/szurubooru/tests/func/test_posts.py @@ -1,40 +1,58 @@ +import os from datetime import datetime from unittest.mock import patch -import os + import pytest + from szurubooru import db, model from szurubooru.func import ( - posts, users, comments, tags, images, files, util, image_hash) + comments, + files, + image_hash, + images, + posts, + tags, + users, + util, +) -@pytest.mark.parametrize('input_mime_type,expected_url', [ - ('image/jpeg', 'http://example.com/posts/1_244c8840887984c4.jpg'), - ('image/gif', 'http://example.com/posts/1_244c8840887984c4.gif'), - ('totally/unknown', 'http://example.com/posts/1_244c8840887984c4.dat'), -]) +@pytest.mark.parametrize( + "input_mime_type,expected_url", + [ + ("image/jpeg", "http://example.com/posts/1_244c8840887984c4.jpg"), + ("image/gif", "http://example.com/posts/1_244c8840887984c4.gif"), + ("totally/unknown", "http://example.com/posts/1_244c8840887984c4.dat"), + ], +) def test_get_post_url(input_mime_type, expected_url, config_injector): - config_injector({'data_url': 'http://example.com/', 'secret': 'test'}) + config_injector({"data_url": "http://example.com/", "secret": "test"}) post = model.Post() post.post_id = 1 post.mime_type = input_mime_type assert posts.get_post_content_url(post) == expected_url -@pytest.mark.parametrize('input_mime_type', ['image/jpeg', 'image/gif']) +@pytest.mark.parametrize("input_mime_type", ["image/jpeg", "image/gif"]) def test_get_post_thumbnail_url(input_mime_type, config_injector): - config_injector({'data_url': 'http://example.com/', 'secret': 'test'}) + config_injector({"data_url": "http://example.com/", "secret": "test"}) post = model.Post() post.post_id = 1 post.mime_type = input_mime_type - assert posts.get_post_thumbnail_url(post) \ - == 'http://example.com/generated-thumbnails/1_244c8840887984c4.jpg' + assert ( + posts.get_post_thumbnail_url(post) + == "http://example.com/generated-thumbnails/1_244c8840887984c4.jpg" + ) -@pytest.mark.parametrize('input_mime_type,expected_path', [ - ('image/jpeg', 'posts/1_244c8840887984c4.jpg'), - ('image/gif', 'posts/1_244c8840887984c4.gif'), - ('totally/unknown', 'posts/1_244c8840887984c4.dat'), -]) +@pytest.mark.parametrize( + "input_mime_type,expected_path", + [ + ("image/jpeg", "posts/1_244c8840887984c4.jpg"), + ("image/gif", "posts/1_244c8840887984c4.gif"), + ("totally/unknown", "posts/1_244c8840887984c4.dat"), + ], +) def test_get_post_content_path(input_mime_type, expected_path): post = model.Post() post.post_id = 1 @@ -42,31 +60,35 @@ def test_get_post_content_path(input_mime_type, expected_path): assert posts.get_post_content_path(post) == expected_path -@pytest.mark.parametrize('input_mime_type', ['image/jpeg', 'image/gif']) +@pytest.mark.parametrize("input_mime_type", ["image/jpeg", "image/gif"]) def test_get_post_thumbnail_path(input_mime_type): post = model.Post() post.post_id = 1 post.mime_type = input_mime_type - assert posts.get_post_thumbnail_path(post) \ - == 'generated-thumbnails/1_244c8840887984c4.jpg' + assert ( + posts.get_post_thumbnail_path(post) + == "generated-thumbnails/1_244c8840887984c4.jpg" + ) -@pytest.mark.parametrize('input_mime_type', ['image/jpeg', 'image/gif']) +@pytest.mark.parametrize("input_mime_type", ["image/jpeg", "image/gif"]) def test_get_post_thumbnail_backup_path(input_mime_type): post = model.Post() post.post_id = 1 post.mime_type = input_mime_type - assert posts.get_post_thumbnail_backup_path(post) \ - == 'posts/custom-thumbnails/1_244c8840887984c4.dat' + assert ( + posts.get_post_thumbnail_backup_path(post) + == "posts/custom-thumbnails/1_244c8840887984c4.dat" + ) def test_serialize_note(): note = model.PostNote() note.polygon = [[0, 1], [1, 1], [1, 0], [0, 0]] - note.text = '...' + note.text = "..." assert posts.serialize_note(note) == { - 'polygon': [[0, 1], [1, 1], [1, 0], [0, 0]], - 'text': '...' + "polygon": [[0, 1], [1, 1], [1, 0], [0, 0]], + "text": "...", } @@ -75,94 +97,110 @@ def test_serialize_post_when_empty(): def test_serialize_post( - user_factory, - comment_factory, - tag_factory, - tag_category_factory, - pool_factory, - pool_category_factory, - config_injector): - config_injector({'data_url': 'http://example.com/', 'secret': 'test'}) - with patch('szurubooru.func.comments.serialize_comment'), \ - patch('szurubooru.func.users.serialize_micro_user'), \ - patch('szurubooru.func.posts.files.has'): + user_factory, + comment_factory, + tag_factory, + tag_category_factory, + pool_factory, + pool_category_factory, + config_injector, +): + config_injector({"data_url": "http://example.com/", "secret": "test"}) + with patch("szurubooru.func.comments.serialize_comment"), patch( + "szurubooru.func.users.serialize_micro_user" + ), patch("szurubooru.func.posts.files.has"): files.has.return_value = True - users.serialize_micro_user.side_effect \ - = lambda user, auth_user: user.name - comments.serialize_comment.side_effect \ - = lambda comment, auth_user: comment.user.name - - auth_user = user_factory(name='auth user') + users.serialize_micro_user.side_effect = ( + lambda user, auth_user: user.name + ) + comments.serialize_comment.side_effect = ( + lambda comment, auth_user: comment.user.name + ) + + auth_user = user_factory(name="auth user") post = model.Post() post.post_id = 1 post.creation_time = datetime(1997, 1, 1) post.last_edit_time = datetime(1998, 1, 1) post.tags = [ tag_factory( - names=['tag1', 'tag2'], - category=tag_category_factory('test-cat1')), + names=["tag1", "tag2"], + category=tag_category_factory("test-cat1"), + ), tag_factory( - names=['tag3'], - category=tag_category_factory('test-cat2')) + names=["tag3"], category=tag_category_factory("test-cat2") + ), ] post.safety = model.Post.SAFETY_SAFE - post.source = '4gag' + post.source = "4gag" post.type = model.Post.TYPE_IMAGE - post.checksum = 'deadbeef' - post.mime_type = 'image/jpeg' + post.checksum = "deadbeef" + post.mime_type = "image/jpeg" post.file_size = 100 - post.user = user_factory(name='post author') + post.user = user_factory(name="post author") post.canvas_width = 200 post.canvas_height = 300 - post.flags = ['loop'] + post.flags = ["loop"] db.session.add(post) db.session.flush() - db.session.add_all([ - comment_factory( - user=user_factory(name='commenter1'), - post=post, - time=datetime(1999, 1, 1)), - comment_factory( - user=user_factory(name='commenter2'), - post=post, - time=datetime(1999, 1, 2)), - model.PostFavorite( - post=post, - user=user_factory(name='fav1'), - time=datetime(1800, 1, 1)), - model.PostFeature( - post=post, - user=user_factory(), - time=datetime(1999, 1, 1)), - model.PostScore( - post=post, - user=auth_user, - score=-1, - time=datetime(1800, 1, 1)), - model.PostScore( - post=post, - user=user_factory(), - score=1, - time=datetime(1800, 1, 1)), - model.PostScore( - post=post, - user=user_factory(), - score=1, - time=datetime(1800, 1, 1))]) + db.session.add_all( + [ + comment_factory( + user=user_factory(name="commenter1"), + post=post, + time=datetime(1999, 1, 1), + ), + comment_factory( + user=user_factory(name="commenter2"), + post=post, + time=datetime(1999, 1, 2), + ), + model.PostFavorite( + post=post, + user=user_factory(name="fav1"), + time=datetime(1800, 1, 1), + ), + model.PostFeature( + post=post, user=user_factory(), time=datetime(1999, 1, 1) + ), + model.PostScore( + post=post, + user=auth_user, + score=-1, + time=datetime(1800, 1, 1), + ), + model.PostScore( + post=post, + user=user_factory(), + score=1, + time=datetime(1800, 1, 1), + ), + model.PostScore( + post=post, + user=user_factory(), + score=1, + time=datetime(1800, 1, 1), + ), + ] + ) db.session.flush() - pool1 = pool_factory(id=1, - names=['pool1', 'pool2'], - description='desc', - category=pool_category_factory('test-cat1')) + pool1 = pool_factory( + id=1, + names=["pool1", "pool2"], + description="desc", + category=pool_category_factory("test-cat1"), + ) pool1.last_edit_time = datetime(1998, 1, 1) pool1.posts.append(post) - pool2 = pool_factory(id=2, - names=['pool3'], - description='desc2', - category=pool_category_factory('test-cat2')) + pool2 = pool_factory( + id=2, + names=["pool3"], + description="desc2", + category=pool_category_factory("test-cat2"), + ) pool2.last_edit_time = datetime(1998, 1, 1) pool2.posts.append(post) @@ -170,105 +208,100 @@ def test_serialize_post( db.session.flush() result = posts.serialize_post(post, auth_user) - result['tags'].sort(key=lambda tag: tag['names'][0]) + result["tags"].sort(key=lambda tag: tag["names"][0]) assert result == { - 'id': 1, - 'version': 1, - 'creationTime': datetime(1997, 1, 1), - 'lastEditTime': datetime(1998, 1, 1), - 'safety': 'safe', - 'source': '4gag', - 'type': 'image', - 'checksum': 'deadbeef', - 'fileSize': 100, - 'canvasWidth': 200, - 'canvasHeight': 300, - 'contentUrl': 'http://example.com/posts/1_244c8840887984c4.jpg', - 'thumbnailUrl': - 'http://example.com/' - 'generated-thumbnails/1_244c8840887984c4.jpg', - 'flags': ['loop'], - 'tags': [ - { - 'names': ['tag1', 'tag2'], - 'category': 'test-cat1', 'usages': 1, - }, + "id": 1, + "version": 1, + "creationTime": datetime(1997, 1, 1), + "lastEditTime": datetime(1998, 1, 1), + "safety": "safe", + "source": "4gag", + "type": "image", + "checksum": "deadbeef", + "fileSize": 100, + "canvasWidth": 200, + "canvasHeight": 300, + "contentUrl": "http://example.com/posts/1_244c8840887984c4.jpg", + "thumbnailUrl": "http://example.com/" + "generated-thumbnails/1_244c8840887984c4.jpg", + "flags": ["loop"], + "tags": [ { - 'names': ['tag3'], - 'category': 'test-cat2', - 'usages': 1, + "names": ["tag1", "tag2"], + "category": "test-cat1", + "usages": 1, }, + {"names": ["tag3"], "category": "test-cat2", "usages": 1,}, ], - 'relations': [], - 'notes': [], - 'pools': [ + "relations": [], + "notes": [], + "pools": [ { - 'id': 1, - 'names': ['pool1', 'pool2'], - 'description': 'desc', - 'category': 'test-cat1', - 'postCount': 1, - 'posts': [ + "id": 1, + "names": ["pool1", "pool2"], + "description": "desc", + "category": "test-cat1", + "postCount": 1, + "posts": [ { - 'id': 1, - 'thumbnailUrl': - 'http://example.com/' - 'generated-thumbnails/1_244c8840887984c4.jpg', + "id": 1, + "thumbnailUrl": "http://example.com/" + "generated-thumbnails/1_244c8840887984c4.jpg", } ], - 'version': 1, - 'creationTime': datetime(1996, 1, 1), - 'lastEditTime': datetime(1998, 1, 1), + "version": 1, + "creationTime": datetime(1996, 1, 1), + "lastEditTime": datetime(1998, 1, 1), }, { - 'id': 2, - 'names': ['pool3'], - 'description': 'desc2', - 'category': 'test-cat2', - 'postCount': 1, - 'posts': [ + "id": 2, + "names": ["pool3"], + "description": "desc2", + "category": "test-cat2", + "postCount": 1, + "posts": [ { - 'id': 1, - 'thumbnailUrl': - 'http://example.com/' - 'generated-thumbnails/1_244c8840887984c4.jpg', + "id": 1, + "thumbnailUrl": "http://example.com/" + "generated-thumbnails/1_244c8840887984c4.jpg", } ], - 'version': 1, - 'creationTime': datetime(1996, 1, 1), - 'lastEditTime': datetime(1998, 1, 1), - } + "version": 1, + "creationTime": datetime(1996, 1, 1), + "lastEditTime": datetime(1998, 1, 1), + }, ], - 'user': 'post author', - 'score': 1, - 'ownFavorite': False, - 'ownScore': -1, - 'tagCount': 2, - 'favoriteCount': 1, - 'commentCount': 2, - 'noteCount': 0, - 'featureCount': 1, - 'relationCount': 0, - 'lastFeatureTime': datetime(1999, 1, 1), - 'favoritedBy': ['fav1'], - 'hasCustomThumbnail': True, - 'mimeType': 'image/jpeg', - 'comments': ['commenter1', 'commenter2'], + "user": "post author", + "score": 1, + "ownFavorite": False, + "ownScore": -1, + "tagCount": 2, + "favoriteCount": 1, + "commentCount": 2, + "noteCount": 0, + "featureCount": 1, + "relationCount": 0, + "lastFeatureTime": datetime(1999, 1, 1), + "favoritedBy": ["fav1"], + "hasCustomThumbnail": True, + "mimeType": "image/jpeg", + "comments": ["commenter1", "commenter2"], } def test_serialize_micro_post(post_factory, user_factory): - with patch('szurubooru.func.posts.get_post_thumbnail_url'): - posts.get_post_thumbnail_url.return_value \ - = 'https://example.com/thumb.png' + with patch("szurubooru.func.posts.get_post_thumbnail_url"): + posts.get_post_thumbnail_url.return_value = ( + "https://example.com/thumb.png" + ) auth_user = user_factory() post = post_factory() db.session.add(post) db.session.flush() assert posts.serialize_micro_post(post, auth_user) == { - 'id': post.post_id, - 'thumbnailUrl': 'https://example.com/thumb.png', + "id": post.post_id, + "thumbnailUrl": "https://example.com/thumb.png", } @@ -299,22 +332,25 @@ def test_get_post_by_id(post_factory): def test_create_post(user_factory, fake_datetime): - with patch('szurubooru.func.posts.update_post_content'), \ - patch('szurubooru.func.posts.update_post_tags'), \ - fake_datetime('1997-01-01'): + with patch("szurubooru.func.posts.update_post_content"), patch( + "szurubooru.func.posts.update_post_tags" + ), fake_datetime("1997-01-01"): auth_user = user_factory() - post, _new_tags = posts.create_post('content', ['tag'], auth_user) + post, _new_tags = posts.create_post("content", ["tag"], auth_user) assert post.creation_time == datetime(1997, 1, 1) assert post.last_edit_time is None - posts.update_post_tags.assert_called_once_with(post, ['tag']) - posts.update_post_content.assert_called_once_with(post, 'content') + posts.update_post_tags.assert_called_once_with(post, ["tag"]) + posts.update_post_content.assert_called_once_with(post, "content") -@pytest.mark.parametrize('input_safety,expected_safety', [ - ('safe', model.Post.SAFETY_SAFE), - ('sketchy', model.Post.SAFETY_SKETCHY), - ('unsafe', model.Post.SAFETY_UNSAFE), -]) +@pytest.mark.parametrize( + "input_safety,expected_safety", + [ + ("safe", model.Post.SAFETY_SAFE), + ("sketchy", model.Post.SAFETY_SKETCHY), + ("unsafe", model.Post.SAFETY_UNSAFE), + ], +) def test_update_post_safety(input_safety, expected_safety): post = model.Post() posts.update_post_safety(post, input_safety) @@ -324,96 +360,104 @@ def test_update_post_safety(input_safety, expected_safety): def test_update_post_safety_with_invalid_string(): post = model.Post() with pytest.raises(posts.InvalidPostSafetyError): - posts.update_post_safety(post, 'bad') + posts.update_post_safety(post, "bad") def test_update_post_source(): post = model.Post() - posts.update_post_source(post, 'x') - assert post.source == 'x' + posts.update_post_source(post, "x") + assert post.source == "x" def test_update_post_source_with_too_long_string(): post = model.Post() with pytest.raises(posts.InvalidPostSourceError): - posts.update_post_source(post, 'x' * 3000) + posts.update_post_source(post, "x" * 3000) @pytest.mark.parametrize( - 'is_existing,input_file,expected_mime_type,expected_type,output_file_name', + "is_existing,input_file,expected_mime_type,expected_type,output_file_name", [ ( True, - 'png.png', - 'image/png', + "png.png", + "image/png", model.Post.TYPE_IMAGE, - '1_244c8840887984c4.png', + "1_244c8840887984c4.png", ), ( False, - 'png.png', - 'image/png', + "png.png", + "image/png", model.Post.TYPE_IMAGE, - '1_244c8840887984c4.png', + "1_244c8840887984c4.png", ), ( False, - 'jpeg.jpg', - 'image/jpeg', + "jpeg.jpg", + "image/jpeg", model.Post.TYPE_IMAGE, - '1_244c8840887984c4.jpg', + "1_244c8840887984c4.jpg", ), ( False, - 'gif.gif', - 'image/gif', + "gif.gif", + "image/gif", model.Post.TYPE_IMAGE, - '1_244c8840887984c4.gif', + "1_244c8840887984c4.gif", ), ( False, - 'gif-animated.gif', - 'image/gif', + "gif-animated.gif", + "image/gif", model.Post.TYPE_ANIMATION, - '1_244c8840887984c4.gif', + "1_244c8840887984c4.gif", ), ( False, - 'webm.webm', - 'video/webm', + "webm.webm", + "video/webm", model.Post.TYPE_VIDEO, - '1_244c8840887984c4.webm', + "1_244c8840887984c4.webm", ), ( False, - 'mp4.mp4', - 'video/mp4', + "mp4.mp4", + "video/mp4", model.Post.TYPE_VIDEO, - '1_244c8840887984c4.mp4', + "1_244c8840887984c4.mp4", ), ( False, - 'flash.swf', - 'application/x-shockwave-flash', + "flash.swf", + "application/x-shockwave-flash", model.Post.TYPE_FLASH, - '1_244c8840887984c4.swf', + "1_244c8840887984c4.swf", ), - ]) + ], +) def test_update_post_content_for_new_post( - tmpdir, config_injector, post_factory, read_asset, is_existing, - input_file, expected_mime_type, expected_type, output_file_name): - with patch('szurubooru.func.util.get_sha1'): - util.get_sha1.return_value = 'crc' - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - 'allow_broken_uploads': False, - }) - output_file_path = '{}/data/posts/{}'.format(tmpdir, output_file_name) + tmpdir, + config_injector, + post_factory, + read_asset, + is_existing, + input_file, + expected_mime_type, + expected_type, + output_file_name, +): + with patch("szurubooru.func.util.get_sha1"): + util.get_sha1.return_value = "crc" + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": {"post_width": 300, "post_height": 300,}, + "secret": "test", + "allow_broken_uploads": False, + } + ) + output_file_path = "{}/data/posts/{}".format(tmpdir, output_file_name) post = post_factory(id=1) db.session.add(post) if is_existing: @@ -426,7 +470,7 @@ def test_update_post_content_for_new_post( db.session.flush() assert post.mime_type == expected_mime_type assert post.type == expected_type - assert post.checksum == 'crc' + assert post.checksum == "crc" assert os.path.exists(output_file_path) if post.type in (model.Post.TYPE_IMAGE, model.Post.TYPE_ANIMATION): assert db.session.query(model.PostSignature).count() == 1 @@ -435,130 +479,134 @@ def test_update_post_content_for_new_post( def test_update_post_content_to_existing_content( - tmpdir, config_injector, post_factory, read_asset): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'data_url': 'example.com', - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - 'allow_broken_uploads': False, - }) + tmpdir, config_injector, post_factory, read_asset +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "data_url": "example.com", + "thumbnails": {"post_width": 300, "post_height": 300,}, + "secret": "test", + "allow_broken_uploads": False, + } + ) post = post_factory() another_post = post_factory() db.session.add_all([post, another_post]) - posts.update_post_content(post, read_asset('png.png')) + posts.update_post_content(post, read_asset("png.png")) db.session.flush() with pytest.raises(posts.PostAlreadyUploadedError): - posts.update_post_content(another_post, read_asset('png.png')) + posts.update_post_content(another_post, read_asset("png.png")) -@pytest.mark.parametrize('allow_broken_uploads', [True, False]) +@pytest.mark.parametrize("allow_broken_uploads", [True, False]) def test_update_post_content_with_broken_content( - tmpdir, config_injector, post_factory, read_asset, - allow_broken_uploads): + tmpdir, config_injector, post_factory, read_asset, allow_broken_uploads +): # the rationale behind this behavior is to salvage user upload even if the # server software thinks it's broken. chances are the server is wrong, # especially about flash movies. - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - 'allow_broken_uploads': allow_broken_uploads, - }) + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": {"post_width": 300, "post_height": 300,}, + "secret": "test", + "allow_broken_uploads": allow_broken_uploads, + } + ) post = post_factory() another_post = post_factory() db.session.add_all([post, another_post]) if allow_broken_uploads: - posts.update_post_content(post, read_asset('png-broken.png')) + posts.update_post_content(post, read_asset("png-broken.png")) db.session.flush() assert post.canvas_width is None assert post.canvas_height is None else: with pytest.raises(posts.InvalidPostContentError): - posts.update_post_content(post, read_asset('png-broken.png')) + posts.update_post_content(post, read_asset("png-broken.png")) db.session.flush() -@pytest.mark.parametrize('input_content', [None, b'not a media file']) +@pytest.mark.parametrize("input_content", [None, b"not a media file"]) def test_update_post_content_with_invalid_content( - config_injector, input_content): - config_injector({ - 'allow_broken_uploads': True, - }) + config_injector, input_content +): + config_injector( + {"allow_broken_uploads": True,} + ) post = model.Post() with pytest.raises(posts.InvalidPostContentError): posts.update_post_content(post, input_content) -@pytest.mark.parametrize('is_existing', (True, False)) +@pytest.mark.parametrize("is_existing", (True, False)) def test_update_post_thumbnail_to_new_one( - tmpdir, config_injector, read_asset, post_factory, is_existing): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - 'allow_broken_uploads': False, - }) + tmpdir, config_injector, read_asset, post_factory, is_existing +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": {"post_width": 300, "post_height": 300,}, + "secret": "test", + "allow_broken_uploads": False, + } + ) post = post_factory(id=1) db.session.add(post) if is_existing: db.session.flush() assert post.post_id generated_path = ( - '{}/data/generated-thumbnails/1_244c8840887984c4.jpg' - .format(tmpdir)) + "{}/data/generated-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.jpg" + ) source_path = ( - '{}/data/posts/custom-thumbnails/1_244c8840887984c4.dat' - .format(tmpdir)) + "{}/data/posts/custom-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.dat" + ) assert not os.path.exists(generated_path) assert not os.path.exists(source_path) - posts.update_post_content(post, read_asset('png.png')) - posts.update_post_thumbnail(post, read_asset('jpeg.jpg')) + posts.update_post_content(post, read_asset("png.png")) + posts.update_post_thumbnail(post, read_asset("jpeg.jpg")) assert not os.path.exists(generated_path) assert not os.path.exists(source_path) db.session.flush() assert os.path.exists(generated_path) assert os.path.exists(source_path) - with open(source_path, 'rb') as handle: - assert handle.read() == read_asset('jpeg.jpg') + with open(source_path, "rb") as handle: + assert handle.read() == read_asset("jpeg.jpg") -@pytest.mark.parametrize('is_existing', (True, False)) +@pytest.mark.parametrize("is_existing", (True, False)) def test_update_post_thumbnail_to_default( - tmpdir, config_injector, read_asset, post_factory, is_existing): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - 'allow_broken_uploads': False, - }) + tmpdir, config_injector, read_asset, post_factory, is_existing +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": {"post_width": 300, "post_height": 300,}, + "secret": "test", + "allow_broken_uploads": False, + } + ) post = post_factory(id=1) db.session.add(post) if is_existing: db.session.flush() assert post.post_id generated_path = ( - '{}/data/generated-thumbnails/1_244c8840887984c4.jpg' - .format(tmpdir)) + "{}/data/generated-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.jpg" + ) source_path = ( - '{}/data/posts/custom-thumbnails/1_244c8840887984c4.dat' - .format(tmpdir)) + "{}/data/posts/custom-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.dat" + ) assert not os.path.exists(generated_path) assert not os.path.exists(source_path) - posts.update_post_content(post, read_asset('png.png')) - posts.update_post_thumbnail(post, read_asset('jpeg.jpg')) + posts.update_post_content(post, read_asset("png.png")) + posts.update_post_thumbnail(post, read_asset("jpeg.jpg")) posts.update_post_thumbnail(post, None) assert not os.path.exists(generated_path) assert not os.path.exists(source_path) @@ -567,82 +615,88 @@ def test_update_post_thumbnail_to_default( assert not os.path.exists(source_path) -@pytest.mark.parametrize('is_existing', (True, False)) +@pytest.mark.parametrize("is_existing", (True, False)) def test_update_post_thumbnail_with_broken_thumbnail( - tmpdir, config_injector, read_asset, post_factory, is_existing): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - 'allow_broken_uploads': False, - }) + tmpdir, config_injector, read_asset, post_factory, is_existing +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": {"post_width": 300, "post_height": 300,}, + "secret": "test", + "allow_broken_uploads": False, + } + ) post = post_factory(id=1) db.session.add(post) if is_existing: db.session.flush() assert post.post_id generated_path = ( - '{}/data/generated-thumbnails/1_244c8840887984c4.jpg' - .format(tmpdir)) + "{}/data/generated-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.jpg" + ) source_path = ( - '{}/data/posts/custom-thumbnails/1_244c8840887984c4.dat' - .format(tmpdir)) + "{}/data/posts/custom-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.dat" + ) assert not os.path.exists(generated_path) assert not os.path.exists(source_path) - posts.update_post_content(post, read_asset('png.png')) - posts.update_post_thumbnail(post, read_asset('png-broken.png')) + posts.update_post_content(post, read_asset("png.png")) + posts.update_post_thumbnail(post, read_asset("png-broken.png")) assert not os.path.exists(generated_path) assert not os.path.exists(source_path) db.session.flush() assert os.path.exists(generated_path) assert os.path.exists(source_path) - with open(source_path, 'rb') as handle: - assert handle.read() == read_asset('png-broken.png') - with open(generated_path, 'rb') as handle: + with open(source_path, "rb") as handle: + assert handle.read() == read_asset("png-broken.png") + with open(generated_path, "rb") as handle: image = images.Image(handle.read()) assert image.width == 1 assert image.height == 1 def test_update_post_content_leaving_custom_thumbnail( - tmpdir, config_injector, read_asset, post_factory): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - 'allow_broken_uploads': False, - }) + tmpdir, config_injector, read_asset, post_factory +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": {"post_width": 300, "post_height": 300,}, + "secret": "test", + "allow_broken_uploads": False, + } + ) post = post_factory(id=1) db.session.add(post) - posts.update_post_content(post, read_asset('png.png')) - posts.update_post_thumbnail(post, read_asset('jpeg.jpg')) - posts.update_post_content(post, read_asset('png.png')) + posts.update_post_content(post, read_asset("png.png")) + posts.update_post_thumbnail(post, read_asset("jpeg.jpg")) + posts.update_post_content(post, read_asset("png.png")) db.session.flush() generated_path = ( - '{}/data/generated-thumbnails/1_244c8840887984c4.jpg' - .format(tmpdir)) + "{}/data/generated-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.jpg" + ) source_path = ( - '{}/data/posts/custom-thumbnails/1_244c8840887984c4.dat' - .format(tmpdir)) + "{}/data/posts/custom-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.dat" + ) assert os.path.exists(source_path) assert os.path.exists(generated_path) def test_update_post_tags(tag_factory): post = model.Post() - with patch('szurubooru.func.tags.get_or_create_tags_by_names'): - tags.get_or_create_tags_by_names.side_effect = lambda tag_names: \ - ([tag_factory(names=[name]) for name in tag_names], []) - posts.update_post_tags(post, ['tag1', 'tag2']) + with patch("szurubooru.func.tags.get_or_create_tags_by_names"): + tags.get_or_create_tags_by_names.side_effect = lambda tag_names: ( + [tag_factory(names=[name]) for name in tag_names], + [], + ) + posts.update_post_tags(post, ["tag1", "tag2"]) assert len(post.tags) == 2 - assert post.tags[0].names[0].name == 'tag1' - assert post.tags[1].names[0].name == 'tag2' + assert post.tags[0].names[0].name == "tag1" + assert post.tags[1].names[0].name == "tag2" def test_update_post_relations(post_factory): @@ -654,7 +708,9 @@ def test_update_post_relations(post_factory): posts.update_post_relations(post, [relation1.post_id, relation2.post_id]) assert len(post.relations) == 2 assert sorted(r.post_id for r in post.relations) == [ - relation1.post_id, relation2.post_id] + relation1.post_id, + relation2.post_id, + ] def test_update_post_relations_bidirectionality(post_factory): @@ -689,35 +745,44 @@ def test_update_post_notes(): posts.update_post_notes( post, [ - {'polygon': [[0, 0], [0, 1], [1, 0], [0, 0]], 'text': 'text1'}, - {'polygon': [[0, 0], [0, 1], [1, 0], [0, 0]], 'text': 'text2'}, - ]) + {"polygon": [[0, 0], [0, 1], [1, 0], [0, 0]], "text": "text1"}, + {"polygon": [[0, 0], [0, 1], [1, 0], [0, 0]], "text": "text2"}, + ], + ) assert len(post.notes) == 2 assert post.notes[0].polygon == [[0, 0], [0, 1], [1, 0], [0, 0]] - assert post.notes[0].text == 'text1' + assert post.notes[0].text == "text1" assert post.notes[1].polygon == [[0, 0], [0, 1], [1, 0], [0, 0]] - assert post.notes[1].text == 'text2' - - -@pytest.mark.parametrize('input', [ - [{'text': '...'}], - [{'polygon': None, 'text': '...'}], - [{'polygon': 'trash', 'text': '...'}], - [{'polygon': ['trash', 'trash', 'trash'], 'text': '...'}], - [{'polygon': {2: 'trash', 3: 'trash', 4: 'trash'}, 'text': '...'}], - [{'polygon': [[0, 0]], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], None], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], 'surprise'], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], {2: 'trash', 3: 'trash'}], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], 5], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], [0, 2]], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], [0, '...']], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], [0, 0, 0]], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], [0]], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], [0, 1]], 'text': ''}], - [{'polygon': [[0, 0], [0, 0], [0, 1]], 'text': None}], - [{'polygon': [[0, 0], [0, 0], [0, 1]]}], -]) + assert post.notes[1].text == "text2" + + +@pytest.mark.parametrize( + "input", + [ + [{"text": "..."}], + [{"polygon": None, "text": "..."}], + [{"polygon": "trash", "text": "..."}], + [{"polygon": ["trash", "trash", "trash"], "text": "..."}], + [{"polygon": {2: "trash", 3: "trash", 4: "trash"}, "text": "..."}], + [{"polygon": [[0, 0]], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], None], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], "surprise"], "text": "..."}], + [ + { + "polygon": [[0, 0], [0, 0], {2: "trash", 3: "trash"}], + "text": "...", + } + ], + [{"polygon": [[0, 0], [0, 0], 5], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], [0, 2]], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], [0, "..."]], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], [0, 0, 0]], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], [0]], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], [0, 1]], "text": ""}], + [{"polygon": [[0, 0], [0, 0], [0, 1]], "text": None}], + [{"polygon": [[0, 0], [0, 0], [0, 1]]}], + ], +) def test_update_post_notes_with_invalid_content(input): post = model.Post() with pytest.raises(posts.InvalidPostNoteError): @@ -726,14 +791,14 @@ def test_update_post_notes_with_invalid_content(input): def test_update_post_flags(): post = model.Post() - posts.update_post_flags(post, ['loop']) - assert post.flags == ['loop'] + posts.update_post_flags(post, ["loop"]) + assert post.flags == ["loop"] def test_update_post_flags_with_invalid_content(): post = model.Post() with pytest.raises(posts.InvalidPostFlagError): - posts.update_post_flags(post, ['invalid']) + posts.update_post_flags(post, ["invalid"]) def test_feature_post(post_factory, user_factory): @@ -749,7 +814,7 @@ def test_feature_post(post_factory, user_factory): def test_delete(post_factory, config_injector): - config_injector({'delete_source_files': False}) + config_injector({"delete_source_files": False}) post = post_factory() db.session.add(post) db.session.flush() @@ -760,7 +825,7 @@ def test_delete(post_factory, config_injector): def test_merge_posts_deletes_source_post(post_factory, config_injector): - config_injector({'delete_source_files': False}) + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() db.session.add_all([source_post, target_post]) @@ -773,7 +838,7 @@ def test_merge_posts_deletes_source_post(post_factory, config_injector): def test_merge_posts_with_itself(post_factory, config_injector): - config_injector({'delete_source_files': False}) + config_injector({"delete_source_files": False}) source_post = post_factory() db.session.add(source_post) db.session.flush() @@ -782,7 +847,7 @@ def test_merge_posts_with_itself(post_factory, config_injector): def test_merge_posts_moves_tags(post_factory, tag_factory, config_injector): - config_injector({'delete_source_files': False}) + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() tag = tag_factory() @@ -798,8 +863,9 @@ def test_merge_posts_moves_tags(post_factory, tag_factory, config_injector): def test_merge_posts_doesnt_duplicate_tags( - post_factory, tag_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, tag_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() tag = tag_factory() @@ -815,8 +881,9 @@ def test_merge_posts_doesnt_duplicate_tags( def test_merge_posts_moves_comments( - post_factory, comment_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, comment_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() comment = comment_factory(post=source_post) @@ -831,8 +898,9 @@ def test_merge_posts_moves_comments( def test_merge_posts_moves_scores( - post_factory, post_score_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, post_score_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() score = post_score_factory(post=source_post, score=1) @@ -847,8 +915,9 @@ def test_merge_posts_moves_scores( def test_merge_posts_doesnt_duplicate_scores( - post_factory, user_factory, post_score_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, user_factory, post_score_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() user = user_factory() @@ -865,8 +934,9 @@ def test_merge_posts_doesnt_duplicate_scores( def test_merge_posts_moves_favorites( - post_factory, post_favorite_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, post_favorite_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() favorite = post_favorite_factory(post=source_post) @@ -881,8 +951,9 @@ def test_merge_posts_moves_favorites( def test_merge_posts_doesnt_duplicate_favorites( - post_factory, user_factory, post_favorite_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, user_factory, post_favorite_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() user = user_factory() @@ -899,7 +970,7 @@ def test_merge_posts_doesnt_duplicate_favorites( def test_merge_posts_moves_child_relations(post_factory, config_injector): - config_injector({'delete_source_files': False}) + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() related_post = post_factory() @@ -915,8 +986,9 @@ def test_merge_posts_moves_child_relations(post_factory, config_injector): def test_merge_posts_doesnt_duplicate_child_relations( - post_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() related_post = post_factory() @@ -933,7 +1005,7 @@ def test_merge_posts_doesnt_duplicate_child_relations( def test_merge_posts_moves_parent_relations(post_factory, config_injector): - config_injector({'delete_source_files': False}) + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() related_post = post_factory() @@ -951,8 +1023,9 @@ def test_merge_posts_moves_parent_relations(post_factory, config_injector): def test_merge_posts_doesnt_duplicate_parent_relations( - post_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() related_post = post_factory() @@ -970,8 +1043,9 @@ def test_merge_posts_doesnt_duplicate_parent_relations( def test_merge_posts_doesnt_create_relation_loop_for_children( - post_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() source_post.relations = [target_post] @@ -986,8 +1060,9 @@ def test_merge_posts_doesnt_create_relation_loop_for_children( def test_merge_posts_doesnt_create_relation_loop_for_parents( - post_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() target_post.relations = [source_post] @@ -1002,30 +1077,33 @@ def test_merge_posts_doesnt_create_relation_loop_for_parents( def test_merge_posts_replaces_content( - post_factory, config_injector, tmpdir, read_asset): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'data_url': 'example.com', - 'delete_source_files': False, - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - }) + post_factory, config_injector, tmpdir, read_asset +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "data_url": "example.com", + "delete_source_files": False, + "thumbnails": {"post_width": 300, "post_height": 300,}, + "secret": "test", + } + ) source_post = post_factory(id=1) target_post = post_factory(id=2) - content = read_asset('png.png') + content = read_asset("png.png") db.session.add_all([source_post, target_post]) db.session.commit() posts.update_post_content(source_post, content) db.session.flush() - source_path = ( - os.path.join('{}/data/posts/1_244c8840887984c4.png'.format(tmpdir))) - target_path1 = ( - os.path.join('{}/data/posts/2_49caeb3ec1643406.png'.format(tmpdir))) - target_path2 = ( - os.path.join('{}/data/posts/2_49caeb3ec1643406.dat'.format(tmpdir))) + source_path = os.path.join( + "{}/data/posts/1_244c8840887984c4.png".format(tmpdir) + ) + target_path1 = os.path.join( + "{}/data/posts/2_49caeb3ec1643406.png".format(tmpdir) + ) + target_path2 = os.path.join( + "{}/data/posts/2_49caeb3ec1643406.dat".format(tmpdir) + ) assert os.path.exists(source_path) assert not os.path.exists(target_path1) assert not os.path.exists(target_path2) @@ -1040,16 +1118,16 @@ def test_merge_posts_replaces_content( def test_search_by_image(post_factory, config_injector, read_asset): - config_injector({'allow_broken_uploads': False}) + config_injector({"allow_broken_uploads": False}) post = post_factory() - posts.generate_post_signature(post, read_asset('jpeg.jpg')) + posts.generate_post_signature(post, read_asset("jpeg.jpg")) db.session.flush() - result1 = posts.search_by_image(read_asset('jpeg-similar.jpg')) + result1 = posts.search_by_image(read_asset("jpeg-similar.jpg")) assert len(result1) == 1 result1_distance, result1_post = result1[0] assert abs(result1_distance - 0.19713075553164386) < 1e-8 assert result1_post.post_id == post.post_id - result2 = posts.search_by_image(read_asset('png.png')) + result2 = posts.search_by_image(read_asset("png.png")) assert not result2 diff --git a/server/szurubooru/tests/func/test_snapshots.py b/server/szurubooru/tests/func/test_snapshots.py index 053cf98..b4dda1c 100644 --- a/server/szurubooru/tests/func/test_snapshots.py +++ b/server/szurubooru/tests/func/test_snapshots.py @@ -1,55 +1,57 @@ -from unittest.mock import patch from datetime import datetime +from unittest.mock import patch + import pytest + from szurubooru import db, model from szurubooru.func import snapshots, users def test_get_tag_category_snapshot(tag_category_factory): - category = tag_category_factory(name='name', color='color') + category = tag_category_factory(name="name", color="color") assert snapshots.get_tag_category_snapshot(category) == { - 'name': 'name', - 'color': 'color', - 'default': False, + "name": "name", + "color": "color", + "default": False, } category.default = True assert snapshots.get_tag_category_snapshot(category) == { - 'name': 'name', - 'color': 'color', - 'default': True, + "name": "name", + "color": "color", + "default": True, } def test_get_tag_snapshot(tag_factory, tag_category_factory): - category = tag_category_factory(name='dummy') - tag = tag_factory(names=['main_name', 'alias'], category=category) + category = tag_category_factory(name="dummy") + tag = tag_factory(names=["main_name", "alias"], category=category) assert snapshots.get_tag_snapshot(tag) == { - 'names': ['main_name', 'alias'], - 'category': 'dummy', - 'suggestions': [], - 'implications': [], + "names": ["main_name", "alias"], + "category": "dummy", + "suggestions": [], + "implications": [], } - tag = tag_factory(names=['main_name', 'alias'], category=category) - imp1 = tag_factory(names=['imp1_main_name', 'imp1_alias']) - imp2 = tag_factory(names=['imp2_main_name', 'imp2_alias']) - sug1 = tag_factory(names=['sug1_main_name', 'sug1_alias']) - sug2 = tag_factory(names=['sug2_main_name', 'sug2_alias']) + tag = tag_factory(names=["main_name", "alias"], category=category) + imp1 = tag_factory(names=["imp1_main_name", "imp1_alias"]) + imp2 = tag_factory(names=["imp2_main_name", "imp2_alias"]) + sug1 = tag_factory(names=["sug1_main_name", "sug1_alias"]) + sug2 = tag_factory(names=["sug2_main_name", "sug2_alias"]) db.session.add_all([imp1, imp2, sug1, sug2]) tag.implications = [imp1, imp2] tag.suggestions = [sug1, sug2] db.session.flush() assert snapshots.get_tag_snapshot(tag) == { - 'names': ['main_name', 'alias'], - 'category': 'dummy', - 'implications': ['imp1_main_name', 'imp2_main_name'], - 'suggestions': ['sug1_main_name', 'sug2_main_name'], + "names": ["main_name", "alias"], + "category": "dummy", + "implications": ["imp1_main_name", "imp2_main_name"], + "suggestions": ["sug1_main_name", "sug2_main_name"], } def test_get_post_snapshot(post_factory, user_factory, tag_factory): - user = user_factory(name='dummy-user') - tag1 = tag_factory(names=['dummy-tag1']) - tag2 = tag_factory(names=['dummy-tag2']) + user = user_factory(name="dummy-user") + tag1 = tag_factory(names=["dummy-tag1"]) + tag2 = tag_factory(names=["dummy-tag2"]) post = post_factory(id=1) related_post1 = post_factory(id=2) related_post2 = post_factory(id=3) @@ -72,13 +74,13 @@ def test_get_post_snapshot(post_factory, user_factory, tag_factory): note = model.PostNote() note.post = post note.polygon = [(1, 1), (200, 1), (200, 200), (1, 200)] - note.text = 'some text' + note.text = "some text" db.session.add_all([score]) db.session.flush() post.user = user - post.checksum = 'deadbeef' - post.source = 'example.com' + post.checksum = "deadbeef" + post.source = "example.com" post.tags.append(tag1) post.tags.append(tag2) post.relations.append(related_post1) @@ -89,17 +91,19 @@ def test_get_post_snapshot(post_factory, user_factory, tag_factory): post.notes.append(note) assert snapshots.get_post_snapshot(post) == { - 'checksum': 'deadbeef', - 'featured': True, - 'flags': [], - 'notes': [{ - 'polygon': [[1, 1], [200, 1], [200, 200], [1, 200]], - 'text': 'some text', - }], - 'relations': [2, 3], - 'safety': 'safe', - 'source': 'example.com', - 'tags': ['dummy-tag1', 'dummy-tag2'], + "checksum": "deadbeef", + "featured": True, + "flags": [], + "notes": [ + { + "polygon": [[1, 1], [200, 1], [200, 200], [1, 200]], + "text": "some text", + } + ], + "relations": [2, 3], + "safety": "safe", + "source": "example.com", + "tags": ["dummy-tag1", "dummy-tag2"], } @@ -107,75 +111,78 @@ def test_serialize_snapshot(user_factory): auth_user = user_factory() snapshot = model.Snapshot() snapshot.operation = snapshot.OPERATION_CREATED - snapshot.resource_type = 'type' - snapshot.resource_name = 'id' - snapshot.user = user_factory(name='issuer') - snapshot.data = {'complex': list('object')} + snapshot.resource_type = "type" + snapshot.resource_name = "id" + snapshot.user = user_factory(name="issuer") + snapshot.data = {"complex": list("object")} snapshot.creation_time = datetime(1997, 1, 1) - with patch('szurubooru.func.users.serialize_micro_user'): - users.serialize_micro_user.return_value = 'mocked' + with patch("szurubooru.func.users.serialize_micro_user"): + users.serialize_micro_user.return_value = "mocked" assert snapshots.serialize_snapshot(snapshot, auth_user) == { - 'operation': 'created', - 'type': 'type', - 'id': 'id', - 'user': 'mocked', - 'data': {'complex': list('object')}, - 'time': datetime(1997, 1, 1), + "operation": "created", + "type": "type", + "id": "id", + "user": "mocked", + "data": {"complex": list("object")}, + "time": datetime(1997, 1, 1), } def test_create(tag_factory, user_factory): - tag = tag_factory(names=['dummy']) + tag = tag_factory(names=["dummy"]) db.session.add(tag) db.session.flush() - with patch('szurubooru.func.snapshots.get_tag_snapshot'): - snapshots.get_tag_snapshot.return_value = 'mocked' + with patch("szurubooru.func.snapshots.get_tag_snapshot"): + snapshots.get_tag_snapshot.return_value = "mocked" snapshots.create(tag, user_factory()) db.session.flush() results = db.session.query(model.Snapshot).all() assert len(results) == 1 assert results[0].operation == model.Snapshot.OPERATION_CREATED - assert results[0].data == 'mocked' + assert results[0].data == "mocked" def test_modify_saves_non_empty_diffs(post_factory, user_factory): - if 'sqlite' in db.session.get_bind().driver: + if "sqlite" in db.session.get_bind().driver: pytest.xfail( - 'SQLite doesn\'t support transaction isolation, ' - 'which is required to retrieve original entity') + "SQLite doesn't support transaction isolation, " + "which is required to retrieve original entity" + ) post = post_factory() - post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text='old')] + post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text="old")] user = user_factory() db.session.add_all([post, user]) db.session.commit() - post.source = 'new source' - post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text='new')] + post.source = "new source" + post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text="new")] db.session.flush() snapshots.modify(post, user) db.session.flush() results = db.session.query(model.Snapshot).all() assert len(results) == 1 assert results[0].data == { - 'type': 'object change', - 'value': { - 'source': { - 'type': 'primitive change', - 'old-value': None, - 'new-value': 'new source', + "type": "object change", + "value": { + "source": { + "type": "primitive change", + "old-value": None, + "new-value": "new source", }, - 'notes': { - 'type': 'list change', - 'removed': [ - {'polygon': [[0, 0], [0, 1], [1, 1]], 'text': 'old'}], - 'added': [ - {'polygon': [[0, 0], [0, 1], [1, 1]], 'text': 'new'}], + "notes": { + "type": "list change", + "removed": [ + {"polygon": [[0, 0], [0, 1], [1, 1]], "text": "old"} + ], + "added": [ + {"polygon": [[0, 0], [0, 1], [1, 1]], "text": "new"} + ], }, }, } def test_modify_doesnt_save_empty_diffs(tag_factory, user_factory): - tag = tag_factory(names=['dummy']) + tag = tag_factory(names=["dummy"]) user = user_factory() db.session.add_all([tag, user]) db.session.commit() @@ -185,26 +192,26 @@ def test_modify_doesnt_save_empty_diffs(tag_factory, user_factory): def test_delete(tag_factory, user_factory): - tag = tag_factory(names=['dummy']) + tag = tag_factory(names=["dummy"]) db.session.add(tag) db.session.flush() - with patch('szurubooru.func.snapshots.get_tag_snapshot'): - snapshots.get_tag_snapshot.return_value = 'mocked' + with patch("szurubooru.func.snapshots.get_tag_snapshot"): + snapshots.get_tag_snapshot.return_value = "mocked" snapshots.delete(tag, user_factory()) db.session.flush() results = db.session.query(model.Snapshot).all() assert len(results) == 1 assert results[0].operation == model.Snapshot.OPERATION_DELETED - assert results[0].data == 'mocked' + assert results[0].data == "mocked" def test_merge(tag_factory, user_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) db.session.add_all([source_tag, target_tag]) db.session.flush() snapshots.merge(source_tag, target_tag, user_factory()) db.session.flush() result = db.session.query(model.Snapshot).one() assert result.operation == model.Snapshot.OPERATION_MERGED - assert result.data == ['tag', 'target'] + assert result.data == ["tag", "target"] diff --git a/server/szurubooru/tests/func/test_tag_categories.py b/server/szurubooru/tests/func/test_tag_categories.py index d886703..143cc49 100644 --- a/server/szurubooru/tests/func/test_tag_categories.py +++ b/server/szurubooru/tests/func/test_tag_categories.py @@ -1,7 +1,9 @@ from unittest.mock import patch + import pytest + from szurubooru import db, model -from szurubooru.func import tag_categories, cache +from szurubooru.func import cache, tag_categories @pytest.fixture(autouse=True) @@ -14,7 +16,7 @@ def test_serialize_category_when_empty(): def test_serialize_category(tag_category_factory, tag_factory): - category = tag_category_factory(name='name', color='color') + category = tag_category_factory(name="name", color="color") category.category_id = 1 category.default = True tag1 = tag_factory(category=category) @@ -23,36 +25,42 @@ def test_serialize_category(tag_category_factory, tag_factory): db.session.flush() result = tag_categories.serialize_category(category) assert result == { - 'name': 'name', - 'color': 'color', - 'default': True, - 'version': 1, - 'usages': 2, + "name": "name", + "color": "color", + "default": True, + "version": 1, + "usages": 2, } def test_create_category_when_first(): - with patch('szurubooru.func.tag_categories.update_category_name'), \ - patch('szurubooru.func.tag_categories.update_category_color'): - category = tag_categories.create_category('name', 'color') + with patch("szurubooru.func.tag_categories.update_category_name"), patch( + "szurubooru.func.tag_categories.update_category_color" + ): + category = tag_categories.create_category("name", "color") assert category.default - tag_categories.update_category_name \ - .assert_called_once_with(category, 'name') - tag_categories.update_category_color \ - .assert_called_once_with(category, 'color') + tag_categories.update_category_name.assert_called_once_with( + category, "name" + ) + tag_categories.update_category_color.assert_called_once_with( + category, "color" + ) def test_create_category_when_subsequent(tag_category_factory): db.session.add(tag_category_factory()) db.session.flush() - with patch('szurubooru.func.tag_categories.update_category_name'), \ - patch('szurubooru.func.tag_categories.update_category_color'): - category = tag_categories.create_category('name', 'color') + with patch("szurubooru.func.tag_categories.update_category_name"), patch( + "szurubooru.func.tag_categories.update_category_color" + ): + category = tag_categories.create_category("name", "color") assert not category.default - tag_categories.update_category_name \ - .assert_called_once_with(category, 'name') - tag_categories.update_category_color \ - .assert_called_once_with(category, 'color') + tag_categories.update_category_name.assert_called_once_with( + category, "name" + ) + tag_categories.update_category_color.assert_called_once_with( + category, "color" + ) def test_update_category_name_with_empty_string(tag_category_factory): @@ -62,38 +70,42 @@ def test_update_category_name_with_empty_string(tag_category_factory): def test_update_category_name_with_invalid_name( - config_injector, tag_category_factory): - config_injector({'tag_category_name_regex': '^[a-z]+$'}) + config_injector, tag_category_factory +): + config_injector({"tag_category_name_regex": "^[a-z]+$"}) category = tag_category_factory() with pytest.raises(tag_categories.InvalidTagCategoryNameError): - tag_categories.update_category_name(category, '0') + tag_categories.update_category_name(category, "0") def test_update_category_name_with_too_long_string( - config_injector, tag_category_factory): - config_injector({'tag_category_name_regex': '^[a-z]+$'}) + config_injector, tag_category_factory +): + config_injector({"tag_category_name_regex": "^[a-z]+$"}) category = tag_category_factory() with pytest.raises(tag_categories.InvalidTagCategoryNameError): - tag_categories.update_category_name(category, 'a' * 3000) + tag_categories.update_category_name(category, "a" * 3000) def test_update_category_name_reusing_other_name( - config_injector, tag_category_factory): - config_injector({'tag_category_name_regex': '.*'}) - db.session.add(tag_category_factory(name='name')) + config_injector, tag_category_factory +): + config_injector({"tag_category_name_regex": ".*"}) + db.session.add(tag_category_factory(name="name")) db.session.flush() category = tag_category_factory() with pytest.raises(tag_categories.TagCategoryAlreadyExistsError): - tag_categories.update_category_name(category, 'name') + tag_categories.update_category_name(category, "name") with pytest.raises(tag_categories.TagCategoryAlreadyExistsError): - tag_categories.update_category_name(category, 'NAME') + tag_categories.update_category_name(category, "NAME") def test_update_category_name_reusing_own_name( - config_injector, tag_category_factory): - config_injector({'tag_category_name_regex': '.*'}) - for name in ['name', 'NAME']: - category = tag_category_factory(name='name') + config_injector, tag_category_factory +): + config_injector({"tag_category_name_regex": ".*"}) + for name in ["name", "NAME"]: + category = tag_category_factory(name="name") db.session.add(category) db.session.flush() tag_categories.update_category_name(category, name) @@ -110,16 +122,16 @@ def test_update_category_color_with_empty_string(tag_category_factory): def test_update_category_color_with_too_long_string(tag_category_factory): category = tag_category_factory() with pytest.raises(tag_categories.InvalidTagCategoryColorError): - tag_categories.update_category_color(category, 'a' * 3000) + tag_categories.update_category_color(category, "a" * 3000) def test_update_category_color_with_invalid_string(tag_category_factory): category = tag_category_factory() with pytest.raises(tag_categories.InvalidTagCategoryColorError): - tag_categories.update_category_color(category, 'NOPE') + tag_categories.update_category_color(category, "NOPE") -@pytest.mark.parametrize('attempt', ['#aaaaaa', '#012345', '012345', 'red']) +@pytest.mark.parametrize("attempt", ["#aaaaaa", "#012345", "012345", "red"]) def test_update_category_color(attempt, tag_category_factory): category = tag_category_factory() tag_categories.update_category_color(category, attempt) @@ -127,35 +139,35 @@ def test_update_category_color(attempt, tag_category_factory): def test_try_get_category_by_name(tag_category_factory): - category = tag_category_factory(name='test') + category = tag_category_factory(name="test") db.session.add(category) db.session.flush() - assert tag_categories.try_get_category_by_name('test') == category - assert tag_categories.try_get_category_by_name('TEST') == category - assert tag_categories.try_get_category_by_name('-') is None + assert tag_categories.try_get_category_by_name("test") == category + assert tag_categories.try_get_category_by_name("TEST") == category + assert tag_categories.try_get_category_by_name("-") is None def test_get_category_by_name(tag_category_factory): - category = tag_category_factory(name='test') + category = tag_category_factory(name="test") db.session.add(category) db.session.flush() - assert tag_categories.get_category_by_name('test') == category - assert tag_categories.get_category_by_name('TEST') == category + assert tag_categories.get_category_by_name("test") == category + assert tag_categories.get_category_by_name("TEST") == category with pytest.raises(tag_categories.TagCategoryNotFoundError): - tag_categories.get_category_by_name('-') + tag_categories.get_category_by_name("-") def test_get_all_category_names(tag_category_factory): - category1 = tag_category_factory(name='cat1') - category2 = tag_category_factory(name='cat2') + category1 = tag_category_factory(name="cat1") + category2 = tag_category_factory(name="cat2") db.session.add_all([category2, category1]) db.session.flush() - assert tag_categories.get_all_category_names() == ['cat1', 'cat2'] + assert tag_categories.get_all_category_names() == ["cat1", "cat2"] def test_get_all_categories(tag_category_factory): - category1 = tag_category_factory(name='cat1') - category2 = tag_category_factory(name='cat2') + category1 = tag_category_factory(name="cat1") + category2 = tag_category_factory(name="cat2") db.session.add_all([category2, category1]) db.session.flush() assert tag_categories.get_all_categories() == [category1, category2] @@ -211,12 +223,12 @@ def test_get_default_category_name_caching(tag_category_factory): def test_get_default_category(): - with patch('szurubooru.func.tag_categories.try_get_default_category'): + with patch("szurubooru.func.tag_categories.try_get_default_category"): tag_categories.try_get_default_category.return_value = None with pytest.raises(tag_categories.TagCategoryNotFoundError): tag_categories.get_default_category() - tag_categories.try_get_default_category.return_value = 'mocked' - assert tag_categories.get_default_category() == 'mocked' + tag_categories.try_get_default_category.return_value = "mocked" + assert tag_categories.get_default_category() == "mocked" def test_set_default_category_with_previous_default(tag_category_factory): @@ -257,9 +269,9 @@ def test_delete_category_with_usages(tag_category_factory, tag_factory): def test_delete_category(tag_category_factory): db.session.add(tag_category_factory()) - category = tag_category_factory(name='target') + category = tag_category_factory(name="target") db.session.add(category) db.session.flush() tag_categories.delete_category(category) db.session.flush() - assert tag_categories.try_get_category_by_name('target') is None + assert tag_categories.try_get_category_by_name("target") is None diff --git a/server/szurubooru/tests/func/test_tags.py b/server/szurubooru/tests/func/test_tags.py index 673e37f..ac8963c 100644 --- a/server/szurubooru/tests/func/test_tags.py +++ b/server/szurubooru/tests/func/test_tags.py @@ -1,10 +1,12 @@ -import os import json -from unittest.mock import patch +import os from datetime import datetime +from unittest.mock import patch + import pytest + from szurubooru import db, model -from szurubooru.func import tags, tag_categories, cache +from szurubooru.func import cache, tag_categories, tags @pytest.fixture(autouse=True) @@ -14,18 +16,35 @@ def purge_cache(): def _assert_tag_siblings(result, expected_names_and_occurrences): actual_names_and_occurences = [ - (tag.names[0].name, occurrences) for tag, occurrences in result] + (tag.names[0].name, occurrences) for tag, occurrences in result + ] assert actual_names_and_occurences == expected_names_and_occurrences -@pytest.mark.parametrize('input,expected_tag_names', [ - ([('a', 'a', True), ('b', 'b', False), ('c', 'c', False)], list('bca')), - ([('c', 'a', True), ('b', 'b', False), ('a', 'c', False)], list('bac')), - ([('a', 'c', True), ('b', 'b', False), ('c', 'a', False)], list('cba')), - ([('a', 'c', False), ('b', 'b', False), ('c', 'a', True)], list('bac')), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ( + [("a", "a", True), ("b", "b", False), ("c", "c", False)], + list("bca"), + ), + ( + [("c", "a", True), ("b", "b", False), ("a", "c", False)], + list("bac"), + ), + ( + [("a", "c", True), ("b", "b", False), ("c", "a", False)], + list("cba"), + ), + ( + [("a", "c", False), ("b", "b", False), ("c", "a", True)], + list("bac"), + ), + ], +) def test_sort_tags( - input, expected_tag_names, tag_factory, tag_category_factory): + input, expected_tag_names, tag_factory, tag_category_factory +): db_tags = [] for tag in input: tag_name, category_name, category_is_default = tag @@ -33,7 +52,10 @@ def test_sort_tags( tag_factory( names=[tag_name], category=tag_category_factory( - name=category_name, default=category_is_default))) + name=category_name, default=category_is_default + ), + ) + ) db.session.add_all(db_tags) db.session.flush() actual_tag_names = [tag.names[0].name for tag in tags.sort_tags(db_tags)] @@ -45,17 +67,17 @@ def test_serialize_tag_when_empty(): def test_serialize_tag(post_factory, tag_factory, tag_category_factory): - cat = tag_category_factory(name='cat') - tag = tag_factory(names=['tag1', 'tag2'], category=cat) + cat = tag_category_factory(name="cat") + tag = tag_factory(names=["tag1", "tag2"], category=cat) # tag.tag_id = 1 - tag.description = 'description' + tag.description = "description" tag.suggestions = [ - tag_factory(names=['sug1'], category=cat), - tag_factory(names=['sug2'], category=cat), + tag_factory(names=["sug1"], category=cat), + tag_factory(names=["sug2"], category=cat), ] tag.implications = [ - tag_factory(names=['impl1'], category=cat), - tag_factory(names=['impl2'], category=cat), + tag_factory(names=["impl1"], category=cat), + tag_factory(names=["impl2"], category=cat), ] tag.last_edit_time = datetime(1998, 1, 1) @@ -67,36 +89,39 @@ def test_serialize_tag(post_factory, tag_factory, tag_category_factory): db.session.flush() result = tags.serialize_tag(tag) - result['suggestions'].sort(key=lambda relation: relation['names'][0]) - result['implications'].sort(key=lambda relation: relation['names'][0]) + result["suggestions"].sort(key=lambda relation: relation["names"][0]) + result["implications"].sort(key=lambda relation: relation["names"][0]) assert result == { - 'names': ['tag1', 'tag2'], - 'version': 1, - 'category': 'cat', - 'creationTime': datetime(1996, 1, 1, 0, 0), - 'lastEditTime': datetime(1998, 1, 1, 0, 0), - 'description': 'description', - 'suggestions': [ - {'names': ['sug1'], 'category': 'cat', 'usages': 0}, - {'names': ['sug2'], 'category': 'cat', 'usages': 0}, + "names": ["tag1", "tag2"], + "version": 1, + "category": "cat", + "creationTime": datetime(1996, 1, 1, 0, 0), + "lastEditTime": datetime(1998, 1, 1, 0, 0), + "description": "description", + "suggestions": [ + {"names": ["sug1"], "category": "cat", "usages": 0}, + {"names": ["sug2"], "category": "cat", "usages": 0}, ], - 'implications': [ - {'names': ['impl1'], 'category': 'cat', 'usages': 0}, - {'names': ['impl2'], 'category': 'cat', 'usages': 0}, + "implications": [ + {"names": ["impl1"], "category": "cat", "usages": 0}, + {"names": ["impl2"], "category": "cat", "usages": 0}, ], - 'usages': 2, + "usages": 2, } -@pytest.mark.parametrize('name_to_search,expected_to_find', [ - ('name', True), - ('NAME', True), - ('alias', True), - ('ALIAS', True), - ('-', False), -]) +@pytest.mark.parametrize( + "name_to_search,expected_to_find", + [ + ("name", True), + ("NAME", True), + ("alias", True), + ("ALIAS", True), + ("-", False), + ], +) def test_try_get_tag_by_name(name_to_search, expected_to_find, tag_factory): - tag = tag_factory(names=['name', 'ALIAS']) + tag = tag_factory(names=["name", "ALIAS"]) db.session.add(tag) db.session.flush() if expected_to_find: @@ -105,15 +130,18 @@ def test_try_get_tag_by_name(name_to_search, expected_to_find, tag_factory): assert tags.try_get_tag_by_name(name_to_search) is None -@pytest.mark.parametrize('name_to_search,expected_to_find', [ - ('name', True), - ('NAME', True), - ('alias', True), - ('ALIAS', True), - ('-', False), -]) +@pytest.mark.parametrize( + "name_to_search,expected_to_find", + [ + ("name", True), + ("NAME", True), + ("alias", True), + ("ALIAS", True), + ("-", False), + ], +) def test_get_tag_by_name(name_to_search, expected_to_find, tag_factory): - tag = tag_factory(names=['name', 'ALIAS']) + tag = tag_factory(names=["name", "ALIAS"]) db.session.add(tag) db.session.flush() if expected_to_find: @@ -123,25 +151,28 @@ def test_get_tag_by_name(name_to_search, expected_to_find, tag_factory): tags.get_tag_by_name(name_to_search) -@pytest.mark.parametrize('names,expected_indexes', [ - ([], []), - (['name1'], [0]), - (['NAME1'], [0]), - (['alias1'], [0]), - (['ALIAS1'], [0]), - (['name2'], [1]), - (['name1', 'name1'], [0]), - (['name1', 'NAME1'], [0]), - (['name1', 'alias1'], [0]), - (['name1', 'alias2'], [0, 1]), - (['NAME1', 'alias2'], [0, 1]), - (['name1', 'ALIAS2'], [0, 1]), - (['name2', 'alias1'], [0, 1]), -]) +@pytest.mark.parametrize( + "names,expected_indexes", + [ + ([], []), + (["name1"], [0]), + (["NAME1"], [0]), + (["alias1"], [0]), + (["ALIAS1"], [0]), + (["name2"], [1]), + (["name1", "name1"], [0]), + (["name1", "NAME1"], [0]), + (["name1", "alias1"], [0]), + (["name1", "alias2"], [0, 1]), + (["NAME1", "alias2"], [0, 1]), + (["name1", "ALIAS2"], [0, 1]), + (["name2", "alias1"], [0, 1]), + ], +) def test_get_tag_by_names(names, expected_indexes, tag_factory): input_tags = [ - tag_factory(names=['name1', 'ALIAS1']), - tag_factory(names=['name2', 'ALIAS2']), + tag_factory(names=["name1", "ALIAS1"]), + tag_factory(names=["name2", "ALIAS2"]), ] db.session.add_all(input_tags) db.session.flush() @@ -151,49 +182,52 @@ def test_get_tag_by_names(names, expected_indexes, tag_factory): @pytest.mark.parametrize( - 'names,expected_indexes,expected_created_names', [ + "names,expected_indexes,expected_created_names", + [ ([], [], []), - (['name1'], [0], []), - (['NAME1'], [0], []), - (['alias1'], [0], []), - (['ALIAS1'], [0], []), - (['name2'], [1], []), - (['name1', 'name1'], [0], []), - (['name1', 'NAME1'], [0], []), - (['name1', 'alias1'], [0], []), - (['name1', 'alias2'], [0, 1], []), - (['NAME1', 'alias2'], [0, 1], []), - (['name1', 'ALIAS2'], [0, 1], []), - (['name2', 'alias1'], [0, 1], []), - (['new'], [], ['new']), - (['new', 'name1'], [0], ['new']), - (['new', 'NAME1'], [0], ['new']), - (['new', 'alias1'], [0], ['new']), - (['new', 'ALIAS1'], [0], ['new']), - (['new', 'name2'], [1], ['new']), - (['new', 'name1', 'name1'], [0], ['new']), - (['new', 'name1', 'NAME1'], [0], ['new']), - (['new', 'name1', 'alias1'], [0], ['new']), - (['new', 'name1', 'alias2'], [0, 1], ['new']), - (['new', 'NAME1', 'alias2'], [0, 1], ['new']), - (['new', 'name1', 'ALIAS2'], [0, 1], ['new']), - (['new', 'name2', 'alias1'], [0, 1], ['new']), - (['new', 'new'], [], ['new']), - (['new', 'NEW'], [], ['new']), - (['new', 'new2'], [], ['new', 'new2']), - ]) + (["name1"], [0], []), + (["NAME1"], [0], []), + (["alias1"], [0], []), + (["ALIAS1"], [0], []), + (["name2"], [1], []), + (["name1", "name1"], [0], []), + (["name1", "NAME1"], [0], []), + (["name1", "alias1"], [0], []), + (["name1", "alias2"], [0, 1], []), + (["NAME1", "alias2"], [0, 1], []), + (["name1", "ALIAS2"], [0, 1], []), + (["name2", "alias1"], [0, 1], []), + (["new"], [], ["new"]), + (["new", "name1"], [0], ["new"]), + (["new", "NAME1"], [0], ["new"]), + (["new", "alias1"], [0], ["new"]), + (["new", "ALIAS1"], [0], ["new"]), + (["new", "name2"], [1], ["new"]), + (["new", "name1", "name1"], [0], ["new"]), + (["new", "name1", "NAME1"], [0], ["new"]), + (["new", "name1", "alias1"], [0], ["new"]), + (["new", "name1", "alias2"], [0, 1], ["new"]), + (["new", "NAME1", "alias2"], [0, 1], ["new"]), + (["new", "name1", "ALIAS2"], [0, 1], ["new"]), + (["new", "name2", "alias1"], [0, 1], ["new"]), + (["new", "new"], [], ["new"]), + (["new", "NEW"], [], ["new"]), + (["new", "new2"], [], ["new", "new2"]), + ], +) def test_get_or_create_tags_by_names( - names, - expected_indexes, - expected_created_names, - tag_factory, - tag_category_factory, - config_injector): - config_injector({'tag_name_regex': '.*'}) + names, + expected_indexes, + expected_created_names, + tag_factory, + tag_category_factory, + config_injector, +): + config_injector({"tag_name_regex": ".*"}) category = tag_category_factory() input_tags = [ - tag_factory(names=['name1', 'ALIAS1'], category=category), - tag_factory(names=['name2', 'ALIAS2'], category=category), + tag_factory(names=["name1", "ALIAS1"], category=category), + tag_factory(names=["name2", "ALIAS2"], category=category), ] db.session.add_all(input_tags) db.session.flush() @@ -206,14 +240,14 @@ def test_get_or_create_tags_by_names( def test_get_tag_siblings_for_unused(tag_factory): - tag = tag_factory(names=['tag']) + tag = tag_factory(names=["tag"]) db.session.add(tag) db.session.flush() _assert_tag_siblings(tags.get_tag_siblings(tag), []) def test_get_tag_siblings_for_used_alone(tag_factory, post_factory): - tag = tag_factory(names=['tag']) + tag = tag_factory(names=["tag"]) post = post_factory() post.tags = [tag] db.session.add_all([post, tag]) @@ -222,20 +256,20 @@ def test_get_tag_siblings_for_used_alone(tag_factory, post_factory): def test_get_tag_siblings_for_used_with_others(tag_factory, post_factory): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) post = post_factory() post.tags = [tag1, tag2] db.session.add_all([post, tag1, tag2]) db.session.flush() - _assert_tag_siblings(tags.get_tag_siblings(tag1), [('t2', 1)]) - _assert_tag_siblings(tags.get_tag_siblings(tag2), [('t1', 1)]) + _assert_tag_siblings(tags.get_tag_siblings(tag1), [("t2", 1)]) + _assert_tag_siblings(tags.get_tag_siblings(tag2), [("t1", 1)]) def test_get_tag_siblings_used_for_multiple_others(tag_factory, post_factory): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) - tag3 = tag_factory(names=['t3']) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) + tag3 = tag_factory(names=["t3"]) post1 = post_factory() post2 = post_factory() post3 = post_factory() @@ -246,16 +280,16 @@ def test_get_tag_siblings_used_for_multiple_others(tag_factory, post_factory): post4.tags = [tag2] db.session.add_all([post1, post2, post3, post4, tag1, tag2, tag3]) db.session.flush() - _assert_tag_siblings(tags.get_tag_siblings(tag1), [('t3', 2), ('t2', 1)]) - _assert_tag_siblings(tags.get_tag_siblings(tag2), [('t1', 1), ('t3', 1)]) + _assert_tag_siblings(tags.get_tag_siblings(tag1), [("t3", 2), ("t2", 1)]) + _assert_tag_siblings(tags.get_tag_siblings(tag2), [("t1", 1), ("t3", 1)]) # even though tag2 is used more widely, tag1 is more relevant to tag3 - _assert_tag_siblings(tags.get_tag_siblings(tag3), [('t1', 2), ('t2', 1)]) + _assert_tag_siblings(tags.get_tag_siblings(tag3), [("t1", 2), ("t2", 1)]) def test_delete(tag_factory): - tag = tag_factory(names=['tag']) - tag.suggestions = [tag_factory(names=['sug'])] - tag.implications = [tag_factory(names=['imp'])] + tag = tag_factory(names=["tag"]) + tag.suggestions = [tag_factory(names=["sug"])] + tag.implications = [tag_factory(names=["imp"])] db.session.add(tag) db.session.flush() assert db.session.query(model.Tag).count() == 3 @@ -265,19 +299,19 @@ def test_delete(tag_factory): def test_merge_tags_deletes_source_tag(tag_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) db.session.add_all([source_tag, target_tag]) db.session.flush() tags.merge_tags(source_tag, target_tag) db.session.flush() - assert tags.try_get_tag_by_name('source') is None - tag = tags.get_tag_by_name('target') + assert tags.try_get_tag_by_name("source") is None + tag = tags.get_tag_by_name("target") assert tag is not None def test_merge_tags_with_itself(tag_factory): - source_tag = tag_factory(names=['source']) + source_tag = tag_factory(names=["source"]) db.session.add(source_tag) db.session.flush() with pytest.raises(tags.InvalidTagRelationError): @@ -285,8 +319,8 @@ def test_merge_tags_with_itself(tag_factory): def test_merge_tags_moves_usages(tag_factory, post_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) post = post_factory() post.tags = [source_tag] db.session.add_all([source_tag, target_tag, post]) @@ -295,13 +329,13 @@ def test_merge_tags_moves_usages(tag_factory, post_factory): assert target_tag.post_count == 0 tags.merge_tags(source_tag, target_tag) db.session.commit() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').post_count == 1 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("target").post_count == 1 def test_merge_tags_doesnt_duplicate_usages(tag_factory, post_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) post = post_factory() post.tags = [source_tag, target_tag] db.session.add_all([source_tag, target_tag, post]) @@ -310,13 +344,13 @@ def test_merge_tags_doesnt_duplicate_usages(tag_factory, post_factory): assert target_tag.post_count == 1 tags.merge_tags(source_tag, target_tag) db.session.flush() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').post_count == 1 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("target").post_count == 1 def test_merge_tags_moves_child_relations(tag_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) related_tag = tag_factory() source_tag.suggestions = [related_tag] source_tag.implications = [related_tag] @@ -328,14 +362,14 @@ def test_merge_tags_moves_child_relations(tag_factory): assert target_tag.implication_count == 0 tags.merge_tags(source_tag, target_tag) db.session.commit() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').suggestion_count == 1 - assert tags.get_tag_by_name('target').implication_count == 1 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("target").suggestion_count == 1 + assert tags.get_tag_by_name("target").implication_count == 1 def test_merge_tags_doesnt_duplicate_child_relations(tag_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) related_tag = tag_factory() source_tag.suggestions = [related_tag] source_tag.implications = [related_tag] @@ -349,15 +383,15 @@ def test_merge_tags_doesnt_duplicate_child_relations(tag_factory): assert target_tag.implication_count == 1 tags.merge_tags(source_tag, target_tag) db.session.commit() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').suggestion_count == 1 - assert tags.get_tag_by_name('target').implication_count == 1 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("target").suggestion_count == 1 + assert tags.get_tag_by_name("target").implication_count == 1 def test_merge_tags_moves_parent_relations(tag_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) - related_tag = tag_factory(names=['related']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) + related_tag = tag_factory(names=["related"]) related_tag.suggestions = [related_tag] related_tag.implications = [related_tag] db.session.add_all([source_tag, target_tag, related_tag]) @@ -368,16 +402,16 @@ def test_merge_tags_moves_parent_relations(tag_factory): assert target_tag.implication_count == 0 tags.merge_tags(source_tag, target_tag) db.session.commit() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('related').suggestion_count == 1 - assert tags.get_tag_by_name('related').suggestion_count == 1 - assert tags.get_tag_by_name('target').suggestion_count == 0 - assert tags.get_tag_by_name('target').implication_count == 0 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("related").suggestion_count == 1 + assert tags.get_tag_by_name("related").suggestion_count == 1 + assert tags.get_tag_by_name("target").suggestion_count == 0 + assert tags.get_tag_by_name("target").implication_count == 0 def test_merge_tags_doesnt_create_relation_loop_for_children(tag_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) source_tag.suggestions = [target_tag] source_tag.implications = [target_tag] db.session.add_all([source_tag, target_tag]) @@ -388,14 +422,14 @@ def test_merge_tags_doesnt_create_relation_loop_for_children(tag_factory): assert target_tag.implication_count == 0 tags.merge_tags(source_tag, target_tag) db.session.commit() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').suggestion_count == 0 - assert tags.get_tag_by_name('target').implication_count == 0 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("target").suggestion_count == 0 + assert tags.get_tag_by_name("target").implication_count == 0 def test_merge_tags_doesnt_create_relation_loop_for_parents(tag_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) target_tag.suggestions = [source_tag] target_tag.implications = [source_tag] db.session.add_all([source_tag, target_tag]) @@ -406,33 +440,35 @@ def test_merge_tags_doesnt_create_relation_loop_for_parents(tag_factory): assert target_tag.implication_count == 1 tags.merge_tags(source_tag, target_tag) db.session.commit() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').suggestion_count == 0 - assert tags.get_tag_by_name('target').implication_count == 0 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("target").suggestion_count == 0 + assert tags.get_tag_by_name("target").implication_count == 0 def test_create_tag(fake_datetime): - with patch('szurubooru.func.tags.update_tag_names'), \ - patch('szurubooru.func.tags.update_tag_category_name'), \ - patch('szurubooru.func.tags.update_tag_suggestions'), \ - patch('szurubooru.func.tags.update_tag_implications'), \ - fake_datetime('1997-01-01'): - tag = tags.create_tag(['name'], 'cat', ['sug'], ['imp']) + with patch("szurubooru.func.tags.update_tag_names"), patch( + "szurubooru.func.tags.update_tag_category_name" + ), patch("szurubooru.func.tags.update_tag_suggestions"), patch( + "szurubooru.func.tags.update_tag_implications" + ), fake_datetime( + "1997-01-01" + ): + tag = tags.create_tag(["name"], "cat", ["sug"], ["imp"]) assert tag.creation_time == datetime(1997, 1, 1) assert tag.last_edit_time is None - tags.update_tag_names.assert_called_once_with(tag, ['name']) - tags.update_tag_category_name.assert_called_once_with(tag, 'cat') - tags.update_tag_suggestions.assert_called_once_with(tag, ['sug']) - tags.update_tag_implications.assert_called_once_with(tag, ['imp']) + tags.update_tag_names.assert_called_once_with(tag, ["name"]) + tags.update_tag_category_name.assert_called_once_with(tag, "cat") + tags.update_tag_suggestions.assert_called_once_with(tag, ["sug"]) + tags.update_tag_implications.assert_called_once_with(tag, ["imp"]) def test_update_tag_category_name(tag_factory): - with patch('szurubooru.func.tag_categories.get_category_by_name'): - tag_categories.get_category_by_name.return_value = 'mocked' + with patch("szurubooru.func.tag_categories.get_category_by_name"): + tag_categories.get_category_by_name.return_value = "mocked" tag = tag_factory() - tags.update_tag_category_name(tag, 'cat') - assert tag_categories.get_category_by_name.called_once_with('cat') - assert tag.category == 'mocked' + tags.update_tag_category_name(tag, "cat") + assert tag_categories.get_category_by_name.called_once_with("cat") + assert tag.category == "mocked" def test_update_tag_names_to_empty(tag_factory): @@ -442,44 +478,45 @@ def test_update_tag_names_to_empty(tag_factory): def test_update_tag_names_with_invalid_name(config_injector, tag_factory): - config_injector({'tag_name_regex': '^[a-z]*$'}) + config_injector({"tag_name_regex": "^[a-z]*$"}) tag = tag_factory() with pytest.raises(tags.InvalidTagNameError): - tags.update_tag_names(tag, ['0']) + tags.update_tag_names(tag, ["0"]) def test_update_tag_names_with_too_long_string(config_injector, tag_factory): - config_injector({'tag_name_regex': '^[a-z]*$'}) + config_injector({"tag_name_regex": "^[a-z]*$"}) tag = tag_factory() with pytest.raises(tags.InvalidTagNameError): - tags.update_tag_names(tag, ['a' * 300]) + tags.update_tag_names(tag, ["a" * 300]) def test_update_tag_names_with_duplicate_names(config_injector, tag_factory): - config_injector({'tag_name_regex': '^[a-z]*$'}) + config_injector({"tag_name_regex": "^[a-z]*$"}) tag = tag_factory() - tags.update_tag_names(tag, ['a', 'A']) - assert [tag_name.name for tag_name in tag.names] == ['a'] + tags.update_tag_names(tag, ["a", "A"]) + assert [tag_name.name for tag_name in tag.names] == ["a"] def test_update_tag_names_trying_to_use_taken_name( - config_injector, tag_factory): - config_injector({'tag_name_regex': '^[a-zA-Z]*$'}) - existing_tag = tag_factory(names=['a']) + config_injector, tag_factory +): + config_injector({"tag_name_regex": "^[a-zA-Z]*$"}) + existing_tag = tag_factory(names=["a"]) db.session.add(existing_tag) tag = tag_factory() db.session.add(tag) db.session.flush() with pytest.raises(tags.TagAlreadyExistsError): - tags.update_tag_names(tag, ['a']) + tags.update_tag_names(tag, ["a"]) with pytest.raises(tags.TagAlreadyExistsError): - tags.update_tag_names(tag, ['A']) + tags.update_tag_names(tag, ["A"]) def test_update_tag_names_reusing_own_name(config_injector, tag_factory): - config_injector({'tag_name_regex': '^[a-zA-Z]*$'}) - for name in list('aA'): - tag = tag_factory(names=['a']) + config_injector({"tag_name_regex": "^[a-zA-Z]*$"}) + for name in list("aA"): + tag = tag_factory(names=["a"]) db.session.add(tag) db.session.flush() tags.update_tag_names(tag, [name]) @@ -488,48 +525,48 @@ def test_update_tag_names_reusing_own_name(config_injector, tag_factory): def test_update_tag_names_changing_primary_name(config_injector, tag_factory): - config_injector({'tag_name_regex': '^[a-zA-Z]*$'}) - tag = tag_factory(names=['a', 'b']) + config_injector({"tag_name_regex": "^[a-zA-Z]*$"}) + tag = tag_factory(names=["a", "b"]) db.session.add(tag) db.session.flush() - tags.update_tag_names(tag, ['b', 'a']) + tags.update_tag_names(tag, ["b", "a"]) db.session.flush() db.session.refresh(tag) - assert [tag_name.name for tag_name in tag.names] == ['b', 'a'] + assert [tag_name.name for tag_name in tag.names] == ["b", "a"] db.session.rollback() -@pytest.mark.parametrize('attempt', ['name', 'NAME', 'alias', 'ALIAS']) +@pytest.mark.parametrize("attempt", ["name", "NAME", "alias", "ALIAS"]) def test_update_tag_suggestions_with_itself(attempt, tag_factory): - tag = tag_factory(names=['name', 'ALIAS']) + tag = tag_factory(names=["name", "ALIAS"]) with pytest.raises(tags.InvalidTagRelationError): tags.update_tag_suggestions(tag, [attempt]) def test_update_tag_suggestions(tag_factory): - tag = tag_factory(names=['name', 'ALIAS']) - with patch('szurubooru.func.tags.get_tags_by_names'): - tags.get_tags_by_names.return_value = ['returned tags'] - tags.update_tag_suggestions(tag, ['test']) - assert tag.suggestions == ['returned tags'] + tag = tag_factory(names=["name", "ALIAS"]) + with patch("szurubooru.func.tags.get_tags_by_names"): + tags.get_tags_by_names.return_value = ["returned tags"] + tags.update_tag_suggestions(tag, ["test"]) + assert tag.suggestions == ["returned tags"] -@pytest.mark.parametrize('attempt', ['name', 'NAME', 'alias', 'ALIAS']) +@pytest.mark.parametrize("attempt", ["name", "NAME", "alias", "ALIAS"]) def test_update_tag_implications_with_itself(attempt, tag_factory): - tag = tag_factory(names=['name', 'ALIAS']) + tag = tag_factory(names=["name", "ALIAS"]) with pytest.raises(tags.InvalidTagRelationError): tags.update_tag_implications(tag, [attempt]) def test_update_tag_implications(tag_factory): - tag = tag_factory(names=['name', 'ALIAS']) - with patch('szurubooru.func.tags.get_tags_by_names'): - tags.get_tags_by_names.return_value = ['returned tags'] - tags.update_tag_implications(tag, ['test']) - assert tag.implications == ['returned tags'] + tag = tag_factory(names=["name", "ALIAS"]) + with patch("szurubooru.func.tags.get_tags_by_names"): + tags.get_tags_by_names.return_value = ["returned tags"] + tags.update_tag_implications(tag, ["test"]) + assert tag.implications == ["returned tags"] def test_update_tag_description(tag_factory): tag = tag_factory() - tags.update_tag_description(tag, 'test') - assert tag.description == 'test' + tags.update_tag_description(tag, "test") + assert tag.description == "test" diff --git a/server/szurubooru/tests/func/test_user_tokens.py b/server/szurubooru/tests/func/test_user_tokens.py index 8c3577c..0422f4d 100644 --- a/server/szurubooru/tests/func/test_user_tokens.py +++ b/server/szurubooru/tests/func/test_user_tokens.py @@ -1,32 +1,35 @@ +import random +import string from datetime import datetime, timedelta from unittest.mock import patch + import pytest import pytz -import random -import string + from szurubooru import db, model -from szurubooru.func import user_tokens, users, auth, util +from szurubooru.func import auth, user_tokens, users, util def test_serialize_user_token(user_token_factory): user_token = user_token_factory() db.session.add(user_token) db.session.flush() - with patch('szurubooru.func.users.get_avatar_url'): - users.get_avatar_url.return_value = 'https://example.com/avatar.png' + with patch("szurubooru.func.users.get_avatar_url"): + users.get_avatar_url.return_value = "https://example.com/avatar.png" result = user_tokens.serialize_user_token(user_token, user_token.user) assert result == { - 'creationTime': datetime(1997, 1, 1, 0, 0), - 'enabled': True, - 'expirationTime': None, - 'lastEditTime': None, - 'lastUsageTime': None, - 'note': None, - 'token': 'dummy', - 'user': { - 'avatarUrl': 'https://example.com/avatar.png', - 'name': user_token.user.name}, - 'version': 1 + "creationTime": datetime(1997, 1, 1, 0, 0), + "enabled": True, + "expirationTime": None, + "lastEditTime": None, + "lastUsageTime": None, + "note": None, + "token": "dummy", + "user": { + "avatarUrl": "https://example.com/avatar.png", + "name": user_token.user.name, + }, + "version": 1, } @@ -41,7 +44,8 @@ def test_get_by_user_and_token(user_token_factory): db.session.flush() db.session.commit() result = user_tokens.get_by_user_and_token( - user_token.user, user_token.token) + user_token.user, user_token.token + ) assert result == user_token @@ -61,10 +65,10 @@ def test_create_user_token(user_factory): db.session.add(user) db.session.flush() db.session.commit() - with patch('szurubooru.func.auth.generate_authorization_token'): - auth.generate_authorization_token.return_value = 'test' + with patch("szurubooru.func.auth.generate_authorization_token"): + auth.generate_authorization_token.return_value = "test" result = user_tokens.create_user_token(user, True) - assert result.token == 'test' + assert result.token == "test" assert result.user == user @@ -85,8 +89,8 @@ def test_update_user_token_edit_time(user_token_factory): def test_update_user_token_note(user_token_factory): user_token = user_token_factory() assert user_token.note is None - user_tokens.update_user_token_note(user_token, ' Test Note ') - assert user_token.note == 'Test Note' + user_tokens.update_user_token_note(user_token, " Test Note ") + assert user_token.note == "Test Note" assert user_token.last_edit_time is not None @@ -94,8 +98,9 @@ def test_update_user_token_note_input_too_long(user_token_factory): user_token = user_token_factory() assert user_token.note is None note_max_length = util.get_column_size(model.UserToken.note) + 1 - note = ''.join( - random.choice(string.ascii_letters) for _ in range(note_max_length)) + note = "".join( + random.choice(string.ascii_letters) for _ in range(note_max_length) + ) with pytest.raises(user_tokens.InvalidNoteError): user_tokens.update_user_token_note(user_token, note) @@ -104,11 +109,11 @@ def test_update_user_token_expiration_time(user_token_factory): user_token = user_token_factory() assert user_token.expiration_time is None expiration_time_str = ( - (datetime.utcnow() + timedelta(days=1)) - .replace(tzinfo=pytz.utc) + (datetime.utcnow() + timedelta(days=1)).replace(tzinfo=pytz.utc) ).isoformat() user_tokens.update_user_token_expiration_time( - user_token, expiration_time_str) + user_token, expiration_time_str + ) assert user_token.expiration_time.isoformat() == expiration_time_str assert user_token.last_edit_time is not None @@ -117,39 +122,45 @@ def test_update_user_token_expiration_time_in_past(user_token_factory): user_token = user_token_factory() assert user_token.expiration_time is None expiration_time_str = ( - (datetime.utcnow() - timedelta(days=1)) - .replace(tzinfo=pytz.utc) + (datetime.utcnow() - timedelta(days=1)).replace(tzinfo=pytz.utc) ).isoformat() with pytest.raises( - user_tokens.InvalidExpirationError, - match='Expiration cannot happen in the past'): + user_tokens.InvalidExpirationError, + match="Expiration cannot happen in the past", + ): user_tokens.update_user_token_expiration_time( - user_token, expiration_time_str) - - -@pytest.mark.parametrize('expiration_time_str', [ - datetime.utcnow().isoformat(), - (datetime.utcnow() - timedelta(days=1)).ctime(), - '1970/01/01 00:00:01.0000Z', - '70/01/01 00:00:01.0000Z', - ''.join(random.choice(string.ascii_letters) for _ in range(15)), - ''.join(random.choice(string.digits) for _ in range(8)) -]) + user_token, expiration_time_str + ) + + +@pytest.mark.parametrize( + "expiration_time_str", + [ + datetime.utcnow().isoformat(), + (datetime.utcnow() - timedelta(days=1)).ctime(), + "1970/01/01 00:00:01.0000Z", + "70/01/01 00:00:01.0000Z", + "".join(random.choice(string.ascii_letters) for _ in range(15)), + "".join(random.choice(string.digits) for _ in range(8)), + ], +) def test_update_user_token_expiration_time_invalid_format( - expiration_time_str, user_token_factory): + expiration_time_str, user_token_factory +): user_token = user_token_factory() assert user_token.expiration_time is None with pytest.raises( - user_tokens.InvalidExpirationError, - match='Expiration is in an invalid format %s' - % expiration_time_str): + user_tokens.InvalidExpirationError, + match="Expiration is in an invalid format %s" % expiration_time_str, + ): user_tokens.update_user_token_expiration_time( - user_token, expiration_time_str) + user_token, expiration_time_str + ) def test_bump_usage_time(user_token_factory, fake_datetime): user_token = user_token_factory() - with fake_datetime('1997-01-01'): + with fake_datetime("1997-01-01"): user_tokens.bump_usage_time(user_token) assert user_token.last_usage_time == datetime(1997, 1, 1) diff --git a/server/szurubooru/tests/func/test_users.py b/server/szurubooru/tests/func/test_users.py index 5506127..94e9c7c 100644 --- a/server/szurubooru/tests/func/test_users.py +++ b/server/szurubooru/tests/func/test_users.py @@ -1,56 +1,70 @@ -from unittest.mock import patch from datetime import datetime +from unittest.mock import patch + import pytest -from szurubooru import db, model, errors -from szurubooru.func import auth, users, files, util +from szurubooru import db, errors, model +from szurubooru.func import auth, files, users, util EMPTY_PIXEL = ( - b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00' - b'\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x01\x00\x2c\x00\x00\x00\x00' - b'\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b') + b"\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00" + b"\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x01\x00\x2c\x00\x00\x00\x00" + b"\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b" +) -@pytest.mark.parametrize('user_name', ['test', 'TEST']) +@pytest.mark.parametrize("user_name", ["test", "TEST"]) def test_get_avatar_path(user_name): - assert users.get_avatar_path(user_name) == 'avatars/test.png' - - -@pytest.mark.parametrize('user_name,user_email,avatar_style,expected_url', [ - ( - 'user', - None, - model.User.AVATAR_GRAVATAR, - ('https://gravatar.com/avatar/' + - 'ee11cbb19052e40b07aac0ca060c23ee?d=retro&s=100'), - ), - ( - None, - 'user@example.com', - model.User.AVATAR_GRAVATAR, - ('https://gravatar.com/avatar/' + - 'b58996c504c5638798eb6b511e6f49af?d=retro&s=100'), - ), - ( - 'user', - 'user@example.com', - model.User.AVATAR_GRAVATAR, - ('https://gravatar.com/avatar/' + - 'b58996c504c5638798eb6b511e6f49af?d=retro&s=100'), - ), - ( - 'user', - None, - model.User.AVATAR_MANUAL, - 'http://example.com/avatars/user.png', - ), -]) + assert users.get_avatar_path(user_name) == "avatars/test.png" + + +@pytest.mark.parametrize( + "user_name,user_email,avatar_style,expected_url", + [ + ( + "user", + None, + model.User.AVATAR_GRAVATAR, + ( + "https://gravatar.com/avatar/" + + "ee11cbb19052e40b07aac0ca060c23ee?d=retro&s=100" + ), + ), + ( + None, + "user@example.com", + model.User.AVATAR_GRAVATAR, + ( + "https://gravatar.com/avatar/" + + "b58996c504c5638798eb6b511e6f49af?d=retro&s=100" + ), + ), + ( + "user", + "user@example.com", + model.User.AVATAR_GRAVATAR, + ( + "https://gravatar.com/avatar/" + + "b58996c504c5638798eb6b511e6f49af?d=retro&s=100" + ), + ), + ( + "user", + None, + model.User.AVATAR_MANUAL, + "http://example.com/avatars/user.png", + ), + ], +) def test_get_avatar_url( - user_name, user_email, avatar_style, expected_url, config_injector): - config_injector({ - 'data_url': 'http://example.com/', - 'thumbnails': {'avatar_width': 100}, - }) + user_name, user_email, avatar_style, expected_url, config_injector +): + config_injector( + { + "data_url": "http://example.com/", + "thumbnails": {"avatar_width": 100}, + } + ) user = model.User() user.name = user_name user.email = user_email @@ -59,23 +73,21 @@ def test_get_avatar_url( @pytest.mark.parametrize( - 'same_user,can_edit_any_email,force_show,expected_email', + "same_user,can_edit_any_email,force_show,expected_email", [ (False, False, False, False), - (True, False, False, 'test@example.com'), - (False, True, False, 'test@example.com'), - (False, False, True, 'test@example.com'), - ]) + (True, False, False, "test@example.com"), + (False, True, False, "test@example.com"), + (False, False, True, "test@example.com"), + ], +) def test_get_email( - same_user, - can_edit_any_email, - force_show, - expected_email, - user_factory): - with patch('szurubooru.func.auth.has_privilege'): + same_user, can_edit_any_email, force_show, expected_email, user_factory +): + with patch("szurubooru.func.auth.has_privilege"): auth.has_privilege = lambda user, name: can_edit_any_email user = user_factory() - user.email = 'test@example.com' + user.email = "test@example.com" auth_user = user if same_user else user_factory() db.session.add_all([user, auth_user]) db.session.flush() @@ -83,25 +95,28 @@ def test_get_email( @pytest.mark.parametrize( - 'same_user,score,expected_liked_post_count,expected_disliked_post_count', + "same_user,score,expected_liked_post_count,expected_disliked_post_count", [ (False, 1, False, False), (False, -1, False, False), (True, 1, 1, 0), (True, -1, 0, 1), - ]) + ], +) def test_get_liked_post_count( - same_user, - score, - expected_liked_post_count, - expected_disliked_post_count, - user_factory, - post_factory): + same_user, + score, + expected_liked_post_count, + expected_disliked_post_count, + user_factory, + post_factory, +): user = user_factory() post = post_factory() auth_user = user if same_user else user_factory() score = model.PostScore( - post=post, user=user, score=score, time=datetime.now()) + post=post, user=user, score=score, time=datetime.now() + ) db.session.add_all([post, user, score]) db.session.flush() actual_liked_post_count = users.get_liked_post_count(user, auth_user) @@ -115,16 +130,17 @@ def test_serialize_user_when_empty(): def test_serialize_user(user_factory): - with patch('szurubooru.func.users.get_email'), \ - patch('szurubooru.func.users.get_avatar_url'), \ - patch('szurubooru.func.users.get_liked_post_count'), \ - patch('szurubooru.func.users.get_disliked_post_count'): - users.get_email.return_value = 'test@example.com' - users.get_avatar_url.return_value = 'https://example.com/avatar.png' + with patch("szurubooru.func.users.get_email"), patch( + "szurubooru.func.users.get_avatar_url" + ), patch("szurubooru.func.users.get_liked_post_count"), patch( + "szurubooru.func.users.get_disliked_post_count" + ): + users.get_email.return_value = "test@example.com" + users.get_avatar_url.return_value = "https://example.com/avatar.png" users.get_liked_post_count.return_value = 66 users.get_disliked_post_count.return_value = 33 auth_user = user_factory() - user = user_factory(name='dummy user') + user = user_factory(name="dummy user") user.creation_time = datetime(1997, 1, 1) user.last_edit_time = datetime(1998, 1, 1) user.avatar_style = model.User.AVATAR_MANUAL @@ -132,36 +148,36 @@ def test_serialize_user(user_factory): db.session.add(user) db.session.flush() assert users.serialize_user(user, auth_user) == { - 'version': 1, - 'name': 'dummy user', - 'email': 'test@example.com', - 'rank': 'administrator', - 'creationTime': datetime(1997, 1, 1, 0, 0), - 'lastLoginTime': None, - 'avatarStyle': 'manual', - 'avatarUrl': 'https://example.com/avatar.png', - 'likedPostCount': 66, - 'dislikedPostCount': 33, - 'commentCount': 0, - 'favoritePostCount': 0, - 'uploadedPostCount': 0, + "version": 1, + "name": "dummy user", + "email": "test@example.com", + "rank": "administrator", + "creationTime": datetime(1997, 1, 1, 0, 0), + "lastLoginTime": None, + "avatarStyle": "manual", + "avatarUrl": "https://example.com/avatar.png", + "likedPostCount": 66, + "dislikedPostCount": 33, + "commentCount": 0, + "favoritePostCount": 0, + "uploadedPostCount": 0, } def test_serialize_micro_user(user_factory): - with patch('szurubooru.func.users.get_avatar_url'): - users.get_avatar_url.return_value = 'https://example.com/avatar.png' + with patch("szurubooru.func.users.get_avatar_url"): + users.get_avatar_url.return_value = "https://example.com/avatar.png" auth_user = user_factory() - user = user_factory(name='dummy user') + user = user_factory(name="dummy user") db.session.add(user) db.session.flush() assert users.serialize_micro_user(user, auth_user) == { - 'name': 'dummy user', - 'avatarUrl': 'https://example.com/avatar.png', + "name": "dummy user", + "avatarUrl": "https://example.com/avatar.png", } -@pytest.mark.parametrize('count', [0, 1, 2]) +@pytest.mark.parametrize("count", [0, 1, 2]) def test_get_user_count(count, user_factory): for _ in range(count): db.session.add(user_factory()) @@ -170,72 +186,73 @@ def test_get_user_count(count, user_factory): def test_try_get_user_by_name(user_factory): - user = user_factory(name='name', email='email') + user = user_factory(name="name", email="email") db.session.add(user) db.session.flush() - assert users.try_get_user_by_name('non-existing') is None - assert users.try_get_user_by_name('email') is None - assert users.try_get_user_by_name('name') is user - assert users.try_get_user_by_name('NAME') is user + assert users.try_get_user_by_name("non-existing") is None + assert users.try_get_user_by_name("email") is None + assert users.try_get_user_by_name("name") is user + assert users.try_get_user_by_name("NAME") is user def test_get_user_by_name(user_factory): - user = user_factory(name='name', email='email') + user = user_factory(name="name", email="email") db.session.add(user) db.session.flush() with pytest.raises(users.UserNotFoundError): - assert users.get_user_by_name('non-existing') + assert users.get_user_by_name("non-existing") with pytest.raises(users.UserNotFoundError): - assert users.get_user_by_name('email') - assert users.get_user_by_name('name') is user - assert users.get_user_by_name('NAME') is user + assert users.get_user_by_name("email") + assert users.get_user_by_name("name") is user + assert users.get_user_by_name("NAME") is user def test_try_get_user_by_name_or_email(user_factory): - user = user_factory(name='name', email='email') + user = user_factory(name="name", email="email") db.session.add(user) db.session.flush() - assert users.try_get_user_by_name_or_email('non-existing') is None - assert users.try_get_user_by_name_or_email('email') is user - assert users.try_get_user_by_name_or_email('EMAIL') is user - assert users.try_get_user_by_name_or_email('name') is user - assert users.try_get_user_by_name_or_email('NAME') is user + assert users.try_get_user_by_name_or_email("non-existing") is None + assert users.try_get_user_by_name_or_email("email") is user + assert users.try_get_user_by_name_or_email("EMAIL") is user + assert users.try_get_user_by_name_or_email("name") is user + assert users.try_get_user_by_name_or_email("NAME") is user def test_get_user_by_name_or_email(user_factory): - user = user_factory(name='name', email='email') + user = user_factory(name="name", email="email") db.session.add(user) db.session.flush() with pytest.raises(users.UserNotFoundError): - assert users.get_user_by_name_or_email('non-existing') - assert users.get_user_by_name_or_email('email') is user - assert users.get_user_by_name_or_email('EMAIL') is user - assert users.get_user_by_name_or_email('name') is user - assert users.get_user_by_name_or_email('NAME') is user + assert users.get_user_by_name_or_email("non-existing") + assert users.get_user_by_name_or_email("email") is user + assert users.get_user_by_name_or_email("EMAIL") is user + assert users.get_user_by_name_or_email("name") is user + assert users.get_user_by_name_or_email("NAME") is user def test_create_user_for_first_user(fake_datetime): - with patch('szurubooru.func.users.update_user_name'), \ - patch('szurubooru.func.users.update_user_password'), \ - patch('szurubooru.func.users.update_user_email'), \ - fake_datetime('1997-01-01'): - user = users.create_user('name', 'password', 'email') + with patch("szurubooru.func.users.update_user_name"), patch( + "szurubooru.func.users.update_user_password" + ), patch("szurubooru.func.users.update_user_email"), fake_datetime( + "1997-01-01" + ): + user = users.create_user("name", "password", "email") assert user.creation_time == datetime(1997, 1, 1) assert user.last_login_time is None assert user.rank == model.User.RANK_ADMINISTRATOR - users.update_user_name.assert_called_once_with(user, 'name') - users.update_user_password.assert_called_once_with(user, 'password') - users.update_user_email.assert_called_once_with(user, 'email') + users.update_user_name.assert_called_once_with(user, "name") + users.update_user_password.assert_called_once_with(user, "password") + users.update_user_email.assert_called_once_with(user, "email") def test_create_user_for_subsequent_users(user_factory, config_injector): - config_injector({'default_rank': 'regular'}) + config_injector({"default_rank": "regular"}) db.session.add(user_factory()) db.session.flush() - with patch('szurubooru.func.users.update_user_name'), \ - patch('szurubooru.func.users.update_user_email'), \ - patch('szurubooru.func.users.update_user_password'): - user = users.create_user('name', 'password', 'email') + with patch("szurubooru.func.users.update_user_name"), patch( + "szurubooru.func.users.update_user_email" + ), patch("szurubooru.func.users.update_user_password"): + user = users.create_user("name", "password", "email") assert user.rank == model.User.RANK_REGULAR @@ -248,56 +265,58 @@ def test_update_user_name_with_empty_string(user_factory): def test_update_user_name_with_too_long_string(user_factory): user = user_factory() with pytest.raises(users.InvalidUserNameError): - users.update_user_name(user, 'a' * 300) + users.update_user_name(user, "a" * 300) def test_update_user_name_with_invalid_name(user_factory, config_injector): - config_injector({'user_name_regex': '^[a-z]+$'}) + config_injector({"user_name_regex": "^[a-z]+$"}) user = user_factory() with pytest.raises(users.InvalidUserNameError): - users.update_user_name(user, '0') + users.update_user_name(user, "0") def test_update_user_name_with_duplicate_name(user_factory, config_injector): - config_injector({'user_name_regex': '^[a-z]+$'}) + config_injector({"user_name_regex": "^[a-z]+$"}) user = user_factory() - existing_user = user_factory(name='dummy') + existing_user = user_factory(name="dummy") db.session.add(existing_user) db.session.flush() with pytest.raises(users.UserAlreadyExistsError): - users.update_user_name(user, 'dummy') + users.update_user_name(user, "dummy") def test_update_user_name_reusing_own_name(user_factory, config_injector): - config_injector({'user_name_regex': '^[a-z]+$'}) - user = user_factory(name='dummy') + config_injector({"user_name_regex": "^[a-z]+$"}) + user = user_factory(name="dummy") db.session.add(user) db.session.flush() - with patch('szurubooru.func.files.has'): + with patch("szurubooru.func.files.has"): files.has.return_value = False - users.update_user_name(user, 'dummy') + users.update_user_name(user, "dummy") db.session.flush() - assert users.try_get_user_by_name('dummy') is user + assert users.try_get_user_by_name("dummy") is user def test_update_user_name_for_new_user(user_factory, config_injector): - config_injector({'user_name_regex': '^[a-z]+$'}) + config_injector({"user_name_regex": "^[a-z]+$"}) user = user_factory() - with patch('szurubooru.func.files.has'): + with patch("szurubooru.func.files.has"): files.has.return_value = False - users.update_user_name(user, 'dummy') - assert user.name == 'dummy' + users.update_user_name(user, "dummy") + assert user.name == "dummy" def test_update_user_name_moves_avatar(user_factory, config_injector): - config_injector({'user_name_regex': '^[a-z]+$'}) - user = user_factory(name='old') - with patch('szurubooru.func.files.has'), \ - patch('szurubooru.func.files.move'): + config_injector({"user_name_regex": "^[a-z]+$"}) + user = user_factory(name="old") + with patch("szurubooru.func.files.has"), patch( + "szurubooru.func.files.move" + ): files.has.return_value = True - users.update_user_name(user, 'new') + users.update_user_name(user, "new") files.move.assert_called_once_with( - 'avatars/old.png', 'avatars/new.png') + "avatars/old.png", "avatars/new.png" + ) def test_update_user_password_with_empty_string(user_factory): @@ -307,72 +326,74 @@ def test_update_user_password_with_empty_string(user_factory): def test_update_user_password_with_invalid_string( - user_factory, config_injector): - config_injector({'password_regex': '^[a-z]+$'}) + user_factory, config_injector +): + config_injector({"password_regex": "^[a-z]+$"}) user = user_factory() with pytest.raises(users.InvalidPasswordError): - users.update_user_password(user, '0') + users.update_user_password(user, "0") def test_update_user_password(user_factory, config_injector): - config_injector({'password_regex': '^[a-z]+$'}) + config_injector({"password_regex": "^[a-z]+$"}) user = user_factory() - with patch('szurubooru.func.auth.create_password'), \ - patch('szurubooru.func.auth.get_password_hash'): - auth.create_password.return_value = 'salt' - auth.get_password_hash.return_value = ('hash', 3) - users.update_user_password(user, 'a') - assert user.password_salt == 'salt' - assert user.password_hash == 'hash' + with patch("szurubooru.func.auth.create_password"), patch( + "szurubooru.func.auth.get_password_hash" + ): + auth.create_password.return_value = "salt" + auth.get_password_hash.return_value = ("hash", 3) + users.update_user_password(user, "a") + assert user.password_salt == "salt" + assert user.password_hash == "hash" assert user.password_revision == 3 def test_update_user_email_with_too_long_string(user_factory): user = user_factory() with pytest.raises(users.InvalidEmailError): - users.update_user_email(user, 'a' * 300) + users.update_user_email(user, "a" * 300) def test_update_user_email_with_invalid_email(user_factory): user = user_factory() - with patch('szurubooru.func.util.is_valid_email'): + with patch("szurubooru.func.util.is_valid_email"): util.is_valid_email.return_value = False with pytest.raises(users.InvalidEmailError): - users.update_user_email(user, 'a') + users.update_user_email(user, "a") def test_update_user_email_with_empty_string(user_factory): user = user_factory() - with patch('szurubooru.func.util.is_valid_email'): + with patch("szurubooru.func.util.is_valid_email"): util.is_valid_email.return_value = True - users.update_user_email(user, '') + users.update_user_email(user, "") assert user.email is None def test_update_user_email(user_factory): user = user_factory() - with patch('szurubooru.func.util.is_valid_email'): + with patch("szurubooru.func.util.is_valid_email"): util.is_valid_email.return_value = True - users.update_user_email(user, 'a') - assert user.email == 'a' + users.update_user_email(user, "a") + assert user.email == "a" def test_update_user_rank_with_empty_string(user_factory): user = user_factory() auth_user = user_factory() with pytest.raises(users.InvalidRankError): - users.update_user_rank(user, '', auth_user) + users.update_user_rank(user, "", auth_user) def test_update_user_rank_with_invalid_string(user_factory): user = user_factory() auth_user = user_factory() with pytest.raises(users.InvalidRankError): - users.update_user_rank(user, 'invalid', auth_user) + users.update_user_rank(user, "invalid", auth_user) with pytest.raises(users.InvalidRankError): - users.update_user_rank(user, 'anonymous', auth_user) + users.update_user_rank(user, "anonymous", auth_user) with pytest.raises(users.InvalidRankError): - users.update_user_rank(user, 'nobody', auth_user) + users.update_user_rank(user, "nobody", auth_user) def test_update_user_rank_with_higher_rank_than_possible(user_factory): @@ -382,9 +403,9 @@ def test_update_user_rank_with_higher_rank_than_possible(user_factory): auth_user = user_factory() auth_user.rank = model.User.RANK_ANONYMOUS with pytest.raises(errors.AuthError): - users.update_user_rank(user, 'regular', auth_user) + users.update_user_rank(user, "regular", auth_user) with pytest.raises(errors.AuthError): - users.update_user_rank(auth_user, 'regular', auth_user) + users.update_user_rank(auth_user, "regular", auth_user) def test_update_user_rank(user_factory): @@ -393,8 +414,8 @@ def test_update_user_rank(user_factory): user = user_factory() auth_user = user_factory() auth_user.rank = model.User.RANK_ADMINISTRATOR - users.update_user_rank(user, 'regular', auth_user) - users.update_user_rank(auth_user, 'regular', auth_user) + users.update_user_rank(user, "regular", auth_user) + users.update_user_rank(auth_user, "regular", auth_user) assert user.rank == model.User.RANK_REGULAR assert auth_user.rank == model.User.RANK_REGULAR @@ -402,54 +423,57 @@ def test_update_user_rank(user_factory): def test_update_user_avatar_with_invalid_style(user_factory): user = user_factory() with pytest.raises(users.InvalidAvatarError): - users.update_user_avatar(user, 'invalid', b'') + users.update_user_avatar(user, "invalid", b"") def test_update_user_avatar_to_gravatar(user_factory): user = user_factory() - users.update_user_avatar(user, 'gravatar') + users.update_user_avatar(user, "gravatar") assert user.avatar_style == model.User.AVATAR_GRAVATAR def test_update_user_avatar_to_empty_manual(user_factory): user = user_factory() - with patch('szurubooru.func.files.has'), \ - pytest.raises(users.InvalidAvatarError): + with patch("szurubooru.func.files.has"), pytest.raises( + users.InvalidAvatarError + ): files.has.return_value = False - users.update_user_avatar(user, 'manual', b'') + users.update_user_avatar(user, "manual", b"") def test_update_user_avatar_to_previous_manual(user_factory): user = user_factory() - with patch('szurubooru.func.files.has'): + with patch("szurubooru.func.files.has"): files.has.return_value = True - users.update_user_avatar(user, 'manual', b'') + users.update_user_avatar(user, "manual", b"") def test_update_user_avatar_to_new_manual(user_factory, config_injector): config_injector( - {'thumbnails': {'avatar_width': 500, 'avatar_height': 500}}) + {"thumbnails": {"avatar_width": 500, "avatar_height": 500}} + ) user = user_factory() - with patch('szurubooru.func.files.save'): - users.update_user_avatar(user, 'manual', EMPTY_PIXEL) + with patch("szurubooru.func.files.save"): + users.update_user_avatar(user, "manual", EMPTY_PIXEL) assert user.avatar_style == model.User.AVATAR_MANUAL assert files.save.called def test_bump_user_login_time(user_factory, fake_datetime): user = user_factory() - with fake_datetime('1997-01-01'): + with fake_datetime("1997-01-01"): users.bump_user_login_time(user) assert user.last_login_time == datetime(1997, 1, 1) def test_reset_user_password(user_factory): - with patch('szurubooru.func.auth.create_password'), \ - patch('szurubooru.func.auth.get_password_hash'): + with patch("szurubooru.func.auth.create_password"), patch( + "szurubooru.func.auth.get_password_hash" + ): user = user_factory() - auth.create_password.return_value = 'salt' - auth.get_password_hash.return_value = ('hash', 3) + auth.create_password.return_value = "salt" + auth.get_password_hash.return_value = ("hash", 3) users.reset_user_password(user) - assert user.password_salt == 'salt' - assert user.password_hash == 'hash' + assert user.password_salt == "salt" + assert user.password_hash == "hash" assert user.password_revision == 3 diff --git a/server/szurubooru/tests/func/test_util.py b/server/szurubooru/tests/func/test_util.py index 1307ab9..f42ba29 100644 --- a/server/szurubooru/tests/func/test_util.py +++ b/server/szurubooru/tests/func/test_util.py @@ -1,40 +1,47 @@ from datetime import datetime + import pytest + from szurubooru import errors from szurubooru.func import util - dt = datetime def test_parsing_empty_date_time(): with pytest.raises(errors.ValidationError): - util.parse_time_range('') - - -@pytest.mark.parametrize('output,input', [ - ((dt(1997, 1, 2, 0, 0, 0), dt(1997, 1, 2, 23, 59, 59)), 'today'), - ((dt(1997, 1, 1, 0, 0, 0), dt(1997, 1, 1, 23, 59, 59)), 'yesterday'), - ((dt(1999, 1, 1, 0, 0, 0), dt(1999, 12, 31, 23, 59, 59)), '1999'), - ((dt(1999, 2, 1, 0, 0, 0), dt(1999, 2, 28, 23, 59, 59)), '1999-2'), - ((dt(1999, 2, 1, 0, 0, 0), dt(1999, 2, 28, 23, 59, 59)), '1999-02'), - ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), '1999-2-6'), - ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), '1999-02-6'), - ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), '1999-2-06'), - ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), '1999-02-06'), -]) + util.parse_time_range("") + + +@pytest.mark.parametrize( + "output,input", + [ + ((dt(1997, 1, 2, 0, 0, 0), dt(1997, 1, 2, 23, 59, 59)), "today"), + ((dt(1997, 1, 1, 0, 0, 0), dt(1997, 1, 1, 23, 59, 59)), "yesterday"), + ((dt(1999, 1, 1, 0, 0, 0), dt(1999, 12, 31, 23, 59, 59)), "1999"), + ((dt(1999, 2, 1, 0, 0, 0), dt(1999, 2, 28, 23, 59, 59)), "1999-2"), + ((dt(1999, 2, 1, 0, 0, 0), dt(1999, 2, 28, 23, 59, 59)), "1999-02"), + ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), "1999-2-6"), + ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), "1999-02-6"), + ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), "1999-2-06"), + ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), "1999-02-06"), + ], +) def test_parsing_date_time(fake_datetime, input, output): - with fake_datetime('1997-01-02 03:04:05'): + with fake_datetime("1997-01-02 03:04:05"): assert util.parse_time_range(input) == output -@pytest.mark.parametrize('input,output', [ - ([], []), - (['a', 'b', 'c'], ['a', 'b', 'c']), - (['a', 'b', 'a'], ['a', 'b']), - (['a', 'a', 'b'], ['a', 'b']), - (['a', 'A', 'b'], ['a', 'b']), - (['a', 'A', 'b', 'B'], ['a', 'b']), -]) +@pytest.mark.parametrize( + "input,output", + [ + ([], []), + (["a", "b", "c"], ["a", "b", "c"]), + (["a", "b", "a"], ["a", "b"]), + (["a", "a", "b"], ["a", "b"]), + (["a", "A", "b"], ["a", "b"]), + (["a", "A", "b", "B"], ["a", "b"]), + ], +) def test_icase_unique(input, output): assert util.icase_unique(input) == output diff --git a/server/szurubooru/tests/middleware/test_authenticator.py b/server/szurubooru/tests/middleware/test_authenticator.py index be21a93..9a4a3cc 100644 --- a/server/szurubooru/tests/middleware/test_authenticator.py +++ b/server/szurubooru/tests/middleware/test_authenticator.py @@ -1,7 +1,9 @@ from unittest.mock import patch + import pytest + from szurubooru import db -from szurubooru.func import auth, users, user_tokens +from szurubooru.func import auth, user_tokens, users from szurubooru.middleware import authenticator from szurubooru.rest import errors @@ -17,14 +19,12 @@ def test_process_request_bump_login(context_factory, user_factory): db.session.add(user) db.session.flush() ctx = context_factory( - headers={ - 'Authorization': 'Basic dGVzdFVzZXI6dGVzdFRva2Vu' - }, - params={ - 'bump-login': 'true' - }) - with patch('szurubooru.func.auth.is_valid_password'), \ - patch('szurubooru.func.users.get_user_by_name'): + headers={"Authorization": "Basic dGVzdFVzZXI6dGVzdFRva2Vu"}, + params={"bump-login": "true"}, + ) + with patch("szurubooru.func.auth.is_valid_password"), patch( + "szurubooru.func.users.get_user_by_name" + ): users.get_user_by_name.return_value = user auth.is_valid_password.return_value = True authenticator.process_request(ctx) @@ -32,20 +32,18 @@ def test_process_request_bump_login(context_factory, user_factory): def test_process_request_bump_login_with_token( - context_factory, user_token_factory): + context_factory, user_token_factory +): user_token = user_token_factory() db.session.add(user_token) db.session.flush() ctx = context_factory( - headers={ - 'Authorization': 'Token dGVzdFVzZXI6dGVzdFRva2Vu' - }, - params={ - 'bump-login': 'true' - }) - with patch('szurubooru.func.auth.is_valid_token'), \ - patch('szurubooru.func.users.get_user_by_name'), \ - patch('szurubooru.func.user_tokens.get_by_user_and_token'): + headers={"Authorization": "Token dGVzdFVzZXI6dGVzdFRva2Vu"}, + params={"bump-login": "true"}, + ) + with patch("szurubooru.func.auth.is_valid_token"), patch( + "szurubooru.func.users.get_user_by_name" + ), patch("szurubooru.func.user_tokens.get_by_user_and_token"): users.get_user_by_name.return_value = user_token.user user_tokens.get_by_user_and_token.return_value = user_token auth.is_valid_token.return_value = True @@ -57,11 +55,11 @@ def test_process_request_bump_login_with_token( def test_process_request_basic_auth_valid(context_factory, user_factory): user = user_factory() ctx = context_factory( - headers={ - 'Authorization': 'Basic dGVzdFVzZXI6dGVzdFBhc3N3b3Jk' - }) - with patch('szurubooru.func.auth.is_valid_password'), \ - patch('szurubooru.func.users.get_user_by_name'): + headers={"Authorization": "Basic dGVzdFVzZXI6dGVzdFBhc3N3b3Jk"} + ) + with patch("szurubooru.func.auth.is_valid_password"), patch( + "szurubooru.func.users.get_user_by_name" + ): users.get_user_by_name.return_value = user auth.is_valid_password.return_value = True authenticator.process_request(ctx) @@ -71,12 +69,11 @@ def test_process_request_basic_auth_valid(context_factory, user_factory): def test_process_request_token_auth_valid(context_factory, user_token_factory): user_token = user_token_factory() ctx = context_factory( - headers={ - 'Authorization': 'Token dGVzdFVzZXI6dGVzdFRva2Vu' - }) - with patch('szurubooru.func.auth.is_valid_token'), \ - patch('szurubooru.func.users.get_user_by_name'), \ - patch('szurubooru.func.user_tokens.get_by_user_and_token'): + headers={"Authorization": "Token dGVzdFVzZXI6dGVzdFRva2Vu"} + ) + with patch("szurubooru.func.auth.is_valid_token"), patch( + "szurubooru.func.users.get_user_by_name" + ), patch("szurubooru.func.user_tokens.get_by_user_and_token"): users.get_user_by_name.return_value = user_token.user user_tokens.get_by_user_and_token.return_value = user_token auth.is_valid_token.return_value = True @@ -85,9 +82,6 @@ def test_process_request_token_auth_valid(context_factory, user_token_factory): def test_process_request_bad_header(context_factory): - ctx = context_factory( - headers={ - 'Authorization': 'Secret SuperSecretValue' - }) + ctx = context_factory(headers={"Authorization": "Secret SuperSecretValue"}) with pytest.raises(errors.HttpBadRequest): authenticator.process_request(ctx) diff --git a/server/szurubooru/tests/model/test_comment.py b/server/szurubooru/tests/model/test_comment.py index ffd5189..fcbf176 100644 --- a/server/szurubooru/tests/model/test_comment.py +++ b/server/szurubooru/tests/model/test_comment.py @@ -1,4 +1,5 @@ from datetime import datetime + from szurubooru import db, model @@ -6,7 +7,7 @@ def test_saving_comment(user_factory, post_factory): user = user_factory() post = post_factory() comment = model.Comment() - comment.text = 'long text' * 1000 + comment.text = "long text" * 1000 comment.user = user comment.post = post comment.creation_time = datetime(1997, 1, 1) @@ -17,7 +18,7 @@ def test_saving_comment(user_factory, post_factory): db.session.refresh(comment) assert not db.session.dirty assert comment.user is not None and comment.user.user_id is not None - assert comment.text == 'long text' * 1000 + assert comment.text == "long text" * 1000 assert comment.creation_time == datetime(1997, 1, 1) assert comment.last_edit_time == datetime(1998, 1, 1) diff --git a/server/szurubooru/tests/model/test_pool.py b/server/szurubooru/tests/model/test_pool.py index 589e9ba..bec9560 100644 --- a/server/szurubooru/tests/model/test_pool.py +++ b/server/szurubooru/tests/model/test_pool.py @@ -1,24 +1,24 @@ from datetime import datetime + import pytest + from szurubooru import db, model @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'delete_source_files': False, - 'secret': 'secret', - 'data_dir': '' - }) + config_injector( + {"delete_source_files": False, "secret": "secret", "data_dir": ""} + ) def test_saving_pool(pool_factory, post_factory): post1 = post_factory() post2 = post_factory() pool = model.Pool() - pool.names = [model.PoolName('alias1', 0), model.PoolName('alias2', 1)] + pool.names = [model.PoolName("alias1", 0), model.PoolName("alias2", 1)] pool.posts = [] - pool.category = model.PoolCategory('category') + pool.category = model.PoolCategory("category") pool.creation_time = datetime(1997, 1, 1) pool.last_edit_time = datetime(1998, 1, 1) db.session.add_all([pool, post1, post2]) @@ -30,13 +30,13 @@ def test_saving_pool(pool_factory, post_factory): db.session.commit() pool = ( - db.session - .query(model.Pool) + db.session.query(model.Pool) .join(model.PoolName) - .filter(model.PoolName.name == 'alias1') - .one()) - assert [pool_name.name for pool_name in pool.names] == ['alias1', 'alias2'] - assert pool.category.name == 'category' + .filter(model.PoolName.name == "alias1") + .one() + ) + assert [pool_name.name for pool_name in pool.names] == ["alias1", "alias2"] + assert pool.category.name == "category" assert pool.creation_time == datetime(1997, 1, 1) assert pool.last_edit_time == datetime(1998, 1, 1) assert [post.post_id for post in pool.posts] == [1, 2] @@ -46,9 +46,9 @@ def test_cascade_deletions(pool_factory, post_factory): post1 = post_factory() post2 = post_factory() pool = model.Pool() - pool.names = [model.PoolName('alias1', 0), model.PoolName('alias2', 1)] + pool.names = [model.PoolName("alias1", 0), model.PoolName("alias2", 1)] pool.posts = [] - pool.category = model.PoolCategory('category') + pool.category = model.PoolCategory("category") pool.creation_time = datetime(1997, 1, 1) pool.last_edit_time = datetime(1998, 1, 1) db.session.add_all([pool, post1, post2]) diff --git a/server/szurubooru/tests/model/test_post.py b/server/szurubooru/tests/model/test_post.py index 47c088d..f01455d 100644 --- a/server/szurubooru/tests/model/test_post.py +++ b/server/szurubooru/tests/model/test_post.py @@ -1,15 +1,15 @@ from datetime import datetime + import pytest + from szurubooru import db, model @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'secret': 'secret', - 'data_dir': '', - 'delete_source_files': False - }) + config_injector( + {"secret": "secret", "data_dir": "", "delete_source_files": False} + ) def test_saving_post(post_factory, user_factory, tag_factory): @@ -19,12 +19,12 @@ def test_saving_post(post_factory, user_factory, tag_factory): related_post1 = post_factory() related_post2 = post_factory() post = model.Post() - post.safety = 'safety' - post.type = 'type' - post.checksum = 'deadbeef' + post.safety = "safety" + post.type = "type" + post.checksum = "deadbeef" post.creation_time = datetime(1997, 1, 1) post.last_edit_time = datetime(1998, 1, 1) - post.mime_type = 'application/whatever' + post.mime_type = "application/whatever" db.session.add_all([user, tag1, tag2, related_post1, related_post2, post]) post.user = user @@ -39,9 +39,9 @@ def test_saving_post(post_factory, user_factory, tag_factory): db.session.refresh(related_post2) assert not db.session.dirty assert post.user.user_id is not None - assert post.safety == 'safety' - assert post.type == 'type' - assert post.checksum == 'deadbeef' + assert post.safety == "safety" + assert post.type == "type" + assert post.checksum == "deadbeef" assert post.creation_time == datetime(1997, 1, 1) assert post.last_edit_time == datetime(1998, 1, 1) assert len(post.relations) == 2 @@ -51,7 +51,8 @@ def test_saving_post(post_factory, user_factory, tag_factory): def test_cascade_deletions( - post_factory, user_factory, tag_factory, comment_factory): + post_factory, user_factory, tag_factory, comment_factory +): user = user_factory() tag1 = tag_factory() tag2 = tag_factory() @@ -59,8 +60,9 @@ def test_cascade_deletions( related_post2 = post_factory() post = post_factory() comment = comment_factory(post=post, user=user) - db.session.add_all([ - user, tag1, tag2, post, related_post1, related_post2, comment]) + db.session.add_all( + [user, tag1, tag2, post, related_post1, related_post2, comment] + ) db.session.flush() score = model.PostScore() @@ -78,11 +80,11 @@ def test_cascade_deletions( feature.time = datetime(1997, 1, 1) note = model.PostNote() note.post = post - note.polygon = '' - note.text = '' + note.polygon = "" + note.text = "" signature = model.PostSignature() signature.post = post - signature.signature = b'testvalue' + signature.signature = b"testvalue" signature.words = list(range(50)) db.session.add_all([score, favorite, feature, note, signature]) db.session.flush() diff --git a/server/szurubooru/tests/model/test_tag.py b/server/szurubooru/tests/model/test_tag.py index b677eef..9332f0d 100644 --- a/server/szurubooru/tests/model/test_tag.py +++ b/server/szurubooru/tests/model/test_tag.py @@ -1,27 +1,27 @@ from datetime import datetime + import pytest + from szurubooru import db, model @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'delete_source_files': False, - 'secret': 'secret', - 'data_dir': '' - }) + config_injector( + {"delete_source_files": False, "secret": "secret", "data_dir": ""} + ) def test_saving_tag(tag_factory): - sug1 = tag_factory(names=['sug1']) - sug2 = tag_factory(names=['sug2']) - imp1 = tag_factory(names=['imp1']) - imp2 = tag_factory(names=['imp2']) + sug1 = tag_factory(names=["sug1"]) + sug2 = tag_factory(names=["sug2"]) + imp1 = tag_factory(names=["imp1"]) + imp2 = tag_factory(names=["imp2"]) tag = model.Tag() - tag.names = [model.TagName('alias1', 0), model.TagName('alias2', 1)] + tag.names = [model.TagName("alias1", 0), model.TagName("alias2", 1)] tag.suggestions = [] tag.implications = [] - tag.category = model.TagCategory('category') + tag.category = model.TagCategory("category") tag.creation_time = datetime(1997, 1, 1) tag.last_edit_time = datetime(1998, 1, 1) db.session.add_all([tag, sug1, sug2, imp1, imp2]) @@ -39,31 +39,35 @@ def test_saving_tag(tag_factory): db.session.commit() tag = ( - db.session - .query(model.Tag) + db.session.query(model.Tag) .join(model.TagName) - .filter(model.TagName.name == 'alias1') - .one()) - assert [tag_name.name for tag_name in tag.names] == ['alias1', 'alias2'] - assert tag.category.name == 'category' + .filter(model.TagName.name == "alias1") + .one() + ) + assert [tag_name.name for tag_name in tag.names] == ["alias1", "alias2"] + assert tag.category.name == "category" assert tag.creation_time == datetime(1997, 1, 1) assert tag.last_edit_time == datetime(1998, 1, 1) - assert [relation.names[0].name for relation in tag.suggestions] \ - == ['sug1', 'sug2'] - assert [relation.names[0].name for relation in tag.implications] \ - == ['imp1', 'imp2'] + assert [relation.names[0].name for relation in tag.suggestions] == [ + "sug1", + "sug2", + ] + assert [relation.names[0].name for relation in tag.implications] == [ + "imp1", + "imp2", + ] def test_cascade_deletions(tag_factory): - sug1 = tag_factory(names=['sug1']) - sug2 = tag_factory(names=['sug2']) - imp1 = tag_factory(names=['imp1']) - imp2 = tag_factory(names=['imp2']) + sug1 = tag_factory(names=["sug1"]) + sug2 = tag_factory(names=["sug2"]) + imp1 = tag_factory(names=["imp1"]) + imp2 = tag_factory(names=["imp2"]) tag = model.Tag() - tag.names = [model.TagName('alias1', 0), model.TagName('alias2', 1)] + tag.names = [model.TagName("alias1", 0), model.TagName("alias2", 1)] tag.suggestions = [] tag.implications = [] - tag.category = model.TagCategory('category') + tag.category = model.TagCategory("category") tag.creation_time = datetime(1997, 1, 1) tag.last_edit_time = datetime(1998, 1, 1) tag.post_count = 1 diff --git a/server/szurubooru/tests/model/test_user.py b/server/szurubooru/tests/model/test_user.py index ced3a5e..20279a0 100644 --- a/server/szurubooru/tests/model/test_user.py +++ b/server/szurubooru/tests/model/test_user.py @@ -1,25 +1,26 @@ from datetime import datetime + from szurubooru import db, model def test_saving_user(): user = model.User() - user.name = 'name' - user.password_salt = 'salt' - user.password_hash = 'hash' - user.email = 'email' - user.rank = 'rank' + user.name = "name" + user.password_salt = "salt" + user.password_hash = "hash" + user.email = "email" + user.rank = "rank" user.creation_time = datetime(1997, 1, 1) user.avatar_style = model.User.AVATAR_GRAVATAR db.session.add(user) db.session.flush() db.session.refresh(user) assert not db.session.dirty - assert user.name == 'name' - assert user.password_salt == 'salt' - assert user.password_hash == 'hash' - assert user.email == 'email' - assert user.rank == 'rank' + assert user.name == "name" + assert user.password_salt == "salt" + assert user.password_hash == "hash" + assert user.email == "email" + assert user.rank == "rank" assert user.creation_time == datetime(1997, 1, 1) assert user.avatar_style == model.User.AVATAR_GRAVATAR @@ -43,10 +44,9 @@ def test_comment_count(user_factory, comment_factory): db.session.add(user) db.session.flush() assert user.comment_count == 0 - db.session.add_all([ - comment_factory(user=user), - comment_factory(), - ]) + db.session.add_all( + [comment_factory(user=user), comment_factory(),] + ) db.session.flush() db.session.refresh(user) assert user.comment_count == 1 @@ -60,10 +60,12 @@ def test_favorite_count(user_factory, post_factory): assert user1.comment_count == 0 post1 = post_factory() post2 = post_factory() - db.session.add_all([ - model.PostFavorite(post=post1, time=datetime.utcnow(), user=user1), - model.PostFavorite(post=post2, time=datetime.utcnow(), user=user2), - ]) + db.session.add_all( + [ + model.PostFavorite(post=post1, time=datetime.utcnow(), user=user1), + model.PostFavorite(post=post2, time=datetime.utcnow(), user=user2), + ] + ) db.session.flush() db.session.refresh(user1) assert user1.favorite_post_count == 1 @@ -78,12 +80,16 @@ def test_liked_post_count(user_factory, post_factory): assert user1.disliked_post_count == 0 post1 = post_factory() post2 = post_factory() - db.session.add_all([ - model.PostScore( - post=post1, time=datetime.utcnow(), user=user1, score=1), - model.PostScore( - post=post2, time=datetime.utcnow(), user=user2, score=1), - ]) + db.session.add_all( + [ + model.PostScore( + post=post1, time=datetime.utcnow(), user=user1, score=1 + ), + model.PostScore( + post=post2, time=datetime.utcnow(), user=user2, score=1 + ), + ] + ) db.session.flush() db.session.refresh(user1) assert user1.liked_post_count == 1 @@ -99,12 +105,16 @@ def test_disliked_post_count(user_factory, post_factory): assert user1.disliked_post_count == 0 post1 = post_factory() post2 = post_factory() - db.session.add_all([ - model.PostScore( - post=post1, time=datetime.utcnow(), user=user1, score=-1), - model.PostScore( - post=post2, time=datetime.utcnow(), user=user2, score=1), - ]) + db.session.add_all( + [ + model.PostScore( + post=post1, time=datetime.utcnow(), user=user1, score=-1 + ), + model.PostScore( + post=post2, time=datetime.utcnow(), user=user2, score=1 + ), + ] + ) db.session.flush() db.session.refresh(user1) assert user1.liked_post_count == 0 @@ -147,10 +157,10 @@ def test_cascade_deletions(post_factory, user_factory, comment_factory): snapshot = model.Snapshot() snapshot.user = user snapshot.creation_time = datetime(1997, 1, 1) - snapshot.resource_type = '-' + snapshot.resource_type = "-" snapshot.resource_pkey = 1 - snapshot.resource_name = '-' - snapshot.operation = '-' + snapshot.resource_name = "-" + snapshot.operation = "-" db.session.add_all([user, post, comment, snapshot]) db.session.commit() diff --git a/server/szurubooru/tests/model/test_user_token.py b/server/szurubooru/tests/model/test_user_token.py index 0280082..cddb5cf 100644 --- a/server/szurubooru/tests/model/test_user_token.py +++ b/server/szurubooru/tests/model/test_user_token.py @@ -1,4 +1,5 @@ from datetime import datetime + from szurubooru import db @@ -9,6 +10,6 @@ def test_saving_user_token(user_token_factory): db.session.refresh(user_token) assert not db.session.dirty assert user_token.user is not None - assert user_token.token == 'dummy' + assert user_token.token == "dummy" assert user_token.enabled is True assert user_token.creation_time == datetime(1997, 1, 1) diff --git a/server/szurubooru/tests/rest/test_context.py b/server/szurubooru/tests/rest/test_context.py index 0de1e32..ec652b1 100644 --- a/server/szurubooru/tests/rest/test_context.py +++ b/server/szurubooru/tests/rest/test_context.py @@ -1,33 +1,38 @@ import unittest.mock + import pytest -from szurubooru import rest, errors + +from szurubooru import errors, rest from szurubooru.func import net def test_has_param(): - ctx = rest.Context(env={}, method=None, url=None, params={'key': 'value'}) - assert ctx.has_param('key') - assert not ctx.has_param('non-existing') + ctx = rest.Context(env={}, method=None, url=None, params={"key": "value"}) + assert ctx.has_param("key") + assert not ctx.has_param("non-existing") def test_get_file(): ctx = rest.Context( - env={}, method=None, url=None, files={'key': b'content'}) - assert ctx.get_file('key') == b'content' + env={}, method=None, url=None, files={"key": b"content"} + ) + assert ctx.get_file("key") == b"content" with pytest.raises(errors.ValidationError): - ctx.get_file('non-existing') + ctx.get_file("non-existing") def test_get_file_from_url(): - with unittest.mock.patch('szurubooru.func.net.download'): - net.download.return_value = b'content' + with unittest.mock.patch("szurubooru.func.net.download"): + net.download.return_value = b"content" ctx = rest.Context( - env={}, method=None, url=None, params={'keyUrl': 'example.com'}) - assert ctx.get_file('key') == b'content' + env={}, method=None, url=None, params={"keyUrl": "example.com"} + ) + assert ctx.get_file("key") == b"content" net.download.assert_called_once_with( - 'example.com', use_video_downloader=False) + "example.com", use_video_downloader=False + ) with pytest.raises(errors.ValidationError): - assert ctx.get_file('non-existing') + assert ctx.get_file("non-existing") def test_getting_list_parameter(): @@ -35,12 +40,13 @@ def test_getting_list_parameter(): env={}, method=None, url=None, - params={'key': 'value', 'list': ['1', '2', '3']}) - assert ctx.get_param_as_list('key') == ['value'] - assert ctx.get_param_as_list('list') == ['1', '2', '3'] + params={"key": "value", "list": ["1", "2", "3"]}, + ) + assert ctx.get_param_as_list("key") == ["value"] + assert ctx.get_param_as_list("list") == ["1", "2", "3"] with pytest.raises(errors.ValidationError): - ctx.get_param_as_list('non-existing') - assert ctx.get_param_as_list('non-existing', default=['def']) == ['def'] + ctx.get_param_as_list("non-existing") + assert ctx.get_param_as_list("non-existing", default=["def"]) == ["def"] def test_getting_string_parameter(): @@ -48,12 +54,13 @@ def test_getting_string_parameter(): env={}, method=None, url=None, - params={'key': 'value', 'list': ['1', '2', '3']}) - assert ctx.get_param_as_string('key') == 'value' - assert ctx.get_param_as_string('list') == '1,2,3' + params={"key": "value", "list": ["1", "2", "3"]}, + ) + assert ctx.get_param_as_string("key") == "value" + assert ctx.get_param_as_string("list") == "1,2,3" with pytest.raises(errors.ValidationError): - ctx.get_param_as_string('non-existing') - assert ctx.get_param_as_string('non-existing', default='x') == 'x' + ctx.get_param_as_string("non-existing") + assert ctx.get_param_as_string("non-existing", default="x") == "x" def test_getting_int_parameter(): @@ -61,55 +68,57 @@ def test_getting_int_parameter(): env={}, method=None, url=None, - params={'key': '50', 'err': 'invalid', 'list': [1, 2, 3]}) - assert ctx.get_param_as_int('key') == 50 + params={"key": "50", "err": "invalid", "list": [1, 2, 3]}, + ) + assert ctx.get_param_as_int("key") == 50 with pytest.raises(errors.ValidationError): - ctx.get_param_as_int('list') + ctx.get_param_as_int("list") with pytest.raises(errors.ValidationError): - ctx.get_param_as_int('non-existing') - assert ctx.get_param_as_int('non-existing', default=5) == 5 + ctx.get_param_as_int("non-existing") + assert ctx.get_param_as_int("non-existing", default=5) == 5 with pytest.raises(errors.ValidationError): - ctx.get_param_as_int('err') + ctx.get_param_as_int("err") with pytest.raises(errors.ValidationError): - assert ctx.get_param_as_int('key', min=50) == 50 - ctx.get_param_as_int('key', min=51) + assert ctx.get_param_as_int("key", min=50) == 50 + ctx.get_param_as_int("key", min=51) with pytest.raises(errors.ValidationError): - assert ctx.get_param_as_int('key', max=50) == 50 - ctx.get_param_as_int('key', max=49) + assert ctx.get_param_as_int("key", max=50) == 50 + ctx.get_param_as_int("key", max=49) def test_getting_bool_parameter(): def test(value): ctx = rest.Context( - env={}, method=None, url=None, params={'key': value}) - return ctx.get_param_as_bool('key') - - assert test('1') is True - assert test('y') is True - assert test('yes') is True - assert test('yep') is True - assert test('yup') is True - assert test('yeah') is True - assert test('t') is True - assert test('true') is True - assert test('TRUE') is True - - assert test('0') is False - assert test('n') is False - assert test('no') is False - assert test('nope') is False - assert test('f') is False - assert test('false') is False - assert test('FALSE') is False + env={}, method=None, url=None, params={"key": value} + ) + return ctx.get_param_as_bool("key") + + assert test("1") is True + assert test("y") is True + assert test("yes") is True + assert test("yep") is True + assert test("yup") is True + assert test("yeah") is True + assert test("t") is True + assert test("true") is True + assert test("TRUE") is True + + assert test("0") is False + assert test("n") is False + assert test("no") is False + assert test("nope") is False + assert test("f") is False + assert test("false") is False + assert test("FALSE") is False with pytest.raises(errors.ValidationError): - test('herp') + test("herp") with pytest.raises(errors.ValidationError): - test('2') + test("2") with pytest.raises(errors.ValidationError): - test(['1', '2']) + test(["1", "2"]) ctx = rest.Context(env={}, method=None, url=None) with pytest.raises(errors.ValidationError): - ctx.get_param_as_bool('non-existing') - assert ctx.get_param_as_bool('non-existing', default=True) is True + ctx.get_param_as_bool("non-existing") + assert ctx.get_param_as_bool("non-existing", default=True) is True diff --git a/server/szurubooru/tests/search/configs/test_comment_search_config.py b/server/szurubooru/tests/search/configs/test_comment_search_config.py index 7279c1b..a592b30 100644 --- a/server/szurubooru/tests/search/configs/test_comment_search_config.py +++ b/server/szurubooru/tests/search/configs/test_comment_search_config.py @@ -1,5 +1,7 @@ from datetime import datetime + import pytest + from szurubooru import db, search @@ -12,22 +14,28 @@ def executor(): def verify_unpaged(executor): def verify(input, expected_comment_text): actual_count, actual_comments = executor.execute( - input, offset=0, limit=100) + input, offset=0, limit=100 + ) actual_comment_text = [c.text for c in actual_comments] assert actual_count == len(expected_comment_text) assert actual_comment_text == expected_comment_text + return verify -@pytest.mark.parametrize('input,expected_comment_text', [ - ('creation-time:2014', ['t2', 't1']), - ('creation-date:2014', ['t2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("creation-time:2014", ["t2", "t1"]), + ("creation-date:2014", ["t2", "t1"]), + ], +) def test_filter_by_creation_time( - verify_unpaged, comment_factory, input, expected_comment_text): - comment1 = comment_factory(text='t1') - comment2 = comment_factory(text='t2') - comment3 = comment_factory(text='t3') + verify_unpaged, comment_factory, input, expected_comment_text +): + comment1 = comment_factory(text="t1") + comment2 = comment_factory(text="t2") + comment3 = comment_factory(text="t3") comment1.creation_time = datetime(2014, 1, 1) comment2.creation_time = datetime(2014, 6, 1) comment3.creation_time = datetime(2015, 1, 1) @@ -36,109 +44,107 @@ def test_filter_by_creation_time( verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('text:t1', ['t1']), - ('text:t2', ['t2']), - ('text:t1,t2', ['t1', 't2']), - ('text:t*', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("text:t1", ["t1"]), + ("text:t2", ["t2"]), + ("text:t1,t2", ["t1", "t2"]), + ("text:t*", ["t1", "t2"]), + ], +) def test_filter_by_text( - verify_unpaged, comment_factory, input, expected_comment_text): - comment1 = comment_factory(text='t1') - comment2 = comment_factory(text='t2') + verify_unpaged, comment_factory, input, expected_comment_text +): + comment1 = comment_factory(text="t1") + comment2 = comment_factory(text="t2") db.session.add_all([comment1, comment2]) db.session.flush() verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('user:u1', ['t1']), - ('user:u2', ['t2']), - ('user:u1,u2', ['t2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [("user:u1", ["t1"]), ("user:u2", ["t2"]), ("user:u1,u2", ["t2", "t1"]),], +) def test_filter_by_user( - verify_unpaged, - comment_factory, - user_factory, - input, - expected_comment_text): - db.session.add(comment_factory(text='t2', user=user_factory(name='u2'))) - db.session.add(comment_factory(text='t1', user=user_factory(name='u1'))) + verify_unpaged, comment_factory, user_factory, input, expected_comment_text +): + db.session.add(comment_factory(text="t2", user=user_factory(name="u2"))) + db.session.add(comment_factory(text="t1", user=user_factory(name="u1"))) db.session.flush() verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('post:1', ['t1']), - ('post:2', ['t2']), - ('post:1,2', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [("post:1", ["t1"]), ("post:2", ["t2"]), ("post:1,2", ["t1", "t2"]),], +) def test_filter_by_post( - verify_unpaged, - comment_factory, - post_factory, - input, - expected_comment_text): - db.session.add(comment_factory(text='t1', post=post_factory(id=1))) - db.session.add(comment_factory(text='t2', post=post_factory(id=2))) + verify_unpaged, comment_factory, post_factory, input, expected_comment_text +): + db.session.add(comment_factory(text="t1", post=post_factory(id=1))) + db.session.add(comment_factory(text="t2", post=post_factory(id=2))) db.session.flush() verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('', ['t1', 't2']), - ('t1', ['t1']), - ('t2', ['t2']), - ('t1,t2', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("", ["t1", "t2"]), + ("t1", ["t1"]), + ("t2", ["t2"]), + ("t1,t2", ["t1", "t2"]), + ], +) def test_anonymous( - verify_unpaged, comment_factory, input, expected_comment_text): - db.session.add(comment_factory(text='t1')) - db.session.add(comment_factory(text='t2')) + verify_unpaged, comment_factory, input, expected_comment_text +): + db.session.add(comment_factory(text="t1")) + db.session.add(comment_factory(text="t2")) db.session.flush() verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('sort:user', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", [("sort:user", ["t1", "t2"]),] +) def test_sort_by_user( - verify_unpaged, - comment_factory, - user_factory, - input, - expected_comment_text): - db.session.add(comment_factory(text='t2', user=user_factory(name='u2'))) - db.session.add(comment_factory(text='t1', user=user_factory(name='u1'))) + verify_unpaged, comment_factory, user_factory, input, expected_comment_text +): + db.session.add(comment_factory(text="t2", user=user_factory(name="u2"))) + db.session.add(comment_factory(text="t1", user=user_factory(name="u1"))) db.session.flush() verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('sort:post', ['t2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", [("sort:post", ["t2", "t1"]),] +) def test_sort_by_post( - verify_unpaged, - comment_factory, - post_factory, - input, - expected_comment_text): - db.session.add(comment_factory(text='t1', post=post_factory(id=1))) - db.session.add(comment_factory(text='t2', post=post_factory(id=2))) + verify_unpaged, comment_factory, post_factory, input, expected_comment_text +): + db.session.add(comment_factory(text="t1", post=post_factory(id=1))) + db.session.add(comment_factory(text="t2", post=post_factory(id=2))) db.session.flush() verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('', ['t3', 't2', 't1']), - ('sort:creation-date', ['t3', 't2', 't1']), - ('sort:creation-time', ['t3', 't2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("", ["t3", "t2", "t1"]), + ("sort:creation-date", ["t3", "t2", "t1"]), + ("sort:creation-time", ["t3", "t2", "t1"]), + ], +) def test_sort_by_creation_time( - verify_unpaged, comment_factory, input, expected_comment_text): - comment1 = comment_factory(text='t1') - comment2 = comment_factory(text='t2') - comment3 = comment_factory(text='t3') + verify_unpaged, comment_factory, input, expected_comment_text +): + comment1 = comment_factory(text="t1") + comment2 = comment_factory(text="t2") + comment3 = comment_factory(text="t3") comment1.creation_time = datetime(1991, 1, 1) comment2.creation_time = datetime(1991, 1, 2) comment3.creation_time = datetime(1991, 1, 3) @@ -147,17 +153,21 @@ def test_sort_by_creation_time( verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('sort:last-edit-date', ['t3', 't2', 't1']), - ('sort:last-edit-time', ['t3', 't2', 't1']), - ('sort:edit-date', ['t3', 't2', 't1']), - ('sort:edit-time', ['t3', 't2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("sort:last-edit-date", ["t3", "t2", "t1"]), + ("sort:last-edit-time", ["t3", "t2", "t1"]), + ("sort:edit-date", ["t3", "t2", "t1"]), + ("sort:edit-time", ["t3", "t2", "t1"]), + ], +) def test_sort_by_last_edit_time( - verify_unpaged, comment_factory, input, expected_comment_text): - comment1 = comment_factory(text='t1') - comment2 = comment_factory(text='t2') - comment3 = comment_factory(text='t3') + verify_unpaged, comment_factory, input, expected_comment_text +): + comment1 = comment_factory(text="t1") + comment2 = comment_factory(text="t2") + comment3 = comment_factory(text="t3") comment1.last_edit_time = datetime(1991, 1, 1) comment2.last_edit_time = datetime(1991, 1, 2) comment3.last_edit_time = datetime(1991, 1, 3) diff --git a/server/szurubooru/tests/search/configs/test_pool_search_config.py b/server/szurubooru/tests/search/configs/test_pool_search_config.py index 731a676..201bf79 100644 --- a/server/szurubooru/tests/search/configs/test_pool_search_config.py +++ b/server/szurubooru/tests/search/configs/test_pool_search_config.py @@ -1,5 +1,7 @@ from datetime import datetime + import pytest + from szurubooru import db, errors, search @@ -12,77 +14,106 @@ def executor(): def verify_unpaged(executor): def verify(input, expected_pool_names): actual_count, actual_pools = executor.execute( - input, offset=0, limit=100) + input, offset=0, limit=100 + ) actual_pool_names = [u.names[0].name for u in actual_pools] assert actual_count == len(expected_pool_names) assert actual_pool_names == expected_pool_names + return verify -@pytest.mark.parametrize('input,expected_pool_names', [ - ('', ['t1', 't2']), - ('t1', ['t1']), - ('t2', ['t2']), - ('t1,t2', ['t1', 't2']), - ('T1,T2', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("", ["t1", "t2"]), + ("t1", ["t1"]), + ("t2", ["t2"]), + ("t1,t2", ["t1", "t2"]), + ("T1,T2", ["t1", "t2"]), + ], +) def test_filter_anonymous( - verify_unpaged, pool_factory, input, expected_pool_names): - db.session.add(pool_factory(id=1, names=['t1'])) - db.session.add(pool_factory(id=2, names=['t2'])) + verify_unpaged, pool_factory, input, expected_pool_names +): + db.session.add(pool_factory(id=1, names=["t1"])) + db.session.add(pool_factory(id=2, names=["t2"])) db.session.flush() verify_unpaged(input, expected_pool_names) -@pytest.mark.parametrize('db_driver,input,expected_pool_names', [ - (None, ',', None), - (None, 't1,', None), - (None, 't1,t2', ['t1', 't2']), - (None, 't1\\,', []), - (None, 'asd..asd', None), - (None, 'asd\\..asd', []), - (None, 'asd.\\.asd', []), - (None, 'asd\\.\\.asd', []), - (None, '-', None), - (None, '\\-', ['-']), - (None, '--', [ - 't1', 't2', '*', '*asd*', ':', 'asd:asd', '\\', '\\asd', '-asd', - ]), - (None, '\\--', []), - (None, '-\\-', [ - 't1', 't2', '*', '*asd*', ':', 'asd:asd', '\\', '\\asd', '-asd', - ]), - (None, '-*', []), - (None, '\\-*', ['-', '-asd']), - (None, ':', None), - (None, '\\:', [':']), - (None, '\\:asd', []), - (None, '*\\:*', [':', 'asd:asd']), - (None, 'asd:asd', None), - (None, 'asd\\:asd', ['asd:asd']), - (None, '*', [ - 't1', 't2', '*', '*asd*', ':', 'asd:asd', '\\', '\\asd', '-', '-asd' - ]), - (None, '\\*', ['*']), - (None, '\\', None), - (None, '\\asd', None), - ('psycopg2', '\\\\', ['\\']), - ('psycopg2', '\\\\asd', ['\\asd']), -]) +@pytest.mark.parametrize( + "db_driver,input,expected_pool_names", + [ + (None, ",", None), + (None, "t1,", None), + (None, "t1,t2", ["t1", "t2"]), + (None, "t1\\,", []), + (None, "asd..asd", None), + (None, "asd\\..asd", []), + (None, "asd.\\.asd", []), + (None, "asd\\.\\.asd", []), + (None, "-", None), + (None, "\\-", ["-"]), + ( + None, + "--", + ["t1", "t2", "*", "*asd*", ":", "asd:asd", "\\", "\\asd", "-asd",], + ), + (None, "\\--", []), + ( + None, + "-\\-", + ["t1", "t2", "*", "*asd*", ":", "asd:asd", "\\", "\\asd", "-asd",], + ), + (None, "-*", []), + (None, "\\-*", ["-", "-asd"]), + (None, ":", None), + (None, "\\:", [":"]), + (None, "\\:asd", []), + (None, "*\\:*", [":", "asd:asd"]), + (None, "asd:asd", None), + (None, "asd\\:asd", ["asd:asd"]), + ( + None, + "*", + [ + "t1", + "t2", + "*", + "*asd*", + ":", + "asd:asd", + "\\", + "\\asd", + "-", + "-asd", + ], + ), + (None, "\\*", ["*"]), + (None, "\\", None), + (None, "\\asd", None), + ("psycopg2", "\\\\", ["\\"]), + ("psycopg2", "\\\\asd", ["\\asd"]), + ], +) def test_escaping( - executor, pool_factory, input, expected_pool_names, db_driver): - db.session.add_all([ - pool_factory(id=1, names=['t1']), - pool_factory(id=2, names=['t2']), - pool_factory(id=3, names=['*']), - pool_factory(id=4, names=['*asd*']), - pool_factory(id=5, names=[':']), - pool_factory(id=6, names=['asd:asd']), - pool_factory(id=7, names=['\\']), - pool_factory(id=8, names=['\\asd']), - pool_factory(id=9, names=['-']), - pool_factory(id=10, names=['-asd']) - ]) + executor, pool_factory, input, expected_pool_names, db_driver +): + db.session.add_all( + [ + pool_factory(id=1, names=["t1"]), + pool_factory(id=2, names=["t2"]), + pool_factory(id=3, names=["*"]), + pool_factory(id=4, names=["*asd*"]), + pool_factory(id=5, names=[":"]), + pool_factory(id=6, names=["asd:asd"]), + pool_factory(id=7, names=["\\"]), + pool_factory(id=8, names=["\\asd"]), + pool_factory(id=9, names=["-"]), + pool_factory(id=10, names=["-asd"]), + ] + ) db.session.flush() if db_driver and db.session.get_bind().driver != db_driver: @@ -92,99 +123,112 @@ def test_escaping( executor.execute(input, offset=0, limit=100) else: actual_count, actual_pools = executor.execute( - input, offset=0, limit=100) + input, offset=0, limit=100 + ) actual_pool_names = [u.names[0].name for u in actual_pools] assert actual_count == len(expected_pool_names) assert sorted(actual_pool_names) == sorted(expected_pool_names) def test_filter_anonymous_starting_with_colon(verify_unpaged, pool_factory): - db.session.add(pool_factory(id=1, names=[':t'])) + db.session.add(pool_factory(id=1, names=[":t"])) db.session.flush() with pytest.raises(errors.SearchError): - verify_unpaged(':t', [':t']) - verify_unpaged('\\:t', [':t']) - - -@pytest.mark.parametrize('input,expected_pool_names', [ - ('name:pool1', ['pool1']), - ('name:pool2', ['pool2']), - ('name:none', []), - ('name:', []), - ('name:*1', ['pool1']), - ('name:*2', ['pool2']), - ('name:*', ['pool1', 'pool2', 'pool3', 'pool4']), - ('name:p*', ['pool1', 'pool2', 'pool3', 'pool4']), - ('name:*o*', ['pool1', 'pool2', 'pool3', 'pool4']), - ('name:*!*', []), - ('name:!*', []), - ('name:*!', []), - ('-name:pool1', ['pool2', 'pool3', 'pool4']), - ('-name:pool2', ['pool1', 'pool3', 'pool4']), - ('name:pool1,pool2', ['pool1', 'pool2']), - ('-name:pool1,pool3', ['pool2', 'pool4']), - ('name:pool4', ['pool4']), - ('name:pool5', ['pool4']), - ('name:pool4,pool5', ['pool4']), -]) + verify_unpaged(":t", [":t"]) + verify_unpaged("\\:t", [":t"]) + + +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("name:pool1", ["pool1"]), + ("name:pool2", ["pool2"]), + ("name:none", []), + ("name:", []), + ("name:*1", ["pool1"]), + ("name:*2", ["pool2"]), + ("name:*", ["pool1", "pool2", "pool3", "pool4"]), + ("name:p*", ["pool1", "pool2", "pool3", "pool4"]), + ("name:*o*", ["pool1", "pool2", "pool3", "pool4"]), + ("name:*!*", []), + ("name:!*", []), + ("name:*!", []), + ("-name:pool1", ["pool2", "pool3", "pool4"]), + ("-name:pool2", ["pool1", "pool3", "pool4"]), + ("name:pool1,pool2", ["pool1", "pool2"]), + ("-name:pool1,pool3", ["pool2", "pool4"]), + ("name:pool4", ["pool4"]), + ("name:pool5", ["pool4"]), + ("name:pool4,pool5", ["pool4"]), + ], +) def test_filter_by_name( - verify_unpaged, pool_factory, input, expected_pool_names): - db.session.add(pool_factory(id=1, names=['pool1'])) - db.session.add(pool_factory(id=2, names=['pool2'])) - db.session.add(pool_factory(id=3, names=['pool3'])) - db.session.add(pool_factory(id=4, names=['pool4', 'pool5', 'pool6'])) + verify_unpaged, pool_factory, input, expected_pool_names +): + db.session.add(pool_factory(id=1, names=["pool1"])) + db.session.add(pool_factory(id=2, names=["pool2"])) + db.session.add(pool_factory(id=3, names=["pool3"])) + db.session.add(pool_factory(id=4, names=["pool4", "pool5", "pool6"])) db.session.flush() verify_unpaged(input, expected_pool_names) -@pytest.mark.parametrize('input,expected_pool_names', [ - ('category:cat1', ['t1', 't2']), - ('category:cat2', ['t3']), - ('category:cat1,cat2', ['t1', 't2', 't3']), -]) +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("category:cat1", ["t1", "t2"]), + ("category:cat2", ["t3"]), + ("category:cat1,cat2", ["t1", "t2", "t3"]), + ], +) def test_filter_by_category( - verify_unpaged, - pool_factory, - pool_category_factory, - input, - expected_pool_names): - cat1 = pool_category_factory(name='cat1') - cat2 = pool_category_factory(name='cat2') - pool1 = pool_factory(id=1, names=['t1'], category=cat1) - pool2 = pool_factory(id=2, names=['t2'], category=cat1) - pool3 = pool_factory(id=3, names=['t3'], category=cat2) + verify_unpaged, + pool_factory, + pool_category_factory, + input, + expected_pool_names, +): + cat1 = pool_category_factory(name="cat1") + cat2 = pool_category_factory(name="cat2") + pool1 = pool_factory(id=1, names=["t1"], category=cat1) + pool2 = pool_factory(id=2, names=["t2"], category=cat1) + pool3 = pool_factory(id=3, names=["t3"], category=cat2) db.session.add_all([pool1, pool2, pool3]) db.session.flush() verify_unpaged(input, expected_pool_names) -@pytest.mark.parametrize('input,expected_pool_names', [ - ('creation-time:2014', ['t1', 't2']), - ('creation-date:2014', ['t1', 't2']), - ('-creation-time:2014', ['t3']), - ('-creation-date:2014', ['t3']), - ('creation-time:2014..2014-06', ['t1', 't2']), - ('creation-time:2014-06..2015-01-01', ['t2', 't3']), - ('creation-time:2014-06..', ['t2', 't3']), - ('creation-time:..2014-06', ['t1', 't2']), - ('-creation-time:2014..2014-06', ['t3']), - ('-creation-time:2014-06..2015-01-01', ['t1']), - ('creation-date:2014..2014-06', ['t1', 't2']), - ('creation-date:2014-06..2015-01-01', ['t2', 't3']), - ('creation-date:2014-06..', ['t2', 't3']), - ('creation-date:..2014-06', ['t1', 't2']), - ('-creation-date:2014..2014-06', ['t3']), - ('-creation-date:2014-06..2015-01-01', ['t1']), - ('creation-time:2014-01,2015', ['t1', 't3']), - ('creation-date:2014-01,2015', ['t1', 't3']), - ('-creation-time:2014-01,2015', ['t2']), - ('-creation-date:2014-01,2015', ['t2']), -]) +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("creation-time:2014", ["t1", "t2"]), + ("creation-date:2014", ["t1", "t2"]), + ("-creation-time:2014", ["t3"]), + ("-creation-date:2014", ["t3"]), + ("creation-time:2014..2014-06", ["t1", "t2"]), + ("creation-time:2014-06..2015-01-01", ["t2", "t3"]), + ("creation-time:2014-06..", ["t2", "t3"]), + ("creation-time:..2014-06", ["t1", "t2"]), + ("-creation-time:2014..2014-06", ["t3"]), + ("-creation-time:2014-06..2015-01-01", ["t1"]), + ("creation-date:2014..2014-06", ["t1", "t2"]), + ("creation-date:2014-06..2015-01-01", ["t2", "t3"]), + ("creation-date:2014-06..", ["t2", "t3"]), + ("creation-date:..2014-06", ["t1", "t2"]), + ("-creation-date:2014..2014-06", ["t3"]), + ("-creation-date:2014-06..2015-01-01", ["t1"]), + ("creation-time:2014-01,2015", ["t1", "t3"]), + ("creation-date:2014-01,2015", ["t1", "t3"]), + ("-creation-time:2014-01,2015", ["t2"]), + ("-creation-date:2014-01,2015", ["t2"]), + ], +) def test_filter_by_creation_time( - verify_unpaged, pool_factory, input, expected_pool_names): - pool1 = pool_factory(id=1, names=['t1']) - pool2 = pool_factory(id=2, names=['t2']) - pool3 = pool_factory(id=3, names=['t3']) + verify_unpaged, pool_factory, input, expected_pool_names +): + pool1 = pool_factory(id=1, names=["t1"]) + pool2 = pool_factory(id=2, names=["t2"]) + pool3 = pool_factory(id=3, names=["t3"]) pool1.creation_time = datetime(2014, 1, 1) pool2.creation_time = datetime(2014, 6, 1) pool3.creation_time = datetime(2015, 1, 1) @@ -193,17 +237,21 @@ def test_filter_by_creation_time( verify_unpaged(input, expected_pool_names) -@pytest.mark.parametrize('input,expected_pool_names', [ - ('last-edit-date:2014', ['t1', 't3']), - ('last-edit-time:2014', ['t1', 't3']), - ('edit-date:2014', ['t1', 't3']), - ('edit-time:2014', ['t1', 't3']), -]) +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("last-edit-date:2014", ["t1", "t3"]), + ("last-edit-time:2014", ["t1", "t3"]), + ("edit-date:2014", ["t1", "t3"]), + ("edit-time:2014", ["t1", "t3"]), + ], +) def test_filter_by_edit_time( - verify_unpaged, pool_factory, input, expected_pool_names): - pool1 = pool_factory(id=1, names=['t1']) - pool2 = pool_factory(id=2, names=['t2']) - pool3 = pool_factory(id=3, names=['t3']) + verify_unpaged, pool_factory, input, expected_pool_names +): + pool1 = pool_factory(id=1, names=["t1"]) + pool2 = pool_factory(id=2, names=["t2"]) + pool3 = pool_factory(id=3, names=["t3"]) pool1.last_edit_time = datetime(2014, 1, 1) pool2.last_edit_time = datetime(2015, 1, 1) pool3.last_edit_time = datetime(2014, 1, 1) @@ -212,24 +260,24 @@ def test_filter_by_edit_time( verify_unpaged(input, expected_pool_names) -@pytest.mark.parametrize('input,expected_pool_names', [ - ('post-count:2', ['t1']), - ('post-count:1', ['t2']), - ('post-count:1..', ['t1', 't2']), - ('post-count-min:1', ['t1', 't2']), - ('post-count:..1', ['t2']), - ('post-count-max:1', ['t2']), -]) +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("post-count:2", ["t1"]), + ("post-count:1", ["t2"]), + ("post-count:1..", ["t1", "t2"]), + ("post-count-min:1", ["t1", "t2"]), + ("post-count:..1", ["t2"]), + ("post-count-max:1", ["t2"]), + ], +) def test_filter_by_post_count( - verify_unpaged, - pool_factory, - post_factory, - input, - expected_pool_names): + verify_unpaged, pool_factory, post_factory, input, expected_pool_names +): post1 = post_factory(id=1) post2 = post_factory(id=2) - pool1 = pool_factory(id=1, names=['t1']) - pool2 = pool_factory(id=2, names=['t2']) + pool1 = pool_factory(id=1, names=["t1"]) + pool2 = pool_factory(id=2, names=["t2"]) db.session.add_all([post1, post2, pool1, pool2]) pool1.posts.append(post1) pool1.posts.append(post2) @@ -238,49 +286,57 @@ def test_filter_by_post_count( verify_unpaged(input, expected_pool_names) -@pytest.mark.parametrize('input', [ - 'post-count:..', - 'post-count:asd', - 'post-count:asd,1', - 'post-count:1,asd', - 'post-count:asd..1', - 'post-count:1..asd', -]) +@pytest.mark.parametrize( + "input", + [ + "post-count:..", + "post-count:asd", + "post-count:asd,1", + "post-count:1,asd", + "post-count:asd..1", + "post-count:1..asd", + ], +) def test_filter_by_invalid_input(executor, input): with pytest.raises(errors.SearchError): executor.execute(input, offset=0, limit=100) -@pytest.mark.parametrize('input,expected_pool_names', [ - ('', ['t1', 't2']), - ('sort:name', ['t1', 't2']), - ('-sort:name', ['t2', 't1']), - ('sort:name,asc', ['t1', 't2']), - ('sort:name,desc', ['t2', 't1']), - ('-sort:name,asc', ['t2', 't1']), - ('-sort:name,desc', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("", ["t1", "t2"]), + ("sort:name", ["t1", "t2"]), + ("-sort:name", ["t2", "t1"]), + ("sort:name,asc", ["t1", "t2"]), + ("sort:name,desc", ["t2", "t1"]), + ("-sort:name,asc", ["t2", "t1"]), + ("-sort:name,desc", ["t1", "t2"]), + ], +) def test_sort_by_name( - verify_unpaged, - pool_factory, - input, - expected_pool_names): - db.session.add(pool_factory(id=2, names=['t2'])) - db.session.add(pool_factory(id=1, names=['t1'])) + verify_unpaged, pool_factory, input, expected_pool_names +): + db.session.add(pool_factory(id=2, names=["t2"])) + db.session.add(pool_factory(id=1, names=["t1"])) db.session.flush() verify_unpaged(input, expected_pool_names) -@pytest.mark.parametrize('input,expected_pool_names', [ - ('', ['t1', 't2', 't3']), - ('sort:creation-date', ['t3', 't2', 't1']), - ('sort:creation-time', ['t3', 't2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("", ["t1", "t2", "t3"]), + ("sort:creation-date", ["t3", "t2", "t1"]), + ("sort:creation-time", ["t3", "t2", "t1"]), + ], +) def test_sort_by_creation_time( - verify_unpaged, pool_factory, input, expected_pool_names): - pool1 = pool_factory(id=1, names=['t1']) - pool2 = pool_factory(id=2, names=['t2']) - pool3 = pool_factory(id=3, names=['t3']) + verify_unpaged, pool_factory, input, expected_pool_names +): + pool1 = pool_factory(id=1, names=["t1"]) + pool2 = pool_factory(id=2, names=["t2"]) + pool3 = pool_factory(id=3, names=["t3"]) pool1.creation_time = datetime(1991, 1, 1) pool2.creation_time = datetime(1991, 1, 2) pool3.creation_time = datetime(1991, 1, 3) @@ -289,18 +345,22 @@ def test_sort_by_creation_time( verify_unpaged(input, expected_pool_names) -@pytest.mark.parametrize('input,expected_pool_names', [ - ('', ['t1', 't2', 't3']), - ('sort:last-edit-date', ['t3', 't2', 't1']), - ('sort:last-edit-time', ['t3', 't2', 't1']), - ('sort:edit-date', ['t3', 't2', 't1']), - ('sort:edit-time', ['t3', 't2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("", ["t1", "t2", "t3"]), + ("sort:last-edit-date", ["t3", "t2", "t1"]), + ("sort:last-edit-time", ["t3", "t2", "t1"]), + ("sort:edit-date", ["t3", "t2", "t1"]), + ("sort:edit-time", ["t3", "t2", "t1"]), + ], +) def test_sort_by_last_edit_time( - verify_unpaged, pool_factory, input, expected_pool_names): - pool1 = pool_factory(id=1, names=['t1']) - pool2 = pool_factory(id=2, names=['t2']) - pool3 = pool_factory(id=3, names=['t3']) + verify_unpaged, pool_factory, input, expected_pool_names +): + pool1 = pool_factory(id=1, names=["t1"]) + pool2 = pool_factory(id=2, names=["t2"]) + pool3 = pool_factory(id=3, names=["t3"]) pool1.last_edit_time = datetime(1991, 1, 1) pool2.last_edit_time = datetime(1991, 1, 2) pool3.last_edit_time = datetime(1991, 1, 3) @@ -309,19 +369,16 @@ def test_sort_by_last_edit_time( verify_unpaged(input, expected_pool_names) -@pytest.mark.parametrize('input,expected_pool_names', [ - ('sort:post-count', ['t2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_pool_names", [("sort:post-count", ["t2", "t1"]),] +) def test_sort_by_post_count( - verify_unpaged, - pool_factory, - post_factory, - input, - expected_pool_names): + verify_unpaged, pool_factory, post_factory, input, expected_pool_names +): post1 = post_factory(id=1) post2 = post_factory(id=2) - pool1 = pool_factory(id=1, names=['t1']) - pool2 = pool_factory(id=2, names=['t2']) + pool1 = pool_factory(id=1, names=["t1"]) + pool2 = pool_factory(id=2, names=["t2"]) db.session.add_all([post1, post2, pool1, pool2]) pool1.posts.append(post1) pool2.posts.append(post1) @@ -330,20 +387,21 @@ def test_sort_by_post_count( verify_unpaged(input, expected_pool_names) -@pytest.mark.parametrize('input,expected_pool_names', [ - ('sort:category', ['t3', 't1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_pool_names", [("sort:category", ["t3", "t1", "t2"]),] +) def test_sort_by_category( - verify_unpaged, - pool_factory, - pool_category_factory, - input, - expected_pool_names): - cat1 = pool_category_factory(name='cat1') - cat2 = pool_category_factory(name='cat2') - pool1 = pool_factory(id=1, names=['t1'], category=cat2) - pool2 = pool_factory(id=2, names=['t2'], category=cat2) - pool3 = pool_factory(id=3, names=['t3'], category=cat1) + verify_unpaged, + pool_factory, + pool_category_factory, + input, + expected_pool_names, +): + cat1 = pool_category_factory(name="cat1") + cat2 = pool_category_factory(name="cat2") + pool1 = pool_factory(id=1, names=["t1"], category=cat2) + pool2 = pool_factory(id=2, names=["t2"], category=cat2) + pool3 = pool_factory(id=3, names=["t3"], category=cat1) db.session.add_all([pool1, pool2, pool3]) db.session.flush() verify_unpaged(input, expected_pool_names) diff --git a/server/szurubooru/tests/search/configs/test_post_search_config.py b/server/szurubooru/tests/search/configs/test_post_search_config.py index 462594c..172d1ff 100644 --- a/server/szurubooru/tests/search/configs/test_post_search_config.py +++ b/server/szurubooru/tests/search/configs/test_post_search_config.py @@ -1,15 +1,17 @@ from datetime import datetime + import pytest -from szurubooru import db, model, errors, search + +from szurubooru import db, errors, model, search @pytest.fixture def fav_factory(user_factory): def factory(post, user=None): return model.PostFavorite( - post=post, - user=user or user_factory(), - time=datetime.utcnow()) + post=post, user=user or user_factory(), time=datetime.utcnow() + ) + return factory @@ -20,14 +22,17 @@ def score_factory(user_factory): post=post, user=user or user_factory(), time=datetime.utcnow(), - score=score) + score=score, + ) + return factory @pytest.fixture def note_factory(): - def factory(text='...'): - return model.PostNote(polygon='...', text=text) + def factory(text="..."): + return model.PostNote(polygon="...", text=text) + return factory @@ -36,11 +41,10 @@ def feature_factory(user_factory): def factory(post=None): if post: return model.PostFeature( - time=datetime.utcnow(), - user=user_factory(), - post=post) - return model.PostFeature( - time=datetime.utcnow(), user=user_factory()) + time=datetime.utcnow(), user=user_factory(), post=post + ) + return model.PostFeature(time=datetime.utcnow(), user=user_factory()) + return factory @@ -57,6 +61,7 @@ def auth_executor(executor, user_factory): db.session.flush() executor.config.user = auth_user return auth_user + return wrapper @@ -64,21 +69,22 @@ def auth_executor(executor, user_factory): def verify_unpaged(executor): def verify(input, expected_post_ids, test_order=False): actual_count, actual_posts = executor.execute( - input, offset=0, limit=100) + input, offset=0, limit=100 + ) actual_post_ids = list([p.post_id for p in actual_posts]) if not test_order: actual_post_ids = sorted(actual_post_ids) expected_post_ids = sorted(expected_post_ids) assert actual_post_ids == expected_post_ids assert actual_count == len(expected_post_ids) + return verify -@pytest.mark.parametrize('input,expected_post_ids', [ - ('id:1', [1]), - ('id:3', [3]), - ('id:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [("id:1", [1]), ("id:3", [3]), ("id:1,3", [1, 3]),], +) def test_filter_by_id(verify_unpaged, post_factory, input, expected_post_ids): post1 = post_factory(id=1) post2 = post_factory(id=2) @@ -88,35 +94,39 @@ def test_filter_by_id(verify_unpaged, post_factory, input, expected_post_ids): verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('tag:t1', [1]), - ('tag:t2', [2]), - ('tag:t1,t2', [1, 2]), - ('tag:t4a', [4]), - ('tag:t4b', [4]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("tag:t1", [1]), + ("tag:t2", [2]), + ("tag:t1,t2", [1, 2]), + ("tag:t4a", [4]), + ("tag:t4b", [4]), + ], +) def test_filter_by_tag( - verify_unpaged, post_factory, tag_factory, input, expected_post_ids): + verify_unpaged, post_factory, tag_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) post4 = post_factory(id=4) - post1.tags = [tag_factory(names=['t1'])] - post2.tags = [tag_factory(names=['t2'])] - post3.tags = [tag_factory(names=['t3'])] - post4.tags = [tag_factory(names=['t4a', 't4b'])] + post1.tags = [tag_factory(names=["t1"])] + post2.tags = [tag_factory(names=["t2"])] + post3.tags = [tag_factory(names=["t3"])] + post4.tags = [tag_factory(names=["t4a", "t4b"])] db.session.add_all([post1, post2, post3, post4]) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('score:1', [1]), - ('score:3', [3]), - ('score:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [("score:1", [1]), ("score:3", [3]), ("score:1,3", [1, 3]),], +) def test_filter_by_score( - verify_unpaged, post_factory, user_factory, input, expected_post_ids): + verify_unpaged, post_factory, user_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -126,96 +136,111 @@ def test_filter_by_score( score=post.post_id, time=datetime.utcnow(), post=post, - user=user_factory())) + user=user_factory(), + ) + ) db.session.add_all([post1, post2, post3]) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('uploader:', [4]), - ('uploader:u1', [1]), - ('uploader:u3', [3]), - ('uploader:u1,u3', [1, 3]), - ('upload:', [4]), - ('upload:u1', [1]), - ('upload:u3', [3]), - ('upload:u1,u3', [1, 3]), - ('submit:', [4]), - ('submit:u1', [1]), - ('submit:u3', [3]), - ('submit:u1,u3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("uploader:", [4]), + ("uploader:u1", [1]), + ("uploader:u3", [3]), + ("uploader:u1,u3", [1, 3]), + ("upload:", [4]), + ("upload:u1", [1]), + ("upload:u3", [3]), + ("upload:u1,u3", [1, 3]), + ("submit:", [4]), + ("submit:u1", [1]), + ("submit:u3", [3]), + ("submit:u1,u3", [1, 3]), + ], +) def test_filter_by_uploader( - verify_unpaged, post_factory, user_factory, input, expected_post_ids): + verify_unpaged, post_factory, user_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) post4 = post_factory(id=4) - post1.user = user_factory(name='u1') - post2.user = user_factory(name='u2') - post3.user = user_factory(name='u3') + post1.user = user_factory(name="u1") + post2.user = user_factory(name="u2") + post3.user = user_factory(name="u3") db.session.add_all([post1, post2, post3, post4]) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('comment:u1', [1]), - ('comment:u3', [3]), - ('comment:u1,u3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [("comment:u1", [1]), ("comment:u3", [3]), ("comment:u1,u3", [1, 3]),], +) def test_filter_by_commenter( - verify_unpaged, - post_factory, - user_factory, - comment_factory, - input, - expected_post_ids): + verify_unpaged, + post_factory, + user_factory, + comment_factory, + input, + expected_post_ids, +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - db.session.add_all([ - comment_factory(post=post1, user=user_factory(name='u1')), - comment_factory(post=post2, user=user_factory(name='u2')), - comment_factory(post=post3, user=user_factory(name='u3')), - post1, post2, post3, - ]) + db.session.add_all( + [ + comment_factory(post=post1, user=user_factory(name="u1")), + comment_factory(post=post2, user=user_factory(name="u2")), + comment_factory(post=post3, user=user_factory(name="u3")), + post1, + post2, + post3, + ] + ) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('fav:u1', [1]), - ('fav:u3', [3]), - ('fav:u1,u3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [("fav:u1", [1]), ("fav:u3", [3]), ("fav:u1,u3", [1, 3]),], +) def test_filter_by_favorite( - verify_unpaged, - post_factory, - user_factory, - fav_factory, - input, - expected_post_ids): + verify_unpaged, + post_factory, + user_factory, + fav_factory, + input, + expected_post_ids, +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - db.session.add_all([ - fav_factory(post=post1, user=user_factory(name='u1')), - fav_factory(post=post2, user=user_factory(name='u2')), - fav_factory(post=post3, user=user_factory(name='u3')), - post1, post2, post3]) + db.session.add_all( + [ + fav_factory(post=post1, user=user_factory(name="u1")), + fav_factory(post=post2, user=user_factory(name="u2")), + fav_factory(post=post3, user=user_factory(name="u3")), + post1, + post2, + post3, + ] + ) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('tag-count:1', [1]), - ('tag-count:3', [3]), - ('tag-count:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [("tag-count:1", [1]), ("tag-count:3", [3]), ("tag-count:1,3", [1, 3]),], +) def test_filter_by_tag_count( - verify_unpaged, post_factory, tag_factory, input, expected_post_ids): + verify_unpaged, post_factory, tag_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -227,61 +252,75 @@ def test_filter_by_tag_count( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('comment-count:1', [1]), - ('comment-count:3', [3]), - ('comment-count:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("comment-count:1", [1]), + ("comment-count:3", [3]), + ("comment-count:1,3", [1, 3]), + ], +) def test_filter_by_comment_count( - verify_unpaged, - post_factory, - comment_factory, - input, - expected_post_ids): + verify_unpaged, post_factory, comment_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - db.session.add_all([ - comment_factory(post=post1), - comment_factory(post=post2), - comment_factory(post=post2), - comment_factory(post=post3), - comment_factory(post=post3), - comment_factory(post=post3), - post1, post2, post3]) + db.session.add_all( + [ + comment_factory(post=post1), + comment_factory(post=post2), + comment_factory(post=post2), + comment_factory(post=post3), + comment_factory(post=post3), + comment_factory(post=post3), + post1, + post2, + post3, + ] + ) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('fav-count:1', [1]), - ('fav-count:3', [3]), - ('fav-count:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [("fav-count:1", [1]), ("fav-count:3", [3]), ("fav-count:1,3", [1, 3]),], +) def test_filter_by_favorite_count( - verify_unpaged, post_factory, fav_factory, input, expected_post_ids): + verify_unpaged, post_factory, fav_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - db.session.add_all([ - fav_factory(post=post1), - fav_factory(post=post2), - fav_factory(post=post2), - fav_factory(post=post3), - fav_factory(post=post3), - fav_factory(post=post3), - post1, post2, post3]) + db.session.add_all( + [ + fav_factory(post=post1), + fav_factory(post=post2), + fav_factory(post=post2), + fav_factory(post=post3), + fav_factory(post=post3), + fav_factory(post=post3), + post1, + post2, + post3, + ] + ) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('note-count:1', [1]), - ('note-count:3', [3]), - ('note-count:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("note-count:1", [1]), + ("note-count:3", [3]), + ("note-count:1,3", [1, 3]), + ], +) def test_filter_by_note_count( - verify_unpaged, post_factory, note_factory, input, expected_post_ids): + verify_unpaged, post_factory, note_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -293,36 +332,40 @@ def test_filter_by_note_count( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('note-text:*', [1, 2, 3]), - ('note-text:text2', [2]), - ('note-text:text3*', [3]), - ('note-text:text3a,text2', [2, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("note-text:*", [1, 2, 3]), + ("note-text:text2", [2]), + ("note-text:text3*", [3]), + ("note-text:text3a,text2", [2, 3]), + ], +) def test_filter_by_note_text( - verify_unpaged, post_factory, note_factory, input, expected_post_ids): + verify_unpaged, post_factory, note_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - post1.notes = [note_factory(text='text1')] - post2.notes = [note_factory(text='text2'), note_factory(text='text2')] - post3.notes = [note_factory(text='text3a'), note_factory(text='text3b')] + post1.notes = [note_factory(text="text1")] + post2.notes = [note_factory(text="text2"), note_factory(text="text2")] + post3.notes = [note_factory(text="text3a"), note_factory(text="text3b")] db.session.add_all([post1, post2, post3]) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('feature-count:1', [1]), - ('feature-count:3', [3]), - ('feature-count:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("feature-count:1", [1]), + ("feature-count:3", [3]), + ("feature-count:1,3", [1, 3]), + ], +) def test_filter_by_feature_count( - verify_unpaged, - post_factory, - feature_factory, - input, - expected_post_ids): + verify_unpaged, post_factory, feature_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -334,18 +377,22 @@ def test_filter_by_feature_count( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('type:image', [1]), - ('type:anim', [2]), - ('type:animation', [2]), - ('type:gif', [2]), - ('type:video', [3]), - ('type:webm', [3]), - ('type:flash', [4]), - ('type:swf', [4]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("type:image", [1]), + ("type:anim", [2]), + ("type:animation", [2]), + ("type:gif", [2]), + ("type:video", [3]), + ("type:webm", [3]), + ("type:flash", [4]), + ("type:swf", [4]), + ], +) def test_filter_by_type( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -359,14 +406,18 @@ def test_filter_by_type( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('safety:safe', [1]), - ('safety:sketchy', [2]), - ('safety:questionable', [2]), - ('safety:unsafe', [3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("safety:safe", [1]), + ("safety:sketchy", [2]), + ("safety:questionable", [2]), + ("safety:unsafe", [3]), + ], +) def test_filter_by_safety( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -380,34 +431,42 @@ def test_filter_by_safety( def test_filter_by_invalid_type(executor): with pytest.raises(errors.SearchError): - executor.execute('type:invalid', offset=0, limit=100) + executor.execute("type:invalid", offset=0, limit=100) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('content-checksum:checksum1', [1]), - ('content-checksum:checksum3', [3]), - ('content-checksum:checksum1,checksum3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("content-checksum:checksum1", [1]), + ("content-checksum:checksum3", [3]), + ("content-checksum:checksum1,checksum3", [1, 3]), + ], +) def test_filter_by_content_checksum( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - post1.checksum = 'checksum1' - post2.checksum = 'checksum2' - post3.checksum = 'checksum3' + post1.checksum = "checksum1" + post2.checksum = "checksum2" + post3.checksum = "checksum3" db.session.add_all([post1, post2, post3]) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('file-size:100', [1]), - ('file-size:102', [3]), - ('file-size:100,102', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("file-size:100", [1]), + ("file-size:102", [3]), + ("file-size:100,102", [1, 3]), + ], +) def test_filter_by_file_size( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -419,25 +478,29 @@ def test_filter_by_file_size( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('image-width:100', [1]), - ('image-width:200', [2]), - ('image-width:100,300', [1, 3]), - ('image-height:200', [1]), - ('image-height:100', [2]), - ('image-height:200,300', [1, 3]), - ('image-area:20000', [1, 2]), - ('image-area:90000', [3]), - ('image-area:20000,90000', [1, 2, 3]), - ('image-ar:1', [3]), - ('image-ar:..0.9', [1, 4]), - ('image-ar:1.1..', [2]), - ('image-ar:1/1..1/1', [3]), - ('image-ar:1:1..1:1', [3]), - ('image-ar:0.62..0.63', [4]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("image-width:100", [1]), + ("image-width:200", [2]), + ("image-width:100,300", [1, 3]), + ("image-height:200", [1]), + ("image-height:100", [2]), + ("image-height:200,300", [1, 3]), + ("image-area:20000", [1, 2]), + ("image-area:90000", [3]), + ("image-area:20000,90000", [1, 2, 3]), + ("image-ar:1", [3]), + ("image-ar:..0.9", [1, 4]), + ("image-ar:1.1..", [2]), + ("image-ar:1/1..1/1", [3]), + ("image-ar:1:1..1:1", [3]), + ("image-ar:0.62..0.63", [4]), + ], +) def test_filter_by_image_size( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -457,25 +520,29 @@ def test_filter_by_image_size( def test_filter_by_invalid_aspect_ratio(executor): with pytest.raises(errors.SearchError): - executor.execute('image-ar:1:1:1', offset=0, limit=100) - - -@pytest.mark.parametrize('input,expected_post_ids', [ - ('creation-date:2014', [1]), - ('creation-date:2016', [3]), - ('creation-date:2014,2016', [1, 3]), - ('creation-time:2014', [1]), - ('creation-time:2016', [3]), - ('creation-time:2014,2016', [1, 3]), - ('date:2014', [1]), - ('date:2016', [3]), - ('date:2014,2016', [1, 3]), - ('time:2014', [1]), - ('time:2016', [3]), - ('time:2014,2016', [1, 3]), -]) + executor.execute("image-ar:1:1:1", offset=0, limit=100) + + +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("creation-date:2014", [1]), + ("creation-date:2016", [3]), + ("creation-date:2014,2016", [1, 3]), + ("creation-time:2014", [1]), + ("creation-time:2016", [3]), + ("creation-time:2014,2016", [1, 3]), + ("date:2014", [1]), + ("date:2016", [3]), + ("date:2014,2016", [1, 3]), + ("time:2014", [1]), + ("time:2016", [3]), + ("time:2014,2016", [1, 3]), + ], +) def test_filter_by_creation_time( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -487,22 +554,26 @@ def test_filter_by_creation_time( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('last-edit-date:2014', [1]), - ('last-edit-date:2016', [3]), - ('last-edit-date:2014,2016', [1, 3]), - ('last-edit-time:2014', [1]), - ('last-edit-time:2016', [3]), - ('last-edit-time:2014,2016', [1, 3]), - ('edit-date:2014', [1]), - ('edit-date:2016', [3]), - ('edit-date:2014,2016', [1, 3]), - ('edit-time:2014', [1]), - ('edit-time:2016', [3]), - ('edit-time:2014,2016', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("last-edit-date:2014", [1]), + ("last-edit-date:2016", [3]), + ("last-edit-date:2014,2016", [1, 3]), + ("last-edit-time:2014", [1]), + ("last-edit-time:2016", [3]), + ("last-edit-time:2014,2016", [1, 3]), + ("edit-date:2014", [1]), + ("edit-date:2016", [3]), + ("edit-date:2014,2016", [1, 3]), + ("edit-time:2014", [1]), + ("edit-time:2016", [3]), + ("edit-time:2014,2016", [1, 3]), + ], +) def test_filter_by_last_edit_time( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -514,20 +585,20 @@ def test_filter_by_last_edit_time( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('comment-date:2014', [1]), - ('comment-date:2016', [3]), - ('comment-date:2014,2016', [1, 3]), - ('comment-time:2014', [1]), - ('comment-time:2016', [3]), - ('comment-time:2014,2016', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("comment-date:2014", [1]), + ("comment-date:2016", [3]), + ("comment-date:2014,2016", [1, 3]), + ("comment-time:2014", [1]), + ("comment-time:2016", [3]), + ("comment-time:2014,2016", [1, 3]), + ], +) def test_filter_by_comment_date( - verify_unpaged, - post_factory, - comment_factory, - input, - expected_post_ids): + verify_unpaged, post_factory, comment_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -542,16 +613,20 @@ def test_filter_by_comment_date( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('fav-date:2014', [1]), - ('fav-date:2016', [3]), - ('fav-date:2014,2016', [1, 3]), - ('fav-time:2014', [1]), - ('fav-time:2016', [3]), - ('fav-time:2014,2016', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("fav-date:2014", [1]), + ("fav-date:2016", [3]), + ("fav-date:2014,2016", [1, 3]), + ("fav-time:2014", [1]), + ("fav-time:2016", [3]), + ("fav-time:2014,2016", [1, 3]), + ], +) def test_filter_by_fav_date( - verify_unpaged, post_factory, fav_factory, input, expected_post_ids): + verify_unpaged, post_factory, fav_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -566,20 +641,20 @@ def test_filter_by_fav_date( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('feature-date:2014', [1]), - ('feature-date:2016', [3]), - ('feature-date:2014,2016', [1, 3]), - ('feature-time:2014', [1]), - ('feature-time:2016', [3]), - ('feature-time:2014,2016', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("feature-date:2014", [1]), + ("feature-date:2016", [3]), + ("feature-date:2014,2016", [1, 3]), + ("feature-time:2014", [1]), + ("feature-time:2016", [3]), + ("feature-time:2014,2016", [1, 3]), + ], +) def test_filter_by_feature_date( - verify_unpaged, - post_factory, - feature_factory, - input, - expected_post_ids): + verify_unpaged, post_factory, feature_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -594,37 +669,40 @@ def test_filter_by_feature_date( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input', [ - 'sort:random', - 'sort:id', - 'sort:score', - 'sort:tag-count', - 'sort:comment-count', - 'sort:fav-count', - 'sort:note-count', - 'sort:feature-count', - 'sort:file-size', - 'sort:image-width', - 'sort:width', - 'sort:image-height', - 'sort:height', - 'sort:image-area', - 'sort:area', - 'sort:creation-date', - 'sort:creation-time', - 'sort:date', - 'sort:time', - 'sort:last-edit-date', - 'sort:last-edit-time', - 'sort:edit-date', - 'sort:edit-time', - 'sort:comment-date', - 'sort:comment-time', - 'sort:fav-date', - 'sort:fav-time', - 'sort:feature-date', - 'sort:feature-time', -]) +@pytest.mark.parametrize( + "input", + [ + "sort:random", + "sort:id", + "sort:score", + "sort:tag-count", + "sort:comment-count", + "sort:fav-count", + "sort:note-count", + "sort:feature-count", + "sort:file-size", + "sort:image-width", + "sort:width", + "sort:image-height", + "sort:height", + "sort:image-area", + "sort:area", + "sort:creation-date", + "sort:creation-time", + "sort:date", + "sort:time", + "sort:last-edit-date", + "sort:last-edit-time", + "sort:edit-date", + "sort:edit-time", + "sort:comment-date", + "sort:comment-time", + "sort:fav-date", + "sort:fav-time", + "sort:feature-date", + "sort:feature-time", + ], +) def test_sort_tokens(verify_unpaged, post_factory, input): post1 = post_factory(id=1) post2 = post_factory(id=2) @@ -634,115 +712,124 @@ def test_sort_tokens(verify_unpaged, post_factory, input): verify_unpaged(input, [1, 2, 3]) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('', [1, 2, 3, 4]), - ('t1', [1]), - ('t2', [2]), - ('t1,t2', [1, 2]), - ('t4a', [4]), - ('t4b', [4]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("", [1, 2, 3, 4]), + ("t1", [1]), + ("t2", [2]), + ("t1,t2", [1, 2]), + ("t4a", [4]), + ("t4b", [4]), + ], +) def test_anonymous( - verify_unpaged, post_factory, tag_factory, input, expected_post_ids): + verify_unpaged, post_factory, tag_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) post4 = post_factory(id=4) - post1.tags = [tag_factory(names=['t1'])] - post2.tags = [tag_factory(names=['t2'])] - post3.tags = [tag_factory(names=['t3'])] - post4.tags = [tag_factory(names=['t4a', 't4b'])] + post1.tags = [tag_factory(names=["t1"])] + post2.tags = [tag_factory(names=["t2"])] + post3.tags = [tag_factory(names=["t3"])] + post4.tags = [tag_factory(names=["t4a", "t4b"])] db.session.add_all([post1, post2, post3, post4]) db.session.flush() verify_unpaged(input, expected_post_ids) def test_own_liked( - auth_executor, - post_factory, - score_factory, - user_factory, - verify_unpaged): + auth_executor, post_factory, score_factory, user_factory, verify_unpaged +): auth_user = auth_executor() post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - db.session.add_all([ - score_factory(post=post1, user=auth_user, score=1), - score_factory(post=post2, user=user_factory(name='dummy'), score=1), - score_factory(post=post3, user=auth_user, score=-1), - post1, post2, post3, - ]) + db.session.add_all( + [ + score_factory(post=post1, user=auth_user, score=1), + score_factory( + post=post2, user=user_factory(name="dummy"), score=1 + ), + score_factory(post=post3, user=auth_user, score=-1), + post1, + post2, + post3, + ] + ) db.session.flush() - verify_unpaged('special:liked', [1]) - verify_unpaged('-special:liked', [2, 3]) + verify_unpaged("special:liked", [1]) + verify_unpaged("-special:liked", [2, 3]) def test_own_disliked( - auth_executor, - post_factory, - score_factory, - user_factory, - verify_unpaged): + auth_executor, post_factory, score_factory, user_factory, verify_unpaged +): auth_user = auth_executor() post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - db.session.add_all([ - score_factory(post=post1, user=auth_user, score=-1), - score_factory(post=post2, user=user_factory(name='dummy'), score=-1), - score_factory(post=post3, user=auth_user, score=1), - post1, post2, post3, - ]) + db.session.add_all( + [ + score_factory(post=post1, user=auth_user, score=-1), + score_factory( + post=post2, user=user_factory(name="dummy"), score=-1 + ), + score_factory(post=post3, user=auth_user, score=1), + post1, + post2, + post3, + ] + ) db.session.flush() - verify_unpaged('special:disliked', [1]) - verify_unpaged('-special:disliked', [2, 3]) + verify_unpaged("special:disliked", [1]) + verify_unpaged("-special:disliked", [2, 3]) -@pytest.mark.parametrize('input', [ - 'liked:x', - 'disliked:x', -]) +@pytest.mark.parametrize("input", ["liked:x", "disliked:x",]) def test_someones_score(executor, input): with pytest.raises(errors.SearchError): executor.execute(input, offset=0, limit=100) def test_own_fav( - auth_executor, - post_factory, - fav_factory, - user_factory, - verify_unpaged): + auth_executor, post_factory, fav_factory, user_factory, verify_unpaged +): auth_user = auth_executor() post1 = post_factory(id=1) post2 = post_factory(id=2) - db.session.add_all([ - fav_factory(post=post1, user=auth_user), - fav_factory(post=post2, user=user_factory(name='unrelated')), - post1, post2, - ]) + db.session.add_all( + [ + fav_factory(post=post1, user=auth_user), + fav_factory(post=post2, user=user_factory(name="unrelated")), + post1, + post2, + ] + ) db.session.flush() - verify_unpaged('special:fav', [1]) - verify_unpaged('-special:fav', [2]) + verify_unpaged("special:fav", [1]) + verify_unpaged("-special:fav", [2]) def test_tumbleweed( - post_factory, - fav_factory, - comment_factory, - score_factory, - verify_unpaged): + post_factory, fav_factory, comment_factory, score_factory, verify_unpaged +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) post4 = post_factory(id=4) - db.session.add_all([ - comment_factory(post=post1), - score_factory(post=post2), - fav_factory(post=post3), - post1, post2, post3, post4, - ]) + db.session.add_all( + [ + comment_factory(post=post1), + score_factory(post=post2), + fav_factory(post=post3), + post1, + post2, + post3, + post4, + ] + ) db.session.flush() - verify_unpaged('special:tumbleweed', [4]) - verify_unpaged('-special:tumbleweed', [1, 2, 3]) + verify_unpaged("special:tumbleweed", [4]) + verify_unpaged("-special:tumbleweed", [1, 2, 3]) diff --git a/server/szurubooru/tests/search/configs/test_tag_search_config.py b/server/szurubooru/tests/search/configs/test_tag_search_config.py index 09a4c40..2ba1b9b 100644 --- a/server/szurubooru/tests/search/configs/test_tag_search_config.py +++ b/server/szurubooru/tests/search/configs/test_tag_search_config.py @@ -1,5 +1,7 @@ from datetime import datetime + import pytest + from szurubooru import db, errors, search @@ -12,77 +14,104 @@ def executor(): def verify_unpaged(executor): def verify(input, expected_tag_names): actual_count, actual_tags = executor.execute( - input, offset=0, limit=100) + input, offset=0, limit=100 + ) actual_tag_names = [u.names[0].name for u in actual_tags] assert actual_count == len(expected_tag_names) assert actual_tag_names == expected_tag_names + return verify -@pytest.mark.parametrize('input,expected_tag_names', [ - ('', ['t1', 't2']), - ('t1', ['t1']), - ('t2', ['t2']), - ('t1,t2', ['t1', 't2']), - ('T1,T2', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("", ["t1", "t2"]), + ("t1", ["t1"]), + ("t2", ["t2"]), + ("t1,t2", ["t1", "t2"]), + ("T1,T2", ["t1", "t2"]), + ], +) def test_filter_anonymous( - verify_unpaged, tag_factory, input, expected_tag_names): - db.session.add(tag_factory(names=['t1'])) - db.session.add(tag_factory(names=['t2'])) + verify_unpaged, tag_factory, input, expected_tag_names +): + db.session.add(tag_factory(names=["t1"])) + db.session.add(tag_factory(names=["t2"])) db.session.flush() verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('db_driver,input,expected_tag_names', [ - (None, ',', None), - (None, 't1,', None), - (None, 't1,t2', ['t1', 't2']), - (None, 't1\\,', []), - (None, 'asd..asd', None), - (None, 'asd\\..asd', []), - (None, 'asd.\\.asd', []), - (None, 'asd\\.\\.asd', []), - (None, '-', None), - (None, '\\-', ['-']), - (None, '--', [ - 't1', 't2', '*', '*asd*', ':', 'asd:asd', '\\', '\\asd', '-asd', - ]), - (None, '\\--', []), - (None, '-\\-', [ - 't1', 't2', '*', '*asd*', ':', 'asd:asd', '\\', '\\asd', '-asd', - ]), - (None, '-*', []), - (None, '\\-*', ['-', '-asd']), - (None, ':', None), - (None, '\\:', [':']), - (None, '\\:asd', []), - (None, '*\\:*', [':', 'asd:asd']), - (None, 'asd:asd', None), - (None, 'asd\\:asd', ['asd:asd']), - (None, '*', [ - 't1', 't2', '*', '*asd*', ':', 'asd:asd', '\\', '\\asd', '-', '-asd' - ]), - (None, '\\*', ['*']), - (None, '\\', None), - (None, '\\asd', None), - ('psycopg2', '\\\\', ['\\']), - ('psycopg2', '\\\\asd', ['\\asd']), -]) -def test_escaping( - executor, tag_factory, input, expected_tag_names, db_driver): - db.session.add_all([ - tag_factory(names=['t1']), - tag_factory(names=['t2']), - tag_factory(names=['*']), - tag_factory(names=['*asd*']), - tag_factory(names=[':']), - tag_factory(names=['asd:asd']), - tag_factory(names=['\\']), - tag_factory(names=['\\asd']), - tag_factory(names=['-']), - tag_factory(names=['-asd']) - ]) +@pytest.mark.parametrize( + "db_driver,input,expected_tag_names", + [ + (None, ",", None), + (None, "t1,", None), + (None, "t1,t2", ["t1", "t2"]), + (None, "t1\\,", []), + (None, "asd..asd", None), + (None, "asd\\..asd", []), + (None, "asd.\\.asd", []), + (None, "asd\\.\\.asd", []), + (None, "-", None), + (None, "\\-", ["-"]), + ( + None, + "--", + ["t1", "t2", "*", "*asd*", ":", "asd:asd", "\\", "\\asd", "-asd",], + ), + (None, "\\--", []), + ( + None, + "-\\-", + ["t1", "t2", "*", "*asd*", ":", "asd:asd", "\\", "\\asd", "-asd",], + ), + (None, "-*", []), + (None, "\\-*", ["-", "-asd"]), + (None, ":", None), + (None, "\\:", [":"]), + (None, "\\:asd", []), + (None, "*\\:*", [":", "asd:asd"]), + (None, "asd:asd", None), + (None, "asd\\:asd", ["asd:asd"]), + ( + None, + "*", + [ + "t1", + "t2", + "*", + "*asd*", + ":", + "asd:asd", + "\\", + "\\asd", + "-", + "-asd", + ], + ), + (None, "\\*", ["*"]), + (None, "\\", None), + (None, "\\asd", None), + ("psycopg2", "\\\\", ["\\"]), + ("psycopg2", "\\\\asd", ["\\asd"]), + ], +) +def test_escaping(executor, tag_factory, input, expected_tag_names, db_driver): + db.session.add_all( + [ + tag_factory(names=["t1"]), + tag_factory(names=["t2"]), + tag_factory(names=["*"]), + tag_factory(names=["*asd*"]), + tag_factory(names=[":"]), + tag_factory(names=["asd:asd"]), + tag_factory(names=["\\"]), + tag_factory(names=["\\asd"]), + tag_factory(names=["-"]), + tag_factory(names=["-asd"]), + ] + ) db.session.flush() if db_driver and db.session.get_bind().driver != db_driver: @@ -92,99 +121,112 @@ def test_escaping( executor.execute(input, offset=0, limit=100) else: actual_count, actual_tags = executor.execute( - input, offset=0, limit=100) + input, offset=0, limit=100 + ) actual_tag_names = [u.names[0].name for u in actual_tags] assert actual_count == len(expected_tag_names) assert sorted(actual_tag_names) == sorted(expected_tag_names) def test_filter_anonymous_starting_with_colon(verify_unpaged, tag_factory): - db.session.add(tag_factory(names=[':t'])) + db.session.add(tag_factory(names=[":t"])) db.session.flush() with pytest.raises(errors.SearchError): - verify_unpaged(':t', [':t']) - verify_unpaged('\\:t', [':t']) - - -@pytest.mark.parametrize('input,expected_tag_names', [ - ('name:tag1', ['tag1']), - ('name:tag2', ['tag2']), - ('name:none', []), - ('name:', []), - ('name:*1', ['tag1']), - ('name:*2', ['tag2']), - ('name:*', ['tag1', 'tag2', 'tag3', 'tag4']), - ('name:t*', ['tag1', 'tag2', 'tag3', 'tag4']), - ('name:*a*', ['tag1', 'tag2', 'tag3', 'tag4']), - ('name:*!*', []), - ('name:!*', []), - ('name:*!', []), - ('-name:tag1', ['tag2', 'tag3', 'tag4']), - ('-name:tag2', ['tag1', 'tag3', 'tag4']), - ('name:tag1,tag2', ['tag1', 'tag2']), - ('-name:tag1,tag3', ['tag2', 'tag4']), - ('name:tag4', ['tag4']), - ('name:tag5', ['tag4']), - ('name:tag4,tag5', ['tag4']), -]) + verify_unpaged(":t", [":t"]) + verify_unpaged("\\:t", [":t"]) + + +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("name:tag1", ["tag1"]), + ("name:tag2", ["tag2"]), + ("name:none", []), + ("name:", []), + ("name:*1", ["tag1"]), + ("name:*2", ["tag2"]), + ("name:*", ["tag1", "tag2", "tag3", "tag4"]), + ("name:t*", ["tag1", "tag2", "tag3", "tag4"]), + ("name:*a*", ["tag1", "tag2", "tag3", "tag4"]), + ("name:*!*", []), + ("name:!*", []), + ("name:*!", []), + ("-name:tag1", ["tag2", "tag3", "tag4"]), + ("-name:tag2", ["tag1", "tag3", "tag4"]), + ("name:tag1,tag2", ["tag1", "tag2"]), + ("-name:tag1,tag3", ["tag2", "tag4"]), + ("name:tag4", ["tag4"]), + ("name:tag5", ["tag4"]), + ("name:tag4,tag5", ["tag4"]), + ], +) def test_filter_by_name( - verify_unpaged, tag_factory, input, expected_tag_names): - db.session.add(tag_factory(names=['tag1'])) - db.session.add(tag_factory(names=['tag2'])) - db.session.add(tag_factory(names=['tag3'])) - db.session.add(tag_factory(names=['tag4', 'tag5', 'tag6'])) + verify_unpaged, tag_factory, input, expected_tag_names +): + db.session.add(tag_factory(names=["tag1"])) + db.session.add(tag_factory(names=["tag2"])) + db.session.add(tag_factory(names=["tag3"])) + db.session.add(tag_factory(names=["tag4", "tag5", "tag6"])) db.session.flush() verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('category:cat1', ['t1', 't2']), - ('category:cat2', ['t3']), - ('category:cat1,cat2', ['t1', 't2', 't3']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("category:cat1", ["t1", "t2"]), + ("category:cat2", ["t3"]), + ("category:cat1,cat2", ["t1", "t2", "t3"]), + ], +) def test_filter_by_category( - verify_unpaged, - tag_factory, - tag_category_factory, - input, - expected_tag_names): - cat1 = tag_category_factory(name='cat1') - cat2 = tag_category_factory(name='cat2') - tag1 = tag_factory(names=['t1'], category=cat1) - tag2 = tag_factory(names=['t2'], category=cat1) - tag3 = tag_factory(names=['t3'], category=cat2) + verify_unpaged, + tag_factory, + tag_category_factory, + input, + expected_tag_names, +): + cat1 = tag_category_factory(name="cat1") + cat2 = tag_category_factory(name="cat2") + tag1 = tag_factory(names=["t1"], category=cat1) + tag2 = tag_factory(names=["t2"], category=cat1) + tag3 = tag_factory(names=["t3"], category=cat2) db.session.add_all([tag1, tag2, tag3]) db.session.flush() verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('creation-time:2014', ['t1', 't2']), - ('creation-date:2014', ['t1', 't2']), - ('-creation-time:2014', ['t3']), - ('-creation-date:2014', ['t3']), - ('creation-time:2014..2014-06', ['t1', 't2']), - ('creation-time:2014-06..2015-01-01', ['t2', 't3']), - ('creation-time:2014-06..', ['t2', 't3']), - ('creation-time:..2014-06', ['t1', 't2']), - ('-creation-time:2014..2014-06', ['t3']), - ('-creation-time:2014-06..2015-01-01', ['t1']), - ('creation-date:2014..2014-06', ['t1', 't2']), - ('creation-date:2014-06..2015-01-01', ['t2', 't3']), - ('creation-date:2014-06..', ['t2', 't3']), - ('creation-date:..2014-06', ['t1', 't2']), - ('-creation-date:2014..2014-06', ['t3']), - ('-creation-date:2014-06..2015-01-01', ['t1']), - ('creation-time:2014-01,2015', ['t1', 't3']), - ('creation-date:2014-01,2015', ['t1', 't3']), - ('-creation-time:2014-01,2015', ['t2']), - ('-creation-date:2014-01,2015', ['t2']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("creation-time:2014", ["t1", "t2"]), + ("creation-date:2014", ["t1", "t2"]), + ("-creation-time:2014", ["t3"]), + ("-creation-date:2014", ["t3"]), + ("creation-time:2014..2014-06", ["t1", "t2"]), + ("creation-time:2014-06..2015-01-01", ["t2", "t3"]), + ("creation-time:2014-06..", ["t2", "t3"]), + ("creation-time:..2014-06", ["t1", "t2"]), + ("-creation-time:2014..2014-06", ["t3"]), + ("-creation-time:2014-06..2015-01-01", ["t1"]), + ("creation-date:2014..2014-06", ["t1", "t2"]), + ("creation-date:2014-06..2015-01-01", ["t2", "t3"]), + ("creation-date:2014-06..", ["t2", "t3"]), + ("creation-date:..2014-06", ["t1", "t2"]), + ("-creation-date:2014..2014-06", ["t3"]), + ("-creation-date:2014-06..2015-01-01", ["t1"]), + ("creation-time:2014-01,2015", ["t1", "t3"]), + ("creation-date:2014-01,2015", ["t1", "t3"]), + ("-creation-time:2014-01,2015", ["t2"]), + ("-creation-date:2014-01,2015", ["t2"]), + ], +) def test_filter_by_creation_time( - verify_unpaged, tag_factory, input, expected_tag_names): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) - tag3 = tag_factory(names=['t3']) + verify_unpaged, tag_factory, input, expected_tag_names +): + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) + tag3 = tag_factory(names=["t3"]) tag1.creation_time = datetime(2014, 1, 1) tag2.creation_time = datetime(2014, 6, 1) tag3.creation_time = datetime(2015, 1, 1) @@ -193,17 +235,21 @@ def test_filter_by_creation_time( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('last-edit-date:2014', ['t1', 't3']), - ('last-edit-time:2014', ['t1', 't3']), - ('edit-date:2014', ['t1', 't3']), - ('edit-time:2014', ['t1', 't3']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("last-edit-date:2014", ["t1", "t3"]), + ("last-edit-time:2014", ["t1", "t3"]), + ("edit-date:2014", ["t1", "t3"]), + ("edit-time:2014", ["t1", "t3"]), + ], +) def test_filter_by_edit_time( - verify_unpaged, tag_factory, input, expected_tag_names): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) - tag3 = tag_factory(names=['t3']) + verify_unpaged, tag_factory, input, expected_tag_names +): + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) + tag3 = tag_factory(names=["t3"]) tag1.last_edit_time = datetime(2014, 1, 1) tag2.last_edit_time = datetime(2015, 1, 1) tag3.last_edit_time = datetime(2014, 1, 1) @@ -212,24 +258,28 @@ def test_filter_by_edit_time( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('post-count:2', ['t1']), - ('post-count:1', ['t2']), - ('post-count:1..', ['t1', 't2']), - ('post-count-min:1', ['t1', 't2']), - ('post-count:..1', ['t2']), - ('post-count-max:1', ['t2']), - ('usage-count:2', ['t1']), - ('usage-count:1', ['t2']), - ('usages:2', ['t1']), - ('usages:1', ['t2']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("post-count:2", ["t1"]), + ("post-count:1", ["t2"]), + ("post-count:1..", ["t1", "t2"]), + ("post-count-min:1", ["t1", "t2"]), + ("post-count:..1", ["t2"]), + ("post-count-max:1", ["t2"]), + ("usage-count:2", ["t1"]), + ("usage-count:1", ["t2"]), + ("usages:2", ["t1"]), + ("usages:1", ["t2"]), + ], +) def test_filter_by_post_count( - verify_unpaged, tag_factory, post_factory, input, expected_tag_names): + verify_unpaged, tag_factory, post_factory, input, expected_tag_names +): post1 = post_factory() post2 = post_factory() - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) db.session.add_all([post1, post2, tag1, tag2]) post1.tags.append(tag1) post1.tags.append(tag2) @@ -238,31 +288,38 @@ def test_filter_by_post_count( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input', [ - 'post-count:..', - 'post-count:asd', - 'post-count:asd,1', - 'post-count:1,asd', - 'post-count:asd..1', - 'post-count:1..asd', -]) +@pytest.mark.parametrize( + "input", + [ + "post-count:..", + "post-count:asd", + "post-count:asd,1", + "post-count:1,asd", + "post-count:asd..1", + "post-count:1..asd", + ], +) def test_filter_by_invalid_input(executor, input): with pytest.raises(errors.SearchError): executor.execute(input, offset=0, limit=100) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('suggestion-count:2', ['t1']), - ('suggestion-count:1', ['t2']), - ('suggestion-count:0', ['sug1', 'sug2', 'sug3']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("suggestion-count:2", ["t1"]), + ("suggestion-count:1", ["t2"]), + ("suggestion-count:0", ["sug1", "sug2", "sug3"]), + ], +) def test_filter_by_suggestion_count( - verify_unpaged, tag_factory, input, expected_tag_names): - sug1 = tag_factory(names=['sug1']) - sug2 = tag_factory(names=['sug2']) - sug3 = tag_factory(names=['sug3']) - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + verify_unpaged, tag_factory, input, expected_tag_names +): + sug1 = tag_factory(names=["sug1"]) + sug2 = tag_factory(names=["sug2"]) + sug3 = tag_factory(names=["sug3"]) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) db.session.add_all([sug1, sug3, tag2, sug2, tag1]) tag1.suggestions.append(sug1) tag1.suggestions.append(sug2) @@ -271,18 +328,22 @@ def test_filter_by_suggestion_count( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('implication-count:2', ['t1']), - ('implication-count:1', ['t2']), - ('implication-count:0', ['sug1', 'sug2', 'sug3']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("implication-count:2", ["t1"]), + ("implication-count:1", ["t2"]), + ("implication-count:0", ["sug1", "sug2", "sug3"]), + ], +) def test_filter_by_implication_count( - verify_unpaged, tag_factory, input, expected_tag_names): - sug1 = tag_factory(names=['sug1']) - sug2 = tag_factory(names=['sug2']) - sug3 = tag_factory(names=['sug3']) - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + verify_unpaged, tag_factory, input, expected_tag_names +): + sug1 = tag_factory(names=["sug1"]) + sug2 = tag_factory(names=["sug2"]) + sug3 = tag_factory(names=["sug3"]) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) db.session.add_all([sug1, sug3, tag2, sug2, tag1]) tag1.implications.append(sug1) tag1.implications.append(sug2) @@ -291,32 +352,39 @@ def test_filter_by_implication_count( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('', ['t1', 't2']), - ('sort:name', ['t1', 't2']), - ('-sort:name', ['t2', 't1']), - ('sort:name,asc', ['t1', 't2']), - ('sort:name,desc', ['t2', 't1']), - ('-sort:name,asc', ['t2', 't1']), - ('-sort:name,desc', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("", ["t1", "t2"]), + ("sort:name", ["t1", "t2"]), + ("-sort:name", ["t2", "t1"]), + ("sort:name,asc", ["t1", "t2"]), + ("sort:name,desc", ["t2", "t1"]), + ("-sort:name,asc", ["t2", "t1"]), + ("-sort:name,desc", ["t1", "t2"]), + ], +) def test_sort_by_name(verify_unpaged, tag_factory, input, expected_tag_names): - db.session.add(tag_factory(names=['t2'])) - db.session.add(tag_factory(names=['t1'])) + db.session.add(tag_factory(names=["t2"])) + db.session.add(tag_factory(names=["t1"])) db.session.flush() verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('', ['t1', 't2', 't3']), - ('sort:creation-date', ['t3', 't2', 't1']), - ('sort:creation-time', ['t3', 't2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("", ["t1", "t2", "t3"]), + ("sort:creation-date", ["t3", "t2", "t1"]), + ("sort:creation-time", ["t3", "t2", "t1"]), + ], +) def test_sort_by_creation_time( - verify_unpaged, tag_factory, input, expected_tag_names): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) - tag3 = tag_factory(names=['t3']) + verify_unpaged, tag_factory, input, expected_tag_names +): + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) + tag3 = tag_factory(names=["t3"]) tag1.creation_time = datetime(1991, 1, 1) tag2.creation_time = datetime(1991, 1, 2) tag3.creation_time = datetime(1991, 1, 3) @@ -325,18 +393,22 @@ def test_sort_by_creation_time( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('', ['t1', 't2', 't3']), - ('sort:last-edit-date', ['t3', 't2', 't1']), - ('sort:last-edit-time', ['t3', 't2', 't1']), - ('sort:edit-date', ['t3', 't2', 't1']), - ('sort:edit-time', ['t3', 't2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("", ["t1", "t2", "t3"]), + ("sort:last-edit-date", ["t3", "t2", "t1"]), + ("sort:last-edit-time", ["t3", "t2", "t1"]), + ("sort:edit-date", ["t3", "t2", "t1"]), + ("sort:edit-time", ["t3", "t2", "t1"]), + ], +) def test_sort_by_last_edit_time( - verify_unpaged, tag_factory, input, expected_tag_names): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) - tag3 = tag_factory(names=['t3']) + verify_unpaged, tag_factory, input, expected_tag_names +): + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) + tag3 = tag_factory(names=["t3"]) tag1.last_edit_time = datetime(1991, 1, 1) tag2.last_edit_time = datetime(1991, 1, 2) tag3.last_edit_time = datetime(1991, 1, 3) @@ -345,17 +417,21 @@ def test_sort_by_last_edit_time( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('sort:post-count', ['t2', 't1']), - ('sort:usage-count', ['t2', 't1']), - ('sort:usages', ['t2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("sort:post-count", ["t2", "t1"]), + ("sort:usage-count", ["t2", "t1"]), + ("sort:usages", ["t2", "t1"]), + ], +) def test_sort_by_post_count( - verify_unpaged, tag_factory, post_factory, input, expected_tag_names): + verify_unpaged, tag_factory, post_factory, input, expected_tag_names +): post1 = post_factory() post2 = post_factory() - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) db.session.add_all([post1, post2, tag1, tag2]) post1.tags.append(tag1) post1.tags.append(tag2) @@ -364,16 +440,18 @@ def test_sort_by_post_count( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('sort:suggestion-count', ['t1', 't2', 'sug1', 'sug2', 'sug3']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [("sort:suggestion-count", ["t1", "t2", "sug1", "sug2", "sug3"]),], +) def test_sort_by_suggestion_count( - verify_unpaged, tag_factory, input, expected_tag_names): - sug1 = tag_factory(names=['sug1']) - sug2 = tag_factory(names=['sug2']) - sug3 = tag_factory(names=['sug3']) - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + verify_unpaged, tag_factory, input, expected_tag_names +): + sug1 = tag_factory(names=["sug1"]) + sug2 = tag_factory(names=["sug2"]) + sug3 = tag_factory(names=["sug3"]) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) db.session.add_all([sug1, sug3, tag2, sug2, tag1]) tag1.suggestions.append(sug1) tag1.suggestions.append(sug2) @@ -382,16 +460,18 @@ def test_sort_by_suggestion_count( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('sort:implication-count', ['t1', 't2', 'sug1', 'sug2', 'sug3']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [("sort:implication-count", ["t1", "t2", "sug1", "sug2", "sug3"]),], +) def test_sort_by_implication_count( - verify_unpaged, tag_factory, input, expected_tag_names): - sug1 = tag_factory(names=['sug1']) - sug2 = tag_factory(names=['sug2']) - sug3 = tag_factory(names=['sug3']) - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + verify_unpaged, tag_factory, input, expected_tag_names +): + sug1 = tag_factory(names=["sug1"]) + sug2 = tag_factory(names=["sug2"]) + sug3 = tag_factory(names=["sug3"]) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) db.session.add_all([sug1, sug3, tag2, sug2, tag1]) tag1.implications.append(sug1) tag1.implications.append(sug2) @@ -400,20 +480,21 @@ def test_sort_by_implication_count( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('sort:category', ['t3', 't1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", [("sort:category", ["t3", "t1", "t2"]),] +) def test_sort_by_category( - verify_unpaged, - tag_factory, - tag_category_factory, - input, - expected_tag_names): - cat1 = tag_category_factory(name='cat1') - cat2 = tag_category_factory(name='cat2') - tag1 = tag_factory(names=['t1'], category=cat2) - tag2 = tag_factory(names=['t2'], category=cat2) - tag3 = tag_factory(names=['t3'], category=cat1) + verify_unpaged, + tag_factory, + tag_category_factory, + input, + expected_tag_names, +): + cat1 = tag_category_factory(name="cat1") + cat2 = tag_category_factory(name="cat2") + tag1 = tag_factory(names=["t1"], category=cat2) + tag2 = tag_factory(names=["t2"], category=cat2) + tag3 = tag_factory(names=["t3"], category=cat1) db.session.add_all([tag1, tag2, tag3]) db.session.flush() verify_unpaged(input, expected_tag_names) diff --git a/server/szurubooru/tests/search/configs/test_user_search_config.py b/server/szurubooru/tests/search/configs/test_user_search_config.py index e9cea5a..485ab1c 100644 --- a/server/szurubooru/tests/search/configs/test_user_search_config.py +++ b/server/szurubooru/tests/search/configs/test_user_search_config.py @@ -1,5 +1,7 @@ from datetime import datetime + import pytest + from szurubooru import db, errors, search @@ -12,42 +14,48 @@ def executor(): def verify_unpaged(executor): def verify(input, expected_user_names): actual_count, actual_users = executor.execute( - input, offset=0, limit=100) + input, offset=0, limit=100 + ) actual_user_names = [u.name for u in actual_users] assert actual_count == len(expected_user_names) assert actual_user_names == expected_user_names + return verify -@pytest.mark.parametrize('input,expected_user_names', [ - ('creation-time:2014', ['u1', 'u2']), - ('creation-date:2014', ['u1', 'u2']), - ('-creation-time:2014', ['u3']), - ('-creation-date:2014', ['u3']), - ('creation-time:2014..2014-06', ['u1', 'u2']), - ('creation-time:2014-06..2015-01-01', ['u2', 'u3']), - ('creation-time:2014-06..', ['u2', 'u3']), - ('creation-time:..2014-06', ['u1', 'u2']), - ('creation-time-min:2014-06', ['u2', 'u3']), - ('creation-time-max:2014-06', ['u1', 'u2']), - ('-creation-time:2014..2014-06', ['u3']), - ('-creation-time:2014-06..2015-01-01', ['u1']), - ('creation-date:2014..2014-06', ['u1', 'u2']), - ('creation-date:2014-06..2015-01-01', ['u2', 'u3']), - ('creation-date:2014-06..', ['u2', 'u3']), - ('creation-date:..2014-06', ['u1', 'u2']), - ('-creation-date:2014..2014-06', ['u3']), - ('-creation-date:2014-06..2015-01-01', ['u1']), - ('creation-time:2014-01,2015', ['u1', 'u3']), - ('creation-date:2014-01,2015', ['u1', 'u3']), - ('-creation-time:2014-01,2015', ['u2']), - ('-creation-date:2014-01,2015', ['u2']), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("creation-time:2014", ["u1", "u2"]), + ("creation-date:2014", ["u1", "u2"]), + ("-creation-time:2014", ["u3"]), + ("-creation-date:2014", ["u3"]), + ("creation-time:2014..2014-06", ["u1", "u2"]), + ("creation-time:2014-06..2015-01-01", ["u2", "u3"]), + ("creation-time:2014-06..", ["u2", "u3"]), + ("creation-time:..2014-06", ["u1", "u2"]), + ("creation-time-min:2014-06", ["u2", "u3"]), + ("creation-time-max:2014-06", ["u1", "u2"]), + ("-creation-time:2014..2014-06", ["u3"]), + ("-creation-time:2014-06..2015-01-01", ["u1"]), + ("creation-date:2014..2014-06", ["u1", "u2"]), + ("creation-date:2014-06..2015-01-01", ["u2", "u3"]), + ("creation-date:2014-06..", ["u2", "u3"]), + ("creation-date:..2014-06", ["u1", "u2"]), + ("-creation-date:2014..2014-06", ["u3"]), + ("-creation-date:2014-06..2015-01-01", ["u1"]), + ("creation-time:2014-01,2015", ["u1", "u3"]), + ("creation-date:2014-01,2015", ["u1", "u3"]), + ("-creation-time:2014-01,2015", ["u2"]), + ("-creation-date:2014-01,2015", ["u2"]), + ], +) def test_filter_by_creation_time( - verify_unpaged, input, expected_user_names, user_factory): - user1 = user_factory(name='u1') - user2 = user_factory(name='u2') - user3 = user_factory(name='u3') + verify_unpaged, input, expected_user_names, user_factory +): + user1 = user_factory(name="u1") + user2 = user_factory(name="u2") + user3 = user_factory(name="u3") user1.creation_time = datetime(2014, 1, 1) user2.creation_time = datetime(2014, 6, 1) user3.creation_time = datetime(2015, 1, 1) @@ -56,59 +64,67 @@ def test_filter_by_creation_time( verify_unpaged(input, expected_user_names) -@pytest.mark.parametrize('input,expected_user_names', [ - ('name:user1', ['user1']), - ('name:user2', ['user2']), - ('name:none', []), - ('name:', []), - ('name:*1', ['user1']), - ('name:*2', ['user2']), - ('name:*', ['user1', 'user2', 'user3']), - ('name:u*', ['user1', 'user2', 'user3']), - ('name:*ser*', ['user1', 'user2', 'user3']), - ('name:*zer*', []), - ('name:zer*', []), - ('name:*zer', []), - ('-name:user1', ['user2', 'user3']), - ('-name:user2', ['user1', 'user3']), - ('name:user1,user2', ['user1', 'user2']), - ('-name:user1,user3', ['user2']), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("name:user1", ["user1"]), + ("name:user2", ["user2"]), + ("name:none", []), + ("name:", []), + ("name:*1", ["user1"]), + ("name:*2", ["user2"]), + ("name:*", ["user1", "user2", "user3"]), + ("name:u*", ["user1", "user2", "user3"]), + ("name:*ser*", ["user1", "user2", "user3"]), + ("name:*zer*", []), + ("name:zer*", []), + ("name:*zer", []), + ("-name:user1", ["user2", "user3"]), + ("-name:user2", ["user1", "user3"]), + ("name:user1,user2", ["user1", "user2"]), + ("-name:user1,user3", ["user2"]), + ], +) def test_filter_by_name( - verify_unpaged, input, expected_user_names, user_factory): - db.session.add(user_factory(name='user1')) - db.session.add(user_factory(name='user2')) - db.session.add(user_factory(name='user3')) + verify_unpaged, input, expected_user_names, user_factory +): + db.session.add(user_factory(name="user1")) + db.session.add(user_factory(name="user2")) + db.session.add(user_factory(name="user3")) db.session.flush() verify_unpaged(input, expected_user_names) -@pytest.mark.parametrize('input,expected_user_names', [ - ('name:u1', ['u1']), - ('name:u2*', ['u2..']), - ('name:u1,u3..x', ['u1', 'u3..x']), - ('name:u2..', None), - ('name:*..*', None), - ('name:u3..x', None), - ('name:*..x', None), - ('name:u2\\..', ['u2..']), - ('name:*\\..*', ['u2..', 'u3..x']), - ('name:u3\\..x', ['u3..x']), - ('name:*\\..x', ['u3..x']), - ('name:u2.\\.', ['u2..']), - ('name:*.\\.*', ['u2..', 'u3..x']), - ('name:u3.\\.x', ['u3..x']), - ('name:*.\\.x', ['u3..x']), - ('name:u2\\.\\.', ['u2..']), - ('name:*\\.\\.*', ['u2..', 'u3..x']), - ('name:u3\\.\\.x', ['u3..x']), - ('name:*\\.\\.x', ['u3..x']), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("name:u1", ["u1"]), + ("name:u2*", ["u2.."]), + ("name:u1,u3..x", ["u1", "u3..x"]), + ("name:u2..", None), + ("name:*..*", None), + ("name:u3..x", None), + ("name:*..x", None), + ("name:u2\\..", ["u2.."]), + ("name:*\\..*", ["u2..", "u3..x"]), + ("name:u3\\..x", ["u3..x"]), + ("name:*\\..x", ["u3..x"]), + ("name:u2.\\.", ["u2.."]), + ("name:*.\\.*", ["u2..", "u3..x"]), + ("name:u3.\\.x", ["u3..x"]), + ("name:*.\\.x", ["u3..x"]), + ("name:u2\\.\\.", ["u2.."]), + ("name:*\\.\\.*", ["u2..", "u3..x"]), + ("name:u3\\.\\.x", ["u3..x"]), + ("name:*\\.\\.x", ["u3..x"]), + ], +) def test_filter_by_name_that_looks_like_range( - verify_unpaged, input, expected_user_names, user_factory): - db.session.add(user_factory(name='u1')) - db.session.add(user_factory(name='u2..')) - db.session.add(user_factory(name='u3..x')) + verify_unpaged, input, expected_user_names, user_factory +): + db.session.add(user_factory(name="u1")) + db.session.add(user_factory(name="u2..")) + db.session.add(user_factory(name="u3..x")) db.session.flush() if not expected_user_names: with pytest.raises(errors.SearchError): @@ -117,30 +133,36 @@ def test_filter_by_name_that_looks_like_range( verify_unpaged(input, expected_user_names) -@pytest.mark.parametrize('input,expected_user_names', [ - ('', ['u1', 'u2']), - ('u1', ['u1']), - ('u2', ['u2']), - ('u1,u2', ['u1', 'u2']), -]) -def test_anonymous( - verify_unpaged, input, expected_user_names, user_factory): - db.session.add(user_factory(name='u1')) - db.session.add(user_factory(name='u2')) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("", ["u1", "u2"]), + ("u1", ["u1"]), + ("u2", ["u2"]), + ("u1,u2", ["u1", "u2"]), + ], +) +def test_anonymous(verify_unpaged, input, expected_user_names, user_factory): + db.session.add(user_factory(name="u1")) + db.session.add(user_factory(name="u2")) db.session.flush() verify_unpaged(input, expected_user_names) -@pytest.mark.parametrize('input,expected_user_names', [ - ('creation-time:2014 u1', ['u1']), - ('creation-time:2014 u2', ['u2']), - ('creation-time:2016 u2', []), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("creation-time:2014 u1", ["u1"]), + ("creation-time:2014 u2", ["u2"]), + ("creation-time:2016 u2", []), + ], +) def test_combining_tokens( - verify_unpaged, input, expected_user_names, user_factory): - user1 = user_factory(name='u1') - user2 = user_factory(name='u2') - user3 = user_factory(name='u3') + verify_unpaged, input, expected_user_names, user_factory +): + user1 = user_factory(name="u1") + user2 = user_factory(name="u2") + user3 = user_factory(name="u3") user1.creation_time = datetime(2014, 1, 1) user2.creation_time = datetime(2014, 6, 1) user3.creation_time = datetime(2015, 1, 1) @@ -150,61 +172,77 @@ def test_combining_tokens( @pytest.mark.parametrize( - 'offset,limit,expected_total_count,expected_user_names', [ - (0, 1, 2, ['u1']), - (1, 1, 2, ['u2']), + "offset,limit,expected_total_count,expected_user_names", + [ + (0, 1, 2, ["u1"]), + (1, 1, 2, ["u2"]), (2, 1, 2, []), (-1, 1, 2, []), - (-1, 2, 2, ['u1']), - (0, 2, 2, ['u1', 'u2']), + (-1, 2, 2, ["u1"]), + (0, 2, 2, ["u1", "u2"]), (3, 1, 2, []), (0, 0, 2, []), - ]) + ], +) def test_paging( - executor, user_factory, offset, limit, - expected_total_count, expected_user_names): - db.session.add(user_factory(name='u1')) - db.session.add(user_factory(name='u2')) + executor, + user_factory, + offset, + limit, + expected_total_count, + expected_user_names, +): + db.session.add(user_factory(name="u1")) + db.session.add(user_factory(name="u2")) db.session.flush() actual_count, actual_users = executor.execute( - '', offset=offset, limit=limit) + "", offset=offset, limit=limit + ) actual_user_names = [u.name for u in actual_users] assert actual_count == expected_total_count assert actual_user_names == expected_user_names -@pytest.mark.parametrize('input,expected_user_names', [ - ('', ['u1', 'u2']), - ('sort:name', ['u1', 'u2']), - ('-sort:name', ['u2', 'u1']), - ('sort:name,asc', ['u1', 'u2']), - ('sort:name,desc', ['u2', 'u1']), - ('-sort:name,asc', ['u2', 'u1']), - ('-sort:name,desc', ['u1', 'u2']), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("", ["u1", "u2"]), + ("sort:name", ["u1", "u2"]), + ("-sort:name", ["u2", "u1"]), + ("sort:name,asc", ["u1", "u2"]), + ("sort:name,desc", ["u2", "u1"]), + ("-sort:name,asc", ["u2", "u1"]), + ("-sort:name,desc", ["u1", "u2"]), + ], +) def test_sort_by_name( - verify_unpaged, input, expected_user_names, user_factory): - db.session.add(user_factory(name='u2')) - db.session.add(user_factory(name='u1')) + verify_unpaged, input, expected_user_names, user_factory +): + db.session.add(user_factory(name="u2")) + db.session.add(user_factory(name="u1")) db.session.flush() verify_unpaged(input, expected_user_names) -@pytest.mark.parametrize('input,expected_user_names', [ - ('', ['u1', 'u2', 'u3']), - ('sort:creation-date', ['u3', 'u2', 'u1']), - ('sort:creation-time', ['u3', 'u2', 'u1']), - ('-sort:creation-date', ['u1', 'u2', 'u3']), - ('sort:creation-date,asc', ['u1', 'u2', 'u3']), - ('sort:creation-date,desc', ['u3', 'u2', 'u1']), - ('-sort:creation-date,asc', ['u3', 'u2', 'u1']), - ('-sort:creation-date,desc', ['u1', 'u2', 'u3']), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("", ["u1", "u2", "u3"]), + ("sort:creation-date", ["u3", "u2", "u1"]), + ("sort:creation-time", ["u3", "u2", "u1"]), + ("-sort:creation-date", ["u1", "u2", "u3"]), + ("sort:creation-date,asc", ["u1", "u2", "u3"]), + ("sort:creation-date,desc", ["u3", "u2", "u1"]), + ("-sort:creation-date,asc", ["u3", "u2", "u1"]), + ("-sort:creation-date,desc", ["u1", "u2", "u3"]), + ], +) def test_sort_by_creation_time( - verify_unpaged, input, expected_user_names, user_factory): - user1 = user_factory(name='u1') - user2 = user_factory(name='u2') - user3 = user_factory(name='u3') + verify_unpaged, input, expected_user_names, user_factory +): + user1 = user_factory(name="u1") + user2 = user_factory(name="u2") + user3 = user_factory(name="u3") user1.creation_time = datetime(1991, 1, 1) user2.creation_time = datetime(1991, 1, 2) user3.creation_time = datetime(1991, 1, 3) @@ -213,18 +251,22 @@ def test_sort_by_creation_time( verify_unpaged(input, expected_user_names) -@pytest.mark.parametrize('input,expected_user_names', [ - ('', ['u1', 'u2', 'u3']), - ('sort:last-login-date', ['u3', 'u2', 'u1']), - ('sort:last-login-time', ['u3', 'u2', 'u1']), - ('sort:login-date', ['u3', 'u2', 'u1']), - ('sort:login-time', ['u3', 'u2', 'u1']), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("", ["u1", "u2", "u3"]), + ("sort:last-login-date", ["u3", "u2", "u1"]), + ("sort:last-login-time", ["u3", "u2", "u1"]), + ("sort:login-date", ["u3", "u2", "u1"]), + ("sort:login-time", ["u3", "u2", "u1"]), + ], +) def test_sort_by_last_login_time( - verify_unpaged, input, expected_user_names, user_factory): - user1 = user_factory(name='u1') - user2 = user_factory(name='u2') - user3 = user_factory(name='u3') + verify_unpaged, input, expected_user_names, user_factory +): + user1 = user_factory(name="u1") + user2 = user_factory(name="u2") + user3 = user_factory(name="u3") user1.last_login_time = datetime(1991, 1, 1) user2.last_login_time = datetime(1991, 1, 2) user3.last_login_time = datetime(1991, 1, 3) @@ -234,39 +276,43 @@ def test_sort_by_last_login_time( def test_random_sort(executor, user_factory): - user1 = user_factory(name='u1') - user2 = user_factory(name='u2') - user3 = user_factory(name='u3') + user1 = user_factory(name="u1") + user2 = user_factory(name="u2") + user3 = user_factory(name="u3") db.session.add_all([user3, user1, user2]) db.session.flush() actual_count, actual_users = executor.execute( - 'sort:random', offset=0, limit=100) + "sort:random", offset=0, limit=100 + ) actual_user_names = [u.name for u in actual_users] assert actual_count == 3 assert len(actual_user_names) == 3 - assert 'u1' in actual_user_names - assert 'u2' in actual_user_names - assert 'u3' in actual_user_names + assert "u1" in actual_user_names + assert "u2" in actual_user_names + assert "u3" in actual_user_names -@pytest.mark.parametrize('input,expected_error', [ - ('creation-date:..', errors.SearchError), - ('creation-date-min:..', errors.ValidationError), - ('creation-date-min:..2014-01-01', errors.ValidationError), - ('creation-date-min:2014-01-01..', errors.ValidationError), - ('creation-date-max:..2014-01-01', errors.ValidationError), - ('creation-date-max:2014-01-01..', errors.ValidationError), - ('creation-date-max:yesterday,today', errors.ValidationError), - ('creation-date:bad..', errors.ValidationError), - ('creation-date:..bad', errors.ValidationError), - ('creation-date:bad..bad', errors.ValidationError), - ('sort:', errors.SearchError), - ('sort:nam', errors.SearchError), - ('sort:name,as', errors.SearchError), - ('sort:name,asc,desc', errors.SearchError), - ('bad:x', errors.SearchError), - ('special:unsupported', errors.SearchError), -]) +@pytest.mark.parametrize( + "input,expected_error", + [ + ("creation-date:..", errors.SearchError), + ("creation-date-min:..", errors.ValidationError), + ("creation-date-min:..2014-01-01", errors.ValidationError), + ("creation-date-min:2014-01-01..", errors.ValidationError), + ("creation-date-max:..2014-01-01", errors.ValidationError), + ("creation-date-max:2014-01-01..", errors.ValidationError), + ("creation-date-max:yesterday,today", errors.ValidationError), + ("creation-date:bad..", errors.ValidationError), + ("creation-date:..bad", errors.ValidationError), + ("creation-date:bad..bad", errors.ValidationError), + ("sort:", errors.SearchError), + ("sort:nam", errors.SearchError), + ("sort:name,as", errors.SearchError), + ("sort:name,asc,desc", errors.SearchError), + ("bad:x", errors.SearchError), + ("special:unsupported", errors.SearchError), + ], +) def test_bad_tokens(executor, input, expected_error): with pytest.raises(expected_error): executor.execute(input, offset=0, limit=100) diff --git a/server/szurubooru/tests/search/test_executor.py b/server/szurubooru/tests/search/test_executor.py index e1b2dac..4530bee 100644 --- a/server/szurubooru/tests/search/test_executor.py +++ b/server/szurubooru/tests/search/test_executor.py @@ -1,23 +1,27 @@ import unittest.mock + import pytest + from szurubooru import search from szurubooru.func import cache def test_retrieving_from_cache(): config = unittest.mock.MagicMock() - with unittest.mock.patch('szurubooru.func.cache.has'), \ - unittest.mock.patch('szurubooru.func.cache.get'): + with unittest.mock.patch("szurubooru.func.cache.has"), unittest.mock.patch( + "szurubooru.func.cache.get" + ): cache.has.side_effect = lambda *args: True executor = search.Executor(config) - executor.execute('test:whatever', 1, 10) + executor.execute("test:whatever", 1, 10) assert cache.get.called def test_putting_equivalent_queries_into_cache(): config = search.configs.PostSearchConfig() - with unittest.mock.patch('szurubooru.func.cache.has'), \ - unittest.mock.patch('szurubooru.func.cache.put'): + with unittest.mock.patch("szurubooru.func.cache.has"), unittest.mock.patch( + "szurubooru.func.cache.put" + ): hashes = [] def appender(key, _value): @@ -26,20 +30,21 @@ def test_putting_equivalent_queries_into_cache(): cache.has.side_effect = lambda *args: False cache.put.side_effect = appender executor = search.Executor(config) - executor.execute('safety:safe test', 1, 10) - executor.execute('safety:safe test', 1, 10) - executor.execute('safety:safe test ', 1, 10) - executor.execute(' safety:safe test', 1, 10) - executor.execute(' SAFETY:safe test', 1, 10) - executor.execute('test safety:safe', 1, 10) + executor.execute("safety:safe test", 1, 10) + executor.execute("safety:safe test", 1, 10) + executor.execute("safety:safe test ", 1, 10) + executor.execute(" safety:safe test", 1, 10) + executor.execute(" SAFETY:safe test", 1, 10) + executor.execute("test safety:safe", 1, 10) assert len(hashes) == 6 assert len(set(hashes)) == 1 def test_putting_non_equivalent_queries_into_cache(): config = search.configs.PostSearchConfig() - with unittest.mock.patch('szurubooru.func.cache.has'), \ - unittest.mock.patch('szurubooru.func.cache.put'): + with unittest.mock.patch("szurubooru.func.cache.has"), unittest.mock.patch( + "szurubooru.func.cache.put" + ): hashes = [] def appender(key, _value): @@ -49,42 +54,42 @@ def test_putting_non_equivalent_queries_into_cache(): cache.put.side_effect = appender executor = search.Executor(config) args = [ - ('', 1, 10), - ('creation-time:2016', 1, 10), - ('creation-time:2015', 1, 10), - ('creation-time:2016-01', 1, 10), - ('creation-time:2016-02', 1, 10), - ('creation-time:2016-01-01', 1, 10), - ('creation-time:2016-01-02', 1, 10), - ('tag-count:1,3', 1, 10), - ('tag-count:1,2', 1, 10), - ('tag-count:1', 1, 10), - ('tag-count:1..3', 1, 10), - ('tag-count:1..4', 1, 10), - ('tag-count:2..3', 1, 10), - ('tag-count:1..', 1, 10), - ('tag-count:2..', 1, 10), - ('tag-count:..3', 1, 10), - ('tag-count:..4', 1, 10), - ('-tag-count:1..3', 1, 10), - ('-tag-count:1..4', 1, 10), - ('-tag-count:2..3', 1, 10), - ('-tag-count:1..', 1, 10), - ('-tag-count:2..', 1, 10), - ('-tag-count:..3', 1, 10), - ('-tag-count:..4', 1, 10), - ('safety:safe', 1, 10), - ('safety:safe', 1, 20), - ('safety:safe', 2, 10), - ('safety:sketchy', 1, 10), - ('safety:safe test', 1, 10), - ('-safety:safe', 1, 10), - ('-safety:safe', 1, 20), - ('-safety:safe', 2, 10), - ('-safety:sketchy', 1, 10), - ('-safety:safe test', 1, 10), - ('safety:safe -test', 1, 10), - ('-test', 1, 10), + ("", 1, 10), + ("creation-time:2016", 1, 10), + ("creation-time:2015", 1, 10), + ("creation-time:2016-01", 1, 10), + ("creation-time:2016-02", 1, 10), + ("creation-time:2016-01-01", 1, 10), + ("creation-time:2016-01-02", 1, 10), + ("tag-count:1,3", 1, 10), + ("tag-count:1,2", 1, 10), + ("tag-count:1", 1, 10), + ("tag-count:1..3", 1, 10), + ("tag-count:1..4", 1, 10), + ("tag-count:2..3", 1, 10), + ("tag-count:1..", 1, 10), + ("tag-count:2..", 1, 10), + ("tag-count:..3", 1, 10), + ("tag-count:..4", 1, 10), + ("-tag-count:1..3", 1, 10), + ("-tag-count:1..4", 1, 10), + ("-tag-count:2..3", 1, 10), + ("-tag-count:1..", 1, 10), + ("-tag-count:2..", 1, 10), + ("-tag-count:..3", 1, 10), + ("-tag-count:..4", 1, 10), + ("safety:safe", 1, 10), + ("safety:safe", 1, 20), + ("safety:safe", 2, 10), + ("safety:sketchy", 1, 10), + ("safety:safe test", 1, 10), + ("-safety:safe", 1, 10), + ("-safety:safe", 1, 20), + ("-safety:safe", 2, 10), + ("-safety:sketchy", 1, 10), + ("-safety:safe test", 1, 10), + ("safety:safe -test", 1, 10), + ("-test", 1, 10), ] for arg in args: executor.execute(*arg) @@ -92,18 +97,22 @@ def test_putting_non_equivalent_queries_into_cache(): assert len(set(hashes)) == len(args) -@pytest.mark.parametrize('input', [ - 'special:fav', - 'special:liked', - 'special:disliked', - '-special:fav', - '-special:liked', - '-special:disliked', -]) +@pytest.mark.parametrize( + "input", + [ + "special:fav", + "special:liked", + "special:disliked", + "-special:fav", + "-special:liked", + "-special:disliked", + ], +) def test_putting_auth_dependent_queries_into_cache(user_factory, input): config = search.configs.PostSearchConfig() - with unittest.mock.patch('szurubooru.func.cache.has'), \ - unittest.mock.patch('szurubooru.func.cache.put'): + with unittest.mock.patch("szurubooru.func.cache.has"), unittest.mock.patch( + "szurubooru.func.cache.put" + ): hashes = [] def appender(key, _value): -- cgit v1.3