A booking system should do more than simply create reservations. In this tutorial, we'll enhance the Jersey City Pickleball Club booking system by allowing members to edit their court, date, and time slot, download a revised entry pass after making changes, and automatically remove bookings once their scheduled time has passed. Using Flask, TinyDB, and the existing Stripe payment integration, we'll turn the basic booking workflow into a more complete and practical booking management system.
Prerequisite:
This tutorial is part of the Flask Landing Page and Reservation System Series.
📚 View the Complete Flask Landing Page and Reservation System Series
⬅ Previous Part ➡ Next Part
Preliminary:
Before I begin, please activate the virtual environment and install the required dependencies.
python -m venv venv
venv\Scripts\activate
pip install flask, flask-wtf, tinydb
Then, we need to set up the file and folder structure, as below:
In this tutorial, I will focus on allowing the user to update the booking date, time slot, and court selection. Therefore, all the files and folders remain the same, except that I will create a new edit_booking.html and add an update and delete function in the app.py files and editBookingForm in the form. py.
class EditBookingForm(FlaskForm):
booking_name = StringField('Name',
[validators.DataRequired(message="Name is required.")])
booking_date = DateField('Date',
[validators.DataRequired(message="Date is required.")])
booking_time_slot = SelectField(
'Time Slot', [validators.DataRequired('time_slot')],
choices=[
('9:00', '9:00 AM - 10:00 AM'),
('10:00', '10:00 AM - 11:00 AM'),
('11:00', '11:00 AM - 12:00 PM'),
('12:00', '12:00 PM - 1:00 PM'),
('1:00', '1:00 PM - 2:00 PM'),
('2:00', '2:00 PM - 3:00 PM'),
('3:00', '3:00 PM - 4:00 PM'),
('4:00', '4:00 PM - 5:00 PM'),
('5:00', '5:00 PM - 6:00 PM'),
('6:00', '6:00 PM - 7:00 PM')
]
)
booking_court_selection = SelectField(
'Time Slot', [validators.DataRequired('time_slot')],
choices=[
('St. Mark'),
('St. Louis'),
('St. Joseph'),
('St. George'),
]
)
update = SubmitField("update")
back = SubmitField("Back")
- This creates a text field, Name: [Kelvin], and the validator means the user cannot submit an empty name.
- This is a
DateField, rather than aStringField. That's important because WTForms converts the submitted date into a Pythondateobject. - The HTML might submit 2026-08-09, but Python receives approximately date(2026, 8, 9)
SelectFieldcreates a dropdown:
- │ 11:00 AM - 12:00 PM ▼ │
- The choices have two parts: ('11:00', '11:00 AM - 12:00 PM'). The first value is the value 11:00, and the second is the label displayed to the user: 11:00 AM - 12:00 PM.
- TinyDB stores 11:00 AM - 12:00 PM, but the
<select>needs are 11:00
SelectFieldcreates a dropdown:- │ St. Mark ▼ │
- choices=[
- ('St. Mark', 'St. Mark'),
- ('St. Louis', 'St. Louis'),
- ('St. Joseph', 'St. Joseph'),
- ('St. George', 'St. George')]
- Each item is actually just a string, not a
(value, label)tuple.
- When the user clicks it, the route reaches edit_booking(booking_id) and updates TinyDB.
- When the user clicks it, it will be redirected to the dashboard
from datetime import datetime
@app.route("/edit-booking/", methods=["GET", "POST"])
@login_required
def edit_booking(booking_id):
booking = bookings_table.get(doc_id=booking_id)
if not booking or booking["username"] != current_user.username:
flash("Booking not found.", "danger")
return redirect(url_for("dashboard"))
form = EditBookingForm()
# GET — prefill form
if request.method == "GET":
form.booking_name.data = booking.get("username", "")
form.booking_date.data = datetime.strptime(
booking["date"],
"%Y-%m-%d"
).date()
for value, label in form.booking_time_slot.choices:
if label == booking.get("time_slot"):
form.booking_time_slot.data = value
break
form.booking_court_selection.data = booking.get("court", "")
if form.back.data:
return redirect(url_for("dashboard"))
if form.validate_on_submit():
selected_time = dict(
form.booking_time_slot.choices
)[form.booking_time_slot.data]
bookings_table.update(
{
"name": current_user.username,
"date": form.booking_date.data.strftime("%Y-%m-%d"),
"time_slot": selected_time,
"court": form.booking_court_selection.data
},
doc_ids=[booking_id]
)
updated_booking = bookings_table.get(doc_id=booking_id)
flash("Booking updated successfully.", "success")
return redirect(url_for("dashboard"))
return render_template(
"partials/edit_booking.html",
booking=booking,
form=form
) This route performs several tasks at once: locating the booking, verifying ownership, pre-filling the edit form, handling Back, validating the update, converting the time slot, updating TinyDB, and returning to the dashboard.- <int:booking_id> tells Flask to convert the URL value to an integer.
- methods=["GET", "POST"] is necessary because
- GET → display the existing booking
- POST → submit the changes
- @login_required ensures only logged-in users can edit bookings.
- Suppose the URL is /edit-booking/5, then TinyDB searches for the document ID
5. It might return:
{
"username": "Kelvin",
"name": "Kelvin",
"court": "st_george",
"date": "2026-08-09",
"time_slot": "11:00 AM - 12:00 PM",
"status": "Accepted",
"payment": "Paid",
"stripe_session": "cs_test_..."
}
3. Make sure the user owns the booking
- There are two checks here.
- Booking doesn't exist—if TinyDB can't find the document
5, then itbookingwill beNone. - Booking belongs to another user—for example:
- Booking username: John
- Logged-in user: Kelvin
- "John" != "Kelvin"
- is
True, so access is denied.
This is an important security check. A user shouldn't be able to change someone else's booking simply by changing to another document ID.
- This creates your WTForms form:
- booking_name
- booking_date
- booking_time_slot
- booking_court_selection
- update
- back
- The purpose is to take the existing TinyDB data and put it into the form.
- Suppose TinyDB contains "name": "Kelvin"; then it becomes "Kelvin." This
""is a fallback if"name"doesn't exist.
- The form
DateFieldneeds a date object when you're populating it.
- There are two things:
- VALUE LABEL
- "11:00" → "11:00 AM - 12:00 PM"
- TinyDB stores the label "11:00 AM - 12:00 PM. "But the form needs the value '11:00.'
- The loop checks 11:00 → 11:00 AM - 12:00 PM ✅. Then, now the HTML
<select>correctly displays 11:00 AM - 12:00 PM.
- If TinyDB has "court": "st_george," then the corresponding radio/select option is selected.
- So when the user clicks Back, the browser submits the form. Then it takes the user back to "/dashboard." No booking is changed, and no success flash is generated.
- Your validators might check that:
- name is provided
- date is provided
- time slot is selected
- court is selected
- If validation fails, the code skips the update and eventually renders the form again.
- After the user submits the form, suppose it contains "12:00." But I need to store "12:00 PM - 1:00 PM" in TinyDB.
- So, the selected_time becomes 12:00 PM - 1:00 PM. This is the reverse of the conversion you performed during GET.
13. Update TinyDB
- This changes only the specified fields of the document
booking_id. - Before:
- court → st_george
- date → 2026-08-09
- time_slot → 11:00 AM - 12:00 PM
- Now:
- court → St. Mark
- date → 2026-08-09
- time_slot → 12:00 PM - 1:00 PM
14. Flash success
- This creates your Bootstrap/Flask flash message: Booking updated successfully.
- After updating, the user goes back to the dashboard rather than staying on the edit page.
- The dashboard then shows the following new items:
- Court,
- Date,
- Time
- If the form isn't successfully submitted—for example, validation fails—the same edit page is displayed again.
🎁 Get Your FREE Flask Cheat Sheet
Get more Flask, Python automation, Docker, and HTMX tutorials delivered to your inbox.
✓ Practical coding tutorials
✓ Automation tips for SMEs
✓ New project ideas and templates
Download my FREE Flask Cheat Sheet (PDF)
{% extends 'base.html' %}
{% block title %}Edit Booking{% endblock %}
{% block content %}
{% if booking %}
<div class="container text-center my-4">
<div class="row">
<div class="col-md-12">
<h1 class="display-4 fw-bold">Update Booking Details</h1>
</div>
</div>
</div>
<div class="container bg-white text-black p-4 rounded shadow-sm">
<form method="POST"
action="{{ url_for('edit_booking', booking_id=booking.doc_id) }}">
{{ form.hidden_tag() }}
<!-- Name -->
<div class="row mb-3 align-items-center">
<div class="col-sm-3">
{{ form.booking_name.label(class="form-label mb-0") }}
</div>
<div class="col-sm-9">
{{ form.booking_name(class="form-control") }}
</div>
</div>
<!-- Date -->
<div class="row mb-3 align-items-center">
<div class="col-sm-3">
{{ form.booking_date.label(class="form-label mb-0") }}
</div>
<div class="col-sm-9">
{{ form.booking_date(class="form-control") }}
</div>
</div>
<!-- Time -->
<div class="row mb-3 align-items-center">
<div class="col-sm-3">
{{ form.booking_time_slot.label(class="form-label mb-0") }}
</div>
<div class="col-sm-9">
{{ form.booking_time_slot(class="form-control") }}
</div>
</div>
<!-- Court -->
<div class="row mb-3 align-items-center">
<div class="col-sm-3">
{{ form.booking_court_selection.label(class="form-label mb-0") }}
</div>
<div class="col-sm-9">
{{ form.booking_court_selection(class="form-select form-select-lg")}}
</div>
</div>
<div class="row">
<div class="col-6">
{{ form.update(class="btn btn-success w-100") }}
</div>
<div class="col-6">
{{ form.back(class="btn btn-secondary w-100") }}
</div>
</div>
</form>
</div>
{% else %}
<p class="text-center text-muted">
No booking details available to update.
</p>
{% endif %}
{% endblock %}
- This means
edit_booking.htmluses your existingbase.htmllayout, and I only provide the content that belongs inside the blocks defined bybase.html.
- Since this is an edit-booking page, the browser tab becomes something like Edit Booking
- Everything inside this block gets inserted into the
contentblock ofbase.html, and at the end you have which closes the block.
- If Jinja receives a booking form, it means the following: a booking was successfully found; display the edit form
{ "username": "Kelvin",
"name": "Kelvin",
"court": "st_george",
"date": "2026-08-09",
"time_slot": "11:00 AM - 12:00 PM",
"status": "Accepted"}
5. The <form> element
- The method is "POST"—When the user clicks Update or Back, the form sends a POST request.
- The action: booking.doc_id = 5. Therefore, the form submits back to the same booking. My Flask route receives booking_id = 5, and this is why my route knows which booking to update.
6. Name field
- This creates a Bootstrap row. Then: This displays the field's label: Name. This
col-sm-3means the label takes approximately 3/12 of the row on small and larger screens. - The field is already populated. For example: Name [Kelvin], and then the user can edit it.
- Displays the date and renders the date input. However, the Flask route converts the TinyDB string "2026-08-09" into a Python
dateobject. Therefore, the HTML date field is prefilled.
8. Time-slot field
- Displays: Time Slot. Then: renders my
SelectField. such as
('9:00', '9:00 AM - 10:00 AM'),
('10:00', '10:00 AM - 11:00 AM'),
('11:00', '11:00 AM - 12:00 PM'),
...
]
- The user gets a dropdown such as 11:00 AM - 12:00 PM ▼ │
- The Flask route is responsible for finding the correct value before rendering the form. So if TinyDB contains 11:00 AM - 12:00 PM. The form selects 11:00 and displays 11:00 AM - 12:00 PM.
- Displays the court. Then, it renders the Bootstrap large select box. For example: St. George ▼
- This renders as a Bootstrap green button. Then it
w-100means: Make the button 100% of the available column width.
- Because both buttons use col-6, I will get an equal half of the button │ Update │ Back │
- At the moment, the
elsesection is empty. I'd recommend displaying a message: No booking details available to update. - Then, if somehow the booking isn't available, the user sees a useful message rather than a blank page.
Step 4: Streamline the time slot.
An extracted app.py
def find_available_slots(court, booking_date):
all_slots = [
"9:00 AM - 10:00 AM",
"10:00 AM - 11:00 AM",
"11:00 AM - 12:00 PM",
"12:00 PM - 1:00 PM",
"1:00 PM - 2:00 PM",
"2:00 PM - 3:00 PM",
"3:00 PM - 4:00 PM",
"4:00 PM - 5:00 PM",
"5:00 PM - 6:00 PM",
"6:00 PM - 7:00 PM"
]
booked_slots = get_booked_slots(
court, booking_date
)
available = [
slot for slot in all_slots
if slot not in booked_slots
]
return available
form.py
class BookingForm(FlaskForm):
name = StringField('Name', [validators.DataRequired('Name')])
date = DateField('Date', [validators.DataRequired('Date')])
time_slot = SelectField(
'Time Slot', [validators.DataRequired('time_slot')],
choices=[
('9:00', '9:00 AM - 10:00 AM'),
('10:00', '10:00 AM - 11:00 AM'),
('11:00', '11:00 AM - 12:00 PM'),
('12:00', '12:00 PM - 1:00 PM'),
('1:00', "1:00 PM - 2:00 PM"),
('2:00', "2:00 PM - 3:00 PM"),
('3:00', "3:00 PM - 4:00 PM"),
('4:00', "4:00 PM - 5:00 PM"),
('5:00', "5:00 PM - 6:00 PM"),
('6:00', "6:00 PM - 7:00 PM")
]
)
court_selection = RadioField('Search', [validators.DataRequired('Search')],
choices=[
('st_mark', 'St. Mark'),
('st_louis', 'St. Louis'),
('st_joseph', 'St. Joseph'),
('st_george', 'St. George')
])
update = SubmitField("Update")
back = SubmitField("Back")from datetime import datetime
def delete_expired_bookings():
now = datetime.now()
for booking in bookings_table.all():
booking_date = booking.get("date")
time_slot = booking.get("time_slot")
if not booking_date or not time_slot:
continue
end_time = time_slot.split(" - ")[1]
end_datetime = datetime.strptime(
f"{booking_date} {end_time}",
"%Y-%m-%d %I:%M %p"
)
if end_datetime:
bookings_table.remove(
doc_ids=[booking.doc_id]
)
This function is a cleanup function for TinyDB. Its purpose is to scan every booking, calculate when the booking ends, and automatically delete bookings whose end time has already passed.- For example, if the function runs at 2026-08-09 11:57:00, then now contains the current date and time.
bookings_table.all()returns every document in your booking table. Theforloop processes them one at a time.
- For example:
- booking_date = "2026-08-09"
- time_slot = "11:00 AM - 12:00 PM"
Using
.get()is useful because if a field doesn't exist, Python returnsNoneinstead of immediately raising aKeyError.
- Suppose a booking accidentally has:
- { "username": "Kelvin",
- "date": "2026-08-09"}
- There is no
time_slot. Instead of crashing, the function executes: continue. Which means skip this booking and move to the next one.
- Suppose time_slot = "11:00 AM - 12:00 PM," and it produces ["11:00 AM," "12:00 PM"]
- Then [1] selects the second element: 12:00 PM. So:end_time = "12:00 PM"
- I am deliberately using the end time, not the start time. That's important because a booking shouldn't be deleted while it is still in progress.
- If booking_date = "2026-08-09", and end_time = "12:00 PM", the result is: 2026-08-09 12:00 PM.
datetimeThis converts: "2026-08-09 12:00 PM"into a real Pythondatetimeobject: datetime(2026, 8, 9, 12, 0)
%Y → 2026
%m → 08
%d → 09
%I → 12-hour clock hour
%M → minutes
%p → AM / PM- So: 2026-08-09 12:00 PM matches: %Y-%m-%d %I:%M %p
- This is the actual expiration check. For example:
- Booking ends: 2026-08-09 12:00 PM
- Current time: 2026-08-09 11:57 AM
- Then: end_datetime <= now is: False. Therefore, don't delete it.
- But after 12:00 PM:
- Booking ends: 2026-08-09 12:00 PM
- Current time: 2026-08-09 12:05 PM
- Now: end_datetime <= now is: True. The booking has expired.
- TinyDB gives every document a
doc_id. For example: doc_id = 5. Then: remove that particular booking.
In this tutorial, we enhanced the Jersey City Pickleball Club booking system with practical booking-management features. Users can now edit their existing bookings, changing the date, time slot, and court while keeping the booking securely associated with their account. We also added automatic cleanup of expired bookings, allowing TinyDB to remove bookings once their scheduled end time has passed. Together with the entry pass and receipt features, the application now provides a more complete end-to-end booking experience—from creating and paying for a booking to modifying, downloading, and automatically removing it after expiration.
Published: July 2026
Last Updated: July 2026
---------------------------------------------------------------------------------------------------------------------------------------------------







.gif)
Comments