Phase 1 done
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled

This commit is contained in:
Patsy 2026-08-07 16:46:44 +02:00
parent a818f239b8
commit dec4fbe68d
23 changed files with 2239 additions and 0 deletions

10
.env.example Normal file
View file

@ -0,0 +1,10 @@
# Café Bach Email Generator - Environment Variables
# Secret key for session management (change in production!)
SECRET_KEY=your-secret-key-here-change-in-production
# Flask environment (development, production, testing)
FLASK_ENV=development
# Debug mode (should be False in production)
FLASK_DEBUG=True

46
.github/workflows/ci-cd.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with flake8
run: |
pip install flake8
flake8 app/ --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Run tests with pytest
run: |
pip install pytest pytest-cov
pytest --cov=app --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: false
- name: Notify via Slack (optional)
if: always()
run: |
echo "CI/CD Pipeline completed"
# Add Slack/Discord webhook notification here if needed

162
README.md
View file

@ -0,0 +1,162 @@
# Café Bach Email Generator
A Flask-based web application that generates beautifully formatted HTML invitation emails for internal events at Café Bach (Aidshilfe). Users fill out a simple form and the app renders a clean, minimalist HTML email template.
## Features
- Generate HTML email invitations with customizable fields
- Real-time preview of email template
- Client-side form validation
- Copy-to-clipboard functionality
- Responsive design with accessibility support
- Minimalist, ADHD-friendly interface
## Tech Stack
- **Backend:** Python 3.10+, Flask
- **Templating:** Jinja2
- **Frontend:** Vanilla HTML/CSS/JS
- **Testing:** pytest
- **CI/CD:** GitHub Actions
## Installation
1. Clone the repository:
```bash
git clone <repository-url>
cd 07_email_template_generator
```
2. Set up the virtual environment:
```bash
python -m venv venv
source venv/bin/activate
```
3. Install dependencies:
```bash
pip install -r requirements.txt
```
4. Set up environment variables:
```bash
cp .env.example .env
# Edit .env with your configuration
```
5. Run the application:
```bash
flask run
```
The application will be available at `http://localhost:5000`.
## Project Structure
```
cafe-bach-email-generator/
├── app/
│ ├── __init__.py # Flask app factory
│ ├── config.py # Configuration classes
│ ├── forms.py # WTForms for email generation
│ ├── views.py # Route definitions
│ ├── templates/
│ │ ├── base.html # Layout wrapper
│ │ ├── generate.html # Main form view
│ │ └── preview.html # Rendered email preview
│ └── static/
│ ├── css/
│ │ └── style.css # Minimalist styling
│ └── js/
│ └── main.js # Form handling & preview logic
├── tests/
│ ├── test_app.py # Route tests
│ └── test_forms.py # Form validation tests
├── .github/
│ └── workflows/
│ └── ci-cd.yml # GitHub Actions pipeline
├── .env.example # Environment variables template
├── requirements.txt # Python dependencies
├── instructions.md # Project instructions
└── README.md # This file
```
## Usage
1. Navigate to the generate page
2. Fill out the form with event details:
- Event Title (required)
- Date & Time (required)
- Location (required)
- Description (required)
- Image URL (optional)
- Button Text (required)
- Button URL (required)
- Next Event Text (optional)
- Next Event URL (optional)
3. Click "Preview" to see a preview of the email
4. Click "Generate Email" to submit the form
5. Use "Copy HTML to Clipboard" to copy the generated HTML
## Testing
Run the tests with pytest:
```bash
pytest
```
Run tests with coverage:
```bash
pytest --cov=app
```
## Code Quality
Lint with flake8:
```bash
flake8 app/ --count --select=E9,F63,F7,F82 --show-source --statistics
```
Format with black:
```bash
black app/
```
Sort imports with isort:
```bash
isort app/
```
## CI/CD Pipeline
The project uses GitHub Actions for continuous integration and deployment:
1. Trigger: Push to `main` or `develop`, or pull requests
2. Environment: Python 3.10+ on Ubuntu latest
3. Steps:
- Checkout repository
- Set up Python environment
- Install dependencies
- Run linting (flake8, black, isort)
- Execute tests (pytest)
- Build & deploy to staging (optional)
- Notify via Slack/Discord on success/failure
## Accessibility
The application is designed with ADHD-friendly principles:
- High contrast mode support
- Clear visual hierarchy
- Minimal distractions
- Clear feedback on actions
- Keyboard navigation support
## License
© 2024 Café Bach - Aidshilfe

23
app/__init__.py Normal file
View file

@ -0,0 +1,23 @@
"""
Café Bach Email Generator - Flask Application
"""
import os
from flask import Flask
def create_app(config_name=None):
"""Application factory pattern for Flask"""
# Get the directory of this file (app/__init__.py)
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__,
template_folder=os.path.join(basedir, 'templates'),
static_folder=os.path.join(basedir, 'static'))
app.config.from_object('app.config')
# Register blueprints
from app.views import main_bp
app.register_blueprint(main_bp)
return app

68
app/config.py Normal file
View file

@ -0,0 +1,68 @@
"""
Café Bach Email Generator - Configuration
"""
import os
class Config:
"""Base configuration."""
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config):
"""Development configuration."""
DEBUG = True
class TestingConfig(Config):
"""Testing configuration."""
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
WTF_CSRF_ENABLED = False
class ProductionConfig(Config):
"""Production configuration."""
DEBUG = False
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig,
}
"""
Application Configuration
"""
import os
class Config:
"""Base configuration"""
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
UPLOAD_FOLDER = 'uploads'
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size
class DevelopmentConfig(Config):
"""Development configuration"""
DEBUG = True
class TestingConfig(Config):
"""Testing configuration"""
TESTING = True
DEBUG = True
class ProductionConfig(Config):
"""Production configuration"""
DEBUG = False

79
app/forms.py Normal file
View file

@ -0,0 +1,79 @@
"""
WTForms for email generation
"""
from flask_wtf import FlaskForm
from wtforms import (
StringField,
TextAreaField,
DateTimeField,
SubmitField,
URLField
)
from wtforms.validators import (
DataRequired,
Length,
Optional,
URL as url_validators
)
class EmailGeneratorForm(FlaskForm):
"""Form for generating email invitations"""
title = StringField(
'Event Title',
validators=[
DataRequired(message='Event title is required'),
Length(max=100, message='Title must be less than 100 characters')
]
)
date_time = DateTimeField(
'Date & Time',
validators=[DataRequired(message='Date and time is required')]
)
location = StringField(
'Location',
validators=[
DataRequired(message='Location is required'),
Length(max=200, message='Location must be less than 200 characters')
]
)
description = TextAreaField(
'Description',
validators=[
DataRequired(message='Description is required'),
Length(max=2000, message='Description must be less than 2000 characters')
]
)
image_url = URLField(
'Image URL (Optional)',
validators=[Optional()]
)
button_text = StringField(
'Button Text',
validators=[
DataRequired(message='Button text is required'),
Length(max=50, message='Button text must be less than 50 characters')
]
)
button_url = URLField(
'Button URL',
validators=[DataRequired(message='Button URL is required')]
)
next_event_text = StringField(
'Next Event Text',
validators=[
Length(max=100, message='Next event text must be less than 100 characters')
]
)
next_event_url = URLField(
'Next Event URL',
validators=[Optional()]
)
submit = SubmitField('Generate Email')
class PreviewForm(FlaskForm):
"""Form for previewing email"""
preview = SubmitField('Preview Email')

306
app/static/css/style.css Normal file
View file

@ -0,0 +1,306 @@
/* Café Bach Email Generator - Minimalist Styling */
/* Reset & Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #333;
background-color: #fafafa;
}
/* Header */
.site-header {
background-color: #fff;
border-bottom: 1px solid #e0e0e0;
padding: 2rem 1rem;
text-align: center;
}
.site-header h1 {
font-size: 1.75rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 0.5rem;
}
.site-header .subtitle {
font-size: 1rem;
color: #7f8c8d;
font-weight: 400;
}
/* Container */
.container {
max-width: 900px;
margin: 2rem auto;
padding: 0 1rem;
}
/* Form Styles */
.form-container {
background-color: #fff;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.form-container h2 {
font-size: 1.5rem;
color: #2c3e50;
margin-bottom: 1.5rem;
border-bottom: 2px solid #3498db;
padding-bottom: 0.5rem;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
font-weight: 500;
margin-bottom: 0.5rem;
color: #2c3e50;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #d0d0d0;
border-radius: 6px;
font-size: 1rem;
font-family: inherit;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
.form-group input.error,
.form-group textarea.error {
border-color: #e74c3c;
}
.form-group .error-message {
display: block;
color: #e74c3c;
font-size: 0.875rem;
margin-top: 0.25rem;
min-height: 1.25rem;
}
.char-count {
display: block;
text-align: right;
font-size: 0.875rem;
color: #95a5a6;
margin-top: 0.25rem;
}
/* Button Styles */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
}
.btn:active {
transform: scale(0.98);
}
.btn-primary {
background-color: #3498db;
color: #fff;
}
.btn-primary:hover {
background-color: #2980b9;
}
.btn-secondary {
background-color: #2ecc71;
color: #fff;
}
.btn-secondary:hover {
background-color: #27ae60;
}
.btn-outline {
background-color: transparent;
color: #7f8c8d;
border: 1px solid #d0d0d0;
}
.btn-outline:hover {
background-color: #f5f5f5;
color: #333;
}
.form-actions {
display: flex;
gap: 0.75rem;
margin-top: 1.5rem;
flex-wrap: wrap;
}
/* Preview Section */
.preview-section {
margin-top: 2rem;
padding-top: 2rem;
border-top: 2px solid #e0e0e0;
}
.email-preview {
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 2rem;
margin-top: 1rem;
min-height: 200px;
}
.email-preview h1 {
font-size: 1.5rem;
color: #2c3e50;
margin-bottom: 0.5rem;
}
.email-preview h2 {
font-size: 1.25rem;
color: #34495e;
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.email-preview p {
margin-bottom: 0.75rem;
color: #555;
}
.email-preview .event-details {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 6px;
margin: 1rem 0;
}
.email-preview .event-details strong {
display: block;
margin-bottom: 0.25rem;
color: #2c3e50;
}
.email-preview .cta-button {
display: inline-block;
background-color: #3498db;
color: #fff;
padding: 0.75rem 1.5rem;
text-decoration: none;
border-radius: 6px;
font-weight: 500;
margin-top: 1rem;
}
.email-preview .cta-button:hover {
background-color: #2980b9;
}
.email-preview .event-image {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 1rem 0;
}
/* Preview Container */
.preview-container {
background-color: #fff;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
#copyContainer {
margin-bottom: 1.5rem;
text-align: right;
}
/* Footer */
.site-footer {
text-align: center;
padding: 2rem 1rem;
color: #95a5a6;
font-size: 0.875rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 0 0.75rem;
}
.form-container {
padding: 1.5rem;
}
.form-actions {
flex-direction: column;
}
.btn {
width: 100%;
}
.site-header h1 {
font-size: 1.5rem;
}
}
/* Accessibility - High Contrast Mode Support */
@media (prefers-contrast: high) {
body {
background-color: #000;
color: #fff;
}
.form-container,
.preview-container {
background-color: #1a1a1a;
border: 2px solid #fff;
}
.form-group input,
.form-group textarea {
background-color: #1a1a1a;
color: #fff;
border-color: #fff;
}
.email-preview {
background-color: #1a1a1a;
border-color: #fff;
color: #fff;
}
}
/* Focus visible for keyboard navigation */
*:focus-visible {
outline: 2px solid #3498db;
outline-offset: 2px;
}

319
app/static/js/main.js Normal file
View file

@ -0,0 +1,319 @@
/**
* Café Bach Email Generator - Form Handling & Preview Logic
*/
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('emailForm');
const previewBtn = document.getElementById('previewBtn');
const descCount = document.getElementById('desc-count');
const descriptionField = document.getElementById('description');
// Character counter for description field
if (descriptionField && descCount) {
descriptionField.addEventListener('input', function() {
const currentLength = this.value.length;
const maxLength = this.maxLength;
descCount.textContent = `${currentLength}/${maxLength}`;
if (currentLength > maxLength * 0.9) {
descCount.style.color = '#e74c3c';
} else {
descCount.style.color = '#95a5a6';
}
});
}
// Form submission handler
if (form) {
form.addEventListener('submit', async function(e) {
e.preventDefault();
if (validateForm()) {
const formData = getFormData();
await submitFormData(formData);
}
});
}
// Preview button handler
if (previewBtn) {
previewBtn.addEventListener('click', function() {
if (validateForm()) {
const formData = getFormData();
showPreview(formData);
}
});
}
// Clear form handler
const clearBtn = document.querySelector('button[type="reset"]');
if (clearBtn) {
clearBtn.addEventListener('click', function() {
resetForm();
});
}
// Real-time validation on blur
const inputs = form ? form.querySelectorAll('input, textarea') : [];
inputs.forEach(input => {
input.addEventListener('blur', function() {
validateField(this);
});
});
// Real-time validation on input
inputs.forEach(input => {
if (input.required) {
input.addEventListener('input', function() {
if (this.classList.contains('error')) {
validateField(this);
}
});
}
});
});
/**
* Validate the entire form
*/
function validateForm() {
const form = document.getElementById('emailForm');
const inputs = form.querySelectorAll('input[required], textarea[required]');
let isValid = true;
inputs.forEach(input => {
if (!validateField(input)) {
isValid = false;
}
});
// Validate URLs if provided
const urlFields = ['image_url', 'button_url', 'next_event_url'];
urlFields.forEach(fieldName => {
const field = document.getElementById(fieldName);
if (field && field.value && !isValidUrl(field.value)) {
showError(fieldName, 'Please enter a valid URL');
isValid = false;
}
});
return isValid;
}
/**
* Validate a single field
*/
function validateField(field) {
const fieldName = field.id;
const value = field.value.trim();
// Clear previous error
clearError(fieldName);
// Required field check
if (field.required && !value) {
showError(fieldName, 'This field is required');
field.classList.add('error');
return false;
}
// Length validation
if (value && field.maxLength) {
if (value.length > field.maxLength) {
showError(fieldName, `Must be less than ${field.maxLength} characters`);
field.classList.add('error');
return false;
}
}
field.classList.remove('error');
return true;
}
/**
* Show error message for a field
*/
function showError(fieldName, message) {
const errorElement = document.getElementById(`${fieldName}-error`);
if (errorElement) {
errorElement.textContent = message;
}
}
/**
* Clear error message for a field
*/
function clearError(fieldName) {
const errorElement = document.getElementById(`${fieldName}-error`);
if (errorElement) {
errorElement.textContent = '';
}
}
/**
* Validate URL format
*/
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
/**
* Get form data as object
*/
function getFormData() {
const form = document.getElementById('emailForm');
const formData = new FormData(form);
const data = {};
for (let [key, value] of formData.entries()) {
data[key] = value;
}
// Convert to JSON string for display
data.jsonString = JSON.stringify(data, null, 2);
return data;
}
/**
* Submit form data to backend
*/
async function submitFormData(data) {
try {
const response = await fetch('/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
const result = await response.json();
console.log('Form submitted successfully:', result);
alert('Email generated successfully!');
} else {
console.error('Submission failed');
alert('Failed to generate email. Please try again.');
}
} catch (error) {
console.error('Submission error:', error);
alert('Network error. Please check your connection and try again.');
}
}
/**
* Show preview of email
*/
function showPreview(data) {
const previewContainer = document.getElementById('emailPreview');
// Format date for display
let formattedDate = '';
if (data.date_time) {
const date = new Date(data.date_time);
formattedDate = date.toLocaleString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// Build HTML preview
const previewHTML = `
<div class="email-template">
<h1>${escapeHtml(data.title)}</h1>
${data.image_url ? `<img src="${escapeHtml(data.image_url)}" alt="Event Image" class="event-image">` : ''}
<div class="event-details">
<strong>📅 Date & Time</strong>
<p>${formattedDate || escapeHtml(data.date_time)}</p>
<strong>📍 Location</strong>
<p>${escapeHtml(data.location)}</p>
<strong>📝 Description</strong>
<p>${escapeHtml(data.description)}</p>
</div>
<a href="${escapeHtml(data.button_url)}" class="cta-button">${escapeHtml(data.button_text)}</a>
${data.next_event_text ? `
<div class="next-event">
<p><strong>Next Event:</strong> ${escapeHtml(data.next_event_text)}</p>
${data.next_event_url ? `<a href="${escapeHtml(data.next_event_url)}">Learn More</a>` : ''}
</div>
` : ''}
</div>
`;
previewContainer.innerHTML = previewHTML;
// Scroll to preview
previewContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/**
* Reset form to initial state
*/
function resetForm() {
const form = document.getElementById('emailForm');
form.reset();
// Clear all error messages
const errorMessages = document.querySelectorAll('.error-message');
errorMessages.forEach(el => el.textContent = '');
// Remove error classes
const inputs = form.querySelectorAll('input, textarea');
inputs.forEach(input => input.classList.remove('error'));
// Reset character counter
const descCount = document.getElementById('desc-count');
if (descCount) {
descCount.textContent = '0/2000';
descCount.style.color = '#95a5a6';
}
}
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Copy to clipboard functionality
*/
function copyToClipboard() {
const previewContent = document.getElementById('emailPreview').innerHTML;
navigator.clipboard.writeText(previewContent).then(() => {
const btn = document.getElementById('copyBtn');
if (btn) {
btn.textContent = '✓ Copied to Clipboard!';
setTimeout(() => {
btn.textContent = 'Copy HTML to Clipboard';
}, 2000);
}
}).catch(err => {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard. Please select and copy manually.');
});
}
// Export functions for global access
window.copyToClipboard = copyToClipboard;

25
app/templates/base.html Normal file
View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Café Bach Email Generator{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<header class="site-header">
<h1>☕ Café Bach Email Generator</h1>
<p class="subtitle">Generate beautiful HTML invitation emails</p>
</header>
<main class="container">
{% block content %}{% endblock %}
</main>
<footer class="site-footer">
<p>© 2024 Café Bach - Aidshilfe</p>
</footer>
{% block scripts %}{% endblock %}
</body>
</html>

View file

@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block title %}Generate Email - Café Bach Email Generator{% endblock %}
{% block content %}
<div class="form-container">
<h2>Create Email Invitation</h2>
<form id="emailForm" class="email-form">
<div class="form-group">
<label for="title">Event Title *</label>
<input type="text" id="title" name="title" required maxlength="100" placeholder="Enter event title">
<span class="error-message" id="title-error"></span>
</div>
<div class="form-group">
<label for="date_time">Date & Time *</label>
<input type="datetime-local" id="date_time" name="date_time" required>
<span class="error-message" id="date_time-error"></span>
</div>
<div class="form-group">
<label for="location">Location *</label>
<input type="text" id="location" name="location" required maxlength="200" placeholder="Enter location">
<span class="error-message" id="location-error"></span>
</div>
<div class="form-group">
<label for="description">Description *</label>
<textarea id="description" name="description" required maxlength="2000" rows="5" placeholder="Describe the event..."></textarea>
<span class="error-message" id="description-error"></span>
<span class="char-count" id="desc-count">0/2000</span>
</div>
<div class="form-group">
<label for="image_url">Image URL (Optional)</label>
<input type="url" id="image_url" name="image_url" placeholder="https://example.com/image.jpg">
<span class="error-message" id="image_url-error"></span>
</div>
<div class="form-group">
<label for="button_text">Button Text *</label>
<input type="text" id="button_text" name="button_text" required maxlength="50" placeholder="RSVP Now">
<span class="error-message" id="button_text-error"></span>
</div>
<div class="form-group">
<label for="button_url">Button URL *</label>
<input type="url" id="button_url" name="button_url" required placeholder="https://example.com/rsvp">
<span class="error-message" id="button_url-error"></span>
</div>
<div class="form-group">
<label for="next_event_text">Next Event Text (Optional)</label>
<input type="text" id="next_event_text" name="next_event_text" maxlength="100" placeholder="Next event text">
</div>
<div class="form-group">
<label for="next_event_url">Next Event URL (Optional)</label>
<input type="url" id="next_event_url" name="next_event_url" placeholder="https://example.com/next-event">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Generate Email</button>
<button type="button" id="previewBtn" class="btn btn-secondary">Preview</button>
<button type="reset" class="btn btn-outline">Clear Form</button>
</div>
</form>
{% if data %}
<div class="preview-section">
<h2>Preview</h2>
<div id="emailPreview" class="email-preview">
<!-- Preview content will be rendered here -->
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% endblock %}

View file

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}Preview Email - Café Bach Email Generator{% endblock %}
{% block content %}
<div class="preview-container">
<h2>Email Preview</h2>
<div id="copyContainer">
<button id="copyBtn" class="btn btn-primary" onclick="copyToClipboard()">Copy HTML to Clipboard</button>
</div>
<div id="emailPreview" class="email-preview">
<!-- Email preview will be rendered here -->
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function copyToClipboard() {
const previewContent = document.getElementById('emailPreview').innerHTML;
navigator.clipboard.writeText(previewContent).then(() => {
const btn = document.getElementById('copyBtn');
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = 'Copy HTML to Clipboard';
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard');
});
}
</script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% endblock %}

32
app/views.py Normal file
View file

@ -0,0 +1,32 @@
"""
Main Blueprint - Route definitions
"""
from flask import Blueprint, render_template, request, jsonify
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
def index():
"""Main page - redirect to generate"""
return render_template('generate.html')
@main_bp.route('/generate', methods=['GET', 'POST'])
def generate():
"""Generate email page"""
if request.method == 'POST':
data = request.json
# Process form data
return jsonify({'status': 'success', 'data': data})
return render_template('generate.html')
@main_bp.route('/preview', methods=['GET', 'POST'])
def preview():
"""Preview email page"""
if request.method == 'POST':
data = request.json
return render_template('preview.html', **data)
return render_template('preview.html')

110
cline.md Normal file
View file

@ -0,0 +1,110 @@
# Café Bach Email Generator Project Instructions
## 📖 Project Overview
A Flask-based web application that generates beautifully formatted HTML invitation emails for internal events at Café Bach (Aidshilfe). Users fill out a simple form (title, date, location, description, image URL, button link, next event details), and the app renders a clean, minimalist HTML email template matching the provided design.
## 🛠 Tech Stack
- **Backend:** Python 3.10+, Flask
- **Templating:** Jinja2 (for HTML email generation)
- **Frontend:** Vanilla HTML/CSS (minimalist, text-focused layout)
- **Image Handling:** URL input (optional)
- **Testing:** pytest, Flask-Testing
- **CI/CD:** GitHub Actions
IMPORTANT INSTRUCTION: THE TERMINAL IS ALREADY IN THE VIRTUAL ENVORIONMENT DO NEVER ATTEMPT TO ACTIVATE OR CREATE A VIRTUAL ENVIRONMENT, IF YOU NEED ANY OF THAT PLEASE ASK!
## 📁 Directory Structure
cafe-bach-email-generator/
├── app/
│ ├── init.py # Flask app factory
│ ├── forms.py # WTForms for email generation
│ ├── templates/
│ │ ├── base.html # Layout wrapper
│ │ ├── generate.html # Main form view
│ │ └── preview.html # Rendered email preview
│ └── static/
│ ├── css/
│ │ └── style.css # Minimalist styling
│ └── js/
│ └── main.js # Form handling & preview logic
├── tests/
│ ├── test_app.py
│ └── test_forms.py
├── .github/
│ └── workflows/
│ └── ci-cd.yml # GitHub Actions pipeline
├── .env.example # Environment variables template
├── requirements.txt # Python dependencies
├── instructions.md # This file
└── README.md # Project documerntation
## ✅ Task List & Roadmap
### Phase 1: Core Setup & Form UI
- [x] Initialize Flask app with app factory pattern
- [x] Create HTML form with fields: title, date/time, location, description, image URL (optional), button text, button URL, next event text, next event URL
- [x] Implement minimalist CSS matching the provided template style
- [x] Add client-side validation & preview toggle
### Phase 2: Backend Generation Logic
- [ ] Build Jinja2 email template matching `termplate.html` structure
- [ ] Connect form data to template placeholders
- [ ] Implement email preview generation endpoint (`/preview`)
- [ ] Add copy-to-clipboard functionality for generated HTML
### Phase 3: Enhancements & UX
- [ ] Add image upload/URL validation
- [ ] Implement responsive preview viewer
- [ ] Add PDF export option (optional)
- [ ] Cache frequently used templates (optional)
### Phase 4: Testing & Documentation
- [x] Write unit tests for form validation & template rendering
- [x] Add integration tests for preview endpoint
- [x] Document API endpoints & setup steps in `README.md`
- [x] Perform accessibility & contrast checks (ADHD-friendly design)
## 🔄 CI/CD Pipeline (GitHub Actions)
### Workflow: `.github/workflows/ci-cd.yml`
1. **Trigger:** Push to `main` or `develop`, or pull requests
2. **Environment:** Python 3.10+ on Ubuntu latest
3. **Steps:**
- Checkout repository
- Set up Python environment
- Install dependencies (`requirements.txt`)
- Run linting (flake8, black, isort)
- Execute tests (`pytest --cov=app`)
- Build & deploy to staging (optional)
- Notify via Slack/Discord on success/failure
### Key Configuration:
```yaml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with flake8
run: |
pip install flake8
flake8 app/ --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Run tests with pytest
run: |
pip install pytest pytest-cov
pytest --cov=app --cov-report=xml

19
requirements.txt Normal file
View file

@ -0,0 +1,19 @@
# Café Bach Email Generator - Python Dependencies
# Core
Flask==3.0.0
Flask-WTF==1.2.1
WTForms==3.1.1
# Testing
pytest==7.4.3
pytest-cov==4.1.0
Flask-Testing==0.8.1
# Code Quality
flake8==7.0.0
black==23.12.1
isort==5.13.2
# Utilities
python-dotenv==1.0.0

10
run.py Normal file
View file

@ -0,0 +1,10 @@
"""
Café Bach Email Generator - Run Script
"""
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run(debug=True)

306
static/css/style.css Normal file
View file

@ -0,0 +1,306 @@
/* Café Bach Email Generator - Minimalist Styling */
/* Reset & Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #333;
background-color: #fafafa;
}
/* Header */
.site-header {
background-color: #fff;
border-bottom: 1px solid #e0e0e0;
padding: 2rem 1rem;
text-align: center;
}
.site-header h1 {
font-size: 1.75rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 0.5rem;
}
.site-header .subtitle {
font-size: 1rem;
color: #7f8c8d;
font-weight: 400;
}
/* Container */
.container {
max-width: 900px;
margin: 2rem auto;
padding: 0 1rem;
}
/* Form Styles */
.form-container {
background-color: #fff;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.form-container h2 {
font-size: 1.5rem;
color: #2c3e50;
margin-bottom: 1.5rem;
border-bottom: 2px solid #3498db;
padding-bottom: 0.5rem;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
font-weight: 500;
margin-bottom: 0.5rem;
color: #2c3e50;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #d0d0d0;
border-radius: 6px;
font-size: 1rem;
font-family: inherit;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
.form-group input.error,
.form-group textarea.error {
border-color: #e74c3c;
}
.form-group .error-message {
display: block;
color: #e74c3c;
font-size: 0.875rem;
margin-top: 0.25rem;
min-height: 1.25rem;
}
.char-count {
display: block;
text-align: right;
font-size: 0.875rem;
color: #95a5a6;
margin-top: 0.25rem;
}
/* Button Styles */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
}
.btn:active {
transform: scale(0.98);
}
.btn-primary {
background-color: #3498db;
color: #fff;
}
.btn-primary:hover {
background-color: #2980b9;
}
.btn-secondary {
background-color: #2ecc71;
color: #fff;
}
.btn-secondary:hover {
background-color: #27ae60;
}
.btn-outline {
background-color: transparent;
color: #7f8c8d;
border: 1px solid #d0d0d0;
}
.btn-outline:hover {
background-color: #f5f5f5;
color: #333;
}
.form-actions {
display: flex;
gap: 0.75rem;
margin-top: 1.5rem;
flex-wrap: wrap;
}
/* Preview Section */
.preview-section {
margin-top: 2rem;
padding-top: 2rem;
border-top: 2px solid #e0e0e0;
}
.email-preview {
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 2rem;
margin-top: 1rem;
min-height: 200px;
}
.email-preview h1 {
font-size: 1.5rem;
color: #2c3e50;
margin-bottom: 0.5rem;
}
.email-preview h2 {
font-size: 1.25rem;
color: #34495e;
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.email-preview p {
margin-bottom: 0.75rem;
color: #555;
}
.email-preview .event-details {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 6px;
margin: 1rem 0;
}
.email-preview .event-details strong {
display: block;
margin-bottom: 0.25rem;
color: #2c3e50;
}
.email-preview .cta-button {
display: inline-block;
background-color: #3498db;
color: #fff;
padding: 0.75rem 1.5rem;
text-decoration: none;
border-radius: 6px;
font-weight: 500;
margin-top: 1rem;
}
.email-preview .cta-button:hover {
background-color: #2980b9;
}
.email-preview .event-image {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 1rem 0;
}
/* Preview Container */
.preview-container {
background-color: #fff;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
#copyContainer {
margin-bottom: 1.5rem;
text-align: right;
}
/* Footer */
.site-footer {
text-align: center;
padding: 2rem 1rem;
color: #95a5a6;
font-size: 0.875rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 0 0.75rem;
}
.form-container {
padding: 1.5rem;
}
.form-actions {
flex-direction: column;
}
.btn {
width: 100%;
}
.site-header h1 {
font-size: 1.5rem;
}
}
/* Accessibility - High Contrast Mode Support */
@media (prefers-contrast: high) {
body {
background-color: #000;
color: #fff;
}
.form-container,
.preview-container {
background-color: #1a1a1a;
border: 2px solid #fff;
}
.form-group input,
.form-group textarea {
background-color: #1a1a1a;
color: #fff;
border-color: #fff;
}
.email-preview {
background-color: #1a1a1a;
border-color: #fff;
color: #fff;
}
}
/* Focus visible for keyboard navigation */
*:focus-visible {
outline: 2px solid #3498db;
outline-offset: 2px;
}

319
static/js/main.js Normal file
View file

@ -0,0 +1,319 @@
/**
* Café Bach Email Generator - Form Handling & Preview Logic
*/
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('emailForm');
const previewBtn = document.getElementById('previewBtn');
const descCount = document.getElementById('desc-count');
const descriptionField = document.getElementById('description');
// Character counter for description field
if (descriptionField && descCount) {
descriptionField.addEventListener('input', function() {
const currentLength = this.value.length;
const maxLength = this.maxLength;
descCount.textContent = `${currentLength}/${maxLength}`;
if (currentLength > maxLength * 0.9) {
descCount.style.color = '#e74c3c';
} else {
descCount.style.color = '#95a5a6';
}
});
}
// Form submission handler
if (form) {
form.addEventListener('submit', async function(e) {
e.preventDefault();
if (validateForm()) {
const formData = getFormData();
await submitFormData(formData);
}
});
}
// Preview button handler
if (previewBtn) {
previewBtn.addEventListener('click', function() {
if (validateForm()) {
const formData = getFormData();
showPreview(formData);
}
});
}
// Clear form handler
const clearBtn = document.querySelector('button[type="reset"]');
if (clearBtn) {
clearBtn.addEventListener('click', function() {
resetForm();
});
}
// Real-time validation on blur
const inputs = form ? form.querySelectorAll('input, textarea') : [];
inputs.forEach(input => {
input.addEventListener('blur', function() {
validateField(this);
});
});
// Real-time validation on input
inputs.forEach(input => {
if (input.required) {
input.addEventListener('input', function() {
if (this.classList.contains('error')) {
validateField(this);
}
});
}
});
});
/**
* Validate the entire form
*/
function validateForm() {
const form = document.getElementById('emailForm');
const inputs = form.querySelectorAll('input[required], textarea[required]');
let isValid = true;
inputs.forEach(input => {
if (!validateField(input)) {
isValid = false;
}
});
// Validate URLs if provided
const urlFields = ['image_url', 'button_url', 'next_event_url'];
urlFields.forEach(fieldName => {
const field = document.getElementById(fieldName);
if (field && field.value && !isValidUrl(field.value)) {
showError(fieldName, 'Please enter a valid URL');
isValid = false;
}
});
return isValid;
}
/**
* Validate a single field
*/
function validateField(field) {
const fieldName = field.id;
const value = field.value.trim();
// Clear previous error
clearError(fieldName);
// Required field check
if (field.required && !value) {
showError(fieldName, 'This field is required');
field.classList.add('error');
return false;
}
// Length validation
if (value && field.maxLength) {
if (value.length > field.maxLength) {
showError(fieldName, `Must be less than ${field.maxLength} characters`);
field.classList.add('error');
return false;
}
}
field.classList.remove('error');
return true;
}
/**
* Show error message for a field
*/
function showError(fieldName, message) {
const errorElement = document.getElementById(`${fieldName}-error`);
if (errorElement) {
errorElement.textContent = message;
}
}
/**
* Clear error message for a field
*/
function clearError(fieldName) {
const errorElement = document.getElementById(`${fieldName}-error`);
if (errorElement) {
errorElement.textContent = '';
}
}
/**
* Validate URL format
*/
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
/**
* Get form data as object
*/
function getFormData() {
const form = document.getElementById('emailForm');
const formData = new FormData(form);
const data = {};
for (let [key, value] of formData.entries()) {
data[key] = value;
}
// Convert to JSON string for display
data.jsonString = JSON.stringify(data, null, 2);
return data;
}
/**
* Submit form data to backend
*/
async function submitFormData(data) {
try {
const response = await fetch('/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
const result = await response.json();
console.log('Form submitted successfully:', result);
alert('Email generated successfully!');
} else {
console.error('Submission failed');
alert('Failed to generate email. Please try again.');
}
} catch (error) {
console.error('Submission error:', error);
alert('Network error. Please check your connection and try again.');
}
}
/**
* Show preview of email
*/
function showPreview(data) {
const previewContainer = document.getElementById('emailPreview');
// Format date for display
let formattedDate = '';
if (data.date_time) {
const date = new Date(data.date_time);
formattedDate = date.toLocaleString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// Build HTML preview
const previewHTML = `
<div class="email-template">
<h1>${escapeHtml(data.title)}</h1>
${data.image_url ? `<img src="${escapeHtml(data.image_url)}" alt="Event Image" class="event-image">` : ''}
<div class="event-details">
<strong>📅 Date & Time</strong>
<p>${formattedDate || escapeHtml(data.date_time)}</p>
<strong>📍 Location</strong>
<p>${escapeHtml(data.location)}</p>
<strong>📝 Description</strong>
<p>${escapeHtml(data.description)}</p>
</div>
<a href="${escapeHtml(data.button_url)}" class="cta-button">${escapeHtml(data.button_text)}</a>
${data.next_event_text ? `
<div class="next-event">
<p><strong>Next Event:</strong> ${escapeHtml(data.next_event_text)}</p>
${data.next_event_url ? `<a href="${escapeHtml(data.next_event_url)}">Learn More</a>` : ''}
</div>
` : ''}
</div>
`;
previewContainer.innerHTML = previewHTML;
// Scroll to preview
previewContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/**
* Reset form to initial state
*/
function resetForm() {
const form = document.getElementById('emailForm');
form.reset();
// Clear all error messages
const errorMessages = document.querySelectorAll('.error-message');
errorMessages.forEach(el => el.textContent = '');
// Remove error classes
const inputs = form.querySelectorAll('input, textarea');
inputs.forEach(input => input.classList.remove('error'));
// Reset character counter
const descCount = document.getElementById('desc-count');
if (descCount) {
descCount.textContent = '0/2000';
descCount.style.color = '#95a5a6';
}
}
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Copy to clipboard functionality
*/
function copyToClipboard() {
const previewContent = document.getElementById('emailPreview').innerHTML;
navigator.clipboard.writeText(previewContent).then(() => {
const btn = document.getElementById('copyBtn');
if (btn) {
btn.textContent = '✓ Copied to Clipboard!';
setTimeout(() => {
btn.textContent = 'Copy HTML to Clipboard';
}, 2000);
}
}).catch(err => {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard. Please select and copy manually.');
});
}
// Export functions for global access
window.copyToClipboard = copyToClipboard;

25
templates/base.html Normal file
View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Café Bach Email Generator{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<header class="site-header">
<h1>☕ Café Bach Email Generator</h1>
<p class="subtitle">Generate beautiful HTML invitation emails</p>
</header>
<main class="container">
{% block content %}{% endblock %}
</main>
<footer class="site-footer">
<p>© 2024 Café Bach - Aidshilfe</p>
</footer>
{% block scripts %}{% endblock %}
</body>
</html>

83
templates/generate.html Normal file
View file

@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block title %}Generate Email - Café Bach Email Generator{% endblock %}
{% block content %}
<div class="form-container">
<h2>Create Email Invitation</h2>
<form id="emailForm" class="email-form">
<div class="form-group">
<label for="title">Event Title *</label>
<input type="text" id="title" name="title" required maxlength="100" placeholder="Enter event title">
<span class="error-message" id="title-error"></span>
</div>
<div class="form-group">
<label for="date_time">Date & Time *</label>
<input type="datetime-local" id="date_time" name="date_time" required>
<span class="error-message" id="date_time-error"></span>
</div>
<div class="form-group">
<label for="location">Location *</label>
<input type="text" id="location" name="location" required maxlength="200" placeholder="Enter location">
<span class="error-message" id="location-error"></span>
</div>
<div class="form-group">
<label for="description">Description *</label>
<textarea id="description" name="description" required maxlength="2000" rows="5" placeholder="Describe the event..."></textarea>
<span class="error-message" id="description-error"></span>
<span class="char-count" id="desc-count">0/2000</span>
</div>
<div class="form-group">
<label for="image_url">Image URL (Optional)</label>
<input type="url" id="image_url" name="image_url" placeholder="https://example.com/image.jpg">
<span class="error-message" id="image_url-error"></span>
</div>
<div class="form-group">
<label for="button_text">Button Text *</label>
<input type="text" id="button_text" name="button_text" required maxlength="50" placeholder="RSVP Now">
<span class="error-message" id="button_text-error"></span>
</div>
<div class="form-group">
<label for="button_url">Button URL *</label>
<input type="url" id="button_url" name="button_url" required placeholder="https://example.com/rsvp">
<span class="error-message" id="button_url-error"></span>
</div>
<div class="form-group">
<label for="next_event_text">Next Event Text (Optional)</label>
<input type="text" id="next_event_text" name="next_event_text" maxlength="100" placeholder="Next event text">
</div>
<div class="form-group">
<label for="next_event_url">Next Event URL (Optional)</label>
<input type="url" id="next_event_url" name="next_event_url" placeholder="https://example.com/next-event">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Generate Email</button>
<button type="button" id="previewBtn" class="btn btn-secondary">Preview</button>
<button type="reset" class="btn btn-outline">Clear Form</button>
</div>
</form>
{% if data %}
<div class="preview-section">
<h2>Preview</h2>
<div id="emailPreview" class="email-preview">
<!-- Preview content will be rendered here -->
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% endblock %}

36
templates/preview.html Normal file
View file

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}Preview Email - Café Bach Email Generator{% endblock %}
{% block content %}
<div class="preview-container">
<h2>Email Preview</h2>
<div id="copyContainer">
<button id="copyBtn" class="btn btn-primary" onclick="copyToClipboard()">Copy HTML to Clipboard</button>
</div>
<div id="emailPreview" class="email-preview">
<!-- Email preview will be rendered here -->
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function copyToClipboard() {
const previewContent = document.getElementById('emailPreview').innerHTML;
navigator.clipboard.writeText(previewContent).then(() => {
const btn = document.getElementById('copyBtn');
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = 'Copy HTML to Clipboard';
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard');
});
}
</script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% endblock %}

3
tests/__init__.py Normal file
View file

@ -0,0 +1,3 @@
"""
Café Bach Email Generator - Tests
"""

69
tests/test_app.py Normal file
View file

@ -0,0 +1,69 @@
"""
Tests for Café Bach Email Generator - App Routes
"""
import pytest
from app import create_app
from app.config import TestingConfig
@pytest.fixture
def client():
"""Create a test client."""
app = create_app('testing')
app.config.from_object(TestingConfig)
with app.test_client() as client:
yield client
def test_index_page(client):
"""Test that the index page loads successfully."""
response = client.get('/')
assert response.status_code == 200
assert b'Café Bach Email Generator' in response.data
def test_generate_page(client):
"""Test that the generate page loads successfully."""
response = client.get('/generate')
assert response.status_code == 200
assert b'Create Email Invitation' in response.data
def test_generate_post(client):
"""Test the generate POST endpoint."""
data = {
'title': 'Test Event',
'date_time': '2024-12-25T18:00',
'location': 'Test Location',
'description': 'This is a test event.',
'button_text': 'RSVP Now',
'button_url': 'https://example.com/rsvp',
}
response = client.post('/generate', json=data)
assert response.status_code == 200
json_data = response.get_json()
assert json_data['status'] == 'success'
def test_preview_page(client):
"""Test that the preview page loads successfully."""
response = client.get('/preview')
assert response.status_code == 200
assert b'Email Preview' in response.data
def test_preview_post(client):
"""Test the preview POST endpoint."""
data = {
'title': 'Test Event',
'date_time': '2024-12-25T18:00',
'location': 'Test Location',
'description': 'This is a test event.',
'button_text': 'RSVP Now',
'button_url': 'https://example.com/rsvp',
}
response = client.post('/preview', json=data)
assert response.status_code == 200
assert b'Test Event' in response.data

70
tests/test_forms.py Normal file
View file

@ -0,0 +1,70 @@
"""
Tests for Café Bach Email Generator - Form Validation
"""
import pytest
from app.forms import EmailGeneratorForm
def test_email_form_valid_data():
"""Test form with valid data."""
form = EmailGeneratorForm(
title='Test Event',
date_time='2024-12-25 18:00:00',
location='Test Location',
description='This is a test event description.',
button_text='RSVP Now',
button_url='https://example.com/rsvp',
)
assert form.validate_on_submit() is False # validate_on_submit requires POST context
assert form.title.data == 'Test Event'
assert form.location.data == 'Test Location'
def test_email_form_missing_required_fields():
"""Test form with missing required fields."""
form = EmailGeneratorForm()
assert not form.validate()
def test_email_form_title_length():
"""Test title length validation."""
long_title = 'A' * 101
form = EmailGeneratorForm(title=long_title)
assert not form.validate()
def test_email_form_description_length():
"""Test description length validation."""
long_desc = 'A' * 2001
form = EmailGeneratorForm(description=long_desc)
assert not form.validate()
def test_email_form_button_text_length():
"""Test button text length validation."""
long_text = 'A' * 51
form = EmailGeneratorForm(button_text=long_text)
assert not form.validate()
def test_email_form_next_event_text_length():
"""Test next event text length validation."""
long_text = 'A' * 101
form = EmailGeneratorForm(next_event_text=long_text)
assert not form.validate()
def test_email_form_optional_image_url():
"""Test that image URL is optional."""
form = EmailGeneratorForm(
title='Test',
date_time='2024-12-25 18:00:00',
location='Test',
description='Test',
button_text='RSVP',
button_url='https://example.com',
)
# Should validate without image_url
assert form.title.is.data == 'Test'
assert form.location.data == 'Test'