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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user