feat: add commentary in concil prep
This commit is contained in:
		| @@ -33,6 +33,26 @@ class CouncilPreparationService: | |||||||
| class StudentEvaluationService: | class StudentEvaluationService: | ||||||
|     def get_students_summaries(class_id, trimester) -> List[StudentTrimesterSummary] |     def get_students_summaries(class_id, trimester) -> List[StudentTrimesterSummary] | ||||||
|     def calculate_student_trimester_average(student_id, trimester) -> float |     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 | #### AppreciationService | ||||||
| @@ -55,6 +75,56 @@ class StudentTrimesterSummary: | |||||||
|     grades_by_assessment: Dict[int, Dict]  # {assessment_id: {score, max, title}} |     grades_by_assessment: Dict[int, Dict]  # {assessment_id: {score, max, title}} | ||||||
|     appreciation: Optional[CouncilAppreciation] |     appreciation: Optional[CouncilAppreciation] | ||||||
|     performance_status: str  # 'excellent', 'good', 'average', 'struggling' |     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 | #### CouncilPreparationData | ||||||
| @@ -113,6 +183,14 @@ class CouncilPreparationData: | |||||||
|   <div class="appreciation-status">[Rédigée|À rédiger]</div> |   <div class="appreciation-status">[Rédigée|À rédiger]</div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | <!-- Résumé compact des valeurs spéciales --> | ||||||
|  | <div class="special-values-summary"> | ||||||
|  |   <span class="text-gray-500">Valeurs spéciales:</span> | ||||||
|  |   <span class="badge-absent">. 2</span>  <!-- Absent/Pas de réponse --> | ||||||
|  |   <span class="badge-excused">d 1</span>  <!-- Dispensé --> | ||||||
|  |   <span class="badge-away">a 1</span>     <!-- Absent justifié --> | ||||||
|  | </div> | ||||||
|  |  | ||||||
| <!-- Résultats par évaluation --> | <!-- Résultats par évaluation --> | ||||||
| <div class="assessment-results"> | <div class="assessment-results"> | ||||||
|   <div class="assessment-item"> |   <div class="assessment-item"> | ||||||
| @@ -131,6 +209,100 @@ class CouncilPreparationData: | |||||||
| </div> | </div> | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | ### 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 | ||||||
|  | <!-- Section des valeurs spéciales détaillées --> | ||||||
|  | <div class="special-values-detail"> | ||||||
|  |   <h6>Valeurs spéciales</h6> | ||||||
|  |   <div class="assessment-special-values"> | ||||||
|  |     <div class="assessment-row"> | ||||||
|  |       <span>Contrôle Chapitre 1</span> | ||||||
|  |       <div class="special-badges"> | ||||||
|  |         <span class="badge" title="Pas de réponse (2)">. 2</span> | ||||||
|  |         <span class="badge" title="Dispensé (1)">d 1</span> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### Commentaires enseignant organisés 📝 | ||||||
|  | **Nouvelle fonctionnalité 2025** : Affichage compact et structuré de tous les commentaires saisis par l'enseignant. | ||||||
|  |  | ||||||
|  | ```html | ||||||
|  | <!-- Section des commentaires par évaluation --> | ||||||
|  | <div class="comments-section"> | ||||||
|  |   <h6>Commentaires (3)</h6> | ||||||
|  |    | ||||||
|  |   <!-- Regroupement par évaluation --> | ||||||
|  |   <div class="assessment-comments"> | ||||||
|  |     <div class="assessment-group"> | ||||||
|  |       <div class="assessment-header"> | ||||||
|  |         <span class="assessment-title">Contrôle Chapitre 1 - Nombres entiers</span> | ||||||
|  |         <span class="comment-count">3 commentaire(s)</span> | ||||||
|  |       </div> | ||||||
|  |        | ||||||
|  |       <!-- Commentaires compacts sur 2 lignes --> | ||||||
|  |       <div class="comments-list"> | ||||||
|  |         <div class="comment-item"> | ||||||
|  |           <!-- Ligne 1: Label • Description --> | ||||||
|  |           <div class="element-info">Additions • Calculs simples</div> | ||||||
|  |           <!-- Ligne 2: [Valeur] Commentaire --> | ||||||
|  |           <div class="comment-content"> | ||||||
|  |             <span class="value-badge">.</span> | ||||||
|  |             <span class="comment-text">Élève absent lors de cette question</span> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div class="comment-item"> | ||||||
|  |           <div class="element-info">Multiplications • Tables et calculs</div> | ||||||
|  |           <div class="comment-content"> | ||||||
|  |             <span class="value-badge">d</span> | ||||||
|  |             <span class="comment-text">Dispensé par décision médicale</span> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### 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 | ## 🎛️ Modes de Visualisation | ||||||
|  |  | ||||||
| ### Mode Liste (par défaut) | ### Mode Liste (par défaut) | ||||||
| @@ -299,6 +471,28 @@ this.state = { | |||||||
|  |  | ||||||
| ## 🎯 Fonctionnalités Avancées | ## 🎯 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 | ### Raccourcis clavier globaux | ||||||
| ```javascript | ```javascript | ||||||
| // Raccourcis disponibles | // Raccourcis disponibles | ||||||
| @@ -409,13 +603,20 @@ console.log('⬅️ Navigation vers élève précédent'); | |||||||
| - **Structuration** : Commencer par les cas prioritaires | - **Structuration** : Commencer par les cas prioritaires | ||||||
| - **Révision** : Mode Liste final pour cohérence globale | - **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 | - [ ] **Collaboration** : Plusieurs enseignants simultanément | ||||||
| - [ ] **Templates** : Appréciations pré-rédigées personnalisables | - [ ] **Templates** : Appréciations pré-rédigées personnalisables | ||||||
| - [ ] **IA Assistant** : Suggestions d'amélioration automatiques | - [ ] **IA Assistant** : Suggestions d'amélioration automatiques | ||||||
| - [ ] **Analytics** : Tendances longitudinales élèves | - [ ] **Analytics** : Tendances longitudinales élèves | ||||||
|  | - [ ] **Export enrichi** : PDF avec valeurs spéciales et commentaires | ||||||
|  |  | ||||||
| ### Version 2.2   | ### Version 2.2   | ||||||
| - [ ] **Mobile App** : Application native iOS/Android | - [ ] **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 | - ✅ **Analyse statistique** automatique des performances | ||||||
| - ✅ **Navigation optimisée** avec raccourcis clavier | - ✅ **Navigation optimisée** avec raccourcis clavier | ||||||
| - ✅ **Architecture robuste** avec gestion d'erreurs complète | - ✅ **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. | 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. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,9 @@ | |||||||
| from typing import List, Optional, Dict, Any | from typing import List, Optional, Dict, Any | ||||||
| from sqlalchemy.orm import joinedload | from sqlalchemy.orm import joinedload | ||||||
|  | from sqlalchemy import func | ||||||
| from models import Grade, GradingElement, Exercise, Assessment, Student | from models import Grade, GradingElement, Exercise, Assessment, Student | ||||||
| from .base_repository import BaseRepository | from .base_repository import BaseRepository | ||||||
|  | from app_config import config_manager | ||||||
|  |  | ||||||
|  |  | ||||||
| class GradeRepository(BaseRepository[Grade]): | class GradeRepository(BaseRepository[Grade]): | ||||||
| @@ -128,3 +130,260 @@ class GradeRepository(BaseRepository[Grade]): | |||||||
|         ).options( |         ).options( | ||||||
|             joinedload(Grade.grading_element).joinedload(GradingElement.exercise) |             joinedload(Grade.grading_element).joinedload(GradingElement.exercise) | ||||||
|         ).all() |         ).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 | ||||||
|  |         } | ||||||
| @@ -3,7 +3,7 @@ Services pour la préparation du conseil de classe. | |||||||
| Comprend CouncilPreparationService, StudentEvaluationService, AppreciationService. | Comprend CouncilPreparationService, StudentEvaluationService, AppreciationService. | ||||||
| """ | """ | ||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| from typing import Dict, List, Optional, Tuple | from typing import Dict, List, Optional, Tuple, Any | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from repositories.appreciation_repository import AppreciationRepository | from repositories.appreciation_repository import AppreciationRepository | ||||||
| from repositories.grade_repository import GradeRepository | from repositories.grade_repository import GradeRepository | ||||||
| @@ -22,6 +22,7 @@ class StudentTrimesterSummary: | |||||||
|     appreciation: Optional[CouncilAppreciation] |     appreciation: Optional[CouncilAppreciation] | ||||||
|     performance_status: str  # 'excellent', 'good', 'average', 'struggling' |     performance_status: str  # 'excellent', 'good', 'average', 'struggling' | ||||||
|     competence_domain_breakdown: Optional[Dict] = None  # Données des compétences et domaines |     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 |     @property | ||||||
|     def has_appreciation(self) -> bool: |     def has_appreciation(self) -> bool: | ||||||
| @@ -101,6 +102,13 @@ class StudentEvaluationService: | |||||||
|         # Calculer les données de compétences et domaines |         # Calculer les données de compétences et domaines | ||||||
|         competence_domain_breakdown = self.get_student_competence_domain_breakdown(student_id, trimester) |         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( |         return StudentTrimesterSummary( | ||||||
|             student=student, |             student=student, | ||||||
|             overall_average=overall_average, |             overall_average=overall_average, | ||||||
| @@ -108,7 +116,8 @@ class StudentEvaluationService: | |||||||
|             grades_by_assessment=grades_by_assessment, |             grades_by_assessment=grades_by_assessment, | ||||||
|             appreciation=appreciation, |             appreciation=appreciation, | ||||||
|             performance_status=performance_status, |             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]: |     def get_students_summaries(self, class_group_id: int, trimester: int) -> List[StudentTrimesterSummary]: | ||||||
| @@ -349,6 +358,80 @@ class StudentEvaluationService: | |||||||
|             'domains': domains |             '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]: |     def _calculate_assessment_score_for_student(self, assessment: Assessment, student_id: int) -> Optional[float]: | ||||||
|         """Calcule le score d'un élève pour une évaluation.""" |         """Calcule le score d'un élève pour une évaluation.""" | ||||||
|         grades = self.grade_repo.get_student_grades_by_assessment(student_id, assessment.id) |         grades = self.grade_repo.get_student_grades_by_assessment(student_id, assessment.id) | ||||||
|   | |||||||
| @@ -272,6 +272,30 @@ | |||||||
|                                     <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/> |                                     <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/> | ||||||
|                                 </svg> |                                 </svg> | ||||||
|                             </div> |                             </div> | ||||||
|  |                              | ||||||
|  |                             {# Résumé compact des valeurs spéciales #} | ||||||
|  |                             {% if summary.special_values_summary and summary.special_values_summary.has_special_values %} | ||||||
|  |                             <div class="mt-2 flex items-center space-x-1 text-xs"> | ||||||
|  |                                 <span class="text-gray-500">Valeurs spéciales:</span> | ||||||
|  |                                 {% 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 %} | ||||||
|  |                                         <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium cursor-help" | ||||||
|  |                                               style="background-color: {{ data.color }}20; color: {{ data.color }};" | ||||||
|  |                                               title="{{ data.label }} ({{ data.count }}){% if tooltip_content|length > 0 %}
{{ tooltip_content|join('
') }}{% endif %}"> | ||||||
|  |                                             {{ special_value }} {{ data.count }} | ||||||
|  |                                         </span> | ||||||
|  |                                     {% endif %} | ||||||
|  |                                 {% endfor %} | ||||||
|  |                             </div> | ||||||
|  |                             {% endif %} | ||||||
|                         </div> |                         </div> | ||||||
|                     </div> |                     </div> | ||||||
|                      |                      | ||||||
| @@ -392,6 +416,87 @@ | |||||||
|                                     </div> |                                     </div> | ||||||
|                                     {% endfor %} |                                     {% endfor %} | ||||||
|                                 </div> |                                 </div> | ||||||
|  |                                  | ||||||
|  |                                 {# Section Valeurs spéciales par évaluation #} | ||||||
|  |                                 {% if summary.special_values_summary and summary.special_values_summary.by_assessment %} | ||||||
|  |                                 <div class="mt-4"> | ||||||
|  |                                     <h6 class="text-xs font-medium text-gray-600 mb-2 flex items-center"> | ||||||
|  |                                         <svg class="w-3 h-3 text-gray-500 mr-1" fill="currentColor" viewBox="0 0 20 20"> | ||||||
|  |                                             <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/> | ||||||
|  |                                         </svg> | ||||||
|  |                                         Valeurs spéciales | ||||||
|  |                                     </h6> | ||||||
|  |                                     <div class="space-y-1"> | ||||||
|  |                                         {% for assessment_id, assessment_data in summary.special_values_summary.by_assessment.items() %} | ||||||
|  |                                         <div class="flex items-center justify-between p-1.5 bg-gray-50 rounded border border-gray-100"> | ||||||
|  |                                             <span class="text-xs text-gray-700 truncate">{{ assessment_data.title }}</span> | ||||||
|  |                                             <div class="flex items-center space-x-1"> | ||||||
|  |                                                 {% 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 %} | ||||||
|  |                                                         <span class="inline-flex items-center px-1 py-0.5 rounded text-xs font-medium cursor-help" | ||||||
|  |                                                               style="background-color: {{ data.color }}15; color: {{ data.color }}; border: 1px solid {{ data.color }}30;" | ||||||
|  |                                                               title="{{ data.label }} ({{ data.count }}){% if detail_tooltip|length > 0 %}
{{ detail_tooltip|join('
') }}{% endif %}"> | ||||||
|  |                                                             {{ special_value }} {{ data.count }} | ||||||
|  |                                                         </span> | ||||||
|  |                                                     {% endif %} | ||||||
|  |                                                 {% endfor %} | ||||||
|  |                                             </div> | ||||||
|  |                                         </div> | ||||||
|  |                                         {% endfor %} | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                                 {% 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 %} | ||||||
|  |                                 <div class="mt-4"> | ||||||
|  |                                     <h6 class="text-xs font-medium text-gray-600 mb-2 flex items-center"> | ||||||
|  |                                         <svg class="w-3 h-3 text-gray-500 mr-1" fill="currentColor" viewBox="0 0 20 20"> | ||||||
|  |                                             <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/> | ||||||
|  |                                         </svg> | ||||||
|  |                                         Commentaires ({{ summary.special_values_summary.comments_by_assessments.total_comments }}) | ||||||
|  |                                     </h6> | ||||||
|  |                                     <div class="space-y-2"> | ||||||
|  |                                         {% for assessment in summary.special_values_summary.comments_by_assessments.assessments %} | ||||||
|  |                                         <div class="bg-amber-50 border border-amber-200 rounded p-2"> | ||||||
|  |                                             <div class="flex items-center justify-between mb-1"> | ||||||
|  |                                                 <span class="text-xs font-medium text-amber-800">{{ assessment.title }}</span> | ||||||
|  |                                                 <span class="text-xs text-amber-600">{{ assessment.comments|length }} commentaire(s)</span> | ||||||
|  |                                             </div> | ||||||
|  |                                             <div class="space-y-1"> | ||||||
|  |                                                 {% for comment in assessment.comments %} | ||||||
|  |                                                 <div class="bg-white border border-amber-100 rounded p-1.5"> | ||||||
|  |                                                     <div class="flex-1 min-w-0"> | ||||||
|  |                                                         {# Ligne 1: Label et description #} | ||||||
|  |                                                         <div class="text-xs font-medium text-gray-800"> | ||||||
|  |                                                             {{ comment.element_label }}{% if comment.element_description %} • {{ comment.element_description }}{% endif %} | ||||||
|  |                                                         </div> | ||||||
|  |                                                         {# Ligne 2: Valeur et commentaire #} | ||||||
|  |                                                         <div class="text-xs text-amber-700 mt-0.5 flex items-center"> | ||||||
|  |                                                             {% if comment.value %} | ||||||
|  |                                                                 <span class="inline-flex items-center px-1 py-0.5 rounded text-xs font-medium mr-2 bg-orange-100 text-orange-800"> | ||||||
|  |                                                                     {{ comment.value }} | ||||||
|  |                                                                 </span> | ||||||
|  |                                                             {% endif %} | ||||||
|  |                                                             <span class="flex-1">{{ comment.comment }}</span> | ||||||
|  |                                                         </div> | ||||||
|  |                                                     </div> | ||||||
|  |                                                 </div> | ||||||
|  |                                                 {% endfor %} | ||||||
|  |                                             </div> | ||||||
|  |                                         </div> | ||||||
|  |                                         {% endfor %} | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                                 {% endif %} | ||||||
|                             </div> |                             </div> | ||||||
|                             {% endif %} |                             {% endif %} | ||||||
|                              |                              | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user