Add nullable region field to evolutions for regional form filtering
Regional evolutions (e.g., Pikachu → Alolan Raichu) only occur in specific regions. This adds a nullable region column so the app can filter evolutions by the game's region. When a regional evolution exists for a given trigger/item, the non-regional counterpart is automatically hidden. Full-stack: migration, model, schemas, API with region query param, seeder, Go fetch tool, frontend types/API/hook/components, and admin form. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
"""add region to evolutions
|
||||
|
||||
Revision ID: f6a7b8c9d0e1
|
||||
Revises: e5f6a7b8c9d0
|
||||
Create Date: 2026-02-07 12:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'f6a7b8c9d0e1'
|
||||
down_revision: Union[str, Sequence[str], None] = 'e5f6a7b8c9d0'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
'evolutions',
|
||||
sa.Column('region', sa.String(30), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('evolutions', 'region')
|
||||
@@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import joinedload, selectinload
|
||||
|
||||
@@ -121,18 +121,40 @@ async def get_pokemon(
|
||||
|
||||
@router.get("/pokemon/{pokemon_id}/evolutions", response_model=list[EvolutionResponse])
|
||||
async def get_pokemon_evolutions(
|
||||
pokemon_id: int, session: AsyncSession = Depends(get_session)
|
||||
pokemon_id: int,
|
||||
region: str | None = Query(None),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
pokemon = await session.get(Pokemon, pokemon_id)
|
||||
if pokemon is None:
|
||||
raise HTTPException(status_code=404, detail="Pokemon not found")
|
||||
|
||||
result = await session.execute(
|
||||
query = (
|
||||
select(Evolution)
|
||||
.where(Evolution.from_pokemon_id == pokemon_id)
|
||||
.options(joinedload(Evolution.to_pokemon))
|
||||
)
|
||||
return result.scalars().unique().all()
|
||||
if region is not None:
|
||||
query = query.where(
|
||||
or_(Evolution.region.is_(None), Evolution.region == region)
|
||||
)
|
||||
result = await session.execute(query)
|
||||
evolutions = result.scalars().unique().all()
|
||||
|
||||
if region is not None:
|
||||
# Regional evolutions replace the non-regional one that shares the
|
||||
# same trigger + item (e.g. Pikachu + thunder-stone → Alolan Raichu
|
||||
# replaces Pikachu + thunder-stone → Raichu in Alola).
|
||||
regional_keys = {
|
||||
(e.trigger, e.item) for e in evolutions if e.region is not None
|
||||
}
|
||||
if regional_keys:
|
||||
evolutions = [
|
||||
e for e in evolutions
|
||||
if e.region is not None or (e.trigger, e.item) not in regional_keys
|
||||
]
|
||||
|
||||
return evolutions
|
||||
|
||||
|
||||
@router.put("/pokemon/{pokemon_id}", response_model=PokemonResponse)
|
||||
|
||||
@@ -15,6 +15,7 @@ class Evolution(Base):
|
||||
item: Mapped[str | None] = mapped_column(String(50)) # e.g. thunder-stone
|
||||
held_item: Mapped[str | None] = mapped_column(String(50))
|
||||
condition: Mapped[str | None] = mapped_column(String(200)) # catch-all for other conditions
|
||||
region: Mapped[str | None] = mapped_column(String(30))
|
||||
|
||||
from_pokemon: Mapped["Pokemon"] = relationship(foreign_keys=[from_pokemon_id])
|
||||
to_pokemon: Mapped["Pokemon"] = relationship(foreign_keys=[to_pokemon_id])
|
||||
|
||||
@@ -28,6 +28,7 @@ class EvolutionResponse(CamelModel):
|
||||
item: str | None
|
||||
held_item: str | None
|
||||
condition: str | None
|
||||
region: str | None
|
||||
|
||||
|
||||
class RouteEncounterResponse(CamelModel):
|
||||
@@ -106,6 +107,7 @@ class EvolutionAdminResponse(CamelModel):
|
||||
item: str | None
|
||||
held_item: str | None
|
||||
condition: str | None
|
||||
region: str | None
|
||||
|
||||
|
||||
class PaginatedEvolutionResponse(CamelModel):
|
||||
@@ -123,6 +125,7 @@ class EvolutionCreate(CamelModel):
|
||||
item: str | None = None
|
||||
held_item: str | None = None
|
||||
condition: str | None = None
|
||||
region: str | None = None
|
||||
|
||||
|
||||
class EvolutionUpdate(CamelModel):
|
||||
@@ -133,3 +136,4 @@ class EvolutionUpdate(CamelModel):
|
||||
item: str | None = None
|
||||
held_item: str | None = None
|
||||
condition: str | None = None
|
||||
region: str | None = None
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
{
|
||||
"remove": [],
|
||||
"add": [],
|
||||
"add": [
|
||||
{
|
||||
"from_dex": 25,
|
||||
"to_dex": 10100,
|
||||
"trigger": "use-item",
|
||||
"item": "thunder-stone",
|
||||
"region": "alola"
|
||||
},
|
||||
{
|
||||
"from_dex": 102,
|
||||
"to_dex": 10101,
|
||||
"trigger": "use-item",
|
||||
"item": "leaf-stone",
|
||||
"region": "alola"
|
||||
},
|
||||
{
|
||||
"from_dex": 77,
|
||||
"to_dex": 10162,
|
||||
"trigger": "level-up",
|
||||
"min_level": 40,
|
||||
"region": "galar"
|
||||
},
|
||||
{
|
||||
"from_dex": 263,
|
||||
"to_dex": 10176,
|
||||
"trigger": "level-up",
|
||||
"min_level": 20,
|
||||
"region": "galar"
|
||||
},
|
||||
{
|
||||
"from_dex": 10176,
|
||||
"to_dex": 10177,
|
||||
"trigger": "level-up",
|
||||
"min_level": 35,
|
||||
"condition": "nighttime",
|
||||
"region": "galar"
|
||||
}
|
||||
],
|
||||
"modify": []
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@ async def upsert_evolutions(
|
||||
item=evo.get("item"),
|
||||
held_item=evo.get("held_item"),
|
||||
condition=evo.get("condition"),
|
||||
region=evo.get("region"),
|
||||
)
|
||||
session.add(evolution)
|
||||
count += 1
|
||||
|
||||
Reference in New Issue
Block a user