email templates page
This commit is contained in:
Binary file not shown.
105
routes/main.py
105
routes/main.py
@@ -1,6 +1,6 @@
|
|||||||
from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session
|
from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session
|
||||||
from flask_login import current_user, login_required
|
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
|
from routes.auth import require_password_change
|
||||||
import os
|
import os
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
@@ -621,56 +621,40 @@ def init_routes(main_bp):
|
|||||||
@login_required
|
@login_required
|
||||||
def settings():
|
def settings():
|
||||||
if not current_user.is_admin:
|
if not current_user.is_admin:
|
||||||
flash('Only administrators can access settings.', 'error')
|
flash('You do not have permission to access settings.', 'error')
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.index'))
|
||||||
|
|
||||||
# Get active tab from URL or default to colors
|
|
||||||
active_tab = request.args.get('tab', 'colors')
|
active_tab = request.args.get('tab', 'colors')
|
||||||
|
|
||||||
# Get site settings
|
|
||||||
site_settings = SiteSettings.get_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
|
events = None
|
||||||
total_pages = 0
|
total_pages = 0
|
||||||
current_page = 1
|
current_page = 1
|
||||||
users = []
|
users = {}
|
||||||
|
|
||||||
if active_tab == 'events':
|
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)
|
page = request.args.get('page', 1, type=int)
|
||||||
per_page = 10
|
per_page = 10
|
||||||
|
events = Event.query.order_by(Event.timestamp.desc()).paginate(page=page, per_page=per_page)
|
||||||
# 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)
|
|
||||||
total_pages = events.pages
|
total_pages = events.pages
|
||||||
current_page = events.page
|
current_page = events.page
|
||||||
|
|
||||||
# Get all users for filter dropdown
|
# Get all users for the events
|
||||||
users = User.query.order_by(User.username).all()
|
user_ids = set()
|
||||||
else:
|
for event in events.items:
|
||||||
events = None
|
user_ids.add(event.user_id)
|
||||||
total_pages = 0
|
if event.details and 'target_user_id' in event.details:
|
||||||
current_page = 1
|
user_ids.add(event.details['target_user_id'])
|
||||||
users = []
|
|
||||||
|
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':
|
if request.method == 'GET':
|
||||||
company_form.company_name.data = site_settings.company_name
|
company_form.company_name.data = site_settings.company_name
|
||||||
company_form.company_website.data = site_settings.company_website
|
company_form.company_website.data = site_settings.company_website
|
||||||
@@ -693,6 +677,7 @@ def init_routes(main_bp):
|
|||||||
total_pages=total_pages,
|
total_pages=total_pages,
|
||||||
current_page=current_page,
|
current_page=current_page,
|
||||||
users=users,
|
users=users,
|
||||||
|
email_templates=email_templates,
|
||||||
form=company_form)
|
form=company_form)
|
||||||
|
|
||||||
@main_bp.route('/settings/colors', methods=['POST'])
|
@main_bp.route('/settings/colors', methods=['POST'])
|
||||||
@@ -1089,3 +1074,51 @@ def init_routes(main_bp):
|
|||||||
'Content-Disposition': f'attachment; filename=event_log_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}.csv'
|
'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
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
{% from "settings/tabs/security.html" import security_tab %}
|
{% from "settings/tabs/security.html" import security_tab %}
|
||||||
{% from "settings/tabs/debugging.html" import debugging_tab %}
|
{% from "settings/tabs/debugging.html" import debugging_tab %}
|
||||||
{% from "settings/tabs/events.html" import events_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 %}
|
{% from "settings/components/reset_colors_modal.html" import reset_colors_modal %}
|
||||||
|
|
||||||
{% block title %}Settings - DocuPulse{% endblock %}
|
{% block title %}Settings - DocuPulse{% endblock %}
|
||||||
@@ -37,6 +38,11 @@
|
|||||||
<i class="fas fa-building me-2"></i>Company Info
|
<i class="fas fa-building me-2"></i>Company Info
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</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">
|
<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' }}">
|
<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
|
<i class="fas fa-shield-alt me-2"></i>Security
|
||||||
@@ -66,6 +72,11 @@
|
|||||||
{{ company_info_tab(site_settings, form) }}
|
{{ company_info_tab(site_settings, form) }}
|
||||||
</div>
|
</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 -->
|
<!-- Security Tab -->
|
||||||
<div class="tab-pane fade {% if active_tab == 'security' %}show active{% endif %}" id="security" role="tabpanel" aria-labelledby="security-tab">
|
<div class="tab-pane fade {% if active_tab == 'security' %}show active{% endif %}" id="security" role="tabpanel" aria-labelledby="security-tab">
|
||||||
{{ security_tab() }}
|
{{ security_tab() }}
|
||||||
|
|||||||
191
templates/settings/tabs/email_templates.html
Normal file
191
templates/settings/tabs/email_templates.html
Normal 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 %}
|
||||||
Reference in New Issue
Block a user