Files

538 lines
21 KiB
Python
Raw Permalink Normal View History

"""
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/<session_id>/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/<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"""
# 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/<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"""
# 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/<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"""
# 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/<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))