Add Sejoli tryout JSON snapshot importer

This commit is contained in:
dwindown
2026-04-02 17:04:01 +07:00
parent 51c577be05
commit b4ebdc9c4f
7 changed files with 910 additions and 1 deletions

View File

@@ -1,14 +1,17 @@
"""
Import/Export API router for Excel question migration.
Import/Export API router for migration and snapshot ingestion.
Endpoints:
- POST /api/v1/import/preview: Preview Excel import without saving
- POST /api/v1/import/questions: Import questions from Excel to database
- GET /api/v1/export/questions: Export questions to Excel file
- POST /api/v1/import-export/tryout-json/preview: Preview Sejoli tryout JSON import
- POST /api/v1/import-export/tryout-json: Import Sejoli tryout JSON as read-only snapshot
"""
import os
import tempfile
import json
from typing import Optional
from fastapi import APIRouter, Depends, File, Form, Header, HTTPException, UploadFile, status
@@ -16,12 +19,18 @@ from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models import Website
from app.services.excel_import import (
bulk_insert_items,
export_questions_to_excel,
parse_excel_import,
validate_excel_structure,
)
from app.services.tryout_json_import import (
TryoutImportError,
import_tryout_json_snapshot,
preview_tryout_json_import,
)
router = APIRouter(prefix="/api/v1/import-export", tags=["import-export"])
@@ -55,6 +64,21 @@ def get_website_id_from_header(
)
async def ensure_website_exists(
website_id: int,
db: AsyncSession,
) -> None:
website = await db.get(Website, website_id)
if website is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=(
f"Website {website_id} not found. Website registration is stored in the database, "
"not in .env."
),
)
@router.post(
"/preview",
summary="Preview Excel import",
@@ -322,3 +346,73 @@ async def export_questions(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Export failed: {str(e)}",
)
@router.post(
"/tryout-json/preview",
summary="Preview Sejoli tryout JSON import",
description="Parse a Sejoli tryout export JSON file and show snapshot diff without writing to database.",
)
async def preview_tryout_json(
file: UploadFile = File(..., description="Sejoli tryout export JSON"),
website_id: int = Depends(get_website_id_from_header),
db: AsyncSession = Depends(get_db),
) -> dict:
if not file.filename or not file.filename.lower().endswith(".json"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="File must be .json format",
)
await ensure_website_exists(website_id, db)
try:
payload = json.loads((await file.read()).decode("utf-8"))
except json.JSONDecodeError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid JSON file: {str(e)}",
)
try:
return await preview_tryout_json_import(payload, website_id, db)
except TryoutImportError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
@router.post(
"/tryout-json",
summary="Import Sejoli tryout JSON snapshot",
description="Store Sejoli tryout export JSON as read-only snapshot data and upsert normalized reference questions.",
)
async def import_tryout_json(
file: UploadFile = File(..., description="Sejoli tryout export JSON"),
website_id: int = Depends(get_website_id_from_header),
db: AsyncSession = Depends(get_db),
) -> dict:
if not file.filename or not file.filename.lower().endswith(".json"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="File must be .json format",
)
await ensure_website_exists(website_id, db)
try:
payload = json.loads((await file.read()).decode("utf-8"))
except json.JSONDecodeError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid JSON file: {str(e)}",
)
try:
return await import_tryout_json_snapshot(payload, website_id, db)
except TryoutImportError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)