From 8dd7baf99d2a2608259dfc1821584ad30691225c Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Tue, 2 Apr 2024 01:07:04 +0200 Subject: [PATCH] Update: Enhanced the vacation request, Changed the start, end dates to be datetime fields. --- ...ation_end_date_alter_vacation_from_date.py | 23 ++++++ ...ation_end_date_alter_vacation_from_date.py | 23 ++++++ .../0008_alter_vacation_actual_days.py | 18 +++++ server/cshr/models/vacations.py | 8 ++- server/cshr/serializers/vacations.py | 2 +- server/cshr/utils/vacation_balance_helper.py | 17 ++++- server/cshr/utils/wrappers.py | 30 ++++---- server/cshr/views/vacations.py | 70 +++++-------------- 8 files changed, 118 insertions(+), 73 deletions(-) create mode 100644 server/cshr/migrations/0006_alter_vacation_end_date_alter_vacation_from_date.py create mode 100644 server/cshr/migrations/0007_alter_vacation_end_date_alter_vacation_from_date.py create mode 100644 server/cshr/migrations/0008_alter_vacation_actual_days.py diff --git a/server/cshr/migrations/0006_alter_vacation_end_date_alter_vacation_from_date.py b/server/cshr/migrations/0006_alter_vacation_end_date_alter_vacation_from_date.py new file mode 100644 index 00000000..852c79b9 --- /dev/null +++ b/server/cshr/migrations/0006_alter_vacation_end_date_alter_vacation_from_date.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.9 on 2024-04-01 21:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("cshr", "0005_alter_user_social_insurance_number"), + ] + + operations = [ + migrations.AlterField( + model_name="vacation", + name="end_date", + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name="vacation", + name="from_date", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/server/cshr/migrations/0007_alter_vacation_end_date_alter_vacation_from_date.py b/server/cshr/migrations/0007_alter_vacation_end_date_alter_vacation_from_date.py new file mode 100644 index 00000000..bc75eb26 --- /dev/null +++ b/server/cshr/migrations/0007_alter_vacation_end_date_alter_vacation_from_date.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.9 on 2024-04-01 22:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("cshr", "0006_alter_vacation_end_date_alter_vacation_from_date"), + ] + + operations = [ + migrations.AlterField( + model_name="vacation", + name="end_date", + field=models.DateTimeField(), + ), + migrations.AlterField( + model_name="vacation", + name="from_date", + field=models.DateTimeField(), + ), + ] diff --git a/server/cshr/migrations/0008_alter_vacation_actual_days.py b/server/cshr/migrations/0008_alter_vacation_actual_days.py new file mode 100644 index 00000000..9eca376e --- /dev/null +++ b/server/cshr/migrations/0008_alter_vacation_actual_days.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.9 on 2024-04-01 22:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("cshr", "0007_alter_vacation_end_date_alter_vacation_from_date"), + ] + + operations = [ + migrations.AlterField( + model_name="vacation", + name="actual_days", + field=models.FloatField(default=0), + ), + ] diff --git a/server/cshr/models/vacations.py b/server/cshr/models/vacations.py index abf9b919..9b6bb011 100644 --- a/server/cshr/models/vacations.py +++ b/server/cshr/models/vacations.py @@ -33,10 +33,12 @@ class Vacation(Requests): choices=REASON_CHOICES.choices, default=REASON_CHOICES.ANNUAL_LEAVES, ) - from_date = models.DateField() - end_date = models.DateField() + + from_date = models.DateTimeField() + end_date = models.DateTimeField() + change_log = models.JSONField(default=list) - actual_days = models.IntegerField(default=0) + actual_days = models.FloatField(default=0) def ___str__(self): return self.reason diff --git a/server/cshr/serializers/vacations.py b/server/cshr/serializers/vacations.py index 0fed7ded..a26f30f0 100644 --- a/server/cshr/serializers/vacations.py +++ b/server/cshr/serializers/vacations.py @@ -18,7 +18,7 @@ class VacationsSerializer(ModelSerializer): class Meta: model = Vacation - fields = "__all__" + fields = ["id", "reason", "from_date", "end_date", "applying_user", "approval_user", "type", "status", "created_at" ] read_only_fields = ("applying_user", "approval_user", "type", "status") diff --git a/server/cshr/utils/vacation_balance_helper.py b/server/cshr/utils/vacation_balance_helper.py index cf246873..67807540 100644 --- a/server/cshr/utils/vacation_balance_helper.py +++ b/server/cshr/utils/vacation_balance_helper.py @@ -42,9 +42,9 @@ def create_new_balance(self): year=datetime.datetime.now().year, location=self.user.location )[0] - annual_leaves: int = round(office_balance.annual_leaves / 12 * month) - leave_excuses: int = round(office_balance.leave_excuses / 12 * month) - emergency_leaves: int = round(office_balance.emergency_leaves / 12 * month) + annual_leaves: int = office_balance.annual_leaves / 12 * month + leave_excuses: int = office_balance.leave_excuses / 12 * month + emergency_leaves: int = office_balance.emergency_leaves / 12 * month balance: VacationBalance = VacationBalance.objects.get_or_create( user=self.user, @@ -79,6 +79,17 @@ def update_user_balance( return True return f"There is no filed or attrbute named {reason} inside VacationBalance model." + def calculate_times(self, start_hour: str, end_hour: str, CORE_HOURS=8) -> float: + """Calculate the hours with the CORE_HOURS""" + return (end_hour - start_hour) / CORE_HOURS + + def is_valid_times(self, times: float, start_hour: str, end_hour: str,) -> bool: + """ + Calculate the times and get the actual values, e.g. the start date is 11:0 AM, and the end date is 01:00 PM then the actual time should be 2 hours. + """ + _times = self.calculate_times(start_hour=start_hour, end_hour=end_hour) + return _times == times + def get_actual_days( self, user: User, start_date: datetime, end_date: datetime ) -> int: diff --git a/server/cshr/utils/wrappers.py b/server/cshr/utils/wrappers.py index 8bfa261b..45be5786 100644 --- a/server/cshr/utils/wrappers.py +++ b/server/cshr/utils/wrappers.py @@ -19,43 +19,43 @@ class LandingPageTypeEnum(Enum): EVENT = "event" -""" -Wrap the vacation request with [type: string] field, to be ready to be sent to the calendar as the `type` field is required there. -""" def wrap_vacation_request(vacation: Vacation) -> LandingPageVacationsSerializer : # type: ignore + """ + Wrap the vacation request with [type: string] field, to be ready to be sent to the calendar as the `type` field is required there. + """ vacation_data = LandingPageVacationsSerializer(vacation).data vacation_data["type"] = LandingPageTypeEnum.VACATION.value vacation_data["applying_user_full_name"] = vacation.applying_user.full_name return vacation_data -""" -Wrap the meeting request with [type: string] field, to be ready to be sent to the calendar as the `type` field is required there. -""" def wrap_meeting_request(meeting: Meetings) -> MeetingsSerializer : # type: ignore + """ + Wrap the meeting request with [type: string] field, to be ready to be sent to the calendar as the `type` field is required there. + """ meeting_data = MeetingsSerializer(meeting).data meeting_data["type"] = LandingPageTypeEnum.MEETING.value return meeting_data -""" -Wrap the event request with [type: string] field, to be ready to be sent to the calendar as the `type` field is required there. -""" def wrap_event_request(event: Event) -> EventSerializer : # type: ignore + """ + Wrap the event request with [type: string] field, to be ready to be sent to the calendar as the `type` field is required there. + """ event_data = EventSerializer(event).data event_data["type"] = LandingPageTypeEnum.EVENT.value return event_data -""" -Wrap the event request with [type: string] field, to be ready to be sent to the calendar as the `type` field is required there. -""" def wrap_holiday_request(holiday: PublicHoliday) -> PublicHolidaySerializer : # type: ignore + """ + Wrap the event request with [type: string] field, to be ready to be sent to the calendar as the `type` field is required there. + """ holiday_data = PublicHolidaySerializer(holiday).data holiday_data["type"] = LandingPageTypeEnum.PUBLIC_HOLIDAY.value return holiday_data -""" -Wrap the birthday request with [type: string] field, to be ready to be sent to the calendar as the `type` field is required there. -""" def wrap_birthday_event(birthday: User) -> BaseUserSerializer : # type: ignore + """ + Wrap the birthday request with [type: string] field, to be ready to be sent to the calendar as the `type` field is required there. + """ today = datetime.datetime.now() birthday_data = BaseUserSerializer(birthday).data birthday_data["type"] = LandingPageTypeEnum.BIRTHDAY.value diff --git a/server/cshr/views/vacations.py b/server/cshr/views/vacations.py index 72b41b0a..2d7d7d8a 100644 --- a/server/cshr/views/vacations.py +++ b/server/cshr/views/vacations.py @@ -45,7 +45,7 @@ ) # from cshr.celery.send_email import send_email_for_request -from cshr.celery.send_email import send_email_for_reply, send_email_for_request +from cshr.celery.send_email import send_email_for_reply from cshr.models.vacations import ( REASON_CHOICES, OfficeVacationBalance, @@ -167,51 +167,6 @@ class BaseVacationsApiView(ListAPIView, GenericAPIView): def post(self, request: Request) -> Response: """Method to create a new vacation request""" - if ( - request.data.get("end_date") - and type(request.data["end_date"]) is str - and request.data.get("from_date") - and type(request.data["from_date"]) is str - ): - start_date: List[str] = request.data.get("from_date").split( - "-" - ) # Year, month, day - - end_date: List[str] = request.data.get("end_date").split( - "-" - ) # Year, month, day - - try: - converted_start_date: datetime = datetime( - year=int(start_date[0]), - month=int(start_date[1]), - day=int(start_date[2]), - ).date() - except Exception: - return CustomResponse.bad_request( - message="Invalid start date format, it must match the following pattern 'yyyy-mm-dd'.", - error=start_date, - ) - - try: - converted_end_date: datetime = datetime( - year=int(end_date[0]), month=int(end_date[1]), day=int(end_date[2]) - ).date() - except Exception: - return CustomResponse.bad_request( - message="Invalid end date format, it must match the following pattern 'yyyy-mm-dd'.", - error=start_date, - ) - - # Check if end date is lower than start date - if converted_end_date < converted_start_date: - return CustomResponse.bad_request( - message="The end date must be later than the start date." - ) - - request.data["from_date"] = converted_start_date - request.data["end_date"] = converted_end_date - serializer = self.get_serializer(data=request.data) if serializer.is_valid(): start_date = serializer.validated_data.get("from_date") @@ -236,8 +191,21 @@ def post(self, request: Request) -> Response: reason: str = serializer.validated_data.get("reason") user_reason_balance = applying_user.vacationbalance + vacation_days = v.get_actual_days(applying_user, start_date, end_date) + if start_date.day == end_date.day: + # The request is the same day + start_hour = start_date.hour + end_hour = end_date.hour + times = v.calculate_times(start_hour=start_hour, end_hour=end_hour) + if times < 1: + if not v.is_valid_times(times=times, start_hour=start_hour, end_hour=end_hour): + return CustomResponse.bad_request( + message=f"You've sent an invalid times, The days should match the {times}" + ) + vacation_days = times + if reason == REASON_CHOICES.PUBLIC_HOLIDAYS: return CustomResponse.bad_request( message=f"You have sent an invalid reason {reason}", @@ -253,6 +221,11 @@ def post(self, request: Request) -> Response: else: curr_balance = getattr(user_reason_balance, reason) + if curr_balance < vacation_days: + return CustomResponse.bad_request( + message=f"You only have {curr_balance} days left of reason '{reason.capitalize().replace('_', ' ')}'" + ) + pending_vacations = Vacation.objects.filter( status=STATUS_CHOICES.PENDING, applying_user=applying_user, @@ -261,11 +234,6 @@ def post(self, request: Request) -> Response: chcked_balance = curr_balance - sum(pending_vacations) - if curr_balance < vacation_days: - return CustomResponse.bad_request( - message=f"You only have {curr_balance} days left of reason '{reason.capitalize().replace('_', ' ')}'" - ) - if chcked_balance < vacation_days: return CustomResponse.bad_request( 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."