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.
241 lines
7.3 KiB
Python
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)
|