diff --git a/client/css/banned-posts.styl b/client/css/banned-posts.styl new file mode 100644 index 00000000..331f35bf --- /dev/null +++ b/client/css/banned-posts.styl @@ -0,0 +1,29 @@ +@import colors + +.content-wrapper.banned-posts + width: 100% + max-width: 45em + table + border-spacing: 0 + width: 100% + tr.default td + background: $default-banned-post-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/html/banned_post_entry.tpl b/client/html/banned_post_entry.tpl new file mode 100644 index 00000000..87ec0f2a --- /dev/null +++ b/client/html/banned_post_entry.tpl @@ -0,0 +1,13 @@ + + + <%- ctx.postBan.checksum %> + + + <%= ctx.makeRelativeTime(ctx.postBan.time) %> + + <% if (ctx.canDelete) { %> + + Unban + + <% } %> + diff --git a/client/html/banned_post_list.tpl b/client/html/banned_post_list.tpl new file mode 100644 index 00000000..a27aa0b3 --- /dev/null +++ b/client/html/banned_post_list.tpl @@ -0,0 +1,25 @@ +
+
+

Banned posts

+
+ + + + + + + + + +
ChecksumTime of ban
+
+ +
+ + <% if (ctx.canDelete) { %> +
+ +
+ <% } %> +
+
diff --git a/client/html/post_edit_sidebar.tpl b/client/html/post_edit_sidebar.tpl index 07dcf6f8..9e3641c8 100644 --- a/client/html/post_edit_sidebar.tpl +++ b/client/html/post_edit_sidebar.tpl @@ -108,7 +108,7 @@ <% } %> - <% if (ctx.canFeaturePosts || ctx.canDeletePosts || ctx.canMergePosts) { %> + <% if (ctx.canFeaturePosts || ctx.canDeletePosts || ctx.canMergePosts || ctx.canBanPosts) { %>
<% } %> diff --git a/client/js/controllers/banned_post_controller.js b/client/js/controllers/banned_post_controller.js new file mode 100644 index 00000000..1daf7a86 --- /dev/null +++ b/client/js/controllers/banned_post_controller.js @@ -0,0 +1,59 @@ +"use strict"; + +const api = require("../api.js"); +const BannedPostList = require("../models/banned_post_list.js"); +const topNavigation = require("../models/top_navigation.js"); +const BannedPostsView = require("../views/banned_posts_view.js"); +const EmptyView = require("../views/empty_view.js"); + +class BannedPostController { + constructor() { + if (!api.hasPrivilege("posts:ban:list")) { + this._view = new EmptyView(); + this._view.showError( + "You don't have privileges to view banned posts." + ); + return; + } + + topNavigation.activate("banned-posts"); + topNavigation.setTitle("Listing banned posts"); + BannedPostList.get().then( + (response) => { + this._bannedPosts = response.results; + this._view = new BannedPostsView({ + bannedPosts: this._bannedPosts, + canDelete: api.hasPrivilege("poolCategories:delete") + }); + 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._bannedPosts.save().then( + () => { + this._view.enableForm(); + this._view.showSuccess("Changes saved."); + }, + (error) => { + this._view.enableForm(); + this._view.showError(error.message); + } + ); + } +} + +module.exports = (router) => { + router.enter(["banned-posts"], (ctx, next) => { + ctx.controller = new BannedPostController(ctx, next); + }); +}; diff --git a/client/js/controllers/post_main_controller.js b/client/js/controllers/post_main_controller.js index bd338129..46d5b16b 100644 --- a/client/js/controllers/post_main_controller.js +++ b/client/js/controllers/post_main_controller.js @@ -88,6 +88,9 @@ class PostMainController extends BasePostController { this._view.sidebarControl.addEventListener("delete", (e) => this._evtDeletePost(e) ); + this._view.sidebarControl.addEventListener("ban", (e) => + this._evtBanPost(e) + ); this._view.sidebarControl.addEventListener("merge", (e) => this._evtMergePost(e) ); @@ -165,6 +168,22 @@ class PostMainController extends BasePostController { ); } + _evtBanPost(e) { + this._view.sidebarControl.disableForm(); + this._view.sidebarControl.clearMessages(); + e.detail.post.ban().then( + () => { + misc.disableExitConfirmation(); + const ctx = router.show(uri.formatClientLink("posts")); + ctx.controller.showSuccess("Post banned."); + }, + (error) => { + this._view.sidebarControl.showError(error.message); + this._view.sidebarControl.enableForm(); + } + ); + } + _evtUpdatePost(e) { this._view.sidebarControl.disableForm(); this._view.sidebarControl.clearMessages(); diff --git a/client/js/controllers/top_navigation_controller.js b/client/js/controllers/top_navigation_controller.js index fbc5399d..da90959a 100644 --- a/client/js/controllers/top_navigation_controller.js +++ b/client/js/controllers/top_navigation_controller.js @@ -53,6 +53,9 @@ class TopNavigationController { if (!api.hasPrivilege("pools:list")) { topNavigation.hide("pools"); } + if (!api.hasPrivilege("posts:ban:list")) { + topNavigation.hide("banned-posts"); + } if (api.isLoggedIn()) { if (!api.hasPrivilege("users:create:any")) { topNavigation.hide("register"); diff --git a/client/js/controls/post_edit_sidebar_control.js b/client/js/controls/post_edit_sidebar_control.js index 3b1c16e7..938088b8 100644 --- a/client/js/controls/post_edit_sidebar_control.js +++ b/client/js/controls/post_edit_sidebar_control.js @@ -46,6 +46,7 @@ class PostEditSidebarControl extends events.EventTarget { "posts:create:anonymous" ), canDeletePosts: api.hasPrivilege("posts:delete"), + canBanPosts: api.hasPrivilege("posts:ban"), canFeaturePosts: api.hasPrivilege("posts:feature"), canMergePosts: api.hasPrivilege("posts:merge"), }) @@ -186,6 +187,12 @@ class PostEditSidebarControl extends events.EventTarget { ); } + if (this._banLinkNode) { + this._banLinkNode.addEventListener("click", (e) => + this._evtBanClick(e) + ); + } + this._postNotesOverlayControl.addEventListener("blur", (e) => this._evtNoteBlur(e) ); @@ -301,6 +308,19 @@ class PostEditSidebarControl extends events.EventTarget { } } + _evtBanClick(e) { + e.preventDefault(); + if (confirm("Are you sure you want to ban this post?")) { + this.dispatchEvent( + new CustomEvent("ban", { + detail: { + post: this._post, + }, + }) + ); + } + } + _evtNoteTextChangeRequest(e) { if (this._editedNote) { this._editedNote.text = this._noteTextareaNode.value; @@ -517,6 +537,11 @@ class PostEditSidebarControl extends events.EventTarget { return this._formNode.querySelector(".management .delete"); } + get _banLinkNode() { + return this._formNode.querySelector(".management .ban"); + } + + get _addNoteLinkNode() { return this._formNode.querySelector(".notes .add"); } diff --git a/client/js/main.js b/client/js/main.js index c5bdc537..a89de2e1 100644 --- a/client/js/main.js +++ b/client/js/main.js @@ -83,6 +83,7 @@ Promise.resolve() controllers.push( require("./controllers/user_registration_controller.js") ); + controllers.push(require("./controllers/banned_post_controller.js")); // 404 controller needs to be registered last controllers.push(require("./controllers/not_found_controller.js")); diff --git a/client/js/models/banned_post.js b/client/js/models/banned_post.js new file mode 100644 index 00000000..cfa80c38 --- /dev/null +++ b/client/js/models/banned_post.js @@ -0,0 +1,57 @@ +"use strict"; + +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const events = require("../events.js"); + +class BannedPost extends events.EventTarget { + constructor() { + super(); + this._checksum = ""; + this._time = new Date(); + } + + get checksum() { + return this._checksum; + } + + get time() { + return this._time; + } + + set checksum(value) { + this._checksum = value; + } + + set time(value) { + this._time = value; + } + + static fromResponse(response) { + const ret = new BannedPost(); + ret._updateFromResponse(response); + return ret; + } + + delete() { + return api + .delete(uri.formatApiLink("post-ban", this._checksum)) + .then((response) => { + this.dispatchEvent( + new CustomEvent("delete", { + detail: { + bannedPost: this, + }, + }) + ); + return Promise.resolve(); + }); + } + + _updateFromResponse(response) { + this._checksum = response.checksum; + this.time = response.time; + } +} + +module.exports = BannedPost; diff --git a/client/js/models/banned_post_list.js b/client/js/models/banned_post_list.js new file mode 100644 index 00000000..61ac3a27 --- /dev/null +++ b/client/js/models/banned_post_list.js @@ -0,0 +1,47 @@ +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const AbstractList = require("./abstract_list.js"); +const BannedPost = require("./banned_post.js"); + +class BannedPostList extends AbstractList { + constructor() { + super(); + this._deletedBans = []; + this.addEventListener("remove", (e) => this._evtBannedPostDeleted(e)); + } + + static get() { + return api + .get(uri.formatApiLink("post-ban")) + .then((response) => { + return Promise.resolve( + Object.assign({}, response, { + results: BannedPostList.fromResponse( + response.results + ), + }) + ); + }); + } + + save() { + let promises = []; + for (let bannedPost of this._deletedBans) { + promises.push(bannedPost.delete()); + } + + return Promise.all(promises).then((response) => { + this._deletedBans = []; + return Promise.resolve(); + }); + } + + _evtBannedPostDeleted(e) { + this._deletedBans.push(e.detail.bannedPost); + } +} + +BannedPostList._itemClass = BannedPost; +BannedPostList._itemName = "bannedPost"; + +module.exports = BannedPostList; diff --git a/client/js/models/post.js b/client/js/models/post.js index 01f81bf1..23274068 100644 --- a/client/js/models/post.js +++ b/client/js/models/post.js @@ -334,6 +334,24 @@ class Post extends events.EventTarget { }); } + ban() { + return api + .post(uri.formatApiLink("post-ban"), { + post_id: this.id, + version: this._version + }) + .then((response) => { + this.dispatchEvent( + new CustomEvent("ban", { + detail: { + post: this + } + }) + ); + return Promise.resolve(); + }) + } + delete() { return api .delete(uri.formatApiLink("post", this.id), { diff --git a/client/js/models/top_navigation.js b/client/js/models/top_navigation.js index a469a034..3a680010 100644 --- a/client/js/models/top_navigation.js +++ b/client/js/models/top_navigation.js @@ -90,6 +90,7 @@ function _makeTopNavigation() { 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("banned-posts", new TopNavigationItem("B", "Banned posts", "banned-posts")); ret.add( "settings", new TopNavigationItem(null, "", "settings") diff --git a/client/js/views/banned_posts_view.js b/client/js/views/banned_posts_view.js new file mode 100644 index 00000000..0a29d955 --- /dev/null +++ b/client/js/views/banned_posts_view.js @@ -0,0 +1,103 @@ +"use strict"; + +const events = require("../events.js"); +const views = require("../util/views.js"); +const BannedPost = require("../models/banned_post.js"); + +const template = views.getTemplate("banned-post-list"); +const rowTemplate = views.getTemplate("banned-post-entry"); + +class BannedPostsView 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 bannedPostsToAdd = Array.from(ctx.bannedPosts); + for (let bannedPost of bannedPostsToAdd) { + this._addBannedPostRowNode(bannedPost); + } + + ctx.bannedPosts.addEventListener("remove", (e) => + this._evtBannedPostDeleted(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"); + } + + _addBannedPostRowNode(bannedPost) { + const rowNode = rowTemplate( + Object.assign({}, this._ctx, { postBan: bannedPost }) + ); + + const removeLinkNode = rowNode.querySelector(".remove a"); + if (removeLinkNode) { + removeLinkNode.addEventListener("click", (e) => + this._evtDeleteButtonClick(e, rowNode) + ); + } + + this._tableBodyNode.appendChild(rowNode); + + rowNode._bannedPost = bannedPost; + bannedPost._rowNode = rowNode; + } + + _removeBannedPostRowNode(bannedPost) { + const rowNode = bannedPost._rowNode; + rowNode.parentNode.removeChild(rowNode); + } + + _evtBannedPostDeleted(e) { + this._removeBannedPostRowNode(e.detail.bannedPost); + } + + _evtDeleteButtonClick(e, rowNode, link) { + e.preventDefault(); + if (e.target.classList.contains("inactive")) { + return; + } + this._ctx.bannedPosts.remove(rowNode._bannedPost); + } + + _evtSaveButtonClick(e, ctx) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent("submit")); + } +} + +module.exports = BannedPostsView; diff --git a/doc/API.md b/doc/API.md index 00ee75a9..3272257d 100644 --- a/doc/API.md +++ b/doc/API.md @@ -39,6 +39,7 @@ - [Getting post](#getting-post) - [Getting around post](#getting-around-post) - [Deleting post](#deleting-post) + - [Banning post](#banning-post) - [Merging posts](#merging-posts) - [Rating post](#rating-post) - [Adding post to favorites](#adding-post-to-favorites) @@ -911,7 +912,7 @@ data. - **Output** - A [post resource](#post). + A [post resource](#banned-post). - **Errors** @@ -1004,6 +1005,81 @@ data. Deletes existing post. Related posts and tags are kept. + + +## Listing banned posts +- **Request** + + `GET /post-ban` + +- **Output** + + An [unpaged search result](#unpaged-search-result) of [banned posts](#postban). + +- **Errors** + + - the post does not exist + - privileges are too low + +- **Description** + + Retrieves information about an existing post. + +## Banning post +- **Request** + + `POST /post-ban` + +- **Input** + + ```json5 + { + "post_id": + "version": + } + ``` + +- **Output** + + ```json5 + {} + ``` + +- **Errors** + + - the version is outdated + - the post does not exist + - privileges are too low + +- **Description** + + Deletes existing post, then adds the sha1sum of the post file to a ban list. + Users will not be able to upload the file again. + + Related posts and tags are kept. + + +## Undoing post ban +- **Request** + + `DELETE /post-ban/` + +- **Output** + + ```json5 + {} + ``` + +- **Errors** + + - there is no banned image with that hash + - privileges are too low + +- **Description** + + Removes a banned image from the ban list. Takes a SHA-1 hash of the image as input. + + ## Merging posts - **Request** @@ -2582,6 +2658,26 @@ An ordered list of posts, with a description and category. A [pool resource](#pool) stripped down to `id`, `names`, `category`, `description` and `postCount` fields. + +## Banned post +**Description** + +A record of a post that has been banned. + +**Structure** + +```json5 +{ + "checksum": , + "time": +} +``` + +**Field meaning** +- ``: SHA-1 hash of an image that has been banned +- ``: time the post was banned + + ## Comment **Description** diff --git a/server/config.yaml.dist b/server/config.yaml.dist index 193aac3a..63bc99fa 100644 --- a/server/config.yaml.dist +++ b/server/config.yaml.dist @@ -116,6 +116,9 @@ privileges: 'posts:bulk-edit:tags': power 'posts:bulk-edit:safety': power 'posts:bulk-edit:delete': power + 'posts:ban:create': moderator + 'posts:ban:delete': moderator + 'posts:ban:list': moderator 'tags:create': regular 'tags:edit:names': power diff --git a/server/szurubooru/api/__init__.py b/server/szurubooru/api/__init__.py index d9b7ecba..b1ba6b3b 100644 --- a/server/szurubooru/api/__init__.py +++ b/server/szurubooru/api/__init__.py @@ -10,3 +10,4 @@ import szurubooru.api.tag_category_api import szurubooru.api.upload_api import szurubooru.api.user_api import szurubooru.api.user_token_api +import szurubooru.api.ban_api \ No newline at end of file diff --git a/server/szurubooru/api/ban_api.py b/server/szurubooru/api/ban_api.py new file mode 100644 index 00000000..10b0f956 --- /dev/null +++ b/server/szurubooru/api/ban_api.py @@ -0,0 +1,58 @@ +from datetime import datetime +from typing import Dict, List, Optional +from szurubooru.api import post_api +from szurubooru.func import posts +from szurubooru.model.bans import PostBan + +from szurubooru import db, errors, model, rest, search +from szurubooru.func import ( + auth, + bans, + serialization, + snapshots, + versions, +) + +def _get_ban_by_hash(hash: str) -> Optional[PostBan]: + try: + return bans.get_bans_by_hash(hash) + except: + return None + + +_search_executor = search.Executor(search.configs.BanSearchConfig()) + + +def _serialize(ctx: rest.Context, ban: model.PostBan) -> rest.Response: + return bans.serialize_ban( + ban, options=serialization.get_serialization_options(ctx) + ) + + +@rest.routes.delete("/post-ban/(?P[^/]+)/?") +def unban_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:ban:delete") + ban = _get_ban_by_hash(params["image_hash"]) + bans.delete(ban) + ctx.session.commit() + return {} + + +@rest.routes.post("/post-ban/?") +def ban_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:ban:create") + # post = post_api._get_post(params) + post = posts.get_post_by_id(ctx.get_param_as_int("post_id")) + versions.verify_version(post, ctx) + posts.ban(bans.create_ban(post)) + snapshots.delete(post, ctx.user) + posts.delete(post) + ctx.session.commit() + return {} + +@rest.routes.get("/post-ban/?") +def get_bans(ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:ban:list") + return _search_executor.execute_and_serialize( + ctx, lambda ban: _serialize(ctx, ban) + ) diff --git a/server/szurubooru/func/bans.py b/server/szurubooru/func/bans.py new file mode 100644 index 00000000..694dda62 --- /dev/null +++ b/server/szurubooru/func/bans.py @@ -0,0 +1,68 @@ +from datetime import datetime +from typing import Any, Callable, Dict, List, Optional, Tuple + +from szurubooru import db, errors, model, rest +from szurubooru.func import ( + serialization, +) + +class PostBannedError(errors.ValidationError): + def __init__(self, message: str = "This file was banned", extra_fields: Dict[str, str] = None) -> None: + super().__init__(message, extra_fields) + + +class HashNotBannedError(errors.ValidationError): + pass + + +class BanSerializer(serialization.BaseSerializer): + def __init__(self, ban: model.PostBan) -> None: + self.ban = ban + + def _serializers(self) -> Dict[str, Callable[[], Any]]: + return { + "checksum": self.serialize_checksum, + "time": self.serialize_time + } + + def serialize_checksum(self) -> Any: + return self.ban.checksum + + def serialize_time(self) -> Any: + return self.ban.time + + +def create_ban(post: model.Post) -> model.PostBan: + ban = model.PostBan() + ban.checksum = post.checksum + ban.time = datetime.utcnow() + + db.session.add(ban) + return ban + + +def try_get_ban_by_checksum(checksum: str) -> Optional[model.PostBan]: + return ( + db.session.query(model.PostBan) + .filter(model.PostBan.checksum == checksum) + .one_or_none() + ) + + +def get_bans_by_hash(hash: str) -> model.PostBan: + ban = try_get_ban_by_checksum(hash) + if ban is None: + raise HashNotBannedError("Hash %s is not banned" % hash) + return ban + + +def delete(ban: model.PostBan) -> None: + db.session.delete(ban) + + +def serialize_ban( + ban: model.PostBan, options: List[str] = [] +) -> Optional[rest.Response]: + if not ban: + return None + return BanSerializer(ban).serialize(options) diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index be2259cf..0348532b 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -4,6 +4,7 @@ from datetime import datetime from typing import Any, Callable, Dict, List, Optional, Tuple import sqlalchemy as sa +from szurubooru.func.bans import PostBannedError from szurubooru import config, db, errors, model, rest from szurubooru.func import ( @@ -50,6 +51,7 @@ class PostAlreadyUploadedError(errors.ValidationError): ) + class InvalidPostIdError(errors.ValidationError): pass @@ -634,6 +636,7 @@ def update_post_content(post: model.Post, content: Optional[bytes]) -> None: .filter(model.Post.post_id != post.post_id) .one_or_none() ) + if ( other_post and other_post.post_id @@ -641,6 +644,15 @@ def update_post_content(post: model.Post, content: Optional[bytes]) -> None: ): raise PostAlreadyUploadedError(other_post) + + post_ban = (db.session.query(model.PostBan) + .filter(model.PostBan.checksum == post.checksum) + .one_or_none() + ) + if (post_ban): + raise PostBannedError() + + if update_signature: purge_post_signature(post) post.signature = generate_post_signature(post, content) @@ -806,6 +818,11 @@ def delete(post: model.Post) -> None: db.session.delete(post) +def ban(ban: model.PostBan) -> None: + assert ban + db.session.add(ban) + + def merge_posts( source_post: model.Post, target_post: model.Post, replace_content: bool ) -> None: diff --git a/server/szurubooru/migrations/versions/cc2956cb8ee7_create_ban_table.py b/server/szurubooru/migrations/versions/cc2956cb8ee7_create_ban_table.py new file mode 100644 index 00000000..f4f40e5f --- /dev/null +++ b/server/szurubooru/migrations/versions/cc2956cb8ee7_create_ban_table.py @@ -0,0 +1,29 @@ +''' +create ban table + +Revision ID: cc2956cb8ee7 +Created at: 2023-05-12 02:04:22.592006 +''' + +import sqlalchemy as sa +from alembic import op + + + +revision = 'cc2956cb8ee7' +down_revision = 'adcd63ff76a2' +branch_labels = None +depends_on = None + +def upgrade(): + op.create_table( + "post_ban", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("checksum", sa.Unicode(64), nullable=False), + sa.Column("time", sa.DateTime, nullable=False), + sa.PrimaryKeyConstraint("id") + ) + op.create_unique_constraint("uq_ban_checksum", "post_ban", ["checksum"]) + +def downgrade(): + op.drop_table("post_ban") diff --git a/server/szurubooru/model/__init__.py b/server/szurubooru/model/__init__.py index 21a178ef..70d20452 100644 --- a/server/szurubooru/model/__init__.py +++ b/server/szurubooru/model/__init__.py @@ -1,5 +1,6 @@ import szurubooru.model.util from szurubooru.model.base import Base +from szurubooru.model.bans import PostBan from szurubooru.model.comment import Comment, CommentScore from szurubooru.model.pool import Pool, PoolName, PoolPost from szurubooru.model.pool_category import PoolCategory diff --git a/server/szurubooru/model/bans.py b/server/szurubooru/model/bans.py new file mode 100644 index 00000000..8abeb7f7 --- /dev/null +++ b/server/szurubooru/model/bans.py @@ -0,0 +1,12 @@ +from typing import List + +import sqlalchemy as sa + +from szurubooru.model.base import Base + +class PostBan(Base): + __tablename__ = "post_ban" + + ban_id = sa.Column("id", sa.Integer, primary_key=True) + checksum = sa.Column("checksum", sa.Unicode(64), nullable=False) + time = sa.Column("time", sa.DateTime, nullable=False) diff --git a/server/szurubooru/search/configs/__init__.py b/server/szurubooru/search/configs/__init__.py index c7218131..dec5ed7f 100644 --- a/server/szurubooru/search/configs/__init__.py +++ b/server/szurubooru/search/configs/__init__.py @@ -4,3 +4,4 @@ from .post_search_config import PostSearchConfig from .snapshot_search_config import SnapshotSearchConfig from .tag_search_config import TagSearchConfig from .user_search_config import UserSearchConfig +from .ban_search_config import BanSearchConfig \ No newline at end of file diff --git a/server/szurubooru/search/configs/ban_search_config.py b/server/szurubooru/search/configs/ban_search_config.py new file mode 100644 index 00000000..ce8bba7d --- /dev/null +++ b/server/szurubooru/search/configs/ban_search_config.py @@ -0,0 +1,67 @@ +from typing import Dict, Tuple + +import sqlalchemy as sa + +from szurubooru import db, model +from szurubooru.func import util +from szurubooru.search.configs import util as search_util +from szurubooru.search.configs.base_search_config import ( + BaseSearchConfig, + Filter, +) +from szurubooru.search.typing import SaColumn, SaQuery + + +class BanSearchConfig(BaseSearchConfig): + def create_filter_query(self, _disable_eager_loads: bool) -> SaQuery: + return db.session.query(model.PostBan) + + def create_count_query(self, _disable_eager_loads: bool) -> SaQuery: + return db.session.query(model.PostBan) + + def create_around_query(self) -> SaQuery: + raise NotImplementedError() + + def finalize_query(self, query: SaQuery) -> SaQuery: + return query.order_by(model.PostBan.time.asc()) + + @property + def anonymous_filter(self) -> Filter: + return search_util.create_subquery_filter( + model.PostBan.checksum, + model.PostBan.time, + search_util.create_str_filter, + ) + + @property + def named_filters(self) -> Dict[str, Filter]: + return util.unalias_dict( + [ + ( + ["time"], + search_util.create_date_filter( + model.PostBan.time, + ), + ), + ( + ["checksum"], + search_util.create_subquery_filter( + model.PostBan.checksum, + search_util.create_str_filter, + ), + ) + ] + ) + + @property + def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: + return util.unalias_dict( + [ + ( + ["random"], + (sa.sql.expression.func.random(), self.SORT_NONE), + ), + (["checksum"], (model.PostBan.checksum, self.SORT_ASC)), + (["time"], (model.PostBan.time, self.SORT_ASC)) + ] + )