feat: add concil page
This commit is contained in:
837
docs/backend/CONSEIL_DE_CLASSE_ARCHITECTURE.md
Normal file
837
docs/backend/CONSEIL_DE_CLASSE_ARCHITECTURE.md
Normal file
@@ -0,0 +1,837 @@
|
||||
# 🏗️ Architecture Technique - Conseil de Classe
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le module **Conseil de Classe** implémente une architecture en couches avec séparation des responsabilités, suivant les patterns **Repository**, **Service Layer** et **Factory**.
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ 🎨 Frontend │ │ 📡 Backend │ │ 🗄️ Database │
|
||||
│ │ │ │ │ │
|
||||
│ • Mode Focus │◄──►│ • Services │◄──►│ • Models │
|
||||
│ • Auto-save │ │ • Repositories │ │ • Relationships │
|
||||
│ • Sync bidirec. │ │ • API Routes │ │ • Constraints │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
## 🔧 Backend Architecture
|
||||
|
||||
### 1. Services Layer
|
||||
|
||||
#### CouncilPreparationService
|
||||
**Responsabilité** : Orchestration principale et agrégation des données
|
||||
```python
|
||||
class CouncilPreparationService:
|
||||
def __init__(self,
|
||||
student_evaluation_service: StudentEvaluationService,
|
||||
appreciation_service: AppreciationService,
|
||||
assessment_repo: AssessmentRepository):
|
||||
self.student_evaluation = student_evaluation_service
|
||||
self.appreciation = appreciation_service
|
||||
self.assessment_repo = assessment_repo
|
||||
|
||||
def prepare_council_data(self, class_group_id: int, trimester: int) -> CouncilPreparationData:
|
||||
"""
|
||||
Point d'entrée principal - agrège toutes les données nécessaires
|
||||
|
||||
Flow:
|
||||
1. Récupère résumés élèves via StudentEvaluationService
|
||||
2. Calcule statistiques classe
|
||||
3. Récupère statistiques appréciations via AppreciationService
|
||||
4. Retourne CouncilPreparationData consolidé
|
||||
"""
|
||||
student_summaries = self.student_evaluation.get_students_summaries(class_group_id, trimester)
|
||||
class_statistics = self._calculate_class_statistics(student_summaries)
|
||||
appreciation_stats = self.appreciation.get_completion_stats(class_group_id, trimester)
|
||||
|
||||
return CouncilPreparationData(...)
|
||||
```
|
||||
|
||||
#### StudentEvaluationService
|
||||
**Responsabilité** : Calculs de performances et moyennes élèves
|
||||
```python
|
||||
class StudentEvaluationService:
|
||||
def calculate_student_trimester_average(self, student_id: int, trimester: int) -> Optional[float]:
|
||||
"""
|
||||
Algorithme de calcul de moyenne pondérée:
|
||||
|
||||
weighted_sum = Σ(score_evaluation × coefficient_evaluation)
|
||||
total_coefficient = Σ(coefficient_evaluation)
|
||||
moyenne = weighted_sum / total_coefficient
|
||||
|
||||
Gestion des cas spéciaux:
|
||||
- Notes manquantes : Exclus du calcul
|
||||
- Valeurs '.' : Comptent comme 0 mais incluent le coefficient
|
||||
- Valeurs 'd' : Dispensé, exclu complètement
|
||||
"""
|
||||
assessments = self.assessment_repo.find_completed_by_class_trimester(class_id, trimester)
|
||||
weighted_sum = total_coefficient = 0.0
|
||||
|
||||
for assessment in assessments:
|
||||
score = self._calculate_assessment_score_for_student(assessment, student_id)
|
||||
if score is not None:
|
||||
weighted_sum += score * assessment.coefficient
|
||||
total_coefficient += assessment.coefficient
|
||||
|
||||
return round(weighted_sum / total_coefficient, 2) if total_coefficient > 0 else None
|
||||
|
||||
def _determine_performance_status(self, average: Optional[float]) -> str:
|
||||
"""
|
||||
Classification automatique des performances:
|
||||
- excellent: ≥ 16/20
|
||||
- good: 14-15.99/20
|
||||
- average: 10-13.99/20
|
||||
- struggling: < 10/20
|
||||
- no_data: Pas de notes disponibles
|
||||
"""
|
||||
if not average: return 'no_data'
|
||||
if average >= 16: return 'excellent'
|
||||
elif average >= 14: return 'good'
|
||||
elif average >= 10: return 'average'
|
||||
else: return 'struggling'
|
||||
```
|
||||
|
||||
#### AppreciationService
|
||||
**Responsabilité** : CRUD et workflow des appréciations
|
||||
```python
|
||||
class AppreciationService:
|
||||
def save_appreciation(self, data: Dict) -> CouncilAppreciation:
|
||||
"""
|
||||
Sauvegarde avec logique de création/mise à jour automatique
|
||||
|
||||
Business Rules:
|
||||
- Création si pas d'appréciation existante
|
||||
- Mise à jour si existe déjà
|
||||
- Horodatage automatique (last_modified)
|
||||
- Validation des champs requis
|
||||
- Gestion du statut (draft/finalized)
|
||||
"""
|
||||
return self.appreciation_repo.create_or_update(
|
||||
student_id=data['student_id'],
|
||||
class_group_id=data['class_group_id'],
|
||||
trimester=data['trimester'],
|
||||
data={
|
||||
'general_appreciation': data.get('general_appreciation'),
|
||||
'strengths': data.get('strengths'),
|
||||
'areas_for_improvement': data.get('areas_for_improvement'),
|
||||
'status': data.get('status', 'draft')
|
||||
}
|
||||
)
|
||||
|
||||
def get_completion_stats(self, class_group_id: int, trimester: int) -> Dict:
|
||||
"""
|
||||
Calcul des statistiques de complétion:
|
||||
- completed_appreciations: Nombre avec contenu
|
||||
- total_students: Nombre total d'élèves
|
||||
- completion_rate: Pourcentage de complétion
|
||||
- average_length: Longueur moyenne des appréciations
|
||||
"""
|
||||
return self.appreciation_repo.get_completion_stats(class_group_id, trimester)
|
||||
```
|
||||
|
||||
### 2. Repository Layer
|
||||
|
||||
#### Architecture générale
|
||||
```python
|
||||
class BaseRepository:
|
||||
"""Repository générique avec opérations CRUD communes"""
|
||||
|
||||
def get_or_404(self, id: int) -> Model:
|
||||
"""Récupération avec gestion 404 automatique"""
|
||||
|
||||
def find_by_filters(self, **filters) -> List[Model]:
|
||||
"""Requête avec filtres dynamiques"""
|
||||
|
||||
def create_or_update(self, **data) -> Model:
|
||||
"""Upsert pattern avec gestion des conflits"""
|
||||
|
||||
class AppreciationRepository(BaseRepository):
|
||||
def find_by_student_trimester(self, student_id: int, class_group_id: int, trimester: int) -> Optional[CouncilAppreciation]:
|
||||
"""
|
||||
Requête optimisée avec index composite:
|
||||
INDEX idx_appreciation_lookup ON council_appreciations (student_id, class_group_id, trimester)
|
||||
"""
|
||||
return CouncilAppreciation.query.filter_by(
|
||||
student_id=student_id,
|
||||
class_group_id=class_group_id,
|
||||
trimester=trimester
|
||||
).first()
|
||||
|
||||
def get_completion_stats(self, class_group_id: int, trimester: int) -> Dict:
|
||||
"""
|
||||
Requête d'agrégation optimisée avec sous-requêtes:
|
||||
|
||||
SELECT
|
||||
COUNT(CASE WHEN general_appreciation IS NOT NULL AND general_appreciation != '' THEN 1 END) as completed,
|
||||
COUNT(DISTINCT s.id) as total_students,
|
||||
AVG(LENGTH(general_appreciation)) as avg_length
|
||||
FROM students s
|
||||
LEFT JOIN council_appreciations ca ON ...
|
||||
WHERE s.class_group_id = ?
|
||||
"""
|
||||
```
|
||||
|
||||
### 3. API Routes Layer
|
||||
|
||||
#### Structure des endpoints
|
||||
```python
|
||||
# /routes/classes.py
|
||||
|
||||
@bp.route('/<int:id>/council')
|
||||
def council_preparation(id):
|
||||
"""
|
||||
Page principale - Rendu HTML complet
|
||||
|
||||
Validations:
|
||||
- Trimestre obligatoire et valide (1,2,3)
|
||||
- Classe existe et accessible
|
||||
- Données préparées via CouncilServiceFactory
|
||||
|
||||
Template: class_council_preparation.html
|
||||
Context: class_group, trimester, council_data, student_summaries, statistics
|
||||
"""
|
||||
trimester = request.args.get('trimestre', type=int)
|
||||
if not trimester or trimester not in [1, 2, 3]:
|
||||
flash('Veuillez sélectionner un trimestre pour préparer le conseil de classe.', 'error')
|
||||
return redirect(url_for('classes.dashboard', id=id))
|
||||
|
||||
council_service = CouncilServiceFactory.create_council_preparation_service()
|
||||
council_data = council_service.prepare_council_data(id, trimester)
|
||||
|
||||
return render_template('class_council_preparation.html', ...)
|
||||
|
||||
@bp.route('/<int:class_id>/council/appreciation/<int:student_id>', methods=['POST'])
|
||||
def save_appreciation_api(class_id, student_id):
|
||||
"""
|
||||
API AJAX - Sauvegarde d'appréciation
|
||||
|
||||
Input validation:
|
||||
- JSON content-type requis
|
||||
- student_id appartient à class_id (security)
|
||||
- trimester valide (1,2,3)
|
||||
- Longueur appréciation < 2000 chars
|
||||
|
||||
Response format:
|
||||
{
|
||||
"success": true,
|
||||
"appreciation_id": 123,
|
||||
"last_modified": "2025-08-10T14:30:00.000Z",
|
||||
"status": "draft",
|
||||
"has_content": true
|
||||
}
|
||||
|
||||
Error handling:
|
||||
- 400: Données invalides
|
||||
- 403: Élève pas dans cette classe
|
||||
- 500: Erreur serveur
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': 'Données manquantes'}), 400
|
||||
|
||||
# Security: Vérifier appartenance élève à classe
|
||||
student = Student.query.get_or_404(student_id)
|
||||
if student.class_group_id != class_id:
|
||||
return jsonify({'success': False, 'error': 'Élève non trouvé dans cette classe'}), 403
|
||||
|
||||
# Business logic via service
|
||||
appreciation_service = CouncilServiceFactory.create_appreciation_service()
|
||||
result = appreciation_service.save_appreciation({
|
||||
'student_id': student_id,
|
||||
'class_group_id': class_id,
|
||||
'trimester': data.get('trimester'),
|
||||
'general_appreciation': data.get('appreciation', '').strip() or None,
|
||||
'status': 'draft'
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'appreciation_id': result.id,
|
||||
'last_modified': result.last_modified.isoformat(),
|
||||
'status': result.status,
|
||||
'has_content': result.has_content
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Erreur sauvegarde appréciation élève {student_id}: {e}')
|
||||
return jsonify({'success': False, 'error': 'Erreur lors de la sauvegarde'}), 500
|
||||
```
|
||||
|
||||
### 4. Data Models
|
||||
|
||||
#### CouncilAppreciation
|
||||
```python
|
||||
class CouncilAppreciation(db.Model):
|
||||
__tablename__ = 'council_appreciations'
|
||||
|
||||
# Primary Key
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# Foreign Keys avec contraintes
|
||||
student_id = db.Column(db.Integer, db.ForeignKey('students.id'), nullable=False)
|
||||
class_group_id = db.Column(db.Integer, db.ForeignKey('class_groups.id'), nullable=False)
|
||||
|
||||
# Business Data
|
||||
trimester = db.Column(db.Integer, nullable=False) # 1, 2, ou 3
|
||||
general_appreciation = db.Column(db.Text)
|
||||
strengths = db.Column(db.Text)
|
||||
areas_for_improvement = db.Column(db.Text)
|
||||
status = db.Column(db.String(20), default='draft') # draft, finalized
|
||||
|
||||
# Metadata
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Constraints
|
||||
__table_args__ = (
|
||||
# Un seul appréciation par élève/classe/trimestre
|
||||
db.UniqueConstraint('student_id', 'class_group_id', 'trimester'),
|
||||
# Index pour les requêtes fréquentes
|
||||
db.Index('idx_appreciation_lookup', 'student_id', 'class_group_id', 'trimester'),
|
||||
db.Index('idx_class_trimester', 'class_group_id', 'trimester'),
|
||||
# Validation trimestre
|
||||
db.CheckConstraint('trimester IN (1, 2, 3)'),
|
||||
# Validation statut
|
||||
db.CheckConstraint("status IN ('draft', 'finalized')")
|
||||
)
|
||||
|
||||
# Relationships
|
||||
student = db.relationship('Student', backref='council_appreciations')
|
||||
class_group = db.relationship('ClassGroup', backref='council_appreciations')
|
||||
|
||||
@property
|
||||
def has_content(self) -> bool:
|
||||
"""Vérifie si l'appréciation a du contenu significatif"""
|
||||
return bool(
|
||||
(self.general_appreciation and self.general_appreciation.strip()) or
|
||||
(self.strengths and self.strengths.strip()) or
|
||||
(self.areas_for_improvement and self.areas_for_improvement.strip())
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""Sérialisation pour API JSON"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'student_id': self.student_id,
|
||||
'class_group_id': self.class_group_id,
|
||||
'trimester': self.trimester,
|
||||
'general_appreciation': self.general_appreciation,
|
||||
'strengths': self.strengths,
|
||||
'areas_for_improvement': self.areas_for_improvement,
|
||||
'status': self.status,
|
||||
'has_content': self.has_content,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'last_modified': self.last_modified.isoformat() if self.last_modified else None
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 Frontend Architecture
|
||||
|
||||
### 1. Modular JavaScript Architecture
|
||||
|
||||
#### Structure générale
|
||||
```javascript
|
||||
// Pattern: Composition over Inheritance
|
||||
class CouncilPreparation {
|
||||
constructor(classId, options = {}) {
|
||||
// État centralisé
|
||||
this.state = {
|
||||
currentTrimester: 2,
|
||||
expandedStudents: new Set(),
|
||||
savingStates: new Map(),
|
||||
modifiedAppreciations: new Set(),
|
||||
// Focus mode state
|
||||
isFocusMode: false,
|
||||
focusCurrentIndex: 0,
|
||||
filteredStudents: []
|
||||
};
|
||||
|
||||
// Composition des gestionnaires spécialisés
|
||||
this.stateManager = new StateManager(this); // URL state & persistence
|
||||
this.filterManager = new FilterManager(this); // Search, sort, filters
|
||||
this.autoSaveManager = new AutoSaveManager(this); // Auto-save logic
|
||||
this.uiManager = new UIManager(this); // Card animations
|
||||
this.focusManager = new FocusManager(this); // Focus mode
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### StateManager - Persistance état
|
||||
```javascript
|
||||
class StateManager {
|
||||
restoreState() {
|
||||
"""
|
||||
Restauration depuis URL et localStorage:
|
||||
|
||||
URL params: ?trimestre=2&sort=average&filter=struggling
|
||||
localStorage: expanded_students, focus_mode_preference
|
||||
|
||||
Flow:
|
||||
1. Parse URL parameters
|
||||
2. Restore localStorage preferences
|
||||
3. Apply initial state to DOM elements
|
||||
4. Trigger initial filters/sorts
|
||||
"""
|
||||
const params = new URLSearchParams(location.search);
|
||||
this.parent.state.sortBy = params.get('sort') || 'alphabetical';
|
||||
this.parent.state.filterStatus = params.get('filter') || 'all';
|
||||
|
||||
// Apply to DOM
|
||||
this.applyInitialState();
|
||||
}
|
||||
|
||||
saveState() {
|
||||
"""
|
||||
Persistance dans URL pour bookmarking/refresh:
|
||||
|
||||
Format: /classes/5/council?trimestre=2&sort=average&filter=struggling
|
||||
|
||||
Benefits:
|
||||
- État persistant sur F5
|
||||
- URLs partageables
|
||||
- Navigation browser (back/forward)
|
||||
"""
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.set('sort', this.parent.state.sortBy);
|
||||
params.set('filter', this.parent.state.filterStatus);
|
||||
|
||||
history.replaceState(null, '', `${location.pathname}?${params.toString()}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### FilterManager - Filtrage intelligent
|
||||
```javascript
|
||||
class FilterManager {
|
||||
applyFilters() {
|
||||
"""
|
||||
Algorithme de filtrage multi-critères avec performances optimisées:
|
||||
|
||||
1. Single DOM query pour tous les éléments
|
||||
2. Filtrage en mémoire (shouldShowStudent)
|
||||
3. Application CSS display/order en batch
|
||||
4. Animations staggered pour UX fluide
|
||||
|
||||
Performance: O(n) où n = nombre d'élèves
|
||||
"""
|
||||
const students = Array.from(document.querySelectorAll('[data-student-card]'));
|
||||
let visibleCount = 0;
|
||||
|
||||
students.forEach((studentCard, index) => {
|
||||
const isVisible = this.shouldShowStudent(studentCard);
|
||||
|
||||
if (isVisible) {
|
||||
studentCard.style.display = '';
|
||||
visibleCount++;
|
||||
// Staggered animation pour UX fluide
|
||||
setTimeout(() => {
|
||||
studentCard.style.opacity = '1';
|
||||
studentCard.style.transform = 'translateY(0)';
|
||||
}, index * 50);
|
||||
} else {
|
||||
studentCard.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
this.applySorting();
|
||||
this.updateResultsCounter(visibleCount, students.length);
|
||||
|
||||
// Notification au FocusManager pour mise à jour
|
||||
this.parent.focusManager?.onFiltersChanged();
|
||||
}
|
||||
|
||||
shouldShowStudent(studentCard) {
|
||||
"""
|
||||
Critères de filtrage combinés:
|
||||
|
||||
1. Recherche textuelle (nom/prénom)
|
||||
2. Statut de performance (excellent/good/average/struggling)
|
||||
3. État appréciation (completed/pending)
|
||||
|
||||
Logic: AND entre tous les critères actifs
|
||||
"""
|
||||
const studentName = studentCard.dataset.studentName?.toLowerCase() || '';
|
||||
const performanceStatus = studentCard.dataset.performanceStatus;
|
||||
const hasAppreciation = studentCard.dataset.hasAppreciation === 'true';
|
||||
|
||||
// Text search filter
|
||||
if (this.parent.state.searchTerm && !studentName.includes(this.parent.state.searchTerm)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Performance status filter
|
||||
if (this.parent.state.filterStatus !== 'all') {
|
||||
switch (this.parent.state.filterStatus) {
|
||||
case 'completed': return hasAppreciation;
|
||||
case 'pending': return !hasAppreciation;
|
||||
case 'struggling': return performanceStatus === 'struggling';
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### AutoSaveManager - Sauvegarde intelligente
|
||||
```javascript
|
||||
class AutoSaveManager {
|
||||
constructor(councilPrep) {
|
||||
this.parent = councilPrep;
|
||||
this.pendingSaves = new Map(); // Par élève
|
||||
this.saveQueue = []; // File FIFO
|
||||
this.isSaving = false; // Mutex
|
||||
}
|
||||
|
||||
queueSave(studentId, appreciation, immediate = false) {
|
||||
"""
|
||||
File de sauvegarde avec deduplication automatique:
|
||||
|
||||
Algorithm:
|
||||
1. Remove previous queued save for same student (deduplication)
|
||||
2. Add new save task to queue
|
||||
3. Process queue if not already processing
|
||||
4. Immediate saves bypass queue for user-triggered actions
|
||||
|
||||
Benefits:
|
||||
- Évite les requêtes multiples pour même élève
|
||||
- Throttling automatique (100ms entre saves)
|
||||
- Priorité aux sauvegardes utilisateur (immediate=true)
|
||||
"""
|
||||
const saveTask = {
|
||||
studentId,
|
||||
appreciation,
|
||||
timestamp: Date.now(),
|
||||
immediate
|
||||
};
|
||||
|
||||
if (immediate) {
|
||||
this.executeSave(saveTask);
|
||||
} else {
|
||||
// Deduplication: Remove previous save for this student
|
||||
this.saveQueue = this.saveQueue.filter(task => task.studentId !== studentId);
|
||||
this.saveQueue.push(saveTask);
|
||||
this.processSaveQueue();
|
||||
}
|
||||
}
|
||||
|
||||
async executeSave(saveTask) {
|
||||
"""
|
||||
Exécution HTTP avec gestion d'erreurs robuste:
|
||||
|
||||
Flow:
|
||||
1. Show saving indicator
|
||||
2. HTTP POST avec retry logic
|
||||
3. Parse response et validation
|
||||
4. Update UI states (success/error)
|
||||
5. Sync avec élément original si mode focus
|
||||
|
||||
Error handling:
|
||||
- Network errors: Retry + user notification
|
||||
- Validation errors: Show specific message
|
||||
- Server errors: Log + generic message
|
||||
"""
|
||||
const { studentId, appreciation } = saveTask;
|
||||
|
||||
try {
|
||||
this.showSavingState(studentId, true);
|
||||
|
||||
const response = await fetch(`/classes/${this.parent.classId}/council/appreciation/${studentId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
appreciation: appreciation,
|
||||
trimester: this.parent.state.currentTrimester
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
this.showSavedState(studentId);
|
||||
this.parent.state.modifiedAppreciations.delete(studentId);
|
||||
// Update UI metadata
|
||||
this.updateLastModified(studentId, result.last_modified);
|
||||
this.updateAppreciationStatus(studentId, result.has_content);
|
||||
} else {
|
||||
throw new Error(result.error || 'Erreur de sauvegarde');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur sauvegarde appréciation:', error);
|
||||
this.showErrorState(studentId, error.message);
|
||||
this.parent.showToast('Erreur sauvegarde', 'error');
|
||||
} finally {
|
||||
this.showSavingState(studentId, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Focus Mode - Architecture avancée
|
||||
|
||||
#### Concept clé : Clonage d'éléments avec événements
|
||||
```javascript
|
||||
class FocusManager {
|
||||
showCurrentStudent() {
|
||||
"""
|
||||
Clonage intelligent avec attachement d'événements:
|
||||
|
||||
Problem: DOM cloneNode() ne clone pas les event listeners
|
||||
Solution: Re-attach events avec bindFocusStudentEvents()
|
||||
|
||||
Flow:
|
||||
1. Clone l'élément DOM élève courant
|
||||
2. Marquer avec data-focus-clone-of pour sync
|
||||
3. Force expand appreciation section
|
||||
4. Re-attach tous les event listeners
|
||||
5. Auto-focus sur textarea
|
||||
6. Optimize layout (no-scroll)
|
||||
"""
|
||||
const currentStudent = this.parent.state.filteredStudents[this.parent.state.focusCurrentIndex];
|
||||
const clonedStudent = currentStudent.cloneNode(true);
|
||||
|
||||
// Traçabilité pour synchronisation
|
||||
const studentId = clonedStudent.dataset.studentCard;
|
||||
clonedStudent.setAttribute('data-focus-clone-of', studentId);
|
||||
|
||||
// Force expand + styling
|
||||
const detailsSection = clonedStudent.querySelector('[data-student-details]');
|
||||
detailsSection.classList.remove('hidden');
|
||||
detailsSection.style.height = 'auto';
|
||||
clonedStudent.classList.add('focus-mode-student');
|
||||
|
||||
// Replace content + re-attach events
|
||||
focusContainer.innerHTML = '';
|
||||
focusContainer.appendChild(clonedStudent);
|
||||
this.bindFocusStudentEvents(clonedStudent, studentId);
|
||||
|
||||
// UX enhancements
|
||||
this.focusAppreciationTextarea(clonedStudent);
|
||||
this.optimizeHeight();
|
||||
}
|
||||
|
||||
bindFocusStudentEvents(clonedStudent, studentId) {
|
||||
"""
|
||||
Re-attachement complet des événements pour élément cloné:
|
||||
|
||||
Events à re-créer:
|
||||
1. textarea input/blur → auto-save avec sync
|
||||
2. save button click → manual save
|
||||
3. finalize button → confirmation workflow
|
||||
4. character counter → real-time update
|
||||
|
||||
Sync bidirectionnelle:
|
||||
- Focus → Original: syncAppreciationToOriginal()
|
||||
- Focus → Original: syncAppreciationStatusToOriginal()
|
||||
"""
|
||||
const textarea = clonedStudent.querySelector(`[data-appreciation-textarea][data-student-id="${studentId}"]`);
|
||||
|
||||
if (textarea) {
|
||||
// Auto-save avec debounce
|
||||
const saveHandler = this.parent.autoSaveManager.debounce(() => {
|
||||
this.saveFocusAppreciation(studentId, textarea.value);
|
||||
}, this.parent.options.debounceTime);
|
||||
|
||||
// Input avec sync temps réel
|
||||
textarea.addEventListener('input', (e) => {
|
||||
this.parent.state.modifiedAppreciations.add(studentId);
|
||||
this.syncAppreciationToOriginal(studentId, e.target.value); // Sync bidirectionnelle
|
||||
saveHandler();
|
||||
});
|
||||
|
||||
// Blur avec save immédiat
|
||||
textarea.addEventListener('blur', () => {
|
||||
if (this.parent.state.modifiedAppreciations.has(studentId)) {
|
||||
this.saveFocusAppreciation(studentId, textarea.value, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
syncAppreciationToOriginal(studentId, value) {
|
||||
"""
|
||||
Synchronisation Focus → Liste en temps réel:
|
||||
|
||||
Challenge: Maintenir cohérence entre élément cloné et original
|
||||
Solution: Sync immédiate sur chaque modification
|
||||
|
||||
Benefits:
|
||||
- Pas de perte de données si switch de mode
|
||||
- État cohérent entre vues
|
||||
- UX fluide
|
||||
"""
|
||||
const originalTextarea = document.querySelector(`[data-student-card="${studentId}"] [data-appreciation-textarea]`);
|
||||
if (originalTextarea && originalTextarea.value !== value) {
|
||||
originalTextarea.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Navigation et UX
|
||||
```javascript
|
||||
// Keyboard shortcuts avec gestion d'état
|
||||
bindKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!this.parent.state.isFocusMode) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
// Quick exit avec confirmation si modifications
|
||||
if (this.parent.state.modifiedAppreciations.size > 0) {
|
||||
if (confirm('Des modifications non sauvegardées seront perdues. Continuer ?')) {
|
||||
this.toggleFocusMode(false);
|
||||
}
|
||||
} else {
|
||||
this.toggleFocusMode(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ArrowLeft':
|
||||
this.navigatePrevious();
|
||||
break;
|
||||
|
||||
case 'ArrowRight':
|
||||
this.navigateNext();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
focusAppreciationTextarea(clonedStudent) {
|
||||
"""
|
||||
Auto-focus intelligent avec gestion de contexte:
|
||||
|
||||
Features:
|
||||
1. Focus sur textarea avec délai pour animation
|
||||
2. Curseur positionné en fin de texte existant
|
||||
3. Scroll smooth vers élément si nécessaire
|
||||
4. Compatible mobile (pas de keyboard pop automatique)
|
||||
"""
|
||||
setTimeout(() => {
|
||||
const textarea = clonedStudent.querySelector('[data-appreciation-textarea]');
|
||||
if (textarea) {
|
||||
textarea.focus();
|
||||
|
||||
// Cursor à la fin pour continuer écriture
|
||||
const textLength = textarea.value.length;
|
||||
textarea.setSelectionRange(textLength, textLength);
|
||||
|
||||
// Scroll smooth si nécessaire
|
||||
textarea.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'nearest'
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 Patterns et Optimisations
|
||||
|
||||
### 1. Repository Pattern
|
||||
- **Avantages** : Découplage, testabilité, réutilisabilité
|
||||
- **Implementation** : BaseRepository avec méthodes communes
|
||||
- **Optimisations** : Requêtes avec jointures, index optimaux
|
||||
|
||||
### 2. Service Layer Pattern
|
||||
- **Avantages** : Logique métier centralisée, transactions
|
||||
- **Implementation** : Services spécialisés avec injection de dépendances
|
||||
- **Factory** : CouncilServiceFactory pour création avec dépendances
|
||||
|
||||
### 3. Frontend State Management
|
||||
- **Centralized State** : Un seul object state par instance
|
||||
- **Immutable Updates** : Pas de mutation directe de state
|
||||
- **Event-driven** : Communication entre modules via événements
|
||||
|
||||
### 4. Performance Optimizations
|
||||
|
||||
#### Backend
|
||||
- **Database** : Index composites sur foreign keys + trimester
|
||||
- **Queries** : Eager loading avec joinedload() pour éviter N+1
|
||||
- **Caching** : Pas de cache DB (données temps réel requises)
|
||||
|
||||
#### Frontend
|
||||
- **DOM Queries** : Cache des sélecteurs dans this.elements
|
||||
- **Debouncing** : Auto-save et recherche avec délais optimaux
|
||||
- **Animation** : CSS transitions > JavaScript animations
|
||||
- **Memory** : Cleanup des event listeners sur mode changes
|
||||
|
||||
## 🧪 Testing Strategy
|
||||
|
||||
### Backend Tests
|
||||
```python
|
||||
# tests/test_council_services.py
|
||||
|
||||
class TestStudentEvaluationService:
|
||||
def test_calculate_trimester_average_with_coefficients(self):
|
||||
"""Test calcul moyenne pondérée avec différents coefficients"""
|
||||
# Given: Évaluations avec coefficients différents
|
||||
# When: Calcul moyenne élève
|
||||
# Then: Résultat pondéré correct
|
||||
|
||||
def test_performance_status_classification(self):
|
||||
"""Test classification automatique des performances"""
|
||||
# Test cases: 18.5→excellent, 14.2→good, 11.8→average, 8.5→struggling, None→no_data
|
||||
|
||||
class TestAppreciationService:
|
||||
def test_create_or_update_logic(self):
|
||||
"""Test logique création/mise à jour d'appréciation"""
|
||||
|
||||
def test_completion_stats_calculation(self):
|
||||
"""Test calcul statistiques de completion"""
|
||||
|
||||
class TestCouncilPreparationService:
|
||||
def test_prepare_council_data_integration(self):
|
||||
"""Test d'intégration complet du workflow"""
|
||||
```
|
||||
|
||||
### Frontend Tests
|
||||
```javascript
|
||||
// tests/council-preparation.test.js
|
||||
|
||||
describe('FocusManager', () => {
|
||||
test('should sync appreciation between focus and list mode', () => {
|
||||
// Given: Text entered in focus mode
|
||||
// When: Switch to list mode
|
||||
// Then: Same text appears in list mode
|
||||
});
|
||||
|
||||
test('should auto-focus textarea on student navigation', () => {
|
||||
// Given: Focus mode active
|
||||
// When: Navigate to next student
|
||||
// Then: Textarea has focus and cursor at end
|
||||
});
|
||||
});
|
||||
|
||||
describe('AutoSaveManager', () => {
|
||||
test('should debounce multiple saves for same student', () => {
|
||||
// Given: Multiple rapid text changes
|
||||
// When: Changes stop
|
||||
// Then: Only one HTTP request sent after debounce delay
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 🚀 Deployment & Monitoring
|
||||
|
||||
### Performance Metrics
|
||||
- **Page Load Time** : < 2s pour classe de 35 élèves
|
||||
- **Auto-save Latency** : < 500ms pour sauvegarde simple
|
||||
- **Memory Usage** : < 50MB JavaScript heap pour session complète
|
||||
- **Database** : < 100ms pour requêtes agrégées
|
||||
|
||||
### Error Tracking
|
||||
- **JavaScript Errors** : Console logging + remote tracking
|
||||
- **API Failures** : HTTP status codes + error messages
|
||||
- **User Experience** : Toast notifications + retry mechanisms
|
||||
|
||||
---
|
||||
|
||||
Cette architecture garantit **performance**, **maintenabilité** et **évolutivité** pour le module Conseil de Classe de Notytex.
|
||||
@@ -27,6 +27,7 @@ Cette documentation couvre l'ensemble de l'**architecture backend Notytex**, ses
|
||||
|----------|-------------|---------|
|
||||
| **[CLASSES_CRUD.md](./CLASSES_CRUD.md)** | Système CRUD des Classes - complet | ✅ |
|
||||
| **[ASSESSMENT_SERVICES.md](./ASSESSMENT_SERVICES.md)** | Services évaluations refactorisés - facade & DI | ✅ |
|
||||
| **[CONSEIL_DE_CLASSE_ARCHITECTURE.md](./CONSEIL_DE_CLASSE_ARCHITECTURE.md)** | Architecture Conseil de Classe - Mode Focus & Auto-save | ✅ |
|
||||
| **[MIGRATION_GUIDE.md](./MIGRATION_GUIDE.md)** | Guide migration Phase 1 - feature flags supprimés | ✅ |
|
||||
| Configuration Management | Gestion configuration dynamique | ✅ |
|
||||
|
||||
@@ -95,7 +96,8 @@ notytex/
|
||||
│ ├── assessment_repository.py # Repository Assessment
|
||||
│ └── class_repository.py # Repository ClassGroup ✅
|
||||
├── 📁 services/ # Logique métier découplée (SOLID)
|
||||
│ └── assessment_services.py # Services évaluations + Statistics + Progress ✅
|
||||
│ ├── assessment_services.py # Services évaluations + Statistics + Progress ✅
|
||||
│ └── council_services.py # Services conseil de classe + Auto-save ✅
|
||||
├── 📁 providers/ # Injection de dépendances (DI Pattern) ✅
|
||||
│ └── concrete_providers.py # ConfigProvider + DatabaseProvider optimisés
|
||||
├── 📁 config/ # Configuration externalisée
|
||||
@@ -175,6 +177,19 @@ notytex/
|
||||
|
||||
**Documentation** : [../CONFIGURATION_SCALES.md](../CONFIGURATION_SCALES.md)
|
||||
|
||||
### **Council Services (✅ Nouveau - Version 2.1)**
|
||||
|
||||
**Responsabilité** : Préparation complète des conseils de classe avec Mode Focus révolutionnaire
|
||||
|
||||
- ✅ **CouncilPreparationService** : Orchestrateur principal avec agrégation des données
|
||||
- ✅ **StudentEvaluationService** : Calculs moyennes pondérées et classification performances
|
||||
- ✅ **AppreciationService** : CRUD appréciations avec auto-sauvegarde intelligente
|
||||
- ✅ **Mode Focus Frontend** : Interface un-élève-à-la-fois avec synchronisation bidirectionnelle
|
||||
- ✅ **Auto-save Manager** : Débouncing, états visuels, gestion d'erreurs robuste
|
||||
- ✅ **Architecture Factory** : CouncilServiceFactory avec injection de dépendances
|
||||
|
||||
**Documentation** : [CONSEIL_DE_CLASSE_ARCHITECTURE.md](./CONSEIL_DE_CLASSE_ARCHITECTURE.md)
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ **Modèles de Données**
|
||||
|
||||
Reference in New Issue
Block a user