From 13c776a11c6ac1bdc83a9b27015e5e4ce08a736b Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Tue, 5 Mar 2024 15:34:25 +0200 Subject: [PATCH 1/3] Implementation: Implemented an endpoint for admins to post vacations behalf of the user. --- server/cshr/routes/vacations.py | 2 + server/cshr/serializers/vacations.py | 5 + server/cshr/utils/redis_functions.py | 2 +- server/cshr/views/vacations.py | 141 +++++++++++++++++++++++++-- 4 files changed, 143 insertions(+), 7 deletions(-) diff --git a/server/cshr/routes/vacations.py b/server/cshr/routes/vacations.py index 11f1fc2c..22989e64 100644 --- a/server/cshr/routes/vacations.py +++ b/server/cshr/routes/vacations.py @@ -12,6 +12,7 @@ GetAdminVacationBalanceApiView, CalculateVacationDaysApiView, VacationBalanceAdjustmentApiView, + AdminApplyVacationForUserApiView ) urlpatterns = [ @@ -27,4 +28,5 @@ path("reject//", VacationsRejectApiView.as_view()), path("/", VacationsHelpersApiView.as_view()), path("comment//", VacationCommentsAPIView.as_view()), + path("admin-post-for-user//", AdminApplyVacationForUserApiView.as_view()), ] diff --git a/server/cshr/serializers/vacations.py b/server/cshr/serializers/vacations.py index 18d22199..0fed7ded 100644 --- a/server/cshr/serializers/vacations.py +++ b/server/cshr/serializers/vacations.py @@ -239,3 +239,8 @@ class VacationBalanceAdjustmentSerializer(serializers.Serializer): officeId = serializers.IntegerField() value = serializers.IntegerField() reason = serializers.CharField() + +class AdminApplyVacationForUserSerializer(serializers.Serializer): + reason = serializers.CharField() + from_date = serializers.DateField() + end_date = serializers.DateField() diff --git a/server/cshr/utils/redis_functions.py b/server/cshr/utils/redis_functions.py index a864f5a6..3c14913d 100644 --- a/server/cshr/utils/redis_functions.py +++ b/server/cshr/utils/redis_functions.py @@ -23,7 +23,7 @@ def set_notification_request_redis(data: Dict) -> bool: """this function set requests notifications""" applying_user = None - if type(data["applying_user"]) is not int and data.get("applying_user").get("id"): + if type(data.get("applying_user")) is not int and data.get("applying_user").get("id"): applying_user = data["applying_user"]["id"] else: applying_user = data["applying_user"] diff --git a/server/cshr/views/vacations.py b/server/cshr/views/vacations.py index c0492b53..29bbed7a 100644 --- a/server/cshr/views/vacations.py +++ b/server/cshr/views/vacations.py @@ -1,5 +1,6 @@ from cshr.serializers.users import TeamSerializer from cshr.serializers.vacations import ( + AdminApplyVacationForUserSerializer, PostOfficeVacationBalanceSerializer, GetOfficeVacationBalanceSerializer, CalculateBalanceSerializer, @@ -27,7 +28,6 @@ from cshr.services.users import get_user_by_id, get_users_by_id from cshr.services.vacations import ( filter_balances_by_users, - get_balance_by_user, get_vacation_by_id, get_all_vacations, send_vacation_to_calendar, @@ -52,7 +52,6 @@ OfficeVacationBalance, PublicHoliday, Vacation, - VacationBalance, ) from cshr.services.vacations import get_vacations_by_user from cshr.utils.redis_functions import ( @@ -217,19 +216,20 @@ def post(self, request: Request) -> Response: if serializer.is_valid(): start_date = serializer.validated_data.get("from_date") end_date = serializer.validated_data.get("end_date") + applying_user = request.user - # Check if there are pinding vacations in the same day + # Check if there are pinding vacations in the same day. pinding_requests = Vacation.objects.filter( from_date__day=start_date.day, end_date__day=end_date.day, status=STATUS_CHOICES.PENDING, + applying_user=applying_user ) if len(pinding_requests) > 0: return CustomResponse.bad_request( message="You have a request with a pending status on the same day. Kindly address the pending requests first by either deleting them or reaching out to the administrators for approval/rejection." ) - applying_user = request.user # Check balance. v = StanderdVacationBalance() v.check(applying_user) @@ -293,11 +293,11 @@ def post(self, request: Request) -> Response: # if not sent: # return CustomResponse.bad_request(message="Error in sending email, can not sent email with this request.") - response_date: Dict = send_vacation_to_calendar(saved) + response_data: Dict = send_vacation_to_calendar(saved) return CustomResponse.success( status_code=201, message="The vacation has been posted successfully.", - data=response_date, + data=response_data, ) return CustomResponse.bad_request(error=serializer.errors) @@ -755,3 +755,132 @@ def get(self, request: Request) -> Response: # actual_days = len(actual_days) if actual_days != None else 0 return CustomResponse.success(message="Balance calculated.", data=actual_days) + +class AdminApplyVacationForUserApiView(GenericAPIView): + permission_classes = [ IsAdmin ] + serializer_class = AdminApplyVacationForUserSerializer + + def post(self, request: Request, user_id: str) -> Response: + """ + ## Apply for Vacation on Behalf of Another User + This function allows the administrator to apply for a vacation on behalf of another user, but only if they work in the same office. + ### Parameters: + - **user_id** (`int`): The ID of the user for whom the vacation is being requested. + - **reason** (`str`): A string describing the reason for the vacation request. + - **from_date** (`datetime`): The starting date of the vacation. + - **end_date** (`datetime`): The ending date of the vacation. + """ + admin = request.user + serializer = self.serializer_class(data=request.data) + + if serializer.is_valid(): + applying_user = get_user_by_id(user_id) + if applying_user is None: + return CustomResponse.not_found(message="User not found.") + + if applying_user.location.id != admin.location.id: + return CustomResponse.unauthorized(message=f"This action can only be performed by administrators who work in the {applying_user.location.name} office.") + + known_reasons = [REASON_CHOICES.ANNUAL_LEAVES, REASON_CHOICES.EMERGENCY_LEAVE, REASON_CHOICES.COMPENSATION, REASON_CHOICES.LEAVE_EXCUSES, REASON_CHOICES.UNPAID, REASON_CHOICES.SICK_LEAVES] + reason = serializer.validated_data.get('reason') + if reason not in known_reasons: + formatted_string = ', '.join(known_reasons[:-1]) + f' and {known_reasons[-1]}' + return CustomResponse.bad_request(message=f"reason {reason} is not valid, the available resons are {formatted_string}") + + from_date = serializer.validated_data.get('from_date') + end_date = serializer.validated_data.get('end_date') + + # Check if there are pinding vacations in the same day + pinding_requests = Vacation.objects.filter( + applying_user=applying_user, + from_date__day=from_date.day, + end_date__day=end_date.day, + status=STATUS_CHOICES.PENDING, + ) + + if len(pinding_requests) > 0: + return CustomResponse.bad_request( + message="The selected user has another request with the same dates and pending status. Please review their previous request before submitting a new one." + ) + + # Check balance. + v = StanderdVacationBalance() + v.check(applying_user) + + user_reason_balance = applying_user.vacationbalance + vacation_days = v.get_actual_days(applying_user, from_date, end_date) + + if reason == REASON_CHOICES.PUBLIC_HOLIDAYS: + return CustomResponse.bad_request( + message=f"You have sent an invalid reason {reason}", + error={ + "message": "This field should be one of the follwing reasons", + "reasons": [ + reason + for reason in REASON_CHOICES + if reason != REASON_CHOICES.PUBLIC_HOLIDAYS + ], + }, + ) + + curr_balance = getattr(user_reason_balance, reason) + + pinding_vacations = Vacation.objects.filter( + status=STATUS_CHOICES.PENDING, + applying_user=applying_user, + reason=reason, + ).values_list("actual_days", flat=True) + + chcked_balance = curr_balance - sum(pinding_vacations) + + if curr_balance < vacation_days: + return CustomResponse.bad_request( + message=f"The user have only {curr_balance} days left of reason '{reason.capitalize().replace('_', ' ')}'" + ) + + if chcked_balance < vacation_days: + return CustomResponse.bad_request( + message=f"The user have an additional pending request that deducts {sum(pinding_vacations)} days from your balance even though the current balance for the '{reason.capitalize().replace('_', ' ')}' category is only {curr_balance} days." + ) + + saved_vacation = Vacation.objects.create( + applying_user=applying_user, + type=TYPE_CHOICES.VACATIONS, + status=STATUS_CHOICES.PENDING, + reason=reason, + from_date=from_date, + end_date=end_date, + actual_days=vacation_days + ) + + message = f"You have successfully applied for a {reason.capitalize().replace('_', ' ')} vacation for {applying_user.full_name}." + if reason == REASON_CHOICES.ANNUAL_LEAVES or reason == REASON_CHOICES.EMERGENCY_LEAVE: + message = f"You have successfully applied for an {reason.capitalize().replace('_', ' ')} vacation for {applying_user.full_name}." + + try: + ping_redis() + except: + return http_ensure_redis_error() + + # set_notification_request_redis(serializer.data) + response_data: Dict = send_vacation_to_calendar(saved_vacation) + + # Update the balance + balance = v.check_and_update_balance( + applying_user=saved_vacation.applying_user, + vacation=saved_vacation, + reason=saved_vacation.reason, + start_date=saved_vacation.from_date, + end_date=saved_vacation.end_date, + ) + + if balance is not True: + return CustomResponse.bad_request(message=balance) + + # Approve the vacation. + saved_vacation.status = STATUS_CHOICES.APPROVED + saved_vacation.approval_user = admin + saved_vacation.save() + + return CustomResponse.success(message=message, data=response_data) + return CustomResponse.bad_request(message="Please make sure that you entered a valid data.", error=serializer.errors) \ No newline at end of file From bae0080d7774c6ace227214cc4b2b06a22ab5c11 Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Tue, 5 Mar 2024 18:13:12 +0200 Subject: [PATCH 2/3] Enhancement: rename the route of the admin vacations request --- server/cshr/routes/vacations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/cshr/routes/vacations.py b/server/cshr/routes/vacations.py index 22989e64..73268c94 100644 --- a/server/cshr/routes/vacations.py +++ b/server/cshr/routes/vacations.py @@ -28,5 +28,5 @@ path("reject//", VacationsRejectApiView.as_view()), path("/", VacationsHelpersApiView.as_view()), path("comment//", VacationCommentsAPIView.as_view()), - path("admin-post-for-user//", AdminApplyVacationForUserApiView.as_view()), + path("admin//", AdminApplyVacationForUserApiView.as_view()), ] From 188122380bcb4dddc14a90d92a684383a287b5f4 Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Wed, 6 Mar 2024 11:12:33 +0200 Subject: [PATCH 3/3] Typo: replaced the pinding by pending. --- server/cshr/services/compensation.py | 2 +- server/cshr/services/hr_letters.py | 2 +- server/cshr/services/official_documents.py | 4 +-- server/cshr/services/requests.py | 12 +++---- server/cshr/utils/vacation_balance_helper.py | 2 +- server/cshr/views/compensation.py | 2 +- server/cshr/views/vacations.py | 38 ++++++++++---------- 7 files changed, 31 insertions(+), 31 deletions(-) diff --git a/server/cshr/services/compensation.py b/server/cshr/services/compensation.py index 0309590b..4d72acce 100644 --- a/server/cshr/services/compensation.py +++ b/server/cshr/services/compensation.py @@ -15,7 +15,7 @@ def get_all_compensations() -> Compensation: return Compensation.objects.all() -def filter_all_compensations_by_pinding_status() -> Compensation: +def filter_all_compensations_by_pending_status() -> Compensation: """Return all compensations""" return Compensation.objects.filter(status=STATUS_CHOICES.PENDING) diff --git a/server/cshr/services/hr_letters.py b/server/cshr/services/hr_letters.py index eb0fd8f0..e62c4bcf 100644 --- a/server/cshr/services/hr_letters.py +++ b/server/cshr/services/hr_letters.py @@ -26,5 +26,5 @@ def filter_all_docs_based_on_user(user: User): return UserDocuments.objects.filter(user=user) -def filter_hr_letter_by_pinding_status(): +def filter_hr_letter_by_pending_status(): return HrLetters.objects.filter(status=STATUS_CHOICES.PENDING) diff --git a/server/cshr/services/official_documents.py b/server/cshr/services/official_documents.py index ea653af5..215c0b77 100644 --- a/server/cshr/services/official_documents.py +++ b/server/cshr/services/official_documents.py @@ -2,8 +2,8 @@ from cshr.models.requests import STATUS_CHOICES -def filter_all_official_docs_by_pinding_status(): - """Method helper to filter all official documents by pinding status""" +def filter_all_official_docs_by_pending_status(): + """Method helper to filter all official documents by pending status""" return OffcialDocument.objects.filter(status=STATUS_CHOICES.PENDING) diff --git a/server/cshr/services/requests.py b/server/cshr/services/requests.py index 6776c429..aee76347 100644 --- a/server/cshr/services/requests.py +++ b/server/cshr/services/requests.py @@ -3,18 +3,18 @@ from cshr.serializers.official_documents import OffcialDocumentSerializer from cshr.models.users import USER_TYPE, User from cshr.services.hr_letters import ( - filter_hr_letter_by_pinding_status, + filter_hr_letter_by_pending_status, get_hr_letter_by_user, ) from cshr.services.official_documents import ( - filter_all_official_docs_by_pinding_status, + filter_all_official_docs_by_pending_status, ) from cshr.services.vacations import ( filter_user_vacations, filter_vacations_by_pending_status, ) from cshr.services.compensation import ( - filter_all_compensations_by_pinding_status, + filter_all_compensations_by_pending_status, get_compensations_by_user, ) from typing import List, Dict @@ -42,14 +42,14 @@ def requests_format_response(user: User) -> Dict: official_docs: List[OffcialDocument] = [] elif user.user_type == USER_TYPE.ADMIN: hr_letters: List[HrLetters] = LandingPageHrLetterSerializer( - filter_hr_letter_by_pinding_status(), many=True + filter_hr_letter_by_pending_status(), many=True ).data vacations: List[Vacation] = [] official_docs: List[OffcialDocument] = OffcialDocumentSerializer( - filter_all_official_docs_by_pinding_status(), many=True + filter_all_official_docs_by_pending_status(), many=True ).data compensations: List[Compensation] = LandingPageCompensationSerializer( - filter_all_compensations_by_pinding_status(), many=True + filter_all_compensations_by_pending_status(), many=True ).data else: # ==> USER_TYPE.SUPERVISOR diff --git a/server/cshr/utils/vacation_balance_helper.py b/server/cshr/utils/vacation_balance_helper.py index 436883f7..30e74199 100644 --- a/server/cshr/utils/vacation_balance_helper.py +++ b/server/cshr/utils/vacation_balance_helper.py @@ -104,7 +104,7 @@ def check_and_update_balance( if vacation.status == STATUS_CHOICES.PENDING: new_value: int = curr_balance - vacation_days return self.update_user_balance(applying_user, reason, new_value) - return f"The vacation status is not pinding, it's {vacation.status}." + return f"The vacation status is not pending, it's {vacation.status}." return f"You only have {curr_balance} days left of reason '{reason.capitalize().replace('_', ' ')}'" return "Unknown reason." diff --git a/server/cshr/views/compensation.py b/server/cshr/views/compensation.py index 6785a206..892d9d85 100644 --- a/server/cshr/views/compensation.py +++ b/server/cshr/views/compensation.py @@ -101,7 +101,7 @@ def delete(self, request: Request, id, format=None) -> Response: return CustomResponse.not_found(message="Compensation not found") if compensation.status != STATUS_CHOICES.PENDING: return CustomResponse.bad_request( - message="You can only delete requests with pinding status." + message="You can only delete requests with pending status." ) if compensation.applying_user != request.user: return CustomResponse.unauthorized() diff --git a/server/cshr/views/vacations.py b/server/cshr/views/vacations.py index 29bbed7a..40385845 100644 --- a/server/cshr/views/vacations.py +++ b/server/cshr/views/vacations.py @@ -218,14 +218,14 @@ def post(self, request: Request) -> Response: end_date = serializer.validated_data.get("end_date") applying_user = request.user - # Check if there are pinding vacations in the same day. - pinding_requests = Vacation.objects.filter( + # Check if there are pending vacations in the same day. + pending_requests = Vacation.objects.filter( from_date__day=start_date.day, end_date__day=end_date.day, status=STATUS_CHOICES.PENDING, applying_user=applying_user ) - if len(pinding_requests) > 0: + if len(pending_requests) > 0: return CustomResponse.bad_request( message="You have a request with a pending status on the same day. Kindly address the pending requests first by either deleting them or reaching out to the administrators for approval/rejection." ) @@ -253,13 +253,13 @@ def post(self, request: Request) -> Response: curr_balance = getattr(user_reason_balance, reason) - pinding_vacations = Vacation.objects.filter( + pending_vacations = Vacation.objects.filter( status=STATUS_CHOICES.PENDING, applying_user=applying_user, reason=reason, ).values_list("actual_days", flat=True) - chcked_balance = curr_balance - sum(pinding_vacations) + chcked_balance = curr_balance - sum(pending_vacations) if curr_balance < vacation_days: return CustomResponse.bad_request( @@ -268,7 +268,7 @@ def post(self, request: Request) -> Response: if chcked_balance < vacation_days: return CustomResponse.bad_request( - message=f"You have an additional pending request that deducts {sum(pinding_vacations)} days from your balance even though the current balance for the '{reason.capitalize().replace('_', ' ')}' category is only {curr_balance} days." + message=f"You have an additional pending request that deducts {sum(pending_vacations)} days from your balance even though the current balance for the '{reason.capitalize().replace('_', ' ')}' category is only {curr_balance} days." ) saved = serializer.save( @@ -367,13 +367,13 @@ def put(self, request: Request, id: str, format=None) -> Response: start_date = serializer.validated_data.get("from_date") end_date = serializer.validated_data.get("end_date") - # Check if there are pinding vacations in the same day - pinding_requests = Vacation.objects.filter( + # Check if there are pending vacations in the same day + pending_requests = Vacation.objects.filter( from_date__day=start_date.day, end_date__day=end_date.day, status=STATUS_CHOICES.PENDING, ) - if len(pinding_requests) > 0: + if len(pending_requests) > 0: return CustomResponse.bad_request( message="You have a request with a pending status on the same day. Kindly address the pending requests first by either deleting them or reaching out to the administrators for approval/rejection." ) @@ -387,13 +387,13 @@ def put(self, request: Request, id: str, format=None) -> Response: curr_balance = getattr(user_reason_balance, reason) - pinding_vacations = Vacation.objects.filter( + pending_vacations = Vacation.objects.filter( status=STATUS_CHOICES.PENDING, applying_user=applying_user, reason=reason, ).values_list("actual_days", flat=True) - chcked_balance = curr_balance - sum(pinding_vacations) + chcked_balance = curr_balance - sum(pending_vacations) if curr_balance < vacation_days: return CustomResponse.bad_request( @@ -402,7 +402,7 @@ def put(self, request: Request, id: str, format=None) -> Response: if chcked_balance < vacation_days: return CustomResponse.bad_request( - message=f"You have an additional pending request that deducts {sum(pinding_vacations)} days from your balance even though the current balance for the '{reason.capitalize().replace('_', ' ')}' category is only {curr_balance} days." + message=f"You have an additional pending request that deducts {sum(pending_vacations)} days from your balance even though the current balance for the '{reason.capitalize().replace('_', ' ')}' category is only {curr_balance} days." ) serializer.save() @@ -518,7 +518,7 @@ def put(self, request: Request, id: str, format=None) -> Response: if vacation.status != STATUS_CHOICES.PENDING: return CustomResponse.bad_request( - message=f"The vacation status is not pinding, it's {vacation.status}." + message=f"The vacation status is not pending, it's {vacation.status}." ) current_user: User = get_user_by_id(request.user.id) @@ -790,15 +790,15 @@ def post(self, request: Request, user_id: str) -> Response: from_date = serializer.validated_data.get('from_date') end_date = serializer.validated_data.get('end_date') - # Check if there are pinding vacations in the same day - pinding_requests = Vacation.objects.filter( + # Check if there are pending vacations in the same day + pending_requests = Vacation.objects.filter( applying_user=applying_user, from_date__day=from_date.day, end_date__day=end_date.day, status=STATUS_CHOICES.PENDING, ) - if len(pinding_requests) > 0: + if len(pending_requests) > 0: return CustomResponse.bad_request( message="The selected user has another request with the same dates and pending status. Please review their previous request before submitting a new one." ) @@ -825,13 +825,13 @@ def post(self, request: Request, user_id: str) -> Response: curr_balance = getattr(user_reason_balance, reason) - pinding_vacations = Vacation.objects.filter( + pending_vacations = Vacation.objects.filter( status=STATUS_CHOICES.PENDING, applying_user=applying_user, reason=reason, ).values_list("actual_days", flat=True) - chcked_balance = curr_balance - sum(pinding_vacations) + chcked_balance = curr_balance - sum(pending_vacations) if curr_balance < vacation_days: return CustomResponse.bad_request( @@ -840,7 +840,7 @@ def post(self, request: Request, user_id: str) -> Response: if chcked_balance < vacation_days: return CustomResponse.bad_request( - message=f"The user have an additional pending request that deducts {sum(pinding_vacations)} days from your balance even though the current balance for the '{reason.capitalize().replace('_', ' ')}' category is only {curr_balance} days." + message=f"The user have an additional pending request that deducts {sum(pending_vacations)} days from your balance even though the current balance for the '{reason.capitalize().replace('_', ' ')}' category is only {curr_balance} days." ) saved_vacation = Vacation.objects.create(