Files
gw/backend/app/services/posts.py
T

100 lines
2.7 KiB
Python
Raw Normal View History

2026-04-18 07:23:55 +12:00
"""
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