Added comment functionality
This commit is contained in:
@@ -60,6 +60,8 @@ class User(Base):
|
||||
back_populates="members"
|
||||
)
|
||||
created_links: Mapped[List["RequirementLink"]] = relationship("RequirementLink", back_populates="creator")
|
||||
comments: Mapped[List["RequirementComment"]] = relationship("RequirementComment", back_populates="user")
|
||||
comment_replies: Mapped[List["RequirementCommentReply"]] = relationship("RequirementCommentReply", back_populates="user")
|
||||
|
||||
|
||||
class Tag(Base):
|
||||
@@ -225,6 +227,11 @@ class Requirement(Base):
|
||||
foreign_keys="RequirementLink.target_req_id",
|
||||
back_populates="target_requirement"
|
||||
)
|
||||
comments: Mapped[List["RequirementComment"]] = relationship(
|
||||
"RequirementComment",
|
||||
back_populates="requirement",
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
@@ -386,3 +393,92 @@ class RequirementLink(Base):
|
||||
Index("idx_link_target", "target_req_id"),
|
||||
UniqueConstraint("source_req_id", "target_req_id", "relationship_type_id", name="uq_req_link_pair"),
|
||||
)
|
||||
|
||||
|
||||
class RequirementComment(Base):
|
||||
"""
|
||||
Top-level comments on a requirement.
|
||||
Supports soft delete to preserve comment threads.
|
||||
"""
|
||||
__tablename__ = "requirement_comments"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
requirement_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("requirements.id", ondelete="CASCADE"),
|
||||
nullable=False
|
||||
)
|
||||
user_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True
|
||||
)
|
||||
comment_text: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=datetime.utcnow,
|
||||
nullable=True
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=datetime.utcnow,
|
||||
onupdate=datetime.utcnow,
|
||||
nullable=True
|
||||
)
|
||||
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
# Relationships
|
||||
requirement: Mapped["Requirement"] = relationship("Requirement", back_populates="comments")
|
||||
user: Mapped[Optional["User"]] = relationship("User", back_populates="comments")
|
||||
replies: Mapped[List["RequirementCommentReply"]] = relationship(
|
||||
"RequirementCommentReply",
|
||||
back_populates="parent_comment",
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index("idx_rc_req", "requirement_id"),
|
||||
)
|
||||
|
||||
|
||||
class RequirementCommentReply(Base):
|
||||
"""
|
||||
Replies to top-level comments.
|
||||
Only links to requirement_comments, NOT to itself - enforces 1-level nesting.
|
||||
"""
|
||||
__tablename__ = "requirement_comment_replies"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
parent_comment_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("requirement_comments.id", ondelete="CASCADE"),
|
||||
nullable=False
|
||||
)
|
||||
user_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True
|
||||
)
|
||||
reply_text: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=datetime.utcnow,
|
||||
nullable=True
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=datetime.utcnow,
|
||||
onupdate=datetime.utcnow,
|
||||
nullable=True
|
||||
)
|
||||
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
# Relationships
|
||||
parent_comment: Mapped["RequirementComment"] = relationship("RequirementComment", back_populates="replies")
|
||||
user: Mapped[Optional["User"]] = relationship("User", back_populates="comment_replies")
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index("idx_rcr_parent", "parent_comment_id"),
|
||||
)
|
||||
|
||||
@@ -13,7 +13,8 @@ from src.models import (
|
||||
ValidationStatusResponse, ValidationHistoryResponse, ValidationCreateRequest,
|
||||
RelationshipTypeResponse, RelationshipTypeCreateRequest, RelationshipTypeUpdateRequest,
|
||||
RequirementLinkResponse, RequirementLinkCreateRequest, RequirementSearchResult,
|
||||
RoleResponse, ProjectMemberResponse, UserRoleUpdateRequest, ROLE_DISPLAY_NAMES
|
||||
RoleResponse, ProjectMemberResponse, UserRoleUpdateRequest, ROLE_DISPLAY_NAMES,
|
||||
CommentResponse, CommentReplyResponse, CommentCreateRequest, ReplyCreateRequest
|
||||
)
|
||||
from src.controller import AuthController
|
||||
from src.config import get_openid, get_settings
|
||||
@@ -21,7 +22,7 @@ from src.database import init_db, close_db, get_db
|
||||
from src.repositories import (
|
||||
RoleRepository, GroupRepository, TagRepository, RequirementRepository,
|
||||
PriorityRepository, ProjectRepository, ValidationStatusRepository, ValidationRepository,
|
||||
RelationshipTypeRepository, RequirementLinkRepository
|
||||
RelationshipTypeRepository, RequirementLinkRepository, CommentRepository, ReplyRepository
|
||||
)
|
||||
import logging
|
||||
|
||||
@@ -1472,3 +1473,278 @@ async def delete_requirement_link(
|
||||
|
||||
await link_repo.delete(link_id)
|
||||
await db.commit()
|
||||
|
||||
|
||||
# ===========================================
|
||||
# Comment Endpoints
|
||||
# ===========================================
|
||||
|
||||
def _build_comment_response(comment) -> CommentResponse:
|
||||
"""Helper function to build CommentResponse from a RequirementComment model."""
|
||||
return CommentResponse(
|
||||
id=comment.id,
|
||||
comment_text=comment.comment_text,
|
||||
created_at=comment.created_at,
|
||||
updated_at=comment.updated_at,
|
||||
author_id=comment.user_id,
|
||||
author_username=comment.user.username if comment.user else None,
|
||||
author_full_name=comment.user.full_name if comment.user else None,
|
||||
author_role=comment.user.role.role_name if comment.user and comment.user.role else None,
|
||||
replies=[
|
||||
CommentReplyResponse(
|
||||
id=reply.id,
|
||||
reply_text=reply.reply_text,
|
||||
created_at=reply.created_at,
|
||||
updated_at=reply.updated_at,
|
||||
author_id=reply.user_id,
|
||||
author_username=reply.user.username if reply.user else None,
|
||||
author_full_name=reply.user.full_name if reply.user else None,
|
||||
author_role=reply.user.role.role_name if reply.user and reply.user.role else None,
|
||||
)
|
||||
for reply in comment.replies
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/requirements/{requirement_id}/comments", response_model=List[CommentResponse])
|
||||
async def get_requirement_comments(
|
||||
requirement_id: int,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get all comments for a requirement with their replies.
|
||||
User must be a member of the requirement's project.
|
||||
|
||||
Args:
|
||||
requirement_id: The requirement ID
|
||||
|
||||
Returns:
|
||||
List of comments with nested replies.
|
||||
"""
|
||||
user = await _get_current_user_db(request, db)
|
||||
|
||||
# Check if requirement exists
|
||||
req_repo = RequirementRepository(db)
|
||||
requirement = await req_repo.get_by_id(requirement_id)
|
||||
if not requirement:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Requirement with id {requirement_id} not found"
|
||||
)
|
||||
|
||||
await _verify_project_membership(requirement.project_id, user.id, db)
|
||||
|
||||
comment_repo = CommentRepository(db)
|
||||
comments = await comment_repo.get_comments_by_requirement_id(requirement_id)
|
||||
|
||||
return [_build_comment_response(c) for c in comments]
|
||||
|
||||
|
||||
@app.post("/api/requirements/{requirement_id}/comments", response_model=CommentResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_comment(
|
||||
requirement_id: int,
|
||||
request: Request,
|
||||
comment_data: CommentCreateRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Create a new comment on a requirement.
|
||||
Any project member can create comments.
|
||||
|
||||
Args:
|
||||
requirement_id: The requirement to comment on
|
||||
comment_data: The comment content
|
||||
|
||||
Returns:
|
||||
The created comment.
|
||||
"""
|
||||
user = await _get_current_user_db(request, db)
|
||||
|
||||
# Check if requirement exists
|
||||
req_repo = RequirementRepository(db)
|
||||
requirement = await req_repo.get_by_id(requirement_id)
|
||||
if not requirement:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Requirement with id {requirement_id} not found"
|
||||
)
|
||||
|
||||
await _verify_project_membership(requirement.project_id, user.id, db)
|
||||
|
||||
# Validate comment text
|
||||
if not comment_data.comment_text.strip():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Comment text cannot be empty"
|
||||
)
|
||||
|
||||
comment_repo = CommentRepository(db)
|
||||
comment = await comment_repo.create_comment(
|
||||
requirement_id=requirement_id,
|
||||
user_id=user.id,
|
||||
comment_text=comment_data.comment_text.strip()
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# Fetch the complete comment with user data
|
||||
comment = await comment_repo.get_comment_by_id(comment.id)
|
||||
|
||||
return _build_comment_response(comment)
|
||||
|
||||
|
||||
@app.delete("/api/comments/{comment_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_comment(
|
||||
comment_id: int,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Soft delete a comment (hides it from view).
|
||||
Only the comment author or an admin can delete.
|
||||
|
||||
Args:
|
||||
comment_id: The comment ID to delete
|
||||
"""
|
||||
user = await _get_current_user_db(request, db)
|
||||
|
||||
comment_repo = CommentRepository(db)
|
||||
comment = await comment_repo.get_comment_by_id(comment_id)
|
||||
|
||||
if not comment:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Comment with id {comment_id} not found"
|
||||
)
|
||||
|
||||
# Verify user is a member of the project
|
||||
req_repo = RequirementRepository(db)
|
||||
requirement = await req_repo.get_by_id(comment.requirement_id)
|
||||
await _verify_project_membership(requirement.project_id, user.id, db)
|
||||
|
||||
# Only author or admin can delete
|
||||
if comment.user_id != user.id and user.role_id != 3:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only the comment author or an admin can delete this comment"
|
||||
)
|
||||
|
||||
await comment_repo.soft_delete_comment(comment_id)
|
||||
await db.commit()
|
||||
|
||||
|
||||
@app.post("/api/comments/{comment_id}/replies", response_model=CommentReplyResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_reply(
|
||||
comment_id: int,
|
||||
request: Request,
|
||||
reply_data: ReplyCreateRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Create a reply to a comment.
|
||||
Any project member can create replies.
|
||||
|
||||
Args:
|
||||
comment_id: The parent comment ID
|
||||
reply_data: The reply content
|
||||
|
||||
Returns:
|
||||
The created reply.
|
||||
"""
|
||||
user = await _get_current_user_db(request, db)
|
||||
|
||||
# Check if parent comment exists
|
||||
comment_repo = CommentRepository(db)
|
||||
parent_comment = await comment_repo.get_comment_by_id(comment_id)
|
||||
|
||||
if not parent_comment:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Comment with id {comment_id} not found"
|
||||
)
|
||||
|
||||
if parent_comment.is_deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot reply to a deleted comment"
|
||||
)
|
||||
|
||||
# Verify user is a member of the project
|
||||
req_repo = RequirementRepository(db)
|
||||
requirement = await req_repo.get_by_id(parent_comment.requirement_id)
|
||||
await _verify_project_membership(requirement.project_id, user.id, db)
|
||||
|
||||
# Validate reply text
|
||||
if not reply_data.reply_text.strip():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Reply text cannot be empty"
|
||||
)
|
||||
|
||||
reply_repo = ReplyRepository(db)
|
||||
reply = await reply_repo.create_reply(
|
||||
parent_comment_id=comment_id,
|
||||
user_id=user.id,
|
||||
reply_text=reply_data.reply_text.strip()
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# Fetch the complete reply with user data
|
||||
reply = await reply_repo.get_reply_by_id(reply.id)
|
||||
|
||||
return CommentReplyResponse(
|
||||
id=reply.id,
|
||||
reply_text=reply.reply_text,
|
||||
created_at=reply.created_at,
|
||||
updated_at=reply.updated_at,
|
||||
author_id=reply.user_id,
|
||||
author_username=reply.user.username if reply.user else None,
|
||||
author_full_name=reply.user.full_name if reply.user else None,
|
||||
author_role=reply.user.role.role_name if reply.user and reply.user.role else None,
|
||||
)
|
||||
|
||||
|
||||
@app.delete("/api/replies/{reply_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_reply(
|
||||
reply_id: int,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Soft delete a reply (hides it from view).
|
||||
Only the reply author or an admin can delete.
|
||||
|
||||
Args:
|
||||
reply_id: The reply ID to delete
|
||||
"""
|
||||
user = await _get_current_user_db(request, db)
|
||||
|
||||
reply_repo = ReplyRepository(db)
|
||||
reply = await reply_repo.get_reply_by_id(reply_id)
|
||||
|
||||
if not reply:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Reply with id {reply_id} not found"
|
||||
)
|
||||
|
||||
# Get the parent comment to check project membership
|
||||
comment_repo = CommentRepository(db)
|
||||
parent_comment = await comment_repo.get_comment_by_id(reply.parent_comment_id)
|
||||
|
||||
# Verify user is a member of the project
|
||||
req_repo = RequirementRepository(db)
|
||||
requirement = await req_repo.get_by_id(parent_comment.requirement_id)
|
||||
await _verify_project_membership(requirement.project_id, user.id, db)
|
||||
|
||||
# Only author or admin can delete
|
||||
if reply.user_id != user.id and user.role_id != 3:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only the reply author or an admin can delete this reply"
|
||||
)
|
||||
|
||||
await reply_repo.soft_delete_reply(reply_id)
|
||||
await db.commit()
|
||||
|
||||
@@ -309,3 +309,45 @@ class RequirementSearchResult(BaseModel):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Comment schemas
|
||||
class CommentReplyResponse(BaseModel):
|
||||
"""Response schema for a comment reply."""
|
||||
id: int
|
||||
reply_text: str
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
author_id: Optional[int] = None
|
||||
author_username: Optional[str] = None
|
||||
author_full_name: Optional[str] = None
|
||||
author_role: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class CommentResponse(BaseModel):
|
||||
"""Response schema for a comment with its replies."""
|
||||
id: int
|
||||
comment_text: str
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
author_id: Optional[int] = None
|
||||
author_username: Optional[str] = None
|
||||
author_full_name: Optional[str] = None
|
||||
author_role: Optional[str] = None
|
||||
replies: List[CommentReplyResponse] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class CommentCreateRequest(BaseModel):
|
||||
"""Request schema for creating a comment."""
|
||||
comment_text: str
|
||||
|
||||
|
||||
class ReplyCreateRequest(BaseModel):
|
||||
"""Request schema for creating a reply."""
|
||||
reply_text: str
|
||||
|
||||
@@ -11,6 +11,7 @@ from src.repositories.validation_status_repository import ValidationStatusReposi
|
||||
from src.repositories.validation_repository import ValidationRepository
|
||||
from src.repositories.relationship_type_repository import RelationshipTypeRepository
|
||||
from src.repositories.requirement_link_repository import RequirementLinkRepository
|
||||
from src.repositories.comment_repository import CommentRepository, ReplyRepository
|
||||
|
||||
__all__ = [
|
||||
"UserRepository",
|
||||
@@ -24,4 +25,6 @@ __all__ = [
|
||||
"ValidationRepository",
|
||||
"RelationshipTypeRepository",
|
||||
"RequirementLinkRepository",
|
||||
"CommentRepository",
|
||||
"ReplyRepository",
|
||||
]
|
||||
|
||||
184
backend/src/repositories/comment_repository.py
Normal file
184
backend/src/repositories/comment_repository.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""
|
||||
Repository for Comment database operations.
|
||||
Handles CRUD operations for requirement comments and replies.
|
||||
"""
|
||||
from typing import List, Optional
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from src.db_models import RequirementComment, RequirementCommentReply, User
|
||||
|
||||
|
||||
class CommentRepository:
|
||||
"""Repository for comment CRUD operations."""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def create_comment(
|
||||
self,
|
||||
requirement_id: int,
|
||||
user_id: int,
|
||||
comment_text: str
|
||||
) -> RequirementComment:
|
||||
"""
|
||||
Create a new top-level comment on a requirement.
|
||||
|
||||
Args:
|
||||
requirement_id: The requirement being commented on
|
||||
user_id: The user creating the comment
|
||||
comment_text: The comment content
|
||||
"""
|
||||
comment = RequirementComment(
|
||||
requirement_id=requirement_id,
|
||||
user_id=user_id,
|
||||
comment_text=comment_text
|
||||
)
|
||||
self.db.add(comment)
|
||||
await self.db.flush()
|
||||
await self.db.refresh(comment)
|
||||
return comment
|
||||
|
||||
async def get_comment_by_id(self, comment_id: int) -> Optional[RequirementComment]:
|
||||
"""Get a comment by ID with related user data."""
|
||||
result = await self.db.execute(
|
||||
select(RequirementComment)
|
||||
.options(
|
||||
selectinload(RequirementComment.user).selectinload(User.role),
|
||||
selectinload(RequirementComment.replies).selectinload(RequirementCommentReply.user).selectinload(User.role)
|
||||
)
|
||||
.where(RequirementComment.id == comment_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_comments_by_requirement_id(self, requirement_id: int) -> List[RequirementComment]:
|
||||
"""
|
||||
Get all non-deleted comments for a requirement, ordered by creation date (oldest first).
|
||||
Includes replies (also filtered by is_deleted) and user data.
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(RequirementComment)
|
||||
.options(
|
||||
selectinload(RequirementComment.user).selectinload(User.role),
|
||||
selectinload(RequirementComment.replies).selectinload(RequirementCommentReply.user).selectinload(User.role)
|
||||
)
|
||||
.where(
|
||||
and_(
|
||||
RequirementComment.requirement_id == requirement_id,
|
||||
RequirementComment.is_deleted == False
|
||||
)
|
||||
)
|
||||
.order_by(RequirementComment.created_at.asc())
|
||||
)
|
||||
comments = list(result.scalars().all())
|
||||
|
||||
# Filter out deleted replies from each comment
|
||||
for comment in comments:
|
||||
comment.replies = [r for r in comment.replies if not r.is_deleted]
|
||||
# Sort replies by created_at
|
||||
comment.replies.sort(key=lambda r: r.created_at or r.id)
|
||||
|
||||
return comments
|
||||
|
||||
async def update_comment(
|
||||
self,
|
||||
comment_id: int,
|
||||
comment_text: str
|
||||
) -> Optional[RequirementComment]:
|
||||
"""Update a comment's text."""
|
||||
comment = await self.get_comment_by_id(comment_id)
|
||||
if not comment:
|
||||
return None
|
||||
|
||||
comment.comment_text = comment_text
|
||||
await self.db.flush()
|
||||
await self.db.refresh(comment)
|
||||
return comment
|
||||
|
||||
async def soft_delete_comment(self, comment_id: int) -> bool:
|
||||
"""
|
||||
Soft delete a comment by setting is_deleted to True.
|
||||
Returns True if deleted, False if not found.
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(RequirementComment).where(RequirementComment.id == comment_id)
|
||||
)
|
||||
comment = result.scalar_one_or_none()
|
||||
if comment:
|
||||
comment.is_deleted = True
|
||||
await self.db.flush()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ReplyRepository:
|
||||
"""Repository for comment reply CRUD operations."""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def create_reply(
|
||||
self,
|
||||
parent_comment_id: int,
|
||||
user_id: int,
|
||||
reply_text: str
|
||||
) -> RequirementCommentReply:
|
||||
"""
|
||||
Create a new reply to a comment.
|
||||
|
||||
Args:
|
||||
parent_comment_id: The parent comment being replied to
|
||||
user_id: The user creating the reply
|
||||
reply_text: The reply content
|
||||
"""
|
||||
reply = RequirementCommentReply(
|
||||
parent_comment_id=parent_comment_id,
|
||||
user_id=user_id,
|
||||
reply_text=reply_text
|
||||
)
|
||||
self.db.add(reply)
|
||||
await self.db.flush()
|
||||
await self.db.refresh(reply)
|
||||
return reply
|
||||
|
||||
async def get_reply_by_id(self, reply_id: int) -> Optional[RequirementCommentReply]:
|
||||
"""Get a reply by ID with related user and parent comment data."""
|
||||
result = await self.db.execute(
|
||||
select(RequirementCommentReply)
|
||||
.options(
|
||||
selectinload(RequirementCommentReply.user).selectinload(User.role),
|
||||
selectinload(RequirementCommentReply.parent_comment)
|
||||
)
|
||||
.where(RequirementCommentReply.id == reply_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def update_reply(
|
||||
self,
|
||||
reply_id: int,
|
||||
reply_text: str
|
||||
) -> Optional[RequirementCommentReply]:
|
||||
"""Update a reply's text."""
|
||||
reply = await self.get_reply_by_id(reply_id)
|
||||
if not reply:
|
||||
return None
|
||||
|
||||
reply.reply_text = reply_text
|
||||
await self.db.flush()
|
||||
await self.db.refresh(reply)
|
||||
return reply
|
||||
|
||||
async def soft_delete_reply(self, reply_id: int) -> bool:
|
||||
"""
|
||||
Soft delete a reply by setting is_deleted to True.
|
||||
Returns True if deleted, False if not found.
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(RequirementCommentReply).where(RequirementCommentReply.id == reply_id)
|
||||
)
|
||||
reply = result.scalar_one_or_none()
|
||||
if reply:
|
||||
reply.is_deleted = True
|
||||
await self.db.flush()
|
||||
return True
|
||||
return False
|
||||
Reference in New Issue
Block a user