diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index ca58e88..a56e18f 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/migrations/versions/add_docupulse_settings_table.py b/migrations/versions/add_docupulse_settings_table.py new file mode 100644 index 0000000..98fdf7a --- /dev/null +++ b/migrations/versions/add_docupulse_settings_table.py @@ -0,0 +1,36 @@ +"""add docupulse settings table + +Revision ID: add_docupulse_settings +Revises: add_notifs_table +Create Date: 2024-03-19 10:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from datetime import datetime + +# revision identifiers, used by Alembic. +revision = 'add_docupulse_settings' +down_revision = 'add_notifs_table' +branch_labels = None +depends_on = None + +def upgrade(): + # Create docupulse_settings table + op.create_table('docupulse_settings', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('max_rooms', sa.Integer(), nullable=False, server_default='10'), + sa.Column('max_conversations', sa.Integer(), nullable=False, server_default='10'), + sa.Column('max_storage', sa.Integer(), nullable=False, server_default='10737418240'), # 10GB in bytes + sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), + sa.PrimaryKeyConstraint('id') + ) + + # Insert default settings + op.execute(""" + INSERT INTO docupulse_settings (id, max_rooms, max_conversations, max_storage, updated_at) + VALUES (1, 10, 10, 10737418240, CURRENT_TIMESTAMP) + """) + +def downgrade(): + op.drop_table('docupulse_settings') \ No newline at end of file diff --git a/models.py b/models.py index 576a659..40383c9 100644 --- a/models.py +++ b/models.py @@ -165,6 +165,46 @@ class SiteSettings(db.Model): db.session.commit() return settings +class DocuPulseSettings(db.Model): + __tablename__ = 'docupulse_settings' + id = db.Column(db.Integer, primary_key=True) + max_rooms = db.Column(db.Integer, default=10) + max_conversations = db.Column(db.Integer, default=10) + max_storage = db.Column(db.Integer, default=10737418240) # 10GB in bytes + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + @classmethod + def get_settings(cls): + settings = cls.query.first() + if not settings: + settings = cls( + max_rooms=10, + max_conversations=10, + max_storage=10737418240 # 10GB in bytes + ) + db.session.add(settings) + db.session.commit() + return settings + + @classmethod + def get_usage_stats(cls): + settings = cls.get_settings() + total_rooms = Room.query.count() + total_conversations = Conversation.query.count() + total_storage = db.session.query(db.func.sum(RoomFile.size)).filter(RoomFile.deleted == False).scalar() or 0 + + return { + 'max_rooms': settings.max_rooms, + 'max_conversations': settings.max_conversations, + 'max_storage': settings.max_storage, + 'current_rooms': total_rooms, + 'current_conversations': total_conversations, + 'current_storage': total_storage, + 'rooms_percentage': (total_rooms / settings.max_rooms) * 100 if settings.max_rooms > 0 else 0, + 'conversations_percentage': (total_conversations / settings.max_conversations) * 100 if settings.max_conversations > 0 else 0, + 'storage_percentage': (total_storage / settings.max_storage) * 100 if settings.max_storage > 0 else 0 + } + class KeyValueSettings(db.Model): id = db.Column(db.Integer, primary_key=True) key = db.Column(db.String(100), unique=True, nullable=False) diff --git a/routes/__pycache__/admin.cpython-313.pyc b/routes/__pycache__/admin.cpython-313.pyc index b5da3be..3304fd8 100644 Binary files a/routes/__pycache__/admin.cpython-313.pyc and b/routes/__pycache__/admin.cpython-313.pyc differ diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 5c83e0a..c7affbd 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/admin.py b/routes/admin.py index 2ef39f2..7f34e49 100644 --- a/routes/admin.py +++ b/routes/admin.py @@ -1,6 +1,6 @@ from flask import Blueprint, jsonify from flask_login import login_required, current_user -from models import db, Room, RoomFile, User +from models import db, Room, RoomFile, User, DocuPulseSettings import os from datetime import datetime @@ -241,4 +241,16 @@ def cleanup_orphaned_records(): }) except Exception as e: db.session.rollback() + return jsonify({'error': str(e)}), 500 + +@admin.route('/api/admin/usage-stats', methods=['GET']) +@login_required +def get_usage_stats(): + if not current_user.is_admin: + return jsonify({'error': 'Unauthorized'}), 403 + + try: + stats = DocuPulseSettings.get_usage_stats() + return jsonify(stats) + except Exception as e: return jsonify({'error': str(e)}), 500 \ No newline at end of file diff --git a/routes/main.py b/routes/main.py index fc69fbf..cb7d5d1 100644 --- a/routes/main.py +++ b/routes/main.py @@ -1,6 +1,6 @@ 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, Conversation, Message, MessageAttachment, Notif, EmailTemplate, Mail, KeyValueSettings +from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif, EmailTemplate, Mail, KeyValueSettings, DocuPulseSettings from routes.auth import require_password_change import os from werkzeug.utils import secure_filename @@ -79,6 +79,9 @@ def init_routes(main_bp): ) ).order_by(Event.timestamp.desc()).limit(7).all() + # Get usage stats + usage_stats = DocuPulseSettings.get_usage_stats() + # Room count and size logic if current_user.is_admin: logger.info("Loading admin dashboard...") @@ -201,26 +204,16 @@ def init_routes(main_bp): 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) + # Check if user has download permission + permission = RoomMemberPermission.query.filter_by( + room_id=room.id, + user_id=current_user.id + ).first() + can_download = permission and permission.can_download if permission else False + activity = { 'name': file.name, 'type': file.type, @@ -229,11 +222,12 @@ def init_routes(main_bp): '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 + 'can_download': can_download } formatted_activity.append(activity) formatted_activities = formatted_activity - # Get storage usage by file type for accessible rooms including trash + + # Get storage usage by file type for accessible rooms storage_by_type = db.session.query( case( (RoomFile.name.like('%.%'), @@ -249,16 +243,13 @@ def init_routes(main_bp): # 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 + 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 accessible 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 + # Get files that will be deleted in next 7 days for accessible rooms seven_days_from_now = datetime.utcnow() + timedelta(days=7) thirty_days_ago = datetime.utcnow() - timedelta(days=30) pending_deletion = RoomFile.query.filter( @@ -268,7 +259,7 @@ def init_routes(main_bp): RoomFile.deleted_at > thirty_days_ago - timedelta(days=7) ).count() - # Get trash file type breakdown for user's rooms + # Get trash file type breakdown for accessible rooms trash_by_type = db.session.query( case( (RoomFile.name.like('%.%'), @@ -280,45 +271,41 @@ def init_routes(main_bp): RoomFile.room_id.in_(room_ids), RoomFile.deleted==True ).group_by('extension').all() - - # Get conversation statistics - if current_user.is_admin: - conversation_count = Conversation.query.count() - message_count = Message.query.count() - attachment_count = MessageAttachment.query.count() - conversation_total_size = db.session.query(db.func.sum(MessageAttachment.size)).scalar() or 0 - recent_conversations = Conversation.query.order_by(Conversation.created_at.desc()).limit(5).all() - else: - # For regular users, only show their conversations - conversation_count = Conversation.query.filter(Conversation.members.any(id=current_user.id)).count() - message_count = Message.query.join(Conversation).filter(Conversation.members.any(id=current_user.id)).count() - attachment_count = MessageAttachment.query.join(Message).join(Conversation).filter(Conversation.members.any(id=current_user.id)).count() - conversation_total_size = db.session.query(db.func.sum(MessageAttachment.size)).join(Message).join(Conversation).filter(Conversation.members.any(id=current_user.id)).scalar() or 0 - recent_conversations = Conversation.query.filter(Conversation.members.any(id=current_user.id)).order_by(Conversation.created_at.desc()).limit(5).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, # Room storage 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, - recent_events=recent_events, - is_admin=current_user.is_admin, - conversation_count=conversation_count, - message_count=message_count, - attachment_count=attachment_count, - conversation_total_size=conversation_total_size, # Conversation storage size - recent_conversations=recent_conversations, - recent_notifications=recent_notifications) + # Get conversation stats + conversation_count = Conversation.query.count() + message_count = Message.query.count() + attachment_count = MessageAttachment.query.count() + conversation_total_size = db.session.query(func.sum(MessageAttachment.size)).scalar() or 0 + recent_conversations = Conversation.query.order_by(Conversation.created_at.desc()).limit(5).all() + + return render_template('dashboard/dashboard.html', + room_count=room_count, + file_count=file_count, + folder_count=folder_count, + total_size=total_size, + storage_by_type=storage_by_type, + recent_activities=formatted_activities, + trash_count=trash_count, + pending_deletion=pending_deletion, + oldest_trash_date=oldest_trash_date, + trash_size=trash_size, + trash_by_type=trash_by_type, + starred_count=starred_count, + recent_events=recent_events, + recent_contacts=recent_contacts, + active_count=active_count, + inactive_count=inactive_count, + recent_notifications=recent_notifications, + unread_notifications=get_unread_count(current_user.id), + conversation_count=conversation_count, + message_count=message_count, + attachment_count=attachment_count, + conversation_total_size=conversation_total_size, + recent_conversations=recent_conversations, + usage_stats=usage_stats, + is_admin=current_user.is_admin + ) UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads', 'profile_pics') if not os.path.exists(UPLOAD_FOLDER): diff --git a/templates/components/usage_limits.html b/templates/components/usage_limits.html new file mode 100644 index 0000000..4c890db --- /dev/null +++ b/templates/components/usage_limits.html @@ -0,0 +1,67 @@ +{% from 'common/macros.html' import format_size %} + +{% macro usage_limits(usage_stats) %} +