Files
docupulse/routes/main.py
2025-05-27 13:30:44 +02:00

516 lines
24 KiB
Python

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
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('/')
def home():
if not current_user.is_authenticated:
return redirect(url_for('auth.login'))
return redirect(url_for('main.dashboard'))
@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.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
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('/conversations')
@login_required
def conversations():
return redirect(url_for('conversations.conversations'))
@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,
site_settings=site_settings)
@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('/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():
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')