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('/', 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('//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)