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