Enhance event registration system:
- Update Google Sheets integration to support optional tab names. - Add functionality to fetch and display public participants. - Revise event configuration in YAML for clarity and consistency. - Improve form layout with additional fields for contact information and meal preferences. - Create a new home page template for event selection. - Update success page to link back to the participants list.
This commit is contained in:
parent
f83a061e39
commit
c160446eac
6 changed files with 219 additions and 42 deletions
129
app.py
129
app.py
|
|
@ -12,72 +12,141 @@ def load_config():
|
||||||
return yaml.safe_load(f)
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
# Connect to Google Sheets
|
# Connect to Google Sheets
|
||||||
def get_google_sheet(sheet_id):
|
# UPDATED: Now accepts an optional tab_name
|
||||||
|
def get_google_sheet(sheet_id, tab_name=None):
|
||||||
gc = gspread.service_account(filename='credentials.json')
|
gc = gspread.service_account(filename='credentials.json')
|
||||||
try:
|
try:
|
||||||
sh = gc.open_by_key(sheet_id)
|
sh = gc.open_by_key(sheet_id)
|
||||||
return sh.sheet1 # Assumes data goes into the first tab
|
if tab_name:
|
||||||
|
# Open specific tab by name
|
||||||
|
return sh.worksheet(tab_name)
|
||||||
|
else:
|
||||||
|
# Default to first tab
|
||||||
|
return sh.sheet1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error connecting to Google Sheet: {e}")
|
print(f"Error connecting to Google Sheet (ID: {sheet_id}, Tab: {tab_name}): {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# NEW: Fetch and filter participants for public display
|
||||||
|
# UPDATED: Now accepts tab_name
|
||||||
|
def get_public_participants(sheet_id, tab_name=None):
|
||||||
|
sheet = get_google_sheet(sheet_id, tab_name)
|
||||||
|
if not sheet:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get all rows
|
||||||
|
rows = sheet.get_all_values()
|
||||||
|
|
||||||
|
# Skip header row (assuming row 1 is header)
|
||||||
|
if len(rows) > 1:
|
||||||
|
data_rows = rows[1:]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
public_list = []
|
||||||
|
for row in data_rows:
|
||||||
|
# Ensure row is long enough to avoid errors
|
||||||
|
if len(row) < 17:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extract ONLY non-sensitive columns based on our save order
|
||||||
|
# 1: Klasse, 2: Zeilnummer, 3: Bootnaam, 4: Boottype
|
||||||
|
# 7: Naam, 10: Plaats, 16: Vereniging
|
||||||
|
entry = {
|
||||||
|
'klasse': row[1],
|
||||||
|
'zeilnummer': row[2],
|
||||||
|
'bootnaam': row[3],
|
||||||
|
'boottype': row[4],
|
||||||
|
'naam': row[7],
|
||||||
|
'plaats': row[10],
|
||||||
|
'vereniging': row[16]
|
||||||
|
}
|
||||||
|
public_list.append(entry)
|
||||||
|
|
||||||
|
return public_list
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching participants: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def home():
|
def home():
|
||||||
# List all available active events
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
events = config.get('events', {})
|
events = config.get('events', {})
|
||||||
return render_template('home.html', events=events)
|
return render_template('home.html', events=events)
|
||||||
|
|
||||||
# Dynamic Route for any event defined in YAML
|
|
||||||
@app.route('/<event_slug>', methods=['GET', 'POST'])
|
@app.route('/<event_slug>', methods=['GET', 'POST'])
|
||||||
def event_form(event_slug):
|
def event_form(event_slug):
|
||||||
config = load_config()
|
config = load_config()
|
||||||
events = config.get('events', {})
|
events = config.get('events', {})
|
||||||
|
|
||||||
# Check if event exists in config
|
# Check for global sheet_id fallback
|
||||||
|
global_sheet_id = config.get('master_sheet_id')
|
||||||
|
|
||||||
if event_slug not in events:
|
if event_slug not in events:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
event_data = events[event_slug]
|
event_data = events[event_slug]
|
||||||
|
|
||||||
|
# Determine which Sheet ID to use (Event specific > Global)
|
||||||
|
sheet_id = event_data.get('sheet_id', global_sheet_id)
|
||||||
|
# Get Tab Name (optional)
|
||||||
|
tab_name = event_data.get('tab_name')
|
||||||
|
|
||||||
|
# Ensure we have a valid sheet ID before proceeding
|
||||||
|
if not sheet_id:
|
||||||
|
return "Configuration Error: No 'sheet_id' found in event config or 'master_sheet_id' in root config."
|
||||||
|
|
||||||
|
# Update event_data with resolved sheet_id so templates work correctly
|
||||||
|
event_data['sheet_id'] = sheet_id
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# 1. Collect Form Data
|
|
||||||
form_data = [
|
form_data = [
|
||||||
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # Timestamp
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 0: Timestamp
|
||||||
request.form.get('klasse'),
|
request.form.get('klasse'), # 1: Klasse
|
||||||
request.form.get('zeilnummer'),
|
request.form.get('zeilnummer'), # 2: Zeilnummer
|
||||||
request.form.get('bootnaam'),
|
request.form.get('bootnaam'), # 3: Bootnaam
|
||||||
request.form.get('boottype'),
|
request.form.get('boottype'), # 4: Boottype
|
||||||
# Checkboxes (Join them or separate columns? Let's join for simplicity)
|
", ".join([k for k in ['genua', 'rolfok', 'spinaker', 'halfwinder', 'genaker', 'dacron'] if k in request.form]), # 5: Zeilvoering
|
||||||
", ".join([k for k in ['genua', 'rolfok', 'spinaker', 'halfwinder', 'genaker', 'dacron'] if k in request.form]),
|
request.form.get('schroef'), # 6: Schroef
|
||||||
request.form.get('schroef'),
|
request.form.get('naam'), # 7: Naam (Keep public)
|
||||||
request.form.get('naam'),
|
request.form.get('straat'), # 8: Straat (PRIVATE)
|
||||||
request.form.get('straat'),
|
request.form.get('postcode'), # 9: Postcode (PRIVATE)
|
||||||
request.form.get('postcode'),
|
request.form.get('plaats'), # 10: Plaats
|
||||||
request.form.get('plaats'),
|
request.form.get('land'), # 11: Land
|
||||||
request.form.get('land'),
|
request.form.get('telefoonmobiel'), # 12: Mobiel (PRIVATE)
|
||||||
request.form.get('telefoonmobiel'),
|
request.form.get('telefoonvast'), # 13: Vast (PRIVATE)
|
||||||
request.form.get('email'),
|
request.form.get('email'), # 14: Email (PRIVATE)
|
||||||
request.form.get('startlicentienummer'),
|
request.form.get('startlicentienummer'), # 15: Licentie
|
||||||
request.form.get('vereniging'),
|
request.form.get('vereniging'), # 16: Vereniging
|
||||||
request.form.get('opmerkingen')
|
request.form.get('buffet', '0'), # 17: Buffet
|
||||||
|
request.form.get('ontbijt', '0'), # 18: Ontbijt
|
||||||
|
request.form.get('opmerkingen') # 19: Opmerkingen
|
||||||
]
|
]
|
||||||
|
|
||||||
# 2. Push to Google Sheet
|
sheet = get_google_sheet(sheet_id, tab_name)
|
||||||
sheet = get_google_sheet(event_data['sheet_id'])
|
|
||||||
if sheet:
|
if sheet:
|
||||||
sheet.append_row(form_data)
|
sheet.append_row(form_data)
|
||||||
return redirect(url_for('success', event_slug=event_slug))
|
return redirect(url_for('success', event_slug=event_slug))
|
||||||
else:
|
else:
|
||||||
return "Error: Could not connect to Google Sheet. Check server logs."
|
return f"Error: Could not connect to Google Sheet Tab '{tab_name}'. Check server logs."
|
||||||
|
|
||||||
return render_template('form.html', event=event_data, slug=event_slug)
|
# GET Request: Fetch participants to show at bottom of form
|
||||||
|
participants = get_public_participants(sheet_id, tab_name)
|
||||||
|
return render_template('form.html', event=event_data, slug=event_slug, participants=participants)
|
||||||
|
|
||||||
@app.route('/<event_slug>/success')
|
@app.route('/<event_slug>/success')
|
||||||
def success(event_slug):
|
def success(event_slug):
|
||||||
config = load_config()
|
config = load_config()
|
||||||
event_data = config['events'].get(event_slug)
|
event_data = config['events'].get(event_slug)
|
||||||
return render_template('success.html', event=event_data)
|
|
||||||
|
# Resolve sheet ID for the success page link too
|
||||||
|
global_sheet_id = config.get('master_sheet_id')
|
||||||
|
sheet_id = event_data.get('sheet_id', global_sheet_id)
|
||||||
|
event_data['sheet_id'] = sheet_id
|
||||||
|
|
||||||
|
# Pass slug so we can link back
|
||||||
|
return render_template('success.html', event=event_data, slug=event_slug)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5000)
|
app.run(host='0.0.0.0', port=5000)
|
||||||
|
|
@ -4,6 +4,8 @@ services:
|
||||||
build: .
|
build: .
|
||||||
container_name: sailing_forms
|
container_name: sailing_forms
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
volumes:
|
volumes:
|
||||||
# Mount the config file so you can edit it without rebuilding
|
# Mount the config file so you can edit it without rebuilding
|
||||||
- ./events.yaml:/app/events.yaml
|
- ./events.yaml:/app/events.yaml
|
||||||
|
|
|
||||||
16
events.yaml
16
events.yaml
|
|
@ -1,18 +1,14 @@
|
||||||
# Define as many events as you want here.
|
# Define as many events as you want here.
|
||||||
# The key (e.g., 'zomeravond') becomes the URL: domain.com/zomeravond
|
# The key (e.g., 'zomeravond') becomes the URL: domain.com/zomeravond
|
||||||
|
master_sheet_id: "1k-eTke2GGmcMtq2-acvWPvjgfDeUHBsR0_Bd_tAbLx0"
|
||||||
|
|
||||||
events:
|
events:
|
||||||
zomeravond:
|
zomeravond:
|
||||||
title: "Inschrijving Zomeravondregatta"
|
title: "Zomeravondregatta 2026"
|
||||||
sheet_id: "1k-eTke2GGmcMtq2-acvWPvjgfDeUHBsR0_Bd_tAbLx0"
|
tab_name: "Zomeravond 2026"
|
||||||
description: "De gezelligste avondwedstrijd van het jaar."
|
description: "De gezelligste avondwedstrijd van het jaar."
|
||||||
|
|
||||||
papklokken:
|
papklokken:
|
||||||
title: "Papklokkenrace 2025"
|
title: "Papklokkenrace 2026"
|
||||||
sheet_id: "1k-eTke2GGmcMtq2-acvWPvjgfDeUHBsR0_Bd_tAbLx0"
|
tab_name: "Papklokken 2026"
|
||||||
description: "Sluit het seizoen af in stijl."
|
description: "Sluit het seizoen af in stijl."
|
||||||
|
|
||||||
winterwedstrijd:
|
|
||||||
title: "Winter Bokaal"
|
|
||||||
sheet_id: "1k-eTke2GGmcMtq2-acvWPvjgfDeUHBsR0_Bd_tAbLx0"
|
|
||||||
description: "Alleen voor de echte bikkels."
|
|
||||||
|
|
@ -94,9 +94,13 @@
|
||||||
<input type="text" class="form-control" name="land" value="Nederland">
|
<input type="text" class="form-control" name="land" value="Nederland">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Telefoon (06)</label>
|
<label class="form-label">Telefoon (Mobiel)</label>
|
||||||
<input type="text" class="form-control" name="telefoonmobiel" required>
|
<input type="text" class="form-control" name="telefoonmobiel" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Telefoon (Vast)</label>
|
||||||
|
<input type="text" class="form-control" name="telefoonvast">
|
||||||
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Email</label>
|
<label class="form-label">Email</label>
|
||||||
<input type="email" class="form-control" name="email" required>
|
<input type="email" class="form-control" name="email" required>
|
||||||
|
|
@ -111,6 +115,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Eten & Drinken -->
|
||||||
|
<h4 class="section-title mt-4">Eten & Drinken</h4>
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Aantal personen Buffet (Zaterdag)</label>
|
||||||
|
<input type="number" class="form-control" name="buffet" min="0" placeholder="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Aantal personen Ontbijt (Zondag)</label>
|
||||||
|
<input type="number" class="form-control" name="ontbijt" min="0" placeholder="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="form-label">Opmerkingen</label>
|
<label class="form-label">Opmerkingen</label>
|
||||||
<textarea class="form-control" name="opmerkingen" rows="3"></textarea>
|
<textarea class="form-control" name="opmerkingen" rows="3"></textarea>
|
||||||
|
|
@ -134,6 +151,45 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Participants List Section -->
|
||||||
|
<div class="form-card mt-5" id="deelnemers">
|
||||||
|
<h3 class="section-title">Huidige Deelnemers</h3>
|
||||||
|
|
||||||
|
{% if participants|length > 0 %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Klasse</th>
|
||||||
|
<th>Zeilnr</th>
|
||||||
|
<th>Bootnaam</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Schipper</th>
|
||||||
|
<th>Vereniging</th>
|
||||||
|
<th>Plaats</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in participants %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ p.klasse }}</td>
|
||||||
|
<td>{{ p.zeilnummer }}</td>
|
||||||
|
<td>{{ p.bootnaam }}</td>
|
||||||
|
<td>{{ p.boottype }}</td>
|
||||||
|
<td>{{ p.naam }}</td>
|
||||||
|
<td>{{ p.vereniging }}</td>
|
||||||
|
<td>{{ p.plaats }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-center text-muted">Er zijn nog geen inschrijvingen.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
51
templates/home.html
Normal file
51
templates/home.html
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Zeilwedstrijden Inschrijving</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.event-card {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.event-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center text-center mb-5">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<h1 class="display-4 fw-bold text-primary">Wedstrijd Inschrijvingen</h1>
|
||||||
|
<p class="lead text-muted">Kies hieronder de wedstrijd waarvoor je je wilt inschrijven.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 justify-content-center">
|
||||||
|
<!-- Loop through the events defined in events.yaml -->
|
||||||
|
{% for slug, data in events.items() %}
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 shadow-sm event-card border-0">
|
||||||
|
<div class="card-body p-4 text-center">
|
||||||
|
<div class="mb-3">
|
||||||
|
<!-- Simple Icon using Bootstrap Icons (SVG) or just text -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="#0d6efd" class="bi bi-water" viewBox="0 0 16 16">
|
||||||
|
<path d="M.036 3.314a.5.5 0 0 1 .65-.278l1.757.703a1.5 1.5 0 0 0 1.114 0l1.014-.406a2.5 2.5 0 0 1 1.857 0l1.015.406a1.5 1.5 0 0 0 1.114 0l1.014-.406a2.5 2.5 0 0 1 1.857 0l1.015.406a1.5 1.5 0 0 0 1.114 0l1.757-.703a.5.5 0 1 1 .354.928l-1.757.703a2.5 2.5 0 0 1-1.857 0l-1.014-.406a1.5 1.5 0 0 0-1.114 0l-1.015.406a2.5 2.5 0 0 1-1.857 0l-1.014-.406a1.5 1.5 0 0 0-1.114 0l-1.015.406a2.5 2.5 0 0 1-1.857 0l-1.757-.703a.5.5 0 0 1-.278-.65zm0 3a.5.5 0 0 1 .65-.278l1.757.703a1.5 1.5 0 0 0 1.114 0l1.014-.406a2.5 2.5 0 0 1 1.857 0l1.015.406a1.5 1.5 0 0 0 1.114 0l1.014-.406a2.5 2.5 0 0 1 1.857 0l1.015.406a1.5 1.5 0 0 0 1.114 0l1.757-.703a.5.5 0 1 1 .354.928l-1.757.703a2.5 2.5 0 0 1-1.857 0l-1.014-.406a1.5 1.5 0 0 0-1.114 0l-1.015.406a2.5 2.5 0 0 1-1.857 0l-1.014-.406a1.5 1.5 0 0 0-1.114 0l-1.015.406a2.5 2.5 0 0 1-1.857 0l-1.757-.703a.5.5 0 0 1-.278-.65z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="card-title h4">{{ data.title }}</h3>
|
||||||
|
<p class="card-text text-muted mb-4">{{ data.description }}</p>
|
||||||
|
<a href="/{{ slug }}" class="btn btn-primary w-100">Inschrijven →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -13,7 +13,10 @@
|
||||||
<p class="lead">Je inschrijving voor <strong>{{ event.title }}</strong> is ontvangen.</p>
|
<p class="lead">Je inschrijving voor <strong>{{ event.title }}</strong> is ontvangen.</p>
|
||||||
<p>De gegevens zijn opgeslagen.</p>
|
<p>De gegevens zijn opgeslagen.</p>
|
||||||
<hr>
|
<hr>
|
||||||
<a href="https://docs.google.com/spreadsheets/d/{{ event.sheet_id }}" target="_blank" class="btn btn-outline-success">Bekijk deelnemers (Google Sheets)</a>
|
<!-- Link back to the form page, scrolled down to the list -->
|
||||||
|
<a href="/{{ slug }}#deelnemers" class="btn btn-primary">Bekijk deelnemerslijst</a>
|
||||||
|
<br><br>
|
||||||
|
<a href="/" class="btn btn-link text-muted">Terug naar overzicht</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue