299 lines
9.6 KiB
Python
299 lines
9.6 KiB
Python
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from ....auth import require_admin
|
|
from ....domain.services.pack_service import PackService, PackNotFoundError
|
|
from ....outbound.postgres.database import get_db
|
|
from ....outbound.postgres.repositories.pack_repository import PostgresPackRepository
|
|
from ....outbound.postgres.repositories.vocab_repository import PostgresVocabRepository
|
|
from ....outbound.postgres.repositories.flashcard_repository import PostgresFlashcardRepository
|
|
from ....outbound.postgres.repositories.dictionary_repository import PostgresDictionaryRepository
|
|
|
|
router = APIRouter(prefix="/admin/packs", tags=["admin-packs"])
|
|
|
|
|
|
# ── Request / Response models ─────────────────────────────────────────────────
|
|
|
|
class CreatePackRequest(BaseModel):
|
|
name: str
|
|
name_target: str
|
|
description: str
|
|
description_target: str
|
|
source_lang: str
|
|
target_lang: str
|
|
proficiencies: list[str] = []
|
|
|
|
|
|
class UpdatePackRequest(BaseModel):
|
|
name: str | None = None
|
|
name_target: str | None = None
|
|
description: str | None = None
|
|
description_target: str | None = None
|
|
proficiencies: list[str] | None = None
|
|
|
|
|
|
class AddEntryRequest(BaseModel):
|
|
sense_id: str | None = None
|
|
surface_text: str
|
|
|
|
|
|
class AddFlashcardTemplateRequest(BaseModel):
|
|
prompt_text: str
|
|
answer_text: str
|
|
prompt_context_text: str | None = None
|
|
answer_context_text: str | None = None
|
|
|
|
|
|
class FlashcardTemplateResponse(BaseModel):
|
|
id: str
|
|
pack_entry_id: str
|
|
prompt_text: str
|
|
answer_text: str
|
|
prompt_context_text: str | None
|
|
answer_context_text: str | None
|
|
created_at: str
|
|
|
|
|
|
class PackEntryResponse(BaseModel):
|
|
id: str
|
|
pack_id: str
|
|
sense_id: str | None
|
|
surface_text: str
|
|
created_at: str
|
|
flashcard_templates: list[FlashcardTemplateResponse] = []
|
|
|
|
|
|
class PackResponse(BaseModel):
|
|
id: str
|
|
name: str
|
|
name_target: str
|
|
description: str
|
|
description_target: str
|
|
source_lang: str
|
|
target_lang: str
|
|
proficiencies: list[str]
|
|
is_published: bool
|
|
created_at: str
|
|
|
|
|
|
class PackDetailResponse(PackResponse):
|
|
entries: list[PackEntryResponse] = []
|
|
|
|
|
|
# ── Dependency ────────────────────────────────────────────────────────────────
|
|
|
|
def _service(db: AsyncSession) -> PackService:
|
|
return PackService(
|
|
pack_repo=PostgresPackRepository(db),
|
|
vocab_repo=PostgresVocabRepository(db),
|
|
flashcard_repo=PostgresFlashcardRepository(db),
|
|
dict_repo=PostgresDictionaryRepository(db),
|
|
)
|
|
|
|
|
|
def _pack_repo(db: AsyncSession) -> PostgresPackRepository:
|
|
return PostgresPackRepository(db)
|
|
|
|
|
|
# ── Endpoints ─────────────────────────────────────────────────────────────────
|
|
|
|
@router.post("", response_model=PackResponse, status_code=201)
|
|
async def create_pack(
|
|
request: CreatePackRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: dict = Depends(require_admin),
|
|
) -> PackResponse:
|
|
pack = await _service(db).create_pack(
|
|
name=request.name,
|
|
name_target=request.name_target,
|
|
description=request.description,
|
|
description_target=request.description_target,
|
|
source_lang=request.source_lang,
|
|
target_lang=request.target_lang,
|
|
proficiencies=request.proficiencies,
|
|
)
|
|
return _to_pack_response(pack)
|
|
|
|
|
|
@router.get("", response_model=list[PackResponse])
|
|
async def list_packs(
|
|
source_lang: str | None = None,
|
|
target_lang: str | None = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: dict = Depends(require_admin),
|
|
) -> list[PackResponse]:
|
|
packs = await _pack_repo(db).list_packs(source_lang=source_lang, target_lang=target_lang)
|
|
return [_to_pack_response(p) for p in packs]
|
|
|
|
|
|
@router.get("/{pack_id}", response_model=PackDetailResponse)
|
|
async def get_pack(
|
|
pack_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: dict = Depends(require_admin),
|
|
) -> PackDetailResponse:
|
|
repo = _pack_repo(db)
|
|
pack = await repo.get_pack(_parse_uuid(pack_id))
|
|
if pack is None:
|
|
raise HTTPException(status_code=404, detail="Pack not found")
|
|
|
|
entries = await repo.get_entries_for_pack(uuid.UUID(pack.id))
|
|
entry_ids = [uuid.UUID(e.id) for e in entries]
|
|
templates_by_entry = await repo.get_templates_for_entries(entry_ids)
|
|
|
|
entry_responses = [
|
|
PackEntryResponse(
|
|
id=e.id,
|
|
pack_id=e.pack_id,
|
|
sense_id=e.sense_id,
|
|
surface_text=e.surface_text,
|
|
created_at=e.created_at.isoformat(),
|
|
flashcard_templates=[
|
|
_to_template_response(t) for t in templates_by_entry.get(e.id, [])
|
|
],
|
|
)
|
|
for e in entries
|
|
]
|
|
return PackDetailResponse(**_to_pack_response(pack).model_dump(), entries=entry_responses)
|
|
|
|
|
|
@router.patch("/{pack_id}", response_model=PackResponse)
|
|
async def update_pack(
|
|
pack_id: str,
|
|
request: UpdatePackRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: dict = Depends(require_admin),
|
|
) -> PackResponse:
|
|
try:
|
|
pack = await _service(db).update_pack(
|
|
pack_id=_parse_uuid(pack_id),
|
|
name=request.name,
|
|
name_target=request.name_target,
|
|
description=request.description,
|
|
description_target=request.description_target,
|
|
proficiencies=request.proficiencies,
|
|
)
|
|
except PackNotFoundError:
|
|
raise HTTPException(status_code=404, detail="Pack not found")
|
|
return _to_pack_response(pack)
|
|
|
|
|
|
@router.post("/{pack_id}/publish", response_model=PackResponse)
|
|
async def publish_pack(
|
|
pack_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: dict = Depends(require_admin),
|
|
) -> PackResponse:
|
|
try:
|
|
pack = await _service(db).publish_pack(_parse_uuid(pack_id))
|
|
except PackNotFoundError:
|
|
raise HTTPException(status_code=404, detail="Pack not found")
|
|
return _to_pack_response(pack)
|
|
|
|
|
|
@router.post("/{pack_id}/entries", response_model=PackEntryResponse, status_code=201)
|
|
async def add_entry(
|
|
pack_id: str,
|
|
request: AddEntryRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: dict = Depends(require_admin),
|
|
) -> PackEntryResponse:
|
|
sense_id = _parse_uuid(request.sense_id) if request.sense_id else None
|
|
try:
|
|
entry = await _service(db).add_entry_to_pack(
|
|
pack_id=_parse_uuid(pack_id),
|
|
sense_id=sense_id,
|
|
surface_text=request.surface_text,
|
|
)
|
|
except PackNotFoundError:
|
|
raise HTTPException(status_code=404, detail="Pack not found")
|
|
return PackEntryResponse(
|
|
id=entry.id,
|
|
pack_id=entry.pack_id,
|
|
sense_id=entry.sense_id,
|
|
surface_text=entry.surface_text,
|
|
created_at=entry.created_at.isoformat(),
|
|
flashcard_templates=[],
|
|
)
|
|
|
|
|
|
@router.delete("/{pack_id}/entries/{entry_id}", status_code=204)
|
|
async def remove_entry(
|
|
pack_id: str,
|
|
entry_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: dict = Depends(require_admin),
|
|
) -> None:
|
|
await _pack_repo(db).remove_entry(_parse_uuid(entry_id))
|
|
|
|
|
|
@router.post(
|
|
"/{pack_id}/entries/{entry_id}/flashcards",
|
|
response_model=FlashcardTemplateResponse,
|
|
status_code=201,
|
|
)
|
|
async def add_flashcard_template(
|
|
pack_id: str,
|
|
entry_id: str,
|
|
request: AddFlashcardTemplateRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: dict = Depends(require_admin),
|
|
) -> FlashcardTemplateResponse:
|
|
template = await _service(db).add_flashcard_template_to_entry(
|
|
pack_entry_id=_parse_uuid(entry_id),
|
|
prompt_text=request.prompt_text,
|
|
answer_text=request.answer_text,
|
|
prompt_context_text=request.prompt_context_text,
|
|
answer_context_text=request.answer_context_text,
|
|
)
|
|
return _to_template_response(template)
|
|
|
|
|
|
@router.delete("/{pack_id}/entries/{entry_id}/flashcards/{template_id}", status_code=204)
|
|
async def remove_flashcard_template(
|
|
pack_id: str,
|
|
entry_id: str,
|
|
template_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: dict = Depends(require_admin),
|
|
) -> None:
|
|
await _pack_repo(db).remove_flashcard_template(_parse_uuid(template_id))
|
|
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
def _parse_uuid(value: str) -> uuid.UUID:
|
|
try:
|
|
return uuid.UUID(value)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail=f"Invalid UUID: {value!r}")
|
|
|
|
|
|
def _to_pack_response(pack) -> PackResponse:
|
|
return PackResponse(
|
|
id=pack.id,
|
|
name=pack.name,
|
|
name_target=pack.name_target,
|
|
description=pack.description,
|
|
description_target=pack.description_target,
|
|
source_lang=pack.source_lang,
|
|
target_lang=pack.target_lang,
|
|
proficiencies=pack.proficiencies,
|
|
is_published=pack.is_published,
|
|
created_at=pack.created_at.isoformat(),
|
|
)
|
|
|
|
|
|
def _to_template_response(template) -> FlashcardTemplateResponse:
|
|
return FlashcardTemplateResponse(
|
|
id=template.id,
|
|
pack_entry_id=template.pack_entry_id,
|
|
prompt_text=template.prompt_text,
|
|
answer_text=template.answer_text,
|
|
prompt_context_text=template.prompt_context_text,
|
|
answer_context_text=template.answer_context_text,
|
|
created_at=template.created_at.isoformat(),
|
|
)
|