Files
yellow-bank-soal/app/schemas/report.py
Dwindi Ramadhana cf193d7ea0 first commit
2026-03-21 23:32:59 +07:00

265 lines
8.4 KiB
Python

"""
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