Files
TimeTracker/app/services/comment_service.py
T
Dries Peeters b4486a627f fix: CI tests, code quality, and duplicate DB indexes
- Webhook models: remove duplicate index definitions so db.create_all()
  no longer raises 'index already exists' (columns already have index=True)
- ImportService: fix circular import by late-importing ClientService,
  ProjectService, TimeTrackingService in __init__
- reports: fix F823 by renaming unpack variable _ to _entry_count to avoid
  shadowing gettext _ in export_task_excel()
- Code quality: add .flake8 with extend-ignore so flake8 CI passes;
  simplify pyproject.toml isort config (drop unsupported options)
- Format: run black and isort on app/
- tests: restore minimal app fixture in test_import_export_models
2026-03-15 10:51:52 +01:00

150 lines
5.3 KiB
Python

"""
Service for comment business logic.
"""
from typing import Any, Dict, List, Optional
from app import db
from app.models import Comment, Project, Task
from app.repositories import CommentRepository, ProjectRepository, TaskRepository
from app.utils.db import safe_commit
from app.utils.event_bus import emit_event
class CommentService:
"""Service for comment operations"""
def __init__(self):
self.comment_repo = CommentRepository()
self.project_repo = ProjectRepository()
self.task_repo = TaskRepository()
def create_comment(
self,
content: str,
user_id: int,
project_id: Optional[int] = None,
task_id: Optional[int] = None,
quote_id: Optional[int] = None,
parent_id: Optional[int] = None,
is_internal: bool = True,
) -> Dict[str, Any]:
"""
Create a new comment.
Returns:
dict with 'success', 'message', and 'comment' keys
"""
# Validate content
if not content or not content.strip():
return {"success": False, "message": "Comment content cannot be empty", "error": "empty_content"}
# Validate target
targets = [x for x in [project_id, task_id, quote_id] if x is not None]
if len(targets) == 0:
return {
"success": False,
"message": "Comment must be associated with a project, task, or quote",
"error": "no_target",
}
if len(targets) > 1:
return {
"success": False,
"message": "Comment cannot be associated with multiple targets",
"error": "multiple_targets",
}
# Validate target exists
if project_id:
project = self.project_repo.get_by_id(project_id)
if not project:
return {"success": False, "message": "Project not found", "error": "invalid_project"}
elif task_id:
task = self.task_repo.get_by_id(task_id)
if not task:
return {"success": False, "message": "Task not found", "error": "invalid_task"}
# Validate parent comment if reply
if parent_id:
parent = self.comment_repo.get_by_id(parent_id)
if not parent:
return {"success": False, "message": "Parent comment not found", "error": "invalid_parent"}
# Verify parent is for same target
if (
(project_id and parent.project_id != project_id)
or (task_id and parent.task_id != task_id)
or (quote_id and parent.quote_id != quote_id)
):
return {"success": False, "message": "Invalid parent comment", "error": "invalid_parent_target"}
# Create comment
comment = self.comment_repo.create(
content=content.strip(),
user_id=user_id,
project_id=project_id,
task_id=task_id,
quote_id=quote_id,
parent_id=parent_id,
is_internal=is_internal,
)
if not safe_commit("create_comment", {"user_id": user_id}):
return {
"success": False,
"message": "Could not create comment due to a database error",
"error": "database_error",
}
# Emit domain event
emit_event(
"comment.created",
{
"comment_id": comment.id,
"user_id": user_id,
"project_id": project_id,
"task_id": task_id,
"quote_id": quote_id,
},
)
return {"success": True, "message": "Comment created successfully", "comment": comment}
def get_project_comments(self, project_id: int, include_replies: bool = True) -> List[Comment]:
"""Get comments for a project"""
return self.comment_repo.get_by_project(
project_id=project_id, include_replies=include_replies, include_relations=True
)
def get_task_comments(self, task_id: int, include_replies: bool = True) -> List[Comment]:
"""Get comments for a task"""
return self.comment_repo.get_by_task(task_id=task_id, include_replies=include_replies, include_relations=True)
def delete_comment(self, comment_id: int, user_id: int) -> Dict[str, Any]:
"""
Delete a comment.
Returns:
dict with 'success' and 'message' keys
"""
comment = self.comment_repo.get_by_id(comment_id)
if not comment:
return {"success": False, "message": "Comment not found", "error": "not_found"}
# Check permissions (user can only delete their own comments unless admin)
from flask_login import current_user
if comment.user_id != user_id and not (hasattr(current_user, "is_admin") and current_user.is_admin):
return {
"success": False,
"message": "You do not have permission to delete this comment",
"error": "unauthorized",
}
if self.comment_repo.delete(comment):
if safe_commit("delete_comment", {"comment_id": comment_id, "user_id": user_id}):
return {"success": True, "message": "Comment deleted successfully"}
return {"success": False, "message": "Could not delete comment", "error": "database_error"}