Add preliminary report system

The report system is pretty low-tech. However the scaffolding is there
for a lot of new stuff. What we currently have:

* Users can create reports
* Staff can view reports
* Admins can create report templates

There's a post drop-down menu available on all posts now, too. This is
where "report post" menu item lives and other things like that can be
added too.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2022-06-14 14:56:50 -07:00
parent ec011dc047
commit b838663d50
9 changed files with 208 additions and 16 deletions

View File

@@ -1,6 +1,6 @@
from django.conf import settings
from django.forms import ModelForm
from board.models import Post
from board.models import Post, Report
from hcaptcha.fields import hCaptchaField
@@ -41,3 +41,20 @@ class ReplyForm(PostForm):
super(ReplyForm, self).__init__(*args, **kwargs)
self.instance.op = op
self.instance.reply = reply
class ReportForm(ModelForm):
"""
A form used to create reports on posts.
This requires the board and the IP address to be specified.
"""
class Meta:
model = Report
fields = ["reason"]
def __init__(self, *args, op, board, ip, **kwargs):
super(ReportForm, self).__init__(*args, **kwargs)
self.instance.ip = ip
self.instance.post = op

View File

@@ -212,7 +212,7 @@ class ReportReason(models.Model):
# Urgency. If true, this post is probably reported as illegal content.
urgent = models.BooleanField(default=False)
# This is a board-specific report reason
board = models.ForeignKey("Board", on_delete=models.CASCADE, null=True)
board = models.ForeignKey("Board", on_delete=models.CASCADE, null=True, blank=True)
def __str__(self):
return self.reason

View File

@@ -1,9 +1,81 @@
function doQuote(sender) {
let id_text = $("#id_text");
let caret = id_text[0].selectionStart;
let text = id_text.val();
let to_add = ">>" + sender.target.innerText + "\n";
id_text.val(text.substring(0, caret) + to_add + text.substring(caret));
const OPEN = "open";
const CLOSED = "closed";
function documentClick(e) {
let sender = e.target;
let id = sender.getAttribute("data-id");
if (!id) {
return;
}
switch (id) {
case "post_menu_button": {
openMenu(e);
}; break;
}
}
$(document).on("click", ".post_id", doQuote);
function doQuote(e) {
let idText = $("#id_text");
let caret = idText[0].selectionStart;
let text = idText.val();
let toAdd = ">>" + e.target.innerText + "\n";
idText.val(text.substring(0, caret) + toAdd + text.substring(caret));
}
function closeMenu(e) {
$(document).off("click", closeMenu);
$(".post_menu").remove();
}
function openMenu(e) {
e.preventDefault();
let sender = e.target;
let reportButton = $("<a>")
.text("Report")
.attr("href", "#")
.on("click", (e) => { return openReportWindow(e, $(sender.parentElement)); });
let menuList = $("<ul></ul>")
.append($("<lh><strong>Actions</strong></lh>").addClass("post_menu_item"))
.append(
$('<li></li>')
.addClass("post_menu_item")
.append(reportButton)
)
.addClass("post_menu_items");
let rect = sender.getBoundingClientRect();
let menu = $("<div></div>")
.addClass("post_menu")
.css({
top: rect.bottom + 3 + window.pageYOffset + "px",
left: rect.left + 3 + window.pageXOffset + "px",
})
.append(menuList);
$("body").append(menu);
$(document).on("click", closeMenu);
}
function openReportWindow(e, postElement) {
e.preventDefault();
// If there's already a report window open, close it and open this one.
if (window.top.reportWindow) {
window.top.reportWindow.close();
}
//let postId = sender.parentElement.getAttribute("id").substring(1);
let reportUrl = postElement.attr("data-report-url");
window.reportWindow = new WinBox("New Report", {
url: reportUrl,
modal: true,
onclose: function (force) {
window.top.reportWindow = null;
}
});
}
function onLoad(e) {
window.reportWindow = null;
}
$(document).on("click", documentClick);
$(document).on("click", ".post_id", doQuote);
$(window).on("load", onLoad);

View File

@@ -1,3 +1,7 @@
body {
background-color: #ededed;
}
hr {
color: #ededed;
}
@@ -26,7 +30,29 @@ hr {
text-align: center;
}
/* Posts */
.post_menu {
position: absolute;
background: #ededed;
outline: 1px solid #555;
}
.post_menu_items {
list-style-type: none;
padding: 0;
margin: 0;
}
.post_menu_item {
padding: 3px;
}
.post_menu_button {
text-decoration: none;
}
/*******************************************************************************
Posts
********************************************************************************/
/*.post_body { }*/
.post_image_info {
font-size: small;

View File

@@ -1,6 +1,6 @@
{% load post_body %}
{% load l10n %}
<div id="p{{post.id}}">
<div id="p{{post.id}}" data-report-url="{% url 'board:report_form' board.url post.id %}">
{# Image #}
{% if post.thumbnail %}
{# Image info #}
@@ -19,7 +19,6 @@
{% endif %}
{# Post ID, username, time #}
<div class="post_content">
<a href="#p{{post.id}}">#.</a>
<span class="post_id">{{post.id}}</span>
{% if post.subject %}
@@ -32,8 +31,10 @@
{% if reply_link %}
[<a href="{{post.get_absolute_url}}">{% localize on %}Reply{% endlocalize %}</a>]
{% endif %}
<a href="#" class="post_menu_button" data-id="post_menu_button"></a>
{# "X replies elided" dialog for OPs on the board #}
<div class="post_content">
{% if replies_elided > 0 %}
<br/>
<span class="replies_elided">

View File

@@ -0,0 +1,16 @@
{% extends "board/base.html" %}
{% load l10n %}
{# Title #}
{% block title %}{% localize on %}Reporting post {{post.id}}{% endlocalize %}{% endblock %}
{# Body #}
{% block content %}
<div class="row">
<form method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
<tr><td>&nbsp;</td><td><input type="submit" value="Submit"></td></tr>
</table>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,29 @@
{% extends "board/base.html" %}
{% load l10n %}
{# Title #}
{% block title %}{% localize on %}Report success{% endlocalize %}{% endblock %}
{# Body #}
{% block content %}
<div class="row" id="message">
{% localize on %}Post reported. This window will close in 1 second.{% endlocalize %}
</div>
<script>
function isIframe() {
try {
return window.self !== window.top;
} catch (_) {
return true;
}
}
setTimeout(function() {
if(isIframe()) {
window.top.reportWindow.close();
} else {
window.close();
}
}, 5000);
</script>
{% endblock %}

View File

@@ -1,6 +1,7 @@
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from django.views.generic.base import TemplateView
from board.views import *
@@ -9,6 +10,12 @@ urlpatterns = [
path("<slug:url>/", BoardView.as_view(), name="board_detail"),
path("<slug:url>/page/<int:page>/", BoardView.as_view(), name="board_detail"),
path("<slug:url>/post/<int:id>/", PostView.as_view(), name="post_detail"),
path("<slug:url>/report/<int:id>/", ReportView.as_view(), name="report_form"),
path(
"report/success/",
TemplateView.as_view(template_name="board/report_success.html"),
name="report_success",
),
]
# TODO - make this conditional so we can serve images up with whatever server we want
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -3,11 +3,11 @@ from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render, get_object_or_404
from django.views.generic import DetailView
from django.views.generic.edit import CreateView
from django.urls import reverse
from board.models import Post, Board
from board.forms import PostForm, ReplyForm
from django.urls import reverse, reverse_lazy
from board.models import Post, Board, Report
from board.forms import PostForm, ReplyForm, ReportForm
__all__ = ("BoardView", "PostView")
__all__ = ("BoardView", "PostView", "ReportView")
def get_client_ip(request):
@@ -107,3 +107,27 @@ class PostView(CreatePostView):
kwargs["op"] = post
kwargs["reply"] = post
return kwargs
class ReportView(CreatePostView):
model = Report
form_class = ReportForm
success_url = reverse_lazy("board:report_success")
def get_context_data(self, **kwargs):
return super(ReportView, self).get_context_data(**kwargs)
@property
def board_url(self) -> str:
return self.kwargs["url"]
@property
def post_id(self) -> int:
return self.kwargs["id"]
def get_form_kwargs(self):
kwargs = super(ReportView, self).get_form_kwargs()
post_id = self.kwargs["id"]
post = get_object_or_404(Post, id=post_id)
kwargs["op"] = post
return kwargs