Files
blog.pyaqa.ru/app/infrastructure/repositories/comment.py
Sergey Vanyushkin 7ff3fa0992
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
feat: add comments feature with nested replies and recursive rendering
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.
2026-05-11 15:34:20 +03:00

241 lines
7.3 KiB
Python

"""SQLAlchemy implementation of CommentRepository.
This module provides the concrete implementation of CommentRepository
using SQLAlchemy ORM for data persistence.
"""
from uuid import UUID
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.entities.comment import Comment
from app.domain.entities.comment_like import CommentLike
from app.domain.repositories import CommentRepository
from app.domain.value_objects.comment_content import CommentContent
from app.infrastructure.database.models import CommentLikeORM, CommentORM
class SQLAlchemyCommentRepository(CommentRepository):
"""SQLAlchemy implementation of Comment repository.
Provides data access methods for Comment entities using SQLAlchemy ORM.
Handles conversion between domain entities and ORM models.
Attributes:
_session: SQLAlchemy async session for database operations.
"""
def __init__(self, session: AsyncSession) -> None:
"""Initialize repository with session.
Args:
session: SQLAlchemy async session instance.
"""
self._session = session
def _to_domain(self, orm: CommentORM) -> Comment:
"""Convert ORM model to domain entity.
Args:
orm: SQLAlchemy ORM model instance.
Returns:
Domain Comment entity with validated value objects.
"""
return Comment(
id=UUID(orm.id),
post_id=UUID(orm.post_id),
author_id=orm.author_id,
content=CommentContent(orm.content),
parent_id=UUID(orm.parent_id) if orm.parent_id else None,
like_count=orm.like_count,
created_at=orm.created_at,
updated_at=orm.updated_at,
)
def _to_orm(self, comment: Comment) -> CommentORM:
"""Convert domain entity to ORM model.
Args:
comment: Domain Comment entity.
Returns:
SQLAlchemy ORM model instance.
"""
return CommentORM(
id=str(comment.id),
post_id=str(comment.post_id),
author_id=comment.author_id,
content=comment.content.value,
parent_id=str(comment.parent_id) if comment.parent_id else None,
like_count=comment.like_count,
created_at=comment.created_at,
updated_at=comment.updated_at,
)
async def get_by_id(self, entity_id: UUID) -> Comment | None:
"""Get comment by ID.
Args:
entity_id: Unique identifier of the comment.
Returns:
Comment entity if found, None otherwise.
"""
result = await self._session.execute(
select(CommentORM).where(CommentORM.id == str(entity_id))
)
orm = result.scalar_one_or_none()
return self._to_domain(orm) if orm else None
async def get_all(self) -> list[Comment]:
"""Get all comments.
Returns:
List of all Comment entities.
"""
result = await self._session.execute(select(CommentORM))
orms = result.scalars().all()
return [self._to_domain(orm) for orm in orms]
async def add(self, entity: Comment) -> None:
"""Add new comment.
Args:
entity: Comment entity to add.
"""
orm = self._to_orm(entity)
self._session.add(orm)
async def update(self, entity: Comment) -> None:
"""Update existing comment.
Args:
entity: Comment entity with updated data.
"""
result = await self._session.execute(
select(CommentORM).where(CommentORM.id == str(entity.id))
)
orm = result.scalar_one()
orm.content = entity.content.value
orm.like_count = entity.like_count
orm.updated_at = entity.updated_at
async def delete(self, entity_id: UUID) -> None:
"""Delete comment by ID.
Args:
entity_id: Unique identifier of the comment to delete.
"""
result = await self._session.execute(
select(CommentORM).where(CommentORM.id == str(entity_id))
)
orm = result.scalar_one_or_none()
if orm:
await self._session.delete(orm)
async def exists(self, entity_id: UUID) -> bool:
"""Check if comment exists.
Args:
entity_id: Unique identifier of the comment.
Returns:
True if comment exists, False otherwise.
"""
result = await self._session.execute(
select(CommentORM).where(CommentORM.id == str(entity_id))
)
return result.scalar_one_or_none() is not None
async def get_by_post(self, post_id: UUID) -> list[Comment]:
"""Get all comments for a post, ordered by creation time.
Args:
post_id: UUID of the post.
Returns:
List of Comment entities for the post.
"""
result = await self._session.execute(
select(CommentORM)
.where(CommentORM.post_id == str(post_id))
.order_by(CommentORM.created_at.asc())
)
orms = result.scalars().all()
return [self._to_domain(orm) for orm in orms]
async def count_by_post(self, post_id: UUID) -> int:
"""Get comment count for a post.
Args:
post_id: UUID of the post.
Returns:
Number of comments on the post.
"""
result = await self._session.execute(
select(func.count()).select_from(CommentORM).where(CommentORM.post_id == str(post_id))
)
count: int = result.scalar() or 0
return count
async def get_like(self, comment_id: UUID, liked_by: str) -> CommentLike | None:
"""Get a like by comment and user.
Args:
comment_id: UUID of the comment.
liked_by: User ID.
Returns:
CommentLike if found, None otherwise.
"""
result = await self._session.execute(
select(CommentLikeORM).where(
CommentLikeORM.comment_id == str(comment_id),
CommentLikeORM.liked_by == liked_by,
)
)
orm = result.scalar_one_or_none()
if not orm:
return None
return CommentLike(
id=UUID(orm.id),
comment_id=UUID(orm.comment_id),
liked_by=orm.liked_by,
created_at=orm.created_at,
)
async def add_like(self, like: CommentLike) -> None:
"""Add a new like to a comment.
Args:
like: CommentLike entity to add.
"""
orm = CommentLikeORM(
id=str(like.id),
comment_id=str(like.comment_id),
liked_by=like.liked_by,
created_at=like.created_at,
)
self._session.add(orm)
async def remove_like(self, comment_id: UUID, liked_by: str) -> None:
"""Remove a like from a comment by user.
Args:
comment_id: UUID of the comment.
liked_by: User ID.
"""
result = await self._session.execute(
select(CommentLikeORM).where(
CommentLikeORM.comment_id == str(comment_id),
CommentLikeORM.liked_by == liked_by,
)
)
orm = result.scalar_one_or_none()
if orm:
await self._session.delete(orm)