diff --git a/documents/admin.py b/documents/admin.py index 44948cb5..aba81856 100644 --- a/documents/admin.py +++ b/documents/admin.py @@ -1,8 +1,9 @@ from django.conf import settings from django.contrib import admin +from django.db.models import Count from django.db.models.query import QuerySet -from .models import BulkDocuments, Document, DocumentError, Vote +from .models import BulkDocuments, Document, DocumentError, DocumentReport, Vote @admin.action(description="Reprocess selected documents") @@ -73,6 +74,7 @@ class DocumentAdmin(admin.ModelAdmin): "pages", "views", "downloads", + "report_count", "hidden", "state", "created", @@ -124,12 +126,37 @@ class DocumentAdmin(admin.ModelAdmin): ), ) + def get_queryset(self, request): + queryset = super().get_queryset(request) + queryset = queryset.annotate(report_count=Count("reports")) + return queryset + + @admin.display(ordering="report_count", description="Reports") + def report_count(self, obj: Document) -> int: + return obj.report_count # type: ignore[attr-defined] + @admin.register(DocumentError) class DocumentErrorAdmin(admin.ModelAdmin): list_display = ("exception", "document", "task_id") +@admin.register(DocumentReport) +class DocumentReportAdmin(admin.ModelAdmin): + list_display = ( + "id", + "document", + "problem_type", + "user", + "created", + ) + list_filter = ("problem_type", "created") + search_fields = ("document__name", "user__netid", "user__email") + raw_id_fields = ("user", "document") + date_hierarchy = "created" + readonly_fields = ("created",) + + @admin.register(BulkDocuments) class BulkDocumentsAdmin(admin.ModelAdmin): list_display = ("url", "processed", "course", "user", "created") diff --git a/documents/forms.py b/documents/forms.py index 1c900121..370f21c8 100644 --- a/documents/forms.py +++ b/documents/forms.py @@ -2,7 +2,7 @@ from django.conf import settings from django.core.exceptions import ValidationError -from documents.models import Document +from documents.models import Document, DocumentReport def validate_uploaded_file(file): @@ -61,3 +61,22 @@ class ReUploadForm(forms.Form): class MultipleUploadFileForm(UploadFileForm): pass + + +class DocumentReportForm(forms.ModelForm): + class Meta: + model = DocumentReport + fields = ("problem_type", "description") + widgets = { + "problem_type": forms.Select( + attrs={ + "class": "form-select", + } + ), + "description": forms.Textarea( + attrs={ + "class": "form-control", + "rows": 2, + } + ), + } diff --git a/documents/migrations/0006_documentreport.py b/documents/migrations/0006_documentreport.py new file mode 100644 index 00000000..0ce24b87 --- /dev/null +++ b/documents/migrations/0006_documentreport.py @@ -0,0 +1,72 @@ +# Generated by Django 6.0 on 2025-12-25 19:59 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "documents", + "0005_alter_vote_unique_together_alter_bulkdocuments_id_and_more", + ), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="DocumentReport", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "problem_type", + models.CharField( + choices=[ + ("wrong_module", "Ce document est dans le mauvais cours"), + ("wrong_title", "Le titre ou la description est erroné"), + ("low_quality", "Contenu de mauvaise qualité ou inutile"), + ("readability", "Problème de lisibilité"), + ("outdated", "Document obsolète"), + ("other", "Autre raison"), + ], + max_length=20, + verbose_name="Type de problème", + ), + ), + ( + "description", + models.TextField( + blank=True, default="", verbose_name="Description" + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ( + "document", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="reports", + to="documents.document", + verbose_name="Document", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="Utilisateur", + ), + ), + ], + ), + ] diff --git a/documents/models.py b/documents/models.py index e0d26968..721a5678 100644 --- a/documents/models.py +++ b/documents/models.py @@ -186,6 +186,55 @@ class Meta: ] +class DocumentReport(models.Model): + class ProblemType(models.TextChoices): + WRONG_MODULE = "wrong_module", "Ce document est dans le mauvais cours" + WRONG_TITLE = "wrong_title", "Le titre ou la description est erroné" + LOW_QUALITY = "low_quality", "Contenu de mauvaise qualité ou inutile" + READABILITY = "readability", "Problème de lisibilité" + OUTDATED = "outdated", "Document obsolète" + OTHER = "other", "Autre raison" + + @classmethod + def get_description(cls, value: str) -> str: + """Get the detailed description for a problem type.""" + descriptions = { + cls.WRONG_MODULE.value: "Ce document appartient à un autre cours", + cls.WRONG_TITLE.value: "Le contenu du document n'est pas correctement décrit", + cls.LOW_QUALITY.value: "Le contenu peut être non pertinent, contenir uniquement le plan du cours, avoir de nombreuses fautes ou être (presque) vide", + cls.READABILITY.value: "Le document est difficile à lire en raison d'une mauvaise écriture ou d'une photo de mauvaise qualité", + cls.OUTDATED.value: "Le document est dépassé ou ne correspond plus au contenu actuel du cours", + cls.OTHER.value: "Une autre raison non listée ci-dessus", + } + return descriptions.get(value, "") + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + verbose_name="Utilisateur", + ) + document = models.ForeignKey( + Document, + on_delete=models.CASCADE, + related_name="reports", + verbose_name="Document", + ) + problem_type = models.CharField( + max_length=20, + choices=ProblemType.choices, + verbose_name="Type de problème", + ) + description = models.TextField( + blank=True, + default="", + verbose_name="Description", + ) + created = models.DateTimeField(auto_now_add=True) + + def __str__(self) -> str: + return f"Report on {self.document.name} by {self.user}" + + class DocumentError(models.Model): document = models.ForeignKey(Document, on_delete=models.CASCADE) task_id = models.CharField(max_length=255) diff --git a/documents/templates/documents/document_report.html b/documents/templates/documents/document_report.html new file mode 100644 index 00000000..b69e5354 --- /dev/null +++ b/documents/templates/documents/document_report.html @@ -0,0 +1,74 @@ +{% extends "base.html" %} + +{% block title %}Signaler {{ document.name }}{% endblock %} + +{% block content %} +