Merge branch 'pedagogy_v2_moderation' into 'pedagogy_v2'

Pedagogy comments moderation

See merge request ae/Sith!215
This commit is contained in:
Antoine Bartuccio 2019-07-04 15:54:24 +02:00
commit 4f7a8661ba
10 changed files with 600 additions and 21 deletions

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-04 13:00
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("core", "0029_auto_20180426_2013")]
operations = [
migrations.AlterField(
model_name="notification",
name="type",
field=models.CharField(
choices=[
("POSTER_MODERATION", "A new poster needs to be moderated"),
("MAILING_MODERATION", "A new mailing list needs to be moderated"),
(
"PEDAGOGY_MODERATION",
"A new pedagogy comment has been signaled for moderation",
),
("NEWS_MODERATION", "There are %s fresh news to be moderated"),
("FILE_MODERATION", "New files to be moderated"),
(
"SAS_MODERATION",
"There are %s pictures to be moderated in the SAS",
),
("NEW_PICTURES", "You've been identified on some pictures"),
("REFILLING", "You just refilled of %s"),
("SELLING", "You just bought %s"),
("GENERIC", "You have a notification"),
],
default="GENERIC",
max_length=32,
verbose_name="type",
),
)
]

View File

@ -23,11 +23,12 @@
#
from django import forms
from django.utils.translation import ugettext_lazy as _
from core.views.forms import MarkdownInput
from core.models import User
from pedagogy.models import UV, UVComment
from pedagogy.models import UV, UVComment, UVCommentReport
class UVForm(forms.ModelForm):
@ -100,3 +101,45 @@ class UVCommentForm(forms.ModelForm):
self.fields["author"].initial = author_id
self.fields["uv"].queryset = UV.objects.filter(id=uv_id).all()
self.fields["uv"].initial = uv_id
class UVCommentReportForm(forms.ModelForm):
"""
Form handeling creation and edit of an UVReport
"""
class Meta:
model = UVCommentReport
fields = ("comment", "reporter", "reason")
widgets = {
"comment": forms.HiddenInput,
"reporter": forms.HiddenInput,
"reason": MarkdownInput,
}
def __init__(self, reporter_id, comment_id, *args, **kwargs):
super(UVCommentReportForm, self).__init__(*args, **kwargs)
self.fields["reporter"].queryset = User.objects.filter(id=reporter_id).all()
self.fields["reporter"].initial = reporter_id
self.fields["comment"].queryset = UVComment.objects.filter(id=comment_id).all()
self.fields["comment"].initial = comment_id
class UVCommentModerationForm(forms.Form):
"""
Form handeling bulk comment deletion
"""
accepted_reports = forms.ModelMultipleChoiceField(
UVCommentReport.objects.all(),
label=_("Accepted reports"),
widget=forms.CheckboxSelectMultiple,
required=False,
)
denied_reports = forms.ModelMultipleChoiceField(
UVCommentReport.objects.all(),
label=_("Denied reports"),
widget=forms.CheckboxSelectMultiple,
required=False,
)

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-06-18 20:07
# Generated by Django 1.11.20 on 2019-06-19 23:05
from __future__ import unicode_literals
from django.conf import settings
@ -190,7 +190,7 @@ class Migration(migrations.Migration):
verbose_name="ID",
),
),
("comment", models.TextField(verbose_name="comment")),
("comment", models.TextField(blank=True, verbose_name="comment")),
(
"grade_global",
models.IntegerField(
@ -281,7 +281,26 @@ class Migration(migrations.Migration):
serialize=False,
verbose_name="ID",
),
)
),
("reason", models.TextField(verbose_name="reason")),
(
"comment",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="reports",
to="pedagogy.UVComment",
verbose_name="report",
),
),
(
"reporter",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="reported_uv_comment",
to=settings.AUTH_USER_MODEL,
verbose_name="reporter",
),
),
],
),
migrations.CreateModel(

View File

@ -27,6 +27,7 @@ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.core import validators
from django.conf import settings
from django.utils.functional import cached_property
from core.models import User
@ -165,6 +166,13 @@ class UVComment(models.Model):
"""
return self.author == user or user.is_owner(self.uv)
@cached_property
def is_reported(self):
"""
Return True if someone reported this UV
"""
return self.reports.exists()
def __str__(self):
return "%s - %s" % (self.uv, self.author)
@ -176,7 +184,7 @@ class UVComment(models.Model):
blank=False,
)
uv = models.ForeignKey(UV, related_name="comments", verbose_name=_("uv"))
comment = models.TextField(_("comment"))
comment = models.TextField(_("comment"), blank=True)
grade_global = models.IntegerField(
_("global grade"),
validators=[validators.MinValueValidator(-1), validators.MaxValueValidator(4)],
@ -251,4 +259,19 @@ class UVCommentReport(models.Model):
Report an inapropriate comment
"""
pass
def is_owned_by(self, user):
"""
Can be created by a pedagogy admin, a superuser or a subscriber
"""
return user.is_subscribed or user.is_owner(self.comment.uv)
comment = models.ForeignKey(
UVComment,
related_name="reports",
verbose_name=_("report"),
on_delete=models.CASCADE,
)
reporter = models.ForeignKey(
User, related_name="reported_uv_comment", verbose_name=_("reporter")
)
reason = models.TextField(_("reason"))

View File

@ -0,0 +1,63 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import select_all_checkbox %}
{% block title %}
{% trans %}UV comment moderation{% endtrans %}
{% endblock title %}
{% block content %}
<form action="{{ url('pedagogy:moderation') }}", id="moderation_delete_form" method="post" enctype="multipart/form-data">
{% csrf_token %}
<p style="margin-bottom: 1em;">{{ select_all_checkbox("moderation_delete_form") }}</p>
<table>
<thead>
<tr>
<td>{% trans %}UV{% endtrans %}</td>
<td>{% trans %}Comment{% endtrans %}</td>
<td>{% trans %}Reason{% endtrans %}</td>
<td>{% trans %}Delete{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% set queryset = form.accepted_reports.field.queryset %}
{% for widget in form.accepted_reports.subwidgets %}
{% set report = queryset.get(id=widget.data.value) %}
<tr>
<td><a href="{{ url('pedagogy:uv_detail', uv_id=report.comment.uv.id) }}">{{ report.comment.uv }}</a></td>
<td>{{ report.comment.comment|markdown }}</td>
<td>{{ report.reason|markdown }}</td>
<td>{{ widget.tag() }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><input type="submit" value="{% trans %}Delete comments{% endtrans %}"></p>
</form>
<form action="{{ url('pedagogy:moderation') }}", id="moderation_keep_form" method="post" enctype="multipart/form-data">
{% csrf_token %}
<p style="margin-bottom: 1em;">{{ select_all_checkbox("moderation_keep_form") }}</p>
<table>
<thead>
<tr>
<td>{% trans %}UV{% endtrans %}</td>
<td>{% trans %}Comment{% endtrans %}</td>
<td>{% trans %}Reason{% endtrans %}</td>
<td>{% trans %}Delete{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% set queryset = form.denied_reports.field.queryset %}
{% for widget in form.denied_reports.subwidgets %}
{% set report = queryset.get(id=widget.data.value) %}
<tr>
<td><a href="{{ url('pedagogy:uv_detail', uv_id=report.comment.uv.id) }}">{{ report.comment.uv }}</a></td>
<td>{{ report.comment.comment|markdown }}</td>
<td>{{ report.reason|markdown }}</td>
<td>{{ widget.tag() }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><input type="submit" value="{% trans %}Delete report{% endtrans %}"></p>
</form>
{% endblock content %}

View File

@ -29,6 +29,10 @@
<p><a href="{{ url('pedagogy:comment_update', comment_id=comment.id) }}">{% trans %}Edit{% endtrans %}</a></p>
<p><a href="{{ url('pedagogy:comment_delete', comment_id=comment.id) }}">{% trans %}Delete{% endtrans %}</a></p>
{% endif %}
<p><a href="{{ url('pedagogy:comment_report', comment_id=comment.id) }}">{% trans %}Report{% endtrans %}</a></p>
{% if comment.is_reported %}
<p>{% trans %}This comment has been reported{% endtrans %}</p>
{% endif %}
{% endfor %}
{% endif %}

View File

@ -22,13 +22,14 @@
#
#
from django.conf import settings
from django.test import TestCase
from django.core.urlresolvers import reverse
from django.core.management import call_command
from core.models import User
from core.models import User, Notification
from pedagogy.models import UV, UVComment
from pedagogy.models import UV, UVComment, UVCommentReport
def create_uv_template(user_id, code="IFC1", exclude_list=[]):
@ -727,3 +728,323 @@ class UVSearchTest(TestCase):
# Search with credit type
response = self.client.get(reverse("pedagogy:guide"), {"credit_type": "TM"})
self.assertNotContains(response, text="PA00")
class UVModerationFormTest(TestCase):
"""
Test moderation view
Assert access rights and if the form works well
"""
def setUp(self):
call_command("populate")
self.krophil = User.objects.get(username="krophil")
# Prepare a comment
comment_kwargs = create_uv_comment_template(self.krophil.id)
comment_kwargs["author"] = self.krophil
comment_kwargs["uv"] = UV.objects.get(id=comment_kwargs["uv"])
self.comment_1 = UVComment(**comment_kwargs)
self.comment_1.save()
# Prepare another comment
comment_kwargs = create_uv_comment_template(self.krophil.id)
comment_kwargs["author"] = self.krophil
comment_kwargs["uv"] = UV.objects.get(id=comment_kwargs["uv"])
self.comment_2 = UVComment(**comment_kwargs)
self.comment_2.save()
# Prepare a comment report for comment 1
self.report_1 = UVCommentReport(
comment=self.comment_1, reporter=self.krophil, reason="C'est moche"
)
self.report_1.save()
self.report_1_bis = UVCommentReport(
comment=self.comment_1, reporter=self.krophil, reason="C'est moche 2"
)
self.report_1_bis.save()
# Prepare a comment report for comment 2
self.report_2 = UVCommentReport(
comment=self.comment_2, reporter=self.krophil, reason="C'est moche"
)
self.report_2.save()
def test_access_authorized_success(self):
# Test with root
self.client.login(username="root", password="plop")
response = self.client.get(reverse("pedagogy:moderation"))
self.assertEquals(response.status_code, 200)
# Test with pedagogy admin
self.client.login(username="tutu", password="plop")
response = self.client.get(reverse("pedagogy:moderation"))
self.assertEquals(response.status_code, 200)
def test_access_unauthorized_fail(self):
# Test with anonymous user
response = self.client.get(reverse("pedagogy:moderation"))
self.assertEquals(response.status_code, 403)
# Test with unsubscribed user
self.client.login(username="guy", password="plop")
response = self.client.get(reverse("pedagogy:moderation"))
self.assertEquals(response.status_code, 403)
# Test with subscribed user
self.client.login(username="sli", password="plop")
response = self.client.get(reverse("pedagogy:moderation"))
self.assertEquals(response.status_code, 403)
def test_do_nothing(self):
self.client.login(username="root", password="plop")
response = self.client.post(reverse("pedagogy:moderation"))
self.assertEquals(response.status_code, 302)
# Test that nothing has changed
self.assertTrue(UVCommentReport.objects.filter(id=self.report_1.id).exists())
self.assertTrue(UVComment.objects.filter(id=self.comment_1.id).exists())
self.assertTrue(
UVCommentReport.objects.filter(id=self.report_1_bis.id).exists()
)
self.assertTrue(UVCommentReport.objects.filter(id=self.report_2.id).exists())
self.assertTrue(UVComment.objects.filter(id=self.comment_2.id).exists())
def test_delete_comment(self):
self.client.login(username="root", password="plop")
response = self.client.post(
reverse("pedagogy:moderation"), {"accepted_reports": [self.report_1.id]}
)
self.assertEquals(response.status_code, 302)
# Test that the comment and it's associated report has been deleted
self.assertFalse(UVCommentReport.objects.filter(id=self.report_1.id).exists())
self.assertFalse(UVComment.objects.filter(id=self.comment_1.id).exists())
# Test that the bis report has been deleted
self.assertFalse(
UVCommentReport.objects.filter(id=self.report_1_bis.id).exists()
)
# Test that the other comment and report still exists
self.assertTrue(UVCommentReport.objects.filter(id=self.report_2.id).exists())
self.assertTrue(UVComment.objects.filter(id=self.comment_2.id).exists())
def test_delete_comment_bulk(self):
self.client.login(username="root", password="plop")
response = self.client.post(
reverse("pedagogy:moderation"),
{"accepted_reports": [self.report_1.id, self.report_2.id]},
)
self.assertEquals(response.status_code, 302)
# Test that comments and their associated reports has been deleted
self.assertFalse(UVCommentReport.objects.filter(id=self.report_1.id).exists())
self.assertFalse(UVComment.objects.filter(id=self.comment_1.id).exists())
self.assertFalse(UVCommentReport.objects.filter(id=self.report_2.id).exists())
self.assertFalse(UVComment.objects.filter(id=self.comment_2.id).exists())
# Test that the bis report has been deleted
self.assertFalse(
UVCommentReport.objects.filter(id=self.report_1_bis.id).exists()
)
def test_delete_comment_with_bis(self):
# Test case if two reports targets the same comment and are both deleted
self.client.login(username="root", password="plop")
response = self.client.post(
reverse("pedagogy:moderation"),
{"accepted_reports": [self.report_1.id, self.report_1_bis.id]},
)
self.assertEquals(response.status_code, 302)
# Test that the comment and it's associated report has been deleted
self.assertFalse(UVCommentReport.objects.filter(id=self.report_1.id).exists())
self.assertFalse(UVComment.objects.filter(id=self.comment_1.id).exists())
# Test that the bis report has been deleted
self.assertFalse(
UVCommentReport.objects.filter(id=self.report_1_bis.id).exists()
)
def test_delete_report(self):
self.client.login(username="root", password="plop")
response = self.client.post(
reverse("pedagogy:moderation"), {"denied_reports": [self.report_1.id]}
)
self.assertEquals(response.status_code, 302)
# Test that the report has been deleted and that the comment still exists
self.assertFalse(UVCommentReport.objects.filter(id=self.report_1.id).exists())
self.assertTrue(UVComment.objects.filter(id=self.comment_1.id).exists())
# Test that the bis report is still there
self.assertTrue(
UVCommentReport.objects.filter(id=self.report_1_bis.id).exists()
)
# Test that the other comment and report still exists
self.assertTrue(UVCommentReport.objects.filter(id=self.report_2.id).exists())
self.assertTrue(UVComment.objects.filter(id=self.comment_2.id).exists())
def test_delete_report_bulk(self):
self.client.login(username="root", password="plop")
response = self.client.post(
reverse("pedagogy:moderation"),
{
"denied_reports": [
self.report_1.id,
self.report_1_bis.id,
self.report_2.id,
]
},
)
self.assertEquals(response.status_code, 302)
# Test that every reports has been deleted
self.assertFalse(UVCommentReport.objects.filter(id=self.report_1.id).exists())
self.assertFalse(
UVCommentReport.objects.filter(id=self.report_1_bis.id).exists()
)
self.assertFalse(UVCommentReport.objects.filter(id=self.report_2.id).exists())
# Test that comments still exists
self.assertTrue(UVComment.objects.filter(id=self.comment_1.id).exists())
self.assertTrue(UVComment.objects.filter(id=self.comment_2.id).exists())
def test_delete_mixed(self):
self.client.login(username="root", password="plop")
response = self.client.post(
reverse("pedagogy:moderation"),
{
"accepted_reports": [self.report_2.id],
"denied_reports": [self.report_1.id],
},
)
self.assertEquals(response.status_code, 302)
# Test that report 2 and his comment has been deleted
self.assertFalse(UVCommentReport.objects.filter(id=self.report_2.id).exists())
self.assertFalse(UVComment.objects.filter(id=self.comment_2.id).exists())
# Test that report 1 has been deleted and it's comment still exists
self.assertFalse(UVCommentReport.objects.filter(id=self.report_1.id).exists())
self.assertTrue(UVComment.objects.filter(id=self.comment_1.id).exists())
# Test that report 1 bis is still there
self.assertTrue(
UVCommentReport.objects.filter(id=self.report_1_bis.id).exists()
)
def test_delete_mixed_with_bis(self):
self.client.login(username="root", password="plop")
response = self.client.post(
reverse("pedagogy:moderation"),
{
"accepted_reports": [self.report_1.id],
"denied_reports": [self.report_1_bis.id],
},
)
self.assertEquals(response.status_code, 302)
# Test that report 1 and 1 bis has been deleted
self.assertFalse(
UVCommentReport.objects.filter(
id__in=[self.report_1.id, self.report_1_bis.id]
).exists()
)
# Test that comment 1 has been deleted
self.assertFalse(UVComment.objects.filter(id=self.comment_1.id).exists())
# Test that report and comment 2 still exists
self.assertTrue(UVCommentReport.objects.filter(id=self.report_2.id).exists())
self.assertTrue(UVComment.objects.filter(id=self.comment_2.id).exists())
class UVCommentReportCreateTest(TestCase):
"""
Test report creation view view
Assert access rights and if you can create with it
"""
def setUp(self):
call_command("populate")
self.krophil = User.objects.get(username="krophil")
self.tutu = User.objects.get(username="tutu")
# Prepare a comment
comment_kwargs = create_uv_comment_template(self.krophil.id)
comment_kwargs["author"] = self.krophil
comment_kwargs["uv"] = UV.objects.get(id=comment_kwargs["uv"])
self.comment = UVComment(**comment_kwargs)
self.comment.save()
def create_report_test(self, username, success):
self.client.login(username=username, password="plop")
response = self.client.post(
reverse("pedagogy:comment_report", kwargs={"comment_id": self.comment.id}),
{
"comment": self.comment.id,
"reporter": User.objects.get(username=username).id,
"reason": "C'est moche",
},
)
if success:
self.assertEquals(response.status_code, 302)
else:
self.assertEquals(response.status_code, 403)
self.assertEquals(UVCommentReport.objects.all().exists(), success)
def test_create_report_root_success(self):
self.create_report_test("root", True)
def test_create_report_pedagogy_admin_success(self):
self.create_report_test("tutu", True)
def test_create_report_subscriber_success(self):
self.create_report_test("sli", True)
def test_create_report_unsubscribed_fail(self):
self.create_report_test("guy", False)
def test_create_report_anonymous_fail(self):
response = self.client.post(
reverse("pedagogy:comment_report", kwargs={"comment_id": self.comment.id}),
{"comment": self.comment.id, "reporter": 0, "reason": "C'est moche"},
)
self.assertEquals(response.status_code, 403)
self.assertFalse(UVCommentReport.objects.all().exists())
def test_notifications(self):
self.assertFalse(
self.tutu.notifications.filter(type="PEDAGOGY_MODERATION").exists()
)
# Create a comment report
self.create_report_test("tutu", True)
# Check that a notification has been created for pedagogy admins
self.assertTrue(
self.tutu.notifications.filter(type="PEDAGOGY_MODERATION").exists()
)
# Check that only pedagogy admins recieves this notification
for notif in Notification.objects.filter(type="PEDAGOGY_MODERATION").all():
self.assertTrue(
notif.user.is_in_group(settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)
)
# Check that notifications are not duplicated if not viewed
self.create_report_test("tutu", True)
self.assertEquals(
self.tutu.notifications.filter(type="PEDAGOGY_MODERATION").count(), 1
)
# Check that a new notification is created when the old one has been viewed
notif = self.tutu.notifications.filter(type="PEDAGOGY_MODERATION").first()
notif.viewed = True
notif.save()
self.create_report_test("tutu", True)
self.assertEquals(
self.tutu.notifications.filter(type="PEDAGOGY_MODERATION").count(), 2
)

View File

@ -46,7 +46,6 @@ urlpatterns = [
name="comment_report",
),
# Moderation
url(r"^reported$", UVCommentReportListView.as_view(), name="comment_report_list"),
url(r"^moderation$", UVModerationFormView.as_view(), name="moderation"),
# Administration : Create Update Delete Edit
url(r"^uv/create$", UVCreateView.as_view(), name="uv_create"),

View File

@ -34,7 +34,10 @@ from django.views.generic import (
from django.core import serializers
from django.utils import html
from django.http import HttpResponse
from django.core.urlresolvers import reverse_lazy
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
from django.core.urlresolvers import reverse_lazy, reverse
from django.shortcuts import get_object_or_404
from django.conf import settings
from core.views import (
DetailFormView,
@ -43,11 +46,17 @@ from core.views import (
CanViewMixin,
CanEditPropMixin,
)
from core.models import RealGroup, Notification
from haystack.query import SearchQuerySet
from pedagogy.forms import UVForm, UVCommentForm
from pedagogy.models import UV, UVComment
from pedagogy.forms import (
UVForm,
UVCommentForm,
UVCommentReportForm,
UVCommentModerationForm,
)
from pedagogy.models import UV, UVComment, UVCommentReport
# Some mixins
@ -200,28 +209,82 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView):
return queryset.filter(id__in=([o.object.id for o in qs]))
class UVCommentReportCreateView(CreateView):
class UVCommentReportCreateView(CanCreateMixin, CreateView):
"""
Create a new report for an inapropriate comment
"""
pass
model = UVCommentReport
form_class = UVCommentReportForm
template_name = "core/edit.jinja"
def dispatch(self, request, *args, **kwargs):
self.uv_comment = get_object_or_404(UVComment, pk=kwargs["comment_id"])
return super(UVCommentReportCreateView, self).dispatch(request, *args, **kwargs)
class UVCommentReportListView(ListView):
"""
List all UV reports for moderation (Privileged)
"""
def get_form_kwargs(self):
kwargs = super(UVCommentReportCreateView, self).get_form_kwargs()
kwargs["reporter_id"] = self.request.user.id
kwargs["comment_id"] = self.uv_comment.id
return kwargs
pass
def form_valid(self, form):
resp = super(UVCommentReportCreateView, self).form_valid(form)
# Send a message to moderation admins
for user in (
RealGroup.objects.filter(id=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)
.first()
.users.all()
):
if not user.notifications.filter(
type="PEDAGOGY_MODERATION", viewed=False
).exists():
Notification(
user=user,
url=reverse("pedagogy:moderation"),
type="PEDAGOGY_MODERATION",
).save()
return resp
def get_success_url(self):
return reverse_lazy(
"pedagogy:uv_detail", kwargs={"uv_id": self.uv_comment.uv.id}
)
class UVModerationFormView(FormView):
"""
List all UVs to moderate and allow to moderate them (Privileged)
Moderation interface (Privileged)
"""
pass
form_class = UVCommentModerationForm
template_name = "pedagogy/moderation.jinja"
def dispatch(self, request, *args, **kwargs):
if not request.user.is_owner(UV()):
raise PermissionDenied
return super(UVModerationFormView, self).dispatch(request, *args, **kwargs)
def form_valid(self, form):
form_clean = form.clean()
for report in form_clean.get("accepted_reports", []):
try:
report.comment.delete() # Delete the related comment
except ObjectDoesNotExist:
# To avoid errors when two reports points the same comment
pass
for report in form_clean.get("denied_reports", []):
try:
report.delete() # Delete the report itself
except ObjectDoesNotExist:
# To avoid errors when two reports points the same comment
pass
return super(UVModerationFormView, self).form_valid(form)
def get_success_url(self):
return reverse_lazy("pedagogy:moderation")
class UVCreateView(CanCreateMixin, CreateView):

View File

@ -560,6 +560,10 @@ SITH_LAUNDERETTE_PRICES = {"WASHING": 1.0, "DRYING": 0.75}
SITH_NOTIFICATIONS = [
("POSTER_MODERATION", _("A new poster needs to be moderated")),
("MAILING_MODERATION", _("A new mailing list needs to be moderated")),
(
"PEDAGOGY_MODERATION",
_("A new pedagogy comment has been signaled for moderation"),
),
("NEWS_MODERATION", _("There are %s fresh news to be moderated")),
("FILE_MODERATION", _("New files to be moderated")),
("SAS_MODERATION", _("There are %s pictures to be moderated in the SAS")),