""" Pydantic schemas for Report API endpoints. """ from datetime import datetime from typing import Any, Dict, List, Literal, Optional from pydantic import BaseModel, Field # ============================================================================= # Student Performance Report Schemas # ============================================================================= class StudentPerformanceRecordOutput(BaseModel): """Individual student performance record output.""" session_id: str wp_user_id: str tryout_id: str NM: Optional[int] = None NN: Optional[int] = None theta: Optional[float] = None theta_se: Optional[float] = None total_benar: int time_spent: int # Total time in seconds start_time: Optional[datetime] = None end_time: Optional[datetime] = None scoring_mode_used: str rataan_used: Optional[float] = None sb_used: Optional[float] = None class AggregatePerformanceStatsOutput(BaseModel): """Aggregate statistics for student performance output.""" tryout_id: str participant_count: int avg_nm: Optional[float] = None std_nm: Optional[float] = None min_nm: Optional[int] = None max_nm: Optional[int] = None median_nm: Optional[float] = None avg_nn: Optional[float] = None std_nn: Optional[float] = None avg_theta: Optional[float] = None pass_rate: float # Percentage with NN >= 500 avg_time_spent: float # Average time in seconds class StudentPerformanceReportOutput(BaseModel): """Complete student performance report output.""" generated_at: datetime tryout_id: str website_id: int date_range: Optional[Dict[str, str]] = None aggregate: AggregatePerformanceStatsOutput individual_records: List[StudentPerformanceRecordOutput] = [] class StudentPerformanceReportRequest(BaseModel): """Request schema for student performance report.""" tryout_id: str = Field(..., description="Tryout identifier") website_id: int = Field(..., description="Website identifier") date_start: Optional[datetime] = Field(None, description="Filter by start date") date_end: Optional[datetime] = Field(None, description="Filter by end date") format_type: Literal["individual", "aggregate", "both"] = Field( default="both", description="Report format" ) # ============================================================================= # Item Analysis Report Schemas # ============================================================================= class ItemAnalysisRecordOutput(BaseModel): """Item analysis record output for a single item.""" item_id: int slot: int level: str ctt_p: Optional[float] = None ctt_bobot: Optional[float] = None ctt_category: Optional[str] = None irt_b: Optional[float] = None irt_se: Optional[float] = None calibrated: bool calibration_sample_size: int correctness_rate: float item_total_correlation: Optional[float] = None information_values: Dict[float, float] = Field(default_factory=dict) optimal_theta_range: str = "N/A" class ItemAnalysisReportOutput(BaseModel): """Complete item analysis report output.""" generated_at: datetime tryout_id: str website_id: int total_items: int items: List[ItemAnalysisRecordOutput] summary: Dict[str, Any] class ItemAnalysisReportRequest(BaseModel): """Request schema for item analysis report.""" tryout_id: str = Field(..., description="Tryout identifier") website_id: int = Field(..., description="Website identifier") filter_by: Optional[Literal["difficulty", "calibrated", "discrimination"]] = Field( None, description="Filter items by category" ) difficulty_level: Optional[Literal["mudah", "sedang", "sulit"]] = Field( None, description="Filter by difficulty level (only when filter_by='difficulty')" ) # ============================================================================= # Calibration Status Report Schemas # ============================================================================= class CalibrationItemStatusOutput(BaseModel): """Calibration status for a single item output.""" item_id: int slot: int level: str sample_size: int calibrated: bool irt_b: Optional[float] = None irt_se: Optional[float] = None ctt_p: Optional[float] = None class CalibrationStatusReportOutput(BaseModel): """Complete calibration status report output.""" generated_at: datetime tryout_id: str website_id: int total_items: int calibrated_items: int calibration_percentage: float items_awaiting_calibration: List[CalibrationItemStatusOutput] avg_calibration_sample_size: float estimated_time_to_90_percent: Optional[str] = None ready_for_irt_rollout: bool items: List[CalibrationItemStatusOutput] class CalibrationStatusReportRequest(BaseModel): """Request schema for calibration status report.""" tryout_id: str = Field(..., description="Tryout identifier") website_id: int = Field(..., description="Website identifier") # ============================================================================= # Tryout Comparison Report Schemas # ============================================================================= class TryoutComparisonRecordOutput(BaseModel): """Tryout comparison data point output.""" tryout_id: str date: Optional[str] = None subject: Optional[str] = None participant_count: int avg_nm: Optional[float] = None avg_nn: Optional[float] = None avg_theta: Optional[float] = None std_nm: Optional[float] = None calibration_percentage: float class TryoutComparisonReportOutput(BaseModel): """Complete tryout comparison report output.""" generated_at: datetime comparison_type: Literal["date", "subject"] tryouts: List[TryoutComparisonRecordOutput] trends: Optional[Dict[str, Any]] = None normalization_impact: Optional[Dict[str, Any]] = None class TryoutComparisonReportRequest(BaseModel): """Request schema for tryout comparison report.""" tryout_ids: List[str] = Field(..., min_length=2, description="List of tryout IDs to compare") website_id: int = Field(..., description="Website identifier") group_by: Literal["date", "subject"] = Field( default="date", description="Group comparison by date or subject" ) # ============================================================================= # Report Scheduling Schemas # ============================================================================= class ReportScheduleRequest(BaseModel): """Request schema for scheduling a report.""" report_type: Literal["student_performance", "item_analysis", "calibration_status", "tryout_comparison"] = Field( ..., description="Type of report to generate" ) schedule: Literal["daily", "weekly", "monthly"] = Field( ..., description="Schedule frequency" ) tryout_ids: List[str] = Field(..., description="List of tryout IDs for the report") website_id: int = Field(..., description="Website identifier") recipients: List[str] = Field(..., description="List of email addresses to send report to") export_format: Literal["csv", "xlsx", "pdf"] = Field( default="xlsx", description="Export format for the report" ) class ReportScheduleOutput(BaseModel): """Output schema for scheduled report.""" schedule_id: str report_type: str schedule: str tryout_ids: List[str] website_id: int recipients: List[str] format: str created_at: datetime last_run: Optional[datetime] = None next_run: Optional[datetime] = None is_active: bool class ReportScheduleResponse(BaseModel): """Response schema for schedule creation.""" schedule_id: str message: str next_run: Optional[datetime] = None # ============================================================================= # Export Schemas # ============================================================================= class ExportRequest(BaseModel): """Request schema for exporting a report.""" schedule_id: str = Field(..., description="Schedule ID to generate report for") export_format: Literal["csv", "xlsx", "pdf"] = Field( default="xlsx", description="Export format" ) class ExportResponse(BaseModel): """Response schema for export request.""" file_path: str file_name: str format: str generated_at: datetime download_url: Optional[str] = None