diff --git a/INSIGHTSAPI/services/tests.py b/INSIGHTSAPI/services/tests.py index 00351c6..607e3a8 100644 --- a/INSIGHTSAPI/services/tests.py +++ b/INSIGHTSAPI/services/tests.py @@ -130,7 +130,6 @@ def test_non_holiday(self): def test_get_holidays(self): """Test that the holidays are retrieved.""" - data = {"year": 2024} - response = self.client.get("/services/holidays/", data) + response = self.client.get("/services/holidays/2024/") self.assertEqual(response.status_code, 200) self.assertEqual(response.data, holidays.CO(years=2024).items()) \ No newline at end of file diff --git a/INSIGHTSAPI/services/urls.py b/INSIGHTSAPI/services/urls.py index 2e14f40..e977506 100644 --- a/INSIGHTSAPI/services/urls.py +++ b/INSIGHTSAPI/services/urls.py @@ -9,7 +9,7 @@ urlpatterns = [ path("send-ethical-line/", send_report_ethical_line, name="send_ethical_line"), path("trigger-error/", trigger_error, name="trigger_error"), - path("holidays/", get_holidays, name="get_holidays"), + path("holidays//", get_holidays, name="get_holidays"), ] if settings.DEBUG: urlpatterns.append(path("test-endpoint/", test_endpoint, name="test_endpoint")) diff --git a/INSIGHTSAPI/services/views.py b/INSIGHTSAPI/services/views.py index 850aa72..bfdf767 100644 --- a/INSIGHTSAPI/services/views.py +++ b/INSIGHTSAPI/services/views.py @@ -72,11 +72,8 @@ def trigger_error(request): raise Exception("Test error") @api_view(["GET"]) -def get_holidays(request): +def get_holidays(request, year): """Get the holidays of the year.""" - year = request.GET.get("year", None) - if year is None: - return Response({"error": "El año es requerido"}, status=400) try: year = int(year) except ValueError: diff --git a/INSIGHTSAPI/users/views.py b/INSIGHTSAPI/users/views.py index 1e2de4b..7758578 100644 --- a/INSIGHTSAPI/users/views.py +++ b/INSIGHTSAPI/users/views.py @@ -22,12 +22,14 @@ def login_staffnet(): else: url = "https://staffnet-api.cyc-bpo.com/login" response = requests.post(url, json=data) - if response.status_code != 200: + if response.status_code != 200 or "StaffNet" not in response.cookies: logger.error("Error logging in StaffNet: {}".format(response.text)) mail_admins( "Error logging in StaffNet", "Error logging in StaffNet: {}".format(response.text), ) + # delete the token to try to login again + del os.environ["StaffNetToken"] return None os.environ["StaffNetToken"] = response.cookies["StaffNet"] return True @@ -61,8 +63,10 @@ def get_profile(request): url.format(user.cedula), cookies={"StaffNet": os.environ["StaffNetToken"]}, ) - if response.status_code != 200: + if response.status_code != 200 or "error" in response.json(): logger.error("Error getting user profile: {}".format(response.text)) + # delete the token to try to login again + del os.environ["StaffNetToken"] return Response( { "error": "Encontramos un error obteniendo tu perfil, por favor intenta más tarde." @@ -143,8 +147,10 @@ def update_profile(request): }, status=400, ) - elif response.status_code != 200: + elif response.status_code != 200 or "error" in response.json(): logger.error("Error updating user profile: {}".format(response.text)) + # delete the token to try to login again + del os.environ["StaffNetToken"] return Response( { "error": "Encontramos un error actualizando tu perfil, por favor intenta más tarde." diff --git a/INSIGHTSAPI/vacation/serializers.py b/INSIGHTSAPI/vacation/serializers.py index e8e6bd6..61d9ba7 100644 --- a/INSIGHTSAPI/vacation/serializers.py +++ b/INSIGHTSAPI/vacation/serializers.py @@ -1,8 +1,10 @@ """Serializers for the vacation app.""" -import datetime +from datetime import datetime from rest_framework import serializers from users.models import User +from distutils.util import strtobool +from .utils import is_working_day, get_working_days from .models import VacationRequest @@ -59,10 +61,43 @@ def validate(self, attrs): # Check if is a creation or an update if not self.instance: # Creation - created_at = datetime.datetime.now() - if created_at.day >= 20: + created_at = datetime.now() + request = self.context["request"] + if request.data.get("mon_to_sat") is None: raise serializers.ValidationError( - "No puedes solicitar vacaciones después del día 20." + "Debes especificar si trabajas los sábados." + ) + else: + try: + mon_to_sat = bool(strtobool(request.data["mon_to_sat"])) + except ValueError: + raise serializers.ValidationError( + "Debes especificar si trabajas los sábados o no." + ) + + if not is_working_day(attrs["start_date"], mon_to_sat): + raise serializers.ValidationError( + "No puedes iniciar tus vacaciones un día no laboral." + ) + if not is_working_day(attrs["end_date"], mon_to_sat): + raise serializers.ValidationError( + "No puedes terminar tus vacaciones un día no laboral." + ) + if request.data["mon_to_sat"] == True: + if attrs["start_date"].weekday() == 5: + raise serializers.ValidationError( + "No puedes iniciar tus vacaciones un sábado." + ) + if get_working_days(attrs["start_date"], attrs["end_date"], mon_to_sat) > 15: + raise serializers.ValidationError( + "No puedes solicitar más de 15 días de vacaciones." + ) + if ( + created_at.day >= 20 + and created_at.month + 1 == attrs["start_date"].month + ): + raise serializers.ValidationError( + "Después del día 20 no puedes solicitar vacaciones para el mes siguiente." ) if ( attrs["start_date"].month == created_at.month @@ -75,15 +110,11 @@ def validate(self, attrs): raise serializers.ValidationError( "La fecha de inicio no puede ser mayor a la fecha de fin." ) - if (attrs["end_date"] - attrs["start_date"]).days > 30: + if attrs["end_date"].weekday() == 6: raise serializers.ValidationError( - "Tu solicitud no puede ser mayor a 30 días." + "No puedes terminar tus vacaciones un domingo." ) - uploaded_by = ( - self.instance.uploaded_by - if self.instance - else self.context["request"].user - ) + uploaded_by = self.instance.uploaded_by if self.instance else request.user if attrs["user"] == uploaded_by: raise serializers.ValidationError( "No puedes subir solicitudes para ti mismo." @@ -101,7 +132,7 @@ def create(self, validated_data): validated_data.pop("payroll_approbation", None) validated_data.pop("manager_approbation", None) validated_data["uploaded_by"] = self.context["request"].user - created_at = datetime.datetime.now() + created_at = datetime.now() if created_at.day >= 20: raise serializers.ValidationError( "No puedes solicitar vacaciones después del día 20." diff --git a/INSIGHTSAPI/vacation/tests.py b/INSIGHTSAPI/vacation/tests.py index 532f6ef..191192e 100644 --- a/INSIGHTSAPI/vacation/tests.py +++ b/INSIGHTSAPI/vacation/tests.py @@ -1,5 +1,6 @@ """This file contains the tests for the vacation model.""" +from django.test import TestCase from freezegun import freeze_time from services.tests import BaseTestCase from rest_framework import status @@ -7,12 +8,40 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.urls import reverse from django.db.models import Q +from .utils import is_working_day, get_working_days from .models import VacationRequest from .serializers import VacationRequestSerializer -# Avoid that if the date is after the 20th the test fails -@freeze_time("2024-07-19 10:00:00") +class WorkingDayTestCase(TestCase): + """Test module for working day utility functions.""" + + def test_is_working_day(self): + """Test the is_working_day function.""" + self.assertTrue(is_working_day("2024-01-02")) + self.assertFalse(is_working_day("2024-01-01")) + self.assertTrue(is_working_day("2024-01-05")) + self.assertTrue(is_working_day("2024-01-06")) + self.assertFalse(is_working_day("2024-01-06", False)) + + def test_get_working_days_no_sat(self): + """Test the get_working_days function.""" + self.assertEqual(get_working_days("2024-01-02", "2024-01-05", False), 4) + self.assertEqual(get_working_days("2024-01-01", "2024-01-05", False), 4) + # The 8th is a holiday + self.assertEqual(get_working_days("2024-01-01", "2024-01-09", False), 5) + self.assertEqual(get_working_days("2024-01-01", "2024-01-23", False), 15) + + def test_get_working_days_sat(self): + """Test the get_working_days function with Saturdays.""" + self.assertEqual(get_working_days("2024-01-02", "2024-01-05", True), 4) + self.assertEqual(get_working_days("2024-01-01", "2024-01-05", True), 4) + # The 8th is a holiday + self.assertEqual(get_working_days("2024-01-01", "2024-01-09", True), 6) + self.assertEqual(get_working_days("2024-01-01", "2024-01-19", True), 15) + + + class VacationRequestModelTestCase(BaseTestCase): """Test module for VacationRequest model.""" @@ -26,16 +55,15 @@ def setUp(self): self.permission = Permission.objects.get(codename="payroll_approbation") self.vacation_request = { "user": self.test_user.pk, - "start_date": "2021-01-01", - "end_date": "2021-01-05", + "start_date": "2024-01-02", + "end_date": "2024-01-18", "request_file": pdf, } def test_vacation_create(self): """Test creating a vacation endpoint.""" - self.vacation_request["hr_approbation"] = ( - True # This is a check for the serializer - ) + self.vacation_request["hr_approbation"] = True # This is just a check + self.vacation_request["mon_to_sat"] = False response = self.client.post( reverse("vacation-list"), self.vacation_request, @@ -43,10 +71,26 @@ def test_vacation_create(self): self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assertEqual(response.data["hr_approbation"], None) + def test_vacation_create_no_mon_to_sat(self): + """Test creating a vacation without mon_to_sat.""" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + self.assertEqual( + response.data["non_field_errors"][0], + "Debes especificar si trabajas los sábados.", + ) + + @freeze_time("2024-07-01 10:00:00") def test_vacation_create_same_month(self): """Test creating a vacation that spans two months.""" - self.vacation_request["start_date"] = "2024-07-01" - self.vacation_request["end_date"] = "2024-07-25" + super().setUp() + self.vacation_request["mon_to_sat"] = False + self.vacation_request["start_date"] = "2024-07-22" response = self.client.post( reverse("vacation-list"), self.vacation_request, @@ -139,9 +183,10 @@ def test_vacation_retrieve(self): ) self.assertEqual(response.data["end_date"], self.vacation_request["end_date"]) - def test_vacation_create_invalid_dates(self): - """Test creating a vacation with invalid dates.""" - self.vacation_request["start_date"] = "2021-01-06" + def test_vacation_create_end_before_start(self): + """Test creating a vacation with the end date before the start date.""" + self.vacation_request["end_date"] = "2021-01-04" + self.vacation_request["mon_to_sat"] = False response = self.client.post( reverse("vacation-list"), self.vacation_request, @@ -154,6 +199,7 @@ def test_vacation_create_invalid_dates(self): def test_vacation_create_invalid_rank(self): """Test creating a vacation with invalid rank.""" + self.vacation_request["mon_to_sat"] = False demo_user = self.test_user demo_user.job_position.rank = 8 demo_user.job_position.save() @@ -169,6 +215,7 @@ def test_vacation_create_invalid_rank(self): def test_vacation_create_invalid_user(self): """Test creating a vacation for the same user.""" + self.vacation_request["mon_to_sat"] = False self.vacation_request["user"] = self.user.pk response = self.client.post( reverse("vacation-list"), @@ -368,6 +415,40 @@ def test_vacation_payroll_approve_no_payroll(self): def test_validate_vacation_request_after_20th(self): """Test the validation of a vacation request after the 20th.""" super().setUp() + self.vacation_request["mon_to_sat"] = False + self.vacation_request["start_date"] = "2024-08-12" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + self.assertEqual( + response.data["non_field_errors"][0], + "Después del día 20 no puedes solicitar vacaciones para el mes siguiente.", + ) + + def test_validate_vacation_request_not_working_day(self): + """Test the validation of a vacation request on a non-working day.""" + self.vacation_request["mon_to_sat"] = False + self.vacation_request["start_date"] = "2024-01-01" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + self.assertEqual( + response.data["non_field_errors"][0], + "No puedes iniciar tus vacaciones un día no laboral.", + ) + + def test_validate_vacation_request_not_working_day_sat(self): + """Test the validation of a vacation request on a Saturday.""" + self.vacation_request["mon_to_sat"] = False + self.vacation_request["start_date"] = "2024-05-04" response = self.client.post( reverse("vacation-list"), self.vacation_request, @@ -375,7 +456,83 @@ def test_validate_vacation_request_after_20th(self): self.assertEqual( response.status_code, status.HTTP_400_BAD_REQUEST, response.data ) + # This is fine because if the user doesn't work on Saturdays, they can't start their vacation on a Saturday self.assertEqual( response.data["non_field_errors"][0], - "No puedes solicitar vacaciones después del día 20.", + "No puedes iniciar tus vacaciones un día no laboral.", ) + + def test_validate_vacation_request_not_working_day_sat_working(self): + """Test the validation of a vacation request on a Saturday with working Saturdays.""" + self.vacation_request["mon_to_sat"] = True + self.vacation_request["start_date"] = "2024-05-04" + self.vacation_request["end_date"] = "2024-05-06" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_201_CREATED, response.data + ) + + def test_validate_vacation_request_not_working_day_end(self): + """Test the validation of a vacation request on a non-working day.""" + self.vacation_request["mon_to_sat"] = False + self.vacation_request["end_date"] = "2024-01-01" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + self.assertEqual( + response.data["non_field_errors"][0], + "No puedes terminar tus vacaciones un día no laboral.", + ) + + def test_validate_vacation_request_not_working_day_sat_end(self): + """Test the validation of a vacation request on a Saturday.""" + self.vacation_request["mon_to_sat"] = False + self.vacation_request["end_date"] = "2024-05-04" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + # This is fine because if the user doesn't work on Saturdays, they can't end their vacation on a Saturday + self.assertEqual( + response.data["non_field_errors"][0], + "No puedes terminar tus vacaciones un día no laboral.", + ) + + def test_validate_vacation_request_not_working_day_sat_working_end(self): + """Test the validation of a vacation request on a Saturday with working Saturdays.""" + self.vacation_request["mon_to_sat"] = True + self.vacation_request["start_date"] = "2024-05-03" + self.vacation_request["end_date"] = "2024-05-04" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_201_CREATED, response.data + ) + + def test_validate_vacation_request_more_than_15_days(self): + """Test the validation of a vacation request with more than 15 days.""" + self.vacation_request["mon_to_sat"] = False + self.vacation_request["end_date"] = "2024-01-24" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + self.assertEqual( + response.data["non_field_errors"][0], + "No puedes solicitar más de 15 días de vacaciones.", + ) \ No newline at end of file diff --git a/INSIGHTSAPI/vacation/utils.py b/INSIGHTSAPI/vacation/utils.py index 65498d7..91a507e 100644 --- a/INSIGHTSAPI/vacation/utils.py +++ b/INSIGHTSAPI/vacation/utils.py @@ -1,24 +1,31 @@ """Utility functions for the vacation app.""" import holidays -from datetime import timedelta +from datetime import datetime, timedelta def is_working_day(date, sat_is_working=True): """Check if a date is a working day.""" - us_holidays = holidays.CO(years=date.year) + if isinstance(date, str): + date = datetime.strptime(date, "%Y-%m-%d") + co_holidays = holidays.CO(years=date.year) + if date in co_holidays: + return False if date.weekday() == 5: return sat_is_working - elif date.weekday() == 6: - return False - elif date in us_holidays: + if date.weekday() == 6: return False return True + def get_working_days(start_date, end_date, sat_is_working=True): """Get the number of working days between two dates.""" working_days = 0 + if isinstance(start_date, str): + start_date = datetime.strptime(start_date, "%Y-%m-%d") + if isinstance(end_date, str): + end_date = datetime.strptime(end_date, "%Y-%m-%d") for i in range((end_date - start_date).days + 1): if is_working_day(start_date + timedelta(days=i), sat_is_working): working_days += 1 - return working_days \ No newline at end of file + return working_days