From e6e42f9847c6a60bbe454bb639206daafdf201db Mon Sep 17 00:00:00 2001 From: RicardoWebProject Date: Tue, 3 Mar 2026 16:15:10 -0600 Subject: [PATCH 01/10] room filter added; new form to query room names, and logic in view RoomsView --- pms/templates/rooms.html | 44 ++++++++++++++++++++++++++++++---------- pms/views.py | 9 ++++++-- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/pms/templates/rooms.html b/pms/templates/rooms.html index c30929f1f..8fa5b1cb6 100644 --- a/pms/templates/rooms.html +++ b/pms/templates/rooms.html @@ -2,18 +2,40 @@ {% block content %}

Habitaciones del hotel

-{% for room in rooms%} -
-
-
- {{room.name}} ({{room.room_type__name}}) -
-
- Ver detalles + +
+
+ +
+
+ +
+ {% if name_filter %} +
+ Limpiar +
+ {% endif %} +
+ +{% if rooms|length == 0 %} +
+ No se encontraron habitaciones. +
+{% else %} + {% for room in rooms%} +
+
+
+ {{room.name}} ({{room.room_type__name}}) +
+ +
- -
-{% endfor %} + {% endfor %} +{% endif %} + {% endblock content%} diff --git a/pms/views.py b/pms/views.py index f38563933..1d88770ae 100644 --- a/pms/views.py +++ b/pms/views.py @@ -8,6 +8,7 @@ from .forms import * from .models import Room from .reservation_code import generate +from .models import Room as RoomModel class BookingSearchView(View): @@ -238,9 +239,13 @@ def get(self, request, pk): class RoomsView(View): def get(self, request): - # renders a list of rooms + # renders a list of rooms, with optional name filter + name_filter = request.GET.get('name', '').strip() rooms = Room.objects.all().values("name", "room_type__name", "id") + if name_filter: + rooms = rooms.filter(name__icontains=name_filter) context = { - 'rooms': rooms + 'rooms': rooms, + 'name_filter': name_filter, } return render(request, "rooms.html", context) From 27fb4632a383a32e49d0be74115c2af043ff741c Mon Sep 17 00:00:00 2001 From: RicardoWebProject Date: Tue, 3 Mar 2026 16:36:51 -0600 Subject: [PATCH 02/10] Added occupacy percentage; new card on interface to show percentage, and added logic to view DashboardView to get te occupancy data. --- pms/templates/dashboard.html | 21 +++++++++++++++++++-- pms/views.py | 10 +++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/pms/templates/dashboard.html b/pms/templates/dashboard.html index 10f0285cc..c7fb2b7ce 100644 --- a/pms/templates/dashboard.html +++ b/pms/templates/dashboard.html @@ -2,7 +2,7 @@ {% block content %}

Dashboard

-
+
Hoy
@@ -17,11 +17,28 @@

{{dashboard.incoming_guests}}

Huéspedes saliendo

{{dashboard.outcoming_guests}}

-
Total facturado

€ {% if dashboard.invoiced.total__sum == None %}0.00{% endif %} {{dashboard.invoiced.total__sum|floatformat:2}}

+ +
+
Ocupación general
+
+
+
% Ocupación
+

{{ dashboard.occupancy }}%

+

Reservas confirmadas / habitaciones totales

+
+
+
+
+
+
+
{% endblock content%} \ No newline at end of file diff --git a/pms/views.py b/pms/views.py index 1d88770ae..77329be91 100644 --- a/pms/views.py +++ b/pms/views.py @@ -210,13 +210,18 @@ def get(self, request): .aggregate(Sum('total')) ) + # get occupancy rate: confirmed bookings / total rooms + total_rooms = RoomModel.objects.count() + confirmed_bookings = Booking.objects.filter(state="NEW").count() + occupancy = round((confirmed_bookings / total_rooms * 100), 1) if total_rooms > 0 else 0 + # preparing context data dashboard = { 'new_bookings': new_bookings, 'incoming_guests': incoming, 'outcoming_guests': outcoming, - 'invoiced': invoiced - + 'invoiced': invoiced, + 'occupancy': occupancy, } context = { @@ -224,7 +229,6 @@ def get(self, request): } return render(request, "dashboard.html", context) - class RoomDetailsView(View): def get(self, request, pk): # renders room details From 4ff738ebb3cce21d4adfe613d03a78119c769fb9 Mon Sep 17 00:00:00 2001 From: RicardoWebProject Date: Tue, 3 Mar 2026 17:06:54 -0600 Subject: [PATCH 03/10] New url added; url in order to edit booking dates. --- pms/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pms/urls.py b/pms/urls.py index c18714abf..2acbd0a22 100644 --- a/pms/urls.py +++ b/pms/urls.py @@ -8,6 +8,7 @@ path("search/booking/", views.BookingSearchView.as_view(), name="booking_search"), path("booking//", views.BookingView.as_view(), name="booking"), path("booking//edit", views.EditBookingView.as_view(), name="edit_booking"), + path("booking//edit-dates", views.EditBookingDatesView.as_view(), name="edit_booking_dates"), path("booking//delete", views.DeleteBookingView.as_view(), name="delete_booking"), path("rooms/", views.RoomsView.as_view(), name="rooms"), path("room//", views.RoomDetailsView.as_view(), name="room_details"), From 76068bbbb38abcd51b5b619def3bdc640f11a217 Mon Sep 17 00:00:00 2001 From: RicardoWebProject Date: Tue, 3 Mar 2026 17:14:11 -0600 Subject: [PATCH 04/10] New booking date form added; created check-in and check-out form fields, and validation. --- pms/forms.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pms/forms.py b/pms/forms.py index f1bc68d08..4835633e5 100644 --- a/pms/forms.py +++ b/pms/forms.py @@ -56,3 +56,32 @@ class Meta: 'total': forms.HiddenInput(), 'state': forms.HiddenInput(), } + + +class BookingDatesForm(forms.Form): + checkin = forms.DateField( + label="Fecha de entrada", + widget=forms.DateInput(attrs={ + 'type': 'date', + 'min': datetime.today().strftime('%Y-%m-%d'), + 'max': '2026-12-31', + 'class': 'form-control', + }) + ) + checkout = forms.DateField( + label="Fecha de salida", + widget=forms.DateInput(attrs={ + 'type': 'date', + 'min': datetime.today().strftime('%Y-%m-%d'), + 'max': '2026-12-31', + 'class': 'form-control', + }) + ) + + def clean(self): + cleaned_data = super().clean() + checkin = cleaned_data.get('checkin') + checkout = cleaned_data.get('checkout') + if checkin and checkout and checkout <= checkin: + raise forms.ValidationError("La fecha de salida debe ser posterior a la de entrada.") + return cleaned_data From a9ea8be1d134dbbe0b873a15c0f80eabc61c0848 Mon Sep 17 00:00:00 2001 From: RicardoWebProject Date: Tue, 3 Mar 2026 17:16:27 -0600 Subject: [PATCH 05/10] Added new edit option; Now it's possible to edit check dates --- pms/templates/home.html | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pms/templates/home.html b/pms/templates/home.html index 1e61b8024..93b56c232 100644 --- a/pms/templates/home.html +++ b/pms/templates/home.html @@ -64,16 +64,18 @@

Reservas Realizadas

- - Editar datos de contacto + {% if booking.state != "DEL" %} + Editar datos de contacto + {% endif %}
- + {% if booking.state != "DEL" %} + Editar fechas + {% endif %}
- {% if booking.state != "DEL" %} - Cancelar reserva + Cancelar reserva {% endif %}
From 1d555a7e202ee60c7ab87b80c93c9e728a8f760b Mon Sep 17 00:00:00 2001 From: RicardoWebProject Date: Tue, 3 Mar 2026 17:17:20 -0600 Subject: [PATCH 06/10] Created a new template in order to take booking info and edit dates. --- pms/templates/edit_booking_dates.html | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 pms/templates/edit_booking_dates.html diff --git a/pms/templates/edit_booking_dates.html b/pms/templates/edit_booking_dates.html new file mode 100644 index 000000000..50784dc7f --- /dev/null +++ b/pms/templates/edit_booking_dates.html @@ -0,0 +1,37 @@ +{% extends "main.html"%} + +{% block content %} +

Editar fechas de la reserva

+ +
+
Reserva {{ booking.code }} — {{ booking.room.name }}
+ + {% if error %} +
{{ error }}
+ {% endif %} + +
+ {% csrf_token %} + {% for field in form %} +
+
+ {{ field.label_tag }} +
+
+ {{ field }} + {% if field.errors %} +
{{ field.errors }}
+ {% endif %} +
+
+ {% endfor %} + {% if form.non_field_errors %} +
{{ form.non_field_errors }}
+ {% endif %} +
+ Volver + +
+
+
+{% endblock content %} From 0a2af438abd2a2080635ea3f345add2ca929152c Mon Sep 17 00:00:00 2001 From: RicardoWebProject Date: Tue, 3 Mar 2026 17:18:11 -0600 Subject: [PATCH 07/10] Improved placeholder in order to know better the function to filter --- pms/templates/main.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms/templates/main.html b/pms/templates/main.html index b2216a759..2ae00bae4 100644 --- a/pms/templates/main.html +++ b/pms/templates/main.html @@ -36,7 +36,7 @@
- +
From 3b1187e25149a6c4d71ec330e6ae68a77cc45a48 Mon Sep 17 00:00:00 2001 From: RicardoWebProject Date: Tue, 3 Mar 2026 17:35:09 -0600 Subject: [PATCH 08/10] Created a new view; EditBookingDatesView is created in order to edit check dates, and error handling if there's a conflict date. --- pms/views.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/pms/views.py b/pms/views.py index 77329be91..59167b7fb 100644 --- a/pms/views.py +++ b/pms/views.py @@ -175,6 +175,60 @@ def post(self, request, pk): return redirect("/") +class EditBookingDatesView(View): + # renders the booking dates edition form + def get(self, request, pk): + booking = Booking.objects.get(id=pk) + form = BookingDatesForm(initial={ + 'checkin': booking.checkin, + 'checkout': booking.checkout, + }) + context = { + 'form': form, + 'booking': booking, + } + return render(request, "edit_booking_dates.html", context) + + # validates availability and updates dates + def post(self, request, pk): + booking = Booking.objects.get(id=pk) + form = BookingDatesForm(request.POST) + if form.is_valid(): + checkin = form.cleaned_data['checkin'] + checkout = form.cleaned_data['checkout'] + # check availability: any NEW booking on same room in requested dates, excluding current booking + conflict = (Booking.objects + .filter( + room=booking.room, + state=Booking.NEW, + checkin__lt=checkout, + checkout__gt=checkin, + ) + .exclude(id=pk) + .exists()) + if conflict: + context = { + 'form': form, + 'booking': booking, + 'error': 'No hay disponibilidad para las fechas seleccionadas.', + } + return render(request, "edit_booking_dates.html", context) + # recalculate total + total_days = (checkout - checkin).days + new_total = total_days * booking.room.room_type.price + Booking.objects.filter(id=pk).update( + checkin=checkin, + checkout=checkout, + total=new_total, + ) + return redirect("/") + context = { + 'form': form, + 'booking': booking, + } + return render(request, "edit_booking_dates.html", context) + + class DashboardView(View): def get(self, request): from datetime import date, time, datetime From 9c9d91f470c3759ea41d09f98999fe534b5a3764 Mon Sep 17 00:00:00 2001 From: RicardoWebProject Date: Wed, 4 Mar 2026 12:28:51 -0600 Subject: [PATCH 09/10] Updated config changes --- .gitignore | 2 ++ db.sqlite3 | Bin 192512 -> 192512 bytes docker-compose.yml | 2 +- requirements.txt | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9a54ec5a8..b9ba520be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ */__pycache__/** .idea/ +*.pyc +*pyc diff --git a/db.sqlite3 b/db.sqlite3 index 28c05bb8e1e66f22eb08b66c849dd73c4d6ca7b5..cac5b62e3c0701aea827705d2448b8bd85ddccb6 100644 GIT binary patch delta 569 zcmZp8z}@hGdxA9M&xtb5tUnp_stPxzoYWKM5D;NjWh}@oj!!NvF3HbLEz)Ur(BJN$ z&v@j55Ib)f1MfB7RlH^VPx*K9_wpxh6=QnK%b2*aF_y7jONW&~TT?bDGdZy+C11g< zG&QdzwYUhvb;!)uOU}MPIzxb~Mb=~EE2ULHM zUl62*3#?v%orRf`k!|}6dnTC)z>xUMz`u<@mtTqRCf`IpH{QP+8=v#myK`_e2rDXb zGCDFc%KEv68yOgx=^7a88koXa7B+CEv4WA2m8pf5v5}s+xw)yCr4E-{R7hZezn_mM zJJ=vbCUk>L;JPiC7#6@eV51CTO|47}^^7fzO-)U;1$;d{f-D>ZeB9XJj^add6v8Z1 l5hRlgEv!r|tqhIz49tMeH_-~W@HI8|H1*hi^B$ABAOKr0l(PT; delta 158 zcmZp8z}@hGdxA9Mhlw)ItREQkyeDoqF%2<$F9G_fTT#}!gTBOpbNQ9{Zf Date: Wed, 4 Mar 2026 12:29:31 -0600 Subject: [PATCH 10/10] Created tests; created tests for the lastests changes added. --- pms/tests.py | 304 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 302 insertions(+), 2 deletions(-) diff --git a/pms/tests.py b/pms/tests.py index 7ce503c2d..3ead2678d 100644 --- a/pms/tests.py +++ b/pms/tests.py @@ -1,3 +1,303 @@ -from django.test import TestCase +from datetime import date, timedelta -# Create your tests here. +from django.test import TestCase, Client, override_settings +from django.urls import reverse + +from .models import Booking, Customer, Room, Room_type +from .forms import BookingDatesForm + + +# --------------------------------------------------------------------------- +# Base class: replaces ManifestStaticFilesStorage so templates render in tests +# --------------------------------------------------------------------------- + +@override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage') +class PmsTestCase(TestCase): + pass + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def make_room_type(name="Individual", price=20.0, max_guests=1): + return Room_type.objects.create(name=name, price=price, max_guests=max_guests) + + +def make_room(room_type, name="Room 1.1", description="Desc"): + return Room.objects.create(room_type=room_type, name=name, description=description) + + +def make_customer(name="Test User", email="test@test.com", phone="600000000"): + return Customer.objects.create(name=name, email=email, phone=phone) + + +def make_booking(room, customer, checkin, checkout, guests=1, state=Booking.NEW, total=None, code="TESTCODE"): + days = (checkout - checkin).days + return Booking.objects.create( + room=room, + customer=customer, + checkin=checkin, + checkout=checkout, + guests=guests, + state=state, + total=total if total is not None else days * room.room_type.price, + code=code, + ) + + +# =========================================================================== +# 1. Filtro de habitaciones por nombre (RoomsView) +# =========================================================================== + +class RoomsViewFilterTest(PmsTestCase): + def setUp(self): + self.client = Client() + rt = make_room_type() + self.room1 = make_room(rt, name="Room 1.1") + self.room2 = make_room(rt, name="Room 1.2") + self.room3 = make_room(rt, name="Room 2.1") + self.url = reverse("rooms") + + def test_sin_filtro_muestra_todas_las_habitaciones(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + rooms = list(response.context["rooms"]) + names = [r["name"] for r in rooms] + self.assertIn("Room 1.1", names) + self.assertIn("Room 1.2", names) + self.assertIn("Room 2.1", names) + + def test_filtro_por_nombre_parcial_devuelve_coincidencias(self): + response = self.client.get(self.url, {"name": "Room 1"}) + self.assertEqual(response.status_code, 200) + rooms = list(response.context["rooms"]) + names = [r["name"] for r in rooms] + self.assertIn("Room 1.1", names) + self.assertIn("Room 1.2", names) + self.assertNotIn("Room 2.1", names) + + def test_filtro_exacto_devuelve_una_habitacion(self): + response = self.client.get(self.url, {"name": "Room 2.1"}) + self.assertEqual(response.status_code, 200) + rooms = list(response.context["rooms"]) + self.assertEqual(len(rooms), 1) + self.assertEqual(rooms[0]["name"], "Room 2.1") + + def test_filtro_insensible_a_mayusculas(self): + response = self.client.get(self.url, {"name": "room 1"}) + self.assertEqual(response.status_code, 200) + rooms = list(response.context["rooms"]) + names = [r["name"] for r in rooms] + self.assertIn("Room 1.1", names) + self.assertIn("Room 1.2", names) + + def test_filtro_sin_resultados(self): + response = self.client.get(self.url, {"name": "XYZ"}) + self.assertEqual(response.status_code, 200) + rooms = list(response.context["rooms"]) + self.assertEqual(len(rooms), 0) + + def test_contexto_incluye_name_filter(self): + response = self.client.get(self.url, {"name": "Room 1"}) + self.assertEqual(response.context["name_filter"], "Room 1") + + def test_contexto_name_filter_vacio_sin_parametro(self): + response = self.client.get(self.url) + self.assertEqual(response.context["name_filter"], "") + + +# =========================================================================== +# 2. Widget % ocupación (DashboardView) +# =========================================================================== + +class DashboardOccupancyTest(PmsTestCase): + def setUp(self): + self.client = Client() + self.url = reverse("dashboard") + rt = make_room_type() + self.room1 = make_room(rt, name="Room 1.1") + self.room2 = make_room(rt, name="Room 1.2") + self.room3 = make_room(rt, name="Room 1.3") + self.customer = make_customer() + + def test_ocupacion_cero_sin_reservas(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context["dashboard"]["occupancy"], 0.0) + + def test_ocupacion_parcial(self): + # 1 de 3 habitaciones reservadas = 33.3 % + today = date.today() + make_booking(self.room1, self.customer, today, today + timedelta(days=2)) + response = self.client.get(self.url) + occupancy = response.context["dashboard"]["occupancy"] + self.assertAlmostEqual(occupancy, 33.3, places=1) + + def test_ocupacion_total(self): + # 3 de 3 = 100 % + today = date.today() + make_booking(self.room1, self.customer, today, today + timedelta(days=1)) + make_booking(self.room2, self.customer, today, today + timedelta(days=1), code="CODE0002") + make_booking(self.room3, self.customer, today, today + timedelta(days=1), code="CODE0003") + response = self.client.get(self.url) + self.assertEqual(response.context["dashboard"]["occupancy"], 100.0) + + def test_reservas_canceladas_no_cuentan_en_ocupacion(self): + today = date.today() + # una confirmada, una cancelada + make_booking(self.room1, self.customer, today, today + timedelta(days=1)) + make_booking(self.room2, self.customer, today, today + timedelta(days=1), + state=Booking.DELETED, code="CODE0002") + response = self.client.get(self.url) + # solo 1 confirmada / 3 habitaciones = 33.3 % + occupancy = response.context["dashboard"]["occupancy"] + self.assertAlmostEqual(occupancy, 33.3, places=1) + + def test_dashboard_renderiza_template_correcto(self): + response = self.client.get(self.url) + self.assertTemplateUsed(response, "dashboard.html") + + +# =========================================================================== +# 3. Editar fechas de reserva (EditBookingDatesView) +# =========================================================================== + +class EditBookingDatesViewTest(PmsTestCase): + def setUp(self): + self.client = Client() + rt = make_room_type(price=30.0) + self.room = make_room(rt, name="Room 1.1") + self.customer = make_customer() + today = date.today() + self.checkin = today + timedelta(days=10) + self.checkout = today + timedelta(days=13) # 3 noches + self.booking = make_booking( + self.room, self.customer, self.checkin, self.checkout + ) + self.url = reverse("edit_booking_dates", kwargs={"pk": self.booking.id}) + + # --- GET --- + + def test_get_renderiza_formulario_con_fechas_actuales(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "edit_booking_dates.html") + form = response.context["form"] + self.assertEqual(form.initial["checkin"], self.checkin) + self.assertEqual(form.initial["checkout"], self.checkout) + + def test_get_incluye_booking_en_contexto(self): + response = self.client.get(self.url) + self.assertEqual(response.context["booking"], self.booking) + + # --- POST válido sin conflicto --- + + def test_post_fechas_validas_actualiza_reserva(self): + new_checkin = self.checkin + timedelta(days=5) + new_checkout = new_checkin + timedelta(days=4) + response = self.client.post(self.url, { + "checkin": new_checkin.strftime("%Y-%m-%d"), + "checkout": new_checkout.strftime("%Y-%m-%d"), + }) + self.assertRedirects(response, "/", fetch_redirect_response=False) + self.booking.refresh_from_db() + self.assertEqual(self.booking.checkin, new_checkin) + self.assertEqual(self.booking.checkout, new_checkout) + + def test_post_recalcula_total_correctamente(self): + new_checkin = self.checkin + timedelta(days=5) + new_checkout = new_checkin + timedelta(days=2) # 2 noches × 30€ = 60€ + self.client.post(self.url, { + "checkin": new_checkin.strftime("%Y-%m-%d"), + "checkout": new_checkout.strftime("%Y-%m-%d"), + }) + self.booking.refresh_from_db() + self.assertAlmostEqual(self.booking.total, 60.0) + + # --- POST con conflicto --- + + def test_post_conflicto_muestra_error(self): + """Otra reserva ocupa las mismas fechas en la misma habitación.""" + conflict_customer = make_customer(name="Other", email="other@test.com") + make_booking( + self.room, conflict_customer, + self.checkin + timedelta(days=5), + self.checkin + timedelta(days=8), + code="CONFLICT", + ) + response = self.client.post(self.url, { + "checkin": (self.checkin + timedelta(days=5)).strftime("%Y-%m-%d"), + "checkout": (self.checkin + timedelta(days=7)).strftime("%Y-%m-%d"), + }) + self.assertEqual(response.status_code, 200) + self.assertIn("error", response.context) + self.assertIn("No hay disponibilidad", response.context["error"]) + + def test_post_conflicto_no_modifica_fechas_originales(self): + conflict_customer = make_customer(name="Other2", email="other2@test.com") + make_booking( + self.room, conflict_customer, + self.checkin + timedelta(days=5), + self.checkin + timedelta(days=8), + code="CONF002", + ) + self.client.post(self.url, { + "checkin": (self.checkin + timedelta(days=5)).strftime("%Y-%m-%d"), + "checkout": (self.checkin + timedelta(days=7)).strftime("%Y-%m-%d"), + }) + self.booking.refresh_from_db() + self.assertEqual(self.booking.checkin, self.checkin) + self.assertEqual(self.booking.checkout, self.checkout) + + def test_post_misma_reserva_no_genera_conflicto_consigo_misma(self): + """Guardar las mismas fechas no debe reportar conflicto.""" + response = self.client.post(self.url, { + "checkin": self.checkin.strftime("%Y-%m-%d"), + "checkout": self.checkout.strftime("%Y-%m-%d"), + }) + self.assertRedirects(response, "/", fetch_redirect_response=False) + + # --- Validación del formulario --- + + def test_post_checkout_anterior_a_checkin_es_invalido(self): + response = self.client.post(self.url, { + "checkin": self.checkout.strftime("%Y-%m-%d"), + "checkout": self.checkin.strftime("%Y-%m-%d"), + }) + self.assertEqual(response.status_code, 200) + form = response.context["form"] + self.assertFalse(form.is_valid()) + + def test_post_fechas_vacias_es_invalido(self): + response = self.client.post(self.url, {"checkin": "", "checkout": ""}) + self.assertEqual(response.status_code, 200) + form = response.context["form"] + self.assertFalse(form.is_valid()) + + +# =========================================================================== +# 4. BookingDatesForm – validaciones unitarias +# =========================================================================== + +class BookingDatesFormTest(TestCase): + def _form(self, checkin, checkout): + return BookingDatesForm(data={ + "checkin": checkin.strftime("%Y-%m-%d"), + "checkout": checkout.strftime("%Y-%m-%d"), + }) + + def test_formulario_valido(self): + today = date.today() + form = self._form(today + timedelta(days=1), today + timedelta(days=3)) + self.assertTrue(form.is_valid()) + + def test_checkout_igual_a_checkin_es_invalido(self): + today = date.today() + form = self._form(today + timedelta(days=1), today + timedelta(days=1)) + self.assertFalse(form.is_valid()) + + def test_checkout_anterior_a_checkin_es_invalido(self): + today = date.today() + form = self._form(today + timedelta(days=3), today + timedelta(days=1)) + self.assertFalse(form.is_valid())