Skip to content

Conversation

Jan-Ha-He
Copy link
Contributor

@Jan-Ha-He Jan-Ha-He commented Mar 10, 2025

Description

Dans cette pull request, nous fusionnons le module zds-antispam dans zds-site afin de supprimer la dépendance aux requêtes HTTP et permettre un accès direct aux données via l’ORM de Django. Concrètement, cela permettra :

  • De simplifier l’architecture en évitant des appels à l’API HTTP ;
  • Un accès plus facile aux biographies des utilisateurs;

Fichiers modifiés

  • signals.py
    Exécute le Spam Detector à chaque mise à jour d'une biographie.

  • spam_detector.py
    Contient la logique principale du module antispam : analyse des contenus et règles permettant de marquer un texte comme suspect.

  • spam_training.py
    Regroupe les fonctions nécessaires à l’entraînement ou à la mise à jour des règles de détection de spam, en utilisant les données recueillies sur Zeste de Savoir.


Contrôle Qualité

Pour vérifier la bonne intégration de zds-antispam :

  1. Lancez python manage.py migrate puis yarn test pour vous assurer du bon fonctionnement global.
  2. Créez ou modifiez un profil utilisateur avec une biographie suspecte.
  3. Vérifiez que le contenu problématique est bien signalé par le nouveau module antispam (consultez les logs ou l’interface d’administration).

En cas de problème ou de question, n’hésitez pas à le mentionner dans cette discussion.

TODO:

  • Création de tests
  • Intégration de données de formation
  • Mise en œuvre du rythme de formation
  • Assurer la persistance du modèle entraîné

wassim aarab and others added 4 commits January 27, 2025 20:16
…és" dans la sidebar des forums.

Fix zestedesavoir#6467

- Vérifiez que les liens s'affichent bien dans la sidebar.
- Testez que les liens redirigent vers les bonnes pages.
- Assurez-vous que le comportement est correct sur mobile et desktop.
Copy link
Member

@Situphen Situphen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merci pour cette PR ! Voici quelques remarques qui me viennent après une première lecture, n'hésites pas à argumenter si tu as un avis différent ;)

Remarques concernant tous les fichiers :

  • Pour une meilleure lisibilité, il est recommandé de grouper les lignes d'importation (dans l'ordre : modules standard de Python, puis modules installés avec Pip, puis modules de ton projet) et de les trier par ordre alphabétique. Ça peut être fait automatiquement avec isort et je viens de créer une PR pour l'inclure dans le projet (#6721).
  • Tu es parti du code existant et c'est très bien, mais n'hésites pas à restructurer complètement le code (passer d'une classe à une fonction, renommer les variables, etc.) car d'une part le cas d'usage est très différent (on n'est plus du tout sur un script qui tourne tous les X minutes et fait des appels API) et d'autre part le code est vieux !

Comment on lines 12 to 13
current_dir = os.path.dirname(os.path.abspath(__file__))
json_path = os.path.join(current_dir, "spamdata.json")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dans les projets Django, on préfère en règle générale créer les chemins à partir de BASE_DIR pour partir de la même base partout. De plus, on préfère travailler avec des objets Path que le module os.path quand c'est possible. La documentation donne les équivalences entre les deux modules : https://docs.python.org/fr/3.13/library/pathlib.html#corresponding-tools

from django.conf import settings
chemin = settings.BASE_DIR / "sous-dossier" / "fichier.txt"

Comment on lines 8 to 18
# Logger Setup
logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR)
current_dir = os.path.dirname(os.path.abspath(__file__))
log_file = os.path.join(current_dir, "spam_signals.log")

# File Handler
handler = logging.FileHandler(log_file)
handler.setLevel(logging.ERROR)
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dans les fichiers de code du projet, on définit le logger avec seulement cette simple ligne :

Suggested change
# Logger Setup
logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR)
current_dir = os.path.dirname(os.path.abspath(__file__))
log_file = os.path.join(current_dir, "spam_signals.log")
# File Handler
handler = logging.FileHandler(log_file)
handler.setLevel(logging.ERROR)
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
logger = logging.getLogger(__name__)

Le reste est géré dans la configuration :

LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "%(levelname)s %(name)s %(message)s",
},
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"loggers": {
"django": {
"handlers": ["console"],
"level": "WARNING",
},
"django.request": {
"level": "ERROR",
"handlers": [],
"propagate": False,
},
"zds": {
"handlers": ["console"],
"level": "WARNING",
},
"root": {
"handlers": ["console"],
"level": "WARNING",
},
},
}

Comment on lines 27 to 33
current_dir = os.path.dirname(os.path.abspath(__file__))
log_file = os.path.join(current_dir, "spam_detector.log")

handler = logging.FileHandler(log_file, mode="a")
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
self.logger.addHandler(handler)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idem, géré dans la configuration du projet

Suggested change
current_dir = os.path.dirname(os.path.abspath(__file__))
log_file = os.path.join(current_dir, "spam_detector.log")
handler = logging.FileHandler(log_file, mode="a")
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
self.logger.addHandler(handler)

Comment on lines 18 to 19
self.logger = logging.getLogger("zds.spam")
self.logger.setLevel(logging.ERROR)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

À remplacer par logger = logging.getLogger(__name__) juste après les lignes d'importation

Suggested change
self.logger = logging.getLogger("zds.spam")
self.logger.setLevel(logging.ERROR)

from django.contrib.auth.models import User


class SpamDetector:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je pense que le code gagnerait en lisibilité en remplaçant cette classe par une fonction, car le code est assez linéaire.

Comment on lines 14 to 15
reported_users_file = "reported_users.txt"
reported_users = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dans le fonctionnement actuel, le code est exécuté toutes les 5 minutes donc ce fichier permet de ne pas vérifier tout le temps la même biographie. Étant donné que dans le nouveau fonctionnement ce code est exécuté pour une biographie seulement à sa création ou à sa modification, ce fichier n'a plus d'utilité ! Tu peux donc retirer tout ce qui est lié à reported_users.

@github-project-automation github-project-automation bot moved this from En développement to Modification demandée in Suivi des PR Mar 10, 2025
Jan-Ha-He and others added 6 commits March 25, 2025 17:06
Merge tests pour zds-antispam integration
Modification spam_training.py et spam_detector.py :
	1-Remplacement des données dynamiques par un jeu de données statique
	2-Utilisation de données factices pour l'entraînement du modèle
Ajout des tests unitaires qui couvrent :
	1-Profils sans biographie
     	2-Contenu spam
     	3-Contenu valide

Lancer python3 manage.py test zds.utils.tests.tests_antispam
Modification spam_training.py et spam_detector.py :
	1-Remplacement des données dynamiques par un jeu de données statique
	2-Utilisation de données factices pour l'entraînement du modèle
Ajout des tests unitaires qui couvrent :
	1-Profils sans biographie
     	2-Contenu spam
     	3-Contenu valide

Lancer python3 manage.py test zds.utils.tests.tests_antispam
Copy link
Contributor

@wassimaarab wassimaarab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

la commit 717cb10 est une duplication involontaire due à un conflit de merge.
Merci d’ignorer celle-ci et de vous référer à la commit précédente 385b8f5 pour les vrais changements

@coveralls
Copy link

coveralls commented Apr 22, 2025

Coverage Status

coverage: 89.07% (-0.2%) from 89.225%
when pulling 724785e on Jan-Ha-He:dev
into e30e481 on zestedesavoir:dev.

@Jan-Ha-He Jan-Ha-He requested a review from Situphen April 22, 2025 09:48
Copy link
Member

@philippemilink philippemilink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pas mal de petits points à revoir.

Ce serait bien d'ajouter quelques commentaires dans le code pour expliquer le fonctionnement général de l'antispam. Et également rédiger la documentation correspondante.

Par contre, la façon dont vous avez conçu le code ne prend pas du tout en compte l'évolution qui permettrait de chercher du spam dans d'autres champs. Mais il ne vous reste sans doute pas assez de temps pour mettre ça en place...

@wassimaarab
Copy link
Contributor

Pour réaliser la tache pour l'évolution, nous pensons qu’il suffit d’entraîner le modèle séparément sur chacun des champs (commentaire, biographie et contenu), puis de le lancer pour l’analyse de spam. Ensuite, on pourrait simplement adapter le message d’alerte en fonction du champ analysé. Est-ce bien la démarche à suivre, ou y a-t-il une autre approche recommandée ?

@philippemilink
Copy link
Member

Pour réaliser la tache pour l'évolution, nous pensons qu’il suffit d’entraîner le modèle séparément sur chacun des champs (commentaire, biographie et contenu), puis de le lancer pour l’analyse de spam. Ensuite, on pourrait simplement adapter le message d’alerte en fonction du champ analysé. Est-ce bien la démarche à suivre, ou y a-t-il une autre approche recommandée ?

C'est pas tant d'appliquer le même processus qui est difficile, mais de coder la chose de façon à ce qu'on puisse très facilement ajouter les champs dans lesquels on souhaite chercher du spam. Dans l'idéal, pour dire au système qu'on souhaite chercher du spam dans un champ supplémentaire, ce serait juste une ligne de code ajouter. On en avait discuté lors d'une réunion de dev's.

Comment on lines 59 to 80
alert_kwargs = {
"author": User.objects.get(username="antispam"),
"scope": scope,
"text": _(f"Potential spam detected in {instance_info}, field '{field_name}'."),
"pubdate": datetime.now(),
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
alert_kwargs = {
"author": User.objects.get(username="antispam"),
"scope": scope,
"text": _(f"Potential spam detected in {instance_info}, field '{field_name}'."),
"pubdate": datetime.now(),
}
scope_to_alert_kwargs = {
"PROFILE": "profile",
"FORUM": "comment",
"CONTENT": "content",
}
alert_kwargs = {
"author": User.objects.get(username="antispam"),
"scope": scope,
"text": _(f"Potential spam detected in {instance_info}, field '{field_name}'."),
"pubdate": datetime.now(),
scope_to_alert_kwargs[scope]: instance,
}

Mais peut-être qu'on peut définir le scope directement à la valeur attendue par Alert ? La question c'est : est-ce qu' on a envie/besoin de distinguer les messages sur le forum et les commentaires des contenus pour détecter du spam ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'ai fait des changements comme j'ai compris le commentaire, je ne suis pas 100% sûre que c'était ce que était signifié par le commentaire

"model": Profile,
"field": "biography",
"scope": "PROFILE",
"get_instance_info": lambda instance: f"Profile of user '{instance.user.username}'",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'ai l'impression que cet attribut correspond aux méthodes __str__() des des objets ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C’est changé pour le profil, mais pas pour les commentaires, car cela entraînerait du HTML en clair dans les alertes.

Comment on lines 28 to 87
def prepare_training_data(self, content_type):
"""
Prepare training data for the given content type.
"""
# Implement logic to fetch or generate training data based on content_type
bios = ["example spam text", "example non-spam text"]
labels = [0, 1] # 0 for spam, 1 for non-spam
return bios, labels

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cette méthode n'a pas sa place dans cette classe, elle doit être directement dans les tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

En général, cette fonction doit aussi collecter des données pour l’entraînement de la base de données, mais il y avait une petite erreur dans le commit.
Pour la création des données de test, j’ai également ajouté un autre commentaire.

Jan-Ha-He added 18 commits May 9, 2025 14:55
wassim aarab and others added 5 commits May 10, 2025 14:11
Vérification des tests :
	-Dans le répertoire racine, lance : python3 manage.py test zds.utils.tests.tests_antispam
Vérification des tests :
	-Dans le répertoire racine, lance : python3 manage.py test zds.utils.tests.tests_spam_manager
Fichiers modifiés/ajoutés :

1-Modifié arborescence-back.rst : ajout le modèle antispam/ à l’arborescence de zds/

2-antispam.rst : rédaction de la documentation pour le modèle antispam
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Modification demandée
Development

Successfully merging this pull request may close these issues.

5 participants