Merge 002f49d7db
into 61b9f81e39
This commit is contained in:
commit
fe230fa862
25 changed files with 758 additions and 2 deletions
29
client/css/banned-posts.styl
Normal file
29
client/css/banned-posts.styl
Normal file
|
@ -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
|
13
client/html/banned_post_entry.tpl
Normal file
13
client/html/banned_post_entry.tpl
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<tr data-category='<%- ctx.postBan.checksum %>'>
|
||||||
|
<td class='name'>
|
||||||
|
<%- ctx.postBan.checksum %>
|
||||||
|
</td>
|
||||||
|
<td class='time'>
|
||||||
|
<%= ctx.makeRelativeTime(ctx.postBan.time) %>
|
||||||
|
</td>
|
||||||
|
<% if (ctx.canDelete) { %>
|
||||||
|
<td class='remove'>
|
||||||
|
<a href>Unban</a>
|
||||||
|
</td>
|
||||||
|
<% } %>
|
||||||
|
</tr>
|
25
client/html/banned_post_list.tpl
Normal file
25
client/html/banned_post_list.tpl
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<div class='content-wrapper banned-posts'>
|
||||||
|
<form>
|
||||||
|
<h1>Banned posts</h1>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class='checksum'>Checksum</th>
|
||||||
|
<th class='time'>Time of ban</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='messages'></div>
|
||||||
|
|
||||||
|
<% if (ctx.canDelete) { %>
|
||||||
|
<div class='buttons'>
|
||||||
|
<input type='submit' class='save' value='Save changes'>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -108,7 +108,7 @@
|
||||||
</section>
|
</section>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if (ctx.canFeaturePosts || ctx.canDeletePosts || ctx.canMergePosts) { %>
|
<% if (ctx.canFeaturePosts || ctx.canDeletePosts || ctx.canMergePosts || ctx.canBanPosts) { %>
|
||||||
<section class='management'>
|
<section class='management'>
|
||||||
<ul>
|
<ul>
|
||||||
<% if (ctx.canFeaturePosts) { %>
|
<% if (ctx.canFeaturePosts) { %>
|
||||||
|
@ -120,6 +120,9 @@
|
||||||
<% if (ctx.canDeletePosts) { %>
|
<% if (ctx.canDeletePosts) { %>
|
||||||
<li><a href class='delete'>Delete this post</a></li>
|
<li><a href class='delete'>Delete this post</a></li>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (ctx.canBanPosts) { %>
|
||||||
|
<li><a href class='ban'>Ban this post</a></li>
|
||||||
|
<% } %>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
59
client/js/controllers/banned_post_controller.js
Normal file
59
client/js/controllers/banned_post_controller.js
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
};
|
|
@ -88,6 +88,9 @@ class PostMainController extends BasePostController {
|
||||||
this._view.sidebarControl.addEventListener("delete", (e) =>
|
this._view.sidebarControl.addEventListener("delete", (e) =>
|
||||||
this._evtDeletePost(e)
|
this._evtDeletePost(e)
|
||||||
);
|
);
|
||||||
|
this._view.sidebarControl.addEventListener("ban", (e) =>
|
||||||
|
this._evtBanPost(e)
|
||||||
|
);
|
||||||
this._view.sidebarControl.addEventListener("merge", (e) =>
|
this._view.sidebarControl.addEventListener("merge", (e) =>
|
||||||
this._evtMergePost(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) {
|
_evtUpdatePost(e) {
|
||||||
this._view.sidebarControl.disableForm();
|
this._view.sidebarControl.disableForm();
|
||||||
this._view.sidebarControl.clearMessages();
|
this._view.sidebarControl.clearMessages();
|
||||||
|
|
|
@ -53,6 +53,9 @@ class TopNavigationController {
|
||||||
if (!api.hasPrivilege("pools:list")) {
|
if (!api.hasPrivilege("pools:list")) {
|
||||||
topNavigation.hide("pools");
|
topNavigation.hide("pools");
|
||||||
}
|
}
|
||||||
|
if (!api.hasPrivilege("posts:ban:list")) {
|
||||||
|
topNavigation.hide("banned-posts");
|
||||||
|
}
|
||||||
if (api.isLoggedIn()) {
|
if (api.isLoggedIn()) {
|
||||||
if (!api.hasPrivilege("users:create:any")) {
|
if (!api.hasPrivilege("users:create:any")) {
|
||||||
topNavigation.hide("register");
|
topNavigation.hide("register");
|
||||||
|
|
|
@ -46,6 +46,7 @@ class PostEditSidebarControl extends events.EventTarget {
|
||||||
"posts:create:anonymous"
|
"posts:create:anonymous"
|
||||||
),
|
),
|
||||||
canDeletePosts: api.hasPrivilege("posts:delete"),
|
canDeletePosts: api.hasPrivilege("posts:delete"),
|
||||||
|
canBanPosts: api.hasPrivilege("posts:ban"),
|
||||||
canFeaturePosts: api.hasPrivilege("posts:feature"),
|
canFeaturePosts: api.hasPrivilege("posts:feature"),
|
||||||
canMergePosts: api.hasPrivilege("posts:merge"),
|
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._postNotesOverlayControl.addEventListener("blur", (e) =>
|
||||||
this._evtNoteBlur(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) {
|
_evtNoteTextChangeRequest(e) {
|
||||||
if (this._editedNote) {
|
if (this._editedNote) {
|
||||||
this._editedNote.text = this._noteTextareaNode.value;
|
this._editedNote.text = this._noteTextareaNode.value;
|
||||||
|
@ -517,6 +537,11 @@ class PostEditSidebarControl extends events.EventTarget {
|
||||||
return this._formNode.querySelector(".management .delete");
|
return this._formNode.querySelector(".management .delete");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get _banLinkNode() {
|
||||||
|
return this._formNode.querySelector(".management .ban");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
get _addNoteLinkNode() {
|
get _addNoteLinkNode() {
|
||||||
return this._formNode.querySelector(".notes .add");
|
return this._formNode.querySelector(".notes .add");
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,6 +83,7 @@ Promise.resolve()
|
||||||
controllers.push(
|
controllers.push(
|
||||||
require("./controllers/user_registration_controller.js")
|
require("./controllers/user_registration_controller.js")
|
||||||
);
|
);
|
||||||
|
controllers.push(require("./controllers/banned_post_controller.js"));
|
||||||
|
|
||||||
// 404 controller needs to be registered last
|
// 404 controller needs to be registered last
|
||||||
controllers.push(require("./controllers/not_found_controller.js"));
|
controllers.push(require("./controllers/not_found_controller.js"));
|
||||||
|
|
57
client/js/models/banned_post.js
Normal file
57
client/js/models/banned_post.js
Normal file
|
@ -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;
|
47
client/js/models/banned_post_list.js
Normal file
47
client/js/models/banned_post_list.js
Normal file
|
@ -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;
|
|
@ -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() {
|
delete() {
|
||||||
return api
|
return api
|
||||||
.delete(uri.formatApiLink("post", this.id), {
|
.delete(uri.formatApiLink("post", this.id), {
|
||||||
|
|
|
@ -90,6 +90,7 @@ function _makeTopNavigation() {
|
||||||
ret.add("login", new TopNavigationItem("L", "Log in", "login"));
|
ret.add("login", new TopNavigationItem("L", "Log in", "login"));
|
||||||
ret.add("logout", new TopNavigationItem("O", "Logout", "logout"));
|
ret.add("logout", new TopNavigationItem("O", "Logout", "logout"));
|
||||||
ret.add("help", new TopNavigationItem("E", "Help", "help"));
|
ret.add("help", new TopNavigationItem("E", "Help", "help"));
|
||||||
|
ret.add("banned-posts", new TopNavigationItem("B", "Banned posts", "banned-posts"));
|
||||||
ret.add(
|
ret.add(
|
||||||
"settings",
|
"settings",
|
||||||
new TopNavigationItem(null, "<i class='fa fa-cog'></i>", "settings")
|
new TopNavigationItem(null, "<i class='fa fa-cog'></i>", "settings")
|
||||||
|
|
103
client/js/views/banned_posts_view.js
Normal file
103
client/js/views/banned_posts_view.js
Normal file
|
@ -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;
|
98
doc/API.md
98
doc/API.md
|
@ -39,6 +39,7 @@
|
||||||
- [Getting post](#getting-post)
|
- [Getting post](#getting-post)
|
||||||
- [Getting around post](#getting-around-post)
|
- [Getting around post](#getting-around-post)
|
||||||
- [Deleting post](#deleting-post)
|
- [Deleting post](#deleting-post)
|
||||||
|
- [Banning post](#banning-post)
|
||||||
- [Merging posts](#merging-posts)
|
- [Merging posts](#merging-posts)
|
||||||
- [Rating post](#rating-post)
|
- [Rating post](#rating-post)
|
||||||
- [Adding post to favorites](#adding-post-to-favorites)
|
- [Adding post to favorites](#adding-post-to-favorites)
|
||||||
|
@ -911,7 +912,7 @@ data.
|
||||||
|
|
||||||
- **Output**
|
- **Output**
|
||||||
|
|
||||||
A [post resource](#post).
|
A [post resource](#banned-post).
|
||||||
|
|
||||||
- **Errors**
|
- **Errors**
|
||||||
|
|
||||||
|
@ -1004,6 +1005,81 @@ data.
|
||||||
|
|
||||||
Deletes existing post. Related posts and tags are kept.
|
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": <post id>
|
||||||
|
"version": <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/<image_hash>`
|
||||||
|
|
||||||
|
- **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
|
## Merging posts
|
||||||
- **Request**
|
- **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`,
|
A [pool resource](#pool) stripped down to `id`, `names`, `category`,
|
||||||
`description` and `postCount` fields.
|
`description` and `postCount` fields.
|
||||||
|
|
||||||
|
|
||||||
|
## Banned post
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
A record of a post that has been banned.
|
||||||
|
|
||||||
|
**Structure**
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"checksum": <sha-hash>,
|
||||||
|
"time": <time-of-ban>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field meaning**
|
||||||
|
- `<sha-hash>`: SHA-1 hash of an image that has been banned
|
||||||
|
- `<time-of-ban>`: time the post was banned
|
||||||
|
|
||||||
|
|
||||||
## Comment
|
## Comment
|
||||||
**Description**
|
**Description**
|
||||||
|
|
||||||
|
|
|
@ -116,6 +116,9 @@ privileges:
|
||||||
'posts:bulk-edit:tags': power
|
'posts:bulk-edit:tags': power
|
||||||
'posts:bulk-edit:safety': power
|
'posts:bulk-edit:safety': power
|
||||||
'posts:bulk-edit:delete': power
|
'posts:bulk-edit:delete': power
|
||||||
|
'posts:ban:create': moderator
|
||||||
|
'posts:ban:delete': moderator
|
||||||
|
'posts:ban:list': moderator
|
||||||
|
|
||||||
'tags:create': regular
|
'tags:create': regular
|
||||||
'tags:edit:names': power
|
'tags:edit:names': power
|
||||||
|
|
|
@ -10,3 +10,4 @@ import szurubooru.api.tag_category_api
|
||||||
import szurubooru.api.upload_api
|
import szurubooru.api.upload_api
|
||||||
import szurubooru.api.user_api
|
import szurubooru.api.user_api
|
||||||
import szurubooru.api.user_token_api
|
import szurubooru.api.user_token_api
|
||||||
|
import szurubooru.api.ban_api
|
58
server/szurubooru/api/ban_api.py
Normal file
58
server/szurubooru/api/ban_api.py
Normal file
|
@ -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<image_hash>[^/]+)/?")
|
||||||
|
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)
|
||||||
|
)
|
68
server/szurubooru/func/bans.py
Normal file
68
server/szurubooru/func/bans.py
Normal file
|
@ -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)
|
|
@ -4,6 +4,7 @@ from datetime import datetime
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from szurubooru.func.bans import PostBannedError
|
||||||
|
|
||||||
from szurubooru import config, db, errors, model, rest
|
from szurubooru import config, db, errors, model, rest
|
||||||
from szurubooru.func import (
|
from szurubooru.func import (
|
||||||
|
@ -50,6 +51,7 @@ class PostAlreadyUploadedError(errors.ValidationError):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidPostIdError(errors.ValidationError):
|
class InvalidPostIdError(errors.ValidationError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -634,6 +636,7 @@ def update_post_content(post: model.Post, content: Optional[bytes]) -> None:
|
||||||
.filter(model.Post.post_id != post.post_id)
|
.filter(model.Post.post_id != post.post_id)
|
||||||
.one_or_none()
|
.one_or_none()
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
other_post
|
other_post
|
||||||
and other_post.post_id
|
and other_post.post_id
|
||||||
|
@ -641,6 +644,15 @@ def update_post_content(post: model.Post, content: Optional[bytes]) -> None:
|
||||||
):
|
):
|
||||||
raise PostAlreadyUploadedError(other_post)
|
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:
|
if update_signature:
|
||||||
purge_post_signature(post)
|
purge_post_signature(post)
|
||||||
post.signature = generate_post_signature(post, content)
|
post.signature = generate_post_signature(post, content)
|
||||||
|
@ -806,6 +818,11 @@ def delete(post: model.Post) -> None:
|
||||||
db.session.delete(post)
|
db.session.delete(post)
|
||||||
|
|
||||||
|
|
||||||
|
def ban(ban: model.PostBan) -> None:
|
||||||
|
assert ban
|
||||||
|
db.session.add(ban)
|
||||||
|
|
||||||
|
|
||||||
def merge_posts(
|
def merge_posts(
|
||||||
source_post: model.Post, target_post: model.Post, replace_content: bool
|
source_post: model.Post, target_post: model.Post, replace_content: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -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")
|
|
@ -1,5 +1,6 @@
|
||||||
import szurubooru.model.util
|
import szurubooru.model.util
|
||||||
from szurubooru.model.base import Base
|
from szurubooru.model.base import Base
|
||||||
|
from szurubooru.model.bans import PostBan
|
||||||
from szurubooru.model.comment import Comment, CommentScore
|
from szurubooru.model.comment import Comment, CommentScore
|
||||||
from szurubooru.model.pool import Pool, PoolName, PoolPost
|
from szurubooru.model.pool import Pool, PoolName, PoolPost
|
||||||
from szurubooru.model.pool_category import PoolCategory
|
from szurubooru.model.pool_category import PoolCategory
|
||||||
|
|
12
server/szurubooru/model/bans.py
Normal file
12
server/szurubooru/model/bans.py
Normal file
|
@ -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)
|
|
@ -4,3 +4,4 @@ from .post_search_config import PostSearchConfig
|
||||||
from .snapshot_search_config import SnapshotSearchConfig
|
from .snapshot_search_config import SnapshotSearchConfig
|
||||||
from .tag_search_config import TagSearchConfig
|
from .tag_search_config import TagSearchConfig
|
||||||
from .user_search_config import UserSearchConfig
|
from .user_search_config import UserSearchConfig
|
||||||
|
from .ban_search_config import BanSearchConfig
|
67
server/szurubooru/search/configs/ban_search_config.py
Normal file
67
server/szurubooru/search/configs/ban_search_config.py
Normal file
|
@ -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))
|
||||||
|
]
|
||||||
|
)
|
Reference in a new issue