Add Sejoli tryout JSON snapshot importer
This commit is contained in:
@@ -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),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user