email templates page

This commit is contained in:
2025-06-01 22:00:45 +02:00
parent 047ad6ef10
commit e8d79cca19
4 changed files with 272 additions and 37 deletions

View File

@@ -1,6 +1,6 @@
from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session
from flask_login import current_user, login_required
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif, EmailTemplate
from routes.auth import require_password_change
import os
from werkzeug.utils import secure_filename
@@ -621,56 +621,40 @@ def init_routes(main_bp):
@login_required
def settings():
if not current_user.is_admin:
flash('Only administrators can access settings.', 'error')
return redirect(url_for('main.dashboard'))
flash('You do not have permission to access settings.', 'error')
return redirect(url_for('main.index'))
# Get active tab from URL or default to colors
active_tab = request.args.get('tab', 'colors')
# Get site settings
site_settings = SiteSettings.get_settings()
company_form = CompanySettingsForm()
# Get events data if events tab is active
# Get events for the events tab
events = None
total_pages = 0
current_page = 1
users = []
users = {}
if active_tab == 'events':
# Get filter parameters
event_type = request.args.get('event_type', '')
date_range = request.args.get('date_range', '7d')
user_id = request.args.get('user_id', '')
page = request.args.get('page', 1, type=int)
per_page = 10
# Build query
query = Event.query
# Apply filters
if event_type:
query = query.filter(Event.event_type == event_type)
if user_id:
query = query.filter(Event.user_id == user_id)
if date_range and date_range != 'all':
cutoff_date = datetime.utcnow() - timedelta(days=int(date_range[:-1]))
query = query.filter(Event.timestamp >= cutoff_date)
# Get paginated events
events = query.order_by(Event.timestamp.desc()).paginate(page=page, per_page=per_page)
events = Event.query.order_by(Event.timestamp.desc()).paginate(page=page, per_page=per_page)
total_pages = events.pages
current_page = events.page
# Get all users for filter dropdown
users = User.query.order_by(User.username).all()
else:
events = None
total_pages = 0
current_page = 1
users = []
# Get all users for the events
user_ids = set()
for event in events.items:
user_ids.add(event.user_id)
if event.details and 'target_user_id' in event.details:
user_ids.add(event.details['target_user_id'])
users = {user.id: user for user in User.query.filter(User.id.in_(user_ids)).all()}
# Get email templates for the email templates tab
email_templates = []
if active_tab == 'email_templates':
email_templates = EmailTemplate.query.filter_by(is_active=True).all()
# Create form for company settings
company_form = CompanySettingsForm()
if request.method == 'GET':
company_form.company_name.data = site_settings.company_name
company_form.company_website.data = site_settings.company_website
@@ -693,6 +677,7 @@ def init_routes(main_bp):
total_pages=total_pages,
current_page=current_page,
users=users,
email_templates=email_templates,
form=company_form)
@main_bp.route('/settings/colors', methods=['POST'])
@@ -1088,4 +1073,52 @@ def init_routes(main_bp):
headers={
'Content-Disposition': f'attachment; filename=event_log_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}.csv'
}
)
)
@main_bp.route('/settings/email-templates/<int:template_id>', methods=['PUT'])
@login_required
def update_email_template(template_id):
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
template = EmailTemplate.query.get_or_404(template_id)
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
template.subject = data.get('subject', template.subject)
template.body = data.get('body', template.body)
try:
db.session.commit()
# Log the template update
log_event(
event_type='settings_update',
details={
'user_id': current_user.id,
'user_name': f"{current_user.username} {current_user.last_name}",
'update_type': 'email_template',
'template_id': template.id,
'template_name': template.name,
'changes': {
'subject': template.subject,
'body': template.body
}
}
)
db.session.commit()
return jsonify({
'message': 'Template updated successfully',
'template': {
'id': template.id,
'name': template.name,
'subject': template.subject,
'body': template.body
}
})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500

View File

@@ -5,6 +5,7 @@
{% from "settings/tabs/security.html" import security_tab %}
{% from "settings/tabs/debugging.html" import debugging_tab %}
{% from "settings/tabs/events.html" import events_tab %}
{% from "settings/tabs/email_templates.html" import email_templates_tab %}
{% from "settings/components/reset_colors_modal.html" import reset_colors_modal %}
{% block title %}Settings - DocuPulse{% endblock %}
@@ -37,6 +38,11 @@
<i class="fas fa-building me-2"></i>Company Info
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'email_templates' %}active{% endif %}" id="email-templates-tab" data-bs-toggle="tab" data-bs-target="#email-templates" type="button" role="tab" aria-controls="email-templates" aria-selected="{{ 'true' if active_tab == 'email_templates' else 'false' }}">
<i class="fas fa-envelope me-2"></i>Email Templates
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'security' %}active{% endif %}" id="security-tab" data-bs-toggle="tab" data-bs-target="#security" type="button" role="tab" aria-controls="security" aria-selected="{{ 'true' if active_tab == 'security' else 'false' }}">
<i class="fas fa-shield-alt me-2"></i>Security
@@ -66,6 +72,11 @@
{{ company_info_tab(site_settings, form) }}
</div>
<!-- Email Templates Tab -->
<div class="tab-pane fade {% if active_tab == 'email_templates' %}show active{% endif %}" id="email-templates" role="tabpanel" aria-labelledby="email-templates-tab">
{{ email_templates_tab(email_templates) }}
</div>
<!-- Security Tab -->
<div class="tab-pane fade {% if active_tab == 'security' %}show active{% endif %}" id="security" role="tabpanel" aria-labelledby="security-tab">
{{ security_tab() }}

View File

@@ -0,0 +1,191 @@
{% macro email_templates_tab(templates) %}
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<!-- Template Selection -->
<div class="mb-4">
<label for="templateSelect" class="form-label">Select Template</label>
<select class="form-select" id="templateSelect">
<option value="">Choose a template...</option>
{% for template in templates %}
<option value="{{ template.id }}"
data-subject="{{ template.subject }}"
data-body="{{ template.body }}">
{{ template.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Template Editor -->
<div class="card">
<div class="card-header bg-light">
<h6 class="mb-0">Template Editor</h6>
</div>
<div class="card-body">
<div class="mb-3">
<label for="templateSubject" class="form-label">Subject</label>
<input type="text" class="form-control" id="templateSubject" placeholder="Enter email subject">
</div>
<div class="mb-3">
<label for="templateBody" class="form-label">Body</label>
<textarea id="templateBody" class="form-control"></textarea>
</div>
<div class="text-end">
<button type="button" class="btn btn-primary" id="saveTemplate">
<i class="fas fa-save me-2"></i>Save Template
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Dependencies -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs4.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs4.min.js"></script>
<script>
// Wait for document and jQuery to be ready
document.addEventListener('DOMContentLoaded', function() {
// Initialize Summernote
$('#templateBody').summernote({
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'italic', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link']],
['view', ['fullscreen', 'codeview', 'help']]
],
styleTags: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
placeholder: 'Enter your email template content here...',
callbacks: {
onImageUpload: function(files) {
// Disable image upload for email templates
return false;
}
},
popover: {
image: [],
link: [],
air: []
}
});
// Handle template selection
$('#templateSelect').on('change', function() {
const selectedOption = this.options[this.selectedIndex];
const subject = selectedOption.dataset.subject || '';
const body = selectedOption.dataset.body || '';
$('#templateSubject').val(subject);
$('#templateBody').summernote('code', body);
});
// Handle template save
$('#saveTemplate').on('click', function() {
const templateId = $('#templateSelect').val();
const subject = $('#templateSubject').val();
const body = $('#templateBody').summernote('code');
if (!templateId) {
alert('Please select a template first');
return;
}
// Show loading state
const saveButton = this;
const originalText = saveButton.innerHTML;
saveButton.disabled = true;
saveButton.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
// Send AJAX request
fetch(`/settings/email-templates/${templateId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
subject: subject,
body: body
})
})
.then(response => response.json())
.then(data => {
if (data.error) {
throw new Error(data.error);
}
// Show success message
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show mt-3';
alert.innerHTML = `
<i class="fas fa-check-circle me-2"></i>Template saved successfully
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
document.querySelector('.card-body').appendChild(alert);
// Update the select option's data attributes
const option = document.getElementById('templateSelect').options[document.getElementById('templateSelect').selectedIndex];
option.dataset.subject = subject;
option.dataset.body = body;
})
.catch(error => {
// Show error message
const alert = document.createElement('div');
alert.className = 'alert alert-danger alert-dismissible fade show mt-3';
alert.innerHTML = `
<i class="fas fa-exclamation-circle me-2"></i>${error.message || 'Failed to save template'}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
document.querySelector('.card-body').appendChild(alert);
})
.finally(() => {
// Restore button state
saveButton.disabled = false;
saveButton.innerHTML = originalText;
});
});
});
</script>
<style>
/* Summernote custom styles */
.note-editor {
margin-bottom: 0;
}
.note-editor.note-frame {
border-color: #dee2e6;
border-radius: 0.375rem;
}
.note-editor.note-frame:focus-within {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.note-toolbar {
background-color: #f8f9fa;
border-top-left-radius: 0.375rem;
border-top-right-radius: 0.375rem;
border-bottom: 1px solid #dee2e6;
}
.note-editing-area {
background-color: #fff;
}
.note-statusbar {
border-top: 1px solid #dee2e6;
}
.note-placeholder {
color: #6c757d;
}
</style>
{% endmacro %}