first commit
This commit is contained in:
792
app/routers/reports.py
Normal file
792
app/routers/reports.py
Normal file
@@ -0,0 +1,792 @@
|
||||
"""
|
||||
Reports API router for comprehensive reporting.
|
||||
|
||||
Endpoints:
|
||||
- GET /reports/student/performance: Get student performance report
|
||||
- GET /reports/items/analysis: Get item analysis report
|
||||
- GET /reports/calibration/status: Get calibration status report
|
||||
- GET /reports/tryout/comparison: Get tryout comparison report
|
||||
- POST /reports/schedule: Schedule a report
|
||||
- GET /reports/export/{schedule_id}/{format}: Export scheduled report
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header, status
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.schemas.report import (
|
||||
StudentPerformanceReportOutput,
|
||||
AggregatePerformanceStatsOutput,
|
||||
StudentPerformanceRecordOutput,
|
||||
ItemAnalysisReportOutput,
|
||||
ItemAnalysisRecordOutput,
|
||||
CalibrationStatusReportOutput,
|
||||
CalibrationItemStatusOutput,
|
||||
TryoutComparisonReportOutput,
|
||||
TryoutComparisonRecordOutput,
|
||||
ReportScheduleRequest,
|
||||
ReportScheduleOutput,
|
||||
ReportScheduleResponse,
|
||||
ExportResponse,
|
||||
)
|
||||
from app.services.reporting import (
|
||||
generate_student_performance_report,
|
||||
generate_item_analysis_report,
|
||||
generate_calibration_status_report,
|
||||
generate_tryout_comparison_report,
|
||||
export_report_to_csv,
|
||||
export_report_to_excel,
|
||||
export_report_to_pdf,
|
||||
schedule_report,
|
||||
get_scheduled_report,
|
||||
list_scheduled_reports,
|
||||
cancel_scheduled_report,
|
||||
StudentPerformanceReport,
|
||||
ItemAnalysisReport,
|
||||
CalibrationStatusReport,
|
||||
TryoutComparisonReport,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/reports", tags=["reports"])
|
||||
|
||||
|
||||
def get_website_id_from_header(
|
||||
x_website_id: Optional[str] = Header(None, alias="X-Website-ID"),
|
||||
) -> int:
|
||||
"""
|
||||
Extract and validate website_id from request header.
|
||||
|
||||
Args:
|
||||
x_website_id: Website ID from header
|
||||
|
||||
Returns:
|
||||
Validated website ID as integer
|
||||
|
||||
Raises:
|
||||
HTTPException: If header is missing or invalid
|
||||
"""
|
||||
if x_website_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="X-Website-ID header is required",
|
||||
)
|
||||
try:
|
||||
return int(x_website_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="X-Website-ID must be a valid integer",
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Student Performance Report Endpoints
|
||||
# =============================================================================
|
||||
|
||||
@router.get(
|
||||
"/student/performance",
|
||||
response_model=StudentPerformanceReportOutput,
|
||||
summary="Get student performance report",
|
||||
description="Generate student performance report with individual and aggregate statistics.",
|
||||
)
|
||||
async def get_student_performance_report(
|
||||
tryout_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
date_start: Optional[datetime] = None,
|
||||
date_end: Optional[datetime] = None,
|
||||
format_type: Literal["individual", "aggregate", "both"] = "both",
|
||||
) -> StudentPerformanceReportOutput:
|
||||
"""
|
||||
Get student performance report.
|
||||
|
||||
Returns individual student records and/or aggregate statistics.
|
||||
"""
|
||||
date_range = None
|
||||
if date_start or date_end:
|
||||
date_range = {}
|
||||
if date_start:
|
||||
date_range["start"] = date_start
|
||||
if date_end:
|
||||
date_range["end"] = date_end
|
||||
|
||||
report = await generate_student_performance_report(
|
||||
tryout_id=tryout_id,
|
||||
website_id=website_id,
|
||||
db=db,
|
||||
date_range=date_range,
|
||||
format_type=format_type,
|
||||
)
|
||||
|
||||
return _convert_student_performance_report(report)
|
||||
|
||||
|
||||
def _convert_student_performance_report(report: StudentPerformanceReport) -> StudentPerformanceReportOutput:
|
||||
"""Convert dataclass report to Pydantic output."""
|
||||
date_range_str = None
|
||||
if report.date_range:
|
||||
date_range_str = {}
|
||||
if report.date_range.get("start"):
|
||||
date_range_str["start"] = report.date_range["start"].isoformat()
|
||||
if report.date_range.get("end"):
|
||||
date_range_str["end"] = report.date_range["end"].isoformat()
|
||||
|
||||
return StudentPerformanceReportOutput(
|
||||
generated_at=report.generated_at,
|
||||
tryout_id=report.tryout_id,
|
||||
website_id=report.website_id,
|
||||
date_range=date_range_str,
|
||||
aggregate=AggregatePerformanceStatsOutput(
|
||||
tryout_id=report.aggregate.tryout_id,
|
||||
participant_count=report.aggregate.participant_count,
|
||||
avg_nm=report.aggregate.avg_nm,
|
||||
std_nm=report.aggregate.std_nm,
|
||||
min_nm=report.aggregate.min_nm,
|
||||
max_nm=report.aggregate.max_nm,
|
||||
median_nm=report.aggregate.median_nm,
|
||||
avg_nn=report.aggregate.avg_nn,
|
||||
std_nn=report.aggregate.std_nn,
|
||||
avg_theta=report.aggregate.avg_theta,
|
||||
pass_rate=report.aggregate.pass_rate,
|
||||
avg_time_spent=report.aggregate.avg_time_spent,
|
||||
),
|
||||
individual_records=[
|
||||
StudentPerformanceRecordOutput(
|
||||
session_id=r.session_id,
|
||||
wp_user_id=r.wp_user_id,
|
||||
tryout_id=r.tryout_id,
|
||||
NM=r.NM,
|
||||
NN=r.NN,
|
||||
theta=r.theta,
|
||||
theta_se=r.theta_se,
|
||||
total_benar=r.total_benar,
|
||||
time_spent=r.time_spent,
|
||||
start_time=r.start_time,
|
||||
end_time=r.end_time,
|
||||
scoring_mode_used=r.scoring_mode_used,
|
||||
rataan_used=r.rataan_used,
|
||||
sb_used=r.sb_used,
|
||||
)
|
||||
for r in report.individual_records
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Item Analysis Report Endpoints
|
||||
# =============================================================================
|
||||
|
||||
@router.get(
|
||||
"/items/analysis",
|
||||
response_model=ItemAnalysisReportOutput,
|
||||
summary="Get item analysis report",
|
||||
description="Generate item analysis report with difficulty, discrimination, and information functions.",
|
||||
)
|
||||
async def get_item_analysis_report(
|
||||
tryout_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
filter_by: Optional[Literal["difficulty", "calibrated", "discrimination"]] = None,
|
||||
difficulty_level: Optional[Literal["mudah", "sedang", "sulit"]] = None,
|
||||
) -> ItemAnalysisReportOutput:
|
||||
"""
|
||||
Get item analysis report.
|
||||
|
||||
Returns item difficulty, discrimination, and information function data.
|
||||
"""
|
||||
report = await generate_item_analysis_report(
|
||||
tryout_id=tryout_id,
|
||||
website_id=website_id,
|
||||
db=db,
|
||||
filter_by=filter_by,
|
||||
difficulty_level=difficulty_level,
|
||||
)
|
||||
|
||||
return ItemAnalysisReportOutput(
|
||||
generated_at=report.generated_at,
|
||||
tryout_id=report.tryout_id,
|
||||
website_id=report.website_id,
|
||||
total_items=report.total_items,
|
||||
items=[
|
||||
ItemAnalysisRecordOutput(
|
||||
item_id=r.item_id,
|
||||
slot=r.slot,
|
||||
level=r.level,
|
||||
ctt_p=r.ctt_p,
|
||||
ctt_bobot=r.ctt_bobot,
|
||||
ctt_category=r.ctt_category,
|
||||
irt_b=r.irt_b,
|
||||
irt_se=r.irt_se,
|
||||
calibrated=r.calibrated,
|
||||
calibration_sample_size=r.calibration_sample_size,
|
||||
correctness_rate=r.correctness_rate,
|
||||
item_total_correlation=r.item_total_correlation,
|
||||
information_values=r.information_values,
|
||||
optimal_theta_range=r.optimal_theta_range,
|
||||
)
|
||||
for r in report.items
|
||||
],
|
||||
summary=report.summary,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Calibration Status Report Endpoints
|
||||
# =============================================================================
|
||||
|
||||
@router.get(
|
||||
"/calibration/status",
|
||||
response_model=CalibrationStatusReportOutput,
|
||||
summary="Get calibration status report",
|
||||
description="Generate calibration status report with progress tracking and readiness metrics.",
|
||||
)
|
||||
async def get_calibration_status_report(
|
||||
tryout_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
) -> CalibrationStatusReportOutput:
|
||||
"""
|
||||
Get calibration status report.
|
||||
|
||||
Returns calibration progress, items awaiting calibration, and IRT readiness status.
|
||||
"""
|
||||
report = await generate_calibration_status_report(
|
||||
tryout_id=tryout_id,
|
||||
website_id=website_id,
|
||||
db=db,
|
||||
)
|
||||
|
||||
return CalibrationStatusReportOutput(
|
||||
generated_at=report.generated_at,
|
||||
tryout_id=report.tryout_id,
|
||||
website_id=report.website_id,
|
||||
total_items=report.total_items,
|
||||
calibrated_items=report.calibrated_items,
|
||||
calibration_percentage=report.calibration_percentage,
|
||||
items_awaiting_calibration=[
|
||||
CalibrationItemStatusOutput(
|
||||
item_id=r.item_id,
|
||||
slot=r.slot,
|
||||
level=r.level,
|
||||
sample_size=r.sample_size,
|
||||
calibrated=r.calibrated,
|
||||
irt_b=r.irt_b,
|
||||
irt_se=r.irt_se,
|
||||
ctt_p=r.ctt_p,
|
||||
)
|
||||
for r in report.items_awaiting_calibration
|
||||
],
|
||||
avg_calibration_sample_size=report.avg_calibration_sample_size,
|
||||
estimated_time_to_90_percent=report.estimated_time_to_90_percent,
|
||||
ready_for_irt_rollout=report.ready_for_irt_rollout,
|
||||
items=[
|
||||
CalibrationItemStatusOutput(
|
||||
item_id=r.item_id,
|
||||
slot=r.slot,
|
||||
level=r.level,
|
||||
sample_size=r.sample_size,
|
||||
calibrated=r.calibrated,
|
||||
irt_b=r.irt_b,
|
||||
irt_se=r.irt_se,
|
||||
ctt_p=r.ctt_p,
|
||||
)
|
||||
for r in report.items
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tryout Comparison Report Endpoints
|
||||
# =============================================================================
|
||||
|
||||
@router.get(
|
||||
"/tryout/comparison",
|
||||
response_model=TryoutComparisonReportOutput,
|
||||
summary="Get tryout comparison report",
|
||||
description="Generate tryout comparison report across dates or subjects.",
|
||||
)
|
||||
async def get_tryout_comparison_report(
|
||||
tryout_ids: str, # Comma-separated list
|
||||
db: AsyncSession = Depends(get_db),
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
group_by: Literal["date", "subject"] = "date",
|
||||
) -> TryoutComparisonReportOutput:
|
||||
"""
|
||||
Get tryout comparison report.
|
||||
|
||||
Compares tryouts across dates or subjects.
|
||||
"""
|
||||
tryout_id_list = [tid.strip() for tid in tryout_ids.split(",")]
|
||||
|
||||
if len(tryout_id_list) < 2:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="At least 2 tryout IDs are required for comparison",
|
||||
)
|
||||
|
||||
report = await generate_tryout_comparison_report(
|
||||
tryout_ids=tryout_id_list,
|
||||
website_id=website_id,
|
||||
db=db,
|
||||
group_by=group_by,
|
||||
)
|
||||
|
||||
return TryoutComparisonReportOutput(
|
||||
generated_at=report.generated_at,
|
||||
comparison_type=report.comparison_type,
|
||||
tryouts=[
|
||||
TryoutComparisonRecordOutput(
|
||||
tryout_id=r.tryout_id,
|
||||
date=r.date,
|
||||
subject=r.subject,
|
||||
participant_count=r.participant_count,
|
||||
avg_nm=r.avg_nm,
|
||||
avg_nn=r.avg_nn,
|
||||
avg_theta=r.avg_theta,
|
||||
std_nm=r.std_nm,
|
||||
calibration_percentage=r.calibration_percentage,
|
||||
)
|
||||
for r in report.tryouts
|
||||
],
|
||||
trends=report.trends,
|
||||
normalization_impact=report.normalization_impact,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Report Scheduling Endpoints
|
||||
# =============================================================================
|
||||
|
||||
@router.post(
|
||||
"/schedule",
|
||||
response_model=ReportScheduleResponse,
|
||||
summary="Schedule a report",
|
||||
description="Schedule a report for automatic generation on a daily, weekly, or monthly basis.",
|
||||
)
|
||||
async def create_report_schedule(
|
||||
request: ReportScheduleRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ReportScheduleResponse:
|
||||
"""
|
||||
Schedule a report.
|
||||
|
||||
Creates a scheduled report that will be generated automatically.
|
||||
"""
|
||||
schedule_id = schedule_report(
|
||||
report_type=request.report_type,
|
||||
schedule=request.schedule,
|
||||
tryout_ids=request.tryout_ids,
|
||||
website_id=request.website_id,
|
||||
recipients=request.recipients,
|
||||
export_format=request.export_format,
|
||||
)
|
||||
|
||||
scheduled = get_scheduled_report(schedule_id)
|
||||
|
||||
return ReportScheduleResponse(
|
||||
schedule_id=schedule_id,
|
||||
message=f"Report scheduled successfully for {request.schedule} generation",
|
||||
next_run=scheduled.next_run if scheduled else None,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/schedule/{schedule_id}",
|
||||
response_model=ReportScheduleOutput,
|
||||
summary="Get scheduled report details",
|
||||
description="Get details of a scheduled report.",
|
||||
)
|
||||
async def get_scheduled_report_details(
|
||||
schedule_id: str,
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
) -> ReportScheduleOutput:
|
||||
"""
|
||||
Get scheduled report details.
|
||||
|
||||
Returns the configuration and status of a scheduled report.
|
||||
"""
|
||||
scheduled = get_scheduled_report(schedule_id)
|
||||
|
||||
if not scheduled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Scheduled report {schedule_id} not found",
|
||||
)
|
||||
|
||||
if scheduled.website_id != website_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this scheduled report",
|
||||
)
|
||||
|
||||
return ReportScheduleOutput(
|
||||
schedule_id=scheduled.schedule_id,
|
||||
report_type=scheduled.report_type,
|
||||
schedule=scheduled.schedule,
|
||||
tryout_ids=scheduled.tryout_ids,
|
||||
website_id=scheduled.website_id,
|
||||
recipients=scheduled.recipients,
|
||||
format=scheduled.format,
|
||||
created_at=scheduled.created_at,
|
||||
last_run=scheduled.last_run,
|
||||
next_run=scheduled.next_run,
|
||||
is_active=scheduled.is_active,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/schedule",
|
||||
response_model=List[ReportScheduleOutput],
|
||||
summary="List scheduled reports",
|
||||
description="List all scheduled reports for a website.",
|
||||
)
|
||||
async def list_scheduled_reports_endpoint(
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
) -> List[ReportScheduleOutput]:
|
||||
"""
|
||||
List all scheduled reports.
|
||||
|
||||
Returns all scheduled reports for the current website.
|
||||
"""
|
||||
reports = list_scheduled_reports(website_id=website_id)
|
||||
|
||||
return [
|
||||
ReportScheduleOutput(
|
||||
schedule_id=r.schedule_id,
|
||||
report_type=r.report_type,
|
||||
schedule=r.schedule,
|
||||
tryout_ids=r.tryout_ids,
|
||||
website_id=r.website_id,
|
||||
recipients=r.recipients,
|
||||
format=r.format,
|
||||
created_at=r.created_at,
|
||||
last_run=r.last_run,
|
||||
next_run=r.next_run,
|
||||
is_active=r.is_active,
|
||||
)
|
||||
for r in reports
|
||||
]
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/schedule/{schedule_id}",
|
||||
summary="Cancel scheduled report",
|
||||
description="Cancel a scheduled report.",
|
||||
)
|
||||
async def cancel_scheduled_report_endpoint(
|
||||
schedule_id: str,
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
) -> dict:
|
||||
"""
|
||||
Cancel a scheduled report.
|
||||
|
||||
Removes the scheduled report from the system.
|
||||
"""
|
||||
scheduled = get_scheduled_report(schedule_id)
|
||||
|
||||
if not scheduled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Scheduled report {schedule_id} not found",
|
||||
)
|
||||
|
||||
if scheduled.website_id != website_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this scheduled report",
|
||||
)
|
||||
|
||||
success = cancel_scheduled_report(schedule_id)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to cancel scheduled report",
|
||||
)
|
||||
|
||||
return {
|
||||
"message": f"Scheduled report {schedule_id} cancelled successfully",
|
||||
"schedule_id": schedule_id,
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Report Export Endpoints
|
||||
# =============================================================================
|
||||
|
||||
@router.get(
|
||||
"/export/{schedule_id}/{format}",
|
||||
summary="Export scheduled report",
|
||||
description="Generate and export a scheduled report in the specified format.",
|
||||
)
|
||||
async def export_scheduled_report(
|
||||
schedule_id: str,
|
||||
format: Literal["csv", "xlsx", "pdf"],
|
||||
db: AsyncSession = Depends(get_db),
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
):
|
||||
"""
|
||||
Export a scheduled report.
|
||||
|
||||
Generates the report and returns it as a file download.
|
||||
"""
|
||||
scheduled = get_scheduled_report(schedule_id)
|
||||
|
||||
if not scheduled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Scheduled report {schedule_id} not found",
|
||||
)
|
||||
|
||||
if scheduled.website_id != website_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this scheduled report",
|
||||
)
|
||||
|
||||
# Generate report based on type
|
||||
report = None
|
||||
base_filename = f"report_{scheduled.report_type}_{schedule_id}"
|
||||
|
||||
try:
|
||||
if scheduled.report_type == "student_performance":
|
||||
if len(scheduled.tryout_ids) > 0:
|
||||
report = await generate_student_performance_report(
|
||||
tryout_id=scheduled.tryout_ids[0],
|
||||
website_id=website_id,
|
||||
db=db,
|
||||
)
|
||||
elif scheduled.report_type == "item_analysis":
|
||||
if len(scheduled.tryout_ids) > 0:
|
||||
report = await generate_item_analysis_report(
|
||||
tryout_id=scheduled.tryout_ids[0],
|
||||
website_id=website_id,
|
||||
db=db,
|
||||
)
|
||||
elif scheduled.report_type == "calibration_status":
|
||||
if len(scheduled.tryout_ids) > 0:
|
||||
report = await generate_calibration_status_report(
|
||||
tryout_id=scheduled.tryout_ids[0],
|
||||
website_id=website_id,
|
||||
db=db,
|
||||
)
|
||||
elif scheduled.report_type == "tryout_comparison":
|
||||
report = await generate_tryout_comparison_report(
|
||||
tryout_ids=scheduled.tryout_ids,
|
||||
website_id=website_id,
|
||||
db=db,
|
||||
)
|
||||
|
||||
if not report:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to generate report",
|
||||
)
|
||||
|
||||
# Export to requested format
|
||||
if format == "csv":
|
||||
file_path = export_report_to_csv(report, base_filename)
|
||||
media_type = "text/csv"
|
||||
elif format == "xlsx":
|
||||
file_path = export_report_to_excel(report, base_filename)
|
||||
media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
else: # pdf
|
||||
file_path = export_report_to_pdf(report, base_filename)
|
||||
media_type = "application/pdf"
|
||||
|
||||
# Return file
|
||||
return FileResponse(
|
||||
path=file_path,
|
||||
media_type=media_type,
|
||||
filename=os.path.basename(file_path),
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to export report: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Direct Export Endpoints (without scheduling)
|
||||
# =============================================================================
|
||||
|
||||
@router.get(
|
||||
"/student/performance/export/{format}",
|
||||
summary="Export student performance report directly",
|
||||
description="Generate and export student performance report directly without scheduling.",
|
||||
)
|
||||
async def export_student_performance_direct(
|
||||
format: Literal["csv", "xlsx", "pdf"],
|
||||
tryout_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
date_start: Optional[datetime] = None,
|
||||
date_end: Optional[datetime] = None,
|
||||
):
|
||||
"""Export student performance report directly."""
|
||||
date_range = None
|
||||
if date_start or date_end:
|
||||
date_range = {}
|
||||
if date_start:
|
||||
date_range["start"] = date_start
|
||||
if date_end:
|
||||
date_range["end"] = date_end
|
||||
|
||||
report = await generate_student_performance_report(
|
||||
tryout_id=tryout_id,
|
||||
website_id=website_id,
|
||||
db=db,
|
||||
date_range=date_range,
|
||||
)
|
||||
|
||||
base_filename = f"student_performance_{tryout_id}"
|
||||
|
||||
if format == "csv":
|
||||
file_path = export_report_to_csv(report, base_filename)
|
||||
media_type = "text/csv"
|
||||
elif format == "xlsx":
|
||||
file_path = export_report_to_excel(report, base_filename)
|
||||
media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
else:
|
||||
file_path = export_report_to_pdf(report, base_filename)
|
||||
media_type = "application/pdf"
|
||||
|
||||
return FileResponse(
|
||||
path=file_path,
|
||||
media_type=media_type,
|
||||
filename=os.path.basename(file_path),
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/items/analysis/export/{format}",
|
||||
summary="Export item analysis report directly",
|
||||
description="Generate and export item analysis report directly without scheduling.",
|
||||
)
|
||||
async def export_item_analysis_direct(
|
||||
format: Literal["csv", "xlsx", "pdf"],
|
||||
tryout_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
filter_by: Optional[Literal["difficulty", "calibrated", "discrimination"]] = None,
|
||||
difficulty_level: Optional[Literal["mudah", "sedang", "sulit"]] = None,
|
||||
):
|
||||
"""Export item analysis report directly."""
|
||||
report = await generate_item_analysis_report(
|
||||
tryout_id=tryout_id,
|
||||
website_id=website_id,
|
||||
db=db,
|
||||
filter_by=filter_by,
|
||||
difficulty_level=difficulty_level,
|
||||
)
|
||||
|
||||
base_filename = f"item_analysis_{tryout_id}"
|
||||
|
||||
if format == "csv":
|
||||
file_path = export_report_to_csv(report, base_filename)
|
||||
media_type = "text/csv"
|
||||
elif format == "xlsx":
|
||||
file_path = export_report_to_excel(report, base_filename)
|
||||
media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
else:
|
||||
file_path = export_report_to_pdf(report, base_filename)
|
||||
media_type = "application/pdf"
|
||||
|
||||
return FileResponse(
|
||||
path=file_path,
|
||||
media_type=media_type,
|
||||
filename=os.path.basename(file_path),
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/calibration/status/export/{format}",
|
||||
summary="Export calibration status report directly",
|
||||
description="Generate and export calibration status report directly without scheduling.",
|
||||
)
|
||||
async def export_calibration_status_direct(
|
||||
format: Literal["csv", "xlsx", "pdf"],
|
||||
tryout_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
):
|
||||
"""Export calibration status report directly."""
|
||||
report = await generate_calibration_status_report(
|
||||
tryout_id=tryout_id,
|
||||
website_id=website_id,
|
||||
db=db,
|
||||
)
|
||||
|
||||
base_filename = f"calibration_status_{tryout_id}"
|
||||
|
||||
if format == "csv":
|
||||
file_path = export_report_to_csv(report, base_filename)
|
||||
media_type = "text/csv"
|
||||
elif format == "xlsx":
|
||||
file_path = export_report_to_excel(report, base_filename)
|
||||
media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
else:
|
||||
file_path = export_report_to_pdf(report, base_filename)
|
||||
media_type = "application/pdf"
|
||||
|
||||
return FileResponse(
|
||||
path=file_path,
|
||||
media_type=media_type,
|
||||
filename=os.path.basename(file_path),
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/tryout/comparison/export/{format}",
|
||||
summary="Export tryout comparison report directly",
|
||||
description="Generate and export tryout comparison report directly without scheduling.",
|
||||
)
|
||||
async def export_tryout_comparison_direct(
|
||||
format: Literal["csv", "xlsx", "pdf"],
|
||||
tryout_ids: str, # Comma-separated
|
||||
db: AsyncSession = Depends(get_db),
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
group_by: Literal["date", "subject"] = "date",
|
||||
):
|
||||
"""Export tryout comparison report directly."""
|
||||
tryout_id_list = [tid.strip() for tid in tryout_ids.split(",")]
|
||||
|
||||
if len(tryout_id_list) < 2:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="At least 2 tryout IDs are required for comparison",
|
||||
)
|
||||
|
||||
report = await generate_tryout_comparison_report(
|
||||
tryout_ids=tryout_id_list,
|
||||
website_id=website_id,
|
||||
db=db,
|
||||
group_by=group_by,
|
||||
)
|
||||
|
||||
base_filename = "tryout_comparison"
|
||||
|
||||
if format == "csv":
|
||||
file_path = export_report_to_csv(report, base_filename)
|
||||
media_type = "text/csv"
|
||||
elif format == "xlsx":
|
||||
file_path = export_report_to_excel(report, base_filename)
|
||||
media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
else:
|
||||
file_path = export_report_to_pdf(report, base_filename)
|
||||
media_type = "application/pdf"
|
||||
|
||||
return FileResponse(
|
||||
path=file_path,
|
||||
media_type=media_type,
|
||||
filename=os.path.basename(file_path),
|
||||
)
|
||||
Reference in New Issue
Block a user