Skip to content

Commit 02ba88d

Browse files
refactor: implement team isolation for task assignments (#240)
* refactor: implement team isolation for task assignments - Added `original_team_id` field to `TaskAssignmentModel` to track original team context - Modified `TaskAssignmentRepository.update_assignment()` to set `original_team_id` when reassigning from team to user - Updated `TaskRepository.list()` and `count()` to include team member tasks with proper team isolation - Team task lists (`/v1/tasks?teamId=...`) now include tasks assigned to team members - Tasks are filtered by `original_team_id` to prevent cross-team contamination - Users in multiple teams only see tasks relevant to each specific team context - Modified `TaskAssignmentDetailView.patch()` to use `update_assignment()` instead of `update_executor()` - This ensures `original_team_id` is properly set when UI calls `PATCH /v1/task-assignments/{task_id}` - Maintains backward compatibility with existing UI workflow - Added support for both ObjectId and string formats in MongoDB queries - Prevents data type mismatches in `original_team_id` filtering * chore: remove unnecessary comments from code * chore: remove redudant query and added a helper method and calling that helper method in list and count method * fix: added the originial_team_id in the create task assignment method * refactor: rename original_team_id to team_id in task assignment models and services - Updated CreateTaskAssignmentDTO and TaskAssignmentDTO to replace original_team_id with team_id for clarity. - Adjusted validation methods and repository logic to reflect the new field name. - Ensured consistency across task assignment service and repository for handling team assignments. * fix: ensure team_id is stored as ObjectId in task assignments --------- Co-authored-by: anuj.k <[email protected]>
1 parent 0cbd912 commit 02ba88d

File tree

7 files changed

+51
-17
lines changed

7 files changed

+51
-17
lines changed

todo/dto/task_assignment_dto.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class CreateTaskAssignmentDTO(BaseModel):
88
task_id: str
99
assignee_id: str
1010
user_type: Literal["user", "team"]
11+
team_id: Optional[str] = None
1112

1213
@validator("task_id")
1314
def validate_task_id(cls, value):
@@ -30,6 +31,13 @@ def validate_user_type(cls, value):
3031
raise ValueError("user_type must be either 'user' or 'team'")
3132
return value
3233

34+
@validator("team_id")
35+
def validate_team_id(cls, value):
36+
"""Validate that the original team ID is a valid ObjectId if provided."""
37+
if value is not None and not ObjectId.is_valid(value):
38+
raise ValueError(f"Invalid original team ID: {value}")
39+
return value
40+
3341

3442
class TaskAssignmentDTO(BaseModel):
3543
id: str
@@ -38,6 +46,7 @@ class TaskAssignmentDTO(BaseModel):
3846
assignee_name: Optional[str] = None
3947
user_type: Literal["user", "team"]
4048
executor_id: Optional[str] = None # User ID executing the task (for team assignments)
49+
team_id: Optional[str] = None
4150
is_active: bool
4251
created_by: str
4352
updated_by: Optional[str] = None

todo/models/task_assignment.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ class TaskAssignmentModel(Document):
2424
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
2525
updated_at: datetime | None = None
2626
executor_id: PyObjectId | None = None # User within the team who is executing the task
27+
team_id: PyObjectId | None = None # Track the original team when reassigned from team to user
2728

28-
@validator("task_id", "assignee_id", "created_by", "updated_by")
29+
@validator("task_id", "assignee_id", "created_by", "updated_by", "team_id")
2930
def validate_object_ids(cls, v):
3031
if v is None:
3132
return v

todo/repositories/task_assignment_repository.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Optional, List
33
from bson import ObjectId
44

5+
from todo.exceptions.task_exceptions import TaskNotFoundException
56
from todo.models.task_assignment import TaskAssignmentModel
67
from todo.repositories.common.mongo_repository import MongoRepository
78
from todo.models.common.pyobjectid import PyObjectId
@@ -72,6 +73,18 @@ def update_assignment(
7273
"""
7374
collection = cls.get_collection()
7475
try:
76+
current_assignment = cls.get_by_task_id(task_id)
77+
78+
if not current_assignment:
79+
raise TaskNotFoundException(task_id)
80+
81+
team_id = None
82+
83+
if user_type == "team":
84+
team_id = assignee_id
85+
elif user_type == "user" and current_assignment.team_id is not None:
86+
team_id = current_assignment.team_id
87+
7588
# Deactivate current assignment if exists (try both ObjectId and string)
7689
collection.update_many(
7790
{"task_id": ObjectId(task_id), "is_active": True},
@@ -102,6 +115,7 @@ def update_assignment(
102115
user_type=user_type,
103116
created_by=PyObjectId(user_id),
104117
updated_by=None,
118+
team_id=PyObjectId(team_id),
105119
)
106120

107121
return cls.create(new_assignment)

todo/repositories/task_repository.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,25 @@
22
from typing import List
33
from bson import ObjectId
44
from pymongo import ReturnDocument
5-
import logging
65

76
from todo.exceptions.task_exceptions import TaskNotFoundException
87
from todo.models.task import TaskModel
98
from todo.repositories.common.mongo_repository import MongoRepository
109
from todo.repositories.task_assignment_repository import TaskAssignmentRepository
1110
from todo.constants.messages import ApiErrors, RepositoryErrors
1211
from todo.constants.task import SORT_FIELD_PRIORITY, SORT_FIELD_ASSIGNEE, SORT_ORDER_DESC, TaskStatus
12+
from todo.repositories.team_repository import UserTeamDetailsRepository
1313

1414

1515
class TaskRepository(MongoRepository):
1616
collection_name = TaskModel.collection_name
1717

18+
@classmethod
19+
def _get_team_task_ids(cls, team_id: str) -> List[ObjectId]:
20+
team_tasks = TaskAssignmentRepository.get_collection().find({"team_id": team_id, "is_active": True})
21+
team_task_ids = [ObjectId(task["task_id"]) for task in team_tasks]
22+
return list(set(team_task_ids))
23+
1824
@classmethod
1925
def _build_status_filter(cls, status_filter: str = None) -> dict:
2026
now = datetime.now(timezone.utc)
@@ -60,17 +66,12 @@ def list(
6066
status_filter: str = None,
6167
) -> List[TaskModel]:
6268
tasks_collection = cls.get_collection()
63-
logger = logging.getLogger(__name__)
6469

6570
base_filter = cls._build_status_filter(status_filter)
6671

6772
if team_id:
68-
logger.debug(f"TaskRepository.list: team_id={team_id}")
69-
team_assignments = TaskAssignmentRepository.get_by_assignee_id(team_id, "team")
70-
team_task_ids = [assignment.task_id for assignment in team_assignments]
71-
logger.debug(f"TaskRepository.list: team_task_ids={team_task_ids}")
72-
query_filter = {"$and": [base_filter, {"_id": {"$in": team_task_ids}}]}
73-
logger.debug(f"TaskRepository.list: query_filter={query_filter}")
73+
all_team_task_ids = cls._get_team_task_ids(team_id)
74+
query_filter = {"$and": [base_filter, {"_id": {"$in": all_team_task_ids}}]}
7475
elif user_id:
7576
assigned_task_ids = cls._get_assigned_task_ids_for_user(user_id)
7677
query_filter = {"$and": [base_filter, {"_id": {"$in": assigned_task_ids}}]}
@@ -98,7 +99,7 @@ def _get_assigned_task_ids_for_user(cls, user_id: str) -> List[ObjectId]:
9899
direct_task_ids = [assignment.task_id for assignment in direct_assignments]
99100

100101
# Get teams where user is a member
101-
from todo.repositories.team_repository import UserTeamDetailsRepository, TeamRepository
102+
from todo.repositories.team_repository import TeamRepository
102103

103104
user_teams = UserTeamDetailsRepository.get_by_user_id(user_id)
104105
team_ids = [str(team.team_id) for team in user_teams]
@@ -128,9 +129,9 @@ def count(cls, user_id: str = None, team_id: str = None, status_filter: str = No
128129
base_filter = cls._build_status_filter(status_filter)
129130

130131
if team_id:
131-
team_assignments = TaskAssignmentRepository.get_by_assignee_id(team_id, "team")
132-
team_task_ids = [assignment.task_id for assignment in team_assignments]
133-
query_filter = {"$and": [base_filter, {"_id": {"$in": team_task_ids}}]}
132+
all_team_task_ids = cls._get_team_task_ids(team_id)
133+
query_filter = {"$and": [base_filter, {"_id": {"$in": all_team_task_ids}}]}
134+
134135
elif user_id:
135136
assigned_task_ids = cls._get_assigned_task_ids_for_user(user_id)
136137
query_filter = {

todo/services/task_assignment_service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def create_task_assignment(cls, dto: CreateTaskAssignmentDTO, user_id: str) -> C
6666
user_type=dto.user_type,
6767
created_by=PyObjectId(user_id),
6868
updated_by=None,
69+
team_id=PyObjectId(dto.team_id) if dto.team_id else None,
6970
)
7071
assignment = TaskAssignmentRepository.create(task_assignment)
7172

todo/services/task_service.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ def _prepare_assignee_dto(cls, assignee_details: TaskAssignmentModel) -> TaskAss
235235
assignee_id=assignee_id,
236236
assignee_name=assignee.name,
237237
user_type=assignee_details.user_type,
238+
executor_id=str(assignee_details.executor_id) if assignee_details.executor_id else None,
239+
team_id=str(assignee_details.team_id) if assignee_details.team_id else None,
238240
is_active=assignee_details.is_active,
239241
created_by=str(assignee_details.created_by),
240242
updated_by=str(assignee_details.updated_by) if assignee_details.updated_by else None,
@@ -628,11 +630,16 @@ def create_task(cls, dto: CreateTaskDTO) -> CreateTaskResponse:
628630
created_task = TaskRepository.create(task)
629631

630632
# Create assignee relationship if assignee is provided
633+
team_id = None
634+
if dto.assignee and dto.assignee.get("user_type") == "team":
635+
team_id = dto.assignee.get("assignee_id")
636+
631637
if dto.assignee:
632638
assignee_dto = CreateTaskAssignmentDTO(
633639
task_id=str(created_task.id),
634640
assignee_id=dto.assignee.get("assignee_id"),
635641
user_type=dto.assignee.get("user_type"),
642+
team_id=team_id,
636643
)
637644
TaskAssignmentService.create_task_assignment(assignee_dto, created_task.createdBy)
638645

todo/views/task_assignment.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,11 +221,12 @@ def patch(self, request: Request, task_id: str):
221221
return Response(
222222
{"error": f"User {executor_id} is not a member of the team."}, status=status.HTTP_400_BAD_REQUEST
223223
)
224-
225224
# Update executor_id
226225
try:
227-
updated = TaskAssignmentRepository.update_executor(task_id, executor_id, user["user_id"])
228-
if not updated:
226+
updated_assignment = TaskAssignmentRepository.update_assignment(
227+
task_id, executor_id, "user", user["user_id"]
228+
)
229+
if not updated_assignment:
229230
# Get more details about why it failed
230231
import traceback
231232

@@ -234,7 +235,7 @@ def patch(self, request: Request, task_id: str):
234235
)
235236
print(f"DEBUG: assignment details: {assignment}")
236237
return Response(
237-
{"error": "Failed to update executor. Check server logs for details."},
238+
{"error": "Failed to update assignment. Check server logs for details."},
238239
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
239240
)
240241
except Exception as e:

0 commit comments

Comments
 (0)