From 706782e245f3087311e0ca5a2d10f1d6ef563e3d Mon Sep 17 00:00:00 2001 From: Vaibhav Singh Date: Sun, 6 Jul 2025 16:34:26 +0530 Subject: [PATCH 1/8] fix: login issue --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 33febe73..0f9b151a 100644 --- a/.env.example +++ b/.env.example @@ -13,4 +13,4 @@ PUBLIC_KEY="generate keys and paste here" # use if required # ACCESS_LIFETIME="20" -# REFRESH_LIFETIME="30" \ No newline at end of file +# REFRESH_LIFETIME="30" From 9db52468e54d966b7701fa50a31d51adb00601e9 Mon Sep 17 00:00:00 2001 From: Vaibhav Singh Date: Sun, 6 Jul 2025 22:28:41 +0530 Subject: [PATCH 2/8] added rsa token and adjusted logic accordingly --- todo/middlewares/jwt_auth.py | 14 +++++++++----- todo/views/auth.py | 2 -- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/todo/middlewares/jwt_auth.py b/todo/middlewares/jwt_auth.py index f88e0672..a7b2a0bc 100644 --- a/todo/middlewares/jwt_auth.py +++ b/todo/middlewares/jwt_auth.py @@ -47,7 +47,8 @@ def __call__(self, request): ], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), + status=status.HTTP_401_UNAUTHORIZED ) except (TokenMissingError, TokenExpiredError, TokenInvalidError) as e: @@ -66,7 +67,8 @@ def __call__(self, request): ], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), + status=status.HTTP_401_UNAUTHORIZED ) def _try_authentication(self, request) -> bool: @@ -186,7 +188,8 @@ def _handle_rds_auth_error(self, exception): errors=[ApiErrorDetail(title=ApiErrors.AUTHENTICATION_FAILED, detail=str(exception))], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), + status=status.HTTP_401_UNAUTHORIZED ) def _handle_google_auth_error(self, exception): @@ -196,7 +199,8 @@ def _handle_google_auth_error(self, exception): errors=[ApiErrorDetail(title=ApiErrors.AUTHENTICATION_FAILED, detail=str(exception))], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), + status=status.HTTP_401_UNAUTHORIZED ) @@ -233,4 +237,4 @@ def get_current_user_info(request) -> dict: } ) - return user_info + return user_info \ No newline at end of file diff --git a/todo/views/auth.py b/todo/views/auth.py index debbd1e4..c5ee8045 100644 --- a/todo/views/auth.py +++ b/todo/views/auth.py @@ -13,7 +13,6 @@ from todo.constants.messages import ApiErrors, AppMessages from todo.middlewares.jwt_auth import get_current_user_info - class GoogleLoginView(APIView): @extend_schema( operation_id="google_login", @@ -156,7 +155,6 @@ def _set_auth_cookies(self, response, tokens): "ext-refresh", tokens["refresh_token"], max_age=settings.GOOGLE_JWT["REFRESH_TOKEN_LIFETIME"], **config ) - class GoogleLogoutView(APIView): @extend_schema( operation_id="google_logout", From 191735aca4c7bfba9aa582f81941e0c0084d2285 Mon Sep 17 00:00:00 2001 From: Vaibhav Singh Date: Sun, 6 Jul 2025 23:40:18 +0530 Subject: [PATCH 3/8] fix: tests based on updated implementation --- todo/tests/integration/base_mongo_test.py | 2 +- todo/tests/unit/views/test_auth.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/todo/tests/integration/base_mongo_test.py b/todo/tests/integration/base_mongo_test.py index 1d0914f8..530774e0 100644 --- a/todo/tests/integration/base_mongo_test.py +++ b/todo/tests/integration/base_mongo_test.py @@ -75,4 +75,4 @@ def get_user_model(self) -> UserModel: name=self.user_data["name"], createdAt=datetime.now(timezone.utc), updatedAt=datetime.now(timezone.utc), - ) + ) \ No newline at end of file diff --git a/todo/tests/unit/views/test_auth.py b/todo/tests/unit/views/test_auth.py index cb25e681..bf00fe79 100644 --- a/todo/tests/unit/views/test_auth.py +++ b/todo/tests/unit/views/test_auth.py @@ -108,6 +108,8 @@ def setUp(self): self.url = reverse("google_callback") self.factory = APIRequestFactory() self.view = GoogleCallbackView.as_view() + + self.test_user_data = users_db_data[0] self.test_user_data = users_db_data[0] @@ -148,7 +150,6 @@ def test_get_redirects_for_valid_code_and_state(self, mock_create_user, mock_han "email": self.test_user_data["email_id"], "name": self.test_user_data["name"], } - user_id = str(ObjectId()) mock_user = Mock() mock_user.id = ObjectId(user_id) From e02e1c8dee1d386c5c02f9efb34849b085b4aa8f Mon Sep 17 00:00:00 2001 From: Vaibhav Singh Date: Sun, 6 Jul 2025 23:46:14 +0530 Subject: [PATCH 4/8] fix: lint and format --- todo/middlewares/jwt_auth.py | 14 +++++--------- todo/tests/integration/base_mongo_test.py | 2 +- todo/tests/unit/views/test_auth.py | 6 +++++- todo/views/auth.py | 2 ++ 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/todo/middlewares/jwt_auth.py b/todo/middlewares/jwt_auth.py index a7b2a0bc..f88e0672 100644 --- a/todo/middlewares/jwt_auth.py +++ b/todo/middlewares/jwt_auth.py @@ -47,8 +47,7 @@ def __call__(self, request): ], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), - status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED ) except (TokenMissingError, TokenExpiredError, TokenInvalidError) as e: @@ -67,8 +66,7 @@ def __call__(self, request): ], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), - status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED ) def _try_authentication(self, request) -> bool: @@ -188,8 +186,7 @@ def _handle_rds_auth_error(self, exception): errors=[ApiErrorDetail(title=ApiErrors.AUTHENTICATION_FAILED, detail=str(exception))], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), - status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED ) def _handle_google_auth_error(self, exception): @@ -199,8 +196,7 @@ def _handle_google_auth_error(self, exception): errors=[ApiErrorDetail(title=ApiErrors.AUTHENTICATION_FAILED, detail=str(exception))], ) return JsonResponse( - data=error_response.model_dump(mode="json", exclude_none=True), - status=status.HTTP_401_UNAUTHORIZED + data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED ) @@ -237,4 +233,4 @@ def get_current_user_info(request) -> dict: } ) - return user_info \ No newline at end of file + return user_info diff --git a/todo/tests/integration/base_mongo_test.py b/todo/tests/integration/base_mongo_test.py index 530774e0..1d0914f8 100644 --- a/todo/tests/integration/base_mongo_test.py +++ b/todo/tests/integration/base_mongo_test.py @@ -75,4 +75,4 @@ def get_user_model(self) -> UserModel: name=self.user_data["name"], createdAt=datetime.now(timezone.utc), updatedAt=datetime.now(timezone.utc), - ) \ No newline at end of file + ) diff --git a/todo/tests/unit/views/test_auth.py b/todo/tests/unit/views/test_auth.py index bf00fe79..7966e967 100644 --- a/todo/tests/unit/views/test_auth.py +++ b/todo/tests/unit/views/test_auth.py @@ -108,7 +108,7 @@ def setUp(self): self.url = reverse("google_callback") self.factory = APIRequestFactory() self.view = GoogleCallbackView.as_view() - + self.test_user_data = users_db_data[0] self.test_user_data = users_db_data[0] @@ -150,6 +150,10 @@ def test_get_redirects_for_valid_code_and_state(self, mock_create_user, mock_han "email": self.test_user_data["email_id"], "name": self.test_user_data["name"], } +<<<<<<< HEAD +======= + +>>>>>>> a57ab63 (fix: lint and format) user_id = str(ObjectId()) mock_user = Mock() mock_user.id = ObjectId(user_id) diff --git a/todo/views/auth.py b/todo/views/auth.py index c5ee8045..debbd1e4 100644 --- a/todo/views/auth.py +++ b/todo/views/auth.py @@ -13,6 +13,7 @@ from todo.constants.messages import ApiErrors, AppMessages from todo.middlewares.jwt_auth import get_current_user_info + class GoogleLoginView(APIView): @extend_schema( operation_id="google_login", @@ -155,6 +156,7 @@ def _set_auth_cookies(self, response, tokens): "ext-refresh", tokens["refresh_token"], max_age=settings.GOOGLE_JWT["REFRESH_TOKEN_LIFETIME"], **config ) + class GoogleLogoutView(APIView): @extend_schema( operation_id="google_logout", From 013eea7c1e8ec0b9181db19b27a0c6d8ee21c763 Mon Sep 17 00:00:00 2001 From: lakshayman Date: Mon, 7 Jul 2025 01:25:29 +0530 Subject: [PATCH 5/8] feat: create teams --- todo/dto/responses/create_team_response.py | 7 ++ todo/dto/team_dto.py | 21 ++++++ todo/models/team.py | 38 ++++++++++ todo/repositories/team_repository.py | 79 ++++++++++++++++++++ todo/serializers/create_team_serializer.py | 12 +++ todo/services/team_service.py | 85 ++++++++++++++++++++++ todo/urls.py | 2 + todo/views/team.py | 85 ++++++++++++++++++++++ 8 files changed, 329 insertions(+) create mode 100644 todo/dto/responses/create_team_response.py create mode 100644 todo/dto/team_dto.py create mode 100644 todo/models/team.py create mode 100644 todo/repositories/team_repository.py create mode 100644 todo/serializers/create_team_serializer.py create mode 100644 todo/services/team_service.py create mode 100644 todo/views/team.py diff --git a/todo/dto/responses/create_team_response.py b/todo/dto/responses/create_team_response.py new file mode 100644 index 00000000..24e7e692 --- /dev/null +++ b/todo/dto/responses/create_team_response.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel +from todo.dto.team_dto import TeamDTO + + +class CreateTeamResponse(BaseModel): + team: TeamDTO + message: str = "Team created successfully" \ No newline at end of file diff --git a/todo/dto/team_dto.py b/todo/dto/team_dto.py new file mode 100644 index 00000000..a2b32fc1 --- /dev/null +++ b/todo/dto/team_dto.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel +from typing import List, Optional +from datetime import datetime + + +class CreateTeamDTO(BaseModel): + name: str + description: Optional[str] = None + member_ids: List[str] = [] + poc_id: str + + +class TeamDTO(BaseModel): + id: str + name: str + description: Optional[str] = None + poc_id: str + created_by: str + updated_by: str + created_at: datetime + updated_at: datetime \ No newline at end of file diff --git a/todo/models/team.py b/todo/models/team.py new file mode 100644 index 00000000..80ff06dc --- /dev/null +++ b/todo/models/team.py @@ -0,0 +1,38 @@ +from pydantic import Field +from typing import ClassVar +from datetime import datetime, timezone + +from todo.models.common.document import Document +from todo.models.common.pyobjectid import PyObjectId + + +class TeamModel(Document): + """ + Model for teams. + """ + collection_name: ClassVar[str] = "teams" + + name: str + description: str | None = None + poc_id: PyObjectId + created_by: PyObjectId + updated_by: PyObjectId + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + is_deleted: bool = False + + +class UserTeamDetailsModel(Document): + """ + Model for user-team relationships. + """ + collection_name: ClassVar[str] = "userTeamDetails" + + user_id: PyObjectId + team_id: PyObjectId + is_active: bool = True + role_id: str + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + created_by: PyObjectId + updated_by: PyObjectId \ No newline at end of file diff --git a/todo/repositories/team_repository.py b/todo/repositories/team_repository.py new file mode 100644 index 00000000..d1475e21 --- /dev/null +++ b/todo/repositories/team_repository.py @@ -0,0 +1,79 @@ +from datetime import datetime, timezone +from typing import Optional +from bson import ObjectId + +from todo.models.team import TeamModel, UserTeamDetailsModel +from todo.repositories.common.mongo_repository import MongoRepository +from todo.models.common.pyobjectid import PyObjectId + + +class TeamRepository(MongoRepository): + collection_name = TeamModel.collection_name + + @classmethod + def create(cls, team: TeamModel) -> TeamModel: + """ + Creates a new team in the repository. + """ + teams_collection = cls.get_collection() + team.created_at = datetime.now(timezone.utc) + team.updated_at = datetime.now(timezone.utc) + + team_dict = team.model_dump(mode="json", by_alias=True, exclude_none=True) + insert_result = teams_collection.insert_one(team_dict) + team.id = insert_result.inserted_id + return team + + @classmethod + def get_by_id(cls, team_id: str) -> Optional[TeamModel]: + """ + Get a team by its ID. + """ + teams_collection = cls.get_collection() + try: + team_data = teams_collection.find_one({"_id": ObjectId(team_id), "is_deleted": False}) + if team_data: + return TeamModel(**team_data) + return None + except Exception: + return None + + +class UserTeamDetailsRepository(MongoRepository): + collection_name = UserTeamDetailsModel.collection_name + + @classmethod + def create(cls, user_team: UserTeamDetailsModel) -> UserTeamDetailsModel: + """ + Creates a new user-team relationship. + """ + collection = cls.get_collection() + user_team.created_at = datetime.now(timezone.utc) + user_team.updated_at = datetime.now(timezone.utc) + + user_team_dict = user_team.model_dump(mode="json", by_alias=True, exclude_none=True) + insert_result = collection.insert_one(user_team_dict) + user_team.id = insert_result.inserted_id + return user_team + + @classmethod + def create_many(cls, user_teams: list[UserTeamDetailsModel]) -> list[UserTeamDetailsModel]: + """ + Creates multiple user-team relationships. + """ + collection = cls.get_collection() + current_time = datetime.now(timezone.utc) + + for user_team in user_teams: + user_team.created_at = current_time + user_team.updated_at = current_time + + user_teams_dicts = [user_team.model_dump(mode="json", by_alias=True, exclude_none=True) + for user_team in user_teams] + insert_result = collection.insert_many(user_teams_dicts) + + # Set the inserted IDs + for i, user_team in enumerate(user_teams): + user_team.id = insert_result.inserted_ids[i] + + return user_teams \ No newline at end of file diff --git a/todo/serializers/create_team_serializer.py b/todo/serializers/create_team_serializer.py new file mode 100644 index 00000000..5438763a --- /dev/null +++ b/todo/serializers/create_team_serializer.py @@ -0,0 +1,12 @@ +from rest_framework import serializers + + +class CreateTeamSerializer(serializers.Serializer): + name = serializers.CharField(max_length=100) + description = serializers.CharField(max_length=500, required=False, allow_blank=True) + member_ids = serializers.ListField( + child=serializers.CharField(), + required=False, + default=list + ) + poc_id = serializers.CharField(required=True, allow_null=False) \ No newline at end of file diff --git a/todo/services/team_service.py b/todo/services/team_service.py new file mode 100644 index 00000000..34bae29b --- /dev/null +++ b/todo/services/team_service.py @@ -0,0 +1,85 @@ +from todo.dto.team_dto import CreateTeamDTO, TeamDTO +from todo.dto.responses.create_team_response import CreateTeamResponse +from todo.models.team import TeamModel, UserTeamDetailsModel +from todo.models.common.pyobjectid import PyObjectId +from todo.repositories.team_repository import TeamRepository, UserTeamDetailsRepository +from todo.repositories.user_repository import UserRepository + + +class TeamService: + @classmethod + def create_team(cls, dto: CreateTeamDTO, created_by_user_id: str) -> CreateTeamResponse: + """ + Create a new team with members and POC. + """ + try: + # Validate that all member IDs exist + if dto.member_ids: + for user_id in dto.member_ids: + user = UserRepository.get_by_id(user_id) + if not user: + raise ValueError(f"User with ID {user_id} not found") + + # Validate POC exists if provided + if dto.poc_id: + poc_user = UserRepository.get_by_id(dto.poc_id) + if not poc_user: + raise ValueError(f"POC user with ID {dto.poc_id} not found") + + # Create team + team = TeamModel( + name=dto.name, + description=dto.description, + poc_id=PyObjectId(dto.poc_id) if dto.poc_id else None, + created_by=PyObjectId(created_by_user_id), + updated_by=PyObjectId(created_by_user_id) + ) + + created_team = TeamRepository.create(team) + + # Create user-team relationships + user_teams = [] + + # Add members to the team + if dto.member_ids: + for user_id in dto.member_ids: + user_team = UserTeamDetailsModel( + user_id=PyObjectId(user_id), + team_id=created_team.id, + role_id="1", + created_by=PyObjectId(created_by_user_id), + updated_by=PyObjectId(created_by_user_id) + ) + user_teams.append(user_team) + + # Add creator if not already in member_ids + if created_by_user_id not in dto.member_ids: + creator_user_team = UserTeamDetailsModel( + user_id=PyObjectId(created_by_user_id), + team_id=created_team.id, + role_id="1", + created_by=PyObjectId(created_by_user_id), + updated_by=PyObjectId(created_by_user_id) + ) + user_teams.append(creator_user_team) + + # Create all user-team relationships + if user_teams: + UserTeamDetailsRepository.create_many(user_teams) + + # Convert to DTO + team_dto = TeamDTO( + id=str(created_team.id), + name=created_team.name, + description=created_team.description, + poc_id=str(created_team.poc_id) if created_team.poc_id else None, + created_by=str(created_team.created_by), + updated_by=str(created_team.updated_by), + created_at=created_team.created_at, + updated_at=created_team.updated_at, + ) + + return CreateTeamResponse(team=team_dto) + + except Exception as e: + raise ValueError(str(e)) \ No newline at end of file diff --git a/todo/urls.py b/todo/urls.py index b56bf1e5..acfea896 100644 --- a/todo/urls.py +++ b/todo/urls.py @@ -2,6 +2,7 @@ from todo.views.task import TaskListView, TaskDetailView from todo.views.label import LabelListView from todo.views.health import HealthView +from todo.views.team import TeamListView from todo.views.auth import ( GoogleLoginView, GoogleCallbackView, @@ -12,6 +13,7 @@ urlpatterns = [ path("tasks", TaskListView.as_view(), name="tasks"), path("tasks/", TaskDetailView.as_view(), name="task_detail"), + path("teams", TeamListView.as_view(), name="teams"), path("health", HealthView.as_view(), name="health"), path("labels", LabelListView.as_view(), name="labels"), path("auth/google/login/", GoogleLoginView.as_view(), name="google_login"), diff --git a/todo/views/team.py b/todo/views/team.py new file mode 100644 index 00000000..b93ead49 --- /dev/null +++ b/todo/views/team.py @@ -0,0 +1,85 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from rest_framework.request import Request +from django.conf import settings + +from todo.serializers.create_team_serializer import CreateTeamSerializer +from todo.services.team_service import TeamService +from todo.dto.team_dto import CreateTeamDTO +from todo.dto.responses.create_team_response import CreateTeamResponse +from todo.dto.responses.error_response import ApiErrorResponse +from todo.constants.messages import ApiErrors + + +class TeamListView(APIView): + def post(self, request: Request): + """ + Create a new team. + """ + serializer = CreateTeamSerializer(data=request.data) + + if not serializer.is_valid(): + return self._handle_validation_errors(serializer.errors) + + try: + dto = CreateTeamDTO(**serializer.validated_data) + created_by_user_id = request.user_id + response: CreateTeamResponse = TeamService.create_team(dto, created_by_user_id) + + return Response( + data=response.model_dump(mode="json"), + status=status.HTTP_201_CREATED + ) + + except ValueError as e: + if isinstance(e.args[0], ApiErrorResponse): + error_response = e.args[0] + return Response( + data=error_response.model_dump(mode="json"), + status=error_response.statusCode + ) + + fallback_response = ApiErrorResponse( + statusCode=500, + message=ApiErrors.UNEXPECTED_ERROR_OCCURRED, + errors=[{"detail": str(e) if settings.DEBUG else ApiErrors.INTERNAL_SERVER_ERROR}], + ) + return Response( + data=fallback_response.model_dump(mode="json"), + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + def _handle_validation_errors(self, errors): + from todo.dto.responses.error_response import ApiErrorDetail, ApiErrorSource + + formatted_errors = [] + for field, messages in errors.items(): + if isinstance(messages, list): + for message in messages: + formatted_errors.append( + ApiErrorDetail( + source={ApiErrorSource.PARAMETER: field}, + title=ApiErrors.VALIDATION_ERROR, + detail=str(message), + ) + ) + else: + formatted_errors.append( + ApiErrorDetail( + source={ApiErrorSource.PARAMETER: field}, + title=ApiErrors.VALIDATION_ERROR, + detail=str(messages) + ) + ) + + error_response = ApiErrorResponse( + statusCode=400, + message=ApiErrors.VALIDATION_ERROR, + errors=formatted_errors + ) + + return Response( + data=error_response.model_dump(mode="json"), + status=status.HTTP_400_BAD_REQUEST + ) \ No newline at end of file From 45d29fb861eea6ffe07ddfc7d242a48364265253 Mon Sep 17 00:00:00 2001 From: lakshayman Date: Mon, 7 Jul 2025 01:41:39 +0530 Subject: [PATCH 6/8] fix: lint --- todo/dto/responses/create_team_response.py | 2 +- todo/dto/team_dto.py | 2 +- todo/models/team.py | 6 +++-- todo/repositories/team_repository.py | 18 ++++++------- todo/serializers/create_team_serializer.py | 8 ++---- todo/services/team_service.py | 21 +++++++++++---- todo/views/team.py | 30 +++++----------------- 7 files changed, 40 insertions(+), 47 deletions(-) diff --git a/todo/dto/responses/create_team_response.py b/todo/dto/responses/create_team_response.py index 24e7e692..ec47e688 100644 --- a/todo/dto/responses/create_team_response.py +++ b/todo/dto/responses/create_team_response.py @@ -4,4 +4,4 @@ class CreateTeamResponse(BaseModel): team: TeamDTO - message: str = "Team created successfully" \ No newline at end of file + message: str = "Team created successfully" diff --git a/todo/dto/team_dto.py b/todo/dto/team_dto.py index a2b32fc1..ce586dfa 100644 --- a/todo/dto/team_dto.py +++ b/todo/dto/team_dto.py @@ -18,4 +18,4 @@ class TeamDTO(BaseModel): created_by: str updated_by: str created_at: datetime - updated_at: datetime \ No newline at end of file + updated_at: datetime diff --git a/todo/models/team.py b/todo/models/team.py index 80ff06dc..3b9554f8 100644 --- a/todo/models/team.py +++ b/todo/models/team.py @@ -10,6 +10,7 @@ class TeamModel(Document): """ Model for teams. """ + collection_name: ClassVar[str] = "teams" name: str @@ -26,13 +27,14 @@ class UserTeamDetailsModel(Document): """ Model for user-team relationships. """ + collection_name: ClassVar[str] = "userTeamDetails" user_id: PyObjectId team_id: PyObjectId - is_active: bool = True + is_active: bool = True role_id: str created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) created_by: PyObjectId - updated_by: PyObjectId \ No newline at end of file + updated_by: PyObjectId diff --git a/todo/repositories/team_repository.py b/todo/repositories/team_repository.py index d1475e21..f530d659 100644 --- a/todo/repositories/team_repository.py +++ b/todo/repositories/team_repository.py @@ -4,7 +4,6 @@ from todo.models.team import TeamModel, UserTeamDetailsModel from todo.repositories.common.mongo_repository import MongoRepository -from todo.models.common.pyobjectid import PyObjectId class TeamRepository(MongoRepository): @@ -50,7 +49,7 @@ def create(cls, user_team: UserTeamDetailsModel) -> UserTeamDetailsModel: collection = cls.get_collection() user_team.created_at = datetime.now(timezone.utc) user_team.updated_at = datetime.now(timezone.utc) - + user_team_dict = user_team.model_dump(mode="json", by_alias=True, exclude_none=True) insert_result = collection.insert_one(user_team_dict) user_team.id = insert_result.inserted_id @@ -63,17 +62,18 @@ def create_many(cls, user_teams: list[UserTeamDetailsModel]) -> list[UserTeamDet """ collection = cls.get_collection() current_time = datetime.now(timezone.utc) - + for user_team in user_teams: user_team.created_at = current_time user_team.updated_at = current_time - - user_teams_dicts = [user_team.model_dump(mode="json", by_alias=True, exclude_none=True) - for user_team in user_teams] + + user_teams_dicts = [ + user_team.model_dump(mode="json", by_alias=True, exclude_none=True) for user_team in user_teams + ] insert_result = collection.insert_many(user_teams_dicts) - + # Set the inserted IDs for i, user_team in enumerate(user_teams): user_team.id = insert_result.inserted_ids[i] - - return user_teams \ No newline at end of file + + return user_teams diff --git a/todo/serializers/create_team_serializer.py b/todo/serializers/create_team_serializer.py index 5438763a..74af88d0 100644 --- a/todo/serializers/create_team_serializer.py +++ b/todo/serializers/create_team_serializer.py @@ -4,9 +4,5 @@ class CreateTeamSerializer(serializers.Serializer): name = serializers.CharField(max_length=100) description = serializers.CharField(max_length=500, required=False, allow_blank=True) - member_ids = serializers.ListField( - child=serializers.CharField(), - required=False, - default=list - ) - poc_id = serializers.CharField(required=True, allow_null=False) \ No newline at end of file + member_ids = serializers.ListField(child=serializers.CharField(), required=False, default=list) + poc_id = serializers.CharField(required=True, allow_null=False) diff --git a/todo/services/team_service.py b/todo/services/team_service.py index 34bae29b..de4be8b8 100644 --- a/todo/services/team_service.py +++ b/todo/services/team_service.py @@ -32,14 +32,14 @@ def create_team(cls, dto: CreateTeamDTO, created_by_user_id: str) -> CreateTeamR description=dto.description, poc_id=PyObjectId(dto.poc_id) if dto.poc_id else None, created_by=PyObjectId(created_by_user_id), - updated_by=PyObjectId(created_by_user_id) + updated_by=PyObjectId(created_by_user_id), ) created_team = TeamRepository.create(team) # Create user-team relationships user_teams = [] - + # Add members to the team if dto.member_ids: for user_id in dto.member_ids: @@ -48,10 +48,21 @@ def create_team(cls, dto: CreateTeamDTO, created_by_user_id: str) -> CreateTeamR team_id=created_team.id, role_id="1", created_by=PyObjectId(created_by_user_id), - updated_by=PyObjectId(created_by_user_id) + updated_by=PyObjectId(created_by_user_id), ) user_teams.append(user_team) + # Add POC if not already in member_ids + if dto.poc_id and dto.poc_id not in dto.member_ids: + poc_user_team = UserTeamDetailsModel( + user_id=PyObjectId(dto.poc_id), + team_id=created_team.id, + role_id="2", # POC role + created_by=PyObjectId(created_by_user_id), + updated_by=PyObjectId(created_by_user_id), + ) + user_teams.append(poc_user_team) + # Add creator if not already in member_ids if created_by_user_id not in dto.member_ids: creator_user_team = UserTeamDetailsModel( @@ -59,7 +70,7 @@ def create_team(cls, dto: CreateTeamDTO, created_by_user_id: str) -> CreateTeamR team_id=created_team.id, role_id="1", created_by=PyObjectId(created_by_user_id), - updated_by=PyObjectId(created_by_user_id) + updated_by=PyObjectId(created_by_user_id), ) user_teams.append(creator_user_team) @@ -82,4 +93,4 @@ def create_team(cls, dto: CreateTeamDTO, created_by_user_id: str) -> CreateTeamR return CreateTeamResponse(team=team_dto) except Exception as e: - raise ValueError(str(e)) \ No newline at end of file + raise ValueError(str(e)) diff --git a/todo/views/team.py b/todo/views/team.py index b93ead49..aeb3cfc0 100644 --- a/todo/views/team.py +++ b/todo/views/team.py @@ -27,18 +27,12 @@ def post(self, request: Request): created_by_user_id = request.user_id response: CreateTeamResponse = TeamService.create_team(dto, created_by_user_id) - return Response( - data=response.model_dump(mode="json"), - status=status.HTTP_201_CREATED - ) + return Response(data=response.model_dump(mode="json"), status=status.HTTP_201_CREATED) except ValueError as e: if isinstance(e.args[0], ApiErrorResponse): error_response = e.args[0] - return Response( - data=error_response.model_dump(mode="json"), - status=error_response.statusCode - ) + return Response(data=error_response.model_dump(mode="json"), status=error_response.statusCode) fallback_response = ApiErrorResponse( statusCode=500, @@ -46,13 +40,12 @@ def post(self, request: Request): errors=[{"detail": str(e) if settings.DEBUG else ApiErrors.INTERNAL_SERVER_ERROR}], ) return Response( - data=fallback_response.model_dump(mode="json"), - status=status.HTTP_500_INTERNAL_SERVER_ERROR + data=fallback_response.model_dump(mode="json"), status=status.HTTP_500_INTERNAL_SERVER_ERROR ) def _handle_validation_errors(self, errors): from todo.dto.responses.error_response import ApiErrorDetail, ApiErrorSource - + formatted_errors = [] for field, messages in errors.items(): if isinstance(messages, list): @@ -67,19 +60,10 @@ def _handle_validation_errors(self, errors): else: formatted_errors.append( ApiErrorDetail( - source={ApiErrorSource.PARAMETER: field}, - title=ApiErrors.VALIDATION_ERROR, - detail=str(messages) + source={ApiErrorSource.PARAMETER: field}, title=ApiErrors.VALIDATION_ERROR, detail=str(messages) ) ) - error_response = ApiErrorResponse( - statusCode=400, - message=ApiErrors.VALIDATION_ERROR, - errors=formatted_errors - ) + error_response = ApiErrorResponse(statusCode=400, message=ApiErrors.VALIDATION_ERROR, errors=formatted_errors) - return Response( - data=error_response.model_dump(mode="json"), - status=status.HTTP_400_BAD_REQUEST - ) \ No newline at end of file + return Response(data=error_response.model_dump(mode="json"), status=status.HTTP_400_BAD_REQUEST) From e614f3c6cf4501e47869319fd4d54440a5a7b8bd Mon Sep 17 00:00:00 2001 From: lakshayman Date: Mon, 7 Jul 2025 03:08:43 +0530 Subject: [PATCH 7/8] fix: korbit suggestion --- todo/constants/messages.py | 1 + todo/dto/responses/create_team_response.py | 2 +- todo/dto/team_dto.py | 32 ++++++++++++++++++++-- todo/models/team.py | 24 ++++++++++++++-- todo/services/team_service.py | 25 ++++++----------- todo/utils/google_jwt_utils.py | 21 ++++++++++---- 6 files changed, 76 insertions(+), 29 deletions(-) diff --git a/todo/constants/messages.py b/todo/constants/messages.py index 6bf38828..98e3f499 100644 --- a/todo/constants/messages.py +++ b/todo/constants/messages.py @@ -1,6 +1,7 @@ # Application Messages class AppMessages: TASK_CREATED = "Task created successfully" + TEAM_CREATED = "Team created successfully" GOOGLE_LOGIN_SUCCESS = "Successfully logged in with Google" GOOGLE_LOGOUT_SUCCESS = "Successfully logged out" TOKEN_REFRESHED = "Access token refreshed successfully" diff --git a/todo/dto/responses/create_team_response.py b/todo/dto/responses/create_team_response.py index ec47e688..78c8d4d5 100644 --- a/todo/dto/responses/create_team_response.py +++ b/todo/dto/responses/create_team_response.py @@ -4,4 +4,4 @@ class CreateTeamResponse(BaseModel): team: TeamDTO - message: str = "Team created successfully" + message: str diff --git a/todo/dto/team_dto.py b/todo/dto/team_dto.py index ce586dfa..cc6b0d63 100644 --- a/todo/dto/team_dto.py +++ b/todo/dto/team_dto.py @@ -1,14 +1,42 @@ -from pydantic import BaseModel +from pydantic import BaseModel, validator from typing import List, Optional from datetime import datetime +from todo.repositories.user_repository import UserRepository class CreateTeamDTO(BaseModel): name: str description: Optional[str] = None - member_ids: List[str] = [] + member_ids: Optional[List[str]] = None poc_id: str + @validator('member_ids') + def validate_member_ids(cls, value): + """Validate that all member IDs exist in the database.""" + if value is None: + return value + + invalid_ids = [] + for member_id in value: + user = UserRepository.get_by_id(member_id) + if not user: + invalid_ids.append(member_id) + + if invalid_ids: + raise ValueError(f'Invalid member IDs: {invalid_ids}') + return value + + @validator('poc_id') + def validate_poc_id(cls, value): + """Validate that the POC ID exists in the database.""" + if value is None: + return value + + user = UserRepository.get_by_id(value) + if not user: + raise ValueError(f'Invalid POC ID: {value}') + return value + class TeamDTO(BaseModel): id: str diff --git a/todo/models/team.py b/todo/models/team.py index 3b9554f8..97c322e6 100644 --- a/todo/models/team.py +++ b/todo/models/team.py @@ -1,4 +1,4 @@ -from pydantic import Field +from pydantic import Field, validator from typing import ClassVar from datetime import datetime, timezone @@ -13,7 +13,7 @@ class TeamModel(Document): collection_name: ClassVar[str] = "teams" - name: str + name: str = Field(..., min_length=1, max_length=100) description: str | None = None poc_id: PyObjectId created_by: PyObjectId @@ -22,13 +22,22 @@ class TeamModel(Document): updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) is_deleted: bool = False + @validator('created_by', 'updated_by') + def validate_user_id(cls, v): + """Validate that the user ID is a valid ObjectId format.""" + if v is None: + raise ValueError('User ID cannot be None') + if not PyObjectId.is_valid(v): + raise ValueError(f'Invalid user ID format: {v}') + return v + class UserTeamDetailsModel(Document): """ Model for user-team relationships. """ - collection_name: ClassVar[str] = "userTeamDetails" + collection_name: ClassVar[str] = "user_team_details" user_id: PyObjectId team_id: PyObjectId @@ -38,3 +47,12 @@ class UserTeamDetailsModel(Document): updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) created_by: PyObjectId updated_by: PyObjectId + + @validator('user_id', 'team_id', 'created_by', 'updated_by') + def validate_object_ids(cls, v): + """Validate that the ObjectId fields are in valid format.""" + if v is None: + raise ValueError('ObjectId cannot be None') + if not PyObjectId.is_valid(v): + raise ValueError(f'Invalid ObjectId format: {v}') + return v diff --git a/todo/services/team_service.py b/todo/services/team_service.py index de4be8b8..e31793ee 100644 --- a/todo/services/team_service.py +++ b/todo/services/team_service.py @@ -4,6 +4,7 @@ from todo.models.common.pyobjectid import PyObjectId from todo.repositories.team_repository import TeamRepository, UserTeamDetailsRepository from todo.repositories.user_repository import UserRepository +from todo.constants.messages import AppMessages class TeamService: @@ -13,18 +14,8 @@ def create_team(cls, dto: CreateTeamDTO, created_by_user_id: str) -> CreateTeamR Create a new team with members and POC. """ try: - # Validate that all member IDs exist - if dto.member_ids: - for user_id in dto.member_ids: - user = UserRepository.get_by_id(user_id) - if not user: - raise ValueError(f"User with ID {user_id} not found") - - # Validate POC exists if provided - if dto.poc_id: - poc_user = UserRepository.get_by_id(dto.poc_id) - if not poc_user: - raise ValueError(f"POC user with ID {dto.poc_id} not found") + # Member IDs and POC ID validation is handled at DTO level + member_ids = dto.member_ids or [] # Create team team = TeamModel( @@ -41,8 +32,8 @@ def create_team(cls, dto: CreateTeamDTO, created_by_user_id: str) -> CreateTeamR user_teams = [] # Add members to the team - if dto.member_ids: - for user_id in dto.member_ids: + if member_ids: + for user_id in member_ids: user_team = UserTeamDetailsModel( user_id=PyObjectId(user_id), team_id=created_team.id, @@ -53,7 +44,7 @@ def create_team(cls, dto: CreateTeamDTO, created_by_user_id: str) -> CreateTeamR user_teams.append(user_team) # Add POC if not already in member_ids - if dto.poc_id and dto.poc_id not in dto.member_ids: + if dto.poc_id and dto.poc_id not in member_ids: poc_user_team = UserTeamDetailsModel( user_id=PyObjectId(dto.poc_id), team_id=created_team.id, @@ -64,7 +55,7 @@ def create_team(cls, dto: CreateTeamDTO, created_by_user_id: str) -> CreateTeamR user_teams.append(poc_user_team) # Add creator if not already in member_ids - if created_by_user_id not in dto.member_ids: + if created_by_user_id not in member_ids: creator_user_team = UserTeamDetailsModel( user_id=PyObjectId(created_by_user_id), team_id=created_team.id, @@ -90,7 +81,7 @@ def create_team(cls, dto: CreateTeamDTO, created_by_user_id: str) -> CreateTeamR updated_at=created_team.updated_at, ) - return CreateTeamResponse(team=team_dto) + return CreateTeamResponse(team=team_dto, message=AppMessages.TEAM_CREATED) except Exception as e: raise ValueError(str(e)) diff --git a/todo/utils/google_jwt_utils.py b/todo/utils/google_jwt_utils.py index c4aa4375..24c8d16e 100644 --- a/todo/utils/google_jwt_utils.py +++ b/todo/utils/google_jwt_utils.py @@ -1,4 +1,5 @@ import jwt +import logging from datetime import datetime, timedelta, timezone from django.conf import settings @@ -10,6 +11,8 @@ from todo.constants.messages import AuthErrorMessages +logger = logging.getLogger(__name__) + def generate_google_access_token(user_data: dict) -> str: try: @@ -34,7 +37,8 @@ def generate_google_access_token(user_data: dict) -> str: return token except Exception as e: - raise GoogleTokenInvalidError(f"Token generation failed: {str(e)}") + logger.error(f"Token generation failed: {str(e)}") # Log the detailed error internally + raise GoogleTokenInvalidError(AuthErrorMessages.GOOGLE_TOKEN_INVALID) # Return generic message to client def generate_google_refresh_token(user_data: dict) -> str: @@ -59,7 +63,8 @@ def generate_google_refresh_token(user_data: dict) -> str: return token except Exception as e: - raise GoogleTokenInvalidError(f"Refresh token generation failed: {str(e)}") + logger.error(f"Refresh token generation failed: {str(e)}") # Log the detailed error internally + raise GoogleTokenInvalidError(AuthErrorMessages.GOOGLE_TOKEN_INVALID) # Return generic message to client def validate_google_access_token(token: str) -> dict: @@ -76,9 +81,11 @@ def validate_google_access_token(token: str) -> dict: except jwt.ExpiredSignatureError: raise GoogleTokenExpiredError() except jwt.InvalidTokenError as e: - raise GoogleTokenInvalidError(f"Invalid token: {str(e)}") + logger.error(f"Invalid token: {str(e)}") # Log the detailed error internally + raise GoogleTokenInvalidError(AuthErrorMessages.GOOGLE_TOKEN_INVALID) # Return generic message to client except Exception as e: - raise GoogleTokenInvalidError(f"Token validation failed: {str(e)}") + logger.error(f"Token validation failed: {str(e)}") # Log the detailed error internally + raise GoogleTokenInvalidError(AuthErrorMessages.GOOGLE_TOKEN_INVALID) # Return generic message to client def validate_google_refresh_token(token: str) -> dict: @@ -94,9 +101,11 @@ def validate_google_refresh_token(token: str) -> dict: except jwt.ExpiredSignatureError: raise GoogleRefreshTokenExpiredError() except jwt.InvalidTokenError as e: - raise GoogleTokenInvalidError(f"Invalid refresh token: {str(e)}") + logger.error(f"Invalid refresh token: {str(e)}") # Log the detailed error internally + raise GoogleTokenInvalidError(AuthErrorMessages.GOOGLE_TOKEN_INVALID) # Return generic message to client except Exception as e: - raise GoogleTokenInvalidError(f"Refresh token validation failed: {str(e)}") + logger.error(f"Refresh token validation failed: {str(e)}") # Log the detailed error internally + raise GoogleTokenInvalidError(AuthErrorMessages.GOOGLE_TOKEN_INVALID) # Return generic message to client def generate_google_token_pair(user_data: dict) -> dict: From 28cb7d5a0866cbd40bb8d1f8c3034f9d38227ccf Mon Sep 17 00:00:00 2001 From: lakshayman Date: Mon, 7 Jul 2025 03:10:49 +0530 Subject: [PATCH 8/8] fix: lint --- todo/dto/team_dto.py | 14 +++++++------- todo/models/team.py | 12 ++++++------ todo/services/team_service.py | 1 - 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/todo/dto/team_dto.py b/todo/dto/team_dto.py index cc6b0d63..f2a73f0b 100644 --- a/todo/dto/team_dto.py +++ b/todo/dto/team_dto.py @@ -10,31 +10,31 @@ class CreateTeamDTO(BaseModel): member_ids: Optional[List[str]] = None poc_id: str - @validator('member_ids') + @validator("member_ids") def validate_member_ids(cls, value): """Validate that all member IDs exist in the database.""" if value is None: return value - + invalid_ids = [] for member_id in value: user = UserRepository.get_by_id(member_id) if not user: invalid_ids.append(member_id) - + if invalid_ids: - raise ValueError(f'Invalid member IDs: {invalid_ids}') + raise ValueError(f"Invalid member IDs: {invalid_ids}") return value - @validator('poc_id') + @validator("poc_id") def validate_poc_id(cls, value): """Validate that the POC ID exists in the database.""" if value is None: return value - + user = UserRepository.get_by_id(value) if not user: - raise ValueError(f'Invalid POC ID: {value}') + raise ValueError(f"Invalid POC ID: {value}") return value diff --git a/todo/models/team.py b/todo/models/team.py index 97c322e6..499c3a58 100644 --- a/todo/models/team.py +++ b/todo/models/team.py @@ -22,13 +22,13 @@ class TeamModel(Document): updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) is_deleted: bool = False - @validator('created_by', 'updated_by') + @validator("created_by", "updated_by") def validate_user_id(cls, v): """Validate that the user ID is a valid ObjectId format.""" if v is None: - raise ValueError('User ID cannot be None') + raise ValueError("User ID cannot be None") if not PyObjectId.is_valid(v): - raise ValueError(f'Invalid user ID format: {v}') + raise ValueError(f"Invalid user ID format: {v}") return v @@ -48,11 +48,11 @@ class UserTeamDetailsModel(Document): created_by: PyObjectId updated_by: PyObjectId - @validator('user_id', 'team_id', 'created_by', 'updated_by') + @validator("user_id", "team_id", "created_by", "updated_by") def validate_object_ids(cls, v): """Validate that the ObjectId fields are in valid format.""" if v is None: - raise ValueError('ObjectId cannot be None') + raise ValueError("ObjectId cannot be None") if not PyObjectId.is_valid(v): - raise ValueError(f'Invalid ObjectId format: {v}') + raise ValueError(f"Invalid ObjectId format: {v}") return v diff --git a/todo/services/team_service.py b/todo/services/team_service.py index e31793ee..2af914b2 100644 --- a/todo/services/team_service.py +++ b/todo/services/team_service.py @@ -3,7 +3,6 @@ from todo.models.team import TeamModel, UserTeamDetailsModel from todo.models.common.pyobjectid import PyObjectId from todo.repositories.team_repository import TeamRepository, UserTeamDetailsRepository -from todo.repositories.user_repository import UserRepository from todo.constants.messages import AppMessages