refactor: extract duplicated patterns into shared helpers

Backend: create api/helpers.py with eligible_enrollment_filter,
count_eligible_students, get_active_enrollment, ensure_unique_name,
upsert_app_configs, and build_heatmap. Add full_name properties to
Student model. Apply across all route files (-481/+184 lines).

Frontend: create stores/helpers.js with withLoading composable,
apply to assessments and classes Pinia stores.

96/96 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 14:05:10 +01:00
parent b1b7d12a9f
commit a0ab7224e1
10 changed files with 402 additions and 481 deletions

196
backend/api/helpers.py Normal file
View File

@@ -0,0 +1,196 @@
"""
Fonctions utilitaires partagées entre les routes API.
Élimine la duplication de code pour les patterns courants.
"""
from datetime import date
from typing import Optional, Dict, List, Tuple, Any, Callable
from fastapi import HTTPException
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from infrastructure.database.models import (
AppConfig,
StudentEnrollment,
Student,
)
from schemas.assessment import HeatmapCell, HeatmapData
def eligible_enrollment_filter(class_group_id: int, at_date: date):
"""
Retourne les clauses WHERE pour filtrer les inscriptions actives à une date donnée.
Usage:
query = select(StudentEnrollment).where(
*eligible_enrollment_filter(class_id, assessment.date)
)
"""
return (
StudentEnrollment.class_group_id == class_group_id,
StudentEnrollment.enrollment_date <= at_date,
(
StudentEnrollment.departure_date.is_(None)
| (StudentEnrollment.departure_date >= at_date)
),
)
async def count_eligible_students(
session: AsyncSession, class_group_id: int, at_date: date
) -> int:
"""Compte les élèves inscrits dans une classe à une date donnée."""
query = select(func.count(StudentEnrollment.id)).where(
*eligible_enrollment_filter(class_group_id, at_date)
)
result = await session.execute(query)
return result.scalar() or 0
def eligible_student_ids_subquery(class_group_id: int, at_date: date):
"""Retourne une sous-requête des student_id éligibles à une date donnée."""
return select(StudentEnrollment.student_id).where(
*eligible_enrollment_filter(class_group_id, at_date)
)
async def get_eligible_enrollments(
session: AsyncSession, class_group_id: int, at_date: date
):
"""Récupère les inscriptions éligibles avec les étudiants chargés."""
query = (
select(StudentEnrollment)
.options(selectinload(StudentEnrollment.student))
.where(*eligible_enrollment_filter(class_group_id, at_date))
)
result = await session.execute(query)
return result.scalars().all()
def get_active_enrollment(
student: Student,
) -> Optional[StudentEnrollment]:
"""Retourne l'inscription active (sans date de départ) d'un élève."""
for enrollment in student.enrollments:
if enrollment.departure_date is None:
return enrollment
return None
async def ensure_unique_name(
session: AsyncSession,
model_class,
name: str,
*,
field_name: str = "name",
exclude_id: Optional[int] = None,
entity_label: str = "enregistrement",
):
"""
Vérifie qu'un nom est unique pour un modèle donné.
Lève HTTPException 400 si un doublon est trouvé.
"""
field = getattr(model_class, field_name)
query = select(model_class).where(field == name)
if exclude_id is not None:
query = query.where(model_class.id != exclude_id)
result = await session.execute(query)
if result.scalar_one_or_none():
raise HTTPException(
status_code=400,
detail=f"Un(e) {entity_label} avec le nom '{name}' existe déjà",
)
async def upsert_app_configs(
session: AsyncSession, updates: Dict[str, str]
) -> None:
"""
Met à jour ou crée des entrées AppConfig en lot.
Ne fait PAS de commit — l'appelant doit appeler session.commit().
"""
for key, value in updates.items():
query = select(AppConfig).where(AppConfig.key == key)
result = await session.execute(query)
config = result.scalar_one_or_none()
if config:
config.value = value
else:
session.add(AppConfig(key=key, value=value))
def build_heatmap(
enrollments,
assessment,
items: dict,
item_extractor: Callable,
grading_calc,
sorted_student_names: List[str],
*,
color_map: Optional[Dict[str, str]] = None,
) -> Optional[HeatmapData]:
"""
Construit une HeatmapData pour des compétences ou domaines.
Args:
enrollments: liste d'inscriptions avec .student chargé
assessment: l'évaluation avec .exercises chargés
items: set ou dict des items à évaluer (noms)
item_extractor: fonction (element) -> nom de l'item ou None
grading_calc: instance de GradingCalculator
sorted_student_names: noms triés pour l'axe Y
color_map: optionnel, dict nom -> couleur pour les cellules
"""
if not items:
return None
item_names = set(items) if isinstance(items, dict) else items
cells = []
for enrollment in enrollments:
student = enrollment.student
student_name = student.full_name_reversed
item_scores = {name: {"score": 0.0, "max": 0.0} for name in item_names}
for exercise in assessment.exercises:
for element in exercise.grading_elements:
item_name = item_extractor(element)
if item_name and item_name in item_scores:
for g in element.grades:
if g.student_id == student.id and g.value:
calc_score = grading_calc.calculate_score(
g.value.strip(),
element.grading_type,
element.max_points,
)
if calc_score is not None:
item_scores[item_name]["score"] += calc_score
item_scores[item_name]["max"] += element.max_points
break
for name, data in item_scores.items():
if data["max"] > 0:
pct = round((data["score"] / data["max"]) * 100, 1)
cell_kwargs = dict(
student_id=student.id,
student_name=student_name,
item_name=name,
score=round(data["score"], 2),
max_points=data["max"],
percentage=pct,
)
if color_map and name in color_map:
cell_kwargs["color"] = color_map[name]
cells.append(HeatmapCell(**cell_kwargs))
return HeatmapData(
items=sorted(list(item_names)),
students=sorted_student_names,
cells=cells,
)

View File

@@ -11,6 +11,12 @@ from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
from api.dependencies import AsyncSessionDep
from api.helpers import (
count_eligible_students,
eligible_student_ids_subquery,
get_eligible_enrollments,
build_heatmap,
)
from infrastructure.database.models import (
Assessment,
Exercise,
@@ -36,8 +42,6 @@ from schemas.assessment import (
SendReportsRequest,
SendReportResult,
SendReportsResponse,
HeatmapCell,
HeatmapData,
)
from schemas.grading import BulkGradeCreate, BulkGradeResponse, GradeRead
from domain.services import GradingCalculator, StatisticsService, StudentReportService, generate_report_html, ConfigService
@@ -124,21 +128,13 @@ async def get_assessments(
total_elements += 1
# Compter les élèves éligibles (inscrits à la date de l'évaluation)
eligible_query = select(func.count(StudentEnrollment.id)).where(
StudentEnrollment.class_group_id == assessment.class_group_id,
StudentEnrollment.enrollment_date <= assessment.date,
(StudentEnrollment.departure_date.is_(None) |
(StudentEnrollment.departure_date >= assessment.date))
eligible_students_count = await count_eligible_students(
session, assessment.class_group_id, assessment.date
)
eligible_result = await session.execute(eligible_query)
eligible_students_count = eligible_result.scalar() or 0
# Compter les notes saisies uniquement pour les élèves éligibles
eligible_student_ids = select(StudentEnrollment.student_id).where(
StudentEnrollment.class_group_id == assessment.class_group_id,
StudentEnrollment.enrollment_date <= assessment.date,
(StudentEnrollment.departure_date.is_(None) |
(StudentEnrollment.departure_date >= assessment.date))
eligible_student_ids = eligible_student_ids_subquery(
assessment.class_group_id, assessment.date
)
grades_query = select(func.count(Grade.id)).where(
Grade.grading_element_id.in_(
@@ -239,21 +235,13 @@ async def get_assessment(
)
# Calculer la progression
eligible_query = select(func.count(StudentEnrollment.id)).where(
StudentEnrollment.class_group_id == assessment.class_group_id,
StudentEnrollment.enrollment_date <= assessment.date,
(StudentEnrollment.departure_date.is_(None) |
(StudentEnrollment.departure_date >= assessment.date))
eligible_students_count = await count_eligible_students(
session, assessment.class_group_id, assessment.date
)
eligible_result = await session.execute(eligible_query)
eligible_students_count = eligible_result.scalar() or 0
# Compter les notes uniquement pour les élèves éligibles
eligible_student_ids = select(StudentEnrollment.student_id).where(
StudentEnrollment.class_group_id == assessment.class_group_id,
StudentEnrollment.enrollment_date <= assessment.date,
(StudentEnrollment.departure_date.is_(None) |
(StudentEnrollment.departure_date >= assessment.date))
eligible_student_ids = eligible_student_ids_subquery(
assessment.class_group_id, assessment.date
)
grades_query = select(func.count(Grade.id)).where(
Grade.grading_element_id.in_(
@@ -266,11 +254,11 @@ async def get_assessment(
)
grades_result = await session.execute(grades_query)
grades_count = grades_result.scalar() or 0
progress = calculate_grading_progress(
assessment, grades_count, total_elements, eligible_students_count
)
return AssessmentDetail(
id=assessment.id,
title=assessment.title,
@@ -322,18 +310,9 @@ async def get_assessment_results(
raise HTTPException(status_code=404, detail="Évaluation non trouvée")
# Récupérer les élèves éligibles
eligible_query = (
select(StudentEnrollment)
.options(selectinload(StudentEnrollment.student))
.where(
StudentEnrollment.class_group_id == assessment.class_group_id,
StudentEnrollment.enrollment_date <= assessment.date,
(StudentEnrollment.departure_date.is_(None) |
(StudentEnrollment.departure_date >= assessment.date))
)
enrollments = await get_eligible_enrollments(
session, assessment.class_group_id, assessment.date
)
eligible_result = await session.execute(eligible_query)
enrollments = eligible_result.scalars().all()
# Calculer le total des points maximum
total_max_points = 0
@@ -392,7 +371,7 @@ async def get_assessment_results(
student_scores[student_id] = StudentScore(
student_id=student_id,
student_name=f"{student.last_name} {student.first_name}",
student_name=student.full_name_reversed,
email=student.email, # Ajouter l'email de l'élève
total_score=round(total_score, 2),
total_max_points=counted_max,
@@ -440,97 +419,25 @@ async def get_assessment_results(
all_domains[element.domain.name] = element.domain.color
# Calculer heatmap des compétences si présentes
if all_competences:
competences_cells = []
for enrollment in enrollments:
student = enrollment.student
student_name = f"{student.last_name} {student.first_name}"
# Calculer score par compétence pour cet élève
competence_scores = {}
for comp in all_competences:
competence_scores[comp] = {"score": 0.0, "max": 0.0}
for exercise in assessment.exercises:
for element in exercise.grading_elements:
if element.skill and element.skill in competence_scores:
# Trouver la note
for g in element.grades:
if g.student_id == student.id and g.value:
calc_score = grading_calc.calculate_score(
g.value.strip(), element.grading_type, element.max_points
)
if calc_score is not None:
competence_scores[element.skill]["score"] += calc_score
competence_scores[element.skill]["max"] += element.max_points
break
# Créer les cellules
for comp, data in competence_scores.items():
if data["max"] > 0:
pct = round((data["score"] / data["max"]) * 100, 1)
competences_cells.append(HeatmapCell(
student_id=student.id,
student_name=student_name,
item_name=comp,
score=round(data["score"], 2),
max_points=data["max"],
percentage=pct
))
competences_heatmap = HeatmapData(
items=sorted(list(all_competences)),
students=[s.student_name for s in sorted_scores],
cells=competences_cells
)
competences_heatmap = build_heatmap(
enrollments=enrollments,
assessment=assessment,
items=all_competences,
item_extractor=lambda el: el.skill,
grading_calc=grading_calc,
sorted_student_names=[s.student_name for s in sorted_scores],
)
# Calculer heatmap des domaines si présents
if all_domains:
domains_cells = []
for enrollment in enrollments:
student = enrollment.student
student_name = f"{student.last_name} {student.first_name}"
# Calculer score par domaine pour cet élève
domain_scores = {}
for dom in all_domains:
domain_scores[dom] = {"score": 0.0, "max": 0.0}
for exercise in assessment.exercises:
for element in exercise.grading_elements:
if element.domain and element.domain.name in domain_scores:
# Trouver la note
for g in element.grades:
if g.student_id == student.id and g.value:
calc_score = grading_calc.calculate_score(
g.value.strip(), element.grading_type, element.max_points
)
if calc_score is not None:
domain_scores[element.domain.name]["score"] += calc_score
domain_scores[element.domain.name]["max"] += element.max_points
break
# Créer les cellules
for dom, data in domain_scores.items():
if data["max"] > 0:
pct = round((data["score"] / data["max"]) * 100, 1)
domains_cells.append(HeatmapCell(
student_id=student.id,
student_name=student_name,
item_name=dom,
score=round(data["score"], 2),
max_points=data["max"],
percentage=pct,
color=all_domains.get(dom)
))
domains_heatmap = HeatmapData(
items=sorted(list(all_domains.keys())),
students=[s.student_name for s in sorted_scores],
cells=domains_cells
)
domains_heatmap = build_heatmap(
enrollments=enrollments,
assessment=assessment,
items=all_domains,
item_extractor=lambda el: el.domain.name if el.domain else None,
grading_calc=grading_calc,
sorted_student_names=[s.student_name for s in sorted_scores],
color_map=all_domains,
)
return AssessmentResults(
assessment_id=assessment.id,
@@ -827,21 +734,13 @@ async def update_assessment(
)
# Calculer la progression
eligible_query = select(func.count(StudentEnrollment.id)).where(
StudentEnrollment.class_group_id == assessment.class_group_id,
StudentEnrollment.enrollment_date <= assessment.date,
(StudentEnrollment.departure_date.is_(None) |
(StudentEnrollment.departure_date >= assessment.date))
eligible_students_count = await count_eligible_students(
session, assessment.class_group_id, assessment.date
)
eligible_result = await session.execute(eligible_query)
eligible_students_count = eligible_result.scalar() or 0
# Compter les notes uniquement pour les élèves éligibles
eligible_student_ids = select(StudentEnrollment.student_id).where(
StudentEnrollment.class_group_id == assessment.class_group_id,
StudentEnrollment.enrollment_date <= assessment.date,
(StudentEnrollment.departure_date.is_(None) |
(StudentEnrollment.departure_date >= assessment.date))
eligible_student_ids = eligible_student_ids_subquery(
assessment.class_group_id, assessment.date
)
grades_query = select(func.count(Grade.id)).where(
Grade.grading_element_id.in_(
@@ -854,11 +753,11 @@ async def update_assessment(
)
grades_result = await session.execute(grades_query)
grades_count = grades_result.scalar() or 0
progress = calculate_grading_progress(
assessment, grades_count, total_elements, eligible_students_count
)
return AssessmentDetail(
id=assessment.id,
title=assessment.title,
@@ -1132,7 +1031,7 @@ async def send_reports(
total_failed = 0
for student in students:
student_name = f"{student.first_name} {student.last_name}"
student_name = student.full_name
# Vérifier que l'élève a une adresse email
if not student.email:
@@ -1307,7 +1206,7 @@ async def preview_report(
if student_id not in all_students_grades:
raise HTTPException(
status_code=400,
detail=f"L'élève {student.first_name} {student.last_name} n'a pas de notes pour cette évaluation"
detail=f"L'élève {student.full_name} n'a pas de notes pour cette évaluation"
)
# Créer le service de rapport

View File

@@ -12,6 +12,7 @@ from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
from api.dependencies import AsyncSessionDep
from api.helpers import ensure_unique_name
from infrastructure.database.models import (
ClassGroup,
Student,
@@ -201,7 +202,7 @@ async def get_class_students(
last_name=student.last_name,
first_name=student.first_name,
email=student.email,
full_name=f"{student.first_name} {student.last_name}",
full_name=student.full_name,
current_class_id=class_id if is_active else None,
current_class_name=cls.name if is_active else None,
enrollment_id=enrollment.id,
@@ -419,13 +420,7 @@ async def create_class(
Crée une nouvelle classe.
"""
# Vérifier l'unicité du nom
existing_query = select(ClassGroup).where(ClassGroup.name == class_data.name)
existing_result = await session.execute(existing_query)
if existing_result.scalar_one_or_none():
raise HTTPException(
status_code=400,
detail=f"Une classe avec le nom '{class_data.name}' existe déjà"
)
await ensure_unique_name(session, ClassGroup, class_data.name, entity_label="classe")
# Créer la nouvelle classe
new_class = ClassGroup(
@@ -466,16 +461,10 @@ async def update_class(
# Vérifier l'unicité du nouveau nom si modifié
if class_data.name and class_data.name != cls.name:
existing_query = select(ClassGroup).where(
ClassGroup.name == class_data.name,
ClassGroup.id != class_id
await ensure_unique_name(
session, ClassGroup, class_data.name,
exclude_id=class_id, entity_label="classe"
)
existing_result = await session.execute(existing_query)
if existing_result.scalar_one_or_none():
raise HTTPException(
status_code=400,
detail=f"Une autre classe avec le nom '{class_data.name}' existe déjà"
)
# Appliquer les modifications
if class_data.name is not None:

View File

@@ -8,6 +8,7 @@ from fastapi import APIRouter, HTTPException
from sqlalchemy import select, func, delete
from api.dependencies import AsyncSessionDep
from api.helpers import ensure_unique_name, upsert_app_configs
from infrastructure.database.models import (
AppConfig,
Competence,
@@ -228,13 +229,7 @@ async def create_competence(
Crée une nouvelle compétence.
"""
# Vérifier l'unicité du nom
existing_query = select(Competence).where(Competence.name == data.name)
existing_result = await session.execute(existing_query)
if existing_result.scalar_one_or_none():
raise HTTPException(
status_code=400,
detail=f"Une compétence avec le nom '{data.name}' existe déjà"
)
await ensure_unique_name(session, Competence, data.name, entity_label="compétence")
# Déterminer l'index d'ordre
if data.order_index is None:
@@ -284,16 +279,7 @@ async def update_competence(
# Vérifier l'unicité du nouveau nom
if data.name and data.name != competence.name:
existing_query = select(Competence).where(
Competence.name == data.name,
Competence.id != competence_id
)
existing_result = await session.execute(existing_query)
if existing_result.scalar_one_or_none():
raise HTTPException(
status_code=400,
detail=f"Une autre compétence avec le nom '{data.name}' existe déjà"
)
await ensure_unique_name(session, Competence, data.name, exclude_id=competence_id, entity_label="compétence")
# Appliquer les modifications
if data.name is not None:
@@ -353,13 +339,7 @@ async def create_domain(
Crée un nouveau domaine.
"""
# Vérifier l'unicité du nom
existing_query = select(Domain).where(Domain.name == data.name)
existing_result = await session.execute(existing_query)
if existing_result.scalar_one_or_none():
raise HTTPException(
status_code=400,
detail=f"Un domaine avec le nom '{data.name}' existe déjà"
)
await ensure_unique_name(session, Domain, data.name, entity_label="domaine")
# Créer le domaine
domain = Domain(
@@ -398,16 +378,7 @@ async def update_domain(
# Vérifier l'unicité du nouveau nom
if data.name and data.name != domain.name:
existing_query = select(Domain).where(
Domain.name == data.name,
Domain.id != domain_id
)
existing_result = await session.execute(existing_query)
if existing_result.scalar_one_or_none():
raise HTTPException(
status_code=400,
detail=f"Un autre domaine avec le nom '{data.name}' existe déjà"
)
await ensure_unique_name(session, Domain, data.name, exclude_id=domain_id, entity_label="domaine")
# Appliquer les modifications
if data.name is not None:
@@ -669,18 +640,7 @@ async def update_app_config(
if data.default_grading_system is not None:
updates.append(("default_grading_system", data.default_grading_system))
for key, value in updates:
# Chercher la config existante
query = select(AppConfig).where(AppConfig.key == key)
result = await session.execute(query)
config = result.scalar_one_or_none()
if config:
config.value = value
else:
# Créer si n'existe pas
new_config = AppConfig(key=key, value=value)
session.add(new_config)
await upsert_app_configs(session, dict(updates))
await session.commit()
@@ -755,18 +715,7 @@ async def update_smtp_config(
if data.from_address is not None:
updates.append(("email.from_address", data.from_address))
for key, value in updates:
# Chercher la config existante
query = select(AppConfig).where(AppConfig.key == key)
result = await session.execute(query)
config = result.scalar_one_or_none()
if config:
config.value = value
else:
# Créer si n'existe pas
new_config = AppConfig(key=key, value=value)
session.add(new_config)
await upsert_app_configs(session, dict(updates))
await session.commit()
@@ -843,16 +792,7 @@ async def update_notes_gradient(
if data.enabled is not None:
updates.append(("grading.notes_gradient.enabled", "true" if data.enabled else "false"))
for key, value in updates:
query = select(AppConfig).where(AppConfig.key == key)
result = await session.execute(query)
config = result.scalar_one_or_none()
if config:
config.value = value
else:
new_config = AppConfig(key=key, value=value)
session.add(new_config)
await upsert_app_configs(session, dict(updates))
await session.commit()

View File

@@ -202,7 +202,7 @@ async def get_council_preparation(
student_id=student.id,
first_name=student.first_name,
last_name=student.last_name,
full_name=f"{student.first_name} {student.last_name}",
full_name=student.full_name,
overall_average=overall_average,
assessment_count=assessment_count,
grades_by_assessment=grades_by_assessment,

View File

@@ -10,6 +10,7 @@ from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
from api.dependencies import AsyncSessionDep
from api.helpers import get_active_enrollment
from infrastructure.database.models import (
Student,
StudentEnrollment,
@@ -62,12 +63,8 @@ async def get_students(
students_list = []
for student in students:
# Trouver l'inscription active
current_enrollment = None
for enrollment in student.enrollments:
if enrollment.departure_date is None:
current_enrollment = enrollment
break
current_enrollment = get_active_enrollment(student)
# Filtrer par classe si demandé
if class_id and (not current_enrollment or current_enrollment.class_group_id != class_id):
continue
@@ -78,7 +75,7 @@ async def get_students(
last_name=student.last_name,
first_name=student.first_name,
email=student.email,
full_name=f"{student.first_name} {student.last_name}",
full_name=student.full_name,
current_class_id=current_enrollment.class_group_id if current_enrollment else None,
current_class_name=current_enrollment.class_group.name if current_enrollment else None
)
@@ -113,13 +110,10 @@ async def get_student(
raise HTTPException(status_code=404, detail="Étudiant non trouvé")
# Trouver l'inscription active
current_enrollment = None
current_enrollment = get_active_enrollment(student)
enrollments_list = []
for enrollment in student.enrollments:
if enrollment.departure_date is None:
current_enrollment = enrollment
enrollments_list.append(
EnrollmentRead(
id=enrollment.id,
@@ -144,7 +138,7 @@ async def get_student(
last_name=student.last_name,
first_name=student.first_name,
email=student.email,
full_name=f"{student.first_name} {student.last_name}",
full_name=student.full_name,
current_class_id=current_enrollment.class_group_id if current_enrollment else None,
current_class_name=current_enrollment.class_group.name if current_enrollment else None,
enrollments=enrollments_list
@@ -210,7 +204,7 @@ async def create_student(
last_name=new_student.last_name,
first_name=new_student.first_name,
email=new_student.email,
full_name=f"{new_student.first_name} {new_student.last_name}",
full_name=new_student.full_name,
current_class_id=current_class_id,
current_class_name=current_class_name
)
@@ -264,18 +258,14 @@ async def update_student(
await session.refresh(student)
# Trouver la classe actuelle
current_enrollment = None
for enrollment in student.enrollments:
if enrollment.departure_date is None:
current_enrollment = enrollment
break
current_enrollment = get_active_enrollment(student)
return StudentWithClass(
id=student.id,
last_name=student.last_name,
first_name=student.first_name,
email=student.email,
full_name=f"{student.first_name} {student.last_name}",
full_name=student.full_name,
current_class_id=current_enrollment.class_group_id if current_enrollment else None,
current_class_name=current_enrollment.class_group.name if current_enrollment else None
)
@@ -325,18 +315,14 @@ async def update_student_email(
await session.refresh(student)
# Trouver la classe actuelle
current_enrollment = None
for enrollment in student.enrollments:
if enrollment.departure_date is None:
current_enrollment = enrollment
break
current_enrollment = get_active_enrollment(student)
return StudentWithClass(
id=student.id,
last_name=student.last_name,
first_name=student.first_name,
email=student.email,
full_name=f"{student.first_name} {student.last_name}",
full_name=student.full_name,
current_class_id=current_enrollment.class_group_id if current_enrollment else None,
current_class_name=current_enrollment.class_group.name if current_enrollment else None
)
@@ -403,7 +389,7 @@ async def enroll_student(
if active_result.scalar_one_or_none():
raise HTTPException(
status_code=400,
detail=f"L'élève {student.first_name} {student.last_name} est déjà inscrit dans une classe"
detail=f"L'élève {student.full_name} est déjà inscrit dans une classe"
)
else:
# Nouvel élève
@@ -447,9 +433,9 @@ async def enroll_student(
return EnrollmentResponse(
enrollment_id=enrollment.id,
student_id=student.id,
student_name=f"{student.first_name} {student.last_name}",
student_name=student.full_name,
class_name=class_group.name,
message=f"Élève {student.first_name} {student.last_name} inscrit en {class_group.name}",
message=f"Élève {student.full_name} inscrit en {class_group.name}",
is_new_student=is_new_student
)
@@ -493,9 +479,9 @@ async def transfer_student(
if not old_enrollment:
raise HTTPException(
status_code=400,
detail=f"Aucune inscription active trouvée pour l'élève {student.first_name} {student.last_name}"
detail=f"Aucune inscription active trouvée pour l'élève {student.full_name}"
)
old_class_name = old_enrollment.class_group.name
# Terminer l'ancienne inscription
@@ -516,10 +502,10 @@ async def transfer_student(
return TransferResponse(
old_enrollment_id=old_enrollment.id,
new_enrollment_id=new_enrollment.id,
student_name=f"{student.first_name} {student.last_name}",
student_name=student.full_name,
old_class_name=old_class_name,
new_class_name=new_class.name,
message=f"Élève {student.first_name} {student.last_name} transféré de {old_class_name} vers {new_class.name}"
message=f"Élève {student.full_name} transféré de {old_class_name} vers {new_class.name}"
)
@@ -554,9 +540,9 @@ async def record_departure(
if not enrollment:
raise HTTPException(
status_code=400,
detail=f"Aucune inscription active trouvée pour l'élève {student.first_name} {student.last_name}"
detail=f"Aucune inscription active trouvée pour l'élève {student.full_name}"
)
class_name = enrollment.class_group.name
# Enregistrer le départ
@@ -567,7 +553,7 @@ async def record_departure(
return DepartureResponse(
enrollment_id=enrollment.id,
student_name=f"{student.first_name} {student.last_name}",
student_name=student.full_name,
class_name=class_name,
message=f"Départ de {student.first_name} {student.last_name} de {class_name} enregistré"
message=f"Départ de {student.full_name} de {class_name} enregistré"
)

View File

@@ -123,6 +123,16 @@ class Student(Base):
"CouncilAppreciation", back_populates="student", lazy="selectin"
)
@property
def full_name(self) -> str:
"""Prénom Nom"""
return f"{self.first_name} {self.last_name}"
@property
def full_name_reversed(self) -> str:
"""Nom Prénom"""
return f"{self.last_name} {self.first_name}"
def __repr__(self):
return f"<Student {self.first_name} {self.last_name}>"