test ferst

This commit is contained in:
jze9
2026-05-13 19:05:07 +05:00
commit 0ac1c8a862
33 changed files with 3702 additions and 0 deletions

224
api/route/admin_articles.py Normal file
View 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()}