Skip to main content

How to Edit Bookings & Auto-Expire in Flask?

 

How to Edit Booking and Automatic Expiration in a Flask Booking System?

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:
File and folder

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.

Step 1: Create an update form
underlying logic
Create an update form logic

An extract of 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")
1. Name
  • This creates a text field, Name: [Kelvin], and the validator means the user cannot submit an empty name.
2. Date
  • This is a DateField, rather than a StringField. That's important because WTForms converts the submitted date into a Python date object.
  • The HTML might submit 2026-08-09, but Python receives approximately date(2026, 8, 9)
3. Time slot
  • SelectField creates 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
4. Court selection
  • SelectField creates 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. 
5. Update button
  • When the user clicks it, the route reaches edit_booking(booking_id) and updates TinyDB.
6. Back button
  • When the user clicks it, it will be redirected to the dashboard


Step 2: Set up the update function
Underlying logic
Set up the update function logic
An extraction of app.py
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.

1. Route and login protection
  • <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.

2. Get the booking from TinyDB
  • 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.

  1. Booking doesn't exist—if TinyDB can't find the document 5, then it booking will be None.
  2. 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. 


4. Create the edit form

  • This creates your WTForms form:
    • booking_name
    • booking_date
    • booking_time_slot
    • booking_court_selection
    • update
    • back 
5. GET — prefill the existing booking

  • The purpose is to take the existing TinyDB data and put it into the form.
6. Pre-fill the name

  • Suppose TinyDB contains "name": "Kelvin"; then it becomes "Kelvin." This "" is a fallback if  "name" doesn't exist.

7. Convert the database date into a Python date
  • The form DateField needs a date object when you're populating it.
8. The time-slot conversion
  • 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.
9. Prefill the court

  • If TinyDB has "court": "st_george," then the corresponding radio/select option is selected.
10. Back button

  • 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.
11. Validate the submitted form

  • 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.
12. Convert the time-slot value back to the label
  • 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.
15. Return to dashboard

  • 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
16. If validation fails
  • 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 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)


---------------------------------------------------------------------------------------------------------------------------------------------------
Step 3: Step up the edit HTML file
Step up the edit HTML file interface

partials/edit_booking.html 
{% 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 %}
1. Extend the base template
  • This means  edit_booking.html uses your existing  base.html layout, and I only provide the content that belongs inside the blocks defined by base.html.
2. Page title
  • Since this is an edit-booking page, the browser tab becomes something like Edit Booking
3. Content block
  • Everything inside this block gets inserted into the content block of base.html, and at the end you have which closes the block.
4. Check whether a booking exists
  • 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-3 means 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.
7. Date field

  • Displays the date and renders the date input. However, the Flask route converts the TinyDB string "2026-08-09" into a Python date object. Therefore, the HTML date field is prefilled.

8. Time-slot field

  • Displays: Time Slot. Then: renders my SelectField. such as

            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'),
                ...
                ]

  • 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.
9. Court field
  • Displays the court. Then, it renders the Bootstrap large select box. For example: St. George ▼

10. Update button
  • This renders as a Bootstrap green button. Then it w-100 means: Make the button 100% of the available column width.
11. Back button
  • Because both buttons use col-6, I will get an equal half of the button │ Update │ Back │
12 What happens when there is no booking?
  • At the moment, the else section 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. 
In my previous tutorial, I used a 24-hour time format both for the booking form and the find_available_slots function. To be consistent, I will change the time format to the 12-hour format.

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")


Step 5: Configure the delete function
Underlying logic
Configure the delete function logic

An extracted app.py
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.

1. Get the current date and time
  • For example, if the function runs at 2026-08-09 11:57:00, then now contains the current date and time.
2. Get every booking from TinyDB
  • bookings_table.all() returns every document in your booking table. The for loop processes them one at a time.
3. Get the booking date and time slot
  • 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 returns None instead of immediately raising a KeyError.

4. Skip incomplete bookings
  • 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.
5. Extract the ending time
  • 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.
6. Combine the date and ending time
  • If booking_date = "2026-08-09", and end_time = "12:00 PM", the result is: 2026-08-09 12:00 PM.
7. Convert the string into a Python datetime
  • This converts: "2026-08-09 12:00 PM" into a real Python datetime object: 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
7. Compare the booking's end time with the current time
  • 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.
8. Delete the expired booking

  • TinyDB gives every document a doc_id. For example: doc_id = 5. Then: remove that particular booking.

Step 6: Verify the update function
In order to ensure the update function is working, I attached the following animated file.
Verify the update function interface

Final wrap-up:

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

---------------------------------------------------------------------------------------------------------------------------------------------------

Thanks for reading! 

If you haven't subscribed yet, join my newsletter to receive future Python and Flask tutorials.


---------------------------------------------------------------------------------------------------------------------------------------------------
Need a similar system for your business? 
I build custom Flask web applications and Python automation solutions for SMEs and solopreneurs. 

---------------------------------------------------------------------------------------------------------------------------------------------------

About the Author 
Kelvin Loh is a Python developer focused on Flask, desktop applications, and business automation solutions. He shares practical tutorials and real-world coding projects to help developers and small businesses build useful applications.

Comments

Popular Posts

How to Build an Audiobook Workflow Desktop System with Python?

In this tutorial, we will build a simple audiobook player using Python and CustomTkinter. You will learn how to convert text into speech using gTTS and play it with PyGame. We will also implement play, pause, and stop controls, like those in a real audio player. By the end, you will have a clean and functional desktop audiobook app. Prerequisite: This tutorial is part of the standalone tutorial. 📚 View the standalone tutorial Preliminary   Before I begin, it is recommended to activate the virtual environment before installing the relevant dependencies. python -m venv venv venv\Scripts\activate pip install customtkinter pillow gTTS pygame CTkMessagebox pypdf Then, the following steps include setting up the file structure, app.py, and two additional folders: the uploads and media folders. The media folder contains the icons necessary to build the app; there are read, pause, and stop icons, as shown on the diagram. Step 1: Build up the app interface I have 4 sections here: the...

How to Set Up PgAdmin and Adminer Using Docker Compose?

If you are new to database management with Docker, this tutorial will guide you through setting up both PgAdmin and Adminer using Docker containers. By containerising these tools, you can quickly launch lightweight and portable database management environments without installing them directly on your operating system. This approach also makes it easier to manage configurations, updates, and multiple projects across different devices.