test ferst
This commit is contained in:
224
api/route/admin_articles.py
Normal file
224
api/route/admin_articles.py
Normal file
@@ -0,0 +1,224 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.orm import selectinload
|
||||
import redis.asyncio as aioredis
|
||||
|
||||
from bd.database import get_db, get_redis
|
||||
from bd.models import Article, Category, Tag, ArticleStatus
|
||||
from route.deps import require_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ArticleIn(BaseModel):
|
||||
title: str
|
||||
slug: Optional[str] = None
|
||||
content: str = ""
|
||||
excerpt: str = ""
|
||||
cover_url: Optional[str] = None
|
||||
font_family: str = "Merriweather"
|
||||
category_id: Optional[str] = None
|
||||
tag_names: list[str] = []
|
||||
status: ArticleStatus = ArticleStatus.draft
|
||||
|
||||
|
||||
def _article_dict(a: Article) -> dict:
|
||||
return {
|
||||
"id": str(a.id),
|
||||
"title": a.title,
|
||||
"slug": a.slug,
|
||||
"content": a.content,
|
||||
"excerpt": a.excerpt,
|
||||
"cover_url": a.cover_url,
|
||||
"font_family": a.font_family,
|
||||
"status": a.status.value,
|
||||
"view_count": a.view_count,
|
||||
"created_at": a.created_at.isoformat(),
|
||||
"updated_at": a.updated_at.isoformat(),
|
||||
"published_at": a.published_at.isoformat() if a.published_at else None,
|
||||
"category": {"id": str(a.category.id), "name": a.category.name, "slug": a.category.slug} if a.category else None,
|
||||
"tags": [{"id": str(t.id), "name": t.name, "slug": t.slug} for t in a.tags],
|
||||
}
|
||||
|
||||
|
||||
async def _resolve_tags(tag_names: list[str], db: AsyncSession) -> list[Tag]:
|
||||
from slugify import slugify
|
||||
tags = []
|
||||
for name in tag_names:
|
||||
name = name.strip()
|
||||
if not name:
|
||||
continue
|
||||
slug = slugify(name)
|
||||
tag = (await db.execute(select(Tag).where(Tag.slug == slug))).scalar_one_or_none()
|
||||
if not tag:
|
||||
tag = Tag(name=name, slug=slug)
|
||||
db.add(tag)
|
||||
tags.append(tag)
|
||||
return tags
|
||||
|
||||
|
||||
async def _invalidate_cache(redis: aioredis.Redis):
|
||||
keys = await redis.keys("news:*")
|
||||
if keys:
|
||||
await redis.delete(*keys)
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_articles(
|
||||
offset: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
status: Optional[ArticleStatus] = Query(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: str = Depends(require_admin),
|
||||
):
|
||||
q = select(Article).options(selectinload(Article.category), selectinload(Article.tags))
|
||||
if status:
|
||||
q = q.where(Article.status == status)
|
||||
total = (await db.execute(select(func.count()).select_from(q.subquery()))).scalar_one()
|
||||
articles = (await db.execute(q.order_by(Article.created_at.desc()).offset(offset).limit(limit))).scalars().all()
|
||||
return {
|
||||
"total": total,
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
"has_more": (offset + limit) < total,
|
||||
"items": [_article_dict(a) for a in articles],
|
||||
}
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_article(
|
||||
data: ArticleIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
_: str = Depends(require_admin),
|
||||
):
|
||||
from slugify import slugify
|
||||
slug = data.slug or slugify(data.title)
|
||||
existing = (await db.execute(select(Article).where(Article.slug == slug))).scalar_one_or_none()
|
||||
if existing:
|
||||
slug = f"{slug}-{uuid.uuid4().hex[:6]}"
|
||||
|
||||
cat = None
|
||||
if data.category_id:
|
||||
cat = (await db.execute(select(Category).where(Category.id == uuid.UUID(data.category_id)))).scalar_one_or_none()
|
||||
|
||||
tags = await _resolve_tags(data.tag_names, db)
|
||||
|
||||
article = Article(
|
||||
title=data.title,
|
||||
slug=slug,
|
||||
content=data.content,
|
||||
excerpt=data.excerpt,
|
||||
cover_url=data.cover_url,
|
||||
font_family=data.font_family,
|
||||
status=data.status,
|
||||
category=cat,
|
||||
tags=tags,
|
||||
published_at=datetime.utcnow() if data.status == ArticleStatus.published else None,
|
||||
)
|
||||
db.add(article)
|
||||
await db.commit()
|
||||
await db.refresh(article)
|
||||
await _invalidate_cache(redis)
|
||||
return _article_dict(article)
|
||||
|
||||
|
||||
@router.get("/{article_id}")
|
||||
async def get_article(
|
||||
article_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: str = Depends(require_admin),
|
||||
):
|
||||
q = select(Article).options(selectinload(Article.category), selectinload(Article.tags)).where(Article.id == uuid.UUID(article_id))
|
||||
article = (await db.execute(q)).scalar_one_or_none()
|
||||
if not article:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _article_dict(article)
|
||||
|
||||
|
||||
@router.put("/{article_id}")
|
||||
async def update_article(
|
||||
article_id: str,
|
||||
data: ArticleIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
_: str = Depends(require_admin),
|
||||
):
|
||||
from slugify import slugify
|
||||
q = select(Article).options(selectinload(Article.category), selectinload(Article.tags)).where(Article.id == uuid.UUID(article_id))
|
||||
article = (await db.execute(q)).scalar_one_or_none()
|
||||
if not article:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
|
||||
slug = data.slug or slugify(data.title)
|
||||
conflict = (await db.execute(
|
||||
select(Article).where(Article.slug == slug, Article.id != article.id)
|
||||
)).scalar_one_or_none()
|
||||
if conflict:
|
||||
slug = f"{slug}-{uuid.uuid4().hex[:6]}"
|
||||
|
||||
was_draft = article.status == ArticleStatus.draft
|
||||
cat = None
|
||||
if data.category_id:
|
||||
cat = (await db.execute(select(Category).where(Category.id == uuid.UUID(data.category_id)))).scalar_one_or_none()
|
||||
|
||||
tags = await _resolve_tags(data.tag_names, db)
|
||||
|
||||
article.title = data.title
|
||||
article.slug = slug
|
||||
article.content = data.content
|
||||
article.excerpt = data.excerpt
|
||||
article.cover_url = data.cover_url
|
||||
article.font_family = data.font_family
|
||||
article.status = data.status
|
||||
article.category = cat
|
||||
article.tags = tags
|
||||
article.updated_at = datetime.utcnow()
|
||||
|
||||
if was_draft and data.status == ArticleStatus.published and not article.published_at:
|
||||
article.published_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(article)
|
||||
await _invalidate_cache(redis)
|
||||
return _article_dict(article)
|
||||
|
||||
|
||||
@router.delete("/{article_id}")
|
||||
async def delete_article(
|
||||
article_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
_: str = Depends(require_admin),
|
||||
):
|
||||
article = (await db.execute(select(Article).where(Article.id == uuid.UUID(article_id)))).scalar_one_or_none()
|
||||
if not article:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
await db.delete(article)
|
||||
await db.commit()
|
||||
await _invalidate_cache(redis)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/{article_id}/publish")
|
||||
async def publish_article(
|
||||
article_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
_: str = Depends(require_admin),
|
||||
):
|
||||
article = (await db.execute(select(Article).where(Article.id == uuid.UUID(article_id)))).scalar_one_or_none()
|
||||
if not article:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
article.status = ArticleStatus.published
|
||||
if not article.published_at:
|
||||
article.published_at = datetime.utcnow()
|
||||
article.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
await _invalidate_cache(redis)
|
||||
return {"ok": True, "published_at": article.published_at.isoformat()}
|
||||
Reference in New Issue
Block a user