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
13 changes: 12 additions & 1 deletion todo/serializers/create_task_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ class CreateTaskSerializer(serializers.Serializer):
dueAt = serializers.DateTimeField(
required=False, allow_null=True, help_text="Due date and time in ISO format (UTC)"
)
team_id = serializers.CharField(
required=False,
allow_null=True,
allow_blank=True,
)

def validate_title(self, value):
if not value.strip():
Expand All @@ -58,14 +63,20 @@ def validate(self, data):
# Compose the 'assignee' dict if assignee_id and user_type are present
assignee_id = data.pop("assignee_id", None)
user_type = data.pop("user_type", None)
team_id = data.pop("team_id", None)
if assignee_id and user_type:
if not ObjectId.is_valid(assignee_id):
raise serializers.ValidationError(
{"assignee_id": ValidationErrors.INVALID_OBJECT_ID.format(assignee_id)}
)
if user_type not in ["user", "team"]:
raise serializers.ValidationError({"user_type": "user_type must be either 'user' or 'team'"})
data["assignee"] = {"assignee_id": assignee_id, "user_type": user_type}
if team_id and team_id.strip():
if not ObjectId.is_valid(team_id):
raise serializers.ValidationError({"team_id": ValidationErrors.INVALID_OBJECT_ID.format(team_id)})
data["assignee"] = {"assignee_id": assignee_id, "user_type": user_type, "team_id": team_id}
else:
data["assignee"] = {"assignee_id": assignee_id, "user_type": user_type}

due_at = data.get("dueAt")
timezone_str = data.get("timezone")
Expand Down
10 changes: 10 additions & 0 deletions todo/services/task_assignment_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ def create_task_assignment(cls, dto: CreateTaskAssignmentDTO, user_id: str) -> C
)
assignment = TaskAssignmentRepository.create(task_assignment)

if assignment.user_type == "user" and assignment.team_id:
AuditLogRepository.create(
AuditLogModel(
task_id=assignment.task_id,
team_id=assignment.team_id,
action="assigned_to_member",
performed_by=PyObjectId(user_id),
)
)

# If new assignment is to a team, log assignment
if assignment.user_type == "team":
AuditLogRepository.create(
Expand Down
13 changes: 10 additions & 3 deletions todo/services/task_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,11 +599,16 @@ def create_task(cls, dto: CreateTaskDTO) -> CreateTaskResponse:
if dto.assignee:
assignee_id = dto.assignee.get("assignee_id")
user_type = dto.assignee.get("user_type")
team_id = dto.assignee.get("team_id")

if user_type == "user":
user = UserRepository.get_by_id(assignee_id)
if not user:
raise UserNotFoundException(assignee_id)
if team_id:
team = TeamRepository.get_by_id(team_id)
if not team:
raise ValueError(f"Team not found: {team_id}")
elif user_type == "team":
team = TeamRepository.get_by_id(assignee_id)
if not team:
Expand Down Expand Up @@ -631,10 +636,12 @@ def create_task(cls, dto: CreateTaskDTO) -> CreateTaskResponse:

# Create assignee relationship if assignee is provided
team_id = None
if dto.assignee and dto.assignee.get("user_type") == "team":
team_id = dto.assignee.get("assignee_id")

if dto.assignee:
if dto.assignee.get("user_type") == "team":
team_id = dto.assignee.get("assignee_id")
elif dto.assignee.get("user_type") == "user" and "team_id" in dto.assignee:
team_id = dto.assignee.get("team_id")

assignee_dto = CreateTaskAssignmentDTO(
task_id=str(created_task.id),
assignee_id=dto.assignee.get("assignee_id"),
Expand Down
23 changes: 23 additions & 0 deletions todo/tests/unit/serializers/test_create_task_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,26 @@ def test_serializer_rejects_invalid_user_type(self):
serializer = CreateTaskSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertIn("user_type", serializer.errors)

def test_serializer_accepts_valid_team_id(self):
data = self.valid_data.copy()
data["team_id"] = str(ObjectId())
serializer = CreateTaskSerializer(data=data)
self.assertTrue(serializer.is_valid())
self.assertIn("assignee", serializer.validated_data)
self.assertEqual(serializer.validated_data["assignee"]["team_id"], data["team_id"])

def test_serializer_rejects_invalid_team_id(self):
data = self.valid_data.copy()
data["team_id"] = "invalid_team_id"
serializer = CreateTaskSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertIn("team_id", serializer.errors)

def test_serializer_handles_empty_team_id(self):
data = self.valid_data.copy()
data["team_id"] = ""
serializer = CreateTaskSerializer(data=data)
self.assertTrue(serializer.is_valid())
self.assertIn("assignee", serializer.validated_data)
self.assertNotIn("team_id", serializer.validated_data["assignee"])
191 changes: 164 additions & 27 deletions todo/tests/unit/services/test_task_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,33 +208,170 @@ def test_get_tasks_handles_general_exception(self, mock_list: Mock):
self.assertEqual(len(response.tasks), 0)
self.assertIsNone(response.links)

# @patch("todo.services.task_service.TaskRepository.create")
# @patch("todo.services.task_service.TaskService.prepare_task_dto")
# def test_create_task_successfully_creates_task(self, mock_prepare_dto, mock_create):
# dto = CreateTaskDTO(
# title="Test Task",
# description="This is a test",
# priority=TaskPriority.HIGH,
# status=TaskStatus.TODO,
# assignee={"assignee_id": str(self.user_id), "user_type": "user"},
# createdBy=str(self.user_id),
# labels=[],
# dueAt=datetime.now(timezone.utc) + timedelta(days=1),
# )

# mock_task_model = MagicMock(spec=TaskModel)
# mock_task_model.id = ObjectId()
# mock_create.return_value = mock_task_model
# mock_task_dto = MagicMock(spec=TaskDTO)
# mock_prepare_dto.return_value = mock_task_dto

# result = TaskService.create_task(dto)

# mock_create.assert_called_once()
# created_task_model_arg = mock_create.call_args[0][0]
# self.assertIsNone(created_task_model_arg.deferredDetails)
# mock_prepare_dto.assert_called_once_with(mock_task_model, str(self.user_id))
# self.assertEqual(result.data, mock_task_dto)
@patch("todo.services.task_service.TaskRepository.create")
@patch("todo.services.task_service.TaskService.prepare_task_dto")
@patch("todo.services.task_service.TaskAssignmentService.create_task_assignment")
@patch("todo.services.task_service.UserRepository.get_by_id")
@patch("todo.services.task_service.TeamRepository.get_by_id")
def test_create_task_with_user_assignment_and_team_id(
self, mock_team_repo, mock_user_repo, mock_create_assignment, mock_prepare_dto, mock_create
):
team_id = str(ObjectId())
user_id = str(ObjectId())

dto = CreateTaskDTO(
title="Test Task",
description="This is a test",
priority=TaskPriority.HIGH,
status=TaskStatus.TODO,
assignee={"assignee_id": user_id, "user_type": "user", "team_id": team_id},
createdBy=str(self.user_id),
labels=[],
dueAt=datetime.now(timezone.utc) + timedelta(days=1),
)

mock_user_repo.return_value = MagicMock()
mock_team_repo.return_value = MagicMock()

mock_task_model = MagicMock(spec=TaskModel)
mock_task_model.id = ObjectId()
mock_task_model.createdBy = str(self.user_id)
mock_create.return_value = mock_task_model
mock_task_dto = MagicMock(spec=TaskDTO)
mock_prepare_dto.return_value = mock_task_dto

mock_assignment_response = MagicMock()
mock_assignment_response.data.task_id = str(mock_task_model.id)
mock_assignment_response.data.assignee_id = user_id
mock_create_assignment.return_value = mock_assignment_response

result = TaskService.create_task(dto)

mock_create.assert_called_once()

mock_create_assignment.assert_called_once()
assignment_call_args = mock_create_assignment.call_args[0][0]
self.assertEqual(assignment_call_args.task_id, str(mock_task_model.id))
self.assertEqual(assignment_call_args.assignee_id, user_id)
self.assertEqual(assignment_call_args.user_type, "user")
self.assertEqual(assignment_call_args.team_id, team_id)

mock_prepare_dto.assert_called_once_with(mock_task_model, str(self.user_id))
self.assertEqual(result.data, mock_task_dto)

@patch("todo.services.task_service.TaskRepository.create")
@patch("todo.services.task_service.TaskService.prepare_task_dto")
@patch("todo.services.task_service.TaskAssignmentService.create_task_assignment")
@patch("todo.services.task_service.UserRepository.get_by_id")
def test_create_task_with_user_assignment_without_team_id(
self, mock_user_repo, mock_create_assignment, mock_prepare_dto, mock_create
):
user_id = str(ObjectId())

dto = CreateTaskDTO(
title="Test Task",
description="This is a test",
priority=TaskPriority.HIGH,
status=TaskStatus.TODO,
assignee={"assignee_id": user_id, "user_type": "user"},
createdBy=str(self.user_id),
labels=[],
dueAt=datetime.now(timezone.utc) + timedelta(days=1),
)

mock_user_repo.return_value = MagicMock()

mock_task_model = MagicMock(spec=TaskModel)
mock_task_model.id = ObjectId()
mock_task_model.createdBy = str(self.user_id)
mock_create.return_value = mock_task_model
mock_task_dto = MagicMock(spec=TaskDTO)
mock_prepare_dto.return_value = mock_task_dto

TaskService.create_task(dto)

mock_create_assignment.assert_called_once()
assignment_call_args = mock_create_assignment.call_args[0][0]
self.assertEqual(assignment_call_args.task_id, str(mock_task_model.id))
self.assertEqual(assignment_call_args.assignee_id, user_id)
self.assertEqual(assignment_call_args.user_type, "user")
self.assertIsNone(assignment_call_args.team_id)

@patch("todo.services.task_service.TaskRepository.create")
@patch("todo.services.task_service.TaskService.prepare_task_dto")
@patch("todo.services.task_service.TaskAssignmentService.create_task_assignment")
@patch("todo.services.task_service.UserRepository.get_by_id")
@patch("todo.services.task_service.TeamRepository.get_by_id")
def test_create_task_validates_team_exists_for_user_assignment(
self, mock_team_repo, mock_user_repo, mock_create_assignment, mock_prepare_dto, mock_create
):
team_id = str(ObjectId())
user_id = str(ObjectId())

dto = CreateTaskDTO(
title="Test Task",
description="This is a test",
priority=TaskPriority.HIGH,
status=TaskStatus.TODO,
assignee={"assignee_id": user_id, "user_type": "user", "team_id": team_id},
createdBy=str(self.user_id),
labels=[],
dueAt=datetime.now(timezone.utc) + timedelta(days=1),
)

mock_user_repo.return_value = MagicMock()
mock_team_repo.return_value = None # Team not found

with self.assertRaises(ValueError) as context:
TaskService.create_task(dto)

self.assertIn(f"Team not found: {team_id}", str(context.exception))
mock_team_repo.assert_called_once_with(team_id)

@patch("todo.services.task_service.TaskRepository.create")
@patch("todo.services.task_service.TaskService.prepare_task_dto")
@patch("todo.services.task_service.TaskAssignmentService.create_task_assignment")
@patch("todo.services.task_service.UserRepository.get_by_id")
@patch("todo.services.task_service.TeamRepository.get_by_id")
def test_create_task_passes_team_id_to_assignment_service(
self, mock_team_repo, mock_user_repo, mock_create_assignment, mock_prepare_dto, mock_create
):
team_id = str(ObjectId())
user_id = str(ObjectId())

dto = CreateTaskDTO(
title="Test Task",
description="This is a test",
priority=TaskPriority.HIGH,
status=TaskStatus.TODO,
assignee={"assignee_id": user_id, "user_type": "user", "team_id": team_id},
createdBy=str(self.user_id),
labels=[],
dueAt=datetime.now(timezone.utc) + timedelta(days=1),
)

mock_user_repo.return_value = MagicMock()
mock_team_repo.return_value = MagicMock()

mock_task_model = MagicMock(spec=TaskModel)
mock_task_model.id = ObjectId()
mock_task_model.createdBy = str(self.user_id)
mock_create.return_value = mock_task_model
mock_task_dto = MagicMock(spec=TaskDTO)
mock_prepare_dto.return_value = mock_task_dto

mock_assignment_response = MagicMock()
mock_assignment_response.data.task_id = str(mock_task_model.id)
mock_assignment_response.data.assignee_id = user_id
mock_create_assignment.return_value = mock_assignment_response

TaskService.create_task(dto)

mock_create_assignment.assert_called_once()
assignment_call_args = mock_create_assignment.call_args[0][0]
self.assertEqual(assignment_call_args.team_id, team_id)
self.assertEqual(assignment_call_args.assignee_id, user_id)
self.assertEqual(assignment_call_args.user_type, "user")

@patch("todo.services.task_service.TaskRepository.get_by_id")
@patch("todo.services.task_service.TaskService.prepare_task_dto")
Expand Down
Loading