Files
natureinpots_community/plugins/admin/routes.py

335 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
from app import db
from plugins.auth.models import User
from plugins.plant.growlog.models import GrowLog
from plugins.plant.models import Plant
from plugins.admin.models import AnalyticsEvent
from .forms import UserForm
bp = Blueprint('admin', __name__, url_prefix='/admin', template_folder='templates')
@bp.route('/dashboard')
@login_required
def dashboard():
if current_user.role != 'admin':
return "Access denied", 403
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()
# Growlogs
total_logs = GrowLog.query.count()
logs_7d = GrowLog.query.filter(GrowLog.created_at >= week_ago).count()
# Signups 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')))