feat: add comments feature with nested replies and recursive rendering
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:
2026-05-11 15:34:20 +03:00
parent 63da25174e
commit 7ff3fa0992
40 changed files with 3161 additions and 44 deletions

View File

@@ -0,0 +1,96 @@
"""Toggle comment like use case.
This module implements the use case for toggling likes on comments.
If the user already liked the comment, the like is removed (unlike).
If not, a new like is added.
"""
from uuid import UUID
from app.application.dtos.comment import CommentResponseDTO
from app.application.interfaces import TransactionManager
from app.domain.entities.comment import Comment
from app.domain.entities.comment_like import CommentLike
from app.domain.exceptions import NotFoundException
from app.domain.repositories import CommentRepository
class ToggleCommentLikeUseCase:
"""Use case for toggling a like on a comment.
Handles like/unlike toggle logic. If the user has already liked
the comment, the like is removed. Otherwise, a new like is created.
Attributes:
_comment_repo: Repository for comment and like data access.
_tx_manager: Transaction manager for commit control.
"""
def __init__(
self,
comment_repo: CommentRepository,
tx_manager: TransactionManager,
) -> None:
"""Initialize use case with dependencies.
Args:
comment_repo: Repository for comment and like operations.
tx_manager: Transaction manager instance.
"""
self._comment_repo = comment_repo
self._tx_manager = tx_manager
async def execute(self, comment_id: UUID, liked_by: str) -> CommentResponseDTO:
"""Toggle like on a comment.
If the user already liked the comment, remove the like.
Otherwise, add a new like.
Args:
comment_id: UUID of the comment to toggle like on.
liked_by: User ID.
Returns:
CommentResponseDTO with updated like_count.
Raises:
NotFoundException: If comment with given ID does not exist.
"""
comment = await self._comment_repo.get_by_id(comment_id)
if not comment:
raise NotFoundException(f"Comment with id '{comment_id}' not found")
existing_like = await self._comment_repo.get_like(comment_id, liked_by)
if existing_like:
await self._comment_repo.remove_like(comment_id, liked_by)
comment.like_count = max(0, comment.like_count - 1)
else:
new_like = CommentLike(comment_id=comment_id, liked_by=liked_by)
await self._comment_repo.add_like(new_like)
comment.like_count += 1
await self._comment_repo.update(comment)
await self._tx_manager.commit()
return self._map_to_dto(comment)
def _map_to_dto(self, comment: Comment) -> CommentResponseDTO:
"""Map domain entity to response DTO.
Args:
comment: Domain Comment entity.
Returns:
CommentResponseDTO with all comment attributes including like_count.
"""
return CommentResponseDTO(
id=comment.id,
post_id=comment.post_id,
author_id=comment.author_id,
content=comment.content.value,
parent_id=comment.parent_id,
like_count=comment.like_count,
created_at=comment.created_at,
updated_at=comment.updated_at,
)