I love to demonstrate to my colleagues how a bit of coding can reduce tedious and repetitive work. We had a recent conversation about how booking of lab equipment could be improved.
In my eyes, codes solve (almost) everything!
So I got on to tinker this morning, and my AI pet made me a prototype of an equipment booking app that I can show my colleagues next week.
It's crazy how an equipment booking app that would take months to conceptualise and create now took me 10 minutes to create with generative AI. Here is the link (while it is there) https://app-keefellow.pythonanywhere.com/login
I am sharing the codes here, written as a Python Flask app.
The main steps are as follows:
- Import necessary Flask modules and other libraries (CSV, datetime, etc.)
- Create a Flask application with a secret key for session management
- Define user credentials with different roles (user and manager)
- Define paths to CSV files for storing equipment and booking data
User Authentication System
- Create login/logout functionality
- Implement decorators to protect routes based on authentication status
- Create separate decorator for manager-only access
- Store user session data (username and role)
Data Management
- Implement functions to load and save equipment data to CSV
- Implement functions to load and save booking data to CSV
- Auto-create sample data files if they don't exist on first run
- Handle proper type conversion for numeric fields
Route Structure
- Home route for redirection to appropriate page based on login status
- Login/logout routes for authentication
- Equipment listing route with search functionality
- Routes for booking, viewing, and managing equipment
- Special routes for managers (all bookings, approve/reject, etc.)
Equipment Management
- Display equipment with availability information
- Add search functionality with filtering
- Allow managers to add new equipment
- Allow managers to edit existing equipment with validation
- Prevent reducing quantities below currently booked amounts
Booking System
- Implement booking functionality with validations
- Track equipment availability based on bookings
- Allow users to view and manage their own bookings
- Allow users to cancel pending bookings
- Allow users to return approved equipment
- Allow managers to approve/reject pending bookings
User Interface
- Create HTML templates with CSS styling inline
- Implement different views based on user roles
- Create consistent navigation between different sections
- Implement status indicators for bookings
- Display appropriate action buttons based on booking status
Error Handling
- Implement form validation for all inputs
- Display flash messages for successful operations and errors
- Handle edge cases like insufficient quantity, invalid dates, etc.
from flask import Flask, render_template_string, request, redirect, url_for, session, flash
import csv
import os
import secrets
from functools import wraps
from datetime import datetime
app = Flask(__name__)
app.secret_key = secrets.token_hex(16) # Set a secret key for session management
# User credentials (in a real app, you would use a database and proper password hashing)
USERS = {
'user': {'password': 'userpass', 'role': 'user'},
'manager': {'password': 'managerpass', 'role': 'manager'}
}
# Path to CSV files
EQUIPMENT_FILE = 'equipment.csv'
BOOKING_FILE = 'bookings.csv'
# Login required decorator
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'username' not in session:
flash('Please log in to access this page')
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
# Manager role required decorator
def manager_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'role' not in session or session['role'] != 'manager':
flash('You need manager permissions to access this page')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
def load_equipment():
"""Load equipment data from CSV file"""
equipment_list = []
# Create a sample equipment file if it doesn't exist
if not os.path.exists(EQUIPMENT_FILE):
with open(EQUIPMENT_FILE, 'w', newline='') as file:
writer = csv.writer(file)
writer.writerow(['id', 'name', 'quantity', 'available'])
for i in range(1, 11):
writer.writerow([i, f'Equipment {i}', 5, 5])
# Read the equipment data
with open(EQUIPMENT_FILE, 'r') as file:
reader = csv.DictReader(file)
for row in reader:
equipment_list.append({
'id': row['id'],
'name': row['name'],
'quantity': int(row['quantity']),
'available': int(row['available']) if 'available' in row else int(row['quantity'])
})
return equipment_list
def save_equipment(equipment_list):
"""Save equipment data to CSV file"""
with open(EQUIPMENT_FILE, 'w', newline='') as file:
writer = csv.DictWriter(file, fieldnames=['id', 'name', 'quantity', 'available'])
writer.writeheader()
for item in equipment_list:
writer.writerow(item)
def load_bookings():
"""Load bookings data from CSV file"""
bookings = []
# Create bookings file if it doesn't exist
if not os.path.exists(BOOKING_FILE):
with open(BOOKING_FILE, 'w', newline='') as file:
writer = csv.writer(file)
writer.writerow(['id', 'equipment_id', 'equipment_name', 'user', 'quantity',
'start_date', 'end_date', 'status'])
# Read the bookings data
with open(BOOKING_FILE, 'r') as file:
reader = csv.DictReader(file)
for row in reader:
bookings.append({
'id': row['id'],
'equipment_id': row['equipment_id'],
'equipment_name': row['equipment_name'],
'user': row['user'],
'quantity': int(row['quantity']),
'start_date': row['start_date'],
'end_date': row['end_date'],
'status': row['status']
})
return bookings
def save_bookings(bookings):
"""Save bookings data to CSV file"""
with open(BOOKING_FILE, 'w', newline='') as file:
writer = csv.DictWriter(file, fieldnames=[
'id', 'equipment_id', 'equipment_name', 'user', 'quantity',
'start_date', 'end_date', 'status'
])
writer.writeheader()
for booking in bookings:
writer.writerow(booking)
@app.route('/')
def home():
"""Redirect to login if not logged in, otherwise to equipment list"""
if 'username' in session:
return redirect(url_for('index'))
return redirect(url_for('login'))
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Handle user login"""
error = None
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username in USERS and USERS[username]['password'] == password:
session['username'] = username
session['role'] = USERS[username]['role']
flash(f'Welcome, {username}!')
return redirect(url_for('index'))
else:
error = 'Invalid username or password. Please try again.'
# Login page template
template = """
Login - Equipment System
container">
Equipment System Login
{% if error %}
error">{{ error }}
{% endif %}
{% if get_flashed_messages() %}
flash">
{{ get_flashed_messages()[0] }}
{% endif %}
post">
form-group">
username">Username:
text" id="username" name="username" required>
form-group">
password">Password:
password" id="password" name="password" required>
submit">Log In
demo-info">
For demonstration purposes:
User login: username = "
user", password = "userpass"
Manager login: username = "
manager", password = "managerpass"
"""
return render_template_string(template, error=error)
@app.route('/logout')
def logout():
"""Handle user logout"""
session.clear()
flash('You have been logged out successfully')
return redirect(url_for('login'))
@app.route('/equipment', methods=['GET'])
@login_required
def index():
"""Home page - show equipment list with search functionality"""
equipment_list = load_equipment()
search_query = request.args.get('search', '').lower()
# Filter equipment based on search query if provided
if search_query:
filtered_equipment = [
item for item in equipment_list
if search_query in item['name'].lower() or search_query in str(item['id'])
]
else:
filtered_equipment = equipment_list
# HTML template as a string
template = """
Equipment Availability
container">
header">
Equipment Availability
user-info">
Logged in as: {{ session.username }} ({{ session.role }})
{{ url_for('logout') }}" class="btn btn-logout">Logout
nav">
{{ url_for('index') }}" class="btn btn-primary">Equipment
{{ url_for('my_bookings') }}" class="btn btn-info">My Bookings
{% if session.role == 'manager' %}
{{ url_for('all_bookings') }}" class="btn btn-info">All Bookings
{{ url_for('add_equipment') }}" class="btn">Add Equipment
{% endif %}
{% if get_flashed_messages() %}
flash">
{{ get_flashed_messages()[0] }}
{% endif %}
search-box">
GET" action="{{ url_for('index') }}">
text" name="search" placeholder="Search by name or ID"
value="{{ search_query }}" class="search-input">
submit" class="btn">Search
{% if search_query %}
{{ url_for('index') }}" class="clear-link">Clear
{% endif %}
{% if equipment|length == 0 %}
No equipment found matching your search.
{% else %}
ID
Name
Total Quantity
Available
Actions
{% for item in equipment %}
{{ item.id }}
{{ item.name }}
{{ item.quantity }}
{{ item.available }}
{% if item.available > 0 %}
{
{ url_for('book_equipment', equipment_id=item.id) }}" class="btn">Book
{% else %}
Not Available
{% endif %}
{% if session.role == 'manager' %}
{{ url_for('edit_equipment', equipment_id=item.id) }}" class="btn btn-info">Edit
{% endif %}
{% endfor %}
{% endif %}
"""
return render_template_string(template, equipment=filtered_equipment, search_query=search_query)
@app.route('/book/', methods=['GET', 'POST'])
@login_required
def book_equipment(equipment_id):
"""Book equipment form and processing"""
equipment_list = load_equipment()
equipment = next((item for item in equipment_list if item['id'] == equipment_id), None)
if not equipment:
flash('Equipment not found')
return redirect(url_for('index'))
if request.method == 'POST':
try:
quantity = int(request.form.get('quantity', 1))
start_date = request.form.get('start_date')
end_date = request.form.get('end_date')
if not start_date or not end_date:
flash('Please provide both start and end dates')
return redirect(url_for('book_equipment', equipment_id=equipment_id))
if quantity <= 0:
flash('Quantity must be at least 1')
return redirect(url_for('book_equipment', equipment_id=equipment_id))
if quantity > equipment['available']:
flash('Not enough equipment available')
return redirect(url_for('book_equipment', equipment_id=equipment_id))
# All validation passed, create booking
bookings = load_bookings()
# Generate new booking ID
new_id = 1
if bookings:
max_id = max(int(booking['id']) for booking in bookings)
new_id = max_id + 1
# Create new booking
new_booking = {
'id': str(new_id),
'equipment_id': equipment_id,
'equipment_name': equipment['name'],
'user': session['username'],
'quantity': quantity,
'start_date': start_date,
'end_date': end_date,
'status': 'approved' if session['role'] == 'manager' else 'pending'
}
bookings.append(new_booking)
save_bookings(bookings)
# Update equipment availability
equipment['available'] -= quantity
save_equipment(equipment_list)
flash(f'Successfully booked {quantity} {equipment["name"]}(s)')
return redirect(url_for('my_bookings'))
except ValueError:
flash('Please enter valid quantity')
return redirect(url_for('book_equipment', equipment_id=equipment_id))
# GET request - show booking form
template = """
Book Equipment
container">
header">
Book Equipment
user-info">
Logged in as: {{ session.username }}
{{ url_for('logout') }}" class="btn btn-logout">Logout
{% if get_flashed_messages() %}
flash">
{{ get_flashed_messages()[0] }}
{% endif %}
equipment-details">
{{ equipment.name }}
Available Quantity: {{ equipment.available }} of {{ equipment.quantity }}
post">
form-group">
quantity">Quantity to Book:
number" id="quantity" name="quantity" min="1" max="{{ equipment.available }}" value="1" required>
form-group">
start_date">Start Date:
date" id="start_date" name="start_date" required>
form-group">
end_date">End Date:
date" id="end_date" name="end_date" required>
display: flex; gap: 10px;">
submit" class="btn">Book Now
{{ url_for('index') }}" class="btn btn-secondary">Cancel
"""
return render_template_string(template, equipment=equipment)
@app.route('/my-bookings')
@login_required
def my_bookings():
"""Show user's bookings"""
bookings = load_bookings()
user_bookings = [b for b in bookings if b['user'] == session['username']]
template = """
My Bookings
container">
header">
My Bookings
user-info">
Logged in as: {{ session.username }} ({{ session.role }})
{{ url_for('logout') }}" class="btn btn-logout">Logout
nav">
{{ url_for('index') }}" class="btn btn-primary">Equipment
{{ url_for('my_bookings') }}" class="btn btn-info">My Bookings
{% if session.role == 'manager' %}
{{ url_for('all_bookings') }}" class="btn btn-info">All Bookings
{{ url_for('add_equipment') }}" class="btn">Add Equipment
{% endif %}
{% if get_flashed_messages() %}
flash">
{{ get_flashed_messages()[0] }}
{% endif %}
{% if bookings|length == 0 %}
You don't have any bookings yet.
{ url_for('index') }}" class="btn">Book Equipment
{% else %}
ID
Equipment
Quantity
Start Date
End Date
Status
Actions
{% for booking in bookings %}
{{ booking.id }}
{{ booking.equipment_name }}
{{ booking.quantity }}
{{ booking.start_date }}
{{ booking.end_date }}
status status-{{ booking.status }}">
{{ booking.status|upper }}
{% if booking.status == "
approved" %}
{{ url_for('return_equipment', booking_id=booking.id) }}" class="btn">Return
{% elif booking.status == "pending" %}
{{ url_for('cancel_booking', booking_id=booking.id) }}" class="btn">Cancel
{% endif %}
{% endfor %}
{% endif %}
"""
return render_template_string(template, bookings=user_bookings)
@app.route('/all-bookings')
@login_required
@manager_required
def all_bookings():
"""Show all bookings (manager only)"""
bookings = load_bookings()
template = """
All Bookings
container">
header">
All Bookings
user-info">
Logged in as: {{ session.username }} (Manager)
{{ url_for('logout') }}" class="btn btn-logout">Logout
nav">
{{ url_for('index') }}" class="btn btn-primary">Equipment
{{ url_for('my_bookings') }}" class="btn btn-info">My Bookings
{{ url_for('all_bookings') }}" class="btn btn-info">All Bookings
{{ url_for('add_equipment') }}" class="btn">Add Equipment
{% if get_flashed_messages() %}
flash">
{{ get_flashed_messages()[0] }}
{% endif %}
{% if bookings|length == 0 %}
There are no bookings in the system.
{% else %}
ID
User
Equipment
Quantity
Start Date
End Date
Status
Actions
{% for booking in bookings %}
{{ booking.id }}
{{ booking.user }}
{{ booking.equipment_name }}
{{ booking.quantity }}
{{ booking.start_date }}
{{ booking.end_date }}
status status-{{ booking.status }}">
{{ booking.status|upper }}
{% if booking.status == "
pending" %}
{{ url_for('approve_booking', booking_id=booking.id) }}" class="btn">Approve
{{ url_for('reject_booking', booking_id=booking.id) }}" class="btn btn-danger">Reject
{% endif %}
{% endfor %}
{% endif %}
"""
return render_template_string(template, bookings=bookings)
@app.route('/approve-booking/')
@login_required
@manager_required
def approve_booking(booking_id):
"""Approve a pending booking (manager only)"""
bookings = load_bookings()
booking = next((b for b in bookings if b['id'] == booking_id and b['status'] == 'pending'), None)
if booking:
booking['status'] = 'approved'
save_bookings(bookings)
flash('Booking approved successfully')
else:
flash('Booking not found or not in pending status')
return redirect(url_for('all_bookings'))
@app.route('/reject-booking/')
@login_required
@manager_required
def reject_booking(booking_id):
"""Reject a pending booking (manager only)"""
bookings = load_bookings()
booking = next((b for b in bookings if b['id'] == booking_id and b['status'] == 'pending'), None)
if booking:
# Return equipment to available pool
equipment_list = load_equipment()
equipment = next((e for e in equipment_list if e['id'] == booking['equipment_id']), None)
if equipment:
equipment['available'] += booking['quantity']
save_equipment(equipment_list)
booking['status'] = 'rejected'
save_bookings(bookings)
flash('Booking rejected successfully')
else:
flash('Booking not found or not in pending status')
return redirect(url_for('all_bookings'))
@app.route('/cancel-booking/')
@login_required
def cancel_booking(booking_id):
"""Cancel a user's pending booking"""
bookings = load_bookings()
booking = next((b for b in bookings if b['id'] == booking_id and
b['user'] == session['username'] and
b['status'] == 'pending'), None)
if booking:
# Return equipment to available pool
equipment_list = load_equipment()
equipment = next((e for e in equipment_list if e['id'] == booking['equipment_id']), None)
if equipment:
equipment['available'] += booking['quantity']
save_equipment(equipment_list)
# Remove the booking
bookings.remove(booking)
save_bookings(bookings)
flash('Booking cancelled successfully')
else:
flash('Booking not found or cannot be cancelled')
return redirect(url_for('my_bookings'))
@app.route('/return-equipment/')
@login_required
def return_equipment(booking_id):
"""Return equipment for an approved booking"""
bookings = load_bookings()
booking = next((b for b in bookings if b['id'] == booking_id and
b['user'] == session['username'] and
b['status'] == 'approved'), None)
if booking:
# Return equipment to available pool
equipment_list = load_equipment()
equipment = next((e for e in equipment_list if e['id'] == booking['equipment_id']), None)
if equipment:
equipment['available'] += booking['quantity']
save_equipment(equipment_list)
booking['status'] = 'returned'
save_bookings(bookings)
flash('Equipment returned successfully')
else:
flash('Booking not found or not in approved status')
return redirect(url_for('my_bookings'))
@app.route('/add-equipment', methods=['GET', 'POST'])
@login_required
@manager_required
def add_equipment():
"""Add new equipment (manager only)"""
if request.method == 'POST':
name = request.form.get('name')
try:
quantity = int(request.form.get('quantity', 1))
if not name or not name.strip():
flash('Please provide a valid equipment name')
return redirect(url_for('add_equipment'))
if quantity <= 0:
flash('Quantity must be at least 1')
return redirect(url_for('add_equipment'))
# All validation passed, add equipment
equipment_list = load_equipment()
# Generate new ID
new_id = 1
if equipment_list:
max_id = max(int(item['id']) for item in equipment_list)
new_id = max_id + 1
# Create new equipment
new_equipment = {
'id': str(new_id),
'name': name,
'quantity': quantity,
'available': quantity
}
equipment_list.append(new_equipment)
save_equipment(equipment_list)
flash(f'Successfully added {quantity} {name}(s)')
return redirect(url_for('index'))
except ValueError:
flash('Please enter a valid quantity')
return redirect(url_for('add_equipment'))
# GET request - show add equipment form
template = """
Add Equipment
container">
header">
Add New Equipment
user-info">
Logged in as: {{ session.username }} (Manager)
{{ url_for('logout') }}" class="btn btn-logout">Logout
nav">
{{ url_for('index') }}" class="btn btn-primary">Equipment
{{ url_for('my_bookings') }}" class="btn btn-info">My Bookings
{{ url_for('all_bookings') }}" class="btn btn-info">All Bookings
{{ url_for('add_equipment') }}" class="btn">Add Equipment
{% if get_flashed_messages() %}
flash">
{{ get_flashed_messages()[0] }}
{% endif %}
post">
form-group">
name">Equipment Name:
text" id="name" name="name" required>
form-group">
quantity">Quantity:
number" id="quantity" name="quantity" min="1" value="1" required>
display: flex; gap: 10px;">
submit" class="btn">Add Equipment
{{ url_for('index') }}" class="btn btn-secondary">Cancel
"""
return render_template_string(template)
@app.route('/edit-equipment/', methods=['GET', 'POST'])
@login_required
@manager_required
def edit_equipment(equipment_id):
"""Edit existing equipment (manager only)"""
equipment_list = load_equipment()
equipment = next((item for item in equipment_list if item['id'] == equipment_id), None)
if not equipment:
flash('Equipment not found')
return redirect(url_for('index'))
if request.method == 'POST':
name = request.form.get('name')
try:
quantity = int(request.form.get('quantity', equipment['quantity']))
if not name or not name.strip():
flash('Please provide a valid equipment name')
return redirect(url_for('edit_equipment', equipment_id=equipment_id))
# Check if total quantity can be reduced (based on current bookings)
booked_quantity = equipment['quantity'] - equipment['available']
if quantity < booked_quantity:
flash('Cannot reduce quantity below the amount currently booked')
return redirect(url_for('edit_equipment', equipment_id=equipment_id))
# Update equipment
equipment['name'] = name
# Calculate new available quantity
equipment['available'] += (quantity - equipment['quantity'])
equipment['quantity'] = quantity
save_equipment(equipment_list)
flash('Equipment updated successfully')
return redirect(url_for('index'))
except ValueError:
flash('Please enter a valid quantity')
return redirect(url_for('edit_equipment', equipment_id=equipment_id))
# GET request - show edit equipment form
template = """
Edit Equipment
container">
header">
Edit Equipment
user-info">
Logged in as: {{ session.username }} (Manager)
{{ url_for('logout') }}" class="btn btn-logout">Logout
nav">
{{ url_for('index') }}" class="btn btn-primary">Equipment
{{ url_for('my_bookings') }}" class="btn btn-info">My Bookings
{{ url_for('all_bookings') }}" class="btn btn-info">All Bookings
{{ url_for('add_equipment') }}" class="btn">Add Equipment
{% if get_flashed_messages() %}
flash">
{{ get_flashed_messages()[0] }}
{% endif %}
equipment-details">
Current Details
Name: {{ equipment.name }}
Total Quantity: {{ equipment.quantity }}
Available: {{ equipment.available }}
Currently Booked: {{ equipment.quantity - equipment.available }}
{% if equipment.quantity > equipment.available %}
warning">
Note: Some of this equipment is currently booked. You cannot reduce the total quantity below {{ equipment.quantity - equipment.available }}.
{% endif %}
post">
form-group">
name">Equipment Name:
text" id="name" name="name" value="{{ equipment.name }}" required>
form-group">
quantity">Total Quantity:
number" id="quantity" name="quantity" min="{{ equipment.quantity - equipment.available }}" value="{{ equipment.quantity }}" required>
display: flex; gap: 10px;">
submit" class="btn">Update Equipment
{{ url_for('index') }}" class="btn btn-secondary">Cancel
"""
return render_template_string(template, equipment=equipment)
if __name__ == '__main__':
app.run(debug=True)