Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,6 @@ import logging
log = logging.getLogger(LOGGER_NAME("my_service"))
```

New logger names must be added to `alembic.ini` under the logger section.

## Testing

- Backend tests: `pdm run tests` (runs pytest in `backend/tests/`)
Expand Down
116 changes: 116 additions & 0 deletions backend/app/interfaces/task_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from abc import ABC, abstractmethod
from typing import List, Optional
from uuid import UUID

from app.schemas.task import TaskCreateRequest, TaskResponse, TaskUpdateRequest


class ITaskService(ABC):
"""
TaskService interface with task management methods
"""

@abstractmethod
async def create_task(self, task: TaskCreateRequest) -> TaskResponse:
"""
Create a new task

:param task: the task to be created
:type task: TaskCreateRequest
:return: the created task
:rtype: TaskResponse
:raises Exception: if task creation fails
"""
pass

@abstractmethod
async def get_task_by_id(self, task_id: UUID) -> TaskResponse:
"""
Get task associated with task_id

:param task_id: task's id
:type task_id: UUID
:return: a TaskResponse with task's information
:rtype: TaskResponse
:raises Exception: if task retrieval fails
"""
pass

@abstractmethod
async def get_all_tasks(
self,
status: Optional[str] = None,
priority: Optional[str] = None,
task_type: Optional[str] = None,
assignee_id: Optional[UUID] = None,
) -> List[TaskResponse]:
"""
Get all tasks with optional filters

:param status: filter by task status
:type status: str, optional
:param priority: filter by task priority
:type priority: str, optional
:param task_type: filter by task type
:type task_type: str, optional
:param assignee_id: filter by assignee
:type assignee_id: UUID, optional
:return: list of TaskResponse
:rtype: List[TaskResponse]
:raises Exception: if task retrieval fails
"""
pass

@abstractmethod
async def update_task(self, task_id: UUID, task_update: TaskUpdateRequest) -> TaskResponse:
"""
Update a task

:param task_id: task's id
:type task_id: UUID
:param task_update: the task updates
:type task_update: TaskUpdateRequest
:return: the updated task
:rtype: TaskResponse
:raises Exception: if task update fails
"""
pass

@abstractmethod
async def assign_task(self, task_id: UUID, assignee_id: UUID) -> TaskResponse:
"""
Assign a task to an admin user

:param task_id: task's id
:type task_id: UUID
:param assignee_id: admin user's id to assign
:type assignee_id: UUID
:return: the updated task
:rtype: TaskResponse
:raises Exception: if task assignment fails
"""
pass

@abstractmethod
async def complete_task(self, task_id: UUID) -> TaskResponse:
"""
Mark a task as completed

:param task_id: task's id
:type task_id: UUID
:return: the completed task
:rtype: TaskResponse
:raises Exception: if task completion fails
"""
pass

@abstractmethod
async def delete_task(self, task_id: UUID) -> None:
"""
Delete a task by task_id

:param task_id: task_id of task to be deleted
:type task_id: UUID
:raises Exception: if task deletion fails
"""
pass
75 changes: 75 additions & 0 deletions backend/app/models/Task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import uuid
from datetime import datetime
from enum import Enum as PyEnum

from sqlalchemy import Column, DateTime, ForeignKey
from sqlalchemy import Enum as SQLEnum
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship

from .Base import Base


class TaskType(str, PyEnum):
INTAKE_FORM_REVIEW = "intake_form_review"
VOLUNTEER_APP_REVIEW = "volunteer_app_review"
PROFILE_UPDATE = "profile_update"
MATCHING = "matching"


class TaskPriority(str, PyEnum):
NO_STATUS = "no_status"
LOW = "low"
MEDIUM = "medium"
HIGH = "high"


class TaskStatus(str, PyEnum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"


class Task(Base):
__tablename__ = "tasks"

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
participant_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
type = Column(
SQLEnum(
TaskType,
name="task_type_enum",
create_type=False,
values_callable=lambda enum_cls: [member.value for member in enum_cls],
),
nullable=False,
)
priority = Column(
SQLEnum(
TaskPriority,
name="task_priority_enum",
create_type=False,
values_callable=lambda enum_cls: [member.value for member in enum_cls],
),
nullable=False,
default=TaskPriority.NO_STATUS,
)
status = Column(
SQLEnum(
TaskStatus,
name="task_status_enum",
create_type=False,
values_callable=lambda enum_cls: [member.value for member in enum_cls],
),
nullable=False,
default=TaskStatus.PENDING,
)
assignee_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
start_date = Column(DateTime, nullable=False, default=datetime.utcnow)
end_date = Column(DateTime, nullable=True)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)

# Relationships
participant = relationship("User", foreign_keys=[participant_id], backref="participant_tasks")
assignee = relationship("User", foreign_keys=[assignee_id], backref="assigned_tasks")
5 changes: 5 additions & 0 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .RankingPreference import RankingPreference
from .Role import Role
from .SuggestedTime import suggested_times
from .Task import Task, TaskPriority, TaskStatus, TaskType
from .TimeBlock import TimeBlock
from .Treatment import Treatment
from .User import FormStatus, User
Expand All @@ -43,6 +44,10 @@
"Form",
"FormSubmission",
"FormStatus",
"Task",
"TaskType",
"TaskPriority",
"TaskStatus",
]

log = logging.getLogger(LOGGER_NAME("models"))
Expand Down
163 changes: 163 additions & 0 deletions backend/app/routes/task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
from typing import Optional
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException, Query

from app.middleware.auth import has_roles
from app.schemas.task import (
TaskAssignRequest,
TaskCreateRequest,
TaskListResponse,
TaskResponse,
TaskUpdateRequest,
)
from app.schemas.user import UserRole
from app.services.implementations.task_service import TaskService
from app.utilities.service_utils import get_task_service

router = APIRouter(
prefix="/tasks",
tags=["tasks"],
)


@router.post("/", response_model=TaskResponse)
async def create_task(
task: TaskCreateRequest,
task_service: TaskService = Depends(get_task_service),
authorized: bool = has_roles([UserRole.ADMIN]),
):
"""
Create a new task (admin only).
This endpoint can also be called internally from other services to automatically create tasks.
"""
try:
return await task_service.create_task(task)
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.get("/", response_model=TaskListResponse)
async def get_tasks(
status: Optional[str] = Query(None, description="Filter by task status"),
priority: Optional[str] = Query(None, description="Filter by task priority"),
task_type: Optional[str] = Query(None, description="Filter by task type"),
assignee_id: Optional[str] = Query(None, description="Filter by assignee ID"),
task_service: TaskService = Depends(get_task_service),
authorized: bool = has_roles([UserRole.ADMIN]),
):
"""
Get all tasks with optional filters (admin only)
"""
try:
assignee_uuid = UUID(assignee_id) if assignee_id else None
tasks = await task_service.get_all_tasks(
status=status, priority=priority, task_type=task_type, assignee_id=assignee_uuid
)
return TaskListResponse(tasks=tasks, total=len(tasks))
except ValueError:
raise HTTPException(status_code=400, detail="Invalid assignee_id format")
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.get("/{task_id}", response_model=TaskResponse)
async def get_task(
task_id: str,
task_service: TaskService = Depends(get_task_service),
authorized: bool = has_roles([UserRole.ADMIN]),
):
"""
Get a single task by ID (admin only)
"""
try:
return await task_service.get_task_by_id(UUID(task_id))
except ValueError:
raise HTTPException(status_code=400, detail="Invalid task ID format")
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.put("/{task_id}", response_model=TaskResponse)
async def update_task(
task_id: str,
task_update: TaskUpdateRequest,
task_service: TaskService = Depends(get_task_service),
authorized: bool = has_roles([UserRole.ADMIN]),
):
"""
Update a task (admin only)
"""
try:
return await task_service.update_task(UUID(task_id), task_update)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid task ID format")
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.put("/{task_id}/assign", response_model=TaskResponse)
async def assign_task(
task_id: str,
assign_request: TaskAssignRequest,
task_service: TaskService = Depends(get_task_service),
authorized: bool = has_roles([UserRole.ADMIN]),
):
"""
Assign a task to an admin user (admin only)
"""
try:
return await task_service.assign_task(UUID(task_id), assign_request.assignee_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid task ID format")
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.put("/{task_id}/complete", response_model=TaskResponse)
async def complete_task(
task_id: str,
task_service: TaskService = Depends(get_task_service),
authorized: bool = has_roles([UserRole.ADMIN]),
):
"""
Mark a task as completed (admin only)
"""
try:
return await task_service.complete_task(UUID(task_id))
except ValueError:
raise HTTPException(status_code=400, detail="Invalid task ID format")
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.delete("/{task_id}")
async def delete_task(
task_id: str,
task_service: TaskService = Depends(get_task_service),
authorized: bool = has_roles([UserRole.ADMIN]),
):
"""
Delete a task (admin only)
"""
try:
await task_service.delete_task(UUID(task_id))
return {"message": "Task deleted successfully"}
except ValueError:
raise HTTPException(status_code=400, detail="Invalid task ID format")
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Loading