HR notification added
This commit is contained in:
@@ -11,3 +11,7 @@ MAIL_DEFAULT_SENDER=noreply@example.com
|
|||||||
|
|
||||||
# Company Settings
|
# Company Settings
|
||||||
COMPANY_NAME=Ihr Firmenname
|
COMPANY_NAME=Ihr Firmenname
|
||||||
|
|
||||||
|
# HR Notification Settings
|
||||||
|
HR_EMAIL=hr@example.com
|
||||||
|
APPLICATION_URL_BASE=http://localhost:5000
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import yaml
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from flask import Flask, render_template, request, redirect, url_for, flash, session
|
from flask import Flask, render_template, request, redirect, url_for, flash, session, send_from_directory, abort
|
||||||
from flask_mail import Mail, Message
|
from flask_mail import Mail, Message
|
||||||
from flask_wtf.csrf import CSRFProtect, CSRFError
|
from flask_wtf.csrf import CSRFProtect, CSRFError
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
@@ -120,6 +120,57 @@ Mit freundlichen Grüßen
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def send_hr_notification(session_id, app_data):
|
||||||
|
"""Send HR notification email when application is submitted"""
|
||||||
|
if not app.config.get('HR_EMAIL'):
|
||||||
|
app.logger.warning("HR_EMAIL not configured, skipping HR notification")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Build application URL
|
||||||
|
application_url = f"{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=[app.config['HR_EMAIL']], body=body)
|
||||||
|
mail.send(msg)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Failed to send HR notification email: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def validate_email(email):
|
def validate_email(email):
|
||||||
"""Validate email format"""
|
"""Validate email format"""
|
||||||
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||||
@@ -522,6 +573,9 @@ def submit_application(session_id):
|
|||||||
|
|
||||||
save_application_data(session_id, app_data)
|
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))
|
return redirect(url_for('page5_confirmation', session_id=session_id))
|
||||||
|
|
||||||
|
|
||||||
@@ -539,6 +593,47 @@ def page5_confirmation(session_id):
|
|||||||
email=app_data['email'])
|
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>')
|
@app.route('/resume/<session_id>')
|
||||||
def resume_application(session_id):
|
def resume_application(session_id):
|
||||||
"""Resume application from email link"""
|
"""Resume application from email link"""
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ class Config:
|
|||||||
# Company name for emails
|
# Company name for emails
|
||||||
COMPANY_NAME = os.getenv('COMPANY_NAME', 'Unser Unternehmen')
|
COMPANY_NAME = os.getenv('COMPANY_NAME', 'Unser Unternehmen')
|
||||||
|
|
||||||
|
# HR notification settings
|
||||||
|
HR_EMAIL = os.getenv('HR_EMAIL')
|
||||||
|
APPLICATION_URL_BASE = os.getenv('APPLICATION_URL_BASE', 'http://localhost:5000')
|
||||||
|
|
||||||
# Application settings
|
# Application settings
|
||||||
APPLICATIONS_FOLDER = './applications'
|
APPLICATIONS_FOLDER = './applications'
|
||||||
MAX_FILE_SIZE = 4 * 1024 * 1024 # 4 MB in bytes
|
MAX_FILE_SIZE = 4 * 1024 * 1024 # 4 MB in bytes
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# Prompt 004: HR Email Notifications and Application Review Page
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Implement HR email notifications when an application is submitted and create a review page where HR can view complete application details and download attachments.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### 1. HR Email Notification
|
||||||
|
- When user submits application on page 4 (submit_application route), send email to HR
|
||||||
|
- HR email address configurable via environment variable `HR_EMAIL`
|
||||||
|
- Email should be in German
|
||||||
|
- Email should include:
|
||||||
|
- Job position name
|
||||||
|
- Applicant's name and email
|
||||||
|
- Submission timestamp
|
||||||
|
- Direct link to application review page: `http://yourdomain.com/application/<session_id>/`
|
||||||
|
- Brief summary (e.g., number of documents uploaded)
|
||||||
|
|
||||||
|
### 2. Application Review Page Route
|
||||||
|
- Create new route: `/application/<session_id>/`
|
||||||
|
- No additional authentication needed (session ID provides security through obscurity)
|
||||||
|
- Display all application information in a clean, readable format
|
||||||
|
- Show all uploaded files with download links
|
||||||
|
|
||||||
|
### 3. Application Review Template
|
||||||
|
- Create `templates/application_view.html`
|
||||||
|
- Display in German:
|
||||||
|
- Job position
|
||||||
|
- Personal information (all fields from page 2)
|
||||||
|
- Motivation answers (all fields from page 3)
|
||||||
|
- List of uploaded documents with:
|
||||||
|
- Original filename
|
||||||
|
- File size
|
||||||
|
- Download link for each file
|
||||||
|
- Submission timestamp
|
||||||
|
- Email address used for application
|
||||||
|
|
||||||
|
### 4. File Download Route
|
||||||
|
- Create route: `/application/<session_id>/download/<filename>`
|
||||||
|
- Serve the actual uploaded file
|
||||||
|
- Use `send_from_directory` with proper security checks
|
||||||
|
- Verify filename exists in application's uploaded_files list
|
||||||
|
|
||||||
|
### 5. Configuration Updates
|
||||||
|
- Add `HR_EMAIL` to `config.py` with environment variable
|
||||||
|
- Add `HR_EMAIL` to `.env.example`
|
||||||
|
- Add `APPLICATION_URL_BASE` to config for constructing full URLs in emails
|
||||||
|
|
||||||
|
### 6. Error Handling
|
||||||
|
- Handle missing application gracefully (404 error)
|
||||||
|
- Handle missing files gracefully (404 error)
|
||||||
|
- Validate session_id format
|
||||||
|
|
||||||
|
### 7. Testing Requirements
|
||||||
|
- Test HR email is sent on application submission
|
||||||
|
- Test email contains correct information and link
|
||||||
|
- Test application review page displays all data correctly
|
||||||
|
- Test file download works
|
||||||
|
- Test error cases (invalid session_id, missing files)
|
||||||
|
- Test that HR email is not sent if email sending fails
|
||||||
|
|
||||||
|
## Technical Implementation Notes
|
||||||
|
|
||||||
|
### Email Template (German)
|
||||||
|
```
|
||||||
|
Betreff: Neue Bewerbung eingegangen: {job_name}
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Storage Structure
|
||||||
|
Files are already stored in: `applications/<session_id>/files/<stored_filename>`
|
||||||
|
The stored_filename is in the uploaded_files list in the YAML data.
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
- Session IDs are UUIDs (sufficiently random and unguessable)
|
||||||
|
- Only allow downloading files that are listed in the application's uploaded_files
|
||||||
|
- Use `secure_filename()` when handling file operations
|
||||||
|
- No directory traversal attacks (verify file is in correct session folder)
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
|
||||||
|
1. **config.py**
|
||||||
|
- Add HR_EMAIL configuration
|
||||||
|
- Add APPLICATION_URL_BASE configuration
|
||||||
|
|
||||||
|
2. **.env.example**
|
||||||
|
- Add HR_EMAIL example
|
||||||
|
- Add APPLICATION_URL_BASE example
|
||||||
|
|
||||||
|
3. **app.py**
|
||||||
|
- Modify `submit_application` route to send HR email
|
||||||
|
- Add new route: `view_application(session_id)`
|
||||||
|
- Add new route: `download_file(session_id, filename)`
|
||||||
|
- Add email helper function for HR notification
|
||||||
|
|
||||||
|
4. **templates/application_view.html** (NEW)
|
||||||
|
- Create clean layout for HR to review application
|
||||||
|
- Display all fields in German
|
||||||
|
- Provide download links for attachments
|
||||||
|
|
||||||
|
## Files to Create (Tests)
|
||||||
|
|
||||||
|
1. **tests/test_hr_notifications.py** (NEW)
|
||||||
|
- Test HR email is sent on submission
|
||||||
|
- Test email content is correct
|
||||||
|
- Test email contains proper link
|
||||||
|
|
||||||
|
2. **tests/test_application_view.py** (NEW)
|
||||||
|
- Test application view page renders correctly
|
||||||
|
- Test all data is displayed
|
||||||
|
- Test file download works
|
||||||
|
- Test 404 for invalid session_id
|
||||||
|
- Test 404 for invalid filename
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
- HR receives email when application is submitted
|
||||||
|
- Email contains all required information in German
|
||||||
|
- Application review page displays all application data
|
||||||
|
- HR can download all uploaded files
|
||||||
|
- All existing tests still pass
|
||||||
|
- New tests achieve high coverage for new functionality
|
||||||
|
- No security vulnerabilities introduced
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Bewerbung ansehen{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}Bewerbungsübersicht{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="application-view">
|
||||||
|
<div class="section">
|
||||||
|
<h2>Stelleninformation</h2>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Position:</span>
|
||||||
|
<span class="value">{{ app_data.job_name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">E-Mail:</span>
|
||||||
|
<span class="value">{{ app_data.email }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Status:</span>
|
||||||
|
<span class="value">{{ app_data.status or 'In Bearbeitung' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Eingereicht am:</span>
|
||||||
|
<span class="value">
|
||||||
|
{% if app_data.submitted_at %}
|
||||||
|
{{ app_data.submitted_at[:10] }} {{ app_data.submitted_at[11:16] }}
|
||||||
|
{% else %}
|
||||||
|
Noch nicht eingereicht
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Persönliche Informationen</h2>
|
||||||
|
{% if app_data.personal_info %}
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Name:</span>
|
||||||
|
<span class="value">{{ app_data.personal_info.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Vorname:</span>
|
||||||
|
<span class="value">{{ app_data.personal_info.firstname }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Adresse:</span>
|
||||||
|
<span class="value">{{ app_data.personal_info.address }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">PLZ:</span>
|
||||||
|
<span class="value">{{ app_data.personal_info.zip_code }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Stadt:</span>
|
||||||
|
<span class="value">{{ app_data.personal_info.city }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Telefon:</span>
|
||||||
|
<span class="value">{{ app_data.personal_info.phone }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Geburtsjahr:</span>
|
||||||
|
<span class="value">{{ app_data.personal_info.birth_year }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Zivilstand:</span>
|
||||||
|
<span class="value">{{ app_data.personal_info.civil_status or 'Nicht angegeben' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="info-message">Noch nicht ausgefüllt</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Motivation und Qualifikationen</h2>
|
||||||
|
{% if app_data.motivation_answers %}
|
||||||
|
<div class="text-responses">
|
||||||
|
<div class="response-item">
|
||||||
|
<h3>Aktuelle berufliche Situation</h3>
|
||||||
|
<p class="response-text">{{ app_data.motivation_answers.current_job or 'Nicht angegeben' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="response-item">
|
||||||
|
<h3>Motivation</h3>
|
||||||
|
<p class="response-text">{{ app_data.motivation_answers.motivation or 'Nicht angegeben' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="response-item">
|
||||||
|
<h3>Qualifikationen</h3>
|
||||||
|
<p class="response-text">{{ app_data.motivation_answers.qualifications or 'Nicht angegeben' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="response-item">
|
||||||
|
<h3>Gehaltsvorstellungen</h3>
|
||||||
|
<p class="response-text">{{ app_data.motivation_answers.salary or 'Nicht angegeben' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="info-message">Noch nicht ausgefüllt</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Hochgeladene Dokumente</h2>
|
||||||
|
{% if app_data.uploaded_files %}
|
||||||
|
<div class="documents-list">
|
||||||
|
<table class="documents-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Dateiname</th>
|
||||||
|
<th>Größe</th>
|
||||||
|
<th>Hochgeladen am</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for file in app_data.uploaded_files %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ file.original_name }}</td>
|
||||||
|
<td>{{ "%.2f"|format(file.size / 1024 / 1024) }} MB</td>
|
||||||
|
<td>{{ file.uploaded_at[:10] }} {{ file.uploaded_at[11:16] }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('download_file', session_id=session_id, filename=file.stored_name) }}"
|
||||||
|
class="btn btn-small btn-secondary">Herunterladen</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="info-message">Keine Dokumente hochgeladen</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Zeitstempel</h2>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Erstellt am:</span>
|
||||||
|
<span class="value">{{ app_data.created_at[:10] }} {{ app_data.created_at[11:16] }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Zuletzt aktualisiert:</span>
|
||||||
|
<span class="value">{{ app_data.updated_at[:10] }} {{ app_data.updated_at[11:16] }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.application-view {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #007bff;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .value {
|
||||||
|
color: #333;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-responses {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-item {
|
||||||
|
background: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-text {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documents-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documents-table th,
|
||||||
|
.documents-table td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documents-table th {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documents-table tr:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-message {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
"""
|
||||||
|
Tests for application view and file download functionality.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class TestApplicationView:
|
||||||
|
"""Test application view page for HR."""
|
||||||
|
|
||||||
|
def test_view_application_page_renders(self, client, create_test_application):
|
||||||
|
"""Test that the application view page renders correctly."""
|
||||||
|
session_id = 'test-view-123'
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='test@example.com',
|
||||||
|
job_name='Software Engineer',
|
||||||
|
personal_info={
|
||||||
|
'name': 'Müller',
|
||||||
|
'firstname': 'Anna',
|
||||||
|
'address': 'Hauptstrasse 123',
|
||||||
|
'zip_code': '8001',
|
||||||
|
'city': 'Zürich',
|
||||||
|
'phone': '+41 79 123 45 67',
|
||||||
|
'birth_year': '1990',
|
||||||
|
'civil_status': 'ledig'
|
||||||
|
},
|
||||||
|
motivation_answers={
|
||||||
|
'current_job': 'Software Developer',
|
||||||
|
'motivation': 'Great opportunity',
|
||||||
|
'qualifications': 'Python, Flask',
|
||||||
|
'salary': '100000 CHF'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(f'/application/{session_id}/')
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'Software Engineer' in response.data
|
||||||
|
assert b'test@example.com' in response.data
|
||||||
|
assert b'Anna' in response.data
|
||||||
|
assert b'Great opportunity' in response.data
|
||||||
|
|
||||||
|
def test_view_application_404_for_invalid_session(self, client):
|
||||||
|
"""Test that viewing a non-existent application returns 404."""
|
||||||
|
response = client.get('/application/invalid-session-id/')
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_view_application_displays_all_personal_info(self, client, create_test_application):
|
||||||
|
"""Test that all personal information fields are displayed."""
|
||||||
|
session_id = 'test-personal-display'
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Data Analyst',
|
||||||
|
personal_info={
|
||||||
|
'name': 'Schmidt',
|
||||||
|
'firstname': 'Max',
|
||||||
|
'address': 'Bahnhofstrasse 1',
|
||||||
|
'zip_code': '8000',
|
||||||
|
'city': 'Zürich',
|
||||||
|
'phone': '+41 79 999 88 77',
|
||||||
|
'birth_year': '1985',
|
||||||
|
'civil_status': 'verheiratet'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(f'/application/{session_id}/')
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'Schmidt' in response.data
|
||||||
|
assert b'Max' in response.data
|
||||||
|
assert b'Bahnhofstrasse 1' in response.data
|
||||||
|
assert b'8000' in response.data
|
||||||
|
assert b'verheiratet' in response.data
|
||||||
|
|
||||||
|
def test_view_application_displays_motivation_answers(self, client, create_test_application):
|
||||||
|
"""Test that motivation answers are properly displayed."""
|
||||||
|
session_id = 'test-motivation-display'
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Project Manager',
|
||||||
|
motivation_answers={
|
||||||
|
'current_job': 'Senior Developer at ABC Corp',
|
||||||
|
'motivation': 'I want to work on challenging projects',
|
||||||
|
'qualifications': '10 years of experience in software development',
|
||||||
|
'salary': '120000 CHF per year'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(f'/application/{session_id}/')
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'Senior Developer at ABC Corp' in response.data
|
||||||
|
assert b'challenging projects' in response.data
|
||||||
|
assert b'10 years of experience' in response.data
|
||||||
|
assert b'120000 CHF' in response.data
|
||||||
|
|
||||||
|
def test_view_application_with_uploaded_files(
|
||||||
|
self, client, create_test_application, temp_applications_dir
|
||||||
|
):
|
||||||
|
"""Test that uploaded files are displayed in the application view."""
|
||||||
|
session_id = 'test-files-display'
|
||||||
|
|
||||||
|
# Create the application with uploaded files
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='UI Designer',
|
||||||
|
uploaded_files=[
|
||||||
|
{
|
||||||
|
'original_name': 'cv.pdf',
|
||||||
|
'stored_name': '20250101_120000_cv.pdf',
|
||||||
|
'size': 102400,
|
||||||
|
'uploaded_at': '2025-01-01T12:00:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'original_name': 'portfolio.pdf',
|
||||||
|
'stored_name': '20250101_120100_portfolio.pdf',
|
||||||
|
'size': 204800,
|
||||||
|
'uploaded_at': '2025-01-01T12:01:00'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(f'/application/{session_id}/')
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'cv.pdf' in response.data
|
||||||
|
assert b'portfolio.pdf' in response.data
|
||||||
|
# Check that file sizes are displayed (in MB)
|
||||||
|
assert b'0.10 MB' in response.data
|
||||||
|
assert b'0.20 MB' in response.data
|
||||||
|
|
||||||
|
def test_view_application_without_uploaded_files(self, client, create_test_application):
|
||||||
|
"""Test that application view works when no files are uploaded."""
|
||||||
|
session_id = 'test-no-files'
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Sales Representative',
|
||||||
|
uploaded_files=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(f'/application/{session_id}/')
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'Keine Dokumente hochgeladen' in response.data
|
||||||
|
|
||||||
|
def test_view_application_shows_timestamps(self, client, create_test_application):
|
||||||
|
"""Test that creation and update timestamps are displayed."""
|
||||||
|
session_id = 'test-timestamps'
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Marketing Manager'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(f'/application/{session_id}/')
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
# Check for timestamp labels in German
|
||||||
|
assert b'Erstellt am:' in response.data
|
||||||
|
assert b'Zuletzt aktualisiert:' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
class TestFileDownload:
|
||||||
|
"""Test file download functionality."""
|
||||||
|
|
||||||
|
def test_download_file_success(self, client, create_test_application, temp_applications_dir):
|
||||||
|
"""Test that files can be successfully downloaded."""
|
||||||
|
session_id = 'test-download-success'
|
||||||
|
|
||||||
|
# Create application directory and attachment
|
||||||
|
from app import get_attachments_path
|
||||||
|
attachments_path = get_attachments_path(session_id)
|
||||||
|
Path(attachments_path).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Create a test file
|
||||||
|
test_filename = '20250101_120000_test.pdf'
|
||||||
|
test_file_path = os.path.join(attachments_path, test_filename)
|
||||||
|
test_content = b'Test PDF content'
|
||||||
|
|
||||||
|
with open(test_file_path, 'wb') as f:
|
||||||
|
f.write(test_content)
|
||||||
|
|
||||||
|
# Create application with the uploaded file
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Test Job',
|
||||||
|
uploaded_files=[
|
||||||
|
{
|
||||||
|
'original_name': 'test.pdf',
|
||||||
|
'stored_name': test_filename,
|
||||||
|
'size': len(test_content),
|
||||||
|
'uploaded_at': '2025-01-01T12:00:00'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Download the file
|
||||||
|
response = client.get(f'/application/{session_id}/download/{test_filename}')
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == test_content
|
||||||
|
assert 'attachment' in response.headers.get('Content-Disposition', '')
|
||||||
|
|
||||||
|
def test_download_file_not_in_uploaded_list(
|
||||||
|
self, client, create_test_application, temp_applications_dir
|
||||||
|
):
|
||||||
|
"""Test that downloading a file not in the uploaded list returns 404."""
|
||||||
|
session_id = 'test-download-unauthorized'
|
||||||
|
|
||||||
|
# Create application directory and attachment
|
||||||
|
from app import get_attachments_path
|
||||||
|
attachments_path = get_attachments_path(session_id)
|
||||||
|
Path(attachments_path).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Create a file that exists physically but is NOT in the uploaded_files list
|
||||||
|
unauthorized_filename = '20250101_120000_unauthorized.pdf'
|
||||||
|
unauthorized_path = os.path.join(attachments_path, unauthorized_filename)
|
||||||
|
|
||||||
|
with open(unauthorized_path, 'wb') as f:
|
||||||
|
f.write(b'Unauthorized content')
|
||||||
|
|
||||||
|
# Create application with NO uploaded files
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Test Job',
|
||||||
|
uploaded_files=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try to download the unauthorized file
|
||||||
|
response = client.get(f'/application/{session_id}/download/{unauthorized_filename}')
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_download_file_nonexistent_file(self, client, create_test_application):
|
||||||
|
"""Test that downloading a non-existent file returns 404."""
|
||||||
|
session_id = 'test-download-nonexistent'
|
||||||
|
|
||||||
|
# Create application with a file in the list, but the file doesn't exist physically
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Test Job',
|
||||||
|
uploaded_files=[
|
||||||
|
{
|
||||||
|
'original_name': 'missing.pdf',
|
||||||
|
'stored_name': 'nonexistent_file.pdf',
|
||||||
|
'size': 1024,
|
||||||
|
'uploaded_at': '2025-01-01T12:00:00'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try to download the missing file
|
||||||
|
response = client.get(f'/application/{session_id}/download/nonexistent_file.pdf')
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_download_file_invalid_session(self, client):
|
||||||
|
"""Test that downloading with an invalid session ID returns 404."""
|
||||||
|
response = client.get('/application/invalid-session/download/somefile.pdf')
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_download_multiple_files(self, client, create_test_application, temp_applications_dir):
|
||||||
|
"""Test downloading multiple files from the same application."""
|
||||||
|
session_id = 'test-download-multiple'
|
||||||
|
|
||||||
|
# Create application directory
|
||||||
|
from app import get_attachments_path
|
||||||
|
attachments_path = get_attachments_path(session_id)
|
||||||
|
Path(attachments_path).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Create multiple test files
|
||||||
|
files = [
|
||||||
|
('20250101_120000_cv.pdf', b'CV content'),
|
||||||
|
('20250101_120100_cover_letter.pdf', b'Cover letter content'),
|
||||||
|
('20250101_120200_certificate.pdf', b'Certificate content')
|
||||||
|
]
|
||||||
|
|
||||||
|
uploaded_files = []
|
||||||
|
for filename, content in files:
|
||||||
|
file_path = os.path.join(attachments_path, filename)
|
||||||
|
with open(file_path, 'wb') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
uploaded_files.append({
|
||||||
|
'original_name': filename.split('_', 2)[2],
|
||||||
|
'stored_name': filename,
|
||||||
|
'size': len(content),
|
||||||
|
'uploaded_at': '2025-01-01T12:00:00'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create application with multiple files
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Test Job',
|
||||||
|
uploaded_files=uploaded_files
|
||||||
|
)
|
||||||
|
|
||||||
|
# Download each file and verify
|
||||||
|
for filename, expected_content in files:
|
||||||
|
response = client.get(f'/application/{session_id}/download/{filename}')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == expected_content
|
||||||
|
|
||||||
|
def test_download_file_with_special_characters(
|
||||||
|
self, client, create_test_application, temp_applications_dir
|
||||||
|
):
|
||||||
|
"""Test downloading files with special characters in the name."""
|
||||||
|
session_id = 'test-download-special-chars'
|
||||||
|
|
||||||
|
# Create application directory
|
||||||
|
from app import get_attachments_path
|
||||||
|
attachments_path = get_attachments_path(session_id)
|
||||||
|
Path(attachments_path).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Create a file with a secure filename (as it would be stored)
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
original_name = 'Lebenslauf Müller.pdf'
|
||||||
|
stored_name = f'20250101_120000_{secure_filename(original_name)}'
|
||||||
|
file_path = os.path.join(attachments_path, stored_name)
|
||||||
|
content = b'CV with special chars'
|
||||||
|
|
||||||
|
with open(file_path, 'wb') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
# Create application
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Test Job',
|
||||||
|
uploaded_files=[
|
||||||
|
{
|
||||||
|
'original_name': original_name,
|
||||||
|
'stored_name': stored_name,
|
||||||
|
'size': len(content),
|
||||||
|
'uploaded_at': '2025-01-01T12:00:00'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Download the file
|
||||||
|
response = client.get(f'/application/{session_id}/download/{stored_name}')
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == content
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
"""
|
||||||
|
Tests for HR email notifications functionality.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from app import send_hr_notification
|
||||||
|
|
||||||
|
|
||||||
|
class TestHRNotifications:
|
||||||
|
"""Test HR notification email functionality."""
|
||||||
|
|
||||||
|
def test_hr_notification_sent_on_submission(self, client, create_test_application, mock_mail):
|
||||||
|
"""Test that HR receives email when application is submitted."""
|
||||||
|
# Set HR_EMAIL in config
|
||||||
|
from app import app
|
||||||
|
app.config['HR_EMAIL'] = 'hr@example.com'
|
||||||
|
app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000'
|
||||||
|
|
||||||
|
# Create a test application with all required data
|
||||||
|
session_id = 'test-session-123'
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Software Developer',
|
||||||
|
current_page=4,
|
||||||
|
personal_info={
|
||||||
|
'name': 'Müller',
|
||||||
|
'firstname': 'Anna',
|
||||||
|
'address': 'Hauptstrasse 123',
|
||||||
|
'zip_code': '8001',
|
||||||
|
'city': 'Zürich',
|
||||||
|
'phone': '+41 79 123 45 67',
|
||||||
|
'birth_year': '1990',
|
||||||
|
'civil_status': 'ledig'
|
||||||
|
},
|
||||||
|
motivation_answers={
|
||||||
|
'current_job': 'Developer',
|
||||||
|
'motivation': 'Great company',
|
||||||
|
'qualifications': 'Python expert',
|
||||||
|
'salary': '100000'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Submit the application
|
||||||
|
response = client.post(f'/apply/{session_id}/submit-application')
|
||||||
|
|
||||||
|
# Verify redirect to confirmation page
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert f'/apply/{session_id}/confirmation' in response.location
|
||||||
|
|
||||||
|
# Verify HR email was sent
|
||||||
|
assert len(mock_mail.sent_messages) == 1
|
||||||
|
hr_email = mock_mail.sent_messages[0]
|
||||||
|
|
||||||
|
assert 'hr@example.com' in hr_email.recipients
|
||||||
|
assert 'Neue Bewerbung eingegangen: Software Developer' == hr_email.subject
|
||||||
|
assert 'Anna Müller' in hr_email.body
|
||||||
|
assert 'applicant@example.com' in hr_email.body
|
||||||
|
assert f'/application/{session_id}/' in hr_email.body
|
||||||
|
|
||||||
|
def test_hr_notification_not_sent_when_hr_email_not_configured(
|
||||||
|
self, client, create_test_application, mock_mail, caplog
|
||||||
|
):
|
||||||
|
"""Test that HR notification is skipped when HR_EMAIL is not configured."""
|
||||||
|
# Ensure HR_EMAIL is not set
|
||||||
|
from app import app
|
||||||
|
app.config['HR_EMAIL'] = None
|
||||||
|
|
||||||
|
# Create a test application
|
||||||
|
session_id = 'test-session-456'
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Data Analyst',
|
||||||
|
current_page=4,
|
||||||
|
personal_info={
|
||||||
|
'name': 'Schmidt',
|
||||||
|
'firstname': 'Max',
|
||||||
|
'address': 'Bahnhofstrasse 1',
|
||||||
|
'zip_code': '8000',
|
||||||
|
'city': 'Zürich',
|
||||||
|
'phone': '+41 79 999 88 77',
|
||||||
|
'birth_year': '1985',
|
||||||
|
'civil_status': 'verheiratet'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Submit the application
|
||||||
|
response = client.post(f'/apply/{session_id}/submit-application')
|
||||||
|
|
||||||
|
# Verify redirect to confirmation page
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
# Verify no HR email was sent
|
||||||
|
assert len(mock_mail.sent_messages) == 0
|
||||||
|
|
||||||
|
# Verify warning was logged
|
||||||
|
assert 'HR_EMAIL not configured' in caplog.text
|
||||||
|
|
||||||
|
def test_hr_notification_includes_file_count(self, client, create_test_application, mock_mail):
|
||||||
|
"""Test that HR notification includes the count of uploaded files."""
|
||||||
|
from app import app
|
||||||
|
app.config['HR_EMAIL'] = 'hr@example.com'
|
||||||
|
app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000'
|
||||||
|
|
||||||
|
# Create application with uploaded files
|
||||||
|
session_id = 'test-session-789'
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Project Manager',
|
||||||
|
current_page=4,
|
||||||
|
personal_info={
|
||||||
|
'name': 'Weber',
|
||||||
|
'firstname': 'Lisa',
|
||||||
|
'address': 'Seestrasse 45',
|
||||||
|
'zip_code': '8002',
|
||||||
|
'city': 'Zürich',
|
||||||
|
'phone': '+41 79 555 44 33',
|
||||||
|
'birth_year': '1992',
|
||||||
|
'civil_status': 'ledig'
|
||||||
|
},
|
||||||
|
uploaded_files=[
|
||||||
|
{
|
||||||
|
'original_name': 'cv.pdf',
|
||||||
|
'stored_name': '20250101_120000_cv.pdf',
|
||||||
|
'size': 102400,
|
||||||
|
'uploaded_at': '2025-01-01T12:00:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'original_name': 'certificate.pdf',
|
||||||
|
'stored_name': '20250101_120100_certificate.pdf',
|
||||||
|
'size': 204800,
|
||||||
|
'uploaded_at': '2025-01-01T12:01:00'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Submit the application
|
||||||
|
response = client.post(f'/apply/{session_id}/submit-application')
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
# Verify HR email contains file count
|
||||||
|
assert len(mock_mail.sent_messages) == 1
|
||||||
|
hr_email = mock_mail.sent_messages[0]
|
||||||
|
assert 'Anzahl der hochgeladenen Dokumente: 2' in hr_email.body
|
||||||
|
|
||||||
|
def test_hr_notification_with_zero_files(self, client, create_test_application, mock_mail):
|
||||||
|
"""Test that HR notification works even when no files are uploaded."""
|
||||||
|
from app import app
|
||||||
|
app.config['HR_EMAIL'] = 'hr@example.com'
|
||||||
|
app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000'
|
||||||
|
|
||||||
|
# Create application without uploaded files
|
||||||
|
session_id = 'test-session-000'
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Sales Representative',
|
||||||
|
current_page=4,
|
||||||
|
personal_info={
|
||||||
|
'name': 'Fischer',
|
||||||
|
'firstname': 'Tom',
|
||||||
|
'address': 'Hauptplatz 10',
|
||||||
|
'zip_code': '8003',
|
||||||
|
'city': 'Zürich',
|
||||||
|
'phone': '+41 79 111 22 33',
|
||||||
|
'birth_year': '1988',
|
||||||
|
'civil_status': 'ledig'
|
||||||
|
},
|
||||||
|
uploaded_files=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Submit the application
|
||||||
|
response = client.post(f'/apply/{session_id}/submit-application')
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
# Verify HR email was sent with 0 file count
|
||||||
|
assert len(mock_mail.sent_messages) == 1
|
||||||
|
hr_email = mock_mail.sent_messages[0]
|
||||||
|
assert 'Anzahl der hochgeladenen Dokumente: 0' in hr_email.body
|
||||||
|
|
||||||
|
def test_send_hr_notification_function_directly(self, app):
|
||||||
|
"""Test the send_hr_notification function directly."""
|
||||||
|
from app import send_hr_notification
|
||||||
|
|
||||||
|
app.config['HR_EMAIL'] = 'hr@example.com'
|
||||||
|
app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000'
|
||||||
|
|
||||||
|
session_id = 'test-123'
|
||||||
|
app_data = {
|
||||||
|
'email': 'applicant@example.com',
|
||||||
|
'job_name': 'Test Job',
|
||||||
|
'submitted_at': '2025-01-01T12:00:00',
|
||||||
|
'personal_info': {
|
||||||
|
'firstname': 'John',
|
||||||
|
'name': 'Doe'
|
||||||
|
},
|
||||||
|
'uploaded_files': [
|
||||||
|
{'original_name': 'cv.pdf', 'stored_name': 'cv_stored.pdf', 'size': 1024}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
# Mock mail.send to prevent actual sending
|
||||||
|
with patch('app.mail.send') as mock_send:
|
||||||
|
result = send_hr_notification(session_id, app_data)
|
||||||
|
assert result is True
|
||||||
|
assert mock_send.called
|
||||||
|
|
||||||
|
def test_send_hr_notification_handles_email_failure(self, app, caplog):
|
||||||
|
"""Test that send_hr_notification handles email sending failures gracefully."""
|
||||||
|
from app import send_hr_notification
|
||||||
|
|
||||||
|
app.config['HR_EMAIL'] = 'hr@example.com'
|
||||||
|
app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000'
|
||||||
|
|
||||||
|
session_id = 'test-456'
|
||||||
|
app_data = {
|
||||||
|
'email': 'applicant@example.com',
|
||||||
|
'job_name': 'Test Job',
|
||||||
|
'submitted_at': '2025-01-01T12:00:00',
|
||||||
|
'personal_info': {
|
||||||
|
'firstname': 'Jane',
|
||||||
|
'name': 'Smith'
|
||||||
|
},
|
||||||
|
'uploaded_files': []
|
||||||
|
}
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
# Mock mail.send to raise an exception
|
||||||
|
with patch('app.mail.send', side_effect=Exception('SMTP error')):
|
||||||
|
result = send_hr_notification(session_id, app_data)
|
||||||
|
assert result is False
|
||||||
|
assert 'Failed to send HR notification email' in caplog.text
|
||||||
|
|
||||||
|
def test_hr_notification_url_format(self, client, create_test_application, mock_mail):
|
||||||
|
"""Test that the application URL in HR notification is correctly formatted."""
|
||||||
|
from app import app
|
||||||
|
app.config['HR_EMAIL'] = 'hr@example.com'
|
||||||
|
app.config['APPLICATION_URL_BASE'] = 'https://example.com'
|
||||||
|
|
||||||
|
session_id = 'test-url-format'
|
||||||
|
create_test_application(
|
||||||
|
session_id=session_id,
|
||||||
|
email='applicant@example.com',
|
||||||
|
job_name='Test Position',
|
||||||
|
current_page=4,
|
||||||
|
personal_info={
|
||||||
|
'name': 'Test',
|
||||||
|
'firstname': 'User',
|
||||||
|
'address': 'Street 1',
|
||||||
|
'zip_code': '8000',
|
||||||
|
'city': 'City',
|
||||||
|
'phone': '+41 79 123 45 67',
|
||||||
|
'birth_year': '1990',
|
||||||
|
'civil_status': 'ledig'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Submit the application
|
||||||
|
client.post(f'/apply/{session_id}/submit-application')
|
||||||
|
|
||||||
|
# Verify URL format in email
|
||||||
|
assert len(mock_mail.sent_messages) == 1
|
||||||
|
hr_email = mock_mail.sent_messages[0]
|
||||||
|
expected_url = f'https://example.com/application/{session_id}/'
|
||||||
|
assert expected_url in hr_email.body
|
||||||
Reference in New Issue
Block a user