Added comment functionality

This commit is contained in:
gulimabr
2025-12-02 10:28:48 -03:00
parent e152c07f65
commit 0e30db3c18
9 changed files with 1041 additions and 10 deletions

View File

@@ -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()