Add bulk import for evolutions, routes, and bosses

Add three new bulk import endpoints that accept the same JSON format as
their corresponding export endpoints, enabling round-trip compatibility:

- POST /evolutions/bulk-import (upsert by from/to pokemon pair)
- POST /games/{id}/routes/bulk-import (reuses seed loader for hierarchy)
- POST /games/{id}/bosses/bulk-import (reuses seed loader with team data)

Generalize BulkImportModal to support all entity types with configurable
title, example, and result labels. Wire up Bulk Import buttons on
AdminEvolutions, and AdminGameDetail routes/bosses tabs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 20:14:19 +01:00
parent 8e1c8b554f
commit 8f6d72a9c4
12 changed files with 373 additions and 15 deletions

View File

@@ -6,6 +6,7 @@ from sqlalchemy.orm import selectinload
from app.core.database import get_session
from app.models.boss_battle import BossBattle
from app.models.game import Game
from app.models.pokemon import Pokemon
from app.models.route import Route
from app.models.route_encounter import RouteEncounter
from app.schemas.game import (
@@ -19,6 +20,8 @@ from app.schemas.game import (
RouteUpdate,
RouteWithChildrenResponse,
)
from app.schemas.pokemon import BulkImportResult, BulkRouteItem
from app.seeds.loader import upsert_route_encounters, upsert_routes
router = APIRouter()
@@ -332,3 +335,68 @@ async def delete_route(
await session.delete(route)
await session.commit()
@router.post("/{game_id}/routes/bulk-import", response_model=BulkImportResult)
async def bulk_import_routes(
game_id: int,
items: list[BulkRouteItem],
session: AsyncSession = Depends(get_session),
):
vg_id = await _get_version_group_id(session, game_id)
# Build pokeapi_id -> id mapping for encounter resolution
result = await session.execute(select(Pokemon.pokeapi_id, Pokemon.id))
dex_to_id = {row.pokeapi_id: row.id for row in result}
errors: list[str] = []
# Upsert routes using the seed loader (handles parent/child hierarchy)
routes_data = [item.model_dump() for item in items]
try:
route_name_to_id = await upsert_routes(session, vg_id, routes_data)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Failed to import routes: {e}")
# Upsert encounters for each route
encounter_count = 0
for item in items:
route_id = route_name_to_id.get(item.name)
if route_id is None:
errors.append(f"Route '{item.name}' not found after upsert")
continue
if item.encounters:
try:
count = await upsert_route_encounters(
session, route_id, [e.model_dump() for e in item.encounters],
dex_to_id, game_id,
)
encounter_count += count
except Exception as e:
errors.append(f"Encounters for '{item.name}': {e}")
for child in item.children:
child_id = route_name_to_id.get(child.name)
if child_id is None:
errors.append(f"Child route '{child.name}' not found after upsert")
continue
if child.encounters:
try:
count = await upsert_route_encounters(
session, child_id, [e.model_dump() for e in child.encounters],
dex_to_id, game_id,
)
encounter_count += count
except Exception as e:
errors.append(f"Encounters for '{child.name}': {e}")
await session.commit()
route_count = len(route_name_to_id)
return BulkImportResult(
created=route_count,
updated=encounter_count,
errors=errors,
)