From f85dfa506309081a05dee0843828c79d84aa6582 Mon Sep 17 00:00:00 2001 From: Rudy Alvarado Date: Mon, 9 Mar 2026 12:48:20 -0600 Subject: [PATCH 1/2] feature: Add room name filter to rooms page with tests --- Dockerfile | 2 +- .../__pycache__/Ymd.cpython-310.pyc | Bin 782 -> 782 bytes .../__pycache__/0001_initial.cpython-310.pyc | Bin 1185 -> 1185 bytes ...room_description_room_name.cpython-310.pyc | Bin 821 -> 821 bytes ...remove_room_price_and_more.cpython-310.pyc | Bin 1073 -> 1073 bytes ...book_state_alter_book_code.cpython-310.pyc | Bin 751 -> 751 bytes ...book_code_alter_book_state.cpython-310.pyc | Bin 711 -> 711 bytes ...book_code_alter_book_state.cpython-310.pyc | Bin 717 -> 717 bytes ...ter_book_checkout_and_more.cpython-310.pyc | Bin 708 -> 708 bytes .../0008_alter_book_code.cpython-310.pyc | Bin 605 -> 605 bytes ...ter_book_checkout_and_more.cpython-310.pyc | Bin 695 -> 695 bytes ...ter_book_checkout_and_more.cpython-310.pyc | Bin 723 -> 723 bytes .../0011_alter_book_code.cpython-310.pyc | Bin 614 -> 614 bytes .../0012_booking_delete_book.cpython-310.pyc | Bin 1190 -> 1190 bytes .../0013_alter_booking_code.cpython-310.pyc | Bin 583 -> 583 bytes .../0014_alter_booking_code.cpython-310.pyc | Bin 561 -> 561 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 125 -> 125 bytes .../__pycache__/generate.cpython-310.pyc | Bin 395 -> 395 bytes pms/templates/rooms.html | 19 ++++ pms/tests.py | 64 ++++++++++++- pms/views.py | 9 +- requermientos.md | 85 ++++++++++++++++++ requirements.txt | 3 +- 23 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 requermientos.md diff --git a/Dockerfile b/Dockerfile index 5b491a37f..632144db7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:latest +FROM python:3.10-slim ENV PYTHONUNBUFFERED 1 ENV DJANGO_SETTINGS_MODULE="chapp.settings" diff --git a/pms/form_dates/__pycache__/Ymd.cpython-310.pyc b/pms/form_dates/__pycache__/Ymd.cpython-310.pyc index 92187ad49a4e008e44ca355d65dcf69aeaec70a5..d5b5a51fa738cfbeaf66f74bfa1d5a3b592735a7 100644 GIT binary patch delta 21 acmeBU>to}|=jG*M0D=OWb(tG^n3w@C6a-BG delta 21 bcmeBU>to}|=jG*M0D}EH=O%6BVPXaVG;Rd} diff --git a/pms/migrations/__pycache__/0001_initial.cpython-310.pyc b/pms/migrations/__pycache__/0001_initial.cpython-310.pyc index a99f1f151b744d033993110a3502ef722f351b69..ea38fe488dd76e9efb463e0217825685f4ea34b1 100644 GIT binary patch delta 21 bcmZ3;xsa15pO=@50SF3g)@5$wnaTnHGo=L$ delta 21 bcmZ3;xsa15pO=@50SNZ*oSU?fXDSN-IR6E~ diff --git a/pms/migrations/__pycache__/0002_book_created_room_description_room_name.cpython-310.pyc b/pms/migrations/__pycache__/0002_book_created_room_description_room_name.cpython-310.pyc index 58da46f648eea7b9823a47439643fe7228489d6a..979de1518fd88aff7926ceeaa250f9e44bcad123 100644 GIT binary patch delta 21 bcmdnWwv~-1pO=@50SF3g)@5$w(PRbyHF^ZX delta 21 bcmdnWwv~-1pO=@50SNZ*oSU?fN0S)dO%y0y#5dWM={B1V th^aGKjrppXI*7>v=A#n2K<+ILo80`A(wtN~Mj)@44M+$uaWIN80|2tz7Bm0= delta 116 zcmdnUv5|u(pO=@50SNZ*oSU?f$B2lW uyo@n}O$Nl2nJmK8uLWU(`5;0TNZjJE$<0qG%}KRm1oDbmfP?^(7&8F$T^IHM delta 111 zcmX@hdX|+ZpO=@50SNZ*oSQU}=P~20i63T53jhUPG6M-s=3DH^8TpyXsl`PgWkuYR umoa9r$%2@&lSP>NwIEC|A4JFjiCY{tx%nxjIjMGxKwdEmkPu)JV+H_c%^5QQ diff --git a/pms/migrations/__pycache__/0007_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc b/pms/migrations/__pycache__/0007_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc index b70e3b9ac5985e23133acd42ba0ec52f76f4c10e..2f77d51e1f3c125457ad6264aef2dfdf51d2603d 100644 GIT binary patch delta 63 zcmX@YdW4lHpO=@50SF3g)@4rQdC7Qd;x{$MTa#rO&$CGag^MI7e_~uA0AYgpAVO;L HDkdoaIIR#` delta 63 zcmX@YdW4lHpO=@50SNZ*oSQU}=Ots*#BXYhQIlmE&$EdGg^R=|e_~uA0AYgpAVOmD HDkdoaGdU0f diff --git a/pms/migrations/__pycache__/0008_alter_book_code.cpython-310.pyc b/pms/migrations/__pycache__/0008_alter_book_code.cpython-310.pyc index 4671a97f8808e0658bf38e7fd959b81da52d0401..4792c2044ca24e38af27db45754dc0e5ce280668 100644 GIT binary patch delta 79 zcmcc1a+ifCpO=@50SF3g)@4rQIm#F{@zQfPQJ_GP=;Zy3C0YtTEI>klNsJi)T1XNp diff --git a/pms/migrations/__pycache__/0009_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc b/pms/migrations/__pycache__/0009_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc index b2a360a865aeccff669418328f0a963a11348c4c..69805694b1f9a46af5382175466b9560105b9ce5 100644 GIT binary patch delta 58 zcmdnax}B9LpO=@50SF3g)@4rQ`Og?NnTPQ*n>bLUNPO~B#uWk(CYTQ*Bqq;gk^%sn C;SC!A delta 58 zcmdnax}B9LpO=@50SNZ*oSQU}=Rf1E$vljg*(87>MG}*rGOiGSFu{BfAvt+2lN12Y CDh^cu diff --git a/pms/migrations/__pycache__/0010_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc b/pms/migrations/__pycache__/0010_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc index e13fb03a935f941e45304f8a7dfc4877470c4196..699e0633470c2b6f48502240a018435fecc2f20f 100644 GIT binary patch delta 21 bcmcc2dYP3cpO=@50SF3g)@5$wInD$CJ3j@P delta 21 bcmcc2dYP3cpO=@50SNZ*oSU?f=QtApK$!+j diff --git a/pms/migrations/__pycache__/0011_alter_book_code.cpython-310.pyc b/pms/migrations/__pycache__/0011_alter_book_code.cpython-310.pyc index b7d323dafea7f069d1d776d2db69cbe03f129269..68a48c1b5357840b8fe9a7769b54b624a36a8598 100644 GIT binary patch delta 21 bcmaFH@{ENipO=@50SF3g)@5$wiDCi(JvRkV delta 21 bcmaFH@{ENipO=@50SNZ*oSU?fCyEIGLXidp diff --git a/pms/migrations/__pycache__/0012_booking_delete_book.cpython-310.pyc b/pms/migrations/__pycache__/0012_booking_delete_book.cpython-310.pyc index adbd8d531c054bd786eba1f6363a76e7dcc845d0..f5db74384c890af6264faf150809ac199c1da055 100644 GIT binary patch delta 21 bcmZ3+xr~!1pO=@50SF3g)@5$wnZ*JCG delta 79 zcmdnUvXO-+pO=@50SNZ*oSQU}=P+Z`#0$IFM1TTCB9kXDmTN(nU_OWt1roP7Y;yBc TN^?@}7=gTE79b(OB*qK?4jB;W diff --git a/pms/migrations/__pycache__/__init__.cpython-310.pyc b/pms/migrations/__pycache__/__init__.cpython-310.pyc index 4e4f7df87ad703f28f8fd172c0b2e99f29d0dca2..d5a09296695cccd4f1e34d0466349a127315d8aa 100644 GIT binary patch delta 18 Xcmb=e<;myeOV diff --git a/pms/reservation_code/__pycache__/generate.cpython-310.pyc b/pms/reservation_code/__pycache__/generate.cpython-310.pyc index 537e81525b3127ca467a225ea85f4341fb9e19c2..f712bda6da4bb2ab2d4820d98ea8e61a8e4c0750 100644 GIT binary patch delta 21 acmeBX?q=r6=jG*M0D=OWb(tG^>KOqqA_Tku delta 21 bcmeBX?q=r6=jG*M0D}EH=O%6Bsb>TLG<*ec diff --git a/pms/templates/rooms.html b/pms/templates/rooms.html index c30929f1f..503e80749 100644 --- a/pms/templates/rooms.html +++ b/pms/templates/rooms.html @@ -2,6 +2,25 @@ {% block content %}

Habitaciones del hotel

+ +
+
+ + +
+
+ + Limpiar +
+
+ {% for room in rooms%}
diff --git a/pms/tests.py b/pms/tests.py index 7ce503c2d..6e1ed81a2 100644 --- a/pms/tests.py +++ b/pms/tests.py @@ -1,3 +1,63 @@ -from django.test import TestCase +from django.test import TestCase, override_settings +from django.urls import reverse -# Create your tests here. +from .models import Room, Room_type + + +@override_settings( + STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage" +) +class RoomsFilterTests(TestCase): + def setUp(self): + self.room_type = Room_type.objects.create( + name="TestType", + price=10.0, + max_guests=2, + ) + Room.objects.create( + room_type=self.room_type, + name="Room 1.1", + description="Test room 1.1", + ) + Room.objects.create( + room_type=self.room_type, + name="Room 1.2", + description="Test room 1.2", + ) + Room.objects.create( + room_type=self.room_type, + name="Room 2.1", + description="Test room 2.1", + ) + + def test_rooms_without_filter_lists_all_rooms(self): + url = reverse("rooms") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Room 1.1") + self.assertContains(resp, "Room 1.2") + self.assertContains(resp, "Room 2.1") + + def test_rooms_filter_partial_name(self): + url = reverse("rooms") + resp = self.client.get(url, {"name": "Room 1"}) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Room 1.1") + self.assertContains(resp, "Room 1.2") + self.assertNotContains(resp, "Room 2.1") + + def test_rooms_filter_is_case_insensitive(self): + url = reverse("rooms") + resp = self.client.get(url, {"name": "room 1"}) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Room 1.1") + self.assertContains(resp, "Room 1.2") + self.assertNotContains(resp, "Room 2.1") + + def test_rooms_filter_empty_or_whitespace_behaves_like_no_filter(self): + url = reverse("rooms") + resp = self.client.get(url, {"name": " "}) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Room 1.1") + self.assertContains(resp, "Room 1.2") + self.assertContains(resp, "Room 2.1") diff --git a/pms/views.py b/pms/views.py index f38563933..e3b8d477b 100644 --- a/pms/views.py +++ b/pms/views.py @@ -239,8 +239,13 @@ def get(self, request, pk): class RoomsView(View): def get(self, request): # renders a list of rooms - rooms = Room.objects.all().values("name", "room_type__name", "id") + name = (request.GET.get("name") or "").strip() + rooms_qs = Room.objects.all() + if name: + rooms_qs = rooms_qs.filter(name__icontains=name) + rooms = rooms_qs.order_by("name").values("name", "room_type__name", "id") context = { - 'rooms': rooms + 'rooms': rooms, + 'name': name, } return render(request, "rooms.html", context) diff --git a/requermientos.md b/requermientos.md new file mode 100644 index 000000000..9e28d0081 --- /dev/null +++ b/requermientos.md @@ -0,0 +1,85 @@ +**Prueba de selección** + +*Buscador de reservas* + +El proyecto consiste en una aplicación Django de un motor de reservas para un hotel que ya ha sido desarrollada y alojada en Github. + +**Enlace al repositorio**: https://github.com/oscarchapp/testbookingengine + +En el *README* del repositorio se encuentra la información para desplegar la aplicación en local. También se incluye un backup de SQLite con la configuración inicial. + +**Descripción del proyecto actual** + +La pantalla principal de la aplicación es un listado de reservas. + +Una reserva consiste de: + +● fecha entrada + +● fecha salida + +● tipo de habitación + +● nº huéspedes + +● datos de contacto (nombre, email, teléfono) + +● precio total de la reserva + +● localizador + +● nº de habitación (opcional) + +La funcionalidad principal es la de crear una nueva reserva. En la parte superior hay un botón de “Nueva reserva” que lleva a una pantalla que permite introducir 2 fechas (entrada y salida) y el número de huéspedes. Al buscar entre 2 fechas se muestra la siguiente información: ● Tipo de habitación + +● Nº de habitaciones disponibles para este rango de fechas + +● Precio total de la estancia + +● Un botón para seleccionar esa habitación + +Tras seleccionar la habitación deseada, se muestra un formulario donde se debe introducir los datos de contacto para la reserva (nombre, email, teléfono). Al finalizar el formulario se creará la reserva (con un localizador alfanumérico único) y se mostrará la pantalla con el listado de reservas. +Algunos detalles a tener en cuenta: + +● Hay 4 tipos de habitaciones: 10 individuales, 5 dobles, 4 triples, 6 cuádruples. ● El precio diario de cada tipo de habitación es: individual=20€/día, doble=30€/día, triple=40€/día, cuádruple=50€/día + +● En una habitación individual sólo cabe 1 persona (huésped). En una doble caben 1 o 2 personas. En una Triple caben 1, 2, o 3 personas. En una cuádruple caben 1, 2, 3 o 4 personas. Por lo tanto si el usuario hace una búsqueda para 3 personas, solo deberá mostrar habitaciones triples y cuádruples (siempre y cuando estén disponibles para esas fechas) . + +● A medida que se vayan creando reservas, debe descontarse del número de habitaciones disponibles de ese tipo para ese rango de fechas. Solo se podrán crear reservas desde la fecha actual hasta el 31/12/2026. + +**Requisitos de la prueba:** + +A continuación, se describen algunos ejercicios sobre cambios de funcionalidad en la aplicación existente. Se requiere que el candidato cree pull-request por cada uno y se valorará la calidad del código, buenas prácticas y la inclusión de tests. + +Cualquier cambio añadido sobre lo aquí explicado que suponga una mejora en la aplicación será bienvenida y tenida en cuenta. + +1\. **Filtrar panel de habitaciones**: + +En la sección de Habitaciones, actualmente se muestra una lista completa de todas las habitaciones existentes, con su nombre (ej Room 1.1). + +Se debe añadir un formulario en la parte superior para poder filtrar los resultados de la lista, permitiendo buscar únicamente por el nombre de la habitación. Dicho filtro debe buscar habitaciones cuyo campo “name” contenga el texto introducido en el formulario. Por ejemplo, si se introduce “Room 1”, se mostrarán las habitaciones “Room 1.1”, “Room 1.2”, etc, pero no otras como “Room 2.1”. + +*Nota*: se puede realizar de cualquier forma, bien por un formulario HTML que envíe un POST a la view, con Javascript y AJAX o cualquier otra que el candidato considere mejor. El diseño de la interfaz no es importante, puede ser un formulario básico sin estilo. + +**2\. Añadir porcentaje de ocupación** + +En la sección de Dashboard, se muestran varios “widgets” con datos de las reservas actuales. +Se requiere añadir un nuevo widget llamado “% ocupación”, cuyo cálculo es el siguiente: *Cantidad total de reservas en estado confirmada / número de habitaciones existentes.* + +Nota: Adaptar el diseño del Dashboard para mostrar este nuevo valor en la interfaz, alineado al resto de elementos o de la forma que considere mejor el candidato. + +**3\. Edición de reservas** + +En la pantalla principal se muestran todas las reservas realizadas y se permite actualmente poder editar los datos de contacto. + +Se requiere añadir una nueva funcionalidad para editar las fechas de la reserva, añadiendo un enlace a cada reserva similar al de “Editar datos de contacto”. Dicho enlace debe llevar una nueva página con un formulario similar al de crear nueva reserva, pero solo con los campos de fecha de entrada y salida y un botón de “Guardar”. + +Además de editar los campos de la reserva, se debe realizar la misma validación que al crear una nueva reserva, es decir, comprobar que la habitación actual está disponible en las nuevas fechas seleccionadas. Si existen otras reservas ocupando en las mismas fechas, se debe mostrar un error: “No hay disponibilidad para las fechas seleccionadas”. + +**Instrucciones para la prueba técnica** + +En el repositorio mencionado encontrarás el punto de partida para la prueba donde debes crear los pull requests. + +Tu objetivo es completar las 3 tareas que indicamos en este documento. No hay una única forma correcta de abordarla. Nos interesa ver cómo trabajas, cómo estructuras tus cambios y cómo los presentas al equipo. + +Piensa en esta prueba como una pequeña simulación del día a día en el trabajo. Cuando hayas terminado, recuerda indicarnos tu nombre de usuario en Github. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 148e2c607..1537a31e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ asgiref==3.5.0 Django==4.0.2 +gunicorn==20.1.0 sqlparse==0.4.2 -whitenoise +whitenoise==6.0.0 From 9b5da17e87212fb80cfd2490f0b02792cb882e48 Mon Sep 17 00:00:00 2001 From: Rudy Alvarado Date: Mon, 9 Mar 2026 14:14:33 -0600 Subject: [PATCH 2/2] Add dashboard occupancy percentage widget with tests --- db.sqlite3 | Bin 192512 -> 192512 bytes pms/templates/dashboard.html | 5 +++ pms/tests.py | 85 ++++++++++++++++++++++++++++++++++- pms/views.py | 12 ++++- 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/db.sqlite3 b/db.sqlite3 index 28c05bb8e1e66f22eb08b66c849dd73c4d6ca7b5..3c84196a351415b5bc3435096149f740b35154d7 100644 GIT binary patch delta 304 zcmZp8z}@hGdxA9Mr-?Gote+V4stPxzoYWI$6A)omWh}@oj!!NvF3HbLEz)Xs(BJN$ z&v@hlA1iMe1MfB7RlH^VPx&`b6<`wCs>XDemvQsP#`TOw23o8P>ZXc8r74vPjyYwC zMTsf-8JVT!nR%IdhUO0Gxrv!Mddc~@CWfXKhK2@)1_s-o@-xW`@UikgV&MP9|Aqe@ z{|o*{n*|Fl@UyWpGcsziO@E=!BmFt&lEf{}rhp|O>L crJkXQsgb#<7I$E{shO#XumAR&_n6cL0W!WGx;0u7@3y>QD@R1xt1@HhaOOdf;$u1KG5EKI?0dQ?|Ut@K1bZ>2Caw&sA zFSkH10mRS_1Pyio4b}~+4R#OZ40W{<0`Uz2<*|X`0k`E30v`|z1P|x{5Bd-E5AF}> zvk`FE50~gK0vreg59k07=nwk04ln}L50Ov^x9C3t9FPnI4%q+?whwI&A`aQHfyoZH I+1>&t5rh#o&j0`b diff --git a/pms/templates/dashboard.html b/pms/templates/dashboard.html index 10f0285cc..a9ca00581 100644 --- a/pms/templates/dashboard.html +++ b/pms/templates/dashboard.html @@ -22,6 +22,11 @@

{{dashboard.outcoming_guests}}

Total facturado

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

+ +
+
% ocupación
+

{{dashboard.occupancy_pct|floatformat:2}}%

+
{% endblock content%} \ No newline at end of file diff --git a/pms/tests.py b/pms/tests.py index 6e1ed81a2..2fa81d759 100644 --- a/pms/tests.py +++ b/pms/tests.py @@ -1,7 +1,9 @@ +from datetime import date, timedelta + from django.test import TestCase, override_settings from django.urls import reverse -from .models import Room, Room_type +from .models import Booking, Customer, Room, Room_type @override_settings( @@ -61,3 +63,84 @@ def test_rooms_filter_empty_or_whitespace_behaves_like_no_filter(self): self.assertContains(resp, "Room 1.1") self.assertContains(resp, "Room 1.2") self.assertContains(resp, "Room 2.1") + + +@override_settings( + STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage" +) +class DashboardOccupancyTests(TestCase): + def setUp(self): + self.room_type = Room_type.objects.create( + name="TestType", + price=10.0, + max_guests=2, + ) + self.rooms = [ + Room.objects.create( + room_type=self.room_type, + name=f"Room {i}", + description=f"Test room {i}", + ) + for i in range(1, 5) + ] + self.customer = Customer.objects.create( + name="Test Customer", + email="test@example.com", + phone="123", + ) + + def test_dashboard_occupancy_pct_counts_only_new_bookings_occupying_today(self): + today = date.today() + + # Counts: state NEW and checkin <= today < checkout + Booking.objects.create( + state="NEW", + checkin=today - timedelta(days=1), + checkout=today + timedelta(days=1), + room=self.rooms[0], + guests=1, + customer=self.customer, + total=10.0, + code="ABCDEFG1", + ) + + # Does not count: outside of today range + Booking.objects.create( + state="NEW", + checkin=today + timedelta(days=5), + checkout=today + timedelta(days=6), + room=self.rooms[1], + guests=1, + customer=self.customer, + total=10.0, + code="ABCDEFG2", + ) + + # Does not count: cancelled + Booking.objects.create( + state="DEL", + checkin=today - timedelta(days=1), + checkout=today + timedelta(days=1), + room=self.rooms[2], + guests=1, + customer=self.customer, + total=10.0, + code="ABCDEFG3", + ) + + url = reverse("dashboard") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + + occupancy_pct = resp.context["dashboard"]["occupancy_pct"] + self.assertAlmostEqual(occupancy_pct, 25.0, places=2) + + def test_dashboard_occupancy_pct_is_zero_when_no_rooms_exist(self): + Room.objects.all().delete() + + url = reverse("dashboard") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + + occupancy_pct = resp.context["dashboard"]["occupancy_pct"] + self.assertEqual(occupancy_pct, 0) diff --git a/pms/views.py b/pms/views.py index e3b8d477b..904d8f9ed 100644 --- a/pms/views.py +++ b/pms/views.py @@ -209,12 +209,22 @@ def get(self, request): .aggregate(Sum('total')) ) + total_rooms = Room.objects.count() + occupied_today = (Booking.objects + .filter(state="NEW", checkin__lte=today, checkout__gt=today) + .values("id") + ).count() + occupancy_pct = 0 + if total_rooms: + occupancy_pct = occupied_today / total_rooms * 100 + # preparing context data dashboard = { 'new_bookings': new_bookings, 'incoming_guests': incoming, 'outcoming_guests': outcoming, - 'invoiced': invoiced + 'invoiced': invoiced, + 'occupancy_pct': occupancy_pct, }