from flask import render_template, Blueprint, redirect, url_for, request, flash, Response from flask_login import current_user, login_required from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings 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 # 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.route('/') def home(): if current_user.is_authenticated: return redirect(url_for('main.dashboard')) return render_template('home.html') @main_bp.route('/dashboard') @login_required 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.split_part(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 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.split_part(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.split_part(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 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.split_part(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 def profile(): if request.method == 'POST': # Handle profile picture removal if 'remove_picture' in request.form: 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') # 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: 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') # Handle password change if provided new_password = request.form.get('new_password') confirm_password = request.form.get('confirm_password') if new_password: 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') try: db.session.commit() flash('Profile updated successfully!', 'success') except Exception as e: db.session.rollback() flash('An error occurred while updating your profile.', 'error') return redirect(url_for('main.profile')) return render_template('profile/profile.html') @main_bp.route('/starred') @login_required def starred(): return render_template('starred/starred.html') @main_bp.route('/trash') @login_required def trash(): return render_template('trash/trash.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') return render_template('settings/settings.html', primary_color=site_settings.primary_color, secondary_color=site_settings.secondary_color, active_tab=active_tab) @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')) primary_color = request.form.get('primary_color') secondary_color = request.form.get('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() flash('Color settings updated successfully!', 'success') except Exception as 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('/dynamic-colors.css') def dynamic_colors(): site_settings = SiteSettings.get_settings() # Calculate derived colors 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): rgb = hex_to_rgb(hex_color) return rgb_to_hex(tuple(min(255, int(c + (255 - c) * amount)) for c in rgb)) # Calculate all color variants primary_light = lighten_color(site_settings.primary_color, 0.15) primary_bg_light = lighten_color(site_settings.primary_color, 0.9) primary_opacity_15 = site_settings.primary_color + '26' secondary_light = lighten_color(site_settings.secondary_color, 0.15) secondary_bg_light = lighten_color(site_settings.secondary_color, 0.9) secondary_opacity_15 = site_settings.secondary_color + '26' # Calculate chart colors primary_chart_light = lighten_color(site_settings.primary_color, 0.2) primary_chart_lighter = lighten_color(site_settings.primary_color, 0.4) primary_chart_lightest = lighten_color(site_settings.primary_color, 0.6) primary_chart_pale = lighten_color(site_settings.primary_color, 0.8) secondary_chart_light = lighten_color(site_settings.secondary_color, 0.2) secondary_chart_lighter = lighten_color(site_settings.secondary_color, 0.4) secondary_chart_lightest = lighten_color(site_settings.secondary_color, 0.6) secondary_chart_pale = lighten_color(site_settings.secondary_color, 0.8) css = f""" :root {{ /* Primary Colors */ --primary-color: {site_settings.primary_color}; --primary-light: {primary_light}; --primary-bg-light: {primary_bg_light}; --primary-opacity-15: {primary_opacity_15}; /* Secondary Colors */ --secondary-color: {site_settings.secondary_color}; --secondary-light: {secondary_light}; --secondary-bg-light: {secondary_bg_light}; --secondary-opacity-15: {secondary_opacity_15}; /* Chart Colors */ --chart-primary: {site_settings.primary_color}; --chart-secondary: {site_settings.secondary_color}; --chart-warning: #ffd700; /* Primary Chart Colors */ --chart-primary-light: {primary_chart_light}; --chart-primary-lighter: {primary_chart_lighter}; --chart-primary-lightest: {primary_chart_lightest}; --chart-primary-pale: {primary_chart_pale}; /* Secondary Chart Colors */ --chart-secondary-light: {secondary_chart_light}; --chart-secondary-lighter: {secondary_chart_lighter}; --chart-secondary-lightest: {secondary_chart_lightest}; --chart-secondary-pale: {secondary_chart_pale}; }} """ return Response(css, mimetype='text/css')