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,183 @@
import pytest
from models import db, CompetenceScaleValue, AppConfig, Competence
from app_config import config_manager
class TestConfigIntegrationReal:
"""Tests d'intégration réalistes pour le système de configuration."""
def test_auto_initialization_scale_values(self, app):
"""Test l'initialisation automatique des valeurs d'échelle."""
with app.app_context():
# S'assurer que la table est vide
CompetenceScaleValue.query.delete()
db.session.commit()
# Appeler get_competence_scale_values() devrait déclencher l'initialisation
scale_values = config_manager.get_competence_scale_values()
# Vérifier que les valeurs par défaut ont été créées
assert len(scale_values) > 0
# Vérifier que les valeurs de base existent
assert '0' in scale_values
assert '1' in scale_values
assert '2' in scale_values
assert '3' in scale_values
assert '.' in scale_values
assert 'd' in scale_values
assert 'a' in scale_values
# Vérifier les propriétés des valeurs
assert scale_values['0']['label'] == 'Non acquis'
assert scale_values['3']['label'] == 'Expert'
assert scale_values['.']['label'] == 'Pas de réponse'
assert scale_values['d']['included_in_total'] == False # Dispensé ne compte pas
def test_config_scale_page_workflow(self, app):
"""Test le workflow complet de la page config/scale."""
with app.app_context():
# 1. Récupérer les valeurs initiales
initial_values = config_manager.get_competence_scale_values()
assert '2' in initial_values
# 2. Modifier une valeur existante (simuler ce que fait routes/config.py)
success = config_manager.update_scale_value('2', 'Acquis (modifié)', '#00ff00', True)
assert success == True
# 3. Vérifier que la modification a été prise en compte
updated_values = config_manager.get_competence_scale_values()
assert updated_values['2']['label'] == 'Acquis (modifié)'
assert updated_values['2']['color'] == '#00ff00'
# 4. Ajouter une nouvelle valeur spéciale
success = config_manager.add_scale_value('X', 'Valeur X', '#purple', False)
assert success == True
# 5. Vérifier que la nouvelle valeur existe
updated_values = config_manager.get_competence_scale_values()
assert 'X' in updated_values
assert updated_values['X']['label'] == 'Valeur X'
# 6. Supprimer la valeur ajoutée
success = config_manager.delete_scale_value('X')
assert success == True
# 7. Vérifier qu'elle a été supprimée
final_values = config_manager.get_competence_scale_values()
assert 'X' not in final_values
def test_config_validation_with_real_data(self, app):
"""Test la validation avec les vraies données de configuration."""
with app.app_context():
# S'assurer que les valeurs par défaut existent
scale_values = config_manager.get_competence_scale_values()
# Valeurs de score standard (0-3)
for i in range(4):
assert config_manager.validate_grade_value(str(i), 'score') == True
# Score invalide (> 3)
assert config_manager.validate_grade_value('4', 'score') == False
# Valeurs spéciales
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
# Notes numériques 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
# Notes invalides
assert config_manager.validate_grade_value('-1', 'notes') == False
assert config_manager.validate_grade_value('abc', 'notes') == False
def test_get_display_info_with_real_data(self, app):
"""Test get_display_info avec les vraies données."""
with app.app_context():
# S'assurer que les valeurs existent
scale_values = config_manager.get_competence_scale_values()
# Test avec une valeur de score existante
if '2' in scale_values:
info = config_manager.get_display_info('2', 'score')
assert info['label'] == scale_values['2']['label']
assert info['color'] == scale_values['2']['color']
# Test avec valeur spéciale
if '.' in scale_values:
info = config_manager.get_display_info('.', 'notes')
assert info['label'] == scale_values['.']['label']
assert info['color'] == scale_values['.']['color']
# Test avec note numérique (valeur par défaut)
info = config_manager.get_display_info('15.5', 'notes')
assert info['label'] == '15.5'
assert info['color'] == '#374151' # Couleur par défaut
def test_route_compatibility(self, app):
"""Test la compatibilité avec les routes existantes."""
with app.app_context():
# Test du format attendu par la route config.scale
scale_values = config_manager.get_competence_scale_values()
# Vérifier le format de données
for value, config in scale_values.items():
assert isinstance(value, str) # Clé doit être string
assert 'label' in config
assert 'color' in config
assert 'included_in_total' in config
assert isinstance(config['included_in_total'], bool)
# Vérifier format couleur
color = config['color']
assert color.startswith('#')
assert len(color) == 7 # Format #RRGGBB
# Test des opérations CRUD utilisées par les routes
original_count = len(scale_values)
# Ajouter (utilisé par routes/config.py:add_scale_value)
success = config_manager.add_scale_value('TEST', 'Test Value', '#123456', True)
assert success == True
assert len(config_manager.get_competence_scale_values()) == original_count + 1
# Modifier (utilisé par routes/config.py:update_scale)
success = config_manager.update_scale_value('TEST', 'Modified Test', '#654321', False)
assert success == True
updated_values = config_manager.get_competence_scale_values()
assert updated_values['TEST']['label'] == 'Modified Test'
assert updated_values['TEST']['color'] == '#654321'
assert updated_values['TEST']['included_in_total'] == False
# Supprimer (utilisé par routes/config.py:delete_scale_value)
success = config_manager.delete_scale_value('TEST')
assert success == True
assert len(config_manager.get_competence_scale_values()) == original_count
def test_grading_system_integration(self, app):
"""Test l'intégration avec le système de notation unifié."""
with app.app_context():
# Vérifier que les types de notation sont bien définis
grading_types = config_manager.get_grading_types()
assert 'notes' in grading_types
assert 'score' in grading_types
# Vérifier que les valeurs spéciales sont cohérentes
special_values = config_manager.get_special_values()
scale_values = config_manager.get_competence_scale_values()
# Les valeurs spéciales doivent exister dans l'échelle
for special_key in special_values.keys():
assert special_key in scale_values
# Vérifier les significations des scores
score_meanings = config_manager.get_score_meanings()
for score in range(4): # 0, 1, 2, 3
assert score in score_meanings
score_str = str(score)
if score_str in scale_values:
# Le label de l'échelle doit correspondre à la signification
assert scale_values[score_str]['label'] == score_meanings[score]['label']

364
tests/test_config_system.py Normal file
View File

@@ -0,0 +1,364 @@
import pytest
from models import db, CompetenceScaleValue, AppConfig, Competence
from app_config import config_manager
from datetime import datetime
class TestConfigManager:
"""Tests pour le ConfigManager et la gestion de la configuration."""
def test_get_grading_types(self, app):
"""Test récupération des types de notation."""
with app.app_context():
grading_types = config_manager.get_grading_types()
assert 'notes' in grading_types
assert 'score' in grading_types
assert grading_types['notes']['label'] == 'Notes numériques'
assert grading_types['score']['max_value'] == 3
def test_get_special_values(self, app):
"""Test récupération des valeurs spéciales."""
with app.app_context():
special_values = config_manager.get_special_values()
assert '.' in special_values
assert 'd' in special_values
assert 'a' in special_values
assert special_values['.']['label'] == 'Pas de réponse'
assert special_values['d']['counts'] == False
assert special_values['a']['value'] == 0
def test_get_score_meanings(self, app):
"""Test récupération des significations des scores."""
with app.app_context():
score_meanings = config_manager.get_score_meanings()
assert 0 in score_meanings
assert 1 in score_meanings
assert 2 in score_meanings
assert 3 in score_meanings
assert score_meanings[0]['label'] == 'Non acquis'
assert score_meanings[3]['label'] == 'Expert'
def test_validate_grade_value(self, app):
"""Test validation des valeurs de notation."""
with app.app_context():
# 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
assert config_manager.validate_grade_value('-1', 'notes') == False
assert config_manager.validate_grade_value('abc', 'notes') == False
assert config_manager.validate_grade_value('1.5', 'score') == False
def test_get_display_info(self, app):
"""Test informations d'affichage."""
with app.app_context():
# Valeur spéciale
info = config_manager.get_display_info('.', 'notes')
assert 'color' in info
assert 'label' in info
assert info['label'] == 'Pas de réponse'
# Score avec signification
info = config_manager.get_display_info('2', 'score')
assert info['label'] == 'Acquis'
# Note numérique
info = config_manager.get_display_info('15.5', 'notes')
assert info['label'] == '15.5'
class TestCompetenceScaleValue:
"""Tests pour le modèle CompetenceScaleValue."""
def test_create_scale_value(self, app):
"""Test création d'une valeur d'échelle."""
with app.app_context():
scale_value = CompetenceScaleValue(
value='test',
label='Test Value',
color='#ff0000',
included_in_total=True
)
db.session.add(scale_value)
db.session.commit()
# Vérifier que la valeur a été créée
saved_value = CompetenceScaleValue.query.get('test')
assert saved_value is not None
assert saved_value.label == 'Test Value'
assert saved_value.color == '#ff0000'
assert saved_value.included_in_total == True
def test_scale_value_primary_key(self, app):
"""Test que 'value' est bien la clé primaire."""
with app.app_context():
scale_value1 = CompetenceScaleValue(
value='unique',
label='First',
color='#ff0000'
)
scale_value2 = CompetenceScaleValue(
value='unique',
label='Second',
color='#00ff00'
)
db.session.add(scale_value1)
db.session.commit()
# Essayer d'ajouter une valeur avec la même clé primaire
db.session.add(scale_value2)
with pytest.raises(Exception):
db.session.commit()
def test_get_competence_scale_values(self, app):
"""Test récupération des valeurs d'échelle."""
with app.app_context():
# Nettoyer la table d'abord
CompetenceScaleValue.query.delete()
db.session.commit()
# Ajouter quelques valeurs de test
values = [
CompetenceScaleValue(value='0', label='Zéro', color='#ff0000'),
CompetenceScaleValue(value='1', label='Un', color='#ffff00'),
CompetenceScaleValue(value='2', label='Deux', color='#00ff00'),
]
for value in values:
db.session.add(value)
db.session.commit()
# Récupérer via config_manager
scale_values = config_manager.get_competence_scale_values()
# Les clés doivent être des strings
assert '0' in scale_values
assert '1' in scale_values
assert '2' in scale_values
assert scale_values['0']['label'] == 'Zéro'
assert scale_values['1']['color'] == '#ffff00'
class TestConfigOperations:
"""Tests pour les opérations CRUD sur la configuration."""
def test_add_scale_value(self, app):
"""Test ajout d'une valeur d'échelle."""
with app.app_context():
# Ajouter une nouvelle valeur
success = config_manager.add_scale_value('X', 'Valeur X', '#123456', False)
assert success == True
# Vérifier qu'elle a été ajoutée
scale_value = CompetenceScaleValue.query.get('X')
assert scale_value is not None
assert scale_value.label == 'Valeur X'
assert scale_value.color == '#123456'
assert scale_value.included_in_total == False
def test_update_scale_value(self, app):
"""Test mise à jour d'une valeur d'échelle."""
with app.app_context():
# Créer une valeur
scale_value = CompetenceScaleValue(
value='update_test',
label='Original',
color='#000000',
included_in_total=True
)
db.session.add(scale_value)
db.session.commit()
# Mettre à jour
success = config_manager.update_scale_value('update_test', 'Updated', '#ffffff', False)
assert success == True
# Vérifier la mise à jour
updated_value = CompetenceScaleValue.query.get('update_test')
assert updated_value.label == 'Updated'
assert updated_value.color == '#ffffff'
assert updated_value.included_in_total == False
def test_update_nonexistent_scale_value(self, app):
"""Test mise à jour d'une valeur qui n'existe pas."""
with app.app_context():
success = config_manager.update_scale_value('nonexistent', 'Test', '#000000', True)
assert success == False
def test_delete_scale_value(self, app):
"""Test suppression d'une valeur d'échelle."""
with app.app_context():
# Créer une valeur
scale_value = CompetenceScaleValue(
value='delete_test',
label='To Delete',
color='#000000'
)
db.session.add(scale_value)
db.session.commit()
# Vérifier qu'elle existe
assert CompetenceScaleValue.query.get('delete_test') is not None
# Supprimer
success = config_manager.delete_scale_value('delete_test')
assert success == True
# Vérifier qu'elle a été supprimée
assert CompetenceScaleValue.query.get('delete_test') is None
def test_delete_nonexistent_scale_value(self, app):
"""Test suppression d'une valeur qui n'existe pas."""
with app.app_context():
success = config_manager.delete_scale_value('nonexistent')
assert success == False
class TestConfigIntegration:
"""Tests d'intégration pour le système de configuration."""
@pytest.fixture
def setup_scale_values(self, app):
"""Fixture pour créer des valeurs d'échelle de test."""
with app.app_context():
values = [
CompetenceScaleValue(value='0', label='Non acquis', color='#ef4444', included_in_total=True),
CompetenceScaleValue(value='1', label='En cours', color='#f59e0b', included_in_total=True),
CompetenceScaleValue(value='2', label='Acquis', color='#22c55e', included_in_total=True),
CompetenceScaleValue(value='3', label='Expert', color='#3b82f6', included_in_total=True),
CompetenceScaleValue(value='.', label='Pas de réponse', color='#6b7280', included_in_total=True),
CompetenceScaleValue(value='d', label='Dispensé', color='#9ca3af', included_in_total=False),
CompetenceScaleValue(value='a', label='Absent', color='#f87171', included_in_total=True),
]
for value in values:
db.session.add(value)
db.session.commit()
return values
def test_full_scale_workflow(self, app, setup_scale_values):
"""Test workflow complet de gestion d'échelle."""
with app.app_context():
# 1. Récupérer les valeurs
scale_values = config_manager.get_competence_scale_values()
assert len(scale_values) == 7
# 2. Ajouter une nouvelle valeur
success = config_manager.add_scale_value('N', 'Non évalué', '#cccccc', False)
assert success == True
scale_values = config_manager.get_competence_scale_values()
assert len(scale_values) == 8
assert 'N' in scale_values
# 3. Modifier une valeur existante
success = config_manager.update_scale_value('N', 'Non évalué (modifié)', '#dddddd', True)
assert success == True
updated_value = CompetenceScaleValue.query.get('N')
assert updated_value.label == 'Non évalué (modifié)'
assert updated_value.included_in_total == True
# 4. Supprimer la valeur
success = config_manager.delete_scale_value('N')
assert success == True
scale_values = config_manager.get_competence_scale_values()
assert len(scale_values) == 7
assert 'N' not in scale_values
def test_validation_with_database_values(self, app, setup_scale_values):
"""Test validation avec les valeurs de la base de données."""
with app.app_context():
# Valeurs numériques des scores
assert config_manager.validate_grade_value('0', 'score') == True
assert config_manager.validate_grade_value('3', 'score') == True
assert config_manager.validate_grade_value('4', 'score') == False # Au-dessus du max
# Valeurs spéciales
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 inexistantes
assert config_manager.validate_grade_value('Z', 'score') == False
def test_display_info_with_database_values(self, app, setup_scale_values):
"""Test informations d'affichage avec valeurs de la base."""
with app.app_context():
# Score avec signification
info = config_manager.get_display_info('2', 'score')
assert info['label'] == 'Acquis'
assert info['color'] == '#22c55e'
# Valeur spéciale
info = config_manager.get_display_info('.', 'notes')
assert info['label'] == 'Pas de réponse'
assert info['color'] == '#6b7280'
# Valeur inexistante (devrait retourner la valeur par défaut)
info = config_manager.get_display_info('99', 'notes')
assert info['label'] == '99' # Valeur brute
assert info['color'] == '#374151' # Couleur par défaut
class TestConfigErrorHandling:
"""Tests de gestion d'erreurs pour la configuration."""
def test_add_duplicate_scale_value(self, app):
"""Test ajout d'une valeur d'échelle en double."""
with app.app_context():
# Ajouter une valeur
success1 = config_manager.add_scale_value('dup', 'Original', '#000000')
assert success1 == True
# Essayer d'ajouter la même valeur
success2 = config_manager.add_scale_value('dup', 'Duplicate', '#ffffff')
assert success2 == False
def test_invalid_color_format(self, app):
"""Test avec format de couleur invalide."""
with app.app_context():
# Ces tests dépendent de la validation dans les routes,
# mais on peut tester le comportement du modèle
scale_value = CompetenceScaleValue(
value='invalid_color',
label='Test',
color='invalid' # Format invalide mais le modèle l'accepte
)
db.session.add(scale_value)
db.session.commit()
# Le modèle accepte n'importe quelle string, la validation doit être faite côté route
saved_value = CompetenceScaleValue.query.get('invalid_color')
assert saved_value.color == 'invalid'
def test_empty_label(self, app):
"""Test avec libellé vide."""
with app.app_context():
scale_value = CompetenceScaleValue(
value='empty_label',
label='', # Libellé vide
color='#000000'
)
db.session.add(scale_value)
db.session.commit()
saved_value = CompetenceScaleValue.query.get('empty_label')
assert saved_value.label == ''

View File

@@ -235,7 +235,7 @@ class TestGradingElement:
description="Calculer 1/2 + 1/3",
skill="Additionner des fractions",
max_points=4.0,
grading_type="points"
grading_type="notes"
)
db.session.add(grading_element)
db.session.commit()
@@ -243,7 +243,7 @@ class TestGradingElement:
assert grading_element.id is not None
assert grading_element.label == "Question 1"
assert grading_element.max_points == 4.0
assert grading_element.grading_type == "points"
assert grading_element.grading_type == "notes"
def test_grading_element_default_type(self, app):
with app.app_context():
@@ -268,7 +268,7 @@ class TestGradingElement:
# Default value is set in the column definition, check after saving
db.session.add(grading_element)
db.session.commit()
assert grading_element.grading_type == "points"
assert grading_element.grading_type == "notes"
def test_grading_element_repr(self, app):
with app.app_context():

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