feat: add comments feature with nested replies and recursive rendering
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
Implement full comments system: domain entities (Comment, CommentLike), value objects (CommentContent), use cases (CRUD, like toggle), SQLAlchemy repository, API v1 endpoints, web UI with comment form and nested replies, i18n translations (EN/RU/FR/DE), and E2E tests. Fix nested reply (reply-to-reply) not displaying — the flat reply_comments dict was only queried for top-level comment IDs, so deeply nested replies were saved to DB (incrementing comment count) but never rendered. Switch to a recursive Jinja2 macro that renders any nesting depth.
This commit is contained in:
131
app/presentation/api/v1/comments.py
Normal file
131
app/presentation/api/v1/comments.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""Comments API routes.
|
||||
|
||||
This module defines FastAPI routes for comment operations including
|
||||
CRUD and like/unlike toggle.
|
||||
"""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from dishka.integrations.fastapi import DishkaRoute
|
||||
from fastapi import APIRouter, status
|
||||
|
||||
from app.presentation.api.deps import (
|
||||
CreateCommentDep,
|
||||
CurrentRoleDep,
|
||||
CurrentUserDep,
|
||||
DeleteCommentDep,
|
||||
ListCommentsDep,
|
||||
ToggleCommentLikeDep,
|
||||
)
|
||||
from app.presentation.schemas import (
|
||||
CommentCreateSchema,
|
||||
CommentLikeResponseSchema,
|
||||
CommentResponseSchema,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["comments"], route_class=DishkaRoute)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/posts/{post_id}/comments",
|
||||
response_model=CommentResponseSchema,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create a comment on a post",
|
||||
)
|
||||
async def create_comment(
|
||||
post_id: UUID,
|
||||
schema: CommentCreateSchema,
|
||||
use_case: CreateCommentDep,
|
||||
current_user_id: CurrentUserDep,
|
||||
) -> CommentResponseSchema:
|
||||
"""Create a comment on a blog post.
|
||||
|
||||
Args:
|
||||
post_id: UUID of the post to comment on.
|
||||
schema: Comment creation data.
|
||||
use_case: CreateCommentUseCase dependency.
|
||||
current_user_id: Authenticated user ID.
|
||||
|
||||
Returns:
|
||||
CommentResponseSchema with created comment data.
|
||||
"""
|
||||
result = await use_case.execute(
|
||||
post_id=post_id,
|
||||
author_id=current_user_id,
|
||||
content=schema.content,
|
||||
parent_id=schema.parent_id,
|
||||
)
|
||||
return CommentResponseSchema(**result.__dict__)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/posts/{post_id}/comments",
|
||||
response_model=list[CommentResponseSchema],
|
||||
summary="List comments for a post",
|
||||
)
|
||||
async def list_comments(
|
||||
post_id: UUID,
|
||||
use_case: ListCommentsDep,
|
||||
) -> list[CommentResponseSchema]:
|
||||
"""Get all comments for a blog post.
|
||||
|
||||
Args:
|
||||
post_id: UUID of the post.
|
||||
use_case: ListCommentsUseCase dependency.
|
||||
|
||||
Returns:
|
||||
List of CommentResponseSchema for the post.
|
||||
"""
|
||||
results = await use_case.execute(post_id=post_id)
|
||||
return [CommentResponseSchema(**r.__dict__) for r in results]
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/comments/{comment_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete a comment",
|
||||
)
|
||||
async def delete_comment(
|
||||
comment_id: UUID,
|
||||
use_case: DeleteCommentDep,
|
||||
current_user_id: CurrentUserDep,
|
||||
role: CurrentRoleDep,
|
||||
) -> None:
|
||||
"""Delete a comment.
|
||||
|
||||
Users can delete their own comments.
|
||||
|
||||
Args:
|
||||
comment_id: UUID of the comment to delete.
|
||||
use_case: DeleteCommentUseCase dependency.
|
||||
current_user_id: Authenticated user ID.
|
||||
role: Current user role.
|
||||
"""
|
||||
await use_case.execute(comment_id=comment_id, user_id=current_user_id)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/comments/{comment_id}/like",
|
||||
response_model=CommentLikeResponseSchema,
|
||||
summary="Toggle like on a comment",
|
||||
)
|
||||
async def toggle_comment_like(
|
||||
comment_id: UUID,
|
||||
use_case: ToggleCommentLikeDep,
|
||||
current_user_id: CurrentUserDep,
|
||||
) -> CommentLikeResponseSchema:
|
||||
"""Toggle like/unlike on a comment.
|
||||
|
||||
If the user already liked the comment, the like is removed (unlike).
|
||||
Otherwise, a new like is added.
|
||||
|
||||
Args:
|
||||
comment_id: UUID of the comment.
|
||||
use_case: ToggleCommentLikeUseCase dependency.
|
||||
current_user_id: Authenticated user ID.
|
||||
|
||||
Returns:
|
||||
CommentLikeResponseSchema with updated like_count.
|
||||
"""
|
||||
result = await use_case.execute(comment_id, current_user_id)
|
||||
return CommentLikeResponseSchema(id=result.id, like_count=result.like_count)
|
||||
Reference in New Issue
Block a user