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()
|
db.session.commit()
|
||||||
return settings
|
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):
|
class KeyValueSettings(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
key = db.Column(db.String(100), unique=True, nullable=False)
|
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 import Blueprint, jsonify
|
||||||
from flask_login import login_required, current_user
|
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
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -242,3 +242,15 @@ def cleanup_orphaned_records():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({'error': str(e)}), 500
|
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
|
||||||
113
routes/main.py
113
routes/main.py
@@ -1,6 +1,6 @@
|
|||||||
from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session
|
from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session
|
||||||
from flask_login import current_user, login_required
|
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
|
from routes.auth import require_password_change
|
||||||
import os
|
import os
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
@@ -79,6 +79,9 @@ def init_routes(main_bp):
|
|||||||
)
|
)
|
||||||
).order_by(Event.timestamp.desc()).limit(7).all()
|
).order_by(Event.timestamp.desc()).limit(7).all()
|
||||||
|
|
||||||
|
# Get usage stats
|
||||||
|
usage_stats = DocuPulseSettings.get_usage_stats()
|
||||||
|
|
||||||
# Room count and size logic
|
# Room count and size logic
|
||||||
if current_user.is_admin:
|
if current_user.is_admin:
|
||||||
logger.info("Loading admin dashboard...")
|
logger.info("Loading admin dashboard...")
|
||||||
@@ -201,26 +204,16 @@ def init_routes(main_bp):
|
|||||||
RoomFile.uploaded_at.desc()
|
RoomFile.uploaded_at.desc()
|
||||||
).limit(10).all()
|
).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
|
# Format the activity data
|
||||||
formatted_activity = []
|
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:
|
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 = {
|
activity = {
|
||||||
'name': file.name,
|
'name': file.name,
|
||||||
'type': file.type,
|
'type': file.type,
|
||||||
@@ -229,11 +222,12 @@ def init_routes(main_bp):
|
|||||||
'uploaded_at': file.uploaded_at,
|
'uploaded_at': file.uploaded_at,
|
||||||
'is_starred': current_user in file.starred_by,
|
'is_starred': current_user in file.starred_by,
|
||||||
'is_deleted': file.deleted,
|
'is_deleted': file.deleted,
|
||||||
'can_download': perm.can_download if perm else False
|
'can_download': can_download
|
||||||
}
|
}
|
||||||
formatted_activity.append(activity)
|
formatted_activity.append(activity)
|
||||||
formatted_activities = formatted_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(
|
storage_by_type = db.session.query(
|
||||||
case(
|
case(
|
||||||
(RoomFile.name.like('%.%'),
|
(RoomFile.name.like('%.%'),
|
||||||
@@ -249,16 +243,13 @@ def init_routes(main_bp):
|
|||||||
|
|
||||||
# Get trash and starred stats for user's accessible rooms
|
# 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()
|
trash_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).count()
|
||||||
starred_count = RoomFile.query.filter(
|
starred_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.starred_by.contains(current_user)).count()
|
||||||
RoomFile.room_id.in_(room_ids),
|
# Get oldest trash date and total trash size for accessible rooms
|
||||||
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 = 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
|
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
|
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)
|
seven_days_from_now = datetime.utcnow() + timedelta(days=7)
|
||||||
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
||||||
pending_deletion = RoomFile.query.filter(
|
pending_deletion = RoomFile.query.filter(
|
||||||
@@ -268,7 +259,7 @@ def init_routes(main_bp):
|
|||||||
RoomFile.deleted_at > thirty_days_ago - timedelta(days=7)
|
RoomFile.deleted_at > thirty_days_ago - timedelta(days=7)
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
# Get trash file type breakdown for user's rooms
|
# Get trash file type breakdown for accessible rooms
|
||||||
trash_by_type = db.session.query(
|
trash_by_type = db.session.query(
|
||||||
case(
|
case(
|
||||||
(RoomFile.name.like('%.%'),
|
(RoomFile.name.like('%.%'),
|
||||||
@@ -281,44 +272,40 @@ def init_routes(main_bp):
|
|||||||
RoomFile.deleted==True
|
RoomFile.deleted==True
|
||||||
).group_by('extension').all()
|
).group_by('extension').all()
|
||||||
|
|
||||||
# Get conversation statistics
|
# Get conversation stats
|
||||||
if current_user.is_admin:
|
conversation_count = Conversation.query.count()
|
||||||
conversation_count = Conversation.query.count()
|
message_count = Message.query.count()
|
||||||
message_count = Message.query.count()
|
attachment_count = MessageAttachment.query.count()
|
||||||
attachment_count = MessageAttachment.query.count()
|
conversation_total_size = db.session.query(func.sum(MessageAttachment.size)).scalar() or 0
|
||||||
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()
|
||||||
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',
|
return render_template('dashboard/dashboard.html',
|
||||||
recent_contacts=recent_contacts,
|
room_count=room_count,
|
||||||
active_count=active_count,
|
file_count=file_count,
|
||||||
inactive_count=inactive_count,
|
folder_count=folder_count,
|
||||||
room_count=room_count,
|
total_size=total_size,
|
||||||
file_count=file_count,
|
storage_by_type=storage_by_type,
|
||||||
folder_count=folder_count,
|
recent_activities=formatted_activities,
|
||||||
total_size=total_size, # Room storage size
|
trash_count=trash_count,
|
||||||
storage_by_type=storage_by_type,
|
pending_deletion=pending_deletion,
|
||||||
trash_count=trash_count,
|
oldest_trash_date=oldest_trash_date,
|
||||||
starred_count=starred_count,
|
trash_size=trash_size,
|
||||||
oldest_trash_date=oldest_trash_date,
|
trash_by_type=trash_by_type,
|
||||||
trash_size=trash_size,
|
starred_count=starred_count,
|
||||||
pending_deletion=pending_deletion,
|
recent_events=recent_events,
|
||||||
trash_by_type=trash_by_type,
|
recent_contacts=recent_contacts,
|
||||||
recent_events=recent_events,
|
active_count=active_count,
|
||||||
is_admin=current_user.is_admin,
|
inactive_count=inactive_count,
|
||||||
conversation_count=conversation_count,
|
recent_notifications=recent_notifications,
|
||||||
message_count=message_count,
|
unread_notifications=get_unread_count(current_user.id),
|
||||||
attachment_count=attachment_count,
|
conversation_count=conversation_count,
|
||||||
conversation_total_size=conversation_total_size, # Conversation storage size
|
message_count=message_count,
|
||||||
recent_conversations=recent_conversations,
|
attachment_count=attachment_count,
|
||||||
recent_notifications=recent_notifications)
|
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')
|
UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads', 'profile_pics')
|
||||||
if not os.path.exists(UPLOAD_FOLDER):
|
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/recent_activity.html' import recent_activity %}
|
||||||
{% from 'components/conversation_storage.html' import conversation_storage %}
|
{% from 'components/conversation_storage.html' import conversation_storage %}
|
||||||
{% from 'components/notification_overview.html' import notification_overview %}
|
{% from 'components/notification_overview.html' import notification_overview %}
|
||||||
|
{% from 'components/usage_limits.html' import usage_limits %}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.masonry {
|
.masonry {
|
||||||
@@ -54,6 +55,9 @@
|
|||||||
<!-- Storage Section -->
|
<!-- Storage Section -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h2 class="section-title">Storage Overview</h2>
|
<h2 class="section-title">Storage Overview</h2>
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
{{ usage_limits(usage_stats) }}
|
||||||
|
{% endif %}
|
||||||
<div class="masonry">
|
<div class="masonry">
|
||||||
{{ storage_overview(room_count, file_count, folder_count, total_size) }}
|
{{ storage_overview(room_count, file_count, folder_count, total_size) }}
|
||||||
{{ storage_usage(storage_by_type) }}
|
{{ storage_usage(storage_by_type) }}
|
||||||
|
|||||||
Reference in New Issue
Block a user