793 lines
26 KiB
Python
793 lines
26 KiB
Python
"""
|
|
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),
|
|
)
|