feat: uniform competence management

This commit is contained in:
2025-08-05 20:44:54 +02:00
parent 4a2d8a73e1
commit 91eb04ca01
26 changed files with 3801 additions and 167 deletions

View File

@@ -0,0 +1,335 @@
import pytest
from models import GradingCalculator, db, Assessment, ClassGroup, Student, Exercise, GradingElement, Grade
from app_config import config_manager
from datetime import date
class TestUnifiedGrading:
"""Tests pour le système de notation unifié (Phase 2 - Refactoring)."""
def test_notes_calculation(self):
"""Test calcul notes numériques."""
# Test des valeurs numériques standard
assert GradingCalculator.calculate_score('15.5', 'notes', 20) == 15.5
assert GradingCalculator.calculate_score('0', 'notes', 20) == 0.0
assert GradingCalculator.calculate_score('20', 'notes', 20) == 20.0
# Test des valeurs décimales
assert GradingCalculator.calculate_score('12.75', 'notes', 20) == 12.75
def test_score_calculation(self):
"""Test calcul scores compétences (0-3)."""
# Score 2/3 * 12 = 8.0
assert GradingCalculator.calculate_score('2', 'score', 12) == 8.0
# Score 0/3 * 20 = 0.0
assert GradingCalculator.calculate_score('0', 'score', 20) == 0.0
# Score 3/3 * 15 = 15.0
assert GradingCalculator.calculate_score('3', 'score', 15) == 15.0
# Score 1/3 * 9 = 3.0
assert GradingCalculator.calculate_score('1', 'score', 9) == 3.0
def test_special_values(self):
"""Test valeurs spéciales unifiées."""
# Pas de réponse = 0
assert GradingCalculator.calculate_score('.', 'notes', 20) == 0
assert GradingCalculator.calculate_score('.', 'score', 12) == 0
# Dispensé = None
assert GradingCalculator.calculate_score('d', 'notes', 20) is None
assert GradingCalculator.calculate_score('d', 'score', 12) is None
# Absent = 0
assert GradingCalculator.calculate_score('a', 'notes', 20) == 0
assert GradingCalculator.calculate_score('a', 'score', 12) == 0
def test_is_counted_in_total(self):
"""Test si les valeurs comptent dans le total."""
# Valeurs normales comptent
assert GradingCalculator.is_counted_in_total('15.5', 'notes') == True
assert GradingCalculator.is_counted_in_total('2', 'score') == True
# Pas de réponse compte (= 0)
assert GradingCalculator.is_counted_in_total('.', 'notes') == True
assert GradingCalculator.is_counted_in_total('.', 'score') == True
# Absent compte (= 0)
assert GradingCalculator.is_counted_in_total('a', 'notes') == True
assert GradingCalculator.is_counted_in_total('a', 'score') == True
# Dispensé ne compte pas
assert GradingCalculator.is_counted_in_total('d', 'notes') == False
assert GradingCalculator.is_counted_in_total('d', 'score') == False
def test_validation(self):
"""Test validation des valeurs."""
# Notes valides
assert config_manager.validate_grade_value('15.5', 'notes') == True
assert config_manager.validate_grade_value('0', 'notes') == True
assert config_manager.validate_grade_value('20', 'notes') == True
# Scores valides
assert config_manager.validate_grade_value('0', 'score') == True
assert config_manager.validate_grade_value('1', 'score') == True
assert config_manager.validate_grade_value('2', 'score') == True
assert config_manager.validate_grade_value('3', 'score') == True
# Valeurs spéciales valides
assert config_manager.validate_grade_value('.', 'notes') == True
assert config_manager.validate_grade_value('d', 'score') == True
assert config_manager.validate_grade_value('a', 'notes') == True
# Valeurs invalides
assert config_manager.validate_grade_value('5', 'score') == False # > 3
assert config_manager.validate_grade_value('-1', 'notes') == False # < 0
assert config_manager.validate_grade_value('abc', 'notes') == False # non numérique
assert config_manager.validate_grade_value('1.5', 'score') == False # décimal pour score
def test_config_manager_methods(self):
"""Test nouvelles méthodes du ConfigManager."""
# Test get_grading_types
types = config_manager.get_grading_types()
assert 'notes' in types
assert 'score' in types
assert types['notes']['label'] == 'Notes numériques'
assert types['score']['max_value'] == 3
# Test get_special_values
special = config_manager.get_special_values()
assert '.' in special
assert 'd' in special
assert 'a' in special
assert special['.']['label'] == 'Pas de réponse'
assert special['d']['counts'] == False
assert special['a']['value'] == 0
# Test get_score_meanings
meanings = config_manager.get_score_meanings()
assert 0 in meanings
assert 3 in meanings
assert meanings[0]['label'] == 'Non acquis'
assert meanings[3]['label'] == 'Expert'
def test_display_info(self):
"""Test informations d'affichage."""
# Valeurs spéciales
info = config_manager.get_display_info('.', 'notes')
assert info['color'] == '#6b7280'
assert info['label'] == 'Pas de réponse'
# Scores avec significations
info = config_manager.get_display_info('2', 'score')
assert info['color'] == '#22c55e'
assert info['label'] == 'Acquis'
# Notes numériques (valeur par défaut)
info = config_manager.get_display_info('15.5', 'notes')
assert info['color'] == '#374151'
assert info['label'] == '15.5'
class TestIntegration:
"""Tests d'intégration pour le système unifié."""
@pytest.fixture
def sample_assessment(self, app):
"""Fixture pour créer une évaluation de test."""
with app.app_context():
# Créer classe
class_group = ClassGroup(name='6ème A', year='2025-2026')
db.session.add(class_group)
db.session.flush()
# Créer étudiants
student1 = Student(first_name='Alice', last_name='Martin', class_group_id=class_group.id)
student2 = Student(first_name='Bob', last_name='Durand', class_group_id=class_group.id)
db.session.add_all([student1, student2])
db.session.flush()
# Créer évaluation
assessment = Assessment(
title='Test Unifié',
date=date.today(),
trimester=1,
class_group_id=class_group.id
)
db.session.add(assessment)
db.session.flush()
# Créer exercice
exercise = Exercise(
title='Exercice 1',
assessment_id=assessment.id
)
db.session.add(exercise)
db.session.flush()
# Créer éléments de notation avec NOUVEAUX types
element_notes = GradingElement(
label='Question A',
exercise_id=exercise.id,
max_points=20.0,
grading_type='notes' # NOUVEAU type
)
element_score = GradingElement(
label='Compétence B',
exercise_id=exercise.id,
max_points=10.0,
grading_type='score' # NOUVEAU type
)
db.session.add_all([element_notes, element_score])
db.session.flush()
# Créer notes avec NOUVEAU système
grades = [
Grade(student_id=student1.id, grading_element_id=element_notes.id, value='15.5'),
Grade(student_id=student1.id, grading_element_id=element_score.id, value='2'),
Grade(student_id=student2.id, grading_element_id=element_notes.id, value='.'),
Grade(student_id=student2.id, grading_element_id=element_score.id, value='d'),
]
db.session.add_all(grades)
db.session.commit()
return assessment.id
def test_full_assessment_workflow(self, app, sample_assessment):
"""Test workflow complet avec nouveaux types."""
with app.app_context():
# Récupérer l'assessment depuis la DB pour éviter les problèmes de session
assessment = Assessment.query.get(sample_assessment)
# Test calcul scores avec logique unifiée
students_scores, exercise_scores = assessment.calculate_student_scores()
# Alice : 15.5 + (2/3 * 10) = 15.5 + 6.67 = 22.17
alice_score = students_scores[1]['total_score'] # ID 1 = Alice
assert alice_score == pytest.approx(22.17, rel=1e-2)
# Bob : 0 + dispensé = 0 (dispensé ne compte pas dans max)
bob_score = students_scores[2]['total_score'] # ID 2 = Bob
assert bob_score == 0.0
# Test max points : Alice a 30 points max (20 + 10)
alice_max = students_scores[1]['total_max_points']
assert alice_max == 30.0
# Bob a 20 points max (20 + dispensé ne compte pas)
bob_max = students_scores[2]['total_max_points']
assert bob_max == 20.0
def test_grading_progress_calculation(self, app, sample_assessment):
"""Test calcul progression avec nouveaux types."""
with app.app_context():
assessment = Assessment.query.get(sample_assessment)
progress = assessment.grading_progress
# 2 étudiants x 2 éléments = 4 notes possibles
# 4 notes saisies (y compris '.' et 'd')
assert progress['total'] == 4
assert progress['completed'] == 4
assert progress['percentage'] == 100
assert progress['status'] == 'completed'
def test_statistics_with_unified_system(self, app, sample_assessment):
"""Test statistiques avec système unifié."""
with app.app_context():
assessment = Assessment.query.get(sample_assessment)
stats = assessment.get_assessment_statistics()
# Vérifier calcul correct des statistiques
assert stats['count'] == 2
assert stats['min'] == 0.0 # Bob
assert stats['max'] == pytest.approx(22.17, rel=1e-2) # Alice
# Moyenne : (22.17 + 0) / 2 = 11.085
assert stats['mean'] == pytest.approx(11.09, rel=1e-2)
class TestPerformance:
"""Tests de performance pour le système unifié."""
def test_performance_large_dataset(self, app):
"""Test performance avec gros datasets (30 étudiants x 20 éléments)."""
import time
with app.app_context():
# Créer données de test
class_group = ClassGroup(name='Grande Classe', year='2025-2026')
db.session.add(class_group)
db.session.flush()
# 30 étudiants
students = []
for i in range(30):
student = Student(
first_name=f'Étudiant{i}',
last_name=f'Test{i}',
class_group_id=class_group.id
)
students.append(student)
db.session.add_all(students)
db.session.flush()
# Évaluation avec 20 éléments
assessment = Assessment(
title='Test Performance',
date=date.today(),
trimester=1,
class_group_id=class_group.id
)
db.session.add(assessment)
db.session.flush()
exercise = Exercise(title='Exercice Performance', assessment_id=assessment.id)
db.session.add(exercise)
db.session.flush()
# 20 éléments de notation (mix notes/scores)
elements = []
for i in range(20):
element_type = 'score' if i % 2 == 0 else 'notes'
element = GradingElement(
label=f'Élément {i}',
exercise_id=exercise.id,
max_points=10.0,
grading_type=element_type
)
elements.append(element)
db.session.add_all(elements)
db.session.flush()
# 600 notes (30 x 20)
grades = []
for student in students:
for element in elements:
if element.grading_type == 'score':
value = str((student.id + element.id) % 4) # 0-3
else:
value = str(((student.id + element.id) % 20) + 1) # 1-20
grade = Grade(
student_id=student.id,
grading_element_id=element.id,
value=value
)
grades.append(grade)
db.session.add_all(grades)
db.session.commit()
# Test performance calcul
start_time = time.time()
students_scores, _ = assessment.calculate_student_scores()
calculation_time = time.time() - start_time
# Vérifier temps réponse < 2s
assert calculation_time < 2.0, f"Calcul trop lent: {calculation_time:.2f}s"
# Vérifier cohérence résultats
assert len(students_scores) == 30
# Vérifier que tous les étudiants ont des scores
for student_id, data in students_scores.items():
assert data['total_score'] > 0
assert data['total_max_points'] == 200.0 # 20 éléments x 10 points