Skip to content

Commit 27d6155

Browse files
authored
added backend task services and endpoints (#61)
## Notion ticket link <!-- Please replace with your ticket's URL --> [Admin Dash - Task Pages](https://www.notion.so/uwblueprintexecs/Admin-Dash-Tasks-Pages-27210f3fb1dc80a2a08ef9f3693429c8) <!-- Give a quick summary of the implementation details, provide design justifications if necessary --> ## Implementation description * Added service to assign, create and list tasks <!-- What should the reviewer do to verify your changes? Describe expected results and include screenshots when appropriate --> ## Steps to test 1. Make a user, try the endpoints <!-- Draw attention to the substantial parts of your PR or anything you'd like a second opinion on --> ## What should reviewers focus on? * Proper permission logic when assigning tasks (Should be for admins only) * Take a look at the current email verified logic * Make sure only admins can access certain endpoints ## Checklist - [x] My PR name is descriptive and in imperative tense - [x] My commit messages are descriptive and in imperative tense. My commits are atomic and trivial commits are squashed or fixup'd into non-trivial commits - [x] I have run the appropriate linter(s) - [x] I have requested a review from the PL, as well as other devs who have background knowledge on this PR or who will be building on top of this PR
1 parent f33269e commit 27d6155

File tree

10 files changed

+788
-3
lines changed

10 files changed

+788
-3
lines changed

CLAUDE.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,6 @@ import logging
157157
log = logging.getLogger(LOGGER_NAME("my_service"))
158158
```
159159

160-
New logger names must be added to `alembic.ini` under the logger section.
161-
162160
## Testing
163161

164162
- Backend tests: `pdm run tests` (runs pytest in `backend/tests/`)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from abc import ABC, abstractmethod
2+
from typing import List, Optional
3+
from uuid import UUID
4+
5+
from app.schemas.task import TaskCreateRequest, TaskResponse, TaskUpdateRequest
6+
7+
8+
class ITaskService(ABC):
9+
"""
10+
TaskService interface with task management methods
11+
"""
12+
13+
@abstractmethod
14+
async def create_task(self, task: TaskCreateRequest) -> TaskResponse:
15+
"""
16+
Create a new task
17+
18+
:param task: the task to be created
19+
:type task: TaskCreateRequest
20+
:return: the created task
21+
:rtype: TaskResponse
22+
:raises Exception: if task creation fails
23+
"""
24+
pass
25+
26+
@abstractmethod
27+
async def get_task_by_id(self, task_id: UUID) -> TaskResponse:
28+
"""
29+
Get task associated with task_id
30+
31+
:param task_id: task's id
32+
:type task_id: UUID
33+
:return: a TaskResponse with task's information
34+
:rtype: TaskResponse
35+
:raises Exception: if task retrieval fails
36+
"""
37+
pass
38+
39+
@abstractmethod
40+
async def get_all_tasks(
41+
self,
42+
status: Optional[str] = None,
43+
priority: Optional[str] = None,
44+
task_type: Optional[str] = None,
45+
assignee_id: Optional[UUID] = None,
46+
) -> List[TaskResponse]:
47+
"""
48+
Get all tasks with optional filters
49+
50+
:param status: filter by task status
51+
:type status: str, optional
52+
:param priority: filter by task priority
53+
:type priority: str, optional
54+
:param task_type: filter by task type
55+
:type task_type: str, optional
56+
:param assignee_id: filter by assignee
57+
:type assignee_id: UUID, optional
58+
:return: list of TaskResponse
59+
:rtype: List[TaskResponse]
60+
:raises Exception: if task retrieval fails
61+
"""
62+
pass
63+
64+
@abstractmethod
65+
async def update_task(self, task_id: UUID, task_update: TaskUpdateRequest) -> TaskResponse:
66+
"""
67+
Update a task
68+
69+
:param task_id: task's id
70+
:type task_id: UUID
71+
:param task_update: the task updates
72+
:type task_update: TaskUpdateRequest
73+
:return: the updated task
74+
:rtype: TaskResponse
75+
:raises Exception: if task update fails
76+
"""
77+
pass
78+
79+
@abstractmethod
80+
async def assign_task(self, task_id: UUID, assignee_id: UUID) -> TaskResponse:
81+
"""
82+
Assign a task to an admin user
83+
84+
:param task_id: task's id
85+
:type task_id: UUID
86+
:param assignee_id: admin user's id to assign
87+
:type assignee_id: UUID
88+
:return: the updated task
89+
:rtype: TaskResponse
90+
:raises Exception: if task assignment fails
91+
"""
92+
pass
93+
94+
@abstractmethod
95+
async def complete_task(self, task_id: UUID) -> TaskResponse:
96+
"""
97+
Mark a task as completed
98+
99+
:param task_id: task's id
100+
:type task_id: UUID
101+
:return: the completed task
102+
:rtype: TaskResponse
103+
:raises Exception: if task completion fails
104+
"""
105+
pass
106+
107+
@abstractmethod
108+
async def delete_task(self, task_id: UUID) -> None:
109+
"""
110+
Delete a task by task_id
111+
112+
:param task_id: task_id of task to be deleted
113+
:type task_id: UUID
114+
:raises Exception: if task deletion fails
115+
"""
116+
pass

backend/app/models/Task.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import uuid
2+
from datetime import datetime
3+
from enum import Enum as PyEnum
4+
5+
from sqlalchemy import Column, DateTime, ForeignKey
6+
from sqlalchemy import Enum as SQLEnum
7+
from sqlalchemy.dialects.postgresql import UUID
8+
from sqlalchemy.orm import relationship
9+
10+
from .Base import Base
11+
12+
13+
class TaskType(str, PyEnum):
14+
INTAKE_FORM_REVIEW = "intake_form_review"
15+
VOLUNTEER_APP_REVIEW = "volunteer_app_review"
16+
PROFILE_UPDATE = "profile_update"
17+
MATCHING = "matching"
18+
19+
20+
class TaskPriority(str, PyEnum):
21+
NO_STATUS = "no_status"
22+
LOW = "low"
23+
MEDIUM = "medium"
24+
HIGH = "high"
25+
26+
27+
class TaskStatus(str, PyEnum):
28+
PENDING = "pending"
29+
IN_PROGRESS = "in_progress"
30+
COMPLETED = "completed"
31+
32+
33+
class Task(Base):
34+
__tablename__ = "tasks"
35+
36+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
37+
participant_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
38+
type = Column(
39+
SQLEnum(
40+
TaskType,
41+
name="task_type_enum",
42+
create_type=False,
43+
values_callable=lambda enum_cls: [member.value for member in enum_cls],
44+
),
45+
nullable=False,
46+
)
47+
priority = Column(
48+
SQLEnum(
49+
TaskPriority,
50+
name="task_priority_enum",
51+
create_type=False,
52+
values_callable=lambda enum_cls: [member.value for member in enum_cls],
53+
),
54+
nullable=False,
55+
default=TaskPriority.NO_STATUS,
56+
)
57+
status = Column(
58+
SQLEnum(
59+
TaskStatus,
60+
name="task_status_enum",
61+
create_type=False,
62+
values_callable=lambda enum_cls: [member.value for member in enum_cls],
63+
),
64+
nullable=False,
65+
default=TaskStatus.PENDING,
66+
)
67+
assignee_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
68+
start_date = Column(DateTime, nullable=False, default=datetime.utcnow)
69+
end_date = Column(DateTime, nullable=True)
70+
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
71+
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
72+
73+
# Relationships
74+
participant = relationship("User", foreign_keys=[participant_id], backref="participant_tasks")
75+
assignee = relationship("User", foreign_keys=[assignee_id], backref="assigned_tasks")

backend/app/models/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .RankingPreference import RankingPreference
2020
from .Role import Role
2121
from .SuggestedTime import suggested_times
22+
from .Task import Task, TaskPriority, TaskStatus, TaskType
2223
from .TimeBlock import TimeBlock
2324
from .Treatment import Treatment
2425
from .User import FormStatus, User
@@ -43,6 +44,10 @@
4344
"Form",
4445
"FormSubmission",
4546
"FormStatus",
47+
"Task",
48+
"TaskType",
49+
"TaskPriority",
50+
"TaskStatus",
4651
]
4752

4853
log = logging.getLogger(LOGGER_NAME("models"))

backend/app/routes/task.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
from typing import Optional
2+
from uuid import UUID
3+
4+
from fastapi import APIRouter, Depends, HTTPException, Query
5+
6+
from app.middleware.auth import has_roles
7+
from app.schemas.task import (
8+
TaskAssignRequest,
9+
TaskCreateRequest,
10+
TaskListResponse,
11+
TaskResponse,
12+
TaskUpdateRequest,
13+
)
14+
from app.schemas.user import UserRole
15+
from app.services.implementations.task_service import TaskService
16+
from app.utilities.service_utils import get_task_service
17+
18+
router = APIRouter(
19+
prefix="/tasks",
20+
tags=["tasks"],
21+
)
22+
23+
24+
@router.post("/", response_model=TaskResponse)
25+
async def create_task(
26+
task: TaskCreateRequest,
27+
task_service: TaskService = Depends(get_task_service),
28+
authorized: bool = has_roles([UserRole.ADMIN]),
29+
):
30+
"""
31+
Create a new task (admin only).
32+
This endpoint can also be called internally from other services to automatically create tasks.
33+
"""
34+
try:
35+
return await task_service.create_task(task)
36+
except HTTPException as http_ex:
37+
raise http_ex
38+
except Exception as e:
39+
raise HTTPException(status_code=500, detail=str(e))
40+
41+
42+
@router.get("/", response_model=TaskListResponse)
43+
async def get_tasks(
44+
status: Optional[str] = Query(None, description="Filter by task status"),
45+
priority: Optional[str] = Query(None, description="Filter by task priority"),
46+
task_type: Optional[str] = Query(None, description="Filter by task type"),
47+
assignee_id: Optional[str] = Query(None, description="Filter by assignee ID"),
48+
task_service: TaskService = Depends(get_task_service),
49+
authorized: bool = has_roles([UserRole.ADMIN]),
50+
):
51+
"""
52+
Get all tasks with optional filters (admin only)
53+
"""
54+
try:
55+
assignee_uuid = UUID(assignee_id) if assignee_id else None
56+
tasks = await task_service.get_all_tasks(
57+
status=status, priority=priority, task_type=task_type, assignee_id=assignee_uuid
58+
)
59+
return TaskListResponse(tasks=tasks, total=len(tasks))
60+
except ValueError:
61+
raise HTTPException(status_code=400, detail="Invalid assignee_id format")
62+
except HTTPException as http_ex:
63+
raise http_ex
64+
except Exception as e:
65+
raise HTTPException(status_code=500, detail=str(e))
66+
67+
68+
@router.get("/{task_id}", response_model=TaskResponse)
69+
async def get_task(
70+
task_id: str,
71+
task_service: TaskService = Depends(get_task_service),
72+
authorized: bool = has_roles([UserRole.ADMIN]),
73+
):
74+
"""
75+
Get a single task by ID (admin only)
76+
"""
77+
try:
78+
return await task_service.get_task_by_id(UUID(task_id))
79+
except ValueError:
80+
raise HTTPException(status_code=400, detail="Invalid task ID format")
81+
except HTTPException as http_ex:
82+
raise http_ex
83+
except Exception as e:
84+
raise HTTPException(status_code=500, detail=str(e))
85+
86+
87+
@router.put("/{task_id}", response_model=TaskResponse)
88+
async def update_task(
89+
task_id: str,
90+
task_update: TaskUpdateRequest,
91+
task_service: TaskService = Depends(get_task_service),
92+
authorized: bool = has_roles([UserRole.ADMIN]),
93+
):
94+
"""
95+
Update a task (admin only)
96+
"""
97+
try:
98+
return await task_service.update_task(UUID(task_id), task_update)
99+
except ValueError:
100+
raise HTTPException(status_code=400, detail="Invalid task ID format")
101+
except HTTPException as http_ex:
102+
raise http_ex
103+
except Exception as e:
104+
raise HTTPException(status_code=500, detail=str(e))
105+
106+
107+
@router.put("/{task_id}/assign", response_model=TaskResponse)
108+
async def assign_task(
109+
task_id: str,
110+
assign_request: TaskAssignRequest,
111+
task_service: TaskService = Depends(get_task_service),
112+
authorized: bool = has_roles([UserRole.ADMIN]),
113+
):
114+
"""
115+
Assign a task to an admin user (admin only)
116+
"""
117+
try:
118+
return await task_service.assign_task(UUID(task_id), assign_request.assignee_id)
119+
except ValueError:
120+
raise HTTPException(status_code=400, detail="Invalid task ID format")
121+
except HTTPException as http_ex:
122+
raise http_ex
123+
except Exception as e:
124+
raise HTTPException(status_code=500, detail=str(e))
125+
126+
127+
@router.put("/{task_id}/complete", response_model=TaskResponse)
128+
async def complete_task(
129+
task_id: str,
130+
task_service: TaskService = Depends(get_task_service),
131+
authorized: bool = has_roles([UserRole.ADMIN]),
132+
):
133+
"""
134+
Mark a task as completed (admin only)
135+
"""
136+
try:
137+
return await task_service.complete_task(UUID(task_id))
138+
except ValueError:
139+
raise HTTPException(status_code=400, detail="Invalid task ID format")
140+
except HTTPException as http_ex:
141+
raise http_ex
142+
except Exception as e:
143+
raise HTTPException(status_code=500, detail=str(e))
144+
145+
146+
@router.delete("/{task_id}")
147+
async def delete_task(
148+
task_id: str,
149+
task_service: TaskService = Depends(get_task_service),
150+
authorized: bool = has_roles([UserRole.ADMIN]),
151+
):
152+
"""
153+
Delete a task (admin only)
154+
"""
155+
try:
156+
await task_service.delete_task(UUID(task_id))
157+
return {"message": "Task deleted successfully"}
158+
except ValueError:
159+
raise HTTPException(status_code=400, detail="Invalid task ID format")
160+
except HTTPException as http_ex:
161+
raise http_ex
162+
except Exception as e:
163+
raise HTTPException(status_code=500, detail=str(e))

0 commit comments

Comments
 (0)