Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update: Enhanced the vacation request, Changed the start, end dates to be datetime fields. #394

Merged
merged 1 commit into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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),
),
]
Original file line number Diff line number Diff line change
@@ -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(),
),
]
18 changes: 18 additions & 0 deletions server/cshr/migrations/0008_alter_vacation_actual_days.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
8 changes: 5 additions & 3 deletions server/cshr/models/vacations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion server/cshr/serializers/vacations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")


Expand Down
17 changes: 14 additions & 3 deletions server/cshr/utils/vacation_balance_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
30 changes: 15 additions & 15 deletions server/cshr/utils/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 19 additions & 51 deletions server/cshr/views/vacations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand All @@ -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}",
Expand All @@ -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,
Expand All @@ -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."
Expand Down
Loading