Compare commits
No commits in common. "d8ceb50e9f89cf3b59550e632feab2c5b1884bf6" and "c160446eac9987fa85b42690d3fffb7a4b6b0e0d" have entirely different histories.
d8ceb50e9f
...
c160446eac
7 changed files with 92 additions and 165 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1,5 +1 @@
|
||||||
credentials.json
|
credentials.json
|
||||||
.venv/
|
|
||||||
*.pyc
|
|
||||||
__pycache__/
|
|
||||||
.env
|
|
||||||
90
app.py
90
app.py
|
|
@ -1,47 +1,11 @@
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
import gspread
|
import gspread
|
||||||
import secrets
|
from flask import Flask, render_template, request, redirect, url_for, abort
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
from flask import Flask, render_template, request, redirect, url_for, abort, session
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Configure logging for Docker/Portainer
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
handlers=[logging.StreamHandler(sys.stdout)]
|
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# Fail-fast secret requirement (allow bypass for pytest testing)
|
|
||||||
secret_key = os.environ.get('SECRET_KEY')
|
|
||||||
if not secret_key:
|
|
||||||
if os.environ.get('TESTING_NO_APPEND') or 'pytest' in sys.modules:
|
|
||||||
secret_key = 'test_secret_bypassed'
|
|
||||||
else:
|
|
||||||
logger.critical("No SECRET_KEY set! Exiting. Set it in .env or Portainer Stack secrets.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
app.secret_key = secret_key
|
|
||||||
|
|
||||||
def generate_csrf_token():
|
|
||||||
if '_csrf_token' not in session:
|
|
||||||
session['_csrf_token'] = secrets.token_hex(16)
|
|
||||||
return session['_csrf_token']
|
|
||||||
|
|
||||||
app.jinja_env.globals['csrf_token'] = generate_csrf_token
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def csrf_protect():
|
|
||||||
if request.method == "POST":
|
|
||||||
token = session.get('_csrf_token', None)
|
|
||||||
if not token or token != request.form.get('_csrf_token'):
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
# Load Configuration
|
# Load Configuration
|
||||||
def load_config():
|
def load_config():
|
||||||
with open('events.yaml', 'r') as f:
|
with open('events.yaml', 'r') as f:
|
||||||
|
|
@ -60,7 +24,7 @@ def get_google_sheet(sheet_id, tab_name=None):
|
||||||
# Default to first tab
|
# Default to first tab
|
||||||
return sh.sheet1
|
return sh.sheet1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error connecting to Google Sheet (ID: {sheet_id}, Tab: {tab_name}): {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
|
# NEW: Fetch and filter participants for public display
|
||||||
|
|
@ -83,26 +47,26 @@ def get_public_participants(sheet_id, tab_name=None):
|
||||||
public_list = []
|
public_list = []
|
||||||
for row in data_rows:
|
for row in data_rows:
|
||||||
# Ensure row is long enough to avoid errors
|
# Ensure row is long enough to avoid errors
|
||||||
if len(row) < 14:
|
if len(row) < 17:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Extract ONLY non-sensitive columns based on our save order
|
# Extract ONLY non-sensitive columns based on our save order
|
||||||
# 1: Klasse, 2: Zeilnummer, 3: Bootnaam, 4: Boottype
|
# 1: Klasse, 2: Zeilnummer, 3: Bootnaam, 4: Boottype
|
||||||
# 7: Naam, 8: Plaats, 13: Vereniging
|
# 7: Naam, 10: Plaats, 16: Vereniging
|
||||||
entry = {
|
entry = {
|
||||||
'klasse': row[1],
|
'klasse': row[1],
|
||||||
'zeilnummer': row[2],
|
'zeilnummer': row[2],
|
||||||
'bootnaam': row[3],
|
'bootnaam': row[3],
|
||||||
'boottype': row[4],
|
'boottype': row[4],
|
||||||
'naam': row[7],
|
'naam': row[7],
|
||||||
'plaats': row[8],
|
'plaats': row[10],
|
||||||
'vereniging': row[13]
|
'vereniging': row[16]
|
||||||
}
|
}
|
||||||
public_list.append(entry)
|
public_list.append(entry)
|
||||||
|
|
||||||
return public_list
|
return public_list
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching participants: {e}")
|
print(f"Error fetching participants: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
|
|
@ -137,44 +101,34 @@ def event_form(event_slug):
|
||||||
event_data['sheet_id'] = sheet_id
|
event_data['sheet_id'] = sheet_id
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# Basic Validation
|
|
||||||
required_fields = ['klasse', 'zeilnummer', 'bootnaam', 'naam', 'telefoonmobiel', 'email']
|
|
||||||
for field in required_fields:
|
|
||||||
val = request.form.get(field)
|
|
||||||
if not val or not val.strip():
|
|
||||||
participants = get_public_participants(sheet_id, tab_name)
|
|
||||||
return render_template('form.html', event=event_data, slug=event_slug, participants=participants, error="Oeps! Bepaalde verplichte velden ontbreken.")
|
|
||||||
|
|
||||||
form_data = [
|
form_data = [
|
||||||
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 0: Timestamp
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 0: Timestamp
|
||||||
request.form.get('klasse'), # 1: Klasse
|
request.form.get('klasse'), # 1: Klasse
|
||||||
request.form.get('zeilnummer'), # 2: Zeilnummer
|
request.form.get('zeilnummer'), # 2: Zeilnummer
|
||||||
request.form.get('bootnaam'), # 3: Bootnaam
|
request.form.get('bootnaam'), # 3: Bootnaam
|
||||||
request.form.get('boottype'), # 4: Boottype
|
request.form.get('boottype'), # 4: Boottype
|
||||||
", ".join([k for k in ['genua', 'rolfok', 'spinaker', 'halfwinder', 'genaker', 'alleendacron'] if k in request.form]), # 5: Zeilvoering
|
", ".join([k for k in ['genua', 'rolfok', 'spinaker', 'halfwinder', 'genaker', 'dacron'] if k in request.form]), # 5: Zeilvoering
|
||||||
request.form.get('schroef'), # 6: Schroef
|
request.form.get('schroef'), # 6: Schroef
|
||||||
request.form.get('naam'), # 7: Naam (Keep public)
|
request.form.get('naam'), # 7: Naam (Keep public)
|
||||||
request.form.get('plaats'), # 8: Plaats
|
request.form.get('straat'), # 8: Straat (PRIVATE)
|
||||||
request.form.get('land', 'Nederland'), # 9: Land
|
request.form.get('postcode'), # 9: Postcode (PRIVATE)
|
||||||
request.form.get('telefoonmobiel'), # 10: Mobiel (PRIVATE)
|
request.form.get('plaats'), # 10: Plaats
|
||||||
request.form.get('email'), # 11: Email (PRIVATE)
|
request.form.get('land'), # 11: Land
|
||||||
request.form.get('startlicentienummer'), # 12: Licentie
|
request.form.get('telefoonmobiel'), # 12: Mobiel (PRIVATE)
|
||||||
request.form.get('vereniging'), # 13: Vereniging
|
request.form.get('telefoonvast'), # 13: Vast (PRIVATE)
|
||||||
request.form.get('opmerkingen') # 14: Opmerkingen
|
request.form.get('email'), # 14: Email (PRIVATE)
|
||||||
|
request.form.get('startlicentienummer'), # 15: Licentie
|
||||||
|
request.form.get('vereniging'), # 16: Vereniging
|
||||||
|
request.form.get('buffet', '0'), # 17: Buffet
|
||||||
|
request.form.get('ontbijt', '0'), # 18: Ontbijt
|
||||||
|
request.form.get('opmerkingen') # 19: Opmerkingen
|
||||||
]
|
]
|
||||||
|
|
||||||
sheet = get_google_sheet(sheet_id, tab_name)
|
sheet = get_google_sheet(sheet_id, tab_name)
|
||||||
if sheet:
|
if sheet:
|
||||||
if not os.environ.get('TESTING_NO_APPEND'):
|
sheet.append_row(form_data)
|
||||||
try:
|
|
||||||
sheet.append_row(form_data)
|
|
||||||
logger.info(f"Successfully appended registration for {form_data[7]} to {tab_name}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to append row to {tab_name}: {e}")
|
|
||||||
return f"Error appending data. Try again later."
|
|
||||||
return redirect(url_for('success', event_slug=event_slug))
|
return redirect(url_for('success', event_slug=event_slug))
|
||||||
else:
|
else:
|
||||||
logger.error(f"Could not connect to tab '{tab_name}' during POST.")
|
|
||||||
return f"Error: Could not connect to Google Sheet Tab '{tab_name}'. Check server logs."
|
return f"Error: Could not connect to Google Sheet Tab '{tab_name}'. Check server logs."
|
||||||
|
|
||||||
# GET Request: Fetch participants to show at bottom of form
|
# GET Request: Fetch participants to show at bottom of form
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ services:
|
||||||
build: .
|
build: .
|
||||||
container_name: sailing_forms
|
container_name: sailing_forms
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,7 @@
|
||||||
<h1 class="text-center mb-2">{{ event.title }}</h1>
|
<h1 class="text-center mb-2">{{ event.title }}</h1>
|
||||||
<p class="text-center text-muted mb-4">{{ event.description }}</p>
|
<p class="text-center text-muted mb-4">{{ event.description }}</p>
|
||||||
|
|
||||||
{% if error %}
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<!-- Boot Info -->
|
<!-- Boot Info -->
|
||||||
<h4 class="section-title">De Boot</h4>
|
<h4 class="section-title">De Boot</h4>
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
|
|
@ -84,6 +77,14 @@
|
||||||
<label class="form-label">Naam</label>
|
<label class="form-label">Naam</label>
|
||||||
<input type="text" class="form-control" name="naam" required>
|
<input type="text" class="form-control" name="naam" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label">Straat + Huisnr</label>
|
||||||
|
<input type="text" class="form-control" name="straat">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Postcode</label>
|
||||||
|
<input type="text" class="form-control" name="postcode">
|
||||||
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Plaats</label>
|
<label class="form-label">Plaats</label>
|
||||||
<input type="text" class="form-control" name="plaats">
|
<input type="text" class="form-control" name="plaats">
|
||||||
|
|
@ -93,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">Telefoonnummer</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>
|
||||||
|
|
@ -110,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>
|
||||||
|
|
|
||||||
43
templates/participants.html
Normal file
43
templates/participants.html
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Deelnemerslijst</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light p-4">
|
||||||
|
<div class="container bg-white p-4 rounded shadow">
|
||||||
|
<h2 class="mb-4 text-primary">Huidige Deelnemers</h2>
|
||||||
|
<a href="/" class="btn btn-outline-secondary mb-4">← Terug naar inschrijving</a>
|
||||||
|
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Klasse</th>
|
||||||
|
<th>Zeilnummer</th>
|
||||||
|
<th>Bootnaam</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Schipper</th>
|
||||||
|
<th>Vereniging</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for boat in boats %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ boat.klasse }}</td>
|
||||||
|
<td>{{ boat.zeilnummer }}</td>
|
||||||
|
<td>{{ boat.bootnaam }}</td>
|
||||||
|
<td>{{ boat.boottype }}</td>
|
||||||
|
<td>{{ boat.naam }}</td>
|
||||||
|
<td>{{ boat.vereniging }}</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center">Nog geen inschrijvingen.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import pytest
|
|
||||||
from app import app
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def client():
|
|
||||||
app.config['TESTING'] = True
|
|
||||||
with app.test_client() as client:
|
|
||||||
with client.session_transaction() as sess:
|
|
||||||
sess['_csrf_token'] = 'test-token'
|
|
||||||
yield client
|
|
||||||
|
|
||||||
def test_home_page(client):
|
|
||||||
response = client.get('/')
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert b"Zomeravond" in response.data or b"Papklokken" in response.data
|
|
||||||
|
|
||||||
@patch('app.get_google_sheet')
|
|
||||||
def test_post_missing_fields(mock_get_sheet, client):
|
|
||||||
response = client.post('/zomeravond', data={'_csrf_token': 'test-token'})
|
|
||||||
assert b"Oeps! Bepaalde verplichte velden ontbreken" in response.data
|
|
||||||
mock_get_sheet.return_value.append_row.assert_not_called()
|
|
||||||
|
|
||||||
@patch('app.get_google_sheet')
|
|
||||||
@patch('app.get_public_participants')
|
|
||||||
def test_post_success(mock_participants, mock_get_sheet, client):
|
|
||||||
mock_participants.return_value = []
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'_csrf_token': 'test-token',
|
|
||||||
'klasse': 'Kajuitklasse',
|
|
||||||
'zeilnummer': '123',
|
|
||||||
'bootnaam': 'TestBoat',
|
|
||||||
'naam': 'Test Name',
|
|
||||||
'telefoonmobiel': '0612345678',
|
|
||||||
'email': 'test@example.com'
|
|
||||||
}
|
|
||||||
|
|
||||||
response = client.post('/zomeravond', data=data)
|
|
||||||
assert response.status_code == 302
|
|
||||||
assert '/zomeravond/success' in response.headers.get('Location', '')
|
|
||||||
|
|
||||||
# In full testing, append_row is called unless TESTING_NO_APPEND is set.
|
|
||||||
# But since it's mocked, we can check it.
|
|
||||||
assert mock_get_sheet.return_value.append_row.called
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import pytest
|
|
||||||
from playwright.sync_api import sync_playwright, expect
|
|
||||||
|
|
||||||
def test_homepage_has_title():
|
|
||||||
with sync_playwright() as p:
|
|
||||||
browser = p.chromium.launch()
|
|
||||||
page = browser.new_page()
|
|
||||||
page.goto("http://localhost:5000/")
|
|
||||||
expect(page).to_have_title(re.compile("Zeilwedstrijden"))
|
|
||||||
browser.close()
|
|
||||||
|
|
||||||
def test_submission_flow():
|
|
||||||
with sync_playwright() as p:
|
|
||||||
browser = p.chromium.launch()
|
|
||||||
page = browser.new_page()
|
|
||||||
page.goto("http://localhost:5000/zomeravond")
|
|
||||||
|
|
||||||
# Fill required fields
|
|
||||||
page.select_option("select[name='klasse']", label="Kajuitklasse")
|
|
||||||
page.fill("input[name='zeilnummer']", "42")
|
|
||||||
page.fill("input[name='bootnaam']", "Vliegende Hollander")
|
|
||||||
page.fill("input[name='naam']", "Hendrik Test")
|
|
||||||
page.fill("input[name='telefoonmobiel']", "0612345678")
|
|
||||||
page.fill("input[name='email']", "hendrik@example.com")
|
|
||||||
|
|
||||||
# Accept terms
|
|
||||||
page.check("input#terms")
|
|
||||||
|
|
||||||
# Submit
|
|
||||||
page.click("button[type='submit']")
|
|
||||||
|
|
||||||
# Expect success redirect
|
|
||||||
expect(page).to_have_url(re.compile(r".*/zomeravond/success"))
|
|
||||||
expect(page.locator("h1")).to_have_text("Bedankt!")
|
|
||||||
browser.close()
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue