752 lines
34 KiB
Python
752 lines
34 KiB
Python
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
|
|
from routes.auth import require_password_change
|
|
from utils.event_logger import log_event
|
|
import os
|
|
from werkzeug.utils import secure_filename
|
|
from sqlalchemy import func, case, literal_column, text
|
|
from datetime import datetime, timedelta
|
|
import logging
|
|
import sys
|
|
import time
|
|
|
|
# Set up logging to show in console
|
|
logging.basicConfig(
|
|
level=logging.DEBUG,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
handlers=[
|
|
logging.StreamHandler(sys.stdout)
|
|
]
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def init_routes(main_bp):
|
|
@main_bp.context_processor
|
|
def inject_site_settings():
|
|
site_settings = SiteSettings.query.first()
|
|
return dict(site_settings=site_settings)
|
|
|
|
@main_bp.route('/')
|
|
@login_required
|
|
@require_password_change
|
|
def home():
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
@main_bp.route('/dashboard')
|
|
@login_required
|
|
@require_password_change
|
|
def dashboard():
|
|
logger.info("Loading dashboard...")
|
|
# Get 3 most recent users
|
|
recent_contacts = User.query.order_by(User.created_at.desc()).limit(3).all()
|
|
# Count active and inactive users
|
|
active_count = User.query.filter_by(is_active=True).count()
|
|
inactive_count = User.query.filter_by(is_active=False).count()
|
|
|
|
# Room count and size logic
|
|
if current_user.is_admin:
|
|
logger.info("Loading admin dashboard...")
|
|
room_count = Room.query.count()
|
|
# Get total file and folder counts for admin
|
|
file_count = RoomFile.query.filter_by(type='file').count()
|
|
folder_count = RoomFile.query.filter_by(type='folder').count()
|
|
logger.info(f"Admin stats - Files: {file_count}, Folders: {folder_count}")
|
|
|
|
# Get total size of all files including trash
|
|
total_size = db.session.query(func.sum(RoomFile.size)).filter(RoomFile.type == 'file').scalar() or 0
|
|
# Get recent activity for all files
|
|
recent_activity = db.session.query(
|
|
RoomFile,
|
|
Room,
|
|
User
|
|
).join(
|
|
Room, RoomFile.room_id == Room.id
|
|
).join(
|
|
User, RoomFile.uploaded_by == User.id
|
|
).filter(
|
|
RoomFile.deleted == False,
|
|
RoomFile.uploaded_at.isnot(None)
|
|
).order_by(
|
|
RoomFile.uploaded_at.desc()
|
|
).limit(10).all()
|
|
|
|
logger.info(f"Recent activity query results: {len(recent_activity)}")
|
|
if len(recent_activity) == 0:
|
|
# Debug query to see what files exist
|
|
all_files = RoomFile.query.filter_by(deleted=False).all()
|
|
logger.info(f"Total non-deleted files: {len(all_files)}")
|
|
for file in all_files[:5]: # Log first 5 files for debugging
|
|
logger.info(f"File: {file.name}, Uploaded: {file.uploaded_at}, Type: {file.type}")
|
|
|
|
# Format the activity data
|
|
formatted_activity = []
|
|
for file, room, user in recent_activity:
|
|
activity = {
|
|
'name': file.name,
|
|
'type': file.type,
|
|
'room': room,
|
|
'uploader': user,
|
|
'uploaded_at': file.uploaded_at,
|
|
'is_starred': current_user in file.starred_by,
|
|
'is_deleted': file.deleted,
|
|
'can_download': True # Admin can download everything
|
|
}
|
|
formatted_activity.append(activity)
|
|
formatted_activities = formatted_activity
|
|
logger.info(f"Formatted activities: {len(formatted_activities)}")
|
|
# Get storage usage by file type including trash
|
|
storage_by_type = db.session.query(
|
|
case(
|
|
(RoomFile.name.like('%.%'),
|
|
func.substring(RoomFile.name, func.strpos(RoomFile.name, '.') + 1)),
|
|
else_=literal_column("'unknown'")
|
|
).label('extension'),
|
|
func.count(RoomFile.id).label('count'),
|
|
func.sum(RoomFile.size).label('total_size')
|
|
).filter(
|
|
RoomFile.type == 'file'
|
|
).group_by('extension').all()
|
|
|
|
# Get trash and starred stats for admin
|
|
trash_count = RoomFile.query.filter_by(deleted=True).count()
|
|
starred_count = RoomFile.query.filter(RoomFile.starred_by.contains(current_user)).count()
|
|
# Get oldest trash date and total trash size
|
|
oldest_trash = RoomFile.query.filter_by(deleted=True).order_by(RoomFile.deleted_at.asc()).first()
|
|
oldest_trash_date = oldest_trash.deleted_at.strftime('%Y-%m-%d') if oldest_trash and oldest_trash.deleted_at else None
|
|
trash_size = db.session.query(db.func.sum(RoomFile.size)).filter(RoomFile.deleted==True).scalar() or 0
|
|
|
|
# Get files that will be deleted in next 7 days
|
|
seven_days_from_now = datetime.utcnow() + timedelta(days=7)
|
|
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
|
pending_deletion = RoomFile.query.filter(
|
|
RoomFile.deleted==True,
|
|
RoomFile.deleted_at <= thirty_days_ago,
|
|
RoomFile.deleted_at > thirty_days_ago - timedelta(days=7)
|
|
).count()
|
|
|
|
# Get trash file type breakdown
|
|
trash_by_type = db.session.query(
|
|
case(
|
|
(RoomFile.name.like('%.%'),
|
|
func.substring(RoomFile.name, func.strpos(RoomFile.name, '.') + 1)),
|
|
else_=literal_column("'unknown'")
|
|
).label('extension'),
|
|
func.count(RoomFile.id).label('count')
|
|
).filter(
|
|
RoomFile.deleted==True
|
|
).group_by('extension').all()
|
|
else:
|
|
# Get rooms the user has access to
|
|
accessible_rooms = Room.query.filter(Room.members.any(id=current_user.id)).all()
|
|
room_count = len(accessible_rooms)
|
|
# Get file and folder counts for accessible rooms
|
|
room_ids = [room.id for room in accessible_rooms]
|
|
file_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.type == 'file').count()
|
|
folder_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.type == 'folder').count()
|
|
# Get total size of files in accessible rooms including trash
|
|
total_size = db.session.query(func.sum(RoomFile.size)).filter(
|
|
RoomFile.room_id.in_(room_ids),
|
|
RoomFile.type == 'file'
|
|
).scalar() or 0
|
|
# Get recent activity for accessible rooms
|
|
recent_activity = db.session.query(
|
|
RoomFile,
|
|
Room,
|
|
User
|
|
).join(
|
|
Room, RoomFile.room_id == Room.id
|
|
).join(
|
|
User, RoomFile.uploaded_by == User.id
|
|
).filter(
|
|
RoomFile.room_id.in_(room_ids),
|
|
RoomFile.deleted == False,
|
|
RoomFile.uploaded_at.isnot(None) # Ensure uploaded_at is not null
|
|
).order_by(
|
|
RoomFile.uploaded_at.desc()
|
|
).limit(10).all()
|
|
|
|
logger.info(f"Recent activity query results (non-admin): {len(recent_activity)}")
|
|
if len(recent_activity) == 0:
|
|
# Debug query to see what files exist
|
|
all_files = RoomFile.query.filter(
|
|
RoomFile.room_id.in_(room_ids),
|
|
RoomFile.deleted == False
|
|
).all()
|
|
logger.info(f"Total non-deleted files in accessible rooms: {len(all_files)}")
|
|
for file in all_files[:5]: # Log first 5 files for debugging
|
|
logger.info(f"File: {file.name}, Uploaded: {file.uploaded_at}, Type: {file.type}")
|
|
|
|
# Format the activity data
|
|
formatted_activity = []
|
|
user_perms = {p.room_id: p for p in RoomMemberPermission.query.filter(
|
|
RoomMemberPermission.room_id.in_(room_ids),
|
|
RoomMemberPermission.user_id==current_user.id
|
|
).all()}
|
|
|
|
for file, room, user in recent_activity:
|
|
perm = user_perms.get(room.id)
|
|
activity = {
|
|
'name': file.name,
|
|
'type': file.type,
|
|
'room': room,
|
|
'uploader': user,
|
|
'uploaded_at': file.uploaded_at,
|
|
'is_starred': current_user in file.starred_by,
|
|
'is_deleted': file.deleted,
|
|
'can_download': perm.can_download if perm else False
|
|
}
|
|
formatted_activity.append(activity)
|
|
formatted_activities = formatted_activity
|
|
# Get storage usage by file type for accessible rooms including trash
|
|
storage_by_type = db.session.query(
|
|
case(
|
|
(RoomFile.name.like('%.%'),
|
|
func.substring(RoomFile.name, func.strpos(RoomFile.name, '.') + 1)),
|
|
else_=literal_column("'unknown'")
|
|
).label('extension'),
|
|
func.count(RoomFile.id).label('count'),
|
|
func.sum(RoomFile.size).label('total_size')
|
|
).filter(
|
|
RoomFile.room_id.in_(room_ids),
|
|
RoomFile.type == 'file'
|
|
).group_by('extension').all()
|
|
|
|
# Get trash and starred stats for user's accessible rooms
|
|
trash_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).count()
|
|
starred_count = RoomFile.query.filter(
|
|
RoomFile.room_id.in_(room_ids),
|
|
RoomFile.starred_by.contains(current_user)
|
|
).count()
|
|
# Get oldest trash date and total trash size for user's rooms
|
|
oldest_trash = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).order_by(RoomFile.deleted_at.asc()).first()
|
|
oldest_trash_date = oldest_trash.deleted_at.strftime('%Y-%m-%d') if oldest_trash and oldest_trash.deleted_at else None
|
|
trash_size = db.session.query(db.func.sum(RoomFile.size)).filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).scalar() or 0
|
|
|
|
# Get files that will be deleted in next 7 days for user's rooms
|
|
seven_days_from_now = datetime.utcnow() + timedelta(days=7)
|
|
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
|
pending_deletion = RoomFile.query.filter(
|
|
RoomFile.room_id.in_(room_ids),
|
|
RoomFile.deleted==True,
|
|
RoomFile.deleted_at <= thirty_days_ago,
|
|
RoomFile.deleted_at > thirty_days_ago - timedelta(days=7)
|
|
).count()
|
|
|
|
# Get trash file type breakdown for user's rooms
|
|
trash_by_type = db.session.query(
|
|
case(
|
|
(RoomFile.name.like('%.%'),
|
|
func.substring(RoomFile.name, func.strpos(RoomFile.name, '.') + 1)),
|
|
else_=literal_column("'unknown'")
|
|
).label('extension'),
|
|
func.count(RoomFile.id).label('count')
|
|
).filter(
|
|
RoomFile.room_id.in_(room_ids),
|
|
RoomFile.deleted==True
|
|
).group_by('extension').all()
|
|
|
|
return render_template('dashboard/dashboard.html',
|
|
recent_contacts=recent_contacts,
|
|
active_count=active_count,
|
|
inactive_count=inactive_count,
|
|
room_count=room_count,
|
|
file_count=file_count,
|
|
folder_count=folder_count,
|
|
total_size=total_size,
|
|
storage_by_type=storage_by_type,
|
|
trash_count=trash_count,
|
|
starred_count=starred_count,
|
|
oldest_trash_date=oldest_trash_date,
|
|
trash_size=trash_size,
|
|
pending_deletion=pending_deletion,
|
|
trash_by_type=trash_by_type)
|
|
|
|
UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads', 'profile_pics')
|
|
if not os.path.exists(UPLOAD_FOLDER):
|
|
os.makedirs(UPLOAD_FOLDER)
|
|
|
|
@main_bp.route('/profile', methods=['GET', 'POST'])
|
|
@login_required
|
|
@require_password_change
|
|
def profile():
|
|
if request.method == 'POST':
|
|
logger.debug(f"Profile form submitted with data: {request.form}")
|
|
logger.debug(f"Files in request: {request.files}")
|
|
|
|
try:
|
|
# Handle profile picture removal
|
|
if 'remove_picture' in request.form:
|
|
logger.debug("Removing profile picture")
|
|
if current_user.profile_picture:
|
|
# Delete the old profile picture file
|
|
old_picture_path = os.path.join(UPLOAD_FOLDER, current_user.profile_picture)
|
|
if os.path.exists(old_picture_path):
|
|
os.remove(old_picture_path)
|
|
current_user.profile_picture = None
|
|
db.session.commit()
|
|
flash('Profile picture removed successfully!', 'success')
|
|
return redirect(url_for('main.profile'))
|
|
|
|
new_email = request.form.get('email')
|
|
logger.debug(f"New email: {new_email}")
|
|
# Check if the new email is already used by another user
|
|
if new_email != current_user.email:
|
|
existing_user = User.query.filter_by(email=new_email).first()
|
|
if existing_user:
|
|
flash('A user with this email already exists.', 'error')
|
|
return render_template('profile/profile.html')
|
|
|
|
# Handle profile picture upload
|
|
file = request.files.get('profile_picture')
|
|
if file and file.filename:
|
|
logger.debug(f"Uploading new profile picture: {file.filename}")
|
|
filename = secure_filename(file.filename)
|
|
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
|
file.save(file_path)
|
|
current_user.profile_picture = filename
|
|
|
|
# Update user information
|
|
current_user.username = request.form.get('first_name')
|
|
current_user.last_name = request.form.get('last_name')
|
|
current_user.email = new_email
|
|
current_user.phone = request.form.get('phone')
|
|
current_user.company = request.form.get('company')
|
|
current_user.position = request.form.get('position')
|
|
current_user.notes = request.form.get('notes')
|
|
|
|
logger.debug(f"Updated user data: username={current_user.username}, last_name={current_user.last_name}, email={current_user.email}")
|
|
|
|
# Handle password change if provided
|
|
new_password = request.form.get('new_password')
|
|
confirm_password = request.form.get('confirm_password')
|
|
if new_password:
|
|
if not confirm_password:
|
|
flash('Please confirm your new password.', 'error')
|
|
return render_template('profile/profile.html')
|
|
if new_password != confirm_password:
|
|
flash('Passwords do not match.', 'error')
|
|
return render_template('profile/profile.html')
|
|
current_user.set_password(new_password)
|
|
flash('Password updated successfully.', 'success')
|
|
elif confirm_password:
|
|
flash('Please enter a new password.', 'error')
|
|
return render_template('profile/profile.html')
|
|
|
|
# Create event details
|
|
event_details = {
|
|
'user_id': current_user.id,
|
|
'email': current_user.email,
|
|
'update_type': 'profile_update',
|
|
'updated_fields': {
|
|
'username': current_user.username,
|
|
'last_name': current_user.last_name,
|
|
'email': current_user.email,
|
|
'phone': current_user.phone,
|
|
'company': current_user.company,
|
|
'position': current_user.position,
|
|
'notes': current_user.notes,
|
|
'profile_picture': bool(current_user.profile_picture)
|
|
},
|
|
'changes': {
|
|
'username': request.form.get('first_name'),
|
|
'last_name': request.form.get('last_name'),
|
|
'email': request.form.get('email'),
|
|
'phone': request.form.get('phone'),
|
|
'company': request.form.get('company'),
|
|
'position': request.form.get('position'),
|
|
'notes': request.form.get('notes'),
|
|
'password_changed': bool(new_password)
|
|
}
|
|
}
|
|
logger.debug(f"Preparing to create profile update event with details: {event_details}")
|
|
|
|
# Create the event
|
|
event = log_event('user_update', event_details, current_user.id)
|
|
logger.debug("Event object created and added to session")
|
|
|
|
# Commit all changes
|
|
db.session.commit()
|
|
logger.debug("Profile changes and event committed to database successfully")
|
|
|
|
flash('Profile updated successfully!', 'success')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating profile: {str(e)}")
|
|
logger.error(f"Full error details: {str(e.__class__.__name__)}: {str(e)}")
|
|
db.session.rollback()
|
|
flash('An error occurred while updating your profile.', 'error')
|
|
return render_template('profile/profile.html')
|
|
|
|
return render_template('profile/profile.html')
|
|
|
|
@main_bp.route('/starred')
|
|
@login_required
|
|
@require_password_change
|
|
def starred():
|
|
return render_template('starred/starred.html')
|
|
|
|
@main_bp.route('/conversations')
|
|
@login_required
|
|
@require_password_change
|
|
def conversations():
|
|
return redirect(url_for('conversations.conversations'))
|
|
|
|
@main_bp.route('/trash')
|
|
@login_required
|
|
@require_password_change
|
|
def trash():
|
|
return render_template('trash/trash.html')
|
|
|
|
@main_bp.route('/notifications')
|
|
@login_required
|
|
@require_password_change
|
|
def notifications():
|
|
return render_template('notifications/notifications.html')
|
|
|
|
@main_bp.route('/settings')
|
|
@login_required
|
|
def settings():
|
|
if not current_user.is_admin:
|
|
flash('Only administrators can access settings.', 'error')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
site_settings = SiteSettings.get_settings()
|
|
active_tab = request.args.get('tab', 'colors')
|
|
|
|
# Get events data if events tab is active
|
|
events = None
|
|
total_pages = 1
|
|
current_page = 1
|
|
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 = 50
|
|
|
|
# Calculate date range
|
|
end_date = datetime.utcnow()
|
|
if date_range == '24h':
|
|
start_date = end_date - timedelta(days=1)
|
|
elif date_range == '7d':
|
|
start_date = end_date - timedelta(days=7)
|
|
elif date_range == '30d':
|
|
start_date = end_date - timedelta(days=30)
|
|
else:
|
|
start_date = None
|
|
|
|
# Build query
|
|
query = Event.query
|
|
|
|
if event_type:
|
|
query = query.filter_by(event_type=event_type)
|
|
if start_date:
|
|
query = query.filter(Event.timestamp >= start_date)
|
|
if user_id:
|
|
query = query.filter_by(user_id=user_id)
|
|
|
|
# Get total count for pagination
|
|
total_events = query.count()
|
|
total_pages = (total_events + per_page - 1) // per_page
|
|
|
|
# Get paginated events
|
|
events = query.order_by(Event.timestamp.desc()).paginate(page=page, per_page=per_page)
|
|
|
|
# Get all users for filter dropdown
|
|
users = User.query.order_by(User.username).all()
|
|
|
|
return render_template('settings/settings.html',
|
|
primary_color=site_settings.primary_color,
|
|
secondary_color=site_settings.secondary_color,
|
|
active_tab=active_tab,
|
|
site_settings=site_settings,
|
|
events=events.items if events else None,
|
|
total_pages=total_pages,
|
|
current_page=current_page,
|
|
users=users,
|
|
csrf_token=session.get('csrf_token'))
|
|
|
|
@main_bp.route('/settings/colors', methods=['POST'])
|
|
@login_required
|
|
def update_colors():
|
|
if not current_user.is_admin:
|
|
flash('Only administrators can update settings.', 'error')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
logger.debug(f"Form data: {request.form}")
|
|
logger.debug(f"CSRF token: {request.form.get('csrf_token')}")
|
|
|
|
primary_color = request.form.get('primary_color')
|
|
secondary_color = request.form.get('secondary_color')
|
|
|
|
logger.debug(f"Primary color: {primary_color}")
|
|
logger.debug(f"Secondary color: {secondary_color}")
|
|
|
|
if not primary_color or not secondary_color:
|
|
flash('Both primary and secondary colors are required.', 'error')
|
|
return redirect(url_for('main.settings'))
|
|
|
|
site_settings = SiteSettings.get_settings()
|
|
site_settings.primary_color = primary_color
|
|
site_settings.secondary_color = secondary_color
|
|
|
|
try:
|
|
db.session.commit()
|
|
logger.debug("Colors updated successfully in database")
|
|
flash('Color settings updated successfully!', 'success')
|
|
except Exception as e:
|
|
logger.error(f"Error updating colors: {str(e)}")
|
|
db.session.rollback()
|
|
flash('An error occurred while updating color settings.', 'error')
|
|
|
|
return redirect(url_for('main.settings'))
|
|
|
|
@main_bp.route('/settings/colors/reset', methods=['POST'])
|
|
@login_required
|
|
def reset_colors():
|
|
if not current_user.is_admin:
|
|
flash('Only administrators can update settings.', 'error')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
site_settings = SiteSettings.get_settings()
|
|
site_settings.primary_color = '#16767b' # Default from colors.css
|
|
site_settings.secondary_color = '#741b5f' # Default from colors.css
|
|
|
|
try:
|
|
db.session.commit()
|
|
flash('Colors reset to defaults successfully!', 'success')
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
flash('An error occurred while resetting colors.', 'error')
|
|
|
|
return redirect(url_for('main.settings'))
|
|
|
|
@main_bp.route('/settings/company', methods=['POST'])
|
|
@login_required
|
|
def update_company_settings():
|
|
if not current_user.is_admin:
|
|
flash('Only administrators can update settings.', 'error')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
site_settings = SiteSettings.get_settings()
|
|
|
|
# Handle logo upload
|
|
if 'company_logo' in request.files:
|
|
logo_file = request.files['company_logo']
|
|
if logo_file and logo_file.filename:
|
|
# Delete old logo if it exists
|
|
if site_settings.company_logo:
|
|
old_logo_path = os.path.join('static', 'uploads', 'company_logos', site_settings.company_logo)
|
|
if os.path.exists(old_logo_path):
|
|
os.remove(old_logo_path)
|
|
|
|
# Save new logo
|
|
filename = secure_filename(logo_file.filename)
|
|
# Add timestamp to filename to prevent caching issues
|
|
filename = f"{int(time.time())}_{filename}"
|
|
logo_path = os.path.join('static', 'uploads', 'company_logos', filename)
|
|
logo_file.save(logo_path)
|
|
site_settings.company_logo = filename
|
|
|
|
# Update all company fields
|
|
site_settings.company_name = request.form.get('company_name')
|
|
site_settings.company_website = request.form.get('company_website')
|
|
site_settings.company_email = request.form.get('company_email')
|
|
site_settings.company_phone = request.form.get('company_phone')
|
|
site_settings.company_address = request.form.get('company_address')
|
|
site_settings.company_city = request.form.get('company_city')
|
|
site_settings.company_state = request.form.get('company_state')
|
|
site_settings.company_zip = request.form.get('company_zip')
|
|
site_settings.company_country = request.form.get('company_country')
|
|
site_settings.company_description = request.form.get('company_description')
|
|
site_settings.company_industry = request.form.get('company_industry')
|
|
|
|
try:
|
|
db.session.commit()
|
|
flash('Company settings updated successfully!', 'success')
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
flash('An error occurred while updating company settings.', 'error')
|
|
|
|
return redirect(url_for('main.settings', tab='general'))
|
|
|
|
@main_bp.route('/dynamic-colors.css')
|
|
def dynamic_colors():
|
|
"""Generate dynamic CSS variables based on site settings"""
|
|
logger.info(f"[Dynamic Colors] Request from: {request.referrer}")
|
|
|
|
# Get colors from settings
|
|
site_settings = SiteSettings.get_settings()
|
|
primary_color = site_settings.primary_color
|
|
secondary_color = site_settings.secondary_color
|
|
|
|
logger.info(f"[Dynamic Colors] Current colors - Primary: {primary_color}, Secondary: {secondary_color}")
|
|
|
|
# Convert hex to RGB for opacity calculations
|
|
def hex_to_rgb(hex_color):
|
|
hex_color = hex_color.lstrip('#')
|
|
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
|
|
|
def rgb_to_hex(rgb):
|
|
return '#{:02x}{:02x}{:02x}'.format(rgb[0], rgb[1], rgb[2])
|
|
|
|
def lighten_color(hex_color, amount=0.2):
|
|
rgb = hex_to_rgb(hex_color)
|
|
rgb = tuple(min(255, int(c + (255 - c) * amount)) for c in rgb)
|
|
return rgb_to_hex(rgb)
|
|
|
|
# Calculate derived colors
|
|
primary_rgb = hex_to_rgb(primary_color)
|
|
secondary_rgb = hex_to_rgb(secondary_color)
|
|
|
|
# Lighten colors for hover states
|
|
primary_light = lighten_color(primary_color, 0.2)
|
|
secondary_light = lighten_color(secondary_color, 0.2)
|
|
|
|
# Generate CSS with opacity variables
|
|
css = f"""
|
|
:root {{
|
|
/* Primary Colors */
|
|
--primary-color: {primary_color};
|
|
--primary-light: {primary_light};
|
|
--primary-rgb: {primary_rgb[0]}, {primary_rgb[1]}, {primary_rgb[2]};
|
|
--primary-opacity-8: rgba({primary_rgb[0]}, {primary_rgb[1]}, {primary_rgb[2]}, 0.08);
|
|
--primary-opacity-15: rgba({primary_rgb[0]}, {primary_rgb[1]}, {primary_rgb[2]}, 0.15);
|
|
--primary-opacity-25: rgba({primary_rgb[0]}, {primary_rgb[1]}, {primary_rgb[2]}, 0.25);
|
|
--primary-opacity-50: rgba({primary_rgb[0]}, {primary_rgb[1]}, {primary_rgb[2]}, 0.5);
|
|
|
|
/* Secondary Colors */
|
|
--secondary-color: {secondary_color};
|
|
--secondary-light: {secondary_light};
|
|
--secondary-rgb: {secondary_rgb[0]}, {secondary_rgb[1]}, {secondary_rgb[2]};
|
|
--secondary-opacity-8: rgba({secondary_rgb[0]}, {secondary_rgb[1]}, {secondary_rgb[2]}, 0.08);
|
|
--secondary-opacity-15: rgba({secondary_rgb[0]}, {secondary_rgb[1]}, {secondary_rgb[2]}, 0.15);
|
|
--secondary-opacity-25: rgba({secondary_rgb[0]}, {secondary_rgb[1]}, {secondary_rgb[2]}, 0.25);
|
|
--secondary-opacity-50: rgba({secondary_rgb[0]}, {secondary_rgb[1]}, {secondary_rgb[2]}, 0.5);
|
|
|
|
/* Chart Colors */
|
|
--chart-color-1: {primary_color};
|
|
--chart-color-2: {secondary_color};
|
|
--chart-color-3: {lighten_color(primary_color, 0.4)};
|
|
--chart-color-4: {lighten_color(secondary_color, 0.4)};
|
|
}}
|
|
"""
|
|
|
|
logger.info(f"[Dynamic Colors] Generated CSS with primary color: {primary_color}")
|
|
logger.info(f"[Dynamic Colors] Cache version: {site_settings.updated_at.timestamp()}")
|
|
|
|
return Response(css, mimetype='text/css')
|
|
|
|
@main_bp.route('/settings/events')
|
|
@login_required
|
|
def events():
|
|
if not current_user.is_admin:
|
|
flash('Only administrators can access event logs.', 'error')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
# 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 = 50
|
|
|
|
# Calculate date range
|
|
end_date = datetime.utcnow()
|
|
if date_range == '24h':
|
|
start_date = end_date - timedelta(days=1)
|
|
elif date_range == '7d':
|
|
start_date = end_date - timedelta(days=7)
|
|
elif date_range == '30d':
|
|
start_date = end_date - timedelta(days=30)
|
|
else:
|
|
start_date = None
|
|
|
|
# Build query
|
|
query = Event.query
|
|
|
|
if event_type:
|
|
query = query.filter_by(event_type=event_type)
|
|
if start_date:
|
|
query = query.filter(Event.timestamp >= start_date)
|
|
if user_id:
|
|
query = query.filter_by(user_id=user_id)
|
|
|
|
# Get total count for pagination
|
|
total_events = query.count()
|
|
total_pages = (total_events + per_page - 1) // per_page
|
|
|
|
# Get paginated events
|
|
events = query.order_by(Event.timestamp.desc()).paginate(page=page, per_page=per_page)
|
|
|
|
# Get all users for filter dropdown
|
|
users = User.query.order_by(User.username).all()
|
|
|
|
# Check if this is an AJAX request
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
|
logger.info(f"Processing AJAX request for events. Found {len(events.items)} events")
|
|
return render_template('settings/tabs/events.html',
|
|
events=events.items,
|
|
total_pages=total_pages,
|
|
current_page=page,
|
|
event_type=event_type,
|
|
date_range=date_range,
|
|
user_id=user_id,
|
|
users=users,
|
|
csrf_token=session.get('csrf_token'))
|
|
|
|
# For full page requests, render the full settings page
|
|
site_settings = SiteSettings.get_settings()
|
|
return render_template('settings/settings.html',
|
|
primary_color=site_settings.primary_color,
|
|
secondary_color=site_settings.secondary_color,
|
|
active_tab='events',
|
|
site_settings=site_settings,
|
|
events=events.items,
|
|
total_pages=total_pages,
|
|
current_page=page,
|
|
users=users,
|
|
csrf_token=session.get('csrf_token'))
|
|
|
|
@main_bp.route('/api/events/<int:event_id>')
|
|
@login_required
|
|
def get_event_details(event_id):
|
|
if not current_user.is_admin:
|
|
return jsonify({'error': 'Unauthorized'}), 403
|
|
|
|
event = Event.query.get_or_404(event_id)
|
|
logger.info(f"Raw event object: {event}")
|
|
logger.info(f"Event details type: {type(event.details)}")
|
|
logger.info(f"Event details value: {event.details}")
|
|
|
|
# Convert details to dict if it's a string
|
|
details = event.details
|
|
if isinstance(details, str):
|
|
try:
|
|
import json
|
|
details = json.loads(details)
|
|
except json.JSONDecodeError:
|
|
details = {'raw_details': details}
|
|
|
|
# Return the raw event data
|
|
response_data = {
|
|
'id': event.id,
|
|
'event_type': event.event_type,
|
|
'timestamp': event.timestamp.isoformat(),
|
|
'user': {
|
|
'id': event.user.id,
|
|
'username': event.user.username,
|
|
'last_name': event.user.last_name
|
|
} if event.user else None,
|
|
'ip_address': event.ip_address,
|
|
'user_agent': event.user_agent,
|
|
'details': details
|
|
}
|
|
|
|
logger.info(f"Sending response: {response_data}")
|
|
return jsonify(response_data) |