diff --git a/src/fellchensammlung/admin.py b/src/fellchensammlung/admin.py index 9ae4a8e..1764b8b 100644 --- a/src/fellchensammlung/admin.py +++ b/src/fellchensammlung/admin.py @@ -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) diff --git a/src/fellchensammlung/forms.py b/src/fellchensammlung/forms.py index db9314d..305bc67 100644 --- a/src/fellchensammlung/forms.py +++ b/src/fellchensammlung/forms.py @@ -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') diff --git a/src/fellchensammlung/management/commands/populate_db.py b/src/fellchensammlung/management/commands/populate_db.py index 746c5af..88d47e4 100644 --- a/src/fellchensammlung/management/commands/populate_db.py +++ b/src/fellchensammlung/management/commands/populate_db.py @@ -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") diff --git a/src/fellchensammlung/migrations/0005_report_moderationaction.py b/src/fellchensammlung/migrations/0005_report_moderationaction.py new file mode 100644 index 0000000..133f713 --- /dev/null +++ b/src/fellchensammlung/migrations/0005_report_moderationaction.py @@ -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')), + ], + ), + ] diff --git a/src/fellchensammlung/migrations/0006_alter_moderationaction_action.py b/src/fellchensammlung/migrations/0006_alter_moderationaction_action.py new file mode 100644 index 0000000..ba1e0b3 --- /dev/null +++ b/src/fellchensammlung/migrations/0006_alter_moderationaction_action.py @@ -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), + ), + ] diff --git a/src/fellchensammlung/migrations/0007_alter_moderationaction_private_comment_and_more.py b/src/fellchensammlung/migrations/0007_alter_moderationaction_private_comment_and_more.py new file mode 100644 index 0000000..5a3647a --- /dev/null +++ b/src/fellchensammlung/migrations/0007_alter_moderationaction_private_comment_and_more.py @@ -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), + ), + ] diff --git a/src/fellchensammlung/models.py b/src/fellchensammlung/models.py index d6c0a99..92a1470 100644 --- a/src/fellchensammlung/models.py +++ b/src/fellchensammlung/models.py @@ -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}" diff --git a/src/fellchensammlung/static/fellchensammlung/css/styles.css b/src/fellchensammlung/static/fellchensammlung/css/styles.css index 565166b..480e71a 100644 --- a/src/fellchensammlung/static/fellchensammlung/css/styles.css +++ b/src/fellchensammlung/static/fellchensammlung/css/styles.css @@ -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%; diff --git a/src/fellchensammlung/templates/fellchensammlung/detail-report.html b/src/fellchensammlung/templates/fellchensammlung/detail-report.html new file mode 100644 index 0000000..66c7fec --- /dev/null +++ b/src/fellchensammlung/templates/fellchensammlung/detail-report.html @@ -0,0 +1,17 @@ +{% extends "fellchensammlung/base_generic.html" %} +{% load i18n %} + +{% block content %} + {% if form_complete %} +

Erfolgreich gemeldet

+ 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 info@notfellchen.org Einspruch einlegen. + {% endif %} + {% include "fellchensammlung/partial-report.html" %} +

Moderationsverlauf

+ {% if report.get_moderation_actions %} + {% include "fellchensammlung/list-moderation-action.html" %} + {% else %} + Bisher wurden keine Maßnahmen vorgenommen + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/src/fellchensammlung/templates/fellchensammlung/form-report.html b/src/fellchensammlung/templates/fellchensammlung/form-report.html new file mode 100644 index 0000000..4491ed2 --- /dev/null +++ b/src/fellchensammlung/templates/fellchensammlung/form-report.html @@ -0,0 +1,12 @@ +{% extends "fellchensammlung/base_generic.html" %} +{% load i18n %} + +{% block content %} +

Melden

+ Wenn diese Vermittlung nicht unseren Regeln 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. +
+ {% csrf_token %} + {{ form.as_p }} + +
+{% endblock %} \ No newline at end of file diff --git a/src/fellchensammlung/templates/fellchensammlung/list-moderation-action.html b/src/fellchensammlung/templates/fellchensammlung/list-moderation-action.html new file mode 100644 index 0000000..bf414ba --- /dev/null +++ b/src/fellchensammlung/templates/fellchensammlung/list-moderation-action.html @@ -0,0 +1,5 @@ +
+{% for moderation_action in moderation_actions %} + {% include "fellchensammlung/partial-moderation-action.html" %} +{% endfor %} +
\ No newline at end of file diff --git a/src/fellchensammlung/templates/fellchensammlung/partial-moderation-action.html b/src/fellchensammlung/templates/fellchensammlung/partial-moderation-action.html new file mode 100644 index 0000000..c31d924 --- /dev/null +++ b/src/fellchensammlung/templates/fellchensammlung/partial-moderation-action.html @@ -0,0 +1,4 @@ +
+
{{ moderation_action.action }}
+

{{ moderation_action.public_comment }}

+
diff --git a/src/fellchensammlung/templates/fellchensammlung/partial-report.html b/src/fellchensammlung/templates/fellchensammlung/partial-report.html new file mode 100644 index 0000000..e133b57 --- /dev/null +++ b/src/fellchensammlung/templates/fellchensammlung/partial-report.html @@ -0,0 +1,14 @@ +
+

Meldung von {{ report.adoption_notice.name }}

+ {% if report.reported_broken_rules %} + Regeln gegen die Verstoßen wurde + + {% endif %} +

Kommentar zur Meldung: + {{ report.comment }} +

+
\ No newline at end of file diff --git a/src/fellchensammlung/urls.py b/src/fellchensammlung/urls.py index b1e105f..94937be 100644 --- a/src/fellchensammlung/urls.py +++ b/src/fellchensammlung/urls.py @@ -20,4 +20,11 @@ urlpatterns = [ name="add-animal-to-adoption"), path("ueber-uns/", views.about, name="about"), -] \ No newline at end of file + + ############# + ## Reports ## + ############# + path("melden//", views.report_adoption, name="report-adoption-notices"), + path("meldung//", views.report_detail, name="report-detail"), + path("meldung//sucess", views.report_detail_success, name="report-detail-success"), +] diff --git a/src/fellchensammlung/views.py b/src/fellchensammlung/views.py index 2ad8897..525dd4d 100644 --- a/src/fellchensammlung/views.py +++ b/src/fellchensammlung/views.py @@ -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)