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:
@@ -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
+
+
+```
+
+#### 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 %}
+
Commentaires (3)
+ + +