rough new admin panel, more changes pending
This commit is contained in:
@ -1,11 +1,334 @@
|
||||
from flask import Blueprint, render_template
|
||||
# plugins/admin/routes.py
|
||||
|
||||
from flask import (
|
||||
Blueprint, render_template, redirect, url_for,
|
||||
flash, request, jsonify
|
||||
)
|
||||
from flask_login import login_required, current_user
|
||||
from flask_wtf.csrf import generate_csrf
|
||||
from sqlalchemy import func, desc
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
bp = Blueprint('admin', __name__, template_folder='templates')
|
||||
from app import db
|
||||
from plugins.auth.models import User
|
||||
from plugins.growlog.models import GrowLog
|
||||
from plugins.plant.models import Plant
|
||||
from plugins.admin.models import AnalyticsEvent
|
||||
from .forms import UserForm
|
||||
|
||||
@bp.route('/admin')
|
||||
bp = Blueprint('admin', __name__, url_prefix='/admin', template_folder='templates')
|
||||
|
||||
|
||||
@bp.route('/dashboard')
|
||||
@login_required
|
||||
def admin_dashboard():
|
||||
def dashboard():
|
||||
if current_user.role != 'admin':
|
||||
return "Access denied", 403
|
||||
return render_template('admin/admin_dashboard.html')
|
||||
|
||||
now = datetime.utcnow()
|
||||
week_ago = now - timedelta(days=7)
|
||||
month_ago= now - timedelta(days=30)
|
||||
|
||||
# ─── Overview metrics ────────────────────────────────────────────── #
|
||||
|
||||
# Users
|
||||
total_users = User.query.count()
|
||||
active_users = User.query.filter(
|
||||
User.is_deleted == False,
|
||||
User.is_banned == False,
|
||||
(User.suspended_until == None) | (User.suspended_until <= now)
|
||||
).count()
|
||||
suspended_users = User.query.filter(User.suspended_until > now).count()
|
||||
banned_users = User.query.filter_by(is_banned=True).count()
|
||||
deleted_users = User.query.filter_by(is_deleted=True).count()
|
||||
new_7d_users = User.query.filter(User.created_at >= week_ago).count()
|
||||
|
||||
# Grow‐logs
|
||||
total_logs = GrowLog.query.count()
|
||||
logs_7d = GrowLog.query.filter(GrowLog.created_at >= week_ago).count()
|
||||
|
||||
# Sign‐ups last 30 days
|
||||
signup_dates = [(month_ago + timedelta(days=i)).date() for i in range(31)]
|
||||
signup_counts = [
|
||||
User.query.filter(func.date(User.created_at)==d).count()
|
||||
for d in signup_dates
|
||||
]
|
||||
chart_dates = [d.strftime('%Y-%m-%d') for d in signup_dates]
|
||||
|
||||
# Status distribution
|
||||
role_counts = {
|
||||
'User': User.query.filter_by(role='user').count(),
|
||||
'Admin': User.query.filter_by(role='admin').count()
|
||||
}
|
||||
|
||||
# ─── Analytics aggregates ─────────────────────────────────────────── #
|
||||
|
||||
ev_q = AnalyticsEvent.query.filter(AnalyticsEvent.timestamp >= week_ago)
|
||||
total_ev = ev_q.count() or 1
|
||||
error_ev = ev_q.filter(AnalyticsEvent.status_code >= 500).count()
|
||||
error_pct = round(error_ev / total_ev * 100, 1)
|
||||
|
||||
top_routes = [
|
||||
{'path': path, 'count': cnt}
|
||||
for path, cnt in db.session.query(
|
||||
AnalyticsEvent.path,
|
||||
func.count(AnalyticsEvent.id)
|
||||
)
|
||||
.filter(AnalyticsEvent.timestamp >= week_ago)
|
||||
.group_by(AnalyticsEvent.path)
|
||||
.order_by(desc(func.count(AnalyticsEvent.id)))
|
||||
.limit(10)
|
||||
.all()
|
||||
]
|
||||
|
||||
# Browser share
|
||||
browsers = {}
|
||||
ua_counts = (
|
||||
db.session.query(
|
||||
func.substr(AnalyticsEvent.user_agent, 1, 200).label('ua'),
|
||||
func.count(AnalyticsEvent.id).label('cnt')
|
||||
)
|
||||
.filter(AnalyticsEvent.user_agent.isnot(None))
|
||||
.filter(AnalyticsEvent.timestamp >= week_ago)
|
||||
.group_by(func.substr(AnalyticsEvent.user_agent, 1, 200))
|
||||
.all()
|
||||
)
|
||||
for ua, cnt in ua_counts:
|
||||
b = 'Other'
|
||||
if 'Chrome' in ua: b = 'Chrome'
|
||||
elif 'Firefox' in ua: b = 'Firefox'
|
||||
elif 'Safari' in ua and 'Chrome' not in ua: b = 'Safari'
|
||||
elif 'Edge' in ua: b = 'Edge'
|
||||
browsers[b] = browsers.get(b, 0) + cnt
|
||||
|
||||
referrers = dict(
|
||||
db.session.query(
|
||||
AnalyticsEvent.referer, func.count(AnalyticsEvent.id)
|
||||
)
|
||||
.filter(AnalyticsEvent.referer.isnot(None))
|
||||
.filter(AnalyticsEvent.timestamp >= week_ago)
|
||||
.group_by(AnalyticsEvent.referer)
|
||||
.order_by(desc(func.count(AnalyticsEvent.id)))
|
||||
.limit(5)
|
||||
.all()
|
||||
)
|
||||
|
||||
# ─── Stats metrics ────────────────────────────────────────────────── #
|
||||
|
||||
# Plant totals
|
||||
total_plants = Plant.query.count()
|
||||
active_plants = Plant.query.filter_by(is_active=True).count()
|
||||
|
||||
# Top 5 popular plant types
|
||||
popular_plants = db.session.query(
|
||||
Plant.plant_type,
|
||||
func.count(Plant.id).label('count')
|
||||
).filter(Plant.is_active==True) \
|
||||
.group_by(Plant.plant_type) \
|
||||
.order_by(desc('count')) \
|
||||
.limit(5) \
|
||||
.all()
|
||||
|
||||
# ─── Render template ──────────────────────────────────────────────── #
|
||||
|
||||
return render_template(
|
||||
'admin/dashboard.html',
|
||||
# Overview
|
||||
total_users=total_users,
|
||||
active_users=active_users,
|
||||
suspended_users=suspended_users,
|
||||
banned_users=banned_users,
|
||||
deleted_users=deleted_users,
|
||||
new_7d_users=new_7d_users,
|
||||
total_logs=total_logs,
|
||||
logs_7d=logs_7d,
|
||||
chart_dates=chart_dates,
|
||||
signup_counts=signup_counts,
|
||||
role_counts=role_counts,
|
||||
# Analytics
|
||||
error_pct=error_pct,
|
||||
top_routes=top_routes,
|
||||
browsers=browsers,
|
||||
referrers=referrers,
|
||||
# Stats
|
||||
total_plants=total_plants,
|
||||
active_plants=active_plants,
|
||||
popular_plants=popular_plants,
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/users')
|
||||
@login_required
|
||||
def list_users():
|
||||
if current_user.role != 'admin':
|
||||
return "Access denied", 403
|
||||
|
||||
# --- parse query parameters ---
|
||||
page = request.args.get('page', 1, type=int)
|
||||
show_deleted = request.args.get('show_deleted', '0') == '1'
|
||||
q = request.args.get('q', '', type=str).strip()
|
||||
|
||||
# --- build base query ---
|
||||
query = User.query
|
||||
if not show_deleted:
|
||||
query = query.filter_by(is_deleted=False)
|
||||
if q:
|
||||
like = f"%{q}%"
|
||||
query = query.filter(User.email.ilike(like))
|
||||
|
||||
# --- paginate the results ---
|
||||
pagination = query.order_by(User.created_at.desc()) \
|
||||
.paginate(page=page, per_page=25, error_out=False)
|
||||
users = pagination.items
|
||||
|
||||
# --- AJAX response (JSON) ---
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
token = generate_csrf()
|
||||
users_data = []
|
||||
for u in users:
|
||||
if u.is_banned:
|
||||
status = 'banned'
|
||||
until = None
|
||||
elif u.suspended_until and u.suspended_until > datetime.utcnow():
|
||||
status = 'suspended'
|
||||
until = u.suspended_until.strftime('%Y-%m-%d')
|
||||
else:
|
||||
status = 'active'
|
||||
until = None
|
||||
|
||||
users_data.append({
|
||||
'id': u.id,
|
||||
'email': u.email,
|
||||
'role': u.role,
|
||||
'is_verified': u.is_verified,
|
||||
'excluded_from_analytics': u.excluded_from_analytics,
|
||||
'status': status,
|
||||
'suspended_until': until,
|
||||
'is_deleted': u.is_deleted,
|
||||
'created_at': u.created_at.strftime('%Y-%m-%d')
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'users': users_data,
|
||||
'csrf_token': token
|
||||
})
|
||||
|
||||
# --- normal HTML response ---
|
||||
return render_template(
|
||||
'admin/users/list.html',
|
||||
users=users,
|
||||
pagination=pagination,
|
||||
show_deleted=show_deleted,
|
||||
q=q
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/users/create', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def create_user():
|
||||
if current_user.role != 'admin':
|
||||
return "Access denied", 403
|
||||
form = UserForm()
|
||||
if form.validate_on_submit():
|
||||
u = User(
|
||||
email=form.email.data,
|
||||
role=form.role.data,
|
||||
is_verified=form.is_verified.data,
|
||||
excluded_from_analytics=form.excluded_from_analytics.data
|
||||
)
|
||||
# ban/suspend logic here if you need it...
|
||||
if form.password.data:
|
||||
u.set_password(form.password.data)
|
||||
db.session.add(u)
|
||||
db.session.commit()
|
||||
flash('User created.', 'success')
|
||||
return redirect(url_for('admin.list_users'))
|
||||
return render_template('admin/users/form.html', form=form, action='Create')
|
||||
|
||||
|
||||
@bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_user(user_id):
|
||||
if current_user.role != 'admin':
|
||||
return "Access denied", 403
|
||||
|
||||
u = User.query.get_or_404(user_id)
|
||||
form = UserForm(obj=u)
|
||||
|
||||
# Pre-populate on GET
|
||||
if request.method == 'GET':
|
||||
if u.is_banned:
|
||||
form.ban_type.data = 'perm'
|
||||
elif u.suspended_until and u.suspended_until > datetime.utcnow():
|
||||
form.ban_type.data = 'temp'
|
||||
days = (u.suspended_until - datetime.utcnow()).days
|
||||
form.suspend_days.data = days if days > 0 else 1
|
||||
else:
|
||||
form.ban_type.data = 'active'
|
||||
|
||||
if form.validate_on_submit():
|
||||
u.email = form.email.data
|
||||
u.role = form.role.data
|
||||
u.is_verified = form.is_verified.data
|
||||
u.excluded_from_analytics = form.excluded_from_analytics.data
|
||||
|
||||
# Ban/Suspend logic
|
||||
if form.ban_type.data == 'perm':
|
||||
u.is_banned = True
|
||||
u.suspended_until = None
|
||||
elif form.ban_type.data == 'temp':
|
||||
u.is_banned = False
|
||||
days = form.suspend_days.data or 7
|
||||
u.suspended_until = datetime.utcnow() + timedelta(days=days)
|
||||
else:
|
||||
u.is_banned = False
|
||||
u.suspended_until = None
|
||||
|
||||
if form.password.data:
|
||||
u.set_password(form.password.data)
|
||||
|
||||
db.session.commit()
|
||||
flash('User updated.', 'success')
|
||||
return redirect(url_for('admin.list_users',
|
||||
page=request.args.get('page'),
|
||||
show_deleted=request.args.get('show_deleted'),
|
||||
q=request.args.get('q')))
|
||||
|
||||
return render_template(
|
||||
'admin/users/form.html',
|
||||
form=form,
|
||||
action='Edit'
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/users/<int:user_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_user(user_id):
|
||||
if current_user.role != 'admin':
|
||||
return "Access denied", 403
|
||||
u = User.query.get_or_404(user_id)
|
||||
if u.id == current_user.id:
|
||||
flash("Cannot delete yourself.", 'warning')
|
||||
else:
|
||||
u.is_deleted = True
|
||||
db.session.commit()
|
||||
flash('User deleted (soft).', 'success')
|
||||
return redirect(url_for('admin.list_users',
|
||||
page=request.args.get('page'),
|
||||
show_deleted=request.args.get('show_deleted'),
|
||||
q=request.args.get('q')))
|
||||
|
||||
|
||||
@bp.route('/users/<int:user_id>/undelete', methods=['POST'])
|
||||
@login_required
|
||||
def undelete_user(user_id):
|
||||
if current_user.role != 'admin':
|
||||
return "Access denied", 403
|
||||
u = User.query.get_or_404(user_id)
|
||||
u.is_deleted = False
|
||||
db.session.commit()
|
||||
flash('User restored.', 'success')
|
||||
return redirect(url_for('admin.list_users',
|
||||
page=request.args.get('page'),
|
||||
show_deleted=request.args.get('show_deleted'),
|
||||
q=request.args.get('q')))
|
||||
|
Reference in New Issue
Block a user