Skip to content

Commit 7d183b9

Browse files
authored
Merge pull request #285 from Real-Dev-Squad/develop
Dev To Main Sync
2 parents e39038c + 2fc60e2 commit 7d183b9

File tree

8 files changed

+342
-81
lines changed

8 files changed

+342
-81
lines changed

todo/constants/messages.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ class ValidationErrors:
8484
SEARCH_QUERY_EMPTY = "Search query cannot be empty"
8585
TASK_ID_STRING_REQUIRED = "Task ID must be a string."
8686
INVALID_IS_ACTIVE_VALUE = "Invalid value for is_active"
87+
USER_NOT_TEAM_MEMBER = "User is not a member of the team"
88+
POC_NOT_PROVIDED = "POC is required for team update"
8789

8890

8991
# Auth messages

todo/exceptions/exception_handler.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@
2020
TokenInvalidError,
2121
RefreshTokenExpiredError,
2222
APIException,
23-
UserNotFoundException,
2423
TokenMissingError,
2524
)
2625

26+
from todo.exceptions.user_exceptions import UserNotFoundException
27+
2728

2829
def format_validation_errors(errors) -> List[ApiErrorDetail]:
2930
formatted_errors = []

todo/repositories/task_repository.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,13 @@ def _get_assigned_task_ids_for_user(cls, user_id: str) -> List[ObjectId]:
130130
if team_ids:
131131
# Get teams where user is POC
132132
poc_teams = TeamRepository.get_collection().find(
133-
{"_id": {"$in": [ObjectId(team_id) for team_id in team_ids]}, "is_deleted": False, "poc_id": user_id}
133+
{
134+
"_id": {"$in": [ObjectId(team_id) for team_id in team_ids]},
135+
"is_deleted": False,
136+
"poc_id": {"$in": [ObjectId(user_id), user_id]},
137+
}
134138
)
139+
135140
poc_team_ids = [str(team["_id"]) for team in poc_teams]
136141

137142
# Get team assignments for POC teams

todo/serializers/update_team_serializer.py

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,12 @@
77
class UpdateTeamSerializer(serializers.Serializer):
88
"""
99
Serializer for updating team details.
10-
All fields are optional for PATCH operations.
11-
"""
12-
13-
name = serializers.CharField(max_length=100, required=False, allow_blank=False)
14-
description = serializers.CharField(max_length=500, required=False, allow_blank=True, allow_null=True)
15-
poc_id = serializers.CharField(required=False, allow_null=True, allow_blank=False)
16-
member_ids = serializers.ListField(child=serializers.CharField(), required=False, allow_empty=True, default=None)
1710
18-
def validate_name(self, value):
19-
if value is not None and not value.strip():
20-
raise serializers.ValidationError("Team name cannot be blank")
21-
return value.strip() if value else None
11+
"""
2212

23-
def validate_description(self, value):
24-
if value is not None:
25-
return value.strip()
26-
return value
13+
poc_id = serializers.CharField(required=True, allow_null=False, allow_blank=False)
2714

2815
def validate_poc_id(self, value):
29-
if not value or not value.strip():
30-
return None
3116
if not ObjectId.is_valid(value):
3217
raise serializers.ValidationError(ValidationErrors.INVALID_OBJECT_ID.format(value))
3318
return value
34-
35-
def validate_member_ids(self, value):
36-
if value is None:
37-
return value
38-
for member_id in value:
39-
if not ObjectId.is_valid(member_id):
40-
raise serializers.ValidationError(ValidationErrors.INVALID_OBJECT_ID.format(member_id))
41-
return value

todo/services/team_service.py

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
from todo.dto.team_dto import CreateTeamDTO, TeamDTO
2-
from todo.dto.update_team_dto import UpdateTeamDTO
32
from todo.dto.responses.create_team_response import CreateTeamResponse
43
from todo.dto.responses.get_user_teams_response import GetUserTeamsResponse
54
from todo.models.team import TeamModel, UserTeamDetailsModel
65
from todo.models.common.pyobjectid import PyObjectId
76
from todo.repositories.team_creation_invite_code_repository import TeamCreationInviteCodeRepository
87
from todo.repositories.team_repository import TeamRepository, UserTeamDetailsRepository
9-
from todo.constants.messages import AppMessages
8+
from todo.constants.messages import AppMessages, ApiErrors, ValidationErrors
109
from todo.constants.role import RoleName
1110
from todo.utils.invite_code_utils import generate_invite_code
1211
from typing import List
@@ -23,6 +22,7 @@
2322
CannotRemoveTeamPOCException,
2423
)
2524

25+
2626
DEFAULT_ROLE_ID = "1"
2727

2828

@@ -324,55 +324,46 @@ def join_team_by_invite_code(cls, invite_code: str, user_id: str) -> TeamDTO:
324324
)
325325

326326
@classmethod
327-
def update_team(cls, team_id: str, dto: UpdateTeamDTO, updated_by_user_id: str) -> TeamDTO:
327+
def update_team(cls, team_id: str, poc_id: str, user_id: str) -> TeamDTO:
328328
"""
329329
Update a team by its ID.
330330
331331
Args:
332332
team_id: ID of the team to update
333-
dto: Team update data including name, description, and POC
334-
updated_by_user_id: ID of the user updating the team
333+
poc_id: ID of the new POC
334+
user_id: ID of the user updating the team
335335
336336
Returns:
337337
TeamDTO with the updated team details
338338
339339
Raises:
340340
ValueError: If team update fails or team not found
341341
"""
342-
try:
343-
# Check if team exists
344-
existing_team = TeamRepository.get_by_id(team_id)
345-
if not existing_team:
346-
raise ValueError(f"Team with id {team_id} not found")
342+
existing_team = TeamRepository.get_by_id(team_id)
343+
if not existing_team:
344+
raise ValueError(f"Team with id {team_id} not found")
345+
346+
cls._validate_is_user_team_admin(team_id, user_id, existing_team)
347347

348-
# Prepare update data
349-
update_data = {}
350-
if dto.name is not None:
351-
update_data["name"] = dto.name
352-
if dto.description is not None:
353-
update_data["description"] = dto.description
354-
if dto.poc_id is not None:
355-
update_data["poc_id"] = PyObjectId(dto.poc_id) if dto.poc_id and dto.poc_id.strip() else None
348+
update_data = {}
356349

350+
validation_error = cls._validate_is_user_team_member(team_id, poc_id)
351+
if validation_error:
352+
return validation_error
353+
update_data["poc_id"] = PyObjectId(poc_id)
354+
355+
try:
357356
# Update the team
358-
updated_team = TeamRepository.update(team_id, update_data, updated_by_user_id)
357+
updated_team = TeamRepository.update(team_id, update_data, user_id)
359358
if not updated_team:
360359
raise ValueError(f"Failed to update team with id {team_id}")
361360

362-
# Handle member updates if provided
363-
if dto.member_ids is not None:
364-
from todo.repositories.team_repository import UserTeamDetailsRepository
365-
366-
success = UserTeamDetailsRepository.update_team_members(team_id, dto.member_ids, updated_by_user_id)
367-
if not success:
368-
raise ValueError(f"Failed to update team members for team with id {team_id}")
369-
370361
# Audit log for team update
371362
AuditLogRepository.create(
372363
AuditLogModel(
373364
team_id=PyObjectId(team_id),
374365
action="team_updated",
375-
performed_by=PyObjectId(updated_by_user_id),
366+
performed_by=PyObjectId(user_id),
376367
)
377368
)
378369

@@ -388,7 +379,6 @@ def update_team(cls, team_id: str, dto: UpdateTeamDTO, updated_by_user_id: str)
388379
created_at=updated_team.created_at,
389380
updated_at=updated_team.updated_at,
390381
)
391-
392382
except Exception as e:
393383
raise ValueError(f"Failed to update team: {str(e)}")
394384

@@ -530,3 +520,31 @@ def remove_member_from_team(cls, user_id: str, team_id: str, removed_by_user_id:
530520
TaskAssignmentService.reassign_tasks_from_user_to_team(user_id, team_id, removed_by_user_id)
531521

532522
return True
523+
524+
@classmethod
525+
def _validate_is_user_team_admin(cls, team_id: str, user_id: str, team):
526+
"""
527+
Validate that the user has admin role in the team.
528+
"""
529+
if str(team.created_by) == user_id:
530+
return
531+
532+
if UserRoleService.has_role(user_id, RoleName.ADMIN.value, RoleScope.TEAM.value, team_id):
533+
return
534+
535+
raise PermissionError(ApiErrors.UNAUTHORIZED_TITLE)
536+
537+
@classmethod
538+
def _validate_is_user_team_member(cls, team_id: str, poc_id: str):
539+
"""
540+
Validate that the user is a member of the team.
541+
Returns ApiErrorResponse if validation fails, None if validation passes.
542+
"""
543+
544+
if not UserRoleService.has_role(poc_id, RoleName.MEMBER.value, RoleScope.TEAM.value, team_id):
545+
return ApiErrorResponse(
546+
statusCode=400,
547+
message=ValidationErrors.USER_NOT_TEAM_MEMBER,
548+
errors=[ApiErrorDetail(detail=ValidationErrors.USER_NOT_TEAM_MEMBER)],
549+
)
550+
return
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
from http import HTTPStatus
2+
from django.urls import reverse
3+
from bson import ObjectId
4+
from datetime import datetime, timezone
5+
import json
6+
7+
from todo.tests.integration.base_mongo_test import AuthenticatedMongoTestCase
8+
from todo.constants.messages import ApiErrors, ValidationErrors
9+
10+
11+
class TeamUpdateIntegrationTests(AuthenticatedMongoTestCase):
12+
def setUp(self):
13+
super().setUp()
14+
self.db.teams.delete_many({})
15+
self.db.user_roles.delete_many({})
16+
17+
self.team_id = str(ObjectId())
18+
self.owner_id = str(self.user_id)
19+
self.admin_id = str(ObjectId())
20+
self.member_id = str(ObjectId())
21+
self.non_member_id = str(ObjectId())
22+
23+
team_doc = {
24+
"_id": ObjectId(self.team_id),
25+
"name": "Test Team",
26+
"description": "Test Description",
27+
"poc_id": ObjectId(self.member_id),
28+
"invite_code": "TEST123",
29+
"created_by": ObjectId(self.owner_id),
30+
"updated_by": ObjectId(self.owner_id),
31+
"created_at": datetime.now(timezone.utc),
32+
"updated_at": datetime.now(timezone.utc),
33+
"is_deleted": False,
34+
}
35+
self.db.teams.insert_one(team_doc)
36+
37+
owner_role_doc = {
38+
"_id": ObjectId(),
39+
"name": "owner",
40+
"scope": "TEAM",
41+
"description": "Team Owner",
42+
"created_at": datetime.now(timezone.utc),
43+
"updated_at": datetime.now(timezone.utc),
44+
}
45+
self.db.roles.insert_one(owner_role_doc)
46+
47+
member_role_doc = {
48+
"_id": ObjectId(),
49+
"name": "member",
50+
"scope": "TEAM",
51+
"description": "Team Member",
52+
"created_at": datetime.now(timezone.utc),
53+
"updated_at": datetime.now(timezone.utc),
54+
}
55+
self.db.roles.insert_one(member_role_doc)
56+
57+
owner_role = {
58+
"_id": ObjectId(),
59+
"user_id": ObjectId(self.owner_id),
60+
"role_id": owner_role_doc["_id"],
61+
"role_name": "owner",
62+
"scope": "TEAM",
63+
"team_id": ObjectId(self.team_id),
64+
"is_active": True,
65+
"created_by": ObjectId(self.owner_id),
66+
"created_at": datetime.now(timezone.utc),
67+
}
68+
self.db.user_roles.insert_one(owner_role)
69+
70+
authenticated_user_role = {
71+
"_id": ObjectId(),
72+
"user_id": str(self.user_id),
73+
"role_id": owner_role_doc["_id"],
74+
"role_name": "owner",
75+
"scope": "TEAM",
76+
"team_id": str(self.team_id),
77+
"is_active": True,
78+
"created_by": str(self.owner_id),
79+
"created_at": datetime.now(timezone.utc),
80+
}
81+
self.db.user_roles.insert_one(authenticated_user_role)
82+
83+
member_role = {
84+
"_id": ObjectId(),
85+
"user_id": self.member_id,
86+
"role_id": member_role_doc["_id"],
87+
"role_name": "member",
88+
"scope": "TEAM",
89+
"team_id": self.team_id,
90+
"is_active": True,
91+
"created_by": self.owner_id,
92+
"created_at": datetime.now(timezone.utc),
93+
}
94+
self.db.user_roles.insert_one(member_role)
95+
96+
self.db.users.insert_one(
97+
{
98+
"_id": ObjectId(self.member_id),
99+
"google_id": "member_google_id",
100+
"email_id": "[email protected]",
101+
"name": "Member User",
102+
"picture": "member_picture",
103+
"createdAt": datetime.now(timezone.utc),
104+
"updatedAt": datetime.now(timezone.utc),
105+
}
106+
)
107+
108+
self.db.users.insert_one(
109+
{
110+
"_id": ObjectId(self.non_member_id),
111+
"google_id": "non_member_google_id",
112+
"email_id": "[email protected]",
113+
"name": "Non Member User",
114+
"picture": "non_member_picture",
115+
"createdAt": datetime.now(timezone.utc),
116+
"updatedAt": datetime.now(timezone.utc),
117+
}
118+
)
119+
120+
self.existing_team_id = self.team_id
121+
self.non_existent_id = str(ObjectId())
122+
self.invalid_team_id = "invalid-team-id"
123+
124+
def test_update_team_success_by_owner(self):
125+
url = reverse("team_detail", args=[self.existing_team_id])
126+
response = self.client.patch(
127+
url,
128+
data=json.dumps({"poc_id": self.member_id}),
129+
content_type="application/json",
130+
)
131+
132+
self.assertEqual(response.status_code, HTTPStatus.OK)
133+
data = response.json()
134+
self.assertEqual(data["poc_id"], self.member_id)
135+
self.assertNotIn("invite_code", data)
136+
137+
def test_update_team_unauthorized_user(self):
138+
other_user_id = ObjectId()
139+
self._create_test_user(other_user_id)
140+
self._set_auth_cookies()
141+
142+
url = reverse("team_detail", args=[self.existing_team_id])
143+
response = self.client.patch(url, data=json.dumps({"name": "Updated Team"}), content_type="application/json")
144+
145+
self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN)
146+
data = response.json()
147+
self.assertEqual(data["detail"], ApiErrors.UNAUTHORIZED_TITLE)
148+
149+
def test_update_team_invalid_poc_id_format(self):
150+
url = reverse("team_detail", args=[self.existing_team_id])
151+
response = self.client.patch(url, data=json.dumps({"poc_id": "invalid-id"}), content_type="application/json")
152+
153+
self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
154+
data = response.json()
155+
self.assertIn("poc_id", data["errors"])
156+
self.assertIn(ValidationErrors.INVALID_OBJECT_ID.format("invalid-id"), str(data["errors"]["poc_id"]))

0 commit comments

Comments
 (0)