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.conf import settings
from django.forms import ModelForm from django.forms import ModelForm
from board.models import Post from board.models import Post, Report
from hcaptcha.fields import hCaptchaField from hcaptcha.fields import hCaptchaField
@@ -41,3 +41,20 @@ class ReplyForm(PostForm):
super(ReplyForm, self).__init__(*args, **kwargs) super(ReplyForm, self).__init__(*args, **kwargs)
self.instance.op = op self.instance.op = op
self.instance.reply = reply 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. # Urgency. If true, this post is probably reported as illegal content.
urgent = models.BooleanField(default=False) urgent = models.BooleanField(default=False)
# This is a board-specific report reason # 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): def __str__(self):
return self.reason return self.reason

View File

@@ -1,9 +1,81 @@
function doQuote(sender) { const OPEN = "open";
let id_text = $("#id_text"); const CLOSED = "closed";
let caret = id_text[0].selectionStart;
let text = id_text.val(); function documentClick(e) {
let to_add = ">>" + sender.target.innerText + "\n"; let sender = e.target;
id_text.val(text.substring(0, caret) + to_add + text.substring(caret)); let id = sender.getAttribute("data-id");
if (!id) {
return;
}
switch (id) {
case "post_menu_button": {
openMenu(e);
}; break;
}
} }
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); $(document).on("click", ".post_id", doQuote);
$(window).on("load", onLoad);

View File

@@ -1,3 +1,7 @@
body {
background-color: #ededed;
}
hr { hr {
color: #ededed; color: #ededed;
} }
@@ -26,7 +30,29 @@ hr {
text-align: center; 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_body { }*/
.post_image_info { .post_image_info {
font-size: small; font-size: small;

View File

@@ -1,6 +1,6 @@
{% load post_body %} {% load post_body %}
{% load l10n %} {% load l10n %}
<div id="p{{post.id}}"> <div id="p{{post.id}}" data-report-url="{% url 'board:report_form' board.url post.id %}">
{# Image #} {# Image #}
{% if post.thumbnail %} {% if post.thumbnail %}
{# Image info #} {# Image info #}
@@ -19,7 +19,6 @@
{% endif %} {% endif %}
{# Post ID, username, time #} {# Post ID, username, time #}
<div class="post_content">
<a href="#p{{post.id}}">#.</a> <a href="#p{{post.id}}">#.</a>
<span class="post_id">{{post.id}}</span> <span class="post_id">{{post.id}}</span>
{% if post.subject %} {% if post.subject %}
@@ -32,8 +31,10 @@
{% if reply_link %} {% if reply_link %}
[<a href="{{post.get_absolute_url}}">{% localize on %}Reply{% endlocalize %}</a>] [<a href="{{post.get_absolute_url}}">{% localize on %}Reply{% endlocalize %}</a>]
{% endif %} {% endif %}
<a href="#" class="post_menu_button" data-id="post_menu_button"></a>
{# "X replies elided" dialog for OPs on the board #} {# "X replies elided" dialog for OPs on the board #}
<div class="post_content">
{% if replies_elided > 0 %} {% if replies_elided > 0 %}
<br/> <br/>
<span class="replies_elided"> <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.urls import path
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.views.generic.base import TemplateView
from board.views import * from board.views import *
@@ -9,6 +10,12 @@ urlpatterns = [
path("<slug:url>/", BoardView.as_view(), name="board_detail"), path("<slug:url>/", BoardView.as_view(), name="board_detail"),
path("<slug:url>/page/<int:page>/", 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>/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 # 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) 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.shortcuts import render, get_object_or_404
from django.views.generic import DetailView from django.views.generic import DetailView
from django.views.generic.edit import CreateView from django.views.generic.edit import CreateView
from django.urls import reverse from django.urls import reverse, reverse_lazy
from board.models import Post, Board from board.models import Post, Board, Report
from board.forms import PostForm, ReplyForm from board.forms import PostForm, ReplyForm, ReportForm
__all__ = ("BoardView", "PostView") __all__ = ("BoardView", "PostView", "ReportView")
def get_client_ip(request): def get_client_ip(request):
@@ -107,3 +107,27 @@ class PostView(CreatePostView):
kwargs["op"] = post kwargs["op"] = post
kwargs["reply"] = post kwargs["reply"] = post
return kwargs 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