Initial commit
This commit is contained in:
@@ -0,0 +1,497 @@
|
||||
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/<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"""
|
||||
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"""
|
||||
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"""
|
||||
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"""
|
||||
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/<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('/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))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
Reference in New Issue
Block a user