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:
2026-05-10 18:24:09 +03:00
parent 4497f452a1
commit 3cf6c94da2
21 changed files with 876 additions and 6 deletions

View File

@@ -7,8 +7,8 @@ Models are used by repositories for data persistence.
from datetime import UTC, datetime
from uuid import uuid4
from sqlalchemy import JSON, Boolean, DateTime, String, Text
from sqlalchemy.orm import Mapped, declarative_base, mapped_column
from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, declarative_base, mapped_column, relationship
Base = declarative_base()
@@ -42,6 +42,10 @@ class PostORM(Base): # type: ignore[valid-type,misc]
slug: Mapped[str] = mapped_column(String(200), nullable=False, unique=True, index=True)
author_id: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
published: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
like_count: Mapped[int] = mapped_column(default=0, nullable=False)
likes: Mapped[list["PostLikeORM"]] = relationship(
back_populates="post", cascade="all, delete-orphan"
)
tags: Mapped[list[str]] = mapped_column(JSON, default=list)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
@@ -54,3 +58,32 @@ class PostORM(Base): # type: ignore[valid-type,misc]
onupdate=lambda: datetime.now(UTC),
nullable=False,
)
class PostLikeORM(Base): # type: ignore[valid-type,misc]
"""SQLAlchemy model for PostLike.
Database table representation of post likes.
Maps to the 'post_likes' table tracking which users/devices liked which posts.
Attributes:
id: Primary key as UUID string.
post_id: Foreign key to the liked post.
liked_by: User ID or device identifier.
created_at: Creation timestamp.
"""
__tablename__ = "post_likes"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
post_id: Mapped[str] = mapped_column(
String(36), ForeignKey("posts.id", ondelete="CASCADE"), nullable=False, index=True
)
liked_by: Mapped[str] = mapped_column(String(200), nullable=False, index=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.now(UTC),
nullable=False,
)
post: Mapped[PostORM] = relationship(back_populates="likes")