import os import uuid import yaml import re from datetime import datetime from pathlib import Path from flask import Flask, render_template, request, redirect, url_for, flash, session from flask_mail import Mail, Message from werkzeug.utils import secure_filename from config import Config app = Flask(__name__) app.config.from_object(Config) mail = Mail(app) # Ensure applications folder exists Path(app.config['APPLICATIONS_FOLDER']).mkdir(exist_ok=True) def get_application_path(session_id): """Get the path to an application folder""" return os.path.join(app.config['APPLICATIONS_FOLDER'], session_id) def get_data_file_path(session_id): """Get the path to the application data YAML file""" return os.path.join(get_application_path(session_id), 'data.yaml') def get_attachments_path(session_id): """Get the path to the attachments folder""" return os.path.join(get_application_path(session_id), 'attachments') def load_application_data(session_id): """Load application data from YAML file""" data_file = get_data_file_path(session_id) if os.path.exists(data_file): with open(data_file, 'r', encoding='utf-8') as f: return yaml.safe_load(f) return None def save_application_data(session_id, data): """Save application data to YAML file""" app_path = get_application_path(session_id) Path(app_path).mkdir(parents=True, exist_ok=True) data_file = get_data_file_path(session_id) with open(data_file, 'w', encoding='utf-8') as f: yaml.dump(data, f, allow_unicode=True, default_flow_style=False) def send_resume_email(email, session_id, job_name): """Send email with resume link""" resume_link = url_for('resume_application', session_id=session_id, _external=True) subject = f"Ihre Bewerbung bei {app.config['COMPANY_NAME']} - Link zum Fortsetzen" body = f"""Guten Tag, vielen Dank für Ihr Interesse an der Position "{job_name}" bei {app.config['COMPANY_NAME']}. Sie können Ihre Bewerbung jederzeit über den folgenden Link fortsetzen: {resume_link} Dieser Link bleibt gültig und Sie können Ihre Bewerbung jederzeit bearbeiten. Mit freundlichen Grüßen {app.config['COMPANY_NAME']} """ try: msg = Message(subject=subject, recipients=[email], body=body) mail.send(msg) return True except Exception as e: app.logger.error(f"Failed to send email: {e}") return False def validate_email(email): """Validate email format""" pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' return re.match(pattern, email) is not None def validate_phone(phone): """Validate international phone format""" # Accepts formats like: +41 79 123 45 67, +41791234567, etc. # Remove spaces and check if it matches the pattern pattern = r'^\+\d{1,3}[\s\d]{1,20}$' if not re.match(pattern, phone): return False # Ensure there are at least some digits after the country code digits_only = re.sub(r'\s', '', phone) return len(digits_only) >= 5 # At least +XX XXX format def validate_year(year): """Validate birth year""" try: year_int = int(year) return (app.config['MIN_BIRTH_YEAR'] <= year_int <= app.config['MAX_BIRTH_YEAR'] and len(str(year)) == 4) except ValueError: return False def validate_zip(zip_code): """Validate ZIP code""" try: zip_int = int(zip_code) return len(str(zip_code)) <= app.config['MAX_ZIP_DIGITS'] except ValueError: return False def allowed_file(filename): """Check if file extension is allowed""" return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS'] @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""" 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""" 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'], data=app_data.get('personal_info', {})) @app.route('/apply//submit-personal', methods=['POST']) def submit_personal(session_id): """Process personal information submission""" 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""" 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'], data=app_data.get('motivation_answers', {})) @app.route('/apply//submit-motivation', methods=['POST']) def submit_motivation(session_id): """Process motivation questions submission""" 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""" 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'], 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""" 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""" 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) return redirect(url_for('page5_confirmation', session_id=session_id)) @app.route('/apply//confirmation') def page5_confirmation(session_id): """Page 5: Confirmation page""" 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'], email=app_data['email']) @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)) if __name__ == '__main__': app.run(debug=True)