From b530c60b6854aa7b295f90265de782b3d1f6be30 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Sat, 11 Apr 2026 14:26:03 -0500 Subject: [PATCH 1/3] feat(rooms): add case-insensitive search filter by room name - RoomsView accepts GET param 'q' and applies icontains filter - Template includes search form with clear button and result count - 12 unit tests covering edge cases (empty, whitespace, partial match, case-insensitivity, ordering, context preservation) --- pms/templates/rooms.html | 52 +++++++++++++++++++---- pms/tests.py | 91 +++++++++++++++++++++++++++++++++++++++- pms/views.py | 13 ++++-- 3 files changed, 142 insertions(+), 14 deletions(-) diff --git a/pms/templates/rooms.html b/pms/templates/rooms.html index c30929f1f..5d85b90c7 100644 --- a/pms/templates/rooms.html +++ b/pms/templates/rooms.html @@ -1,19 +1,53 @@ {% extends "main.html"%} {% block content %} -

Habitaciones del hotel

-{% for room in rooms%} +
+

Habitaciones del hotel

+
+ +
+
+ + + {% if query %} + Limpiar + {% endif %} +
+
+ +{% if query %} +

+ {% if rooms %} + {{ rooms|length }} resultado{{ rooms|length|pluralize:"s" }} para "{{ query }}" + {% else %} + No se encontraron habitaciones para "{{ query }}". + {% endif %} +

+{% endif %} + +{% for room in rooms %}
-
- {{room.name}} ({{room.room_type__name}}) -
+
{{ room.name }} ({{ room.room_type__name }})
- Ver detalles + Ver detalles
-
-
+{% empty %} +{% if not query %} +

No hay habitaciones registradas.

+{% endif %} {% endfor %} -{% endblock content%} +{% endblock content %} + diff --git a/pms/tests.py b/pms/tests.py index 7ce503c2d..0acc411d3 100644 --- a/pms/tests.py +++ b/pms/tests.py @@ -1,3 +1,90 @@ -from django.test import TestCase +from django.test import TestCase, Client +from django.urls import reverse + +from .models import Room, Room_type + + +class RoomsViewSearchTest(TestCase): + """Tests for the room list search filter (GET ?q=).""" + + @classmethod + def setUpTestData(cls): + room_type = Room_type.objects.create(name="Estándar", price=80.0, max_guests=2) + Room.objects.create(name="Habitación 101", description="Vista al jardín", room_type=room_type) + Room.objects.create(name="Habitación 202", description="Vista a la piscina", room_type=room_type) + Room.objects.create(name="Suite Presidencial", description="Planta ática", room_type=room_type) + + def setUp(self): + self.client = Client() + self.url = reverse("rooms") + + # --- baseline --- + + def test_returns_200(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + + def test_no_query_returns_all_rooms(self): + response = self.client.get(self.url) + self.assertEqual(len(response.context["rooms"]), 3) + + def test_empty_query_string_returns_all_rooms(self): + response = self.client.get(self.url, {"q": ""}) + self.assertEqual(len(response.context["rooms"]), 3) + + def test_whitespace_only_query_returns_all_rooms(self): + """Strips whitespace so ' ' is treated as no query.""" + response = self.client.get(self.url, {"q": " "}) + self.assertEqual(len(response.context["rooms"]), 3) + + # --- filtering --- + + def test_search_returns_matching_rooms(self): + response = self.client.get(self.url, {"q": "Habitación"}) + rooms = list(response.context["rooms"]) + self.assertEqual(len(rooms), 2) + self.assertTrue(all("Habitación" in r["name"] for r in rooms)) + + def test_search_partial_match(self): + response = self.client.get(self.url, {"q": "101"}) + rooms = list(response.context["rooms"]) + self.assertEqual(len(rooms), 1) + self.assertEqual(rooms[0]["name"], "Habitación 101") + + def test_search_no_results(self): + response = self.client.get(self.url, {"q": "xyzabc"}) + rooms = list(response.context["rooms"]) + self.assertEqual(len(rooms), 0) + + # --- case insensitivity --- + + def test_search_is_case_insensitive_lowercase(self): + response = self.client.get(self.url, {"q": "suite"}) + rooms = list(response.context["rooms"]) + self.assertEqual(len(rooms), 1) + self.assertEqual(rooms[0]["name"], "Suite Presidencial") + + def test_search_is_case_insensitive_uppercase_ascii(self): + # SQLite's LIKE only case-folds ASCII characters; use an ASCII-safe term. + response = self.client.get(self.url, {"q": "SUITE"}) + rooms = list(response.context["rooms"]) + self.assertEqual(len(rooms), 1) + self.assertEqual(rooms[0]["name"], "Suite Presidencial") + + # --- context --- + + def test_query_term_preserved_in_context(self): + response = self.client.get(self.url, {"q": "Suite"}) + self.assertEqual(response.context["query"], "Suite") + + def test_empty_query_context_is_empty_string(self): + response = self.client.get(self.url) + self.assertEqual(response.context["query"], "") + + # --- ordering --- + + def test_results_ordered_by_name(self): + response = self.client.get(self.url) + names = [r["name"] for r in response.context["rooms"]] + self.assertEqual(names, sorted(names)) -# Create your tests here. diff --git a/pms/views.py b/pms/views.py index f38563933..a1f9ca755 100644 --- a/pms/views.py +++ b/pms/views.py @@ -238,9 +238,16 @@ 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") + query = request.GET.get("q", "").strip() + rooms = Room.objects.select_related("room_type").values("name", "room_type__name", "id") + + if query: + rooms = rooms.filter(name__icontains=query) + + rooms = rooms.order_by("name") + context = { - 'rooms': rooms + "rooms": rooms, + "query": query, } return render(request, "rooms.html", context) From ad5813957516fd98baa1fbaa8076f7afeafbb927 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Sat, 11 Apr 2026 14:42:13 -0500 Subject: [PATCH 2/3] feat(dashboard): add occupancy rate widget - Calculate occupancy as (active NEW bookings / total rooms) * 100 - Guard division by zero when no rooms exist - Replace hardcoded 'DEL' strings with Booking.DELETED constant - Move datetime imports to module level - Reuse bookings_today queryset to reduce redundant DB calls - 6 unit tests: correct calc, cancelled excluded, zero rooms, 100% full --- pms/templates/dashboard.html | 5 ++- pms/tests.py | 73 +++++++++++++++++++++++++++++++++++- pms/views.py | 62 +++++++++++++----------------- 3 files changed, 102 insertions(+), 38 deletions(-) diff --git a/pms/templates/dashboard.html b/pms/templates/dashboard.html index 10f0285cc..e331c78ee 100644 --- a/pms/templates/dashboard.html +++ b/pms/templates/dashboard.html @@ -17,11 +17,14 @@

{{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
+

{{ dashboard.occupancy_rate }}%

+
{% endblock content%} \ No newline at end of file diff --git a/pms/tests.py b/pms/tests.py index 0acc411d3..61b0b7728 100644 --- a/pms/tests.py +++ b/pms/tests.py @@ -1,7 +1,7 @@ from django.test import TestCase, Client from django.urls import reverse -from .models import Room, Room_type +from .models import Booking, Customer, Room, Room_type class RoomsViewSearchTest(TestCase): @@ -88,3 +88,74 @@ def test_results_ordered_by_name(self): names = [r["name"] for r in response.context["rooms"]] self.assertEqual(names, sorted(names)) + +class DashboardOccupancyTest(TestCase): + """Tests for the occupancy rate widget in the Dashboard.""" + + @classmethod + def setUpTestData(cls): + room_type = Room_type.objects.create(name="Estándar", price=100.0, max_guests=2) + customer = Customer.objects.create(name="Ana García", email="ana@example.com", phone="600000000") + + cls.room1 = Room.objects.create(name="Hab 101", description="", room_type=room_type) + cls.room2 = Room.objects.create(name="Hab 102", description="", room_type=room_type) + cls.room3 = Room.objects.create(name="Hab 103", description="", room_type=room_type) + cls.room4 = Room.objects.create(name="Hab 104", description="", room_type=room_type) + + # 3 active bookings (NEW), 1 cancelled (DEL) + for room in [cls.room1, cls.room2, cls.room3]: + Booking.objects.create( + checkin="2026-05-01", + checkout="2026-05-05", + room=room, + guests=1, + customer=customer, + total=400.0, + code=f"TST{room.id:05d}", + state=Booking.NEW, + ) + Booking.objects.create( + checkin="2026-05-01", + checkout="2026-05-05", + room=cls.room4, + guests=1, + customer=customer, + total=400.0, + code="TSTDEL01", + state=Booking.DELETED, + ) + + def setUp(self): + self.client = Client() + self.url = reverse("dashboard") + + def test_dashboard_returns_200(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + + def test_occupancy_rate_calculation(self): + # 3 NEW bookings / 4 rooms * 100 = 75% + response = self.client.get(self.url) + self.assertEqual(response.context["dashboard"]["occupancy_rate"], 75) + + def test_cancelled_bookings_excluded_from_occupancy(self): + # The DEL booking must not count towards occupancy + response = self.client.get(self.url) + self.assertLess(response.context["dashboard"]["occupancy_rate"], 100) + + def test_occupancy_rate_in_template(self): + response = self.client.get(self.url) + self.assertContains(response, "75%") + + def test_occupancy_zero_when_no_rooms(self): + # Temporarily wipe rooms to test division-by-zero guard + Room.objects.all().delete() + response = self.client.get(self.url) + self.assertEqual(response.context["dashboard"]["occupancy_rate"], 0) + + def test_occupancy_100_when_all_rooms_booked(self): + Booking.objects.filter(state=Booking.DELETED).update(state=Booking.NEW) + response = self.client.get(self.url) + self.assertEqual(response.context["dashboard"]["occupancy_rate"], 100) + + diff --git a/pms/views.py b/pms/views.py index a1f9ca755..1a18859b6 100644 --- a/pms/views.py +++ b/pms/views.py @@ -1,3 +1,5 @@ +from datetime import date, datetime, time + from django.db.models import F, Q, Count, Sum from django.shortcuts import render, redirect from django.utils.decorators import method_decorator @@ -6,7 +8,7 @@ from .form_dates import Ymd from .forms import * -from .models import Room +from .models import Booking, Room from .reservation_code import generate @@ -176,52 +178,40 @@ def post(self, request, pk): class DashboardView(View): def get(self, request): - from datetime import date, time, datetime today = date.today() + today_range = (datetime.combine(today, time.min), datetime.combine(today, time.max)) + + bookings_today = Booking.objects.filter(created__range=today_range) - # get bookings created today - today_min = datetime.combine(today, time.min) - today_max = datetime.combine(today, time.max) - today_range = (today_min, today_max) - new_bookings = (Booking.objects - .filter(created__range=today_range) - .values("id") - ).count() + new_bookings = bookings_today.count() - # get incoming guests incoming = (Booking.objects .filter(checkin=today) - .exclude(state="DEL") - .values("id") - ).count() + .exclude(state=Booking.DELETED) + .count()) - # get outcoming guests outcoming = (Booking.objects .filter(checkout=today) - .exclude(state="DEL") - .values("id") - ).count() - - # get outcoming guests - invoiced = (Booking.objects - .filter(created__range=today_range) - .exclude(state="DEL") - .aggregate(Sum('total')) - ) - - # preparing context data - dashboard = { - 'new_bookings': new_bookings, - 'incoming_guests': incoming, - 'outcoming_guests': outcoming, - 'invoiced': invoiced + .exclude(state=Booking.DELETED) + .count()) - } + invoiced = (bookings_today + .exclude(state=Booking.DELETED) + .aggregate(Sum("total"))) - context = { - 'dashboard': dashboard + total_rooms = Room.objects.count() + active_bookings = Booking.objects.filter(state=Booking.NEW).count() + occupancy_rate = round((active_bookings / total_rooms) * 100) if total_rooms > 0 else 0 + + dashboard = { + "new_bookings": new_bookings, + "incoming_guests": incoming, + "outcoming_guests": outcoming, + "invoiced": invoiced, + "occupancy_rate": occupancy_rate, } - return render(request, "dashboard.html", context) + + return render(request, "dashboard.html", {"dashboard": dashboard}) class RoomDetailsView(View): From d4fcb1df4af752459f4f6ce7980bf039b8bb2613 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Sat, 11 Apr 2026 15:08:20 -0500 Subject: [PATCH 3/3] feat(bookings): add date editing with availability validation - New BookingDatesForm with clean() validating: order, max date, overlap - Overlap uses standard hotel formula (checkin__lt / checkout__gt) - Cancelled bookings and the booking itself are excluded from conflict check - EditBookingDatesView: GET pre-fills form, POST saves + recalculates total - 13 integration tests: success, self-edit, partial overlaps, cancelled, date constraints --- pms/forms.py | 68 +++++++++++- pms/templates/edit_booking_dates.html | 38 +++++++ pms/templates/home.html | 5 +- pms/tests.py | 151 ++++++++++++++++++++++++++ pms/urls.py | 1 + pms/views.py | 31 +++++- 6 files changed, 290 insertions(+), 4 deletions(-) create mode 100644 pms/templates/edit_booking_dates.html diff --git a/pms/forms.py b/pms/forms.py index f1bc68d08..1bf5bd689 100644 --- a/pms/forms.py +++ b/pms/forms.py @@ -1,9 +1,12 @@ -from datetime import datetime +from datetime import date, datetime from django import forms from django.forms import ModelForm from .models import Booking, Customer +MAX_BOOKING_DATE = date(2026, 12, 31) + + class RoomSearchForm(ModelForm): class Meta: @@ -56,3 +59,66 @@ class Meta: 'total': forms.HiddenInput(), 'state': forms.HiddenInput(), } + + +class BookingDatesForm(forms.Form): + checkin = forms.DateField( + label="Check-in", + widget=forms.DateInput(attrs={ + "type": "date", + "min": date.today().isoformat(), + "max": MAX_BOOKING_DATE.isoformat(), + "class": "form-control", + }), + ) + checkout = forms.DateField( + label="Check-out", + widget=forms.DateInput(attrs={ + "type": "date", + "min": date.today().isoformat(), + "max": MAX_BOOKING_DATE.isoformat(), + "class": "form-control", + }), + ) + + def __init__(self, *args, room, booking_id, **kwargs): + super().__init__(*args, **kwargs) + self._room = room + self._booking_id = booking_id + + def clean(self): + cleaned_data = super().clean() + checkin = cleaned_data.get("checkin") + checkout = cleaned_data.get("checkout") + + if not checkin or not checkout: + return cleaned_data + + if checkout <= checkin: + raise forms.ValidationError( + "La fecha de check-out debe ser posterior al check-in." + ) + + if checkout > MAX_BOOKING_DATE: + raise forms.ValidationError( + f"La fecha máxima de check-out es {MAX_BOOKING_DATE.strftime('%d/%m/%Y')}." + ) + + overlaps = ( + Booking.objects + .filter( + room=self._room, + state=Booking.NEW, + checkin__lt=checkout, + checkout__gt=checkin, + ) + .exclude(pk=self._booking_id) + .exists() + ) + if overlaps: + raise forms.ValidationError( + "No hay disponibilidad para las fechas seleccionadas." + ) + + return cleaned_data + diff --git a/pms/templates/edit_booking_dates.html b/pms/templates/edit_booking_dates.html new file mode 100644 index 000000000..1c2fb5450 --- /dev/null +++ b/pms/templates/edit_booking_dates.html @@ -0,0 +1,38 @@ +{% extends "main.html" %} + +{% block content %} +
+

Editar fechas de reserva

+

+ Reserva {{ booking.code }} — {{ booking.room.name }} +

+ + {% if form.non_field_errors %} + + {% endif %} + +
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
{{ field.errors.0 }}
+ {% endif %} +
+ {% endfor %} + +
+ + Cancelar +
+
+
+{% endblock content %} diff --git a/pms/templates/home.html b/pms/templates/home.html index 1e61b8024..9f895f34e 100644 --- a/pms/templates/home.html +++ b/pms/templates/home.html @@ -68,8 +68,9 @@

Reservas Realizadas

Editar datos de contacto
- -
+ {% if booking.state != "DEL" %} + Editar fechas + {% endif %}
{% if booking.state != "DEL" %} diff --git a/pms/tests.py b/pms/tests.py index 61b0b7728..4f5029fde 100644 --- a/pms/tests.py +++ b/pms/tests.py @@ -1,3 +1,5 @@ +from datetime import date + from django.test import TestCase, Client from django.urls import reverse @@ -159,3 +161,152 @@ def test_occupancy_100_when_all_rooms_booked(self): self.assertEqual(response.context["dashboard"]["occupancy_rate"], 100) +class EditBookingDatesViewTest(TestCase): + """Integration tests for the booking date editing feature.""" + + @classmethod + def setUpTestData(cls): + room_type = Room_type.objects.create(name="Estándar", price=100.0, max_guests=2) + customer = Customer.objects.create(name="Luis Pérez", email="luis@example.com", phone="611000000") + + cls.room = Room.objects.create(name="Hab 201", description="", room_type=room_type) + cls.other_room = Room.objects.create(name="Hab 202", description="", room_type=room_type) + + # The booking we will edit in most tests + cls.booking = Booking.objects.create( + checkin=date(2026, 6, 1), + checkout=date(2026, 6, 5), + room=cls.room, + guests=1, + customer=customer, + total=400.0, + code="EDIT0001", + state=Booking.NEW, + ) + + # An overlapping booking on the same room (by a different customer) + cls.blocking_booking = Booking.objects.create( + checkin=date(2026, 6, 10), + checkout=date(2026, 6, 15), + room=cls.room, + guests=1, + customer=customer, + total=500.0, + code="EDIT0002", + state=Booking.NEW, + ) + + # A cancelled booking on the same room — must NOT block dates + cls.cancelled_booking = Booking.objects.create( + checkin=date(2026, 7, 1), + checkout=date(2026, 7, 5), + room=cls.room, + guests=1, + customer=customer, + total=400.0, + code="EDIT0003", + state=Booking.DELETED, + ) + + def setUp(self): + self.client = Client() + self.url = reverse("edit_booking_dates", kwargs={"pk": self.booking.pk}) + + # --- GET --- + + def test_get_returns_200(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + + def test_get_prepopulates_form_with_current_dates(self): + response = self.client.get(self.url) + form = response.context["form"] + self.assertEqual(form.initial["checkin"], self.booking.checkin) + self.assertEqual(form.initial["checkout"], self.booking.checkout) + + # --- successful update --- + + def test_successful_date_change_redirects_to_home(self): + response = self.client.post(self.url, {"checkin": "2026-08-01", "checkout": "2026-08-05"}) + self.assertRedirects(response, reverse("home")) + + def test_successful_date_change_persists_in_db(self): + self.client.post(self.url, {"checkin": "2026-08-01", "checkout": "2026-08-05"}) + self.booking.refresh_from_db() + self.assertEqual(str(self.booking.checkin), "2026-08-01") + self.assertEqual(str(self.booking.checkout), "2026-08-05") + + def test_successful_date_change_recalculates_total(self): + # 4 nights × 100 €/night = 400 € + self.client.post(self.url, {"checkin": "2026-08-01", "checkout": "2026-08-05"}) + self.booking.refresh_from_db() + self.assertEqual(self.booking.total, 400.0) + + def test_editing_same_dates_is_allowed(self): + """A booking must not conflict with itself.""" + response = self.client.post(self.url, {"checkin": "2026-06-01", "checkout": "2026-06-05"}) + self.assertRedirects(response, reverse("home")) + + # --- overlap validation --- + + def test_overlap_with_other_booking_is_blocked(self): + # Tries to move into the window occupied by blocking_booking (2026-06-10 / 2026-06-15) + response = self.client.post(self.url, {"checkin": "2026-06-12", "checkout": "2026-06-14"}) + self.assertEqual(response.status_code, 200) + self.assertFormError( + response.context["form"], + None, + "No hay disponibilidad para las fechas seleccionadas.", + ) + + def test_partial_overlap_at_start_is_blocked(self): + response = self.client.post(self.url, {"checkin": "2026-06-08", "checkout": "2026-06-12"}) + self.assertEqual(response.status_code, 200) + self.assertFormError( + response.context["form"], + None, + "No hay disponibilidad para las fechas seleccionadas.", + ) + + def test_partial_overlap_at_end_is_blocked(self): + response = self.client.post(self.url, {"checkin": "2026-06-13", "checkout": "2026-06-17"}) + self.assertEqual(response.status_code, 200) + self.assertFormError( + response.context["form"], + None, + "No hay disponibilidad para las fechas seleccionadas.", + ) + + def test_cancelled_booking_does_not_block_dates(self): + # cancelled_booking sits on 2026-07-01 / 2026-07-05 — must be ignored + response = self.client.post(self.url, {"checkin": "2026-07-01", "checkout": "2026-07-05"}) + self.assertRedirects(response, reverse("home")) + + # --- date constraint validation --- + + def test_checkout_before_checkin_is_invalid(self): + response = self.client.post(self.url, {"checkin": "2026-09-10", "checkout": "2026-09-05"}) + self.assertEqual(response.status_code, 200) + self.assertFormError( + response.context["form"], + None, + "La fecha de check-out debe ser posterior al check-in.", + ) + + def test_checkout_same_as_checkin_is_invalid(self): + response = self.client.post(self.url, {"checkin": "2026-09-10", "checkout": "2026-09-10"}) + self.assertEqual(response.status_code, 200) + self.assertFormError( + response.context["form"], + None, + "La fecha de check-out debe ser posterior al check-in.", + ) + + def test_checkout_beyond_max_date_is_invalid(self): + response = self.client.post(self.url, {"checkin": "2026-12-30", "checkout": "2027-01-02"}) + self.assertEqual(response.status_code, 200) + self.assertFormError( + response.context["form"], + None, + "La fecha máxima de check-out es 31/12/2026.", + ) 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"), diff --git a/pms/views.py b/pms/views.py index 1a18859b6..d40a22ee3 100644 --- a/pms/views.py +++ b/pms/views.py @@ -1,7 +1,7 @@ from datetime import date, datetime, time from django.db.models import F, Q, Count, Sum -from django.shortcuts import render, redirect +from django.shortcuts import get_object_or_404, render, redirect from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import ensure_csrf_cookie @@ -176,6 +176,35 @@ def post(self, request, pk): return redirect("/") +class EditBookingDatesView(View): + def get(self, request, pk): + booking = get_object_or_404(Booking, pk=pk) + form = BookingDatesForm( + initial={"checkin": booking.checkin, "checkout": booking.checkout}, + room=booking.room, + booking_id=booking.pk, + ) + return render(request, "edit_booking_dates.html", {"form": form, "booking": booking}) + + @method_decorator(ensure_csrf_cookie) + def post(self, request, pk): + booking = get_object_or_404(Booking, pk=pk) + form = BookingDatesForm( + request.POST, + room=booking.room, + booking_id=booking.pk, + ) + if form.is_valid(): + checkin = form.cleaned_data["checkin"] + checkout = form.cleaned_data["checkout"] + booking.checkin = checkin + booking.checkout = checkout + booking.total = (checkout - checkin).days * booking.room.room_type.price + booking.save() + return redirect("home") + return render(request, "edit_booking_dates.html", {"form": form, "booking": booking}) + + class DashboardView(View): def get(self, request): today = date.today()