v1
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Service layer for BlogPost CRUD operations.
|
||||
"""
|
||||
import math
|
||||
import nh3
|
||||
from typing import Optional
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.post import BlogPost
|
||||
from app.schemas.post import PostCreate, PostUpdate, PaginatedPostsResponse, PostResponse
|
||||
|
||||
|
||||
def _sanitize_body(body: str) -> str:
|
||||
"""Strip dangerous HTML tags/attributes using nh3."""
|
||||
return nh3.clean(body)
|
||||
|
||||
|
||||
async def get_published_posts(
|
||||
db: AsyncSession, page: int = 1, per_page: int = 10
|
||||
) -> PaginatedPostsResponse:
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
count_result = await db.execute(
|
||||
select(func.count()).select_from(BlogPost).where(BlogPost.published == True)
|
||||
)
|
||||
total = count_result.scalar_one()
|
||||
|
||||
result = await db.execute(
|
||||
select(BlogPost)
|
||||
.where(BlogPost.published == True)
|
||||
.order_by(BlogPost.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(per_page)
|
||||
)
|
||||
items = list(result.scalars().all())
|
||||
|
||||
total_pages = math.ceil(total / per_page) if per_page > 0 else 0
|
||||
|
||||
return PaginatedPostsResponse(
|
||||
items=[PostResponse.model_validate(p) for p in items],
|
||||
total=total,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
total_pages=total_pages,
|
||||
)
|
||||
|
||||
|
||||
async def get_post_by_slug(
|
||||
db: AsyncSession, slug: str, published_only: bool = True
|
||||
) -> Optional[BlogPost]:
|
||||
stmt = select(BlogPost).where(BlogPost.slug == slug)
|
||||
if published_only:
|
||||
stmt = stmt.where(BlogPost.published == True)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().first()
|
||||
|
||||
|
||||
async def create_post(db: AsyncSession, data: PostCreate) -> BlogPost:
|
||||
post = BlogPost(
|
||||
title=data.title,
|
||||
slug=data.slug,
|
||||
excerpt=data.excerpt,
|
||||
body=_sanitize_body(data.body),
|
||||
author=data.author,
|
||||
featured_image_url=data.featured_image_url,
|
||||
tags=data.tags,
|
||||
published=data.published,
|
||||
)
|
||||
db.add(post)
|
||||
await db.flush()
|
||||
await db.refresh(post)
|
||||
return post
|
||||
|
||||
|
||||
async def update_post(db: AsyncSession, slug: str, data: PostUpdate) -> Optional[BlogPost]:
|
||||
post = await get_post_by_slug(db, slug, published_only=False)
|
||||
if post is None:
|
||||
return None
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
if "body" in update_data and update_data["body"] is not None:
|
||||
update_data["body"] = _sanitize_body(update_data["body"])
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(post, field, value)
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(post)
|
||||
return post
|
||||
|
||||
|
||||
async def delete_post(db: AsyncSession, slug: str) -> bool:
|
||||
post = await get_post_by_slug(db, slug, published_only=False)
|
||||
if post is None:
|
||||
return False
|
||||
await db.delete(post)
|
||||
await db.flush()
|
||||
return True
|
||||
Reference in New Issue
Block a user