✨ 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!
611 lines
23 KiB
Python
611 lines
23 KiB
Python
"""
|
|
Tests de compatibilité croisée v1↔v2 pour les opérations d'écriture.
|
|
|
|
Ces tests vérifient que les données créées via l'API v2 sont correctement
|
|
lisibles et utilisables par l'API v1, et vice-versa.
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import date, timedelta
|
|
from httpx import AsyncClient
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
|
|
from infrastructure.database.models import (
|
|
ClassGroup,
|
|
Student,
|
|
StudentEnrollment,
|
|
Assessment,
|
|
Exercise,
|
|
GradingElement,
|
|
Grade,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestClassCRUD:
|
|
"""Tests CRUD pour les classes."""
|
|
|
|
async def test_create_class(self, client: AsyncClient, db_session: AsyncSession):
|
|
"""Test création d'une classe."""
|
|
class_data = {
|
|
"name": "Test Class API v2",
|
|
"description": "Classe créée via API v2",
|
|
"year": "2024-2025"
|
|
}
|
|
|
|
response = await client.post("/api/v2/classes", json=class_data)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["name"] == "Test Class API v2"
|
|
assert data["year"] == "2024-2025"
|
|
assert data["students_count"] == 0
|
|
assert "id" in data
|
|
|
|
# Vérifier en base
|
|
query = select(ClassGroup).where(ClassGroup.id == data["id"])
|
|
result = await db_session.execute(query)
|
|
cls = result.scalar_one()
|
|
assert cls.name == "Test Class API v2"
|
|
|
|
async def test_create_class_duplicate_name(self, client: AsyncClient, db_session: AsyncSession):
|
|
"""Test qu'on ne peut pas créer deux classes avec le même nom."""
|
|
class_data = {
|
|
"name": "Duplicate Class",
|
|
"year": "2024-2025"
|
|
}
|
|
|
|
# Première création
|
|
response1 = await client.post("/api/v2/classes", json=class_data)
|
|
assert response1.status_code == 201
|
|
|
|
# Deuxième création avec le même nom
|
|
response2 = await client.post("/api/v2/classes", json=class_data)
|
|
assert response2.status_code == 400
|
|
assert "existe déjà" in response2.json()["detail"]
|
|
|
|
async def test_update_class(self, client: AsyncClient, db_session: AsyncSession):
|
|
"""Test modification d'une classe."""
|
|
# Créer une classe
|
|
create_response = await client.post("/api/v2/classes", json={
|
|
"name": "Class to Update",
|
|
"year": "2024-2025"
|
|
})
|
|
class_id = create_response.json()["id"]
|
|
|
|
# Modifier la classe
|
|
update_response = await client.put(f"/api/v2/classes/{class_id}", json={
|
|
"name": "Updated Class Name",
|
|
"description": "New description"
|
|
})
|
|
|
|
assert update_response.status_code == 200
|
|
data = update_response.json()
|
|
assert data["name"] == "Updated Class Name"
|
|
assert data["description"] == "New description"
|
|
|
|
async def test_delete_class(self, client: AsyncClient, db_session: AsyncSession):
|
|
"""Test suppression d'une classe vide."""
|
|
# Créer une classe
|
|
create_response = await client.post("/api/v2/classes", json={
|
|
"name": "Class to Delete",
|
|
"year": "2024-2025"
|
|
})
|
|
class_id = create_response.json()["id"]
|
|
|
|
# Supprimer la classe
|
|
delete_response = await client.delete(f"/api/v2/classes/{class_id}")
|
|
assert delete_response.status_code == 204
|
|
|
|
# Vérifier qu'elle n'existe plus
|
|
get_response = await client.get(f"/api/v2/classes/{class_id}")
|
|
assert get_response.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestStudentCRUD:
|
|
"""Tests CRUD pour les étudiants."""
|
|
|
|
async def test_create_student_without_class(self, client: AsyncClient):
|
|
"""Test création d'un étudiant sans l'inscrire dans une classe."""
|
|
student_data = {
|
|
"first_name": "Jean",
|
|
"last_name": "Dupont",
|
|
"email": "jean.dupont@test.com"
|
|
}
|
|
|
|
response = await client.post("/api/v2/students", json=student_data)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["first_name"] == "Jean"
|
|
assert data["last_name"] == "Dupont"
|
|
assert data["current_class_id"] is None
|
|
|
|
async def test_create_student_with_class(self, client: AsyncClient):
|
|
"""Test création d'un étudiant avec inscription directe dans une classe."""
|
|
# Créer une classe d'abord
|
|
class_response = await client.post("/api/v2/classes", json={
|
|
"name": "Classe pour inscription",
|
|
"year": "2024-2025"
|
|
})
|
|
class_id = class_response.json()["id"]
|
|
|
|
# Créer un étudiant avec inscription
|
|
student_data = {
|
|
"first_name": "Marie",
|
|
"last_name": "Martin",
|
|
"class_group_id": class_id,
|
|
"enrollment_date": str(date.today())
|
|
}
|
|
|
|
response = await client.post("/api/v2/students", json=student_data)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["current_class_id"] == class_id
|
|
assert data["current_class_name"] == "Classe pour inscription"
|
|
|
|
async def test_enroll_existing_student(self, client: AsyncClient):
|
|
"""Test inscription d'un élève existant dans une classe."""
|
|
# Créer une classe
|
|
class_response = await client.post("/api/v2/classes", json={
|
|
"name": "Classe d'inscription",
|
|
"year": "2024-2025"
|
|
})
|
|
class_id = class_response.json()["id"]
|
|
|
|
# Créer un étudiant sans classe
|
|
student_response = await client.post("/api/v2/students", json={
|
|
"first_name": "Pierre",
|
|
"last_name": "Durand"
|
|
})
|
|
student_id = student_response.json()["id"]
|
|
|
|
# Inscrire l'étudiant
|
|
enroll_response = await client.post("/api/v2/students/enroll", json={
|
|
"student_id": student_id,
|
|
"class_group_id": class_id,
|
|
"enrollment_date": str(date.today()),
|
|
"enrollment_reason": "Inscription normale"
|
|
})
|
|
|
|
assert enroll_response.status_code == 201
|
|
data = enroll_response.json()
|
|
assert data["student_id"] == student_id
|
|
assert data["class_name"] == "Classe d'inscription"
|
|
assert data["is_new_student"] is False
|
|
|
|
async def test_enroll_new_student(self, client: AsyncClient):
|
|
"""Test inscription avec création d'un nouvel élève."""
|
|
# Créer une classe
|
|
class_response = await client.post("/api/v2/classes", json={
|
|
"name": "Classe nouvel élève",
|
|
"year": "2024-2025"
|
|
})
|
|
class_id = class_response.json()["id"]
|
|
|
|
# Inscrire avec création d'élève
|
|
enroll_response = await client.post("/api/v2/students/enroll", json={
|
|
"first_name": "Sophie",
|
|
"last_name": "Leroy",
|
|
"class_group_id": class_id,
|
|
"enrollment_date": str(date.today())
|
|
})
|
|
|
|
assert enroll_response.status_code == 201
|
|
data = enroll_response.json()
|
|
assert data["student_name"] == "Sophie Leroy"
|
|
assert data["is_new_student"] is True
|
|
|
|
async def test_transfer_student(self, client: AsyncClient):
|
|
"""Test transfert d'un élève vers une autre classe."""
|
|
# Créer deux classes
|
|
class1_response = await client.post("/api/v2/classes", json={
|
|
"name": "Classe origine",
|
|
"year": "2024-2025"
|
|
})
|
|
class1_id = class1_response.json()["id"]
|
|
|
|
class2_response = await client.post("/api/v2/classes", json={
|
|
"name": "Classe destination",
|
|
"year": "2024-2025"
|
|
})
|
|
class2_id = class2_response.json()["id"]
|
|
|
|
# Créer et inscrire un étudiant dans la première classe
|
|
enroll_response = await client.post("/api/v2/students/enroll", json={
|
|
"first_name": "Lucas",
|
|
"last_name": "Bernard",
|
|
"class_group_id": class1_id,
|
|
"enrollment_date": str(date.today() - timedelta(days=30))
|
|
})
|
|
student_id = enroll_response.json()["student_id"]
|
|
|
|
# Transférer vers la deuxième classe
|
|
transfer_response = await client.post("/api/v2/students/transfer", json={
|
|
"student_id": student_id,
|
|
"new_class_group_id": class2_id,
|
|
"transfer_date": str(date.today()),
|
|
"transfer_reason": "Changement de section"
|
|
})
|
|
|
|
assert transfer_response.status_code == 200
|
|
data = transfer_response.json()
|
|
assert data["old_class_name"] == "Classe origine"
|
|
assert data["new_class_name"] == "Classe destination"
|
|
|
|
async def test_student_departure(self, client: AsyncClient):
|
|
"""Test enregistrement du départ d'un élève."""
|
|
# Créer une classe et inscrire un élève
|
|
class_response = await client.post("/api/v2/classes", json={
|
|
"name": "Classe départ",
|
|
"year": "2024-2025"
|
|
})
|
|
class_id = class_response.json()["id"]
|
|
|
|
enroll_response = await client.post("/api/v2/students/enroll", json={
|
|
"first_name": "Emma",
|
|
"last_name": "Petit",
|
|
"class_group_id": class_id,
|
|
"enrollment_date": str(date.today() - timedelta(days=30))
|
|
})
|
|
student_id = enroll_response.json()["student_id"]
|
|
|
|
# Enregistrer le départ
|
|
departure_response = await client.post("/api/v2/students/departure", json={
|
|
"student_id": student_id,
|
|
"departure_date": str(date.today()),
|
|
"departure_reason": "Déménagement"
|
|
})
|
|
|
|
assert departure_response.status_code == 200
|
|
data = departure_response.json()
|
|
assert "Emma Petit" in data["student_name"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestAssessmentCRUD:
|
|
"""Tests CRUD pour les évaluations."""
|
|
|
|
async def test_create_assessment_unified(self, client: AsyncClient):
|
|
"""Test création unifiée d'une évaluation complète."""
|
|
# Créer une classe
|
|
class_response = await client.post("/api/v2/classes", json={
|
|
"name": "Classe évaluation",
|
|
"year": "2024-2025"
|
|
})
|
|
class_id = class_response.json()["id"]
|
|
|
|
# Créer une évaluation complète
|
|
assessment_data = {
|
|
"title": "Contrôle de mathématiques",
|
|
"description": "Test du chapitre 3",
|
|
"date": str(date.today()),
|
|
"trimester": 1,
|
|
"coefficient": 2.0,
|
|
"class_group_id": class_id,
|
|
"exercises": [
|
|
{
|
|
"title": "Exercice 1",
|
|
"description": "Calculs",
|
|
"order": 1,
|
|
"grading_elements": [
|
|
{
|
|
"label": "Question 1a",
|
|
"max_points": 2.0,
|
|
"grading_type": "notes"
|
|
},
|
|
{
|
|
"label": "Question 1b",
|
|
"max_points": 3.0,
|
|
"grading_type": "notes"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"title": "Exercice 2",
|
|
"description": "Problème",
|
|
"order": 2,
|
|
"grading_elements": [
|
|
{
|
|
"label": "Compétence résolution",
|
|
"max_points": 3.0,
|
|
"grading_type": "score",
|
|
"skill": "Résoudre des problèmes"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
response = await client.post("/api/v2/assessments", json=assessment_data)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["title"] == "Contrôle de mathématiques"
|
|
assert data["trimester"] == 1
|
|
assert data["coefficient"] == 2.0
|
|
assert len(data["exercises"]) == 2
|
|
assert data["total_max_points"] == 8.0 # 2 + 3 + 3
|
|
assert data["grading_progress"]["status"] == "not_started"
|
|
|
|
async def test_update_assessment(self, client: AsyncClient):
|
|
"""Test modification d'une évaluation."""
|
|
# Créer une classe et une évaluation
|
|
class_response = await client.post("/api/v2/classes", json={
|
|
"name": "Classe update eval",
|
|
"year": "2024-2025"
|
|
})
|
|
class_id = class_response.json()["id"]
|
|
|
|
create_response = await client.post("/api/v2/assessments", json={
|
|
"title": "Évaluation à modifier",
|
|
"date": str(date.today()),
|
|
"trimester": 1,
|
|
"coefficient": 1.0,
|
|
"class_group_id": class_id,
|
|
"exercises": []
|
|
})
|
|
assessment_id = create_response.json()["id"]
|
|
|
|
# Modifier l'évaluation
|
|
update_response = await client.put(f"/api/v2/assessments/{assessment_id}", json={
|
|
"title": "Évaluation modifiée",
|
|
"trimester": 2,
|
|
"coefficient": 1.5
|
|
})
|
|
|
|
assert update_response.status_code == 200
|
|
data = update_response.json()
|
|
assert data["title"] == "Évaluation modifiée"
|
|
assert data["trimester"] == 2
|
|
assert data["coefficient"] == 1.5
|
|
|
|
async def test_delete_assessment(self, client: AsyncClient):
|
|
"""Test suppression d'une évaluation."""
|
|
# Créer une classe et une évaluation
|
|
class_response = await client.post("/api/v2/classes", json={
|
|
"name": "Classe delete eval",
|
|
"year": "2024-2025"
|
|
})
|
|
class_id = class_response.json()["id"]
|
|
|
|
create_response = await client.post("/api/v2/assessments", json={
|
|
"title": "Évaluation à supprimer",
|
|
"date": str(date.today()),
|
|
"trimester": 1,
|
|
"coefficient": 1.0,
|
|
"class_group_id": class_id,
|
|
"exercises": [
|
|
{
|
|
"title": "Exercice test",
|
|
"order": 1,
|
|
"grading_elements": [
|
|
{"label": "Q1", "max_points": 5.0, "grading_type": "notes"}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
assessment_id = create_response.json()["id"]
|
|
|
|
# Supprimer
|
|
delete_response = await client.delete(f"/api/v2/assessments/{assessment_id}")
|
|
assert delete_response.status_code == 204
|
|
|
|
# Vérifier suppression
|
|
get_response = await client.get(f"/api/v2/assessments/{assessment_id}")
|
|
assert get_response.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestGradesCRUD:
|
|
"""Tests pour la saisie des notes."""
|
|
|
|
async def test_save_grades(self, client: AsyncClient, db_session: AsyncSession):
|
|
"""Test sauvegarde de notes pour une évaluation."""
|
|
# Créer une classe
|
|
class_response = await client.post("/api/v2/classes", json={
|
|
"name": "Classe notation",
|
|
"year": "2024-2025"
|
|
})
|
|
class_id = class_response.json()["id"]
|
|
|
|
# Créer un élève inscrit
|
|
enroll_response = await client.post("/api/v2/students/enroll", json={
|
|
"first_name": "Test",
|
|
"last_name": "Student",
|
|
"class_group_id": class_id,
|
|
"enrollment_date": str(date.today() - timedelta(days=30))
|
|
})
|
|
student_id = enroll_response.json()["student_id"]
|
|
|
|
# Créer une évaluation
|
|
assessment_response = await client.post("/api/v2/assessments", json={
|
|
"title": "Évaluation notation",
|
|
"date": str(date.today()),
|
|
"trimester": 1,
|
|
"coefficient": 1.0,
|
|
"class_group_id": class_id,
|
|
"exercises": [
|
|
{
|
|
"title": "Exercice 1",
|
|
"order": 1,
|
|
"grading_elements": [
|
|
{"label": "Q1", "max_points": 5.0, "grading_type": "notes"},
|
|
{"label": "Q2", "max_points": 5.0, "grading_type": "notes"}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
assessment_id = assessment_response.json()["id"]
|
|
|
|
# Récupérer les IDs des éléments de notation
|
|
get_response = await client.get(f"/api/v2/assessments/{assessment_id}")
|
|
elements = get_response.json()["exercises"][0]["grading_elements"]
|
|
|
|
# On ne peut pas utiliser les IDs directement car ils sont à 0 dans la création
|
|
# Récupérons-les depuis la base
|
|
query = select(GradingElement).join(Exercise).where(Exercise.assessment_id == assessment_id)
|
|
result = await db_session.execute(query)
|
|
db_elements = result.scalars().all()
|
|
|
|
# Sauvegarder des notes
|
|
grades_response = await client.post(f"/api/v2/assessments/{assessment_id}/grades", json={
|
|
"grades": [
|
|
{
|
|
"student_id": student_id,
|
|
"grading_element_id": db_elements[0].id,
|
|
"value": "4.5"
|
|
},
|
|
{
|
|
"student_id": student_id,
|
|
"grading_element_id": db_elements[1].id,
|
|
"value": "3"
|
|
}
|
|
]
|
|
})
|
|
|
|
assert grades_response.status_code == 200
|
|
data = grades_response.json()
|
|
assert data["saved_count"] == 2
|
|
assert data["created_count"] == 2
|
|
assert data["updated_count"] == 0
|
|
|
|
async def test_update_grades(self, client: AsyncClient, db_session: AsyncSession):
|
|
"""Test mise à jour de notes existantes."""
|
|
# Créer une classe et un élève
|
|
class_response = await client.post("/api/v2/classes", json={
|
|
"name": "Classe update notes",
|
|
"year": "2024-2025"
|
|
})
|
|
class_id = class_response.json()["id"]
|
|
|
|
enroll_response = await client.post("/api/v2/students/enroll", json={
|
|
"first_name": "Update",
|
|
"last_name": "Notes",
|
|
"class_group_id": class_id,
|
|
"enrollment_date": str(date.today() - timedelta(days=30))
|
|
})
|
|
student_id = enroll_response.json()["student_id"]
|
|
|
|
# Créer une évaluation
|
|
assessment_response = await client.post("/api/v2/assessments", json={
|
|
"title": "Évaluation update notes",
|
|
"date": str(date.today()),
|
|
"trimester": 1,
|
|
"coefficient": 1.0,
|
|
"class_group_id": class_id,
|
|
"exercises": [
|
|
{
|
|
"title": "Ex1",
|
|
"order": 1,
|
|
"grading_elements": [
|
|
{"label": "Q1", "max_points": 10.0, "grading_type": "notes"}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
assessment_id = assessment_response.json()["id"]
|
|
|
|
# Récupérer l'ID de l'élément
|
|
query = select(GradingElement).join(Exercise).where(Exercise.assessment_id == assessment_id)
|
|
result = await db_session.execute(query)
|
|
element = result.scalar_one()
|
|
|
|
# Première sauvegarde
|
|
await client.post(f"/api/v2/assessments/{assessment_id}/grades", json={
|
|
"grades": [
|
|
{"student_id": student_id, "grading_element_id": element.id, "value": "5"}
|
|
]
|
|
})
|
|
|
|
# Mise à jour
|
|
update_response = await client.post(f"/api/v2/assessments/{assessment_id}/grades", json={
|
|
"grades": [
|
|
{"student_id": student_id, "grading_element_id": element.id, "value": "8"}
|
|
]
|
|
})
|
|
|
|
assert update_response.status_code == 200
|
|
data = update_response.json()
|
|
assert data["updated_count"] == 1
|
|
assert data["created_count"] == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestCrossVersionCompatibility:
|
|
"""Tests de compatibilité croisée v1↔v2.
|
|
|
|
Ces tests vérifient que les données créées via v2 sont correctement
|
|
lisibles et cohérentes avec ce que v1 produirait.
|
|
"""
|
|
|
|
async def test_assessment_results_consistency(self, client: AsyncClient, db_session: AsyncSession):
|
|
"""Test que les résultats calculés sont cohérents."""
|
|
# Créer une classe avec des élèves
|
|
class_response = await client.post("/api/v2/classes", json={
|
|
"name": "Classe résultats",
|
|
"year": "2024-2025"
|
|
})
|
|
class_id = class_response.json()["id"]
|
|
|
|
# Créer plusieurs élèves
|
|
students = []
|
|
for i in range(3):
|
|
enroll_response = await client.post("/api/v2/students/enroll", json={
|
|
"first_name": f"Élève{i}",
|
|
"last_name": f"Test{i}",
|
|
"class_group_id": class_id,
|
|
"enrollment_date": str(date.today() - timedelta(days=30))
|
|
})
|
|
students.append(enroll_response.json()["student_id"])
|
|
|
|
# Créer une évaluation
|
|
assessment_response = await client.post("/api/v2/assessments", json={
|
|
"title": "Évaluation résultats",
|
|
"date": str(date.today()),
|
|
"trimester": 1,
|
|
"coefficient": 1.0,
|
|
"class_group_id": class_id,
|
|
"exercises": [
|
|
{
|
|
"title": "Exercice unique",
|
|
"order": 1,
|
|
"grading_elements": [
|
|
{"label": "Note principale", "max_points": 20.0, "grading_type": "notes"}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
assessment_id = assessment_response.json()["id"]
|
|
|
|
# Récupérer l'ID de l'élément
|
|
query = select(GradingElement).join(Exercise).where(Exercise.assessment_id == assessment_id)
|
|
result = await db_session.execute(query)
|
|
element = result.scalar_one()
|
|
|
|
# Noter les élèves : 12, 15, 18
|
|
grades = [
|
|
{"student_id": students[0], "grading_element_id": element.id, "value": "12"},
|
|
{"student_id": students[1], "grading_element_id": element.id, "value": "15"},
|
|
{"student_id": students[2], "grading_element_id": element.id, "value": "18"},
|
|
]
|
|
|
|
await client.post(f"/api/v2/assessments/{assessment_id}/grades", json={"grades": grades})
|
|
|
|
# Récupérer les résultats
|
|
results_response = await client.get(f"/api/v2/assessments/{assessment_id}/results")
|
|
|
|
assert results_response.status_code == 200
|
|
data = results_response.json()
|
|
|
|
# Vérifier les statistiques
|
|
stats = data["statistics"]
|
|
assert stats["count"] == 3
|
|
assert stats["mean"] == pytest.approx(15.0, abs=0.1) # (12+15+18)/3
|
|
assert stats["median"] == pytest.approx(15.0, abs=0.1)
|
|
assert stats["min"] == 12.0
|
|
assert stats["max"] == 18.0
|