2026-03-27 11:04:05 +00:00
|
|
|
from datetime import datetime
|
|
|
|
|
|
2026-03-26 20:47:15 +00:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
|
from pydantic import BaseModel
|
2026-03-27 11:04:05 +00:00
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
2026-03-26 20:47:15 +00:00
|
|
|
|
2026-06-02 20:02:50 +00:00
|
|
|
from app.domain.services.article_service import ArticleService
|
|
|
|
|
from app.outbound.postgres.repositories.article_repository import (
|
|
|
|
|
PostgresArticleOwnershipRepository,
|
|
|
|
|
PostgresArticleRepository,
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-27 11:04:05 +00:00
|
|
|
from ...auth import verify_token
|
|
|
|
|
from ...outbound.postgres.database import get_db
|
2026-05-18 20:18:19 +00:00
|
|
|
from ...outbound.storage_client import get_storage_client
|
2026-03-27 11:04:05 +00:00
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/articles", tags=["bff", "articles"])
|
|
|
|
|
|
|
|
|
|
|
2026-06-02 20:02:50 +00:00
|
|
|
def _make_article_service(db) -> ArticleService:
|
|
|
|
|
return ArticleService(
|
|
|
|
|
article_repository=PostgresArticleRepository(db),
|
|
|
|
|
article_ownership_repository=PostgresArticleOwnershipRepository(db),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-03-27 11:04:05 +00:00
|
|
|
class ArticleItem(BaseModel):
|
|
|
|
|
id: str
|
2026-06-02 20:02:50 +00:00
|
|
|
published_at: datetime | None
|
|
|
|
|
language: str
|
|
|
|
|
title: str
|
|
|
|
|
complexity: str
|
2026-03-26 20:47:15 +00:00
|
|
|
|
|
|
|
|
|
2026-03-27 11:04:05 +00:00
|
|
|
class ArticleListResponse(BaseModel):
|
|
|
|
|
articles: list[ArticleItem]
|
2026-03-26 20:47:15 +00:00
|
|
|
|
|
|
|
|
|
2026-03-27 11:04:05 +00:00
|
|
|
class ArticleDetail(BaseModel):
|
|
|
|
|
id: str
|
2026-06-02 20:02:50 +00:00
|
|
|
published_at: datetime | None
|
|
|
|
|
language: str
|
|
|
|
|
complexity: str
|
|
|
|
|
title: str
|
|
|
|
|
body: str
|
|
|
|
|
audio_url: str | None
|
|
|
|
|
body_pos: dict | None
|
2026-03-26 20:47:15 +00:00
|
|
|
|
|
|
|
|
|
2026-03-27 11:04:05 +00:00
|
|
|
def _audio_url(key: str | None) -> str | None:
|
|
|
|
|
if key is None:
|
|
|
|
|
return None
|
2026-05-18 20:18:19 +00:00
|
|
|
return get_storage_client().get_url(key)
|
2026-03-26 20:47:15 +00:00
|
|
|
|
2026-03-27 11:04:05 +00:00
|
|
|
|
|
|
|
|
@router.get("", response_model=ArticleListResponse, status_code=200)
|
|
|
|
|
async def list_articles(
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
2026-06-02 20:02:50 +00:00
|
|
|
token_data: dict = Depends(verify_token),
|
2026-03-27 11:04:05 +00:00
|
|
|
) -> ArticleListResponse:
|
2026-06-02 20:02:50 +00:00
|
|
|
service = _make_article_service(db)
|
|
|
|
|
user_id = token_data["sub"]
|
|
|
|
|
articles = await service.get_articles_for_user(user_id=user_id)
|
2026-03-27 11:04:05 +00:00
|
|
|
return ArticleListResponse(
|
|
|
|
|
articles=[
|
|
|
|
|
ArticleItem(
|
|
|
|
|
id=a.id,
|
|
|
|
|
published_at=a.published_at,
|
2026-06-02 20:02:50 +00:00
|
|
|
language=a.language,
|
|
|
|
|
title=a.title,
|
|
|
|
|
complexity=a.target_complexity,
|
2026-03-27 11:04:05 +00:00
|
|
|
)
|
|
|
|
|
for a in articles
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{article_id}", response_model=ArticleDetail, status_code=200)
|
|
|
|
|
async def get_article(
|
|
|
|
|
article_id: str,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
2026-06-02 20:02:50 +00:00
|
|
|
token_data: dict = Depends(verify_token),
|
2026-03-27 11:04:05 +00:00
|
|
|
) -> ArticleDetail:
|
2026-06-02 20:02:50 +00:00
|
|
|
uid: str = token_data["sub"]
|
|
|
|
|
service = _make_article_service(db)
|
|
|
|
|
article = await service.get_article_as_user(article_id, uid)
|
2026-03-27 11:04:05 +00:00
|
|
|
|
|
|
|
|
if article is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Article not found")
|
|
|
|
|
|
|
|
|
|
return ArticleDetail(
|
|
|
|
|
id=article.id,
|
|
|
|
|
published_at=article.published_at,
|
2026-06-02 20:02:50 +00:00
|
|
|
language=article.language,
|
|
|
|
|
complexity=article.target_complexity,
|
|
|
|
|
title=article.title,
|
|
|
|
|
body=article.text,
|
|
|
|
|
body_pos=article.text_linguistic_data,
|
|
|
|
|
audio_url=_audio_url(article.audio_key),
|
2026-03-27 11:04:05 +00:00
|
|
|
)
|