refactor: split monolithic app.py into modular Flask application with factory pattern
- Created app/ package with 6 modules: - __init__.py: Application factory (create_app function) - routes.py: All route handlers (473 lines) - models.py: Data persistence layer (41 lines) - validators.py: Input validation functions (48 lines) - email_service.py: Email sending functions (85 lines) - utils.py: Utility functions (30 lines) - Implemented Flask application factory pattern - Created run.py as minimal entry point - Updated all test imports to use new module structure - Fixed template/static folder paths for package structure - All 102 tests passing - Improved maintainability with clear separation of concerns - Follows Flask best practices for scalable applications
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Flask job application system - Application factory module.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from flask import Flask, redirect, url_for, flash
|
||||
from flask_mail import Mail
|
||||
from flask_wtf.csrf import CSRFProtect, CSRFError
|
||||
from config import Config
|
||||
|
||||
# Initialize extensions (not bound to app yet)
|
||||
mail = Mail()
|
||||
csrf = CSRFProtect()
|
||||
|
||||
|
||||
def create_app(config_class=Config):
|
||||
"""
|
||||
Application factory function.
|
||||
Creates and configures the Flask application instance.
|
||||
|
||||
Args:
|
||||
config_class: Configuration class to use (defaults to Config)
|
||||
|
||||
Returns:
|
||||
Configured Flask application instance
|
||||
"""
|
||||
# Create Flask app instance
|
||||
# Specify template and static folders relative to project root (parent of app/)
|
||||
app = Flask(__name__,
|
||||
template_folder='../templates',
|
||||
static_folder='../static')
|
||||
|
||||
# Load configuration
|
||||
app.config.from_object(config_class)
|
||||
|
||||
# Initialize extensions with app
|
||||
mail.init_app(app)
|
||||
csrf.init_app(app)
|
||||
|
||||
# Ensure applications folder exists
|
||||
Path(app.config['APPLICATIONS_FOLDER']).mkdir(exist_ok=True)
|
||||
|
||||
# Register error handlers
|
||||
@app.errorhandler(CSRFError)
|
||||
def handle_csrf_error(e):
|
||||
"""Handle CSRF validation errors."""
|
||||
flash('Sicherheitsfehler: Die Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.', 'error')
|
||||
return redirect(url_for('page1_email')), 400
|
||||
|
||||
@app.errorhandler(429)
|
||||
def handle_rate_limit_error(e):
|
||||
"""Handle rate limit errors."""
|
||||
return str(e), 429
|
||||
|
||||
# Register routes
|
||||
from app.routes import register_routes
|
||||
register_routes(app, mail)
|
||||
|
||||
return app
|
||||
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Email service functions for the Flask job application system.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from flask import url_for, current_app
|
||||
from flask_mail import Message
|
||||
from app import mail
|
||||
|
||||
|
||||
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 {current_app.config['COMPANY_NAME']} - Link zum Fortsetzen"
|
||||
body = f"""Guten Tag,
|
||||
|
||||
vielen Dank für Ihr Interesse an der Position "{job_name}" bei {current_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
|
||||
{current_app.config['COMPANY_NAME']}
|
||||
"""
|
||||
|
||||
try:
|
||||
msg = Message(subject=subject, recipients=[email], body=body)
|
||||
mail.send(msg)
|
||||
return True
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Failed to send email: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def send_hr_notification(session_id, app_data):
|
||||
"""Send HR notification email when application is submitted"""
|
||||
if not current_app.config.get('HR_EMAIL'):
|
||||
current_app.logger.warning("HR_EMAIL not configured, skipping HR notification")
|
||||
return False
|
||||
|
||||
# Build application URL
|
||||
application_url = f"{current_app.config['APPLICATION_URL_BASE']}/application/{session_id}/"
|
||||
|
||||
# Get personal info
|
||||
personal_info = app_data.get('personal_info', {})
|
||||
firstname = personal_info.get('firstname', 'N/A')
|
||||
name = personal_info.get('name', 'N/A')
|
||||
email = app_data.get('email', 'N/A')
|
||||
job_name = app_data.get('job_name', 'N/A')
|
||||
file_count = len(app_data.get('uploaded_files', []))
|
||||
|
||||
# Format timestamp
|
||||
submitted_at = app_data.get('submitted_at', datetime.now().isoformat())
|
||||
try:
|
||||
timestamp = datetime.fromisoformat(submitted_at).strftime('%d.%m.%Y %H:%M')
|
||||
except (ValueError, TypeError):
|
||||
timestamp = submitted_at
|
||||
|
||||
subject = f"Neue Bewerbung eingegangen: {job_name}"
|
||||
body = f"""Sehr geehrtes HR-Team,
|
||||
|
||||
es ist eine neue Bewerbung eingegangen:
|
||||
|
||||
Position: {job_name}
|
||||
Name: {firstname} {name}
|
||||
E-Mail: {email}
|
||||
Eingereicht am: {timestamp}
|
||||
Anzahl der hochgeladenen Dokumente: {file_count}
|
||||
|
||||
Sie können die vollständige Bewerbung hier einsehen:
|
||||
{application_url}
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
Ihr Bewerbungssystem
|
||||
"""
|
||||
|
||||
try:
|
||||
msg = Message(subject=subject, recipients=[current_app.config['HR_EMAIL']], body=body)
|
||||
mail.send(msg)
|
||||
return True
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Failed to send HR notification email: {e}")
|
||||
return False
|
||||
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Data models and storage functions for the Flask job application system.
|
||||
"""
|
||||
import os
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from flask import current_app
|
||||
|
||||
|
||||
def get_application_path(session_id):
|
||||
"""Get the path to an application folder"""
|
||||
return os.path.join(current_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)
|
||||
+473
@@ -0,0 +1,473 @@
|
||||
"""
|
||||
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"""
|
||||
|
||||
@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/<session_id>/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/<session_id>/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/<session_id>/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/<session_id>/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/<session_id>/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/<session_id>/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/<session_id>/remove-file/<int:file_index>', 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/<session_id>/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/<session_id>/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('/application/<session_id>/')
|
||||
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/<session_id>/download/<filename>')
|
||||
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/<session_id>')
|
||||
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))
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Utility functions for the Flask job application system.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from flask import session, current_app
|
||||
|
||||
|
||||
def check_rate_limit():
|
||||
"""
|
||||
Check if user is submitting forms too quickly.
|
||||
Returns (allowed: bool, wait_seconds: int)
|
||||
"""
|
||||
now = datetime.now()
|
||||
last_submit_str = session.get('last_submission_time')
|
||||
|
||||
if last_submit_str:
|
||||
try:
|
||||
last_submit = datetime.fromisoformat(last_submit_str)
|
||||
elapsed = (now - last_submit).total_seconds()
|
||||
|
||||
if elapsed < current_app.config['RATE_LIMIT_SECONDS']:
|
||||
wait_seconds = int(current_app.config['RATE_LIMIT_SECONDS'] - elapsed) + 1
|
||||
return False, wait_seconds
|
||||
except (ValueError, TypeError):
|
||||
# Invalid timestamp, allow submission
|
||||
pass
|
||||
|
||||
# Update last submission time
|
||||
session['last_submission_time'] = now.isoformat()
|
||||
return True, 0
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Validation functions for the Flask job application system.
|
||||
"""
|
||||
import re
|
||||
from flask import current_app
|
||||
|
||||
|
||||
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 (current_app.config['MIN_BIRTH_YEAR'] <= year_int <= current_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)) <= current_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 current_app.config['ALLOWED_EXTENSIONS']
|
||||
Reference in New Issue
Block a user