feat: Enhance admin dashboard with user, team, and event statistics, and system health monitoring

This commit is contained in:
Christian Krakau-Louis
2025-04-16 14:23:40 +02:00
parent 61a0a0a6a6
commit d48c16119a
6 changed files with 821 additions and 2 deletions
+161
View File
@@ -0,0 +1,161 @@
# LeagueLedger - TODO List
This document outlines upcoming tasks and improvements for the LeagueLedger application.
## Admin Dashboard Enhancements
- [ ] Add a settings section to manage configuration values
- [ ] Create a system to invite other admins
- [ ] Develop a more robust user management interface
- [ ] Add user roles and permissions beyond just admin/non-admin
- [x] Create a dashboard overview with system statistics
- [ ] Add bulk operations for users and teams
- [x] Basic CRUD operations for database models
- [x] Add dashboard with system activity metrics and stats
- [ ] Improve record filtering and searching capabilities
- [ ] Add relationship handling in edit forms
- [ ] Implement form validation with meaningful error messages
- [ ] Add user action audit logging
- [ ] Create specialized interfaces for common admin tasks
- [ ] Add admin reports generation functionality
- [ ] Implement system backup functionality from admin panel
## Admin Panel Specific Features
- [ ] **User Management**
- [ ] Add specialized user verification controls
- [ ] Implement password reset functionality
- [ ] Add user role assignment interface
- [ ] Create user activity logs viewer
- [ ] **Team Management**
- [ ] Implement team member management interface
- [ ] Add team ownership transfer functionality
- [ ] Create team join request approval workflow
- [ ] Add team archiving functionality
- [ ] **QR Code Management**
- [ ] Create QR code batch generation tool
- [ ] Implement QR code printing functionality
- [ ] Add QR code usage tracking dashboard
- [ ] Create QR code invalidation controls
- [ ] **Event Management**
- [ ] Add event creation wizard
- [ ] Implement event QR set assignment interface
- [ ] Create event attendance tracking
- [ ] Add event results display
- [ ] **System Configuration**
- [ ] Implement email settings management
- [ ] Add OAuth provider configuration interface
- [ ] Create appearance/branding settings
- [ ] Add general system settings controls
## User Management & Profile Features
- [ ] **Profile Management**
- [ ] Update profile picture functionality
- [ ] Change username capability
- [ ] Account deletion process
- [ ] Profile privacy settings
- [ ] Social media integration
## Environment Variables & Configuration
- [ ] Implement a database-backed settings storage system
- [ ] Create a UI for managing environment variables in the admin panel
- [ ] Add configuration for email templates
- [ ] Add configuration for OAuth providers
- [ ] Create backup/export functionality for configuration
- [ ] Add validation for configuration values
## Security Improvements
- [ ] Add logging for administrative actions
- [ ] Implement IP-based access restrictions for the setup page
- [ ] Add two-factor authentication for admin users
- [ ] Review password security policies
- [ ] Implement rate limiting for login attempts
- [ ] Set up regular security audits
- [ ] Improve CSRF protection
## User Experience Enhancements
- [ ] Create a guided tour for new administrators
- [ ] Add more visual feedback for administrative actions
- [ ] Improve mobile responsiveness of admin interfaces
- [ ] Implement notifications for important system events
- [ ] Add keyboard shortcuts for common actions
- [ ] Create a dark mode theme option
- [ ] Improve accessibility of the application
## System Health Monitoring
- [ ] Add a status dashboard for admins
- [ ] Implement database maintenance tools
- [ ] Create backup/restore functionality
- [ ] Set up regular health checks
- [ ] Add monitoring for application errors
- [ ] Create performance metrics tracking
- [ ] Set up automated alerts for system issues
## Team Management
- [ ] Improve team join request workflow
- [ ] Add team hierarchy options
- [ ] Create team member roles beyond admin/member
- [ ] Add team activity logs
- [ ] Implement team communication tools
- [ ] Add team profile customization options
- [ ] Create team achievement badges
## QR Code System
- [ ] Add support for dynamic QR codes
- [ ] Improve QR code generation options
- [ ] Add QR code statistics and usage tracking
- [ ] Create a QR code management dashboard
- [ ] Support for bulk QR code generation
- [ ] Add QR code categories and tagging
- [ ] Implement QR code expiration and scheduling
## Documentation
- [ ] Update API documentation
- [ ] Create user guides for different roles
- [ ] Document database schema and relationships
- [ ] Add developer onboarding documentation
- [ ] Create deployment guides for different environments
- [ ] Document system architecture and design decisions
- [ ] Add troubleshooting guides
## Testing
- [ ] Expand automated test coverage
- [ ] Create end-to-end testing workflows
- [ ] Add performance benchmarking tests
- [ ] Implement load testing for high-traffic scenarios
- [ ] Set up continuous integration testing
- [ ] Create testing documentation
- [ ] Add visual regression testing
## Internationalization
- [ ] Complete translation of all UI elements
- [ ] Add support for RTL languages
- [ ] Implement locale-specific formatting
- [ ] Add language selection UI
- [ ] Create translation contribution guidelines
- [ ] Support for multiple time zones
- [ ] Add regional customization options
## Infrastructure & Deployment
- [ ] Optimize Docker configuration
- [ ] Set up automated deployments
- [ ] Implement proper staging environment
- [ ] Create database migration tools
- [ ] Add support for clustering/high availability
- [ ] Implement CDN for static assets
- [ ] Create backup and disaster recovery procedures
+289
View File
@@ -0,0 +1,289 @@
{% extends "base.html" %}
{% block title %}Admin Dashboard - LeagueLedger{% endblock %}
{% block extra_head %}
<!-- Chart.js for statistics visualization -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4">
<h1 class="text-3xl font-garamond text-irish-green font-bold mb-6">Admin Dashboard</h1>
<!-- Dashboard Overview -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<!-- User Stats Card -->
<div class="bg-white rounded-lg shadow-md p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-irish-green">Users</h3>
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full">Total: {{ user_stats.total_users }}</span>
</div>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-500">Active:</span>
<span class="font-medium">{{ user_stats.active_users }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Verified:</span>
<span class="font-medium">{{ user_stats.verified_users }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Admins:</span>
<span class="font-medium">{{ user_stats.admin_users }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">New (30d):</span>
<span class="font-medium">{{ user_stats.new_registrations_30d }}</span>
</div>
<div class="mt-4">
<a href="/admin/user" class="text-irish-green hover:underline text-sm flex items-center">
<i class="fas fa-users mr-1"></i> Manage Users
</a>
</div>
</div>
</div>
<!-- Team Stats Card -->
<div class="bg-white rounded-lg shadow-md p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-irish-green">Teams</h3>
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full">Total: {{ team_stats.total_teams }}</span>
</div>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-500">Active:</span>
<span class="font-medium">{{ team_stats.active_teams }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Public:</span>
<span class="font-medium">{{ team_stats.public_teams }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Private:</span>
<span class="font-medium">{{ team_stats.total_teams - team_stats.public_teams }}</span>
</div>
<div class="mt-4">
<a href="/admin/team" class="text-irish-green hover:underline text-sm flex items-center">
<i class="fas fa-arrow-right mr-1"></i> Manage Teams
</a>
</div>
</div>
</div>
<!-- Event Stats Card -->
<div class="bg-white rounded-lg shadow-md p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-irish-green">Events</h3>
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full">Total: {{ event_stats.total_events }}</span>
</div>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-500">Past:</span>
<span class="font-medium">{{ event_stats.past_events }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Upcoming:</span>
<span class="font-medium">{{ event_stats.upcoming_events_count }}</span>
</div>
{% if event_stats.upcoming_events %}
<div class="text-gray-500 text-sm mt-2">Next event:</div>
<div class="text-sm font-medium">{{ event_stats.upcoming_events[0].name }}</div>
<div class="text-xs text-gray-500">{{ event_stats.upcoming_events[0].event_date.strftime('%Y-%m-%d') }}</div>
{% endif %}
<div class="mt-2">
<a href="/admin/event" class="text-irish-green hover:underline text-sm flex items-center">
<i class="fas fa-arrow-right mr-1"></i> Manage Events
</a>
</div>
</div>
</div>
<!-- System Health Card -->
<div class="bg-white rounded-lg shadow-md p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-irish-green">System</h3>
<span class="bg-{{ 'green' if system_health.database_status == 'online' else 'red' }}-100 text-{{ 'green' if system_health.database_status == 'online' else 'red' }}-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
{{ system_health.database_status }}
</span>
</div>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-500">Uptime:</span>
<span class="font-medium text-sm">{{ system_health.uptime }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Errors:</span>
<span class="font-medium">{{ system_health.recent_errors|length }}</span>
</div>
<div class="mt-4">
<a href="#" class="text-irish-green hover:underline text-sm flex items-center">
<i class="fas fa-cog mr-1"></i> System Settings
</a>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- User Growth Chart -->
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-irish-green mb-4">User Growth</h3>
<div class="h-64">
<canvas id="userGrowthChart"></canvas>
</div>
</div>
<!-- Team Distribution Chart -->
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-irish-green mb-4">Team Sizes</h3>
<div class="h-64">
<canvas id="teamSizeChart"></canvas>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Upcoming Events Table -->
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-irish-green mb-4">Upcoming Events</h3>
{% if event_stats.upcoming_events %}
<div class="overflow-x-auto">
<table class="min-w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Location</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for event in event_stats.upcoming_events %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm">{{ event.name }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">{{ event.event_date.strftime('%Y-%m-%d') }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">{{ event.location }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4 text-gray-500">No upcoming events</div>
{% endif %}
<div class="mt-4 flex justify-end">
<a href="/admin/event/new" class="bg-irish-green hover:bg-opacity-90 text-white px-4 py-2 rounded-md text-sm">
<i class="fas fa-plus mr-2"></i>Add Event
</a>
</div>
</div>
<!-- Top Events Attendance -->
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-irish-green mb-4">Top Events by Attendance</h3>
{% if event_stats.attendance_rates %}
<div class="overflow-x-auto">
<table class="min-w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Event</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Attendees</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for event in event_stats.attendance_rates %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm">{{ event.event_name }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">{{ event.attendee_count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4 text-gray-500">No attendance data available</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// User growth chart with real data from backend
const userGrowthCtx = document.getElementById('userGrowthChart').getContext('2d');
const userGrowthChart = new Chart(userGrowthCtx, {
type: 'line',
data: {
labels: {{ user_stats.month_labels | tojson }},
datasets: [{
label: 'New Users',
data: {{ user_stats.monthly_registrations | tojson }},
backgroundColor: 'rgba(0, 104, 55, 0.1)',
borderColor: 'rgba(0, 104, 55, 1)',
borderWidth: 2,
tension: 0.3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
}
}
});
// Team size distribution chart
const teamData = [0, 0, 0, 0, 0, 0, 0]; // 0 to 6+ members
{% if team_stats.team_distribution %}
{% for item in team_stats.team_distribution %}
{% if item.member_count < 6 %}
teamData[{{ item.member_count }}] = {{ item.count }};
{% else %}
teamData[6] += {{ item.count }};
{% endif %}
{% endfor %}
{% endif %}
const teamSizeCtx = document.getElementById('teamSizeChart').getContext('2d');
const teamSizeChart = new Chart(teamSizeCtx, {
type: 'bar',
data: {
labels: ['0', '1', '2', '3', '4', '5', '6+'],
datasets: [{
label: 'Teams by Member Count',
data: teamData,
backgroundColor: 'rgba(255, 180, 0, 0.7)',
borderColor: 'rgba(255, 180, 0, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
},
x: {
title: {
display: true,
text: 'Number of Members'
}
}
}
}
});
});
</script>
{% endblock %}
+9 -1
View File
@@ -2,7 +2,12 @@
{% block content %} {% block content %}
<div class="max-w-5xl mx-auto"> <div class="max-w-5xl mx-auto">
<div class="bg-white rounded-lg shadow-md p-6"> <div class="bg-white rounded-lg shadow-md p-6">
<h1 class="text-2xl font-bold text-irish-green mb-6">Admin Dashboard</h1> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-irish-green">Admin Panel</h1>
<a href="/admin/dashboard" class="bg-golden-ale text-black-stout px-4 py-2 rounded-md hover:bg-opacity-90 transition flex items-center">
<i class="fas fa-chart-line mr-2"></i> Statistics Dashboard
</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for model_key, display_name in models %} {% for model_key, display_name in models %}
@@ -23,6 +28,9 @@
<div class="mt-10 pt-6 border-t border-gray-200"> <div class="mt-10 pt-6 border-t border-gray-200">
<h2 class="text-xl font-bold text-irish-green mb-4">Quick Actions</h2> <h2 class="text-xl font-bold text-irish-green mb-4">Quick Actions</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<a href="/admin/dashboard" class="bg-golden-ale text-black-stout px-4 py-3 rounded-md hover:bg-opacity-90 transition flex items-center justify-center">
<i class="fas fa-chart-line mr-2"></i> Statistics Dashboard
</a>
<a href="/qr" class="bg-white border border-irish-green text-irish-green hover:bg-irish-green hover:text-white px-4 py-3 rounded-md transition flex items-center"> <a href="/qr" class="bg-white border border-irish-green text-irish-green hover:bg-irish-green hover:text-white px-4 py-3 rounded-md transition flex items-center">
<i class="fas fa-qrcode mr-2"></i> QR Code Dashboard <i class="fas fa-qrcode mr-2"></i> QR Code Dashboard
</a> </a>
+190 -1
View File
@@ -5,10 +5,15 @@ Admin interface for managing database records.
from fastapi import APIRouter, Depends, Request, Form, HTTPException, Query from fastapi import APIRouter, Depends, Request, Form, HTTPException, Query
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import inspect from sqlalchemy import inspect, func, desc, text
import json import json
from typing import Dict, Any, List, Type, Optional from typing import Dict, Any, List, Type, Optional
import inspect as py_inspect import inspect as py_inspect
from datetime import datetime, timedelta
import time
import os
import psutil
from dateutil.relativedelta import relativedelta
from ..db import SessionLocal, Base from ..db import SessionLocal, Base
from ..models import ( from ..models import (
@@ -42,6 +47,168 @@ def get_db():
finally: finally:
db.close() db.close()
# Get user statistics for the dashboard
def get_user_statistics(db: Session) -> Dict[str, Any]:
"""Get user statistics for the admin dashboard."""
stats = {}
# Total users
stats["total_users"] = db.query(User).count()
# Active users (not disabled)
stats["active_users"] = db.query(User).filter(User.is_active == True).count()
# Verified users
stats["verified_users"] = db.query(User).filter(User.is_verified == True).count()
# Admin users
stats["admin_users"] = db.query(User).filter(User.is_admin == True).count()
# New registrations in the last 30 days
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
stats["new_registrations_30d"] = db.query(User).filter(
User.created_at >= thirty_days_ago
).count()
# Monthly user registration data for the chart (last 6 months)
monthly_data = []
month_labels = []
# Get the current month and year
current_date = datetime.now()
# Loop through the last 6 months
for i in range(5, -1, -1):
# Calculate month and year for this data point
month_date = current_date - relativedelta(months=i)
start_of_month = datetime(month_date.year, month_date.month, 1)
# For the current month, only count until today
if i == 0:
end_of_month = current_date
else:
# Calculate the end of the month
if month_date.month == 12:
end_of_month = datetime(month_date.year + 1, 1, 1) - timedelta(days=1)
else:
end_of_month = datetime(month_date.year, month_date.month + 1, 1) - timedelta(days=1)
# Format month as abbreviated month name
month_name = month_date.strftime('%b')
month_labels.append(month_name)
# Count users registered in this month
monthly_count = db.query(User).filter(
User.created_at >= start_of_month,
User.created_at <= end_of_month
).count()
monthly_data.append(monthly_count)
# Add the data to the stats
stats["monthly_registrations"] = monthly_data
stats["month_labels"] = month_labels
return stats
# Get team statistics for the dashboard
def get_team_statistics(db: Session) -> Dict[str, Any]:
"""Get team statistics for the admin dashboard."""
stats = {}
# Total teams
stats["total_teams"] = db.query(Team).count()
# Active teams
stats["active_teams"] = db.query(Team).filter(Team.is_active == True).count()
# Public teams
stats["public_teams"] = db.query(Team).filter(Team.is_public == True).count()
# Team size distribution - teams grouped by member count
# Modified query to correctly count team members and group by team
team_sizes = db.query(
TeamMembership.team_id,
func.count(TeamMembership.user_id).label('member_count')
).group_by(TeamMembership.team_id).subquery()
# Now we can query the distribution from the subquery
team_distribution = db.query(
team_sizes.c.member_count,
func.count().label('count')
).group_by(team_sizes.c.member_count).all()
# Convert to a list of dictionaries for easier handling in the template
team_dist_list = [{"member_count": size[0], "count": size[1]} for size in team_distribution]
stats["team_distribution"] = team_dist_list
return stats
# Get event statistics for the dashboard
def get_event_statistics(db: Session) -> Dict[str, Any]:
"""Get event statistics for the admin dashboard."""
stats = {}
# Total events
stats["total_events"] = db.query(Event).count()
# Past events
today = datetime.now().date()
stats["past_events"] = db.query(Event).filter(Event.event_date < today).count()
# Upcoming events
stats["upcoming_events_count"] = db.query(Event).filter(Event.event_date >= today).count()
# List of upcoming events
upcoming_events = db.query(Event).filter(
Event.event_date >= today
).order_by(Event.event_date).limit(5).all()
stats["upcoming_events"] = upcoming_events
# Events with highest attendance
attendance_rates = db.query(
Event.id.label('event_id'),
Event.name.label('event_name'),
func.count(EventAttendee.id).label('attendee_count')
).join(EventAttendee).group_by(Event.id, Event.name).order_by(
desc('attendee_count')
).limit(5).all()
stats["attendance_rates"] = attendance_rates
return stats
# Get system health information
def get_system_health(db: Session) -> Dict[str, Any]:
"""Get system health information for the admin dashboard."""
health_info = {}
# Database status
try:
result = db.execute(text("SELECT 'online' as status")).fetchall()
health_info["database_status"] = "online" if result else "offline"
except Exception as e:
health_info["database_status"] = "error"
health_info["database_error"] = str(e)
# System uptime
try:
uptime_seconds = time.time() - psutil.boot_time()
days, remainder = divmod(uptime_seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
health_info["uptime"] = f"{int(days)} days, {int(hours)} hours, {int(minutes)} minutes"
except Exception:
health_info["uptime"] = "Unknown"
# Recent errors (would be fetched from a logging system in production)
# For this example, we'll return a placeholder
health_info["recent_errors"] = []
return health_info
def get_model_info(model_class: Type[Base]) -> Dict[str, Dict[str, Any]]: def get_model_info(model_class: Type[Base]) -> Dict[str, Dict[str, Any]]:
"""Get column information for a model.""" """Get column information for a model."""
mapper = inspect(model_class) mapper = inspect(model_class)
@@ -78,6 +245,28 @@ def get_relationships(model_class: Type[Base]) -> Dict[str, str]:
@require_admin(redirect_url="/auth/login?next=/admin/") @require_admin(redirect_url="/auth/login?next=/admin/")
async def admin_home(request: Request, db: Session = Depends(get_db)): async def admin_home(request: Request, db: Session = Depends(get_db)):
"""Admin dashboard home.""" """Admin dashboard home."""
# Get statistics
user_stats = get_user_statistics(db)
team_stats = get_team_statistics(db)
event_stats = get_event_statistics(db)
system_health = get_system_health(db)
return templates.TemplateResponse(
"admin/dashboard.html",
{
"request": request,
"user": request.user,
"user_stats": user_stats,
"team_stats": team_stats,
"event_stats": event_stats,
"system_health": system_health
}
)
@router.get("/models", response_class=HTMLResponse)
@require_admin(redirect_url="/auth/login?next=/admin/models")
async def admin_models(request: Request, db: Session = Depends(get_db)):
"""Admin models overview."""
# Since we're using Starlette's authentication, the user is now available in request.user # Since we're using Starlette's authentication, the user is now available in request.user
model_list = [(key, name) for key, (_, name) in MODELS.items()] model_list = [(key, name) for key, (_, name) in MODELS.items()]
return templates.TemplateResponse( return templates.TemplateResponse(
+2
View File
@@ -30,6 +30,8 @@ python-dotenv>=1.0.0
email-validator>=2.0.0 email-validator>=2.0.0
pydantic>=2.3.0 pydantic>=2.3.0
qrcode>=7.4.2 qrcode>=7.4.2
psutil>=5.9.0 # System monitoring and statistics
python-dateutil>=2.8.2 # Date manipulation utilities
# Email support # Email support
fastapi-mail>=1.4.2 fastapi-mail>=1.4.2
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
import pytest
from unittest import mock
from sqlalchemy.orm import Session
from fastapi.testclient import TestClient
from datetime import datetime, timedelta
from app.main import app
from app.models import User, Team, Event, QRCode
from app.views.admin import get_user_statistics, get_team_statistics
from app.views.admin import get_event_statistics, get_system_health
# Fixture for mocking the database session
@pytest.fixture
def mock_db():
"""Create a mock database session for testing."""
mock_session = mock.MagicMock(spec=Session)
return mock_session
# Test cases for user statistics
def test_get_user_statistics(mock_db):
"""Test getting user statistics."""
# Setup mock query results
mock_db.query().count.side_effect = [100, 80, 20]
mock_db.query().filter().count.return_value = 10
# Get last 30 days
thirty_days_ago = datetime.now() - timedelta(days=30)
mock_db.query().filter().filter().count.return_value = 15
# Get the statistics
stats = get_user_statistics(mock_db)
# Assert the expected results
assert stats["total_users"] == 100
assert stats["active_users"] == 80
assert stats["verified_users"] == 20
assert stats["admin_users"] == 10
assert stats["new_registrations_30d"] == 15
# Test cases for team statistics
def test_get_team_statistics(mock_db):
"""Test getting team statistics."""
# Setup mock query results
mock_db.query().count.side_effect = [50, 45]
mock_db.query().filter().count.return_value = 5
# Team distribution mock
mock_team_distribution = [
{"member_count": 0, "count": 5},
{"member_count": 1, "count": 10},
{"member_count": 2, "count": 15},
{"member_count": 3, "count": 10},
{"member_count": 4, "count": 7},
{"member_count": 5, "count": 3}
]
mock_db.query().group_by().all.return_value = mock_team_distribution
# Get the statistics
stats = get_team_statistics(mock_db)
# Assert the expected results
assert stats["total_teams"] == 50
assert stats["active_teams"] == 45
assert stats["public_teams"] == 5
assert stats["team_distribution"] == mock_team_distribution
# Test cases for event statistics
def test_get_event_statistics(mock_db):
"""Test getting event statistics."""
# Setup mock query results
mock_db.query().count.side_effect = [30, 25, 5]
# Setup mock for upcoming events
today = datetime.now().date()
upcoming_events = [
mock.MagicMock(name="Event 1", event_date=today + timedelta(days=1), location="Location 1"),
mock.MagicMock(name="Event 2", event_date=today + timedelta(days=3), location="Location 2"),
mock.MagicMock(name="Event 3", event_date=today + timedelta(days=7), location="Location 3")
]
mock_db.query().filter().order_by().limit().all.return_value = upcoming_events
# Setup mock for attendance rates
mock_attendance_rates = [
{"event_id": 1, "event_name": "Event A", "attendee_count": 25},
{"event_id": 2, "event_name": "Event B", "attendee_count": 18},
{"event_id": 3, "event_name": "Event C", "attendee_count": 30}
]
mock_db.query().join().group_by().order_by().limit().all.return_value = mock_attendance_rates
# Get the statistics
stats = get_event_statistics(mock_db)
# Assert the expected results
assert stats["total_events"] == 30
assert stats["past_events"] == 25
assert stats["upcoming_events_count"] == 5
assert len(stats["upcoming_events"]) == 3
assert stats["attendance_rates"] == mock_attendance_rates
# Test cases for system health
def test_get_system_health(mock_db):
"""Test getting system health information."""
# Mock database status
mock_db.execute().fetchall.return_value = [{"status": "online"}]
# Get the health information
health_info = get_system_health(mock_db)
# Assert expected results
assert health_info["database_status"] == "online"
assert "uptime" in health_info
assert "recent_errors" in health_info
# Integration test for admin dashboard endpoint
@mock.patch("app.views.admin.get_db")
def test_admin_dashboard_endpoint(mock_get_db, mock_db):
"""Test the admin dashboard endpoint."""
# Setup mock DB to be returned from get_db
mock_get_db.return_value = mock_db
# Mock user stats
mock_user_stats = {
"total_users": 100,
"active_users": 80,
"verified_users": 20,
"admin_users": 10,
"new_registrations_30d": 15
}
# Mock team stats
mock_team_stats = {
"total_teams": 50,
"active_teams": 45,
"public_teams": 5,
"team_distribution": []
}
# Mock event stats
mock_event_stats = {
"total_events": 30,
"past_events": 25,
"upcoming_events_count": 5,
"upcoming_events": [],
"attendance_rates": []
}
# Mock system health
mock_system_health = {
"database_status": "online",
"uptime": "3 days, 2 hours",
"recent_errors": []
}
# Setup mock return values for our statistics functions
with mock.patch("app.views.admin.get_user_statistics", return_value=mock_user_stats), \
mock.patch("app.views.admin.get_team_statistics", return_value=mock_team_stats), \
mock.patch("app.views.admin.get_event_statistics", return_value=mock_event_stats), \
mock.patch("app.views.admin.get_system_health", return_value=mock_system_health), \
mock.patch("app.views.admin.require_admin", return_value=lambda f: f):
client = TestClient(app)
response = client.get("/admin/dashboard")
# Assert the response
assert response.status_code == 200
assert "user_stats" in response.context
assert "team_stats" in response.context
assert "event_stats" in response.context
assert "system_health" in response.context