"""Posts API routes.""" from uuid import UUID from dishka.integrations.fastapi import DishkaRoute from fastapi import APIRouter, status from app.application.dtos import CreatePostDTO, UpdatePostDTO from app.domain.exceptions import ForbiddenException from app.domain.roles import Permission, has_permission from app.presentation.api.deps import ( CreatePostDep, CurrentRoleDep, CurrentUserDep, DeletePostDep, GetPostDep, ListPostsDep, PublishPostDep, UpdatePostDep, ) from app.presentation.schemas import ( PostCreateSchema, PostListResponseSchema, PostResponseSchema, PostUpdateSchema, ) router = APIRouter(prefix="/posts", tags=["posts"], route_class=DishkaRoute) @router.post( "", response_model=PostResponseSchema, status_code=status.HTTP_201_CREATED, summary="Create a new post", ) async def create_post( schema: PostCreateSchema, use_case: CreatePostDep, current_user_id: CurrentUserDep, ) -> PostResponseSchema: """Create a new blog post.""" dto = CreatePostDTO( title=schema.title, content=schema.content, author_id=current_user_id, tags=schema.tags, ) result = await use_case.execute(dto) return PostResponseSchema(**result.__dict__) @router.get( "", response_model=PostListResponseSchema, summary="List posts", ) async def list_posts( use_case: ListPostsDep, role: CurrentRoleDep, include_unpublished: bool = False, limit: int = 10, offset: int = 0, ) -> PostListResponseSchema: """Get blog posts with optional filtering and pagination. Args: include_unpublished: If True, returns all posts including drafts. Only admins can use this parameter. limit: Maximum number of posts to return (default: 10, max: 100). offset: Number of posts to skip (default: 0). Raises: ForbiddenException: If non-admin tries to include unpublished posts. """ # Clamp limit to reasonable range limit = max(1, min(limit, 100)) offset = max(0, offset) # Check permissions for unpublished posts if include_unpublished: if not has_permission(role, Permission.POST_READ_UNPUBLISHED): raise ForbiddenException("Only admins can view unpublished posts") results = await use_case.all_posts() else: results = await use_case.published_posts(limit=limit, offset=offset) items = [PostResponseSchema(**r.__dict__) for r in results] return PostListResponseSchema(items=items, total=len(items)) @router.get( "/published", response_model=PostListResponseSchema, summary="List published posts", ) async def list_published_posts( use_case: ListPostsDep, ) -> PostListResponseSchema: """Get all published blog posts.""" results = await use_case.published_posts() items = [PostResponseSchema(**r.__dict__) for r in results] return PostListResponseSchema(items=items, total=len(items)) @router.get( "/search", response_model=PostListResponseSchema, summary="Search posts", ) async def search_posts( query: str, use_case: ListPostsDep, ) -> PostListResponseSchema: """Search posts by query.""" results = await use_case.search(query) items = [PostResponseSchema(**r.__dict__) for r in results] return PostListResponseSchema(items=items, total=len(items)) @router.get( "/by-tag/{tag}", response_model=PostListResponseSchema, summary="Get posts by tag", ) async def get_posts_by_tag( tag: str, use_case: ListPostsDep, ) -> PostListResponseSchema: """Get posts by tag.""" results = await use_case.by_tag(tag) items = [PostResponseSchema(**r.__dict__) for r in results] return PostListResponseSchema(items=items, total=len(items)) @router.get( "/by-author/{author_id}", response_model=PostListResponseSchema, summary="Get posts by author", ) async def get_posts_by_author( author_id: str, use_case: ListPostsDep, ) -> PostListResponseSchema: """Get posts by author.""" results = await use_case.by_author(author_id) items = [PostResponseSchema(**r.__dict__) for r in results] return PostListResponseSchema(items=items, total=len(items)) @router.get( "/{post_id}", response_model=PostResponseSchema, summary="Get post by ID", ) async def get_post( post_id: UUID, use_case: GetPostDep, ) -> PostResponseSchema: """Get a post by its ID.""" result = await use_case.by_id(post_id) return PostResponseSchema(**result.__dict__) @router.get( "/slug/{slug}", response_model=PostResponseSchema, summary="Get post by slug", ) async def get_post_by_slug( slug: str, use_case: GetPostDep, ) -> PostResponseSchema: """Get a post by its slug.""" result = await use_case.by_slug(slug) return PostResponseSchema(**result.__dict__) @router.patch( "/{post_id}", response_model=PostResponseSchema, summary="Update post", ) async def update_post( post_id: UUID, schema: PostUpdateSchema, use_case: UpdatePostDep, current_user_id: CurrentUserDep, ) -> PostResponseSchema: """Update a post.""" dto = UpdatePostDTO( title=schema.title, content=schema.content, tags=schema.tags, ) result = await use_case.execute(post_id, dto, current_user_id) return PostResponseSchema(**result.__dict__) @router.delete( "/{post_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete post", ) async def delete_post( post_id: UUID, use_case: DeletePostDep, current_user_id: CurrentUserDep, ) -> None: """Delete a post.""" await use_case.execute(post_id, current_user_id) @router.post( "/{post_id}/publish", response_model=PostResponseSchema, summary="Publish post", ) async def publish_post( post_id: UUID, use_case: PublishPostDep, current_user_id: CurrentUserDep, ) -> PostResponseSchema: """Publish a post.""" result = await use_case.publish(post_id, current_user_id) return PostResponseSchema(**result.__dict__) @router.post( "/{post_id}/unpublish", response_model=PostResponseSchema, summary="Unpublish post", ) async def unpublish_post( post_id: UUID, use_case: PublishPostDep, current_user_id: CurrentUserDep, ) -> PostResponseSchema: """Unpublish a post.""" result = await use_case.unpublish(post_id, current_user_id) return PostResponseSchema(**result.__dict__)