From c235196990d2363c374e918e62a1e30899037a16 Mon Sep 17 00:00:00 2001 From: deepvasoya Date: Mon, 12 May 2025 18:46:11 +0530 Subject: [PATCH] feat: user routes fix: global delete filter refactor: user update schema --- apis/__init__.py | 5 ++-- apis/endpoints/auth.py | 2 +- apis/endpoints/users.py | 59 ++++++++++++++++++++++++++++++++++++++ models/CustomBase.py | 36 +++++++++++------------- schemas/UpdateSchemas.py | 8 ++++++ services/userServices.py | 61 ++++++++++++++++++++++++++++++++++++++-- 6 files changed, 145 insertions(+), 26 deletions(-) create mode 100644 apis/endpoints/users.py diff --git a/apis/__init__.py b/apis/__init__.py index 21bacbb..31502eb 100644 --- a/apis/__init__.py +++ b/apis/__init__.py @@ -5,7 +5,7 @@ from fastapi.security import HTTPBearer # Import the security scheme bearer_scheme = HTTPBearer(scheme_name="Bearer Authentication") -from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth, s3 +from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth, s3, users api_router = APIRouter() # api_router.include_router(twilio.router, prefix="/twilio") @@ -20,4 +20,5 @@ api_router.include_router( dependencies=[Depends(auth_required)], tags=["admin"]) api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) -api_router.include_router(s3.router, dependencies=[Depends(auth_required)], prefix="/s3", tags=["s3"]) \ No newline at end of file +api_router.include_router(s3.router, dependencies=[Depends(auth_required)], prefix="/s3", tags=["s3"]) +api_router.include_router(users.router, prefix="/users", tags=["users"], dependencies=[Depends(auth_required)]) diff --git a/apis/endpoints/auth.py b/apis/endpoints/auth.py index 81a967c..7717fe6 100644 --- a/apis/endpoints/auth.py +++ b/apis/endpoints/auth.py @@ -20,4 +20,4 @@ async def register(user_data: UserCreate): return ApiResponse( data="OK", message="User registered successfully" - ) + ) \ No newline at end of file diff --git a/apis/endpoints/users.py b/apis/endpoints/users.py new file mode 100644 index 0000000..4fa2848 --- /dev/null +++ b/apis/endpoints/users.py @@ -0,0 +1,59 @@ +from fastapi import APIRouter, Request, Depends + +from middleware.auth_dependency import auth_required +from services.userServices import UserServices +from schemas.ApiResponse import ApiResponse +from schemas.UpdateSchemas import UserUpdate + +router = APIRouter() + +@router.get("/") +async def get_users(): + user = await UserServices().get_users() + return ApiResponse( + data=user, + message="User fetched successfully" + ) + +@router.get("/me") +async def get_user(request: Request): + user_id = request.state.user["id"] + user = await UserServices().get_user(user_id) + return ApiResponse( + data=user, + message="User fetched successfully" + ) + +@router.get("/{user_id}") +async def get_user(request: Request, user_id: int): + user = await UserServices().get_user(user_id) + return ApiResponse( + data=user, + message="User fetched successfully" + ) + +@router.delete("/") +async def delete_user(request: Request): + user_id = request.state.user["id"] + await UserServices().delete_user(user_id) + return ApiResponse( + data="OK", + message="User deleted successfully" + ) + +@router.put("/") +async def update_user(request: Request, user_data: UserUpdate): + user_id = request.state.user["id"] + user = await UserServices().update_user(user_id, user_data) + return ApiResponse( + data=user, + message="User updated successfully" + ) + +@router.put("/{user_id}") +async def update_user(request: Request, user_id: int, user_data: UserUpdate): + user = await UserServices().update_user(user_id, user_data) + return ApiResponse( + data=user, + message="User updated successfully" + ) diff --git a/models/CustomBase.py b/models/CustomBase.py index e207040..9e55f1b 100644 --- a/models/CustomBase.py +++ b/models/CustomBase.py @@ -22,26 +22,22 @@ class CustomBase: session.add(self) session.commit() - # Global filter for deleted records - @staticmethod - @event.listens_for(Query, "before_compile", retval=True) - def _filter_deleted(query): - # Skip filtering if this query explicitly asks to include deleted items - for option in query._with_options: - if getattr(option, 'name', None) == 'include_deleted': - return query - - # Find entities that inherit from CustomBase - for entity in query._entities: - if hasattr(entity, 'entity_zero'): - entity_zero = entity.entity_zero - if hasattr(entity_zero, 'mapper') and entity_zero.mapper: - mapper = entity_zero.mapper - if issubclass(mapper.class_, CustomBase): - # Apply filter for this entity - query = query.filter(mapper.class_.deleted_at.is_(None)) - - return query +# Global filter for deleted records +@event.listens_for(SessionLocal, "do_orm_execute") +def _add_filtering_criteria(execute_state): + if ( + execute_state.is_select + and not execute_state.execution_options.get("include_deleted", False) + ): + # Check if any of the entities inherit from CustomBase + for entity in execute_state.statement.column_descriptions: + entity_class = entity.get("entity", None) + if entity_class and issubclass(entity_class, CustomBase): + # Add filter condition to exclude soft-deleted records + execute_state.statement = execute_state.statement.filter( + entity_class.deleted_at.is_(None) + ) + break # Option to include deleted records class IncludeDeleted(object): diff --git a/schemas/UpdateSchemas.py b/schemas/UpdateSchemas.py index b87e52f..77bee3c 100644 --- a/schemas/UpdateSchemas.py +++ b/schemas/UpdateSchemas.py @@ -36,3 +36,11 @@ class AppointmentUpdate(BaseModel): class CalendarUpdate(BaseModel): doc_id: Optional[int] = None rrule: Optional[str] = None + + +class UserUpdate(BaseModel): + username: Optional[str] = None + clinicRole: Optional[ClinicUserRoles] = None + userType: Optional[UserType] = None + profile_pic: Optional[str] = None + password: Optional[str] = None diff --git a/services/userServices.py b/services/userServices.py index dbe1020..ae64d10 100644 --- a/services/userServices.py +++ b/services/userServices.py @@ -7,6 +7,8 @@ from exceptions.validation_exception import ValidationException from schemas.ResponseSchemas import UserResponse from models import Clinics from enums.enums import ClinicStatus +from schemas.UpdateSchemas import UserUpdate +from exceptions.unauthorized_exception import UnauthorizedException from utils.password_utils import hash_password from schemas.CreateSchemas import UserCreate from exceptions.resource_not_found_exception import ResourceNotFoundException @@ -118,6 +120,23 @@ class UserServices: return user_response + def get_users(self, limit:int, offset:int, search:str): + query = self.db.query(Users) + if search: + query = query.filter( + or_( + Users.username.contains(search), + Users.email.contains(search), + Users.clinicRole.contains(search), + Users.userType.contains(search) + ) + ) + users = query.limit(limit).offset(offset).all() + + user_response = [UserResponse(**user.__dict__.copy()) for user in users] + + return user_response + async def get_user_by_email(self, email: str) -> UserResponse: user = self.db.query(Users).filter(Users.email == email.lower()).first() @@ -131,8 +150,44 @@ class UserServices: return user_response - def update_user(self, user_id, user): + def update_user(self, admin_id:int|None, user_id: int, user_data: UserUpdate): + + if admin_id: + admin = self.db.query(Users).filter(Users.id == admin_id).first() + + if not admin: + logger.error("Admin not found") + raise ResourceNotFoundException("Admin not found") + + if admin.userType != UserType.ADMIN: + logger.error("User is not authorized to perform this action") + raise UnauthorizedException("User is not authorized to perform this action") + + user = self.db.query(Users).filter(Users.id == user_id).first() + + if not user: + logger.error("User not found") + raise ResourceNotFoundException("User not found") + + user.username = user_data.username + user.clinicRole = user_data.clinicRole + user.userType = user_data.userType + user.profile_pic = user_data.profile_pic + + self.db.add(user) + self.db.commit() + return user - def delete_user(self, user_id): - return user + def delete_user(self, user_id: int): + user = self.db.query(Users).filter(Users.id == user_id).first() + + if not user: + logger.error("User not found") + raise ResourceNotFoundException("User not found") + + # Use the soft_delete method from CustomBase + user.soft_delete(self.db) + + return True + \ No newline at end of file