feat: Add basic reporting structure

This commit is contained in:
moanos [he/him] 2024-03-22 12:45:50 +01:00
parent 86f02214d7
commit 3516ca1d29
15 changed files with 277 additions and 5 deletions

View File

@ -2,7 +2,7 @@ from django.contrib import admin
from django.contrib import admin
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, Report
admin.site.register(Animal)
admin.site.register(Species)
@ -11,3 +11,5 @@ admin.site.register(Location)
admin.site.register(AdoptionNotice)
admin.site.register(Rule)
admin.site.register(Image)
admin.site.register(Report)
admin.site.register(ModerationAction)

View File

@ -1,5 +1,5 @@
from django import forms
from .models import AdoptionNotice, Animal, Image
from .models import AdoptionNotice, Animal, Image, Report, ModerationAction
class DateInput(forms.DateInput):
@ -29,3 +29,15 @@ class ImageForm(forms.ModelForm):
class Meta:
model = Image
fields = ('title', 'image', 'alt_text')
class ReportForm(forms.ModelForm):
class Meta:
model = Report
fields = ('reported_broken_rules', 'comment')
class ModerationActionForm(forms.ModelForm):
class Meta:
model = ModerationAction
fields = ('action', 'public_comment', 'private_comment')

View File

@ -33,6 +33,18 @@ class Command(BaseCommand):
rule2 = baker.make(Rule,
title="Keep al least the minimum number of animals for species",
rule_text="This is not markdown")
rule3 = baker.make(Rule,
title="Rule three",
rule_text="Everything needs at least three rules")
report1 = baker.make(Report, reported_broken_rules=[rule1, rule2], comment="This seems sketchy")
moderation_action1 = baker.make(ModerationAction,
action=ModerationAction.COMMENT,
public_comment="This has been seen by a moderator")
moderation_action1 = baker.make(ModerationAction,
action=ModerationAction.DELETE,
public_comment="A moderator has deleted the reported content")
User.objects.create_user('test', password='foobar')
User.objects.create_superuser(username="admin", password="admin")

View File

@ -0,0 +1,37 @@
# Generated by Django 5.0.3 on 2024-03-22 09:20
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0004_alter_animal_sex'),
]
operations = [
migrations.CreateModel(
name='Report',
fields=[
('id', models.UUIDField(default=uuid.uuid4, help_text='ID dieses reports', primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('action taken', 'Action was taken'), ('no action taken', 'No action was taken'), ('waiting', 'Waiting for moderator action')], max_length=30)),
('comment', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice')),
('reported_broken_rules', models.ManyToManyField(blank=True, to='fellchensammlung.rule')),
],
),
migrations.CreateModel(
name='ModerationAction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action', models.CharField(choices=[('user_banned', 'User was banned'), ('content_deleted', 'Content was deleted'), ('other_action_taken', 'Other action was taken'), ('no_action_taken', 'No action was taken')], max_length=30)),
('created_at', models.DateTimeField(auto_now_add=True)),
('public_comment', models.TextField()),
('private_comment', models.TextField()),
('report', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.report')),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.3 on 2024-03-22 11:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0005_report_moderationaction'),
]
operations = [
migrations.AlterField(
model_name='moderationaction',
name='action',
field=models.CharField(choices=[('user_banned', 'User was banned'), ('content_deleted', 'Content was deleted'), ('comment', 'Comment was added'), ('other_action_taken', 'Other action was taken'), ('no_action_taken', 'No action was taken')], max_length=30),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.3 on 2024-03-22 11:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0006_alter_moderationaction_action'),
]
operations = [
migrations.AlterField(
model_name='moderationaction',
name='private_comment',
field=models.TextField(blank=True),
),
migrations.AlterField(
model_name='moderationaction',
name='public_comment',
field=models.TextField(blank=True),
),
]

View File

@ -1,3 +1,5 @@
import uuid
from django.db import models
from django.urls import reverse
from django.contrib.auth.models import User
@ -180,6 +182,7 @@ class MarkdownContent(models.Model):
def __str__(self):
return self.title
class Rule(models.Model):
"""
Class to store rules
@ -192,3 +195,55 @@ class Rule(models.Model):
def __str__(self):
return self.title
class Report(models.Model):
ACTION_TAKEN = "action taken"
NO_ACTION_TAKEN = "no action taken"
WAITING = "waiting"
STATES = {
ACTION_TAKEN: "Action was taken",
NO_ACTION_TAKEN: "No action was taken",
WAITING: "Waiting for moderator action",
}
id = models.UUIDField(primary_key=True, default=uuid.uuid4, help_text=_('ID dieses reports'),
verbose_name=_('ID'))
status = models.CharField(max_length=30, choices=STATES)
reported_broken_rules = models.ManyToManyField(Rule, blank=True)
adoption_notice = models.ForeignKey("AdoptionNotice", on_delete=models.CASCADE)
comment = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"[{self.status}]: {self.adoption_notice.name}"
def get_reported_rules(self):
return self.reported_broken_rules.all()
def get_moderation_actions(self):
return ModerationAction.objects.filter(report=self)
class ModerationAction(models.Model):
BAN = "user_banned"
DELETE = "content_deleted"
COMMENT = "comment"
OTHER = "other_action_taken"
NONE = "no_action_taken"
ACTIONS = {
BAN: "User was banned",
DELETE: "Content was deleted",
COMMENT: "Comment was added",
OTHER: "Other action was taken",
NONE: "No action was taken"
}
action = models.CharField(max_length=30, choices=ACTIONS.items())
created_at = models.DateTimeField(auto_now_add=True)
public_comment = models.TextField(blank=True)
# Only visible to moderator
private_comment = models.TextField(blank=True)
report = models.ForeignKey(Report, on_delete=models.CASCADE)
# TODO: Needs field for moderator that performed the action
def __str__(self):
return f"[{self.action}]: {self.public_comment}"

View File

@ -277,6 +277,23 @@ h1 {
box-sizing: border-box;
}
.container-list-moderation-actions {
display: flex;
flex-wrap: wrap;
}
.card-moderation-action {
width: 25%;
margin: 10px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 5px;
padding: 2%;
box-sizing: border-box;
}
@media (max-width: 920px) {
.card-rule {
width: 100%;

View File

@ -0,0 +1,17 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% block content %}
{% if form_complete %}
<h1>Erfolgreich gemeldet</h1>
Wenn du sehen willst welche Moderationsentscheidungen getroffen werden, schau zu einem späteren Zeitpunkt wieder auf dieser Seite vorbei.
Wenn du unzufrieden mit der Entscheidung bist kannst du per Mail an <a href="mailto:info@notfellchen.org">info@notfellchen.org</a> Einspruch einlegen.
{% endif %}
{% include "fellchensammlung/partial-report.html" %}
<h2>Moderationsverlauf</h2>
{% if report.get_moderation_actions %}
{% include "fellchensammlung/list-moderation-action.html" %}
{% else %}
Bisher wurden keine Maßnahmen vorgenommen
{% endif %}
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% block content %}
<h1>Melden</h1>
Wenn diese Vermittlung nicht unseren <a href='{% url "about" %}'>Regeln</a> entspricht, wähle bitte eine der folgenden Regeln aus und hinterlasse einen Kommentar der es detaillierter erklärt, insbesondere wenn der Regelverstoß nicht offensichtlich ist.
<form method = "post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button class="button-report" type="submit">Melden</button>
</form>
{% endblock %}

View File

@ -0,0 +1,5 @@
<div class="container-list-moderation-actions">
{% for moderation_action in moderation_actions %}
{% include "fellchensammlung/partial-moderation-action.html" %}
{% endfor %}
</div>

View File

@ -0,0 +1,4 @@
<div class="card-animal">
<div class="tag">{{ moderation_action.action }}</div>
<p>{{ moderation_action.public_comment }}</p>
</div>

View File

@ -0,0 +1,14 @@
<div class="report">
<h2>Meldung von {{ report.adoption_notice.name }}</h2>
{% if report.reported_broken_rules %}
Regeln gegen die Verstoßen wurde
<ul>
{% for rule in report.get_reported_rules %}
<li>{{ rule }}</li>
{% endfor %}
</ul>
{% endif %}
<p><b>Kommentar zur Meldung:</b>
{{ report.comment }}
</p>
</div>

View File

@ -20,4 +20,11 @@ urlpatterns = [
name="add-animal-to-adoption"),
path("ueber-uns/", views.about, name="about"),
#############
## Reports ##
#############
path("melden/<int:adoption_notice_id>/", views.report_adoption, name="report-adoption-notices"),
path("meldung/<uuid:report_id>/", views.report_detail, name="report-detail"),
path("meldung/<uuid:report_id>/sucess", views.report_detail_success, name="report-detail-success"),
]

View File

@ -3,8 +3,8 @@ from django.http import HttpResponse
from django.urls import reverse
import markdown
from fellchensammlung.models import AdoptionNotice, MarkdownContent, Animal, Rule, Image
from .forms import AdoptionNoticeForm, AnimalForm, ImageForm
from fellchensammlung.models import AdoptionNotice, MarkdownContent, Animal, Rule, Image, Report, ModerationAction
from .forms import AdoptionNoticeForm, AnimalForm, ImageForm, ReportForm
def index(request):
@ -81,3 +81,40 @@ def about(request):
"fellchensammlung/about.html",
context=context
)
def report_adoption(request, adoption_notice_id):
"""
Form to report adoption notices
"""
if request.method == 'POST':
form = ReportForm(request.POST)
if form.is_valid():
report_instance = form.save(commit=False)
report_instance.adoption_notice_id = adoption_notice_id
report_instance.status = Report.WAITING
report_instance.save()
return redirect(reverse("report-detail-success", args=[report_instance.pk], ))
else:
form = ReportForm()
return render(request, 'fellchensammlung/form-report.html', {'form': form})
def report_detail(request, report_id, form_complete=False):
"""
Detailed view of a report, including moderation actions
"""
report = Report.objects.get(pk=report_id)
moderation_actions = ModerationAction.objects.filter(report_id=report_id)
context = {"report": report, "moderation_actions": moderation_actions, "form_complete": form_complete}
return render(request, 'fellchensammlung/detail-report.html', context)
def report_detail_success(request, report_id):
"""
Calls the report detail view with form_complete set to true, so success message shows
"""
return report_detail(request, report_id, form_complete=True)