""" Schemas Pydantic pour Assessment, Exercise, GradingElement. """ from datetime import date, datetime from typing import Optional, List, Union from pydantic import Field, field_validator from schemas.common import BaseSchema # GradingElement schemas class GradingElementBase(BaseSchema): """Schema de base pour GradingElement.""" label: str description: Optional[str] = None skill: Optional[str] = None max_points: float grading_type: str = "notes" domain_id: Optional[int] = None class GradingElementRead(GradingElementBase): """Schema pour la lecture d'un GradingElement.""" id: int exercise_id: int domain_name: Optional[str] = None domain_color: Optional[str] = None # Exercise schemas class ExerciseBase(BaseSchema): """Schema de base pour Exercise.""" title: str description: Optional[str] = None order: int = 1 class ExerciseRead(ExerciseBase): """Schema pour la lecture d'un Exercise.""" id: int assessment_id: int grading_elements: List[GradingElementRead] = [] @property def total_points(self) -> float: return sum(e.max_points for e in self.grading_elements) # Assessment schemas class AssessmentBase(BaseSchema): """Schema de base pour Assessment.""" title: str description: Optional[str] = None date: date trimester: int = Field(ge=1, le=3) coefficient: float = 1.0 class AssessmentRead(AssessmentBase): """Schema pour la lecture d'un Assessment.""" id: int class_group_id: int class_name: Optional[str] = None class AssessmentWithProgress(AssessmentRead): """Schema avec progression de correction.""" grading_progress: dict = Field(default_factory=dict) total_max_points: float = 0 class AssessmentDetail(AssessmentWithProgress): """Schema détaillé avec exercices.""" exercises: List[ExerciseRead] = [] class AssessmentList(BaseSchema): """Schema pour la liste des évaluations.""" assessments: List[AssessmentWithProgress] total: int # Schemas pour les résultats class StudentScore(BaseSchema): """Score d'un élève pour une évaluation.""" student_id: int student_name: str total_score: float total_max_points: float percentage: float exercise_scores: dict = Field(default_factory=dict) has_grades: bool = False # Indique si l'élève a au moins une note class AssessmentStatistics(BaseSchema): """Statistiques d'une évaluation.""" count: int mean: float median: float min: float max: float std_dev: float class HeatmapCell(BaseSchema): """Cellule d'une heatmap (élève x compétence/domaine).""" student_id: int student_name: str item_name: str # Nom de la compétence ou du domaine score: float max_points: float percentage: float color: Optional[str] = None class HeatmapData(BaseSchema): """Données pour une heatmap.""" items: List[str] # Liste des compétences ou domaines students: List[str] # Liste des noms d'élèves cells: List[HeatmapCell] # Données par cellule class AssessmentResults(BaseSchema): """Résultats complets d'une évaluation.""" assessment_id: int assessment_title: str statistics: AssessmentStatistics students_scores: List[StudentScore] histogram_data: List[int] = Field(default_factory=list) competences_heatmap: Optional[HeatmapData] = None domains_heatmap: Optional[HeatmapData] = None # Schemas pour les opérations d'écriture (CRUD) class GradingElementCreate(BaseSchema): """Schema pour créer ou mettre à jour un élément de notation.""" id: Optional[int] = None # Si présent, mise à jour; sinon création label: str description: Optional[str] = None skill: Optional[str] = None max_points: float grading_type: str = "notes" domain_id: Optional[int] = None class ExerciseCreate(BaseSchema): """Schema pour créer ou mettre à jour un exercice avec ses éléments.""" id: Optional[int] = None # Si présent, mise à jour; sinon création title: str description: Optional[str] = None order: int = 1 grading_elements: List[GradingElementCreate] = Field(default_factory=list) class AssessmentCreate(AssessmentBase): """Schema pour créer une évaluation complète (unifiée).""" class_group_id: int exercises: List[ExerciseCreate] = Field(default_factory=list) class AssessmentUpdate(BaseSchema): """Schema pour modifier une évaluation.""" title: Optional[str] = None description: Optional[str] = None date: Optional[str] = None # Accepter string, convertir après trimester: Optional[int] = None coefficient: Optional[float] = None # Pour modification complète avec exercices exercises: Optional[List[ExerciseCreate]] = None @field_validator('date', mode='before') @classmethod def parse_date(cls, v): if v is None: return None if isinstance(v, date): return v.isoformat() return v class AssessmentResponse(BaseSchema): """Schema de réponse après création/modification.""" id: int title: str message: str = "" # Schemas pour l'envoi de bilans par email class SendReportsRequest(BaseSchema): """Requête pour envoyer des bilans par email.""" student_ids: List[int] = Field(..., min_length=1, description="Liste des IDs d'élèves") custom_message: Optional[str] = Field(None, description="Message personnalisé du professeur") include_class_stats: bool = Field(True, description="Inclure les statistiques de classe") class SendReportResult(BaseSchema): """Résultat de l'envoi pour un élève.""" student_id: int student_name: str email: Optional[str] = None success: bool error_message: Optional[str] = None class SendReportsResponse(BaseSchema): """Réponse de l'envoi de bilans.""" success: bool total_sent: int total_failed: int results: List[SendReportResult] message: str