From f438082c4c6f981172e14ca61fe648fe094b1210 Mon Sep 17 00:00:00 2001 From: Bertrand Benjamin Date: Fri, 15 Aug 2025 07:59:23 +0200 Subject: [PATCH] feat: add commentary in concil prep --- docs/features/CONSEIL_DE_CLASSE.md | 208 +++++++++++++++++- repositories/grade_repository.py | 261 ++++++++++++++++++++++- services/council_services.py | 87 +++++++- templates/class_council_preparation.html | 105 +++++++++ 4 files changed, 656 insertions(+), 5 deletions(-) diff --git a/docs/features/CONSEIL_DE_CLASSE.md b/docs/features/CONSEIL_DE_CLASSE.md index f151a69..bf8340a 100644 --- a/docs/features/CONSEIL_DE_CLASSE.md +++ b/docs/features/CONSEIL_DE_CLASSE.md @@ -33,6 +33,26 @@ class CouncilPreparationService: class StudentEvaluationService: def get_students_summaries(class_id, trimester) -> List[StudentTrimesterSummary] def calculate_student_trimester_average(student_id, trimester) -> float + + # Nouvelles méthodes 2025 + def get_student_special_values_summary(student_id, trimester) -> Dict[str, Any] + def get_student_competence_domain_breakdown(student_id, trimester) -> Dict[str, List[Dict]] +``` + +#### GradeRepository (Nouvelles méthodes 2025) +```python +# Repository étendu pour valeurs spéciales et commentaires +class GradeRepository: + # Méthodes existantes... + + # Nouvelles méthodes pour valeurs spéciales + def get_special_values_counts_by_student_trimester(student_id, trimester) -> Dict[str, int] + def get_special_values_counts_by_student_assessment(student_id, assessment_id) -> Dict[str, int] + def get_special_values_details_by_student_trimester(student_id, trimester) -> Dict[str, List[Dict]] + def get_special_values_details_by_student_assessment(student_id, assessment_id) -> Dict[str, List[Dict]] + + # Nouvelle méthode pour commentaires organisés + def get_all_comments_by_student_trimester(student_id, trimester) -> Dict[str, Any] ``` #### AppreciationService @@ -55,6 +75,56 @@ class StudentTrimesterSummary: grades_by_assessment: Dict[int, Dict] # {assessment_id: {score, max, title}} appreciation: Optional[CouncilAppreciation] performance_status: str # 'excellent', 'good', 'average', 'struggling' + competence_domain_breakdown: Optional[Dict] = None # Données compétences/domaines + special_values_summary: Optional[Dict] = None # Nouvelle: résumé valeurs spéciales et commentaires +``` + +#### Structure special_values_summary (Nouveau 2025) +```python +special_values_summary = { + # Résumé global des valeurs spéciales pour le trimestre + "global": { + ".": {"count": 2, "label": "Pas de réponse", "color": "#ef4444", "details": [...]}, + "d": {"count": 1, "label": "Dispensé", "color": "#6b7280", "details": [...]}, + "a": {"count": 1, "label": "Absent", "color": "#f59e0b", "details": [...]} + }, + + # Détail par évaluation + "by_assessment": { + 1: { + "title": "Contrôle Chapitre 1", + "date": "2025-08-10", + "special_values": { + ".": {"count": 1, "label": "Pas de réponse", "details": [...]} + } + } + }, + + # Commentaires organisés par évaluations (Nouveau) + "comments_by_assessments": { + "assessments": [ + { + "id": 1, + "title": "Contrôle Chapitre 1 - Nombres entiers", + "date": "2025-08-10", + "comments": [ + { + "element_label": "Additions", + "element_description": "Calculs simples", + "value": ".", + "comment": "Élève absent lors de cette question" + } + ] + } + ], + "total_comments": 3, + "has_comments": true + }, + + # Métadonnées + "total_special_values": 4, + "has_special_values": true +} ``` #### CouncilPreparationData @@ -113,6 +183,14 @@ class CouncilPreparationData:
[Rédigée|À rédiger]
+ +
+ Valeurs spéciales: + . 2 + d 1 + a 1 +
+
@@ -131,6 +209,100 @@ class CouncilPreparationData:
``` +### Vue détaillée expandable + +Chaque carte élève peut être étendue pour révéler des informations complémentaires essentielles à la préparation du conseil de classe. + +#### Valeurs spéciales par évaluation +```html + +
+
Valeurs spéciales
+
+
+ Contrôle Chapitre 1 +
+ . 2 + d 1 +
+
+
+
+``` + +#### Commentaires enseignant organisés 📝 +**Nouvelle fonctionnalité 2025** : Affichage compact et structuré de tous les commentaires saisis par l'enseignant. + +```html + +
+
Commentaires (3)
+ + +
+
+
+ Contrôle Chapitre 1 - Nombres entiers + 3 commentaire(s) +
+ + +
+
+ +
Additions • Calculs simples
+ +
+ . + Élève absent lors de cette question +
+
+ +
+
Multiplications • Tables et calculs
+
+ d + Dispensé par décision médicale +
+
+
+
+
+
+``` + +#### Structure des données commentaires +```python +# Nouvelle structure pour les commentaires +comments_by_assessments = { + "assessments": [ + { + "id": 1, + "title": "Contrôle Chapitre 1 - Nombres entiers", + "date": "2025-08-10", + "comments": [ + { + "element_label": "Additions", + "element_description": "Calculs simples", + "value": ".", # Valeur optionnelle (peut être None) + "comment": "Élève absent lors de cette question", + "exercise_title": "Exercice 1" + } + ] + } + ], + "total_comments": 3, + "has_comments": true +} +``` + +#### Avantages de l'affichage compact +- **Gain d'espace** : 2 lignes par commentaire maximum +- **Contexte complet** : Label, description, valeur et commentaire visibles +- **Organisation logique** : Regroupement par évaluation chronologique +- **Lecture rapide** : Information hiérarchisée et codes couleur cohérents +- **Positionnement optimal** : Après résultats et valeurs spéciales, avant compétences + ## 🎛️ Modes de Visualisation ### Mode Liste (par défaut) @@ -299,6 +471,28 @@ this.state = { ## 🎯 Fonctionnalités Avancées +### Analyse des valeurs spéciales et commentaires 📊 + +**Nouveauté 2025** : Système complet d'analyse des valeurs spéciales (absences, dispenses) et des commentaires enseignant pour faciliter la préparation des conseils de classe. + +#### Valeurs spéciales configurables +- **Configuration dynamique** : Valeurs et couleurs modifiables via interface d'administration +- **Valeurs par défaut** : `.` (Pas de réponse), `d` (Dispensé), `a` (Absent) +- **Comptage automatique** : Calcul global et par évaluation en temps réel +- **Tooltips informatifs** : Détail des éléments concernés au survol + +#### Commentaires structurés +- **Regroupement intelligent** : Organisation automatique par évaluation +- **Affichage compact** : 2 lignes par commentaire (contexte + contenu) +- **Métadonnées complètes** : Label, description, valeur associée +- **Tri chronologique** : Évaluations les plus récentes en premier + +#### Bénéfices pédagogiques +- **Vue d'ensemble rapide** : Identification immédiate des difficultés +- **Contexte enrichi** : Commentaires précédents accessibles lors des discussions +- **Suivi longitudinal** : Évolution des problématiques par trimestre +- **Préparation optimisée** : Toutes les informations centralisées + ### Raccourcis clavier globaux ```javascript // Raccourcis disponibles @@ -409,13 +603,20 @@ console.log('⬅️ Navigation vers élève précédent'); - **Structuration** : Commencer par les cas prioritaires - **Révision** : Mode Liste final pour cohérence globale -## 🔄 Évolutions Futures +## 🔄 Évolutions Récentes et Futures -### Version 2.1 +### Version 2.0.1 (Août 2025) ✅ +- [x] **Valeurs spéciales** : Comptage et affichage automatiques (`.`, `d`, `a`) +- [x] **Commentaires structurés** : Organisation par évaluation avec affichage compact +- [x] **Repository étendu** : Nouvelles méthodes optimisées pour l'analyse +- [x] **Interface enrichie** : Vue détaillée expandable avec toutes les données contextuelles + +### Version 2.1 (Roadmap) - [ ] **Collaboration** : Plusieurs enseignants simultanément - [ ] **Templates** : Appréciations pré-rédigées personnalisables - [ ] **IA Assistant** : Suggestions d'amélioration automatiques - [ ] **Analytics** : Tendances longitudinales élèves +- [ ] **Export enrichi** : PDF avec valeurs spéciales et commentaires ### Version 2.2 - [ ] **Mobile App** : Application native iOS/Android @@ -434,6 +635,9 @@ La **Préparation du Conseil de Classe** de Notytex révolutionne le workflow tr - ✅ **Analyse statistique** automatique des performances - ✅ **Navigation optimisée** avec raccourcis clavier - ✅ **Architecture robuste** avec gestion d'erreurs complète +- 🆕 **Valeurs spéciales** : Comptage automatique des absences et dispenses +- 🆕 **Commentaires structurés** : Historique organisé par évaluation +- 🆕 **Vue contextuelle** : Toutes les données pédagogiques centralisées Cette fonctionnalité transforme une tâche chronophage en un processus fluide et efficace, permettant aux enseignants de se concentrer sur l'essentiel : l'analyse pédagogique et la rédaction d'appréciations personnalisées. diff --git a/repositories/grade_repository.py b/repositories/grade_repository.py index 2ac9a5f..e1f2a11 100644 --- a/repositories/grade_repository.py +++ b/repositories/grade_repository.py @@ -1,7 +1,9 @@ from typing import List, Optional, Dict, Any from sqlalchemy.orm import joinedload +from sqlalchemy import func from models import Grade, GradingElement, Exercise, Assessment, Student from .base_repository import BaseRepository +from app_config import config_manager class GradeRepository(BaseRepository[Grade]): @@ -127,4 +129,261 @@ class GradeRepository(BaseRepository[Grade]): Exercise.assessment_id == assessment_id ).options( joinedload(Grade.grading_element).joinedload(GradingElement.exercise) - ).all() \ No newline at end of file + ).all() + + def get_special_values_counts_by_student_trimester(self, student_id: int, trimester: int) -> Dict[str, int]: + """ + Compte les valeurs spéciales pour un élève dans un trimestre donné. + + Args: + student_id: ID de l'élève + trimester: Numéro du trimestre (1, 2, ou 3) + + Returns: + Dictionnaire avec les comptes par valeur spéciale (ex: {'.': 3, 'd': 1, 'a': 0}) + """ + # Récupération dynamique des valeurs spéciales configurées + special_values = config_manager.get_special_values() + + # Requête pour compter les valeurs spéciales avec jointures optimisées + grade_values = Grade.query.join( + GradingElement + ).join( + Exercise + ).join( + Assessment + ).filter( + Grade.student_id == student_id, + Assessment.trimester == trimester, + Grade.value.in_(list(special_values.keys())) + ).with_entities( + Grade.value, + func.count(Grade.value).label('count') + ).group_by( + Grade.value + ).all() + + # Initialiser le dictionnaire avec toutes les valeurs spéciales à 0 + result = {value: 0 for value in special_values.keys()} + + # Mettre à jour avec les comptes réels + for value, count in grade_values: + if value in result: + result[value] = count + + return result + + def get_special_values_counts_by_student_assessment(self, student_id: int, assessment_id: int) -> Dict[str, int]: + """ + Compte les valeurs spéciales pour un élève dans une évaluation donnée. + + Args: + student_id: ID de l'élève + assessment_id: ID de l'évaluation + + Returns: + Dictionnaire avec les comptes par valeur spéciale (ex: {'.': 2, 'd': 0, 'a': 1}) + """ + # Récupération dynamique des valeurs spéciales configurées + special_values = config_manager.get_special_values() + + # Requête pour compter les valeurs spéciales avec jointures optimisées + grade_values = Grade.query.join( + GradingElement + ).join( + Exercise + ).filter( + Grade.student_id == student_id, + Exercise.assessment_id == assessment_id, + Grade.value.in_(list(special_values.keys())) + ).with_entities( + Grade.value, + func.count(Grade.value).label('count') + ).group_by( + Grade.value + ).all() + + # Initialiser le dictionnaire avec toutes les valeurs spéciales à 0 + result = {value: 0 for value in special_values.keys()} + + # Mettre à jour avec les comptes réels + for value, count in grade_values: + if value in result: + result[value] = count + + return result + + def get_special_values_details_by_student_trimester(self, student_id: int, trimester: int) -> Dict[str, List[Dict[str, Any]]]: + """ + Récupère les détails des valeurs spéciales pour un élève dans un trimestre donné. + + Args: + student_id: ID de l'élève + trimester: Numéro du trimestre (1, 2, ou 3) + + Returns: + Dictionnaire avec les détails par valeur spéciale incluant les commentaires + Format: {'.': [{'element_name': str, 'comment': str, 'assessment_title': str}, ...]} + """ + # Récupération dynamique des valeurs spéciales configurées + special_values = config_manager.get_special_values() + + # Requête pour récupérer les détails des valeurs spéciales + grade_details = Grade.query.join( + GradingElement + ).join( + Exercise + ).join( + Assessment + ).filter( + Grade.student_id == student_id, + Assessment.trimester == trimester, + Grade.value.in_(list(special_values.keys())) + ).with_entities( + Grade.value, + Grade.comment, + GradingElement.label.label('element_name'), + Assessment.title.label('assessment_title'), + Assessment.id.label('assessment_id') + ).all() + + # Organiser par valeur spéciale + result_details = {} + + # Initialiser avec toutes les valeurs spéciales + for special_value in special_values.keys(): + result_details[special_value] = [] + + # Ajouter les détails trouvés + for detail in grade_details: + result_details[detail.value].append({ + 'element_name': detail.element_name, + 'comment': detail.comment, + 'assessment_title': detail.assessment_title, + 'assessment_id': detail.assessment_id + }) + + return result_details + + def get_special_values_details_by_student_assessment(self, student_id: int, assessment_id: int) -> Dict[str, List[Dict[str, Any]]]: + """ + Récupère les détails des valeurs spéciales pour un élève dans une évaluation donnée. + + Args: + student_id: ID de l'élève + assessment_id: ID de l'évaluation + + Returns: + Dictionnaire avec les détails par valeur spéciale incluant les commentaires + Format: {'.': [{'element_name': str, 'comment': str}, ...]} + """ + # Récupération dynamique des valeurs spéciales configurées + special_values = config_manager.get_special_values() + + # Requête pour récupérer les détails des valeurs spéciales + grade_details = Grade.query.join( + GradingElement + ).join( + Exercise + ).filter( + Grade.student_id == student_id, + Exercise.assessment_id == assessment_id, + Grade.value.in_(list(special_values.keys())) + ).with_entities( + Grade.value, + Grade.comment, + GradingElement.label.label('element_name') + ).all() + + # Organiser par valeur spéciale + result_details = {} + + # Initialiser avec toutes les valeurs spéciales + for special_value in special_values.keys(): + result_details[special_value] = [] + + # Ajouter les détails trouvés + for detail in grade_details: + result_details[detail.value].append({ + 'element_name': detail.element_name, + 'comment': detail.comment + }) + + return result_details + + def get_all_comments_by_student_trimester(self, student_id: int, trimester: int) -> Dict[str, Any]: + """ + Récupère tous les commentaires regroupés par évaluations pour un élève dans un trimestre. + + Args: + student_id: ID de l'élève + trimester: Numéro du trimestre (1, 2, ou 3) + + Returns: + Dict avec commentaires organisés par évaluation avec métadonnées complètes + """ + # Requête pour récupérer tous les commentaires non vides avec toutes les informations + grade_comments = Grade.query.join( + GradingElement + ).join( + Exercise + ).join( + Assessment + ).filter( + Grade.student_id == student_id, + Assessment.trimester == trimester, + Grade.comment.isnot(None), + Grade.comment != '' + ).with_entities( + Grade.value, + Grade.comment, + GradingElement.label.label('element_label'), + GradingElement.description.label('element_description'), + Assessment.title.label('assessment_title'), + Assessment.id.label('assessment_id'), + Assessment.date.label('assessment_date'), + Exercise.title.label('exercise_title'), + Exercise.order.label('exercise_order') + ).order_by( + Assessment.date.desc(), + Exercise.order, + GradingElement.id + ).all() + + # Organiser par évaluation + comments_by_assessment = {} + + for comment_data in grade_comments: + assessment_id = comment_data.assessment_id + + # Initialiser l'évaluation si pas encore présente + if assessment_id not in comments_by_assessment: + comments_by_assessment[assessment_id] = { + 'id': assessment_id, + 'title': comment_data.assessment_title, + 'date': comment_data.assessment_date, + 'comments': [] + } + + # Ajouter le commentaire + comments_by_assessment[assessment_id]['comments'].append({ + 'value': comment_data.value, + 'comment': comment_data.comment, + 'element_label': comment_data.element_label, + 'element_description': comment_data.element_description, + 'exercise_title': comment_data.exercise_title, + 'exercise_order': comment_data.exercise_order + }) + + # Convertir en liste triée par date (plus récent en premier) + assessments_with_comments = list(comments_by_assessment.values()) + assessments_with_comments.sort(key=lambda x: x['date'] if x['date'] else '', reverse=True) + + # Calculer le total de commentaires + total_comments = sum(len(assessment['comments']) for assessment in assessments_with_comments) + + return { + 'assessments': assessments_with_comments, + 'total_comments': total_comments, + 'has_comments': total_comments > 0 + } \ No newline at end of file diff --git a/services/council_services.py b/services/council_services.py index dfb6f18..efb1503 100644 --- a/services/council_services.py +++ b/services/council_services.py @@ -3,7 +3,7 @@ Services pour la préparation du conseil de classe. Comprend CouncilPreparationService, StudentEvaluationService, AppreciationService. """ from dataclasses import dataclass -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple, Any from datetime import datetime from repositories.appreciation_repository import AppreciationRepository from repositories.grade_repository import GradeRepository @@ -22,6 +22,7 @@ class StudentTrimesterSummary: appreciation: Optional[CouncilAppreciation] performance_status: str # 'excellent', 'good', 'average', 'struggling' competence_domain_breakdown: Optional[Dict] = None # Données des compétences et domaines + special_values_summary: Optional[Dict] = None # Résumé des valeurs spéciales @property def has_appreciation(self) -> bool: @@ -101,6 +102,13 @@ class StudentEvaluationService: # Calculer les données de compétences et domaines competence_domain_breakdown = self.get_student_competence_domain_breakdown(student_id, trimester) + # Calculer le résumé des valeurs spéciales + special_values_summary = self.get_student_special_values_summary(student_id, trimester) + + # Ajouter tous les commentaires organisés par évaluations + all_comments_data = self.grade_repo.get_all_comments_by_student_trimester(student_id, trimester) + special_values_summary['comments_by_assessments'] = all_comments_data + return StudentTrimesterSummary( student=student, overall_average=overall_average, @@ -108,7 +116,8 @@ class StudentEvaluationService: grades_by_assessment=grades_by_assessment, appreciation=appreciation, performance_status=performance_status, - competence_domain_breakdown=competence_domain_breakdown + competence_domain_breakdown=competence_domain_breakdown, + special_values_summary=special_values_summary ) def get_students_summaries(self, class_group_id: int, trimester: int) -> List[StudentTrimesterSummary]: @@ -349,6 +358,80 @@ class StudentEvaluationService: 'domains': domains } + def get_student_special_values_summary(self, student_id: int, trimester: int) -> Dict[str, Any]: + """ + Calcule le résumé des valeurs spéciales pour un élève sur un trimestre. + + Returns: + Dict avec 'global' (total par trimestre) et 'by_assessment' (détail par évaluation) + """ + from app_config import config_manager + + # Récupérer les valeurs spéciales configurées + special_values_config = config_manager.get_special_values() + + # 1. Comptes globaux par trimestre + global_counts = self.grade_repo.get_special_values_counts_by_student_trimester(student_id, trimester) + global_details = self.grade_repo.get_special_values_details_by_student_trimester(student_id, trimester) + + # 2. Comptes par évaluation + assessments = self.assessment_repo.find_by_class_trimester_with_details( + Student.query.get(student_id).class_group_id, trimester + ) + + by_assessment = {} + for assessment in assessments: + assessment_counts = self.grade_repo.get_special_values_counts_by_student_assessment( + student_id, assessment.id + ) + assessment_details = self.grade_repo.get_special_values_details_by_student_assessment( + student_id, assessment.id + ) + + # Ajouter seulement si l'élève a des valeurs spéciales dans cette évaluation + if any(count > 0 for count in assessment_counts.values()): + by_assessment[assessment.id] = { + 'title': assessment.title, + 'date': assessment.date, + 'counts': assessment_counts, + 'details': assessment_details + } + + # 3. Enrichir avec les métadonnées de configuration + enriched_global = {} + enriched_by_assessment = {} + + for special_value, config in special_values_config.items(): + enriched_global[special_value] = { + 'count': global_counts.get(special_value, 0), + 'label': config['label'], + 'color': config['color'], + 'counts_in_total': config['counts'], + 'details': global_details.get(special_value, []) + } + + for assessment_id, assessment_data in by_assessment.items(): + enriched_by_assessment[assessment_id] = { + 'title': assessment_data['title'], + 'date': assessment_data['date'], + 'special_values': {} + } + + for special_value, config in special_values_config.items(): + enriched_by_assessment[assessment_id]['special_values'][special_value] = { + 'count': assessment_data['counts'].get(special_value, 0), + 'label': config['label'], + 'color': config['color'], + 'details': assessment_data['details'].get(special_value, []) + } + + return { + 'global': enriched_global, + 'by_assessment': enriched_by_assessment, + 'total_special_values': sum(global_counts.values()), + 'has_special_values': sum(global_counts.values()) > 0 + } + def _calculate_assessment_score_for_student(self, assessment: Assessment, student_id: int) -> Optional[float]: """Calcule le score d'un élève pour une évaluation.""" grades = self.grade_repo.get_student_grades_by_assessment(student_id, assessment.id) diff --git a/templates/class_council_preparation.html b/templates/class_council_preparation.html index 1d4973e..a7e4e32 100644 --- a/templates/class_council_preparation.html +++ b/templates/class_council_preparation.html @@ -272,6 +272,30 @@
+ + {# Résumé compact des valeurs spéciales #} + {% if summary.special_values_summary and summary.special_values_summary.has_special_values %} +
+ Valeurs spéciales: + {% for special_value, data in summary.special_values_summary.global.items() %} + {% if data.count > 0 %} + {% set tooltip_content = [] %} + {% for detail in data.details %} + {% if detail.comment %} + {% set _ = tooltip_content.append(detail.element_name + ': ' + detail.comment) %} + {% else %} + {% set _ = tooltip_content.append(detail.element_name) %} + {% endif %} + {% endfor %} + + {{ special_value }} {{ data.count }} + + {% endif %} + {% endfor %} +
+ {% endif %} @@ -392,6 +416,87 @@ {% endfor %} + + {# Section Valeurs spéciales par évaluation #} + {% if summary.special_values_summary and summary.special_values_summary.by_assessment %} +
+
+ + + + Valeurs spéciales +
+
+ {% for assessment_id, assessment_data in summary.special_values_summary.by_assessment.items() %} +
+ {{ assessment_data.title }} +
+ {% for special_value, data in assessment_data.special_values.items() %} + {% if data.count > 0 %} + {% set detail_tooltip = [] %} + {% for detail in data.details %} + {% if detail.comment %} + {% set _ = detail_tooltip.append(detail.element_name + ': ' + detail.comment) %} + {% else %} + {% set _ = detail_tooltip.append(detail.element_name) %} + {% endif %} + {% endfor %} + + {{ special_value }} {{ data.count }} + + {% endif %} + {% endfor %} +
+
+ {% endfor %} +
+
+ {% endif %} + + {# Section Commentaires par évaluation #} + {% if summary.special_values_summary and summary.special_values_summary.comments_by_assessments and summary.special_values_summary.comments_by_assessments.has_comments %} +
+
+ + + + Commentaires ({{ summary.special_values_summary.comments_by_assessments.total_comments }}) +
+
+ {% for assessment in summary.special_values_summary.comments_by_assessments.assessments %} +
+
+ {{ assessment.title }} + {{ assessment.comments|length }} commentaire(s) +
+
+ {% for comment in assessment.comments %} +
+
+ {# Ligne 1: Label et description #} +
+ {{ comment.element_label }}{% if comment.element_description %} • {{ comment.element_description }}{% endif %} +
+ {# Ligne 2: Valeur et commentaire #} +
+ {% if comment.value %} + + {{ comment.value }} + + {% endif %} + {{ comment.comment }} +
+
+
+ {% endfor %} +
+
+ {% endfor %} +
+
+ {% endif %} {% endif %}