Files
moto-adv-website/app/routes/admin.py
ske087 8691a6cd2d debug: Add enhanced logging to admin post deletion
- Add detailed logging before and after post deletion
- Track which posts exist before/after deletion operation
- Add error handling and logging for deletion failures
- Investigate reported issue where deleting one post deletes multiple posts
2025-07-24 17:31:27 +03:00

398 lines
14 KiB
Python

from flask import Blueprint, render_template, request, flash, redirect, url_for, jsonify, current_app
from flask_login import login_required, current_user
from functools import wraps
from datetime import datetime, timedelta
from sqlalchemy import func, desc
from app.extensions import db
from app.models import User, Post, PostImage, GPXFile, Comment, Like, PageView
admin = Blueprint('admin', __name__, url_prefix='/admin')
def admin_required(f):
"""Decorator to require admin access"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or not current_user.is_admin:
flash('Admin access required.', 'error')
return redirect(url_for('auth.login'))
return f(*args, **kwargs)
return decorated_function
@admin.route('/')
@login_required
@admin_required
def dashboard():
"""Admin dashboard with site statistics"""
# Get basic statistics
total_users = User.query.count()
total_posts = Post.query.count()
published_posts = Post.query.filter_by(published=True).count()
pending_posts = Post.query.filter_by(published=False).count()
total_comments = Comment.query.count()
total_likes = Like.query.count()
# Active users (users who created posts or comments in the last 30 days)
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
active_users = db.session.query(User.id).filter(
db.or_(
User.posts.any(Post.created_at >= thirty_days_ago),
User.comments.any(Comment.created_at >= thirty_days_ago)
)
).distinct().count()
# Recent activity
recent_posts = Post.query.order_by(Post.created_at.desc()).limit(5).all()
recent_users = User.query.order_by(User.created_at.desc()).limit(5).all()
# Page views statistics
today = datetime.utcnow().date()
yesterday = today - timedelta(days=1)
week_ago = today - timedelta(days=7)
views_today = PageView.query.filter(
func.date(PageView.created_at) == today
).count()
views_yesterday = PageView.query.filter(
func.date(PageView.created_at) == yesterday
).count()
views_this_week = PageView.query.filter(
PageView.created_at >= week_ago
).count()
# Most viewed posts
most_viewed_posts = db.session.query(
Post.id, Post.title, func.count(PageView.id).label('view_count')
).join(PageView, Post.id == PageView.post_id)\
.group_by(Post.id, Post.title)\
.order_by(desc('view_count'))\
.limit(10).all()
# Most viewed pages
most_viewed_pages = db.session.query(
PageView.path, func.count(PageView.id).label('view_count')
).group_by(PageView.path)\
.order_by(desc('view_count'))\
.limit(10).all()
return render_template('admin/dashboard.html',
total_users=total_users,
total_posts=total_posts,
published_posts=published_posts,
pending_posts=pending_posts,
total_comments=total_comments,
total_likes=total_likes,
active_users=active_users,
recent_posts=recent_posts,
recent_users=recent_users,
views_today=views_today,
views_yesterday=views_yesterday,
views_this_week=views_this_week,
most_viewed_posts=most_viewed_posts,
most_viewed_pages=most_viewed_pages)
@admin.route('/posts')
@login_required
@admin_required
def posts():
"""Admin post management - review posts"""
page = request.args.get('page', 1, type=int)
status = request.args.get('status', 'pending') # pending, published, all
query = Post.query
if status == 'pending':
query = query.filter_by(published=False)
elif status == 'published':
query = query.filter_by(published=True)
# 'all' shows all posts
posts = query.order_by(Post.created_at.desc()).paginate(
page=page, per_page=20, error_out=False
)
return render_template('admin/posts.html', posts=posts, status=status)
@admin.route('/posts/<int:post_id>')
@login_required
@admin_required
def post_detail(post_id):
"""View post details for review"""
post = Post.query.get_or_404(post_id)
return render_template('admin/post_detail.html', post=post)
@admin.route('/posts/<int:post_id>/publish', methods=['POST'])
@login_required
@admin_required
def publish_post(post_id):
"""Publish a post"""
post = Post.query.get_or_404(post_id)
post.published = True
post.updated_at = datetime.utcnow()
db.session.commit()
flash(f'Post "{post.title}" has been published.', 'success')
return redirect(url_for('admin.posts'))
@admin.route('/posts/<int:post_id>/unpublish', methods=['POST'])
@login_required
@admin_required
def unpublish_post(post_id):
"""Unpublish a post"""
post = Post.query.get_or_404(post_id)
post.published = False
post.updated_at = datetime.utcnow()
db.session.commit()
flash(f'Post "{post.title}" has been unpublished.', 'success')
return redirect(url_for('admin.posts'))
@admin.route('/posts/<int:post_id>/delete', methods=['POST'])
@login_required
@admin_required
def delete_post(post_id):
"""Delete a post"""
current_app.logger.info(f'Admin {current_user.id} attempting to delete post {post_id}')
# Get all posts before deletion for debugging
all_posts_before = Post.query.all()
current_app.logger.info(f'Posts before deletion: {[p.id for p in all_posts_before]}')
post = Post.query.get_or_404(post_id)
title = post.title
current_app.logger.info(f'Found post to delete: ID={post.id}, Title="{title}"')
try:
# Delete associated files and records
db.session.delete(post)
db.session.commit()
# Check posts after deletion
all_posts_after = Post.query.all()
current_app.logger.info(f'Posts after deletion: {[p.id for p in all_posts_after]}')
current_app.logger.info(f'Successfully deleted post {post_id}: "{title}"')
flash(f'Post "{title}" has been deleted.', 'success')
except Exception as e:
db.session.rollback()
current_app.logger.error(f'Error deleting post {post_id}: {str(e)}')
flash(f'Error deleting post: {str(e)}', 'error')
return redirect(url_for('admin.posts'))
@admin.route('/users')
@login_required
@admin_required
def users():
"""Admin user management"""
page = request.args.get('page', 1, type=int)
users = User.query.order_by(User.created_at.desc()).paginate(
page=page, per_page=50, error_out=False
)
return render_template('admin/users.html', users=users)
@admin.route('/users/<int:user_id>')
@login_required
@admin_required
def user_detail(user_id):
"""View user details"""
user = User.query.get_or_404(user_id)
user_posts = Post.query.filter_by(author_id=user_id).order_by(Post.created_at.desc()).all()
user_comments = Comment.query.filter_by(author_id=user_id).order_by(Comment.created_at.desc()).all()
return render_template('admin/user_detail.html',
user=user,
user_posts=user_posts,
user_comments=user_comments)
@admin.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@login_required
@admin_required
def toggle_user_status(user_id):
"""Toggle user active/inactive status"""
user = User.query.get_or_404(user_id)
# Prevent self-modification if current user is admin
if user.is_admin and user.id == current_user.id:
return jsonify({'success': False, 'error': 'Cannot deactivate your own admin account'})
# Toggle status
user.is_active = not user.is_active
status = "activated" if user.is_active else "deactivated"
try:
db.session.commit()
return jsonify({
'success': True,
'message': f'User {user.nickname} has been {status}',
'is_active': user.is_active
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': f'Failed to update user status: {str(e)}'})
@admin.route('/users/<int:user_id>/delete', methods=['DELETE', 'POST'])
@login_required
@admin_required
def delete_user(user_id):
"""Delete user and transfer their posts to admin"""
user = User.query.get_or_404(user_id)
# Prevent self-deletion
if user.id == current_user.id:
return jsonify({'success': False, 'error': 'Cannot delete your own account'})
# Prevent deletion of other admins
if user.is_admin:
return jsonify({'success': False, 'error': 'Cannot delete admin accounts'})
try:
# Transfer all user's posts to current admin
user_posts = Post.query.filter_by(author_id=user.id).all()
for post in user_posts:
post.author_id = current_user.id
# Transfer all user's comments to current admin
user_comments = Comment.query.filter_by(author_id=user.id).all()
for comment in user_comments:
comment.author_id = current_user.id
# Delete user's likes (they will be orphaned)
Like.query.filter_by(user_id=user.id).delete()
# Delete user's page views
PageView.query.filter_by(user_id=user.id).delete()
username = user.nickname
user_posts_count = len(user_posts)
# Delete the user
db.session.delete(user)
db.session.commit()
return jsonify({
'success': True,
'message': f'User {username} has been deleted. {user_posts_count} posts transferred to {current_user.nickname}.'
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': f'Failed to delete user: {str(e)}'})
@admin.route('/analytics')
@login_required
@admin_required
def analytics():
"""Detailed analytics page"""
# Get date range from query params
days = request.args.get('days', 30, type=int)
end_date = datetime.utcnow().date()
start_date = end_date - timedelta(days=days)
# Daily views for the chart
daily_views = db.session.query(
func.date(PageView.created_at).label('date'),
func.count(PageView.id).label('views')
).filter(
func.date(PageView.created_at) >= start_date
).group_by(func.date(PageView.created_at))\
.order_by('date').all()
# Top posts by views
top_posts = db.session.query(
Post.id, Post.title, Post.author, func.count(PageView.id).label('view_count')
).join(PageView, Post.id == PageView.post_id)\
.filter(PageView.created_at >= start_date)\
.group_by(Post.id, Post.title)\
.order_by(desc('view_count'))\
.limit(20).all()
# User activity - separate queries to avoid cartesian product
user_posts = db.session.query(
User.id,
User.nickname,
func.count(Post.id).label('post_count')
).outerjoin(Post, User.id == Post.author_id)\
.group_by(User.id, User.nickname).subquery()
user_comments = db.session.query(
User.id,
func.count(Comment.id).label('comment_count')
).outerjoin(Comment, User.id == Comment.author_id)\
.group_by(User.id).subquery()
user_activity = db.session.query(
user_posts.c.nickname,
user_posts.c.post_count,
func.coalesce(user_comments.c.comment_count, 0).label('comment_count')
).outerjoin(user_comments, user_posts.c.id == user_comments.c.id)\
.order_by(desc(user_posts.c.post_count))\
.limit(20).all()
# Get overall statistics
total_views = PageView.query.count()
unique_visitors = db.session.query(PageView.ip_address).distinct().count()
today_views = PageView.query.filter(
func.date(PageView.created_at) == datetime.utcnow().date()
).count()
week_start = datetime.utcnow().date() - timedelta(days=7)
week_views = PageView.query.filter(
func.date(PageView.created_at) >= week_start
).count()
# Popular pages
popular_pages = db.session.query(
PageView.path,
func.count(PageView.id).label('view_count')
).group_by(PageView.path)\
.order_by(desc('view_count'))\
.limit(10).all()
# Recent activity
recent_views = PageView.query.join(User, PageView.user_id == User.id, isouter=True)\
.order_by(desc(PageView.created_at))\
.limit(20).all()
# Browser statistics (extract from user agent)
browser_stats = db.session.query(
func.substr(PageView.user_agent, 1, 50).label('browser'),
func.count(PageView.id).label('view_count')
).filter(PageView.user_agent.isnot(None))\
.group_by(func.substr(PageView.user_agent, 1, 50))\
.order_by(desc('view_count'))\
.limit(10).all()
return render_template('admin/analytics.html',
total_views=total_views,
unique_visitors=unique_visitors,
today_views=today_views,
week_views=week_views,
popular_pages=popular_pages,
recent_views=recent_views,
browser_stats=browser_stats,
daily_views=daily_views,
top_posts=top_posts,
user_activity=user_activity,
days=days)
# API endpoints for AJAX requests
@admin.route('/api/quick-stats')
@login_required
@admin_required
def api_quick_stats():
"""Quick stats for dashboard widgets"""
pending_count = Post.query.filter_by(published=False).count()
today_views = PageView.query.filter(
func.date(PageView.created_at) == datetime.utcnow().date()
).count()
return jsonify({
'pending_posts': pending_count,
'today_views': today_views
})