Migration v1 (Flask) -> v2 (FastAPI + Vue.js) complétée
✨ Changements majeurs: - Suppression complète du code Flask legacy - Migration backend FastAPI vers racine /backend - Migration frontend Vue.js vers racine /frontend - Suppression de notytex-v2/ (code monté à la racine) ✅ Validations: - Backend démarre correctement (port 8000) - API /api/v2/health répond healthy - 99/99 tests unitaires passent - Frontend configuré avec proxy Vite 📝 Documentation: - README.md réécrit pour v2 - Instructions de démarrage mises à jour - .gitignore adapté pour backend/frontend/ 🎯 Architecture finale: notytex/ ├── backend/ # FastAPI + SQLAlchemy + Pydantic ├── frontend/ # Vue 3 + Vite + TailwindCSS ├── docs/ # Documentation └── school_management.db # Base de données (inchangée) Jalon 6 complété: Application v2 prête pour utilisation!
This commit is contained in:
369
backend/tests/unit/test_student_report_service.py
Normal file
369
backend/tests/unit/test_student_report_service.py
Normal file
@@ -0,0 +1,369 @@
|
||||
"""
|
||||
Tests unitaires pour le service de génération de rapports.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from domain.services import StudentReportService, generate_report_html
|
||||
|
||||
|
||||
class TestStudentReportService:
|
||||
"""Tests pour le service de génération de rapports."""
|
||||
|
||||
@pytest.fixture
|
||||
def report_service(self):
|
||||
"""Fixture du service de rapport."""
|
||||
return StudentReportService()
|
||||
|
||||
@pytest.fixture
|
||||
def sample_assessment_data(self):
|
||||
"""Données d'évaluation de test."""
|
||||
return {
|
||||
'title': 'Contrôle de Mathématiques',
|
||||
'description': 'Chapitre 3 - Fonctions',
|
||||
'date': '2025-01-15',
|
||||
'trimester': 2,
|
||||
'class_name': '6ème A',
|
||||
'coefficient': 1.0
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def sample_exercises_data(self):
|
||||
"""Données d'exercices de test."""
|
||||
return [
|
||||
{
|
||||
'id': 1,
|
||||
'title': 'Exercice 1',
|
||||
'description': 'Calculs',
|
||||
'order': 1,
|
||||
'elements': [
|
||||
{
|
||||
'id': 101,
|
||||
'exercise_id': 1,
|
||||
'label': 'Question 1a',
|
||||
'description': 'Calcul simple',
|
||||
'skill': 'Calculer',
|
||||
'domain_name': 'Nombres',
|
||||
'domain_color': '#3b82f6',
|
||||
'grading_type': 'notes',
|
||||
'max_points': 2.0
|
||||
},
|
||||
{
|
||||
'id': 102,
|
||||
'exercise_id': 1,
|
||||
'label': 'Question 1b',
|
||||
'description': 'Calcul complexe',
|
||||
'skill': 'Calculer',
|
||||
'domain_name': 'Nombres',
|
||||
'domain_color': '#3b82f6',
|
||||
'grading_type': 'notes',
|
||||
'max_points': 3.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'title': 'Exercice 2',
|
||||
'description': 'Compétences',
|
||||
'order': 2,
|
||||
'elements': [
|
||||
{
|
||||
'id': 201,
|
||||
'exercise_id': 2,
|
||||
'label': 'Compétence graphique',
|
||||
'description': 'Tracer un graphe',
|
||||
'skill': 'Représenter',
|
||||
'domain_name': 'Géométrie',
|
||||
'domain_color': '#22c55e',
|
||||
'grading_type': 'score',
|
||||
'max_points': 3.0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@pytest.fixture
|
||||
def sample_student_data(self):
|
||||
"""Données d'élève de test."""
|
||||
return {
|
||||
'id': 1,
|
||||
'first_name': 'Marie',
|
||||
'last_name': 'Dupont',
|
||||
'email': 'marie.dupont@test.com'
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def sample_grades(self):
|
||||
"""Notes de test."""
|
||||
return {
|
||||
1: [ # Student 1
|
||||
{'element_id': 101, 'value': '1.5', 'comment': ''},
|
||||
{'element_id': 102, 'value': '2.5', 'comment': 'Bon travail'},
|
||||
{'element_id': 201, 'value': '2', 'comment': ''}
|
||||
],
|
||||
2: [ # Student 2
|
||||
{'element_id': 101, 'value': '2', 'comment': ''},
|
||||
{'element_id': 102, 'value': '1', 'comment': ''},
|
||||
{'element_id': 201, 'value': '3', 'comment': ''}
|
||||
]
|
||||
}
|
||||
|
||||
def test_generate_student_report(
|
||||
self, report_service, sample_assessment_data,
|
||||
sample_student_data, sample_grades, sample_exercises_data
|
||||
):
|
||||
"""Test de génération d'un rapport complet."""
|
||||
report = report_service.generate_student_report(
|
||||
sample_assessment_data,
|
||||
sample_student_data,
|
||||
sample_grades,
|
||||
sample_exercises_data
|
||||
)
|
||||
|
||||
# Vérifier la structure du rapport
|
||||
assert 'assessment' in report
|
||||
assert 'student' in report
|
||||
assert 'results' in report
|
||||
assert 'exercises' in report
|
||||
assert 'competences' in report
|
||||
assert 'domains' in report
|
||||
assert 'class_statistics' in report
|
||||
|
||||
# Vérifier les données de l'évaluation
|
||||
assert report['assessment']['title'] == 'Contrôle de Mathématiques'
|
||||
assert report['assessment']['trimester'] == 2
|
||||
|
||||
# Vérifier les données de l'élève
|
||||
assert report['student']['full_name'] == 'Marie Dupont'
|
||||
assert report['student']['email'] == 'marie.dupont@test.com'
|
||||
|
||||
# Vérifier les résultats
|
||||
# Score exercice 1: 1.5 + 2.5 = 4.0
|
||||
# Score exercice 2: 2 (score) -> 2/3 * 3 = 2.0
|
||||
# Total: 6.0
|
||||
# Student 2: 2 + 1 + (3/3)*3 = 6.0 (égalité)
|
||||
assert report['results']['total_score'] == 6.0
|
||||
assert report['results']['total_max_points'] == 8.0
|
||||
assert report['results']['percentage'] == 75.0
|
||||
assert report['results']['position'] == 1 # 1er ex-aequo (même score)
|
||||
assert report['results']['total_students'] == 2
|
||||
|
||||
def test_generate_report_with_special_values(
|
||||
self, report_service, sample_assessment_data,
|
||||
sample_exercises_data
|
||||
):
|
||||
"""Test avec valeurs spéciales (., d)."""
|
||||
student_data = {
|
||||
'id': 1,
|
||||
'first_name': 'Jean',
|
||||
'last_name': 'Martin',
|
||||
'email': 'jean@test.com'
|
||||
}
|
||||
|
||||
grades = {
|
||||
1: [
|
||||
{'element_id': 101, 'value': '.', 'comment': 'Pas de réponse'},
|
||||
{'element_id': 102, 'value': 'd', 'comment': 'Dispensé'},
|
||||
{'element_id': 201, 'value': '1', 'comment': ''}
|
||||
]
|
||||
}
|
||||
|
||||
report = report_service.generate_student_report(
|
||||
sample_assessment_data,
|
||||
student_data,
|
||||
grades,
|
||||
sample_exercises_data
|
||||
)
|
||||
|
||||
# . compte comme 0, d ne compte pas
|
||||
# Total: 0 + 1 (score 1/3 * 3) = 1.0
|
||||
# Max: 2 + 3 = 5.0 (le dispensé ne compte pas)
|
||||
assert report['results']['total_score'] == 1.0
|
||||
assert report['results']['total_max_points'] == 5.0
|
||||
|
||||
def test_competences_performance(
|
||||
self, report_service, sample_assessment_data,
|
||||
sample_student_data, sample_grades, sample_exercises_data
|
||||
):
|
||||
"""Test du calcul des performances par compétence."""
|
||||
report = report_service.generate_student_report(
|
||||
sample_assessment_data,
|
||||
sample_student_data,
|
||||
sample_grades,
|
||||
sample_exercises_data
|
||||
)
|
||||
|
||||
competences = {c['name']: c for c in report['competences']}
|
||||
|
||||
# Compétence "Calculer": 1.5 + 2.5 = 4.0 / 5.0
|
||||
assert 'Calculer' in competences
|
||||
assert competences['Calculer']['score'] == 4.0
|
||||
assert competences['Calculer']['max_points'] == 5.0
|
||||
assert competences['Calculer']['percentage'] == 80.0
|
||||
|
||||
# Compétence "Représenter": 2.0 / 3.0
|
||||
assert 'Représenter' in competences
|
||||
assert competences['Représenter']['score'] == 2.0
|
||||
assert competences['Représenter']['max_points'] == 3.0
|
||||
|
||||
def test_domains_performance(
|
||||
self, report_service, sample_assessment_data,
|
||||
sample_student_data, sample_grades, sample_exercises_data
|
||||
):
|
||||
"""Test du calcul des performances par domaine."""
|
||||
report = report_service.generate_student_report(
|
||||
sample_assessment_data,
|
||||
sample_student_data,
|
||||
sample_grades,
|
||||
sample_exercises_data
|
||||
)
|
||||
|
||||
domains = {d['name']: d for d in report['domains']}
|
||||
|
||||
# Domaine "Nombres": 4.0 / 5.0
|
||||
assert 'Nombres' in domains
|
||||
assert domains['Nombres']['score'] == 4.0
|
||||
assert domains['Nombres']['color'] == '#3b82f6'
|
||||
|
||||
# Domaine "Géométrie": 2.0 / 3.0
|
||||
assert 'Géométrie' in domains
|
||||
assert domains['Géométrie']['score'] == 2.0
|
||||
assert domains['Géométrie']['color'] == '#22c55e'
|
||||
|
||||
def test_student_not_in_grades(
|
||||
self, report_service, sample_assessment_data,
|
||||
sample_exercises_data
|
||||
):
|
||||
"""Test avec un élève sans notes."""
|
||||
student_data = {
|
||||
'id': 999,
|
||||
'first_name': 'Unknown',
|
||||
'last_name': 'Student',
|
||||
'email': 'unknown@test.com'
|
||||
}
|
||||
|
||||
grades = {
|
||||
1: [{'element_id': 101, 'value': '2', 'comment': ''}]
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="n'a pas de notes"):
|
||||
report_service.generate_student_report(
|
||||
sample_assessment_data,
|
||||
student_data,
|
||||
grades,
|
||||
sample_exercises_data
|
||||
)
|
||||
|
||||
|
||||
class TestGenerateReportHTML:
|
||||
"""Tests pour la génération HTML des rapports."""
|
||||
|
||||
@pytest.fixture
|
||||
def sample_report_data(self):
|
||||
"""Données de rapport pour le HTML."""
|
||||
return {
|
||||
'assessment': {
|
||||
'title': 'Test Évaluation',
|
||||
'description': 'Description test',
|
||||
'date': '2025-01-15',
|
||||
'trimester': 2,
|
||||
'class_name': '6ème A',
|
||||
'coefficient': 1.0
|
||||
},
|
||||
'student': {
|
||||
'full_name': 'Marie Dupont',
|
||||
'first_name': 'Marie',
|
||||
'last_name': 'Dupont',
|
||||
'email': 'marie@test.com'
|
||||
},
|
||||
'results': {
|
||||
'total_score': 15.0,
|
||||
'total_max_points': 20.0,
|
||||
'percentage': 75.0,
|
||||
'position': 5,
|
||||
'total_students': 25
|
||||
},
|
||||
'exercises': [
|
||||
{
|
||||
'title': 'Exercice 1',
|
||||
'description': 'Test',
|
||||
'score': 8.0,
|
||||
'max_points': 10.0,
|
||||
'percentage': 80.0,
|
||||
'elements': [
|
||||
{
|
||||
'label': 'Q1',
|
||||
'raw_value': '4',
|
||||
'score_label': '',
|
||||
'max_points': 5.0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
'competences': [],
|
||||
'domains': [],
|
||||
'class_statistics': {
|
||||
'count': 25,
|
||||
'mean': 12.5,
|
||||
'median': 13.0,
|
||||
'min': 5.0,
|
||||
'max': 19.0,
|
||||
'std_dev': 3.2
|
||||
}
|
||||
}
|
||||
|
||||
def test_generate_html_contains_student_name(self, sample_report_data):
|
||||
"""Le HTML contient le prénom de l'élève."""
|
||||
html = generate_report_html(sample_report_data)
|
||||
assert 'Marie' in html
|
||||
|
||||
def test_generate_html_contains_assessment_title(self, sample_report_data):
|
||||
"""Le HTML contient le titre de l'évaluation."""
|
||||
html = generate_report_html(sample_report_data)
|
||||
assert 'Test Évaluation' in html
|
||||
|
||||
def test_generate_html_contains_score(self, sample_report_data):
|
||||
"""Le HTML contient le score."""
|
||||
html = generate_report_html(sample_report_data)
|
||||
assert '15.0' in html or '15' in html
|
||||
assert '20.0' in html or '20' in html
|
||||
assert '75' in html # percentage
|
||||
|
||||
def test_generate_html_contains_statistics(self, sample_report_data):
|
||||
"""Le HTML contient les statistiques."""
|
||||
html = generate_report_html(sample_report_data)
|
||||
assert '12.5' in html # mean
|
||||
assert '13.0' in html or '13' in html # median
|
||||
|
||||
def test_generate_html_with_message(self, sample_report_data):
|
||||
"""Le HTML inclut le message personnalisé."""
|
||||
message = "Excellent travail cette année !"
|
||||
html = generate_report_html(sample_report_data, message)
|
||||
assert message in html
|
||||
|
||||
def test_generate_html_color_code_green(self):
|
||||
"""Couleur verte pour les bons scores (>= 80%)."""
|
||||
report_data = {
|
||||
'assessment': {'title': 'Test', 'date': '2025-01-01', 'trimester': 1, 'class_name': 'Test', 'coefficient': 1},
|
||||
'student': {'full_name': 'Test', 'first_name': 'Test', 'last_name': 'Test', 'email': 'test@test.com'},
|
||||
'results': {'total_score': 18, 'total_max_points': 20, 'percentage': 90.0, 'position': 1, 'total_students': 10},
|
||||
'exercises': [],
|
||||
'competences': [],
|
||||
'domains': [],
|
||||
'class_statistics': {'count': 10, 'mean': 12, 'median': 12, 'min': 5, 'max': 18, 'std_dev': 2}
|
||||
}
|
||||
html = generate_report_html(report_data)
|
||||
assert '#22c55e' in html # green
|
||||
|
||||
def test_generate_html_color_code_red(self):
|
||||
"""Couleur rouge pour les mauvais scores (< 40%)."""
|
||||
report_data = {
|
||||
'assessment': {'title': 'Test', 'date': '2025-01-01', 'trimester': 1, 'class_name': 'Test', 'coefficient': 1},
|
||||
'student': {'full_name': 'Test', 'first_name': 'Test', 'last_name': 'Test', 'email': 'test@test.com'},
|
||||
'results': {'total_score': 5, 'total_max_points': 20, 'percentage': 25.0, 'position': 10, 'total_students': 10},
|
||||
'exercises': [],
|
||||
'competences': [],
|
||||
'domains': [],
|
||||
'class_statistics': {'count': 10, 'mean': 12, 'median': 12, 'min': 5, 'max': 18, 'std_dev': 2}
|
||||
}
|
||||
html = generate_report_html(report_data)
|
||||
assert '#ef4444' in html # red
|
||||
Reference in New Issue
Block a user