sailing_forms/app.py

198 lines
No EOL
7.3 KiB
Python

import os
import yaml
import gspread
import secrets
import logging
import sys
from flask import Flask, render_template, request, redirect, url_for, abort, session
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__)
# 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
def load_config():
with open('events.yaml', 'r') as f:
return yaml.safe_load(f)
# Connect to Google Sheets
# UPDATED: Now accepts an optional tab_name
def get_google_sheet(sheet_id, tab_name=None):
gc = gspread.service_account(filename='credentials.json')
try:
sh = gc.open_by_key(sheet_id)
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:
logger.error(f"Error connecting to Google Sheet (ID: {sheet_id}, Tab: {tab_name}): {e}")
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) < 14:
continue
# Extract ONLY non-sensitive columns based on our save order
# 1: Klasse, 2: Zeilnummer, 3: Bootnaam, 4: Boottype
# 7: Naam, 8: Plaats, 13: Vereniging
entry = {
'klasse': row[1],
'zeilnummer': row[2],
'bootnaam': row[3],
'boottype': row[4],
'naam': row[7],
'plaats': row[8],
'vereniging': row[13]
}
public_list.append(entry)
return public_list
except Exception as e:
logger.error(f"Error fetching participants: {e}")
return []
@app.route('/')
def home():
config = load_config()
events = config.get('events', {})
return render_template('home.html', events=events)
@app.route('/<event_slug>', methods=['GET', 'POST'])
def event_form(event_slug):
config = load_config()
events = config.get('events', {})
# Check for global sheet_id fallback
global_sheet_id = config.get('master_sheet_id')
if event_slug not in events:
abort(404)
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':
# 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 = [
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 0: Timestamp
request.form.get('klasse'), # 1: Klasse
request.form.get('zeilnummer'), # 2: Zeilnummer
request.form.get('bootnaam'), # 3: Bootnaam
request.form.get('boottype'), # 4: Boottype
", ".join([k for k in ['genua', 'rolfok', 'spinaker', 'halfwinder', 'genaker', 'alleendacron'] if k in request.form]), # 5: Zeilvoering
request.form.get('schroef'), # 6: Schroef
request.form.get('naam'), # 7: Naam (Keep public)
request.form.get('plaats'), # 8: Plaats
request.form.get('land', 'Nederland'), # 9: Land
request.form.get('telefoonmobiel'), # 10: Mobiel (PRIVATE)
request.form.get('email'), # 11: Email (PRIVATE)
request.form.get('startlicentienummer'), # 12: Licentie
request.form.get('vereniging'), # 13: Vereniging
request.form.get('opmerkingen') # 14: Opmerkingen
]
sheet = get_google_sheet(sheet_id, tab_name)
if sheet:
if not os.environ.get('TESTING_NO_APPEND'):
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))
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."
# 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')
def success(event_slug):
config = load_config()
event_data = config['events'].get(event_slug)
# 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__':
app.run(host='0.0.0.0', port=5000)