added usage limit visuals and DB
This commit is contained in:
Binary file not shown.
36
migrations/versions/add_docupulse_settings_table.py
Normal file
36
migrations/versions/add_docupulse_settings_table.py
Normal file
@@ -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')
|
||||
40
models.py
40
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)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -242,3 +242,15 @@ 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
|
||||
@@ -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('%.%'),
|
||||
@@ -281,44 +272,40 @@ def init_routes(main_bp):
|
||||
RoomFile.deleted==True
|
||||
).group_by('extension').all()
|
||||
|
||||
# Get conversation statistics
|
||||
if current_user.is_admin:
|
||||
# Get conversation stats
|
||||
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
|
||||
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()
|
||||
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
|
||||
total_size=total_size,
|
||||
storage_by_type=storage_by_type,
|
||||
recent_activities=formatted_activities,
|
||||
trash_count=trash_count,
|
||||
starred_count=starred_count,
|
||||
pending_deletion=pending_deletion,
|
||||
oldest_trash_date=oldest_trash_date,
|
||||
trash_size=trash_size,
|
||||
pending_deletion=pending_deletion,
|
||||
trash_by_type=trash_by_type,
|
||||
starred_count=starred_count,
|
||||
recent_events=recent_events,
|
||||
is_admin=current_user.is_admin,
|
||||
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, # Conversation storage size
|
||||
conversation_total_size=conversation_total_size,
|
||||
recent_conversations=recent_conversations,
|
||||
recent_notifications=recent_notifications)
|
||||
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):
|
||||
|
||||
67
templates/components/usage_limits.html
Normal file
67
templates/components/usage_limits.html
Normal file
@@ -0,0 +1,67 @@
|
||||
{% from 'common/macros.html' import format_size %}
|
||||
|
||||
{% macro usage_limits(usage_stats) %}
|
||||
<div class="masonry-card" style="width: 100%;">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="card-title mb-0"><i class="fas fa-chart-line me-2"></i>Usage Limits</h5>
|
||||
</div>
|
||||
|
||||
<!-- Rooms Progress -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-door-open me-2 icon-primary"></i>
|
||||
<span class="text-muted">Rooms:</span>
|
||||
</div>
|
||||
<div class="fw-bold text-primary">{{ usage_stats.current_rooms }} / {{ usage_stats.max_rooms }}</div>
|
||||
</div>
|
||||
<div class="progress" style="height: 8px;">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
style="width: {{ usage_stats.rooms_percentage }}%; background-color: var(--secondary-color);"
|
||||
aria-valuenow="{{ usage_stats.rooms_percentage }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conversations Progress -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-comments me-2 icon-primary"></i>
|
||||
<span class="text-muted">Conversations:</span>
|
||||
</div>
|
||||
<div class="fw-bold text-primary">{{ usage_stats.current_conversations }} / {{ usage_stats.max_conversations }}</div>
|
||||
</div>
|
||||
<div class="progress" style="height: 8px;">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
style="width: {{ usage_stats.conversations_percentage }}%; background-color: var(--secondary-color);"
|
||||
aria-valuenow="{{ usage_stats.conversations_percentage }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Storage Progress -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-hdd me-2 icon-primary"></i>
|
||||
<span class="text-muted">Storage:</span>
|
||||
</div>
|
||||
<div class="fw-bold text-primary">{{ format_size(usage_stats.current_storage) }} / {{ format_size(usage_stats.max_storage) }}</div>
|
||||
</div>
|
||||
<div class="progress" style="height: 8px;">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
style="width: {{ usage_stats.storage_percentage }}%; background-color: var(--secondary-color);"
|
||||
aria-valuenow="{{ usage_stats.storage_percentage }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -26,6 +26,7 @@
|
||||
{% from 'components/recent_activity.html' import recent_activity %}
|
||||
{% from 'components/conversation_storage.html' import conversation_storage %}
|
||||
{% from 'components/notification_overview.html' import notification_overview %}
|
||||
{% from 'components/usage_limits.html' import usage_limits %}
|
||||
|
||||
<style>
|
||||
.masonry {
|
||||
@@ -54,6 +55,9 @@
|
||||
<!-- Storage Section -->
|
||||
<div class="mb-4">
|
||||
<h2 class="section-title">Storage Overview</h2>
|
||||
{% if current_user.is_admin %}
|
||||
{{ usage_limits(usage_stats) }}
|
||||
{% endif %}
|
||||
<div class="masonry">
|
||||
{{ storage_overview(room_count, file_count, folder_count, total_size) }}
|
||||
{{ storage_usage(storage_by_type) }}
|
||||
|
||||
Reference in New Issue
Block a user