commit
b64fb3e0aa
|
@ -2,6 +2,7 @@
|
||||||
/venv
|
/venv
|
||||||
*.pyc
|
*.pyc
|
||||||
*.swp
|
*.swp
|
||||||
|
**/__pycache__
|
||||||
|
|
||||||
# VSCode
|
# VSCode
|
||||||
/.vscode
|
/.vscode
|
||||||
|
@ -15,4 +16,4 @@
|
||||||
/images/
|
/images/
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
.coverage
|
.coverage
|
||||||
|
|
|
@ -231,3 +231,9 @@ class ListForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.List
|
model = models.List
|
||||||
fields = ["user", "name", "description", "curation", "privacy"]
|
fields = ["user", "name", "description", "curation", "privacy"]
|
||||||
|
|
||||||
|
|
||||||
|
class ReportForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Report
|
||||||
|
fields = ["user", "reporter", "statuses", "note"]
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
# Generated by Django 3.0.7 on 2021-03-09 01:56
|
||||||
|
|
||||||
|
import bookwyrm.models.fields
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.db.models.expressions
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0048_merge_20210308_1754"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Report",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_date", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"remote_id",
|
||||||
|
bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("note", models.TextField(blank=True, null=True)),
|
||||||
|
("resolved", models.BooleanField(default=False)),
|
||||||
|
(
|
||||||
|
"reporter",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="reporter",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"statuses",
|
||||||
|
models.ManyToManyField(blank=True, null=True, to="bookwyrm.Status"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ReportComment",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_date", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"remote_id",
|
||||||
|
bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("note", models.TextField()),
|
||||||
|
(
|
||||||
|
"report",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="bookwyrm.Report",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="report",
|
||||||
|
constraint=models.CheckConstraint(
|
||||||
|
check=models.Q(
|
||||||
|
_negated=True, reporter=django.db.models.expressions.F("user")
|
||||||
|
),
|
||||||
|
name="self_report",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -21,6 +21,7 @@ from .tag import Tag, UserTag
|
||||||
|
|
||||||
from .user import User, KeyPair, AnnualGoal
|
from .user import User, KeyPair, AnnualGoal
|
||||||
from .relationship import UserFollows, UserFollowRequest, UserBlocks
|
from .relationship import UserFollows, UserFollowRequest, UserBlocks
|
||||||
|
from .report import Report, ReportComment
|
||||||
from .federated_server import FederatedServer
|
from .federated_server import FederatedServer
|
||||||
|
|
||||||
from .import_job import ImportJob, ImportItem
|
from .import_job import ImportJob, ImportItem
|
||||||
|
|
|
@ -4,7 +4,7 @@ from .base_model import BookWyrmModel
|
||||||
|
|
||||||
|
|
||||||
class FederatedServer(BookWyrmModel):
|
class FederatedServer(BookWyrmModel):
|
||||||
""" store which server's we federate with """
|
""" store which servers we federate with """
|
||||||
|
|
||||||
server_name = models.CharField(max_length=255, unique=True)
|
server_name = models.CharField(max_length=255, unique=True)
|
||||||
# federated, blocked, whatever else
|
# federated, blocked, whatever else
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
""" flagged for moderation """
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import F, Q
|
||||||
|
from .base_model import BookWyrmModel
|
||||||
|
|
||||||
|
|
||||||
|
class Report(BookWyrmModel):
|
||||||
|
""" reported status or user """
|
||||||
|
|
||||||
|
reporter = models.ForeignKey(
|
||||||
|
"User", related_name="reporter", on_delete=models.PROTECT
|
||||||
|
)
|
||||||
|
note = models.TextField(null=True, blank=True)
|
||||||
|
user = models.ForeignKey("User", on_delete=models.PROTECT)
|
||||||
|
statuses = models.ManyToManyField("Status", null=True, blank=True)
|
||||||
|
resolved = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
""" don't let users report themselves """
|
||||||
|
|
||||||
|
constraints = [
|
||||||
|
models.CheckConstraint(check=~Q(reporter=F("user")), name="self_report")
|
||||||
|
]
|
||||||
|
ordering = ("-created_date",)
|
||||||
|
|
||||||
|
|
||||||
|
class ReportComment(BookWyrmModel):
|
||||||
|
""" updates on a report """
|
||||||
|
|
||||||
|
user = models.ForeignKey("User", on_delete=models.PROTECT)
|
||||||
|
note = models.TextField()
|
||||||
|
report = models.ForeignKey(Report, on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
""" sort comments """
|
||||||
|
|
||||||
|
ordering = ("-created_date",)
|
|
@ -114,8 +114,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.bookwyrm.edit_instance_settings %}
|
{% if perms.bookwyrm.edit_instance_settings %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'settings-site' %}" class="navbar-item">
|
<a href="{% url 'settings-reports' %}" class="navbar-item">
|
||||||
{% trans 'Site Configuration' %}
|
{% trans 'Admin' %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
{% extends 'settings/admin_layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
|
{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
|
||||||
|
{% block header %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
<div class="block">
|
||||||
|
<a href="{% url 'settings-reports' %}">{% trans "Back to reports" %}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
{% include 'moderation/report_preview.html' with report=report %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block content">
|
||||||
|
<h3>{% trans "Actions" %}</h3>
|
||||||
|
<p><a href="{{ report.user.local_path }}">{% trans "View user profile" %}</a></p>
|
||||||
|
<div class="is-flex">
|
||||||
|
<p class="mr-1">
|
||||||
|
<a class="button" href="{% url 'direct-messages-user' report.user.username %}">{% trans "Send direct message" %}</a>
|
||||||
|
</p>
|
||||||
|
<form name="deactivate" method="post" action="{% url 'settings-report-deactivate' report.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if report.user.is_active %}
|
||||||
|
<button type="submit" class="button is-danger is-light">{% trans "Deactivate user" %}</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="button">{% trans "Reactivate user" %}</button>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>
|
||||||
|
{% for comment in report.reportcomment_set.all %}
|
||||||
|
<div class="card block">
|
||||||
|
<p class="card-content">{{ comment.note }}</p>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="card-footer-item">
|
||||||
|
<a href="{{ comment.user.local_path }}">{{ comment.user.display_name }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer-item">
|
||||||
|
{{ comment.created_date | naturaltime }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<form class="block" name="report-comment" method="post" action="{% url 'settings-report' report.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<label for="report_comment" class="label">Comment on report</label>
|
||||||
|
<textarea name="note" id="report_comment" class="textarea"></textarea>
|
||||||
|
<button class="button">{% trans "Comment" %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3 class="title is-4">{% trans "Reported statuses" %}</h3>
|
||||||
|
{% if not report.statuses.exists %}
|
||||||
|
<em>{% trans "No statuses reported" %}</em>
|
||||||
|
{% else %}
|
||||||
|
<ul>
|
||||||
|
{% for status in report.statuses.select_subclasses.all %}
|
||||||
|
<li>
|
||||||
|
{% if status.deleted %}
|
||||||
|
<em>{% trans "Statuses has been deleted" %}</em>
|
||||||
|
{% else %}
|
||||||
|
{% include 'snippets/status/status.html' with status=status moderation_mode=True %}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,37 @@
|
||||||
|
{% extends 'components/modal.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
|
{% block modal-title %}
|
||||||
|
{% blocktrans with username=user.username %}Report @{{ username }}{% endblocktrans %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-form-open %}
|
||||||
|
<form name="report" method="post" action="{% url 'report' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-body %}
|
||||||
|
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="reporter" value="{{ reporter.id }}">
|
||||||
|
<input type="hidden" name="user" value="{{ user.id }}">
|
||||||
|
<input type="hidden" name="statuses" value="{{ status.id }}">
|
||||||
|
|
||||||
|
<section class="content">
|
||||||
|
<p>{% blocktrans with site_name=site.name %}This report will be sent to {{ site_name }}'s moderators for review.{% endblocktrans %}</p>
|
||||||
|
<label class="label" for="id_{{ controls_uid }}_report_note">{% trans "More info about this report:" %}</label>
|
||||||
|
<textarea class="textarea" name="note" id="id_{{ controls_uid }}_report_note"></textarea>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block modal-footer %}
|
||||||
|
|
||||||
|
<button class="button is-success" type="submit">{% trans "Submit" %}</button>
|
||||||
|
{% trans "Cancel" as button_text %}
|
||||||
|
{% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="report" controls_uid=report_uuid class="" %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
{% block modal-form-close %}</form>{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
{% extends 'components/card.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
|
{% block card-header %}
|
||||||
|
<h2 class="card-header-title has-background-white-ter is-block">
|
||||||
|
<a href="{% url 'settings-report' report.id %}">{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}</a>
|
||||||
|
</h2>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block card-content %}
|
||||||
|
<div class="block content">
|
||||||
|
<p>
|
||||||
|
{% if report.note %}{{ report.note }}{% else %}<em>{% trans "No notes provided" %}</em>{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block card-footer %}
|
||||||
|
<div class="card-footer-item">
|
||||||
|
<p>{% blocktrans with username=report.reporter.display_name path=report.reporter.local_path %}Reported by <a href="{{ path }}">{{ username }}</a>{% endblocktrans %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer-item">
|
||||||
|
{{ report.created_date | naturaltime }}
|
||||||
|
</div>
|
||||||
|
<div class="card-footer-item">
|
||||||
|
<form name="resolve" method="post" action="{% url 'settings-report-resolve' report.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="button" type="submit">
|
||||||
|
{% if report.resolved %}
|
||||||
|
{% trans "Re-open" %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Resolve" %}
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,28 @@
|
||||||
|
{% extends 'settings/admin_layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Reports" %}{% endblock %}
|
||||||
|
{% block header %}{% trans "Reports" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
<div class="tabs">
|
||||||
|
<ul>
|
||||||
|
<li class="{% if not resolved %}is-active{% endif %}"{% if not resolved == 'open' %} aria-current="page"{% endif %}>
|
||||||
|
<a href="{% url 'settings-reports' %}?resolved=false">{% trans "Open" %}</a>
|
||||||
|
</li>
|
||||||
|
<li class="{% if resolved %}is-active{% endif %}"{% if resolved %} aria-current="page"{% endif %}>
|
||||||
|
<a href="{% url 'settings-reports' %}?resolved=true">{% trans "Resolved" %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
{% for report in reports %}
|
||||||
|
<div class="block">
|
||||||
|
{% include 'moderation/report_preview.html' with report=report %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -115,15 +115,7 @@
|
||||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white{% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %} has-text-black{% else %}-bis has-text-grey-dark{% endif %}{% endif %}">
|
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white{% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %} has-text-black{% else %}-bis has-text-grey-dark{% endif %}{% endif %}">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
{% if related_status.content %}
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
<a href="{{ related_status.local_path }}">
|
|
||||||
{{ related_status.content | safe | truncatewords_html:10 }}{% if related_status.mention_books %} <em>{{ related_status.mention_books.first.title }}</em>{% endif %}
|
|
||||||
</a>
|
|
||||||
{% elif related_status.quote %}
|
|
||||||
<a href="{{ related_status.local_path }}">{{ related_status.quote | safe | truncatewords_html:10 }}</a>
|
|
||||||
{% elif related_status.rating %}
|
|
||||||
{% include 'snippets/stars.html' with rating=related_status.rating %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow {% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %}has-text-black{% else %}has-text-grey-dark{% endif %}">
|
<div class="column is-narrow {% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %}has-text-black{% else %}has-text-grey-dark{% endif %}">
|
||||||
{{ related_status.published_date | post_date }}
|
{{ related_status.published_date | post_date }}
|
||||||
|
|
|
@ -18,6 +18,10 @@
|
||||||
{% url 'settings-invites' as url %}
|
{% url 'settings-invites' as url %}
|
||||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Invites" %}</a>
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Invites" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
{% url 'settings-reports' as url %}
|
||||||
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Reports" %}</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{% url 'settings-federation' as url %}
|
{% url 'settings-federation' as url %}
|
||||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Federated Servers" %}</a>
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Federated Servers" %}</a>
|
||||||
|
@ -42,7 +46,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
<div class="column content">
|
<div class="column">
|
||||||
{% block panel %}{% endblock %}
|
{% block panel %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
{% with 0|uuid as report_uuid %}
|
||||||
|
|
||||||
|
{% trans "Report" as button_text %}
|
||||||
|
{% include 'snippets/toggle/toggle_button.html' with class="is-danger is-light is-small is-fullwidth" text=button_text controls_text="report" controls_uid=report_uuid focus="modal-title-report" disabled=is_current %}
|
||||||
|
|
||||||
|
{% include 'moderation/report_modal.html' with user=user reporter=request.user controls_text="report" controls_uid=report_uuid %}
|
||||||
|
|
||||||
|
{% endwith %}
|
|
@ -18,7 +18,17 @@
|
||||||
|
|
||||||
{% block card-footer %}
|
{% block card-footer %}
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
{% if request.user.is_authenticated %}
|
{% if moderation_mode and perms.bookwyrm.moderate_post %}
|
||||||
|
|
||||||
|
{# moderation options #}
|
||||||
|
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="button is-danger is-light" type="submit">
|
||||||
|
{% trans "Delete status" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% elif request.user.is_authenticated %}
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{% trans "Reply" as button_text %}
|
{% trans "Reply" as button_text %}
|
||||||
|
@ -56,14 +66,16 @@
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
<a href="{{ status.remote_id }}">{{ status.published_date | post_date }}</a>
|
<a href="{{ status.remote_id }}">{{ status.published_date | post_date }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% if not moderation_mode %}
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
{% include 'snippets/status/status_options.html' with class="is-small" right=True %}
|
{% include 'snippets/status/status_options.html' with class="is-small" right=True %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block card-bonus %}
|
{% block card-bonus %}
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated and not moderation_mode %}
|
||||||
{% with status.id|uuid as uuid %}
|
{% with status.id|uuid as uuid %}
|
||||||
<section class="hidden" id="show-comment-{{ status.id }}">
|
<section class="hidden" id="show-comment-{{ status.id }}">
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
{% block dropdown-list %}
|
{% block dropdown-list %}
|
||||||
{% if status.user == request.user %}
|
{% if status.user == request.user %}
|
||||||
|
{# things you can do to your own statuses #}
|
||||||
<li role="menuitem">
|
<li role="menuitem">
|
||||||
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
|
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
@ -19,8 +20,12 @@
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
{# things you can do to other people's statuses #}
|
||||||
<li role="menuitem">
|
<li role="menuitem">
|
||||||
<a href="/direct-messages/{{ status.user|username }}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
|
<a href="{% url 'direct-messages-user' status.user|username %}" class="button is-small is-fullwidth">{% trans "Send direct message" %}</a>
|
||||||
|
</li>
|
||||||
|
<li role="menuitem">
|
||||||
|
{% include 'snippets/report_button.html' with user=status.user status=status %}
|
||||||
</li>
|
</li>
|
||||||
<li role="menuitem">
|
<li role="menuitem">
|
||||||
{% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %}
|
{% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% if status.content %}
|
||||||
|
<a href="{{ status.local_path }}">
|
||||||
|
{{ status.content | safe | truncatewords_html:10 }}{% if status.mention_books %} <em>{{ status.mention_books.first.title }}</em>{% endif %}
|
||||||
|
</a>
|
||||||
|
{% elif status.quote %}
|
||||||
|
<a href="{{ status.local_path }}">{{ status.quote | safe | truncatewords_html:10 }}</a>
|
||||||
|
{% elif status.rating %}
|
||||||
|
{% include 'snippets/stars.html' with rating=status.rating %}
|
||||||
|
{% endif %}
|
|
@ -12,6 +12,9 @@
|
||||||
<li role="menuitem">
|
<li role="menuitem">
|
||||||
<a href="/direct-messages/{{ user|username }}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
|
<a href="/direct-messages/{{ user|username }}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li role="menuitem">
|
||||||
|
{% include 'snippets/report_button.html' with user=status.user class="is-fullwidth" %}
|
||||||
|
</li>
|
||||||
<li role="menuitem">
|
<li role="menuitem">
|
||||||
{% include 'snippets/block_button.html' with user=user class="is-fullwidth" %}
|
{% include 'snippets/block_button.html' with user=user class="is-fullwidth" %}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
""" test for app action functionality """
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import forms, models, views
|
||||||
|
|
||||||
|
|
||||||
|
class ReportViews(TestCase):
|
||||||
|
""" every response to a get request, html or json """
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
""" we need basic test data and mocks """
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
"mouse@local.com",
|
||||||
|
"mouse@mouse.mouse",
|
||||||
|
"password",
|
||||||
|
local=True,
|
||||||
|
localname="mouse",
|
||||||
|
)
|
||||||
|
self.rat = models.User.objects.create_user(
|
||||||
|
"rat@local.com",
|
||||||
|
"rat@mouse.mouse",
|
||||||
|
"password",
|
||||||
|
local=True,
|
||||||
|
localname="rat",
|
||||||
|
)
|
||||||
|
models.SiteSettings.objects.create()
|
||||||
|
|
||||||
|
def test_reports_page(self):
|
||||||
|
""" there are so many views, this just makes sure it LOADS """
|
||||||
|
view = views.Reports.as_view()
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.local_user
|
||||||
|
request.user.is_superuser = True
|
||||||
|
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
result.render()
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_reports_page_with_data(self):
|
||||||
|
""" there are so many views, this just makes sure it LOADS """
|
||||||
|
view = views.Reports.as_view()
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.local_user
|
||||||
|
request.user.is_superuser = True
|
||||||
|
models.Report.objects.create(reporter=self.local_user, user=self.rat)
|
||||||
|
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
result.render()
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_report_page(self):
|
||||||
|
""" there are so many views, this just makes sure it LOADS """
|
||||||
|
view = views.Report.as_view()
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.local_user
|
||||||
|
request.user.is_superuser = True
|
||||||
|
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
|
||||||
|
|
||||||
|
result = view(request, report.id)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
result.render()
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_report_comment(self):
|
||||||
|
""" comment on a report """
|
||||||
|
view = views.Report.as_view()
|
||||||
|
request = self.factory.post("", {"note": "hi"})
|
||||||
|
request.user = self.local_user
|
||||||
|
request.user.is_superuser = True
|
||||||
|
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
|
||||||
|
|
||||||
|
view(request, report.id)
|
||||||
|
|
||||||
|
comment = models.ReportComment.objects.get()
|
||||||
|
self.assertEqual(comment.user, self.local_user)
|
||||||
|
self.assertEqual(comment.note, "hi")
|
||||||
|
self.assertEqual(comment.report, report)
|
||||||
|
|
||||||
|
def test_make_report(self):
|
||||||
|
""" a user reports another user """
|
||||||
|
form = forms.ReportForm()
|
||||||
|
form.data["reporter"] = self.local_user.id
|
||||||
|
form.data["user"] = self.rat.id
|
||||||
|
request = self.factory.post("", form.data)
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
views.make_report(request)
|
||||||
|
|
||||||
|
report = models.Report.objects.get()
|
||||||
|
self.assertEqual(report.reporter, self.local_user)
|
||||||
|
self.assertEqual(report.user, self.rat)
|
||||||
|
|
||||||
|
def test_resolve_report(self):
|
||||||
|
""" toggle report resolution status """
|
||||||
|
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
|
||||||
|
self.assertFalse(report.resolved)
|
||||||
|
request = self.factory.post("")
|
||||||
|
request.user = self.local_user
|
||||||
|
request.user.is_superuser = True
|
||||||
|
|
||||||
|
# resolve
|
||||||
|
views.resolve_report(request, report.id)
|
||||||
|
report.refresh_from_db()
|
||||||
|
self.assertTrue(report.resolved)
|
||||||
|
|
||||||
|
# un-resolve
|
||||||
|
views.resolve_report(request, report.id)
|
||||||
|
report.refresh_from_db()
|
||||||
|
self.assertFalse(report.resolved)
|
||||||
|
|
||||||
|
def test_deactivate_user(self):
|
||||||
|
""" toggle whether a user is able to log in """
|
||||||
|
self.assertTrue(self.rat.is_active)
|
||||||
|
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
|
||||||
|
request = self.factory.post("")
|
||||||
|
request.user = self.local_user
|
||||||
|
request.user.is_superuser = True
|
||||||
|
|
||||||
|
# de-activate
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
views.deactivate_user(request, report.id)
|
||||||
|
self.rat.refresh_from_db()
|
||||||
|
self.assertFalse(self.rat.is_active)
|
||||||
|
|
||||||
|
# re-activate
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
views.deactivate_user(request, report.id)
|
||||||
|
self.rat.refresh_from_db()
|
||||||
|
self.assertTrue(self.rat.is_active)
|
|
@ -216,7 +216,7 @@ class StatusViews(TestCase):
|
||||||
'<a href="%s">'
|
'<a href="%s">'
|
||||||
"archive.org/details/dli.granth.72113/page/n25/mode/2up</a>" % url,
|
"archive.org/details/dli.granth.72113/page/n25/mode/2up</a>" % url,
|
||||||
)
|
)
|
||||||
url = "https://openlibrary.org/search" "?q=arkady+strugatsky&mode=everything"
|
url = "https://openlibrary.org/search?q=arkady+strugatsky&mode=everything"
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
views.status.format_links(url),
|
views.status.format_links(url),
|
||||||
'<a href="%s">openlibrary.org/search'
|
'<a href="%s">openlibrary.org/search'
|
||||||
|
@ -253,3 +253,35 @@ class StatusViews(TestCase):
|
||||||
self.assertEqual(activity["object"]["type"], "Tombstone")
|
self.assertEqual(activity["object"]["type"], "Tombstone")
|
||||||
status.refresh_from_db()
|
status.refresh_from_db()
|
||||||
self.assertTrue(status.deleted)
|
self.assertTrue(status.deleted)
|
||||||
|
|
||||||
|
def test_handle_delete_status_permission_denied(self):
|
||||||
|
""" marks a status as deleted """
|
||||||
|
view = views.DeleteStatus.as_view()
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
status = models.Status.objects.create(user=self.local_user, content="hi")
|
||||||
|
self.assertFalse(status.deleted)
|
||||||
|
request = self.factory.post("")
|
||||||
|
request.user = self.remote_user
|
||||||
|
|
||||||
|
view(request, status.id)
|
||||||
|
|
||||||
|
status.refresh_from_db()
|
||||||
|
self.assertFalse(status.deleted)
|
||||||
|
|
||||||
|
def test_handle_delete_status_moderator(self):
|
||||||
|
""" marks a status as deleted """
|
||||||
|
view = views.DeleteStatus.as_view()
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
status = models.Status.objects.create(user=self.local_user, content="hi")
|
||||||
|
self.assertFalse(status.deleted)
|
||||||
|
request = self.factory.post("")
|
||||||
|
request.user = self.remote_user
|
||||||
|
request.user.is_superuser = True
|
||||||
|
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
|
||||||
|
view(request, status.id)
|
||||||
|
activity = json.loads(mock.call_args_list[0][0][1])
|
||||||
|
self.assertEqual(activity["type"], "Delete")
|
||||||
|
self.assertEqual(activity["object"]["type"], "Tombstone")
|
||||||
|
status.refresh_from_db()
|
||||||
|
self.assertTrue(status.deleted)
|
||||||
|
|
|
@ -55,6 +55,24 @@ urlpatterns = [
|
||||||
r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites"
|
r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites"
|
||||||
),
|
),
|
||||||
re_path(r"^invite/(?P<code>[A-Za-z0-9]+)/?$", views.Invite.as_view()),
|
re_path(r"^invite/(?P<code>[A-Za-z0-9]+)/?$", views.Invite.as_view()),
|
||||||
|
# moderation
|
||||||
|
re_path(r"^settings/reports/?$", views.Reports.as_view(), name="settings-reports"),
|
||||||
|
re_path(
|
||||||
|
r"^settings/reports/(?P<report_id>\d+)/?$",
|
||||||
|
views.Report.as_view(),
|
||||||
|
name="settings-report",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^settings/reports/(?P<report_id>\d+)/deactivate/?$",
|
||||||
|
views.deactivate_user,
|
||||||
|
name="settings-report-deactivate",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^settings/reports/(?P<report_id>\d+)/resolve/?$",
|
||||||
|
views.resolve_report,
|
||||||
|
name="settings-report-resolve",
|
||||||
|
),
|
||||||
|
re_path(r"^report/?$", views.make_report, name="report"),
|
||||||
# landing pages
|
# landing pages
|
||||||
re_path(r"^about/?$", views.About.as_view()),
|
re_path(r"^about/?$", views.About.as_view()),
|
||||||
path("", views.Home.as_view()),
|
path("", views.Home.as_view()),
|
||||||
|
@ -62,10 +80,13 @@ urlpatterns = [
|
||||||
re_path(r"^notifications/?$", views.Notifications.as_view()),
|
re_path(r"^notifications/?$", views.Notifications.as_view()),
|
||||||
# feeds
|
# feeds
|
||||||
re_path(r"^(?P<tab>home|local|federated)/?$", views.Feed.as_view()),
|
re_path(r"^(?P<tab>home|local|federated)/?$", views.Feed.as_view()),
|
||||||
re_path(r"^direct-messages/?$", views.DirectMessage.as_view()),
|
re_path(
|
||||||
|
r"^direct-messages/?$", views.DirectMessage.as_view(), name="direct-messages"
|
||||||
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^direct-messages/(?P<username>%s)?$" % regex.username,
|
r"^direct-messages/(?P<username>%s)?$" % regex.username,
|
||||||
views.DirectMessage.as_view(),
|
views.DirectMessage.as_view(),
|
||||||
|
name="direct-messages-user",
|
||||||
),
|
),
|
||||||
# search
|
# search
|
||||||
re_path(r"^search/?$", views.Search.as_view()),
|
re_path(r"^search/?$", views.Search.as_view()),
|
||||||
|
|
|
@ -20,15 +20,16 @@ from .notifications import Notifications
|
||||||
from .outbox import Outbox
|
from .outbox import Outbox
|
||||||
from .reading import edit_readthrough, create_readthrough, delete_readthrough
|
from .reading import edit_readthrough, create_readthrough, delete_readthrough
|
||||||
from .reading import start_reading, finish_reading, delete_progressupdate
|
from .reading import start_reading, finish_reading, delete_progressupdate
|
||||||
|
from .reports import Report, Reports, make_report, resolve_report, deactivate_user
|
||||||
from .rss_feed import RssFeed
|
from .rss_feed import RssFeed
|
||||||
from .password import PasswordResetRequest, PasswordReset, ChangePassword
|
from .password import PasswordResetRequest, PasswordReset, ChangePassword
|
||||||
from .tag import Tag, AddTag, RemoveTag
|
|
||||||
from .search import Search
|
from .search import Search
|
||||||
from .shelf import Shelf
|
from .shelf import Shelf
|
||||||
from .shelf import user_shelves_page, create_shelf, delete_shelf
|
from .shelf import user_shelves_page, create_shelf, delete_shelf
|
||||||
from .shelf import shelve, unshelve
|
from .shelf import shelve, unshelve
|
||||||
from .site import Site
|
from .site import Site
|
||||||
from .status import CreateStatus, DeleteStatus
|
from .status import CreateStatus, DeleteStatus
|
||||||
|
from .tag import Tag, AddTag, RemoveTag
|
||||||
from .updates import Updates
|
from .updates import Updates
|
||||||
from .user import User, EditUser, Followers, Following
|
from .user import User, EditUser, Followers, Following
|
||||||
from .isbn import Isbn
|
from .isbn import Isbn
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
""" moderation via flagged posts and users """
|
||||||
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
|
from bookwyrm import forms, models
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
@method_decorator(login_required, name="dispatch")
|
||||||
|
@method_decorator(
|
||||||
|
permission_required("bookwyrm.moderate_user", raise_exception=True),
|
||||||
|
name="dispatch",
|
||||||
|
)
|
||||||
|
@method_decorator(
|
||||||
|
permission_required("bookwyrm.moderate_post", raise_exception=True),
|
||||||
|
name="dispatch",
|
||||||
|
)
|
||||||
|
class Reports(View):
|
||||||
|
""" list of reports """
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
""" view current reports """
|
||||||
|
resolved = request.GET.get("resolved") == "true"
|
||||||
|
data = {
|
||||||
|
"resolved": resolved,
|
||||||
|
"reports": models.Report.objects.filter(resolved=resolved),
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, "moderation/reports.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name="dispatch")
|
||||||
|
@method_decorator(
|
||||||
|
permission_required("bookwyrm.moderate_user", raise_exception=True),
|
||||||
|
name="dispatch",
|
||||||
|
)
|
||||||
|
@method_decorator(
|
||||||
|
permission_required("bookwyrm.moderate_post", raise_exception=True),
|
||||||
|
name="dispatch",
|
||||||
|
)
|
||||||
|
class Report(View):
|
||||||
|
""" view a specific report """
|
||||||
|
|
||||||
|
def get(self, request, report_id):
|
||||||
|
""" load a report """
|
||||||
|
data = {
|
||||||
|
"report": get_object_or_404(models.Report, id=report_id),
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, "moderation/report.html", data)
|
||||||
|
|
||||||
|
def post(self, request, report_id):
|
||||||
|
""" comment on a report """
|
||||||
|
report = get_object_or_404(models.Report, id=report_id)
|
||||||
|
models.ReportComment.objects.create(
|
||||||
|
user=request.user,
|
||||||
|
report=report,
|
||||||
|
note=request.POST.get("note"),
|
||||||
|
)
|
||||||
|
return redirect("settings-report", report.id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("bookwyrm_moderate_user")
|
||||||
|
def deactivate_user(_, report_id):
|
||||||
|
""" mark an account as inactive """
|
||||||
|
report = get_object_or_404(models.Report, id=report_id)
|
||||||
|
report.user.is_active = not report.user.is_active
|
||||||
|
report.user.save()
|
||||||
|
return redirect("settings-report", report.id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("bookwyrm_moderate_post")
|
||||||
|
def resolve_report(_, report_id):
|
||||||
|
""" mark a report as (un)resolved """
|
||||||
|
report = get_object_or_404(models.Report, id=report_id)
|
||||||
|
report.resolved = not report.resolved
|
||||||
|
report.save()
|
||||||
|
if not report.resolved:
|
||||||
|
return redirect("settings-report", report.id)
|
||||||
|
return redirect("settings-reports")
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def make_report(request):
|
||||||
|
""" a user reports something """
|
||||||
|
form = forms.ReportForm(request.POST)
|
||||||
|
if not form.is_valid():
|
||||||
|
print(form.errors)
|
||||||
|
return redirect(request.headers.get("Referer", "/"))
|
||||||
|
|
||||||
|
form.save()
|
||||||
|
return redirect(request.headers.get("Referer", "/"))
|
|
@ -75,7 +75,7 @@ class DeleteStatus(View):
|
||||||
status = get_object_or_404(models.Status, id=status_id)
|
status = get_object_or_404(models.Status, id=status_id)
|
||||||
|
|
||||||
# don't let people delete other people's statuses
|
# don't let people delete other people's statuses
|
||||||
if status.user != request.user:
|
if status.user != request.user and not request.user.has_perm("moderate_post"):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
# perform deletion
|
# perform deletion
|
||||||
|
|
Loading…
Reference in New Issue