feat: add like/unlike toggle on blog posts with per-user tracking
- PostLike domain entity (post_id, liked_by) with BaseEntity integration
- Post entity: add like_count field (default 0) and to_dict serialization
- PostRepository interface: add get_like, add_like, remove_like methods
- TogglePostLikeUseCase: toggle logic (like → unlike, unlike → like)
- PostResponseDTO/PostResponseSchema: add like_count field
- PostLikeORM model with FK to posts and cascade delete
- SQLAlchemyPostRepository: implement like query/add/remove with ORM mapping
- DI provider registration for TogglePostLikeUseCase
- API endpoint POST /api/v1/posts/{id}/like (auth required)
- Unit tests: PostLike entity, Post.like_count, TogglePostLikeUseCase (7 tests)
- API tests: POST /api/v1/posts/{id}/like (4 tests)
- Test model files: FEATURE_LIKES.md, TEST_MODEL.md updated
This commit is contained in:
@@ -10,9 +10,10 @@ from sqlalchemy import or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.domain.entities import Post
|
||||
from app.domain.entities.like import PostLike
|
||||
from app.domain.repositories import PostRepository
|
||||
from app.domain.value_objects import Content, Slug, Title
|
||||
from app.infrastructure.database.models import PostORM
|
||||
from app.infrastructure.database.models import PostLikeORM, PostORM
|
||||
|
||||
|
||||
class SQLAlchemyPostRepository(PostRepository):
|
||||
@@ -53,6 +54,7 @@ class SQLAlchemyPostRepository(PostRepository):
|
||||
slug=Slug(orm.slug),
|
||||
author_id=orm.author_id,
|
||||
published=orm.published,
|
||||
like_count=orm.like_count,
|
||||
tags=orm.tags or [],
|
||||
created_at=orm.created_at,
|
||||
updated_at=orm.updated_at,
|
||||
@@ -74,6 +76,7 @@ class SQLAlchemyPostRepository(PostRepository):
|
||||
slug=post.slug.value,
|
||||
author_id=post.author_id,
|
||||
published=post.published,
|
||||
like_count=post.like_count,
|
||||
tags=post.tags,
|
||||
created_at=post.created_at,
|
||||
updated_at=post.updated_at,
|
||||
@@ -124,6 +127,7 @@ class SQLAlchemyPostRepository(PostRepository):
|
||||
orm.content = entity.content.value
|
||||
orm.slug = entity.slug.value
|
||||
orm.published = entity.published
|
||||
orm.like_count = entity.like_count
|
||||
orm.tags = entity.tags
|
||||
orm.updated_at = entity.updated_at
|
||||
|
||||
@@ -284,3 +288,60 @@ class SQLAlchemyPostRepository(PostRepository):
|
||||
result = await self._session.execute(stmt)
|
||||
orms = result.scalars().all()
|
||||
return [self._to_domain(orm) for orm in orms]
|
||||
|
||||
async def get_like(self, post_id: UUID, liked_by: str) -> PostLike | None:
|
||||
"""Get a like by post and user/device.
|
||||
|
||||
Args:
|
||||
post_id: UUID of the post.
|
||||
liked_by: User ID or device ID.
|
||||
|
||||
Returns:
|
||||
PostLike if found, None otherwise.
|
||||
"""
|
||||
result = await self._session.execute(
|
||||
select(PostLikeORM).where(
|
||||
PostLikeORM.post_id == str(post_id),
|
||||
PostLikeORM.liked_by == liked_by,
|
||||
)
|
||||
)
|
||||
orm = result.scalar_one_or_none()
|
||||
if not orm:
|
||||
return None
|
||||
return PostLike(
|
||||
id=UUID(orm.id),
|
||||
post_id=UUID(orm.post_id),
|
||||
liked_by=orm.liked_by,
|
||||
created_at=orm.created_at,
|
||||
)
|
||||
|
||||
async def add_like(self, like: PostLike) -> None:
|
||||
"""Add a new like.
|
||||
|
||||
Args:
|
||||
like: PostLike entity to add.
|
||||
"""
|
||||
orm = PostLikeORM(
|
||||
id=str(like.id),
|
||||
post_id=str(like.post_id),
|
||||
liked_by=like.liked_by,
|
||||
created_at=like.created_at,
|
||||
)
|
||||
self._session.add(orm)
|
||||
|
||||
async def remove_like(self, post_id: UUID, liked_by: str) -> None:
|
||||
"""Remove a like by post and user/device.
|
||||
|
||||
Args:
|
||||
post_id: UUID of the post.
|
||||
liked_by: User ID or device ID.
|
||||
"""
|
||||
result = await self._session.execute(
|
||||
select(PostLikeORM).where(
|
||||
PostLikeORM.post_id == str(post_id),
|
||||
PostLikeORM.liked_by == liked_by,
|
||||
)
|
||||
)
|
||||
orm = result.scalar_one_or_none()
|
||||
if orm:
|
||||
await self._session.delete(orm)
|
||||
|
||||
Reference in New Issue
Block a user