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:
@@ -12,6 +12,7 @@ from app.application.use_cases import (
|
||||
GetPostUseCase,
|
||||
ListPostsUseCase,
|
||||
PublishPostUseCase,
|
||||
TogglePostLikeUseCase,
|
||||
UpdatePostUseCase,
|
||||
)
|
||||
|
||||
@@ -26,4 +27,5 @@ __all__ = [
|
||||
"DeletePostUseCase",
|
||||
"ListPostsUseCase",
|
||||
"PublishPostUseCase",
|
||||
"TogglePostLikeUseCase",
|
||||
]
|
||||
|
||||
@@ -100,3 +100,4 @@ class PostResponseDTO:
|
||||
tags: list[str]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
like_count: int = 0
|
||||
|
||||
@@ -9,6 +9,7 @@ from app.application.use_cases.delete_post import DeletePostUseCase
|
||||
from app.application.use_cases.get_post import GetPostUseCase
|
||||
from app.application.use_cases.list_posts import ListPostsUseCase
|
||||
from app.application.use_cases.publish_post import PublishPostUseCase
|
||||
from app.application.use_cases.toggle_like import TogglePostLikeUseCase
|
||||
from app.application.use_cases.update_post import UpdatePostUseCase
|
||||
|
||||
__all__ = [
|
||||
@@ -18,4 +19,5 @@ __all__ = [
|
||||
"DeletePostUseCase",
|
||||
"ListPostsUseCase",
|
||||
"PublishPostUseCase",
|
||||
"TogglePostLikeUseCase",
|
||||
]
|
||||
|
||||
102
app/application/use_cases/toggle_like.py
Normal file
102
app/application/use_cases/toggle_like.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Toggle post like use case.
|
||||
|
||||
This module implements the use case for toggling likes on blog posts.
|
||||
If the user already liked the post, the like is removed (unlike).
|
||||
If not, a new like is added.
|
||||
"""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from app.application.dtos.post import PostResponseDTO
|
||||
from app.application.interfaces import TransactionManager
|
||||
from app.domain.entities import Post
|
||||
from app.domain.entities.like import PostLike
|
||||
from app.domain.exceptions import NotFoundException
|
||||
from app.domain.repositories import PostRepository
|
||||
|
||||
|
||||
class TogglePostLikeUseCase:
|
||||
"""Use case for toggling a like on a blog post.
|
||||
|
||||
Handles like/unlike toggle logic. If the user or device has already
|
||||
liked the post, the like is removed. Otherwise, a new like is created.
|
||||
|
||||
Attributes:
|
||||
_post_repo: Repository for post and like data access.
|
||||
_tx_manager: Transaction manager for commit control.
|
||||
|
||||
Example:
|
||||
>>> use_case = TogglePostLikeUseCase(post_repo, tx_manager)
|
||||
>>> result = await use_case.execute("my-post-slug", "user-123")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
post_repo: PostRepository,
|
||||
tx_manager: TransactionManager,
|
||||
) -> None:
|
||||
"""Initialize use case with dependencies.
|
||||
|
||||
Args:
|
||||
post_repo: Repository for post and like operations.
|
||||
tx_manager: Transaction manager instance.
|
||||
"""
|
||||
self._post_repo = post_repo
|
||||
self._tx_manager = tx_manager
|
||||
|
||||
async def execute(self, post_id: UUID, liked_by: str) -> PostResponseDTO:
|
||||
"""Toggle like on a post.
|
||||
|
||||
If the user/device already liked the post, remove the like.
|
||||
Otherwise, add a new like.
|
||||
|
||||
Args:
|
||||
post_id: UUID of the post to toggle like on.
|
||||
liked_by: User ID or device identifier.
|
||||
|
||||
Returns:
|
||||
PostResponseDTO with updated like_count.
|
||||
|
||||
Raises:
|
||||
NotFoundException: If post with given ID does not exist.
|
||||
"""
|
||||
post = await self._post_repo.get_by_id(post_id)
|
||||
if not post:
|
||||
raise NotFoundException(f"Post with id '{post_id}' not found")
|
||||
|
||||
existing_like = await self._post_repo.get_like(post_id, liked_by)
|
||||
|
||||
if existing_like:
|
||||
await self._post_repo.remove_like(post_id, liked_by)
|
||||
post.like_count = max(0, post.like_count - 1)
|
||||
else:
|
||||
new_like = PostLike(post_id=post_id, liked_by=liked_by)
|
||||
await self._post_repo.add_like(new_like)
|
||||
post.like_count += 1
|
||||
|
||||
await self._post_repo.update(post)
|
||||
await self._tx_manager.commit()
|
||||
|
||||
return self._map_to_dto(post)
|
||||
|
||||
def _map_to_dto(self, post: Post) -> PostResponseDTO:
|
||||
"""Map domain entity to response DTO.
|
||||
|
||||
Args:
|
||||
post: Domain post entity.
|
||||
|
||||
Returns:
|
||||
PostResponseDTO with all post attributes including like_count.
|
||||
"""
|
||||
return PostResponseDTO(
|
||||
id=post.id,
|
||||
title=post.title.value,
|
||||
content=post.content.value,
|
||||
slug=post.slug.value,
|
||||
author_id=post.author_id,
|
||||
published=post.published,
|
||||
like_count=post.like_count,
|
||||
tags=post.tags.copy(),
|
||||
created_at=post.created_at,
|
||||
updated_at=post.updated_at,
|
||||
)
|
||||
Reference in New Issue
Block a user