Merge website-branding feature and adjust for compatibility

- Resolved conflicts in models.py, app.py, and template files
- Added branding checks to prevent errors when g.branding is None
- Updated all template references to use conditional branding
- Added BrandingSettings to migrations
- Created branding uploads directory
- Integrated branding with existing comment and task management features
This commit is contained in:
2025-07-06 16:58:29 +02:00
14 changed files with 466 additions and 35 deletions

77
app.py
View File

@@ -1,5 +1,5 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility, BrandingSettings
from data_formatting import (
format_duration, prepare_export_data, prepare_team_hours_export_data,
format_table_data, format_graph_data, format_team_data, format_burndown_data
@@ -41,7 +41,7 @@ app.config['MAIL_PORT'] = int(os.environ.get('MAIL_PORT') or 587)
app.config['MAIL_USE_TLS'] = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME', 'your-email@example.com')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD', 'your-password')
app.config['MAIL_DEFAULT_SENDER'] = os.environ.get('MAIL_DEFAULT_SENDER', 'TimeTrack <noreply@timetrack.com>')
app.config['MAIL_DEFAULT_SENDER'] = os.environ.get('MAIL_DEFAULT_SENDER', 'TimeTrack <noreply@timetrack.com>') # Will be overridden by branding in mail sending functions
# Log mail configuration (without password)
logger.info(f"Mail server: {app.config['MAIL_SERVER']}")
@@ -406,6 +406,9 @@ def load_logged_in_user():
return redirect(url_for('login'))
else:
g.company = None
# Load branding settings
g.branding = BrandingSettings.get_current()
@app.route('/')
def home():
@@ -638,19 +641,19 @@ def register():
else:
# Send verification email for regular users when verification is required
verification_url = url_for('verify_email', token=token, _external=True)
msg = Message('Verify your TimeTrack account', recipients=[email])
msg = Message(f'Verify your {g.branding.app_name} account', recipients=[email])
msg.body = f'''Hello {username},
Thank you for registering with TimeTrack. To complete your registration, please click on the link below:
Thank you for registering with {g.branding.app_name}. To complete your registration, please click on the link below:
{verification_url}
This link will expire in 24 hours.
If you did not register for TimeTrack, please ignore this email.
If you did not register for {g.branding.app_name}, please ignore this email.
Best regards,
The TimeTrack Team
The {g.branding.app_name} Team
'''
mail.send(msg)
logger.info(f"Verification email sent to {email}")
@@ -967,17 +970,17 @@ def create_user():
# Generate verification token and send email
token = new_user.generate_verification_token()
verification_url = url_for('verify_email', token=token, _external=True)
msg = Message('Verify your TimeTrack account', recipients=[email])
msg = Message(f'Verify your {g.branding.app_name} account', recipients=[email])
msg.body = f'''Hello {username},
An administrator has created an account for you on TimeTrack. To activate your account, please click on the link below:
An administrator has created an account for you on {g.branding.app_name}. To activate your account, please click on the link below:
{verification_url}
This link will expire in 24 hours.
Best regards,
The TimeTrack Team
The {g.branding.app_name} Team
'''
mail.send(msg)
@@ -1490,7 +1493,7 @@ def setup_2fa():
import io
import base64
qr_uri = g.user.get_2fa_uri()
qr_uri = g.user.get_2fa_uri(issuer_name=g.branding.app_name)
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(qr_uri)
qr.make(fit=True)
@@ -2469,6 +2472,60 @@ def system_admin_settings():
total_users=total_users,
total_system_admins=total_system_admins)
@app.route('/system-admin/branding', methods=['GET', 'POST'])
@system_admin_required
def system_admin_branding():
"""System Admin: Branding settings"""
if request.method == 'POST':
branding = BrandingSettings.get_current()
# Handle form data
branding.app_name = request.form.get('app_name', g.branding.app_name).strip()
branding.logo_alt_text = request.form.get('logo_alt_text', '').strip()
branding.primary_color = request.form.get('primary_color', '#007bff').strip()
branding.updated_by_id = g.user.id
# Handle logo upload
if 'logo_file' in request.files:
logo_file = request.files['logo_file']
if logo_file and logo_file.filename:
# Create uploads directory if it doesn't exist
upload_dir = os.path.join(app.static_folder, 'uploads', 'branding')
os.makedirs(upload_dir, exist_ok=True)
# Save the file with a timestamp to avoid conflicts
import time
filename = f"logo_{int(time.time())}_{logo_file.filename}"
logo_path = os.path.join(upload_dir, filename)
logo_file.save(logo_path)
branding.logo_filename = filename
# Handle favicon upload
if 'favicon_file' in request.files:
favicon_file = request.files['favicon_file']
if favicon_file and favicon_file.filename:
# Create uploads directory if it doesn't exist
upload_dir = os.path.join(app.static_folder, 'uploads', 'branding')
os.makedirs(upload_dir, exist_ok=True)
# Save the file with a timestamp to avoid conflicts
import time
filename = f"favicon_{int(time.time())}_{favicon_file.filename}"
favicon_path = os.path.join(upload_dir, filename)
favicon_file.save(favicon_path)
branding.favicon_filename = filename
db.session.commit()
flash('Branding settings updated successfully.', 'success')
return redirect(url_for('system_admin_branding'))
# Get current branding settings
branding = BrandingSettings.get_current()
return render_template('system_admin_branding.html',
title='System Administrator - Branding Settings',
branding=branding)
@app.route('/system-admin/health')
@system_admin_required
def system_admin_health():