-
Notifications
You must be signed in to change notification settings - Fork 14
tests: add tests for remove from team API #271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
13e97ad
ad26f3a
f25bc28
8ef56b8
3f0bd93
7877f29
f075980
13485ff
6bb20aa
7713420
b0e48d3
482f7f0
f4ca977
f96e4c4
42c9485
3b50a66
4b4ae55
bd88687
3d20bfb
d567b3a
01e59b8
bbc2132
3bf3c2a
3c6f245
8bcea47
2059402
918cd79
8027660
d156c32
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
from todo.constants.messages import ApiErrors | ||
|
||
|
||
class BaseTeamException(Exception): | ||
def __init__(self, message: str): | ||
self.message = message | ||
super().__init__(self.message) | ||
Hariom01010 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
class CannotRemoveOwnerException(BaseTeamException): | ||
def __init__(self, message=ApiErrors.CANNOT_REMOVE_OWNER): | ||
super().__init__(message) | ||
|
||
|
||
class NotTeamAdminException(BaseTeamException): | ||
def __init__(self, message=ApiErrors.UNAUTHORIZED_TITLE): | ||
super().__init__(message) | ||
Hariom01010 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
class CannotRemoveTeamPOCException(BaseTeamException): | ||
def __init__(self, message=ApiErrors.CANNOT_REMOVE_POC): | ||
super().__init__(message) | ||
Hariom01010 marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Generated by Django 5.1.5 on 2025-09-09 14:49 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("todo", "0002_rename_postgres_ta_assignee_95ca3b_idx_postgres_ta_assigne_f1c6e7_idx_and_more"), | ||
] | ||
|
||
operations = [ | ||
migrations.AlterUniqueTogether( | ||
name="postgresuserrole", | ||
unique_together=set(), | ||
), | ||
migrations.AddConstraint( | ||
model_name="postgresuserrole", | ||
constraint=models.UniqueConstraint( | ||
condition=models.Q(("is_active", True)), | ||
fields=("user_id", "role_name", "scope", "team_id"), | ||
name="unique_active_user_team_role", | ||
), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -6,7 +6,9 @@ | |||||||||||||
from todo.models.task_assignment import TaskAssignmentModel | ||||||||||||||
from todo.repositories.common.mongo_repository import MongoRepository | ||||||||||||||
from todo.models.common.pyobjectid import PyObjectId | ||||||||||||||
from todo.constants.task import TaskStatus | ||||||||||||||
from todo.services.enhanced_dual_write_service import EnhancedDualWriteService | ||||||||||||||
from todo.repositories.audit_log_repository import AuditLogRepository, AuditLogModel | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
class TaskAssignmentRepository(MongoRepository): | ||||||||||||||
|
@@ -355,3 +357,167 @@ def deactivate_by_task_id(cls, task_id: str, user_id: str) -> bool: | |||||||||||||
return result.modified_count > 0 | ||||||||||||||
except Exception: | ||||||||||||||
return False | ||||||||||||||
|
||||||||||||||
@classmethod | ||||||||||||||
def reassign_tasks_from_user_to_team(cls, user_id: str, team_id: str, performed_by_user_id: str) -> bool: | ||||||||||||||
""" | ||||||||||||||
Reassign all tasks of user to team | ||||||||||||||
""" | ||||||||||||||
collection = cls.get_collection() | ||||||||||||||
client = cls.get_client() | ||||||||||||||
with client.start_session() as session: | ||||||||||||||
try: | ||||||||||||||
with session.start_transaction(): | ||||||||||||||
now = datetime.now(timezone.utc) | ||||||||||||||
user_task_assignments = list( | ||||||||||||||
collection.find( | ||||||||||||||
{ | ||||||||||||||
"$and": [ | ||||||||||||||
{"is_active": True}, | ||||||||||||||
{ | ||||||||||||||
"$or": [{"assignee_id": user_id}, {"assignee_id": ObjectId(user_id)}], | ||||||||||||||
}, | ||||||||||||||
{"$or": [{"team_id": team_id}, {"team_id": ObjectId(team_id)}]}, | ||||||||||||||
] | ||||||||||||||
}, | ||||||||||||||
session=session, | ||||||||||||||
) | ||||||||||||||
) | ||||||||||||||
if not user_task_assignments: | ||||||||||||||
return 0 | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logic error: function returns 0 instead of boolean. The function signature and other return statements suggest it should return a boolean (True/False), but line 387 returns
Suggested change
Spotted by Diamond |
||||||||||||||
active_user_task_assignments_ids = [ | ||||||||||||||
ObjectId(assignment["task_id"]) for assignment in user_task_assignments | ||||||||||||||
] | ||||||||||||||
|
||||||||||||||
from todo.repositories.task_repository import TaskRepository | ||||||||||||||
|
||||||||||||||
tasks_collection = TaskRepository.get_collection() | ||||||||||||||
active_tasks = list( | ||||||||||||||
tasks_collection.find( | ||||||||||||||
{ | ||||||||||||||
"_id": {"$in": active_user_task_assignments_ids}, | ||||||||||||||
"status": {"$ne": TaskStatus.DONE.value}, | ||||||||||||||
}, | ||||||||||||||
session=session, | ||||||||||||||
) | ||||||||||||||
) | ||||||||||||||
not_done_tasks_ids = [str(tasks["_id"]) for tasks in active_tasks] | ||||||||||||||
tasks_to_reset_status_ids = [] | ||||||||||||||
tasks_to_clear_deferred_ids = [] | ||||||||||||||
for tasks in active_tasks: | ||||||||||||||
if tasks["status"] == TaskStatus.IN_PROGRESS.value: | ||||||||||||||
tasks_to_reset_status_ids.append(tasks["_id"]) | ||||||||||||||
elif tasks.get("deferredDetails") is not None: | ||||||||||||||
tasks_to_clear_deferred_ids.append(tasks["_id"]) | ||||||||||||||
|
||||||||||||||
collection.update_many( | ||||||||||||||
{ | ||||||||||||||
"task_id": {"$in": not_done_tasks_ids}, | ||||||||||||||
}, | ||||||||||||||
{ | ||||||||||||||
"$set": { | ||||||||||||||
"assignee_id": team_id, | ||||||||||||||
"user_type": "team", | ||||||||||||||
"updated_at": now, | ||||||||||||||
"updated_by": ObjectId(performed_by_user_id), | ||||||||||||||
} | ||||||||||||||
}, | ||||||||||||||
session=session, | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
for assignment in user_task_assignments: | ||||||||||||||
AuditLogRepository.create( | ||||||||||||||
AuditLogModel( | ||||||||||||||
task_id=PyObjectId(assignment["task_id"]), | ||||||||||||||
team_id=PyObjectId(team_id), | ||||||||||||||
action="assigned_to_team", | ||||||||||||||
performed_by=PyObjectId(performed_by_user_id), | ||||||||||||||
) | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
tasks_collection.update_many( | ||||||||||||||
{"_id": {"$in": tasks_to_reset_status_ids}}, | ||||||||||||||
{ | ||||||||||||||
"$set": { | ||||||||||||||
"status": TaskStatus.TODO.value, | ||||||||||||||
"updated_at": now, | ||||||||||||||
"updated_by": ObjectId(performed_by_user_id), | ||||||||||||||
} | ||||||||||||||
}, | ||||||||||||||
session=session, | ||||||||||||||
) | ||||||||||||||
tasks_collection.update_many( | ||||||||||||||
{"_id": {"$in": tasks_to_clear_deferred_ids}}, | ||||||||||||||
{ | ||||||||||||||
"$set": { | ||||||||||||||
"status": TaskStatus.TODO.value, | ||||||||||||||
"deferredDetails": None, | ||||||||||||||
"updated_at": now, | ||||||||||||||
"updated_by": ObjectId(performed_by_user_id), | ||||||||||||||
} | ||||||||||||||
}, | ||||||||||||||
session=session, | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
tasks_by_id = {task["_id"]: task for task in active_tasks} | ||||||||||||||
operations = [] | ||||||||||||||
dual_write_service = EnhancedDualWriteService() | ||||||||||||||
for assignment in user_task_assignments: | ||||||||||||||
operations.append( | ||||||||||||||
{ | ||||||||||||||
"collection_name": "task_assignments", | ||||||||||||||
"operation": "update", | ||||||||||||||
"mongo_id": assignment["_id"], | ||||||||||||||
"data": { | ||||||||||||||
"task_mongo_id": str(assignment["task_id"]), | ||||||||||||||
"assignee_id": str(assignment["team_id"]), | ||||||||||||||
"user_type": "team", | ||||||||||||||
"team_id": str(assignment["team_id"]), | ||||||||||||||
"is_active": True, | ||||||||||||||
"created_at": assignment["created_at"], | ||||||||||||||
"created_by": str(assignment["created_by"]), | ||||||||||||||
"updated_at": datetime.now(timezone.utc), | ||||||||||||||
"updated_by": str(performed_by_user_id), | ||||||||||||||
}, | ||||||||||||||
} | ||||||||||||||
) | ||||||||||||||
if ( | ||||||||||||||
assignment["task_id"] in tasks_to_clear_deferred_ids | ||||||||||||||
or assignment["task_id"] in tasks_to_reset_status_ids | ||||||||||||||
Comment on lines
+484
to
+486
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logic error in task ID comparison. The code compares
Suggested change
Spotted by Diamond |
||||||||||||||
): | ||||||||||||||
task = tasks_by_id[assignment["task_id"]] | ||||||||||||||
operations.append( | ||||||||||||||
{ | ||||||||||||||
"collection_name": "tasks", | ||||||||||||||
"operation": "update", | ||||||||||||||
"mongo_id": assignment["task_id"], | ||||||||||||||
"data": { | ||||||||||||||
"title": task.get("title"), | ||||||||||||||
"description": task.get("description"), | ||||||||||||||
"priority": task.get("priority"), | ||||||||||||||
"status": TaskStatus.TODO.value, | ||||||||||||||
"displayId": task.get("displayId"), | ||||||||||||||
"deferredDetails": None, | ||||||||||||||
"isAcknowledged": task.get("isAcknowledged", False), | ||||||||||||||
"isDeleted": task.get("isDeleted", False), | ||||||||||||||
"startedAt": task.get("startedAt"), | ||||||||||||||
"dueAt": task.get("dueAt"), | ||||||||||||||
"createdAt": task.get("createdAt"), | ||||||||||||||
"createdBy": str(task.get("createdBy")), | ||||||||||||||
"updatedAt": datetime.now(timezone.utc), | ||||||||||||||
"updated_by": str(performed_by_user_id), | ||||||||||||||
}, | ||||||||||||||
} | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
dual_write_success = dual_write_service.batch_operations(operations) | ||||||||||||||
if not dual_write_success: | ||||||||||||||
import logging | ||||||||||||||
|
||||||||||||||
logger = logging.getLogger(__name__) | ||||||||||||||
logger.warning("Failed to sync task reassignments to Postgres") | ||||||||||||||
|
||||||||||||||
return False | ||||||||||||||
return True | ||||||||||||||
except Exception: | ||||||||||||||
return False |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
from rest_framework import serializers | ||
from bson import ObjectId | ||
from todo.constants.messages import ValidationErrors | ||
|
||
|
||
class RemoveFromTeamSerializer(serializers.Serializer): | ||
team_id = serializers.CharField() | ||
user_id = serializers.CharField() | ||
|
||
def validate_team_id(self, value): | ||
if not ObjectId.is_valid(value): | ||
raise serializers.ValidationError(ValidationErrors.INVALID_OBJECT_ID.format(value)) | ||
return value | ||
|
||
def validate_user_id(self, value): | ||
if not ObjectId.is_valid(value): | ||
raise serializers.ValidationError(ValidationErrors.INVALID_OBJECT_ID.format(value)) | ||
return value |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -13,6 +13,15 @@ | |||||||||||||||||||||||||||||||
from todo.models.audit_log import AuditLogModel | ||||||||||||||||||||||||||||||||
from todo.repositories.audit_log_repository import AuditLogRepository | ||||||||||||||||||||||||||||||||
from todo.dto.responses.error_response import ApiErrorResponse, ApiErrorDetail | ||||||||||||||||||||||||||||||||
from todo.constants.role import RoleScope | ||||||||||||||||||||||||||||||||
from todo.services.user_role_service import UserRoleService | ||||||||||||||||||||||||||||||||
from todo.services.task_assignment_service import TaskAssignmentService | ||||||||||||||||||||||||||||||||
from todo.services.user_service import UserService | ||||||||||||||||||||||||||||||||
from todo.exceptions.team_exceptions import ( | ||||||||||||||||||||||||||||||||
CannotRemoveOwnerException, | ||||||||||||||||||||||||||||||||
NotTeamAdminException, | ||||||||||||||||||||||||||||||||
CannotRemoveTeamPOCException, | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
DEFAULT_ROLE_ID = "1" | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
@@ -483,7 +492,25 @@ class TeamOrUserNotFound(Exception): | |||||||||||||||||||||||||||||||
pass | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
@classmethod | ||||||||||||||||||||||||||||||||
def remove_member_from_team(cls, user_id: str, team_id: str, removed_by_user_id: str = None): | ||||||||||||||||||||||||||||||||
def _validate_remove_member_permissions(cls, user_id: str, team_id: str, removed_by_user_id: str): | ||||||||||||||||||||||||||||||||
team = TeamService.get_team_by_id(team_id) | ||||||||||||||||||||||||||||||||
team_members = UserService.get_users_by_team_id(team_id) | ||||||||||||||||||||||||||||||||
team_member_ids = [user.id for user in team_members] | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
if user_id not in team_member_ids: | ||||||||||||||||||||||||||||||||
raise cls.TeamOrUserNotFound | ||||||||||||||||||||||||||||||||
if user_id == team.created_by: | ||||||||||||||||||||||||||||||||
Hariom01010 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||
raise CannotRemoveOwnerException() | ||||||||||||||||||||||||||||||||
if user_id == team.poc_id: | ||||||||||||||||||||||||||||||||
raise CannotRemoveTeamPOCException() | ||||||||||||||||||||||||||||||||
if user_id != removed_by_user_id: | ||||||||||||||||||||||||||||||||
if not UserRoleService.has_role(removed_by_user_id, RoleName.ADMIN.value, RoleScope.TEAM.value, team_id): | ||||||||||||||||||||||||||||||||
raise NotTeamAdminException() | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
@classmethod | ||||||||||||||||||||||||||||||||
def remove_member_from_team(cls, user_id: str, team_id: str, removed_by_user_id: str): | ||||||||||||||||||||||||||||||||
cls._validate_remove_member_permissions(user_id, team_id, removed_by_user_id) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
from todo.repositories.user_team_details_repository import UserTeamDetailsRepository | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
success = UserTeamDetailsRepository.remove_member_from_team(user_id=user_id, team_id=team_id) | ||||||||||||||||||||||||||||||||
|
@@ -494,9 +521,12 @@ def remove_member_from_team(cls, user_id: str, team_id: str, removed_by_user_id: | |||||||||||||||||||||||||||||||
AuditLogRepository.create( | ||||||||||||||||||||||||||||||||
AuditLogModel( | ||||||||||||||||||||||||||||||||
team_id=PyObjectId(team_id), | ||||||||||||||||||||||||||||||||
action="member_removed_from_team", | ||||||||||||||||||||||||||||||||
action="member_removed_from_team" if user_id != removed_by_user_id else "member_left_team", | ||||||||||||||||||||||||||||||||
performed_by=PyObjectId(removed_by_user_id) if removed_by_user_id else PyObjectId(user_id), | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
Comment on lines
521
to
527
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Minor: simplify performed_by (param is required).
- performed_by=PyObjectId(removed_by_user_id) if removed_by_user_id else PyObjectId(user_id),
+ performed_by=PyObjectId(removed_by_user_id), 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
UserRoleService.remove_all_user_roles_for_team(user_id, team_id) | ||||||||||||||||||||||||||||||||
TaskAssignmentService.reassign_tasks_from_user_to_team(user_id, team_id, removed_by_user_id) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return True |
Uh oh!
There was an error while loading. Please reload this page.