Skip to content

Commit

Permalink
Development admin accessibility backend (#368)
Browse files Browse the repository at this point in the history
* Implementation: Implemented an endpoint for admins to post vacations behalf of the user.

* Enhancement: rename the route of the admin vacations request

* Typo: replaced the pinding by pending.
  • Loading branch information
Mahmoud-Emad authored Mar 6, 2024
1 parent 83d43ec commit e8a4018
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 31 deletions.
2 changes: 2 additions & 0 deletions server/cshr/routes/vacations.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
GetAdminVacationBalanceApiView,
CalculateVacationDaysApiView,
VacationBalanceAdjustmentApiView,
AdminApplyVacationForUserApiView
)

urlpatterns = [
Expand All @@ -27,4 +28,5 @@
path("reject/<str:id>/", VacationsRejectApiView.as_view()),
path("<str:id>/", VacationsHelpersApiView.as_view()),
path("comment/<str:id>/", VacationCommentsAPIView.as_view()),
path("admin/<str:user_id>/", AdminApplyVacationForUserApiView.as_view()),
]
5 changes: 5 additions & 0 deletions server/cshr/serializers/vacations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
2 changes: 1 addition & 1 deletion server/cshr/services/compensation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion server/cshr/services/hr_letters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
4 changes: 2 additions & 2 deletions server/cshr/services/official_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
12 changes: 6 additions & 6 deletions server/cshr/services/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion server/cshr/utils/redis_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion server/cshr/utils/vacation_balance_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."

Expand Down
2 changes: 1 addition & 1 deletion server/cshr/views/compensation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
165 changes: 147 additions & 18 deletions server/cshr/views/vacations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from cshr.serializers.users import TeamSerializer
from cshr.serializers.vacations import (
AdminApplyVacationForUserSerializer,
PostOfficeVacationBalanceSerializer,
GetOfficeVacationBalanceSerializer,
CalculateBalanceSerializer,
Expand Down Expand Up @@ -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,
Expand All @@ -52,7 +52,6 @@
OfficeVacationBalance,
PublicHoliday,
Vacation,
VacationBalance,
)
from cshr.services.vacations import get_vacations_by_user
from cshr.utils.redis_functions import (
Expand Down Expand Up @@ -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
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."
)

applying_user = request.user
# Check balance.
v = StanderdVacationBalance()
v.check(applying_user)
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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)

Expand Down Expand Up @@ -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."
)
Expand All @@ -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(
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 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(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."
)

# 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)

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(pending_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(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(
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)

0 comments on commit e8a4018

Please sign in to comment.