Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(forum_moderation): enregistrement des messages bloquées #659

Merged
merged 22 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a958894
feat(forum_conversation): ajoute BlockedPost modèle
calummackervoy Jun 6, 2024
29e60f9
feat(forum_conversation): sauvegarde les messages bloquées dans le ta…
calummackervoy Jun 6, 2024
089036f
feat(forum_conversation): BlockedPost admin
calummackervoy Jun 6, 2024
efe98b6
update(forum_conversation): traductions des modèles
calummackervoy Jun 6, 2024
7e863cd
fix(search): traduction française manquante
calummackervoy Jun 6, 2024
3dc9f24
fix(build): migrations manquantes
calummackervoy Jun 6, 2024
548633b
feat(forum_moderation): enregistrer les messages supprimés lors de la…
calummackervoy Jun 6, 2024
c5953fb
refactor(forum): migration BlockedPost à forum_moderation
calummackervoy Jun 7, 2024
25ea3cf
refactor(forum_moderation): déplacement d'enum dans un fichier enums.py
calummackervoy Jun 7, 2024
d466965
style(forum_conversation): suppression du commentaire
calummackervoy Jun 7, 2024
c9b11e7
refactor(tests du forum): assertions succinctes
calummackervoy Jun 7, 2024
8c9b100
feat(forum_conversation): nom du CertifiedPost traduit
calummackervoy Jun 7, 2024
63d22e0
fix(locale): Postes -> Message
calummackervoy Jun 7, 2024
0565328
refactor(PostDisapproveView): création du BlockedPost dans post()
calummackervoy Jun 7, 2024
826096f
feat(BlockedPost): utilisateurs authentifié traqués sur le modèle
calummackervoy Jun 7, 2024
1180ce1
refactor(BlockedPost): conversion explicite à CharField
calummackervoy Jun 7, 2024
493d00b
style(BlockedPost): enlève paramètres superflus de block_reason
calummackervoy Jun 11, 2024
5d6903b
style(BlockedPost): enlève paramètres superflus d'username
calummackervoy Jun 11, 2024
06e1fe6
style: enlève changements du CertifiedPost pour plus de clarité
calummackervoy Jun 11, 2024
58b69d0
fix(BlockedPost): remet username nullable
calummackervoy Jun 11, 2024
8b5ce01
fix(CertifiedPost): missing changes from origin
calummackervoy Jun 11, 2024
9b46db4
refactor(BlockedPostReason): remplace Enum avec models.TextChoices
calummackervoy Jun 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lacommunaute/forum_conversation/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from taggit.models import Tag

from lacommunaute.forum_conversation.models import Post
from lacommunaute.forum_moderation.enums import BlockedPostReason
from lacommunaute.forum_moderation.models import BlockedPost
from lacommunaute.forum_moderation.utils import check_post_approbation


Expand All @@ -18,6 +20,12 @@ def clean(self):
)
if not post.approved:
self.add_error(None, "Votre message ne respecte pas les règles de la communauté.")

if self.user.is_authenticated:
post.poster = self.user

if post.update_reason in [x.label for x in BlockedPostReason.reasons_tracked_for_stats()]:
BlockedPost.create_from_post(post)
return cleaned_data

def update_post(self, post):
Expand Down
23 changes: 23 additions & 0 deletions lacommunaute/forum_conversation/tests/tests_views_htmx.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
from lacommunaute.forum_conversation.forms import PostForm
from lacommunaute.forum_conversation.models import CertifiedPost, Topic
from lacommunaute.forum_conversation.views_htmx import PostListView
from lacommunaute.forum_moderation.enums import BlockedPostReason
from lacommunaute.forum_moderation.factories import BlockedDomainNameFactory, BlockedEmailFactory
from lacommunaute.forum_moderation.models import BlockedPost
from lacommunaute.forum_upvote.factories import UpVoteFactory
from lacommunaute.users.factories import UserFactory

Expand Down Expand Up @@ -385,6 +387,12 @@ def test_create_post_as_blocked_not_blocked_anonymous(self, *args):
self.topic.refresh_from_db()
self.assertEqual(self.topic.posts.count(), 2)

# the blocked post should be recorded in the database
blocked_post = BlockedPost.objects.get()
assert blocked_post.content == self.content
assert blocked_post.username == username
assert blocked_post.block_reason == BlockedPostReason.BLOCKED_USER.label

def test_create_post_with_nonfr_content(self):
assign_perm("can_reply_to_topics", self.user, self.topic.forum)
assign_perm("can_post_without_approval", self.user, self.topic.forum)
Expand All @@ -407,6 +415,12 @@ def test_create_post_with_nonfr_content(self):
self.topic.refresh_from_db()
self.assertEqual(self.topic.posts.count(), 1)

# the blocked post should be recorded in the database
blocked_post = BlockedPost.objects.get()
assert blocked_post.poster == self.user
assert blocked_post.content == "популярные лучшие песни слушать онлайн"
assert blocked_post.block_reason == BlockedPostReason.ALTERNATIVE_LANGUAGE.label

def test_create_post_with_html_content(self):
assign_perm("can_reply_to_topics", self.user, self.topic.forum)
assign_perm("can_post_without_approval", self.user, self.topic.forum)
Expand All @@ -432,6 +446,9 @@ def test_create_post_with_html_content(self):
self.topic.refresh_from_db()
self.assertEqual(self.topic.posts.count(), 1)

# we don't create a BlockedPost record for HTML content to avoid storing malicious code
assert BlockedPost.objects.count() == 0

def test_create_post_with_blocked_domain_name(self):
BlockedDomainNameFactory(domain="blocked.com")

Expand All @@ -456,6 +473,12 @@ def test_create_post_with_blocked_domain_name(self):
self.topic.refresh_from_db()
self.assertEqual(self.topic.posts.count(), 1)

# the blocked post should be recorded in the database
blocked_post = BlockedPost.objects.get()
assert blocked_post.content == "la communauté"
assert blocked_post.username == "[email protected]"
assert blocked_post.block_reason == BlockedPostReason.BLOCKED_DOMAIN.label


class CertifiedPostViewTest(TestCase):
@classmethod
Expand Down
8 changes: 7 additions & 1 deletion lacommunaute/forum_moderation/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib import admin

from lacommunaute.forum_moderation.models import BlockedDomainName, BlockedEmail
from lacommunaute.forum_moderation.models import BlockedDomainName, BlockedEmail, BlockedPost


@admin.register(BlockedEmail)
Expand All @@ -13,3 +13,9 @@ class BlockedEmailAdmin(admin.ModelAdmin):
class BlockedDomainNameAdmin(admin.ModelAdmin):
list_display = ("domain", "created", "reason")
list_filter = ("reason",)


@admin.register(BlockedPost)
class BlockedPostAdmin(admin.ModelAdmin):
list_display = ("username", "created", "block_reason")
list_filter = ("block_reason",)
22 changes: 22 additions & 0 deletions lacommunaute/forum_moderation/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.db import models


class BlockedPostReason(models.TextChoices):
HTML_TAGS = "HTML_TAGS", "HTML tags detected"
ALTERNATIVE_LANGUAGE = "ALTERNATIVE_LANGUAGE", "Alternative Language detected"
BLOCKED_DOMAIN = "BLOCKED_DOMAIN", "Blocked Domain detected"
BLOCKED_USER = "BLOCKED_USER", "Blocked Email detected"
MODERATOR_DISAPPROVAL = "MODERATOR_DISAPPROVAL", "Moderator disapproval"

@classmethod
def reasons_tracked_for_stats(cls):
"""
We store BlockedPost objects for posts which are of interest for review
The list of "reasons for interest" are returned by this function
"""
return [
cls.ALTERNATIVE_LANGUAGE,
cls.BLOCKED_DOMAIN,
cls.BLOCKED_USER,
cls.MODERATOR_DISAPPROVAL,
]
51 changes: 51 additions & 0 deletions lacommunaute/forum_moderation/migrations/0003_blockedpost.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Generated by Django 5.0.6 on 2024-06-07 10:09

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("forum_moderation", "0002_copy_data_from_BouncedEmail_to_BlockedEmail"),
]

operations = [
migrations.CreateModel(
name="BlockedPost",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created", models.DateTimeField(auto_now_add=True, verbose_name="Creation date")),
("updated", models.DateTimeField(auto_now=True, verbose_name="Update date")),
(
"poster",
models.ForeignKey(
blank=True,
null=True,
on_delete=models.deletion.CASCADE,
related_name="blocked_posts",
to=settings.AUTH_USER_MODEL,
verbose_name="Poster",
),
),
("username", models.EmailField(max_length=254, null=True, blank=True, verbose_name="Adresse email")),
("content", models.CharField(verbose_name="Content")),
(
"block_reason",
models.CharField(
choices=[
("HTML_TAGS", "HTML tags detected"),
("ALTERNATIVE_LANGUAGE", "Alternative Language detected"),
("BLOCKED_DOMAIN", "Blocked Domain detected"),
("BLOCKED_USER", "Blocked Email detected"),
("MODERATOR_DISAPPROVAL", "Moderator disapproval"),
],
verbose_name="Block Reason",
),
),
],
options={
"verbose_name": "Blocked Post",
"verbose_name_plural": "Blocked Posts",
},
),
]
44 changes: 44 additions & 0 deletions lacommunaute/forum_moderation/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
from machina.models.abstract_models import DatedModel

from lacommunaute.forum_moderation.enums import BlockedPostReason


class BlockedEmail(DatedModel):
email = models.EmailField(verbose_name="email", null=False, blank=False, unique=True)
Expand All @@ -24,3 +28,43 @@ class Meta:

def __str__(self):
return f"{self.domain} - {self.created}"


class BlockedPost(DatedModel):
"""
When a user submits a Post and it is blocked by our quality control app (forum_moderation),
we save a record of the blocked Post in this table for reference purposes

It is built of a subset of fields from django-machina's model AbstractPost
"""

poster = models.ForeignKey(
settings.AUTH_USER_MODEL,
related_name="blocked_posts",
blank=True,
null=True,
on_delete=models.CASCADE,
verbose_name=_("Poster"),
)
username = models.EmailField(null=True, blank=True, verbose_name=("Adresse email"))
content = models.CharField(verbose_name=_("Content"))
block_reason = models.CharField(verbose_name=_("Block Reason"), choices=BlockedPostReason)

class Meta:
verbose_name = _("Blocked Post")
verbose_name_plural = _("Blocked Posts")

def __str__(self):
return f"Blocked Message [{ str(self.created) }]"

@classmethod
def create_from_post(cls, post):
"""
Creates a BlockedPost object from parameterised Post (machina)
"""
return cls.objects.create(
poster=post.poster,
username=getattr(post, "username", ""),
content=str(post.content),
block_reason=post.update_reason,
)
15 changes: 13 additions & 2 deletions lacommunaute/forum_moderation/tests/test_post_disapprove_view.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import pytest # noqa

from django.urls import reverse

from lacommunaute.forum_conversation.factories import TopicFactory, AnonymousPostFactory
from lacommunaute.forum_moderation.models import BlockedEmail
from lacommunaute.forum_conversation.models import Post
from lacommunaute.forum_moderation.models import BlockedEmail, BlockedPost
from lacommunaute.users.factories import UserFactory
from django.urls import reverse
from lacommunaute.forum_moderation.enums import BlockedPostReason
from lacommunaute.forum_moderation.factories import BlockedEmailFactory


Expand All @@ -15,6 +19,13 @@ def test_post_disapprove_view(client, db):
assert response.status_code == 302
assert BlockedEmail.objects.get(email=disapproved_post.username)

# the original Post should be deleted, but a BlockedPost saved
assert Post.objects.count() == 0
blocked_post = BlockedPost.objects.get()
assert blocked_post.content == str(disapproved_post.content)
assert blocked_post.username == disapproved_post.username
assert blocked_post.block_reason == BlockedPostReason.MODERATOR_DISAPPROVAL.label


def test_post_disapprove_view_with_existing_blocked_email(client, db):
disapproved_post = AnonymousPostFactory(topic=TopicFactory(approved=False))
Expand Down
12 changes: 8 additions & 4 deletions lacommunaute/forum_moderation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from machina.models.fields import render_func
from markdown2 import Markdown

from lacommunaute.forum_moderation.enums import BlockedPostReason
from lacommunaute.forum_moderation.models import BlockedDomainName, BlockedEmail


Expand All @@ -17,11 +18,14 @@ def check_post_approbation(post):
conditions = [
(
post.username and BlockedDomainName.objects.filter(domain=post.username.split("@")[-1]).exists(),
"Blocked Domain detected",
BlockedPostReason.BLOCKED_DOMAIN.label,
),
(Markdown.html_removed_text_compat in rendered, BlockedPostReason.HTML_TAGS.label),
(detect(post.content.raw) not in settings.LANGUAGE_CODE, BlockedPostReason.ALTERNATIVE_LANGUAGE.label),
(
post.username and BlockedEmail.objects.filter(email=post.username).exists(),
BlockedPostReason.BLOCKED_USER.label,
),
(Markdown.html_removed_text_compat in rendered, "HTML tags detected"),
(detect(post.content.raw) not in settings.LANGUAGE_CODE, "Alternative Language detected"),
(post.username and BlockedEmail.objects.filter(email=post.username).exists(), "Blocked Email detected"),
]
post.approved, post.update_reason = next(
((not condition, reason) for condition, reason in conditions if condition), (post.approved, post.update_reason)
Expand Down
7 changes: 6 additions & 1 deletion lacommunaute/forum_moderation/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
TopicDeleteView as BaseTopicDeleteView,
)

from lacommunaute.forum_moderation.models import BlockedEmail
from lacommunaute.forum_moderation.enums import BlockedPostReason
from lacommunaute.forum_moderation.models import BlockedEmail, BlockedPost


class TopicDeleteView(BaseTopicDeleteView):
Expand All @@ -30,4 +31,8 @@ def post(self, request, *args, **kwargs):
self.request,
"l'adresse email de l'utilisateur est déjà dans la liste des emails bloqués.",
)

post.update_reason = BlockedPostReason.MODERATOR_DISAPPROVAL.label
BlockedPost.create_from_post(post)

return self.disapprove(request, *args, **kwargs)
Binary file modified locale/fr/LC_MESSAGES/django.mo
Binary file not shown.
27 changes: 26 additions & 1 deletion locale/fr/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: lacommunaute.inclusion.beta.gouv.fr\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-10-18 18:07+0200\n"
"PO-Revision-Date: 2022-10-27 17:22+0200\n"
"PO-Revision-Date: 2024-06-06 11:19+0200\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr\n"
Expand All @@ -26,6 +26,30 @@ msgstr "Message certifié"
msgid "Certified Posts"
msgstr "Messages certifiés"

#: lacommunaute/forum_moderation/models.py
msgid "Content"
msgstr "Contenu"

#: lacommunaute/forum_moderation/models.py
msgid "Blocked Post"
msgstr "Message bloqué"

#: lacommunaute/forum_moderation/models.py
msgid "Blocked Posts"
msgstr "Messages bloqués"

#: lacommunaute/forum_moderation/models.py
msgid "Block Reason"
msgstr "Motif du blocage"

#: lacommunaute/forum_moderation/models.py
msgid "Poster"
msgstr "Auteur"

#: lacommunaute/search/forms.py:24
msgid "Keywords or phrase"
msgstr "Mots clés ou phrase"
calummackervoy marked this conversation as resolved.
Show resolved Hide resolved

msgid "This topic type has been changed successfully."
msgstr "Le type du sujet a été modifié avec succès."

Expand Down Expand Up @@ -183,6 +207,7 @@ msgstr "Copier"
msgid "Search"
msgstr "Rechercher"

#: lacommunaute/search/forms.py
msgid "Search in"
msgstr "Rechercher dans"

Expand Down
Loading