""" Route handlers for the Flask job application system. """ import os import uuid from datetime import datetime from pathlib import Path from flask import render_template, request, redirect, url_for, flash, session, send_from_directory, abort from werkzeug.utils import secure_filename from app.models import ( load_application_data, save_application_data, get_attachments_path ) from app.validators import ( validate_email, validate_phone, validate_year, validate_zip, allowed_file ) from app.email_service import send_resume_email, send_hr_notification from app.utils import check_rate_limit def register_routes(app, mail): """Register all application routes""" def can_access_page(session_id, requested_page): """ Determine if user can access the requested page. Rules: - Can always access pages <= current_page (backward navigation) - Cannot access pages > current_page (must progress sequentially) - Page 1 is always accessible """ if requested_page == 1: return True app_data = load_application_data(session_id) if not app_data: return False current_page = app_data.get('current_page', 1) return requested_page <= current_page @app.route('/') def index(): """Redirect to apply page""" return redirect(url_for('page1_email')) @app.route('/apply') def page1_email(): """Page 1: Email capture and session initialization""" job_name = request.args.get('job', 'Offene Position') return render_template('page1_email.html', job_name=job_name) @app.route('/apply/submit-email', methods=['POST']) def submit_email(): """Process email submission and create session""" # Check rate limit allowed, wait_seconds = check_rate_limit() if not allowed: flash(f'Bitte warten Sie noch {wait_seconds} Sekunden vor der nächsten Eingabe.', 'error') return redirect(url_for('page1_email')), 429 email = request.form.get('email', '').strip() job_name = request.form.get('job_name', 'Offene Position') # Validate email if not validate_email(email): flash('Bitte geben Sie eine gültige E-Mail-Adresse ein.', 'error') return redirect(url_for('page1_email', job=job_name)) # Generate session ID session_id = str(uuid.uuid4()) # Create application data app_data = { 'session_id': session_id, 'email': email, 'job_name': job_name, 'current_page': 2, 'created_at': datetime.now().isoformat(), 'updated_at': datetime.now().isoformat(), 'personal_info': {}, 'motivation_answers': {}, 'uploaded_files': [] } # Save application data save_application_data(session_id, app_data) # Send resume email if send_resume_email(email, session_id, job_name): flash('Eine E-Mail mit einem Link zum Fortsetzen wurde an Ihre Adresse gesendet.', 'success') else: flash('Warnung: Die E-Mail konnte nicht gesendet werden. Bitte notieren Sie sich diese URL.', 'warning') # Store session ID in Flask session session['application_id'] = session_id return redirect(url_for('page2_personal', session_id=session_id)) @app.route('/apply//personal') def page2_personal(session_id): """Page 2: Basic personal information""" # Validate page access if not can_access_page(session_id, 2): flash('Sie müssen zuerst die vorherigen Schritte abschließen.', 'error') return redirect(url_for('page1_email')) app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') return redirect(url_for('page1_email')) return render_template('page2_personal.html', session_id=session_id, job_name=app_data['job_name'], current_page=app_data.get('current_page', 2), data=app_data.get('personal_info', {})) @app.route('/apply//submit-personal', methods=['POST']) def submit_personal(session_id): """Process personal information submission""" # Check rate limit allowed, wait_seconds = check_rate_limit() if not allowed: flash(f'Bitte warten Sie noch {wait_seconds} Sekunden vor der nächsten Eingabe.', 'error') return redirect(url_for('page2_personal', session_id=session_id)), 429 app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') return redirect(url_for('page1_email')) # Get form data name = request.form.get('name', '').strip() firstname = request.form.get('firstname', '').strip() address = request.form.get('address', '').strip() zip_code = request.form.get('zip_code', '').strip() city = request.form.get('city', '').strip() phone = request.form.get('phone', '').strip() birth_year = request.form.get('birth_year', '').strip() civil_status = request.form.get('civil_status', '').strip() # Validate required fields errors = [] if not name or len(name) > app.config['MAX_STRING_LENGTH']: errors.append('Name ist erforderlich und darf maximal 255 Zeichen lang sein.') if not firstname or len(firstname) > app.config['MAX_STRING_LENGTH']: errors.append('Vorname ist erforderlich und darf maximal 255 Zeichen lang sein.') if not address or len(address) > app.config['MAX_STRING_LENGTH']: errors.append('Adresse ist erforderlich und darf maximal 255 Zeichen lang sein.') if not zip_code or not validate_zip(zip_code): errors.append('PLZ ist erforderlich und darf maximal 10 Ziffern lang sein.') if not city or len(city) > app.config['MAX_STRING_LENGTH']: errors.append('Stadt ist erforderlich und darf maximal 255 Zeichen lang sein.') if not phone or not validate_phone(phone): errors.append('Telefonnummer ist erforderlich und muss im internationalen Format sein (z.B. +41 79 123 45 67).') if not birth_year or not validate_year(birth_year): errors.append(f'Geburtsjahr ist erforderlich und muss zwischen {app.config["MIN_BIRTH_YEAR"]} und {app.config["MAX_BIRTH_YEAR"]} liegen.') if civil_status and len(civil_status) > app.config['MAX_STRING_LENGTH']: errors.append('Zivilstand darf maximal 255 Zeichen lang sein.') if errors: for error in errors: flash(error, 'error') return redirect(url_for('page2_personal', session_id=session_id)) # Save personal information app_data['personal_info'] = { 'name': name, 'firstname': firstname, 'address': address, 'zip_code': zip_code, 'city': city, 'phone': phone, 'birth_year': birth_year, 'civil_status': civil_status } app_data['current_page'] = 3 app_data['updated_at'] = datetime.now().isoformat() save_application_data(session_id, app_data) return redirect(url_for('page3_motivation', session_id=session_id)) @app.route('/apply//motivation') def page3_motivation(session_id): """Page 3: Motivation and qualification questions""" # Validate page access if not can_access_page(session_id, 3): app_data = load_application_data(session_id) if app_data: current_page = app_data.get('current_page', 2) if current_page == 2: return redirect(url_for('page2_personal', session_id=session_id)) flash('Sie müssen zuerst die vorherigen Schritte abschließen.', 'error') return redirect(url_for('page1_email')) app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') return redirect(url_for('page1_email')) return render_template('page3_motivation.html', session_id=session_id, job_name=app_data['job_name'], current_page=app_data.get('current_page', 3), data=app_data.get('motivation_answers', {})) @app.route('/apply//submit-motivation', methods=['POST']) def submit_motivation(session_id): """Process motivation questions submission""" # Check rate limit allowed, wait_seconds = check_rate_limit() if not allowed: flash(f'Bitte warten Sie noch {wait_seconds} Sekunden vor der nächsten Eingabe.', 'error') return redirect(url_for('page3_motivation', session_id=session_id)), 429 app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') return redirect(url_for('page1_email')) # Get form data current_job = request.form.get('current_job', '').strip() motivation = request.form.get('motivation', '').strip() qualifications = request.form.get('qualifications', '').strip() salary = request.form.get('salary', '').strip() # Validate length errors = [] if current_job and len(current_job) > app.config['MAX_TEXT_AREA_LENGTH']: errors.append(f'Die Beschreibung Ihrer beruflichen Situation darf maximal {app.config["MAX_TEXT_AREA_LENGTH"]} Zeichen lang sein.') if motivation and len(motivation) > app.config['MAX_TEXT_AREA_LENGTH']: errors.append(f'Die Motivationsbeschreibung darf maximal {app.config["MAX_TEXT_AREA_LENGTH"]} Zeichen lang sein.') if qualifications and len(qualifications) > app.config['MAX_TEXT_AREA_LENGTH']: errors.append(f'Die Qualifikationsbeschreibung darf maximal {app.config["MAX_TEXT_AREA_LENGTH"]} Zeichen lang sein.') if salary and len(salary) > app.config['MAX_TEXT_AREA_LENGTH']: errors.append(f'Die Gehaltsvorstellungen dürfen maximal {app.config["MAX_TEXT_AREA_LENGTH"]} Zeichen lang sein.') if errors: for error in errors: flash(error, 'error') return redirect(url_for('page3_motivation', session_id=session_id)) # Save motivation answers app_data['motivation_answers'] = { 'current_job': current_job, 'motivation': motivation, 'qualifications': qualifications, 'salary': salary } app_data['current_page'] = 4 app_data['updated_at'] = datetime.now().isoformat() save_application_data(session_id, app_data) return redirect(url_for('page4_upload', session_id=session_id)) @app.route('/apply//upload') def page4_upload(session_id): """Page 4: Document upload""" # Validate page access if not can_access_page(session_id, 4): app_data = load_application_data(session_id) if app_data: current_page = app_data.get('current_page', 2) if current_page == 2: return redirect(url_for('page2_personal', session_id=session_id)) elif current_page == 3: return redirect(url_for('page3_motivation', session_id=session_id)) flash('Sie müssen zuerst die vorherigen Schritte abschließen.', 'error') return redirect(url_for('page1_email')) app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') return redirect(url_for('page1_email')) return render_template('page4_upload.html', session_id=session_id, job_name=app_data['job_name'], current_page=app_data.get('current_page', 4), uploaded_files=app_data.get('uploaded_files', []), max_files=app.config['MAX_FILES']) @app.route('/apply//upload-file', methods=['POST']) def upload_file(session_id): """Handle file upload""" # Check rate limit allowed, wait_seconds = check_rate_limit() if not allowed: flash(f'Bitte warten Sie noch {wait_seconds} Sekunden vor der nächsten Eingabe.', 'error') return redirect(url_for('page4_upload', session_id=session_id)), 429 app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') return redirect(url_for('page1_email')) # Check file count if len(app_data.get('uploaded_files', [])) >= app.config['MAX_FILES']: flash(f'Sie können maximal {app.config["MAX_FILES"]} Dokumente hochladen.', 'error') return redirect(url_for('page4_upload', session_id=session_id)) # Check if file was uploaded if 'file' not in request.files: flash('Keine Datei ausgewählt.', 'error') return redirect(url_for('page4_upload', session_id=session_id)) file = request.files['file'] if file.filename == '': flash('Keine Datei ausgewählt.', 'error') return redirect(url_for('page4_upload', session_id=session_id)) # Validate file if not allowed_file(file.filename): flash('Ungültiger Dateityp. Erlaubte Formate: PDF, DOC, DOCX, TXT, JPG, JPEG, PNG', 'error') return redirect(url_for('page4_upload', session_id=session_id)) # Check file size file.seek(0, os.SEEK_END) file_size = file.tell() file.seek(0) if file_size > app.config['MAX_FILE_SIZE']: flash(f'Die Datei ist zu groß. Maximale Größe: 4 MB', 'error') return redirect(url_for('page4_upload', session_id=session_id)) # Save file filename = secure_filename(file.filename) timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') unique_filename = f"{timestamp}_{filename}" attachments_path = get_attachments_path(session_id) Path(attachments_path).mkdir(parents=True, exist_ok=True) file_path = os.path.join(attachments_path, unique_filename) file.save(file_path) # Update application data if 'uploaded_files' not in app_data: app_data['uploaded_files'] = [] app_data['uploaded_files'].append({ 'original_name': filename, 'stored_name': unique_filename, 'uploaded_at': datetime.now().isoformat(), 'size': file_size }) app_data['updated_at'] = datetime.now().isoformat() save_application_data(session_id, app_data) flash('Datei erfolgreich hochgeladen.', 'success') return redirect(url_for('page4_upload', session_id=session_id)) @app.route('/apply//remove-file/', methods=['POST']) def remove_file(session_id, file_index): """Remove an uploaded file""" app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') return redirect(url_for('page1_email')) uploaded_files = app_data.get('uploaded_files', []) if 0 <= file_index < len(uploaded_files): file_info = uploaded_files[file_index] # Delete physical file file_path = os.path.join(get_attachments_path(session_id), file_info['stored_name']) if os.path.exists(file_path): os.remove(file_path) # Remove from data uploaded_files.pop(file_index) app_data['updated_at'] = datetime.now().isoformat() save_application_data(session_id, app_data) flash('Datei erfolgreich entfernt.', 'success') else: flash('Datei nicht gefunden.', 'error') return redirect(url_for('page4_upload', session_id=session_id)) @app.route('/apply//submit-application', methods=['POST']) def submit_application(session_id): """Submit final application""" # Check rate limit allowed, wait_seconds = check_rate_limit() if not allowed: flash(f'Bitte warten Sie noch {wait_seconds} Sekunden vor der nächsten Eingabe.', 'error') return redirect(url_for('page4_upload', session_id=session_id)), 429 app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') return redirect(url_for('page1_email')) # Mark as submitted app_data['current_page'] = 5 app_data['submitted_at'] = datetime.now().isoformat() app_data['updated_at'] = datetime.now().isoformat() app_data['status'] = 'submitted' save_application_data(session_id, app_data) # Send HR notification send_hr_notification(session_id, app_data) return redirect(url_for('page5_confirmation', session_id=session_id)) @app.route('/apply//confirmation') def page5_confirmation(session_id): """Page 5: Confirmation page""" # Validate page access if not can_access_page(session_id, 5): app_data = load_application_data(session_id) if app_data: current_page = app_data.get('current_page', 2) if current_page == 2: return redirect(url_for('page2_personal', session_id=session_id)) elif current_page == 3: return redirect(url_for('page3_motivation', session_id=session_id)) elif current_page == 4: return redirect(url_for('page4_upload', session_id=session_id)) flash('Sie müssen zuerst die vorherigen Schritte abschließen.', 'error') return redirect(url_for('page1_email')) app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') return redirect(url_for('page1_email')) return render_template('page5_confirmation.html', session_id=session_id, job_name=app_data['job_name'], current_page=app_data.get('current_page', 5), email=app_data['email']) @app.route('/application//') def view_application(session_id): """View complete application details (for HR)""" app_data = load_application_data(session_id) if not app_data: abort(404) return render_template('application_view.html', session_id=session_id, app_data=app_data) @app.route('/application//download/') def download_file(session_id, filename): """Download an uploaded file""" app_data = load_application_data(session_id) if not app_data: abort(404) # Verify the file is in the uploaded files list uploaded_files = app_data.get('uploaded_files', []) file_found = False for file_info in uploaded_files: if file_info.get('stored_name') == filename: file_found = True break if not file_found: abort(404) # Serve the file attachments_path = get_attachments_path(session_id) file_path = os.path.join(attachments_path, filename) if not os.path.exists(file_path): abort(404) return send_from_directory(attachments_path, filename, as_attachment=True) @app.route('/resume/') def resume_application(session_id): """Resume application from email link""" app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') return redirect(url_for('page1_email')) # Store session ID in Flask session session['application_id'] = session_id # Redirect to current page current_page = app_data.get('current_page', 2) if current_page == 2: return redirect(url_for('page2_personal', session_id=session_id)) elif current_page == 3: return redirect(url_for('page3_motivation', session_id=session_id)) elif current_page == 4: return redirect(url_for('page4_upload', session_id=session_id)) elif current_page == 5: return redirect(url_for('page5_confirmation', session_id=session_id)) else: return redirect(url_for('page2_personal', session_id=session_id))