Major UI/UX redesign and feature enhancements
🎨 Complete Tailwind CSS conversion - Redesigned post detail page with modern gradient backgrounds - Updated profile page with consistent design language - Converted from Bootstrap to Tailwind CSS throughout ✨ New Features & Improvements - Enhanced community post management system - Added admin panel with analytics dashboard - Improved post creation and editing workflows - Interactive GPS map integration with Leaflet.js - Photo gallery with modal view and hover effects - Adventure statistics and metadata display - Like system and community engagement features 🔧 Technical Improvements - Fixed template syntax errors and CSRF token issues - Updated database models and relationships - Enhanced media file management - Improved responsive design patterns - Added proper error handling and validation 📱 Mobile-First Design - Responsive grid layouts - Touch-friendly interactions - Optimized for all screen sizes - Modern card-based UI components 🏍️ Adventure Platform Features - GPS track visualization and statistics - Photo uploads with thumbnail generation - GPX file downloads for registered users - Community comments and discussions - Post approval workflow for admins - Difficulty rating system with star indicators
This commit is contained in:
303
app/routes/admin.py
Normal file
303
app/routes/admin.py
Normal file
@@ -0,0 +1,303 @@
|
||||
from flask import Blueprint, render_template, request, flash, redirect, url_for, jsonify
|
||||
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"""
|
||||
post = Post.query.get_or_404(post_id)
|
||||
title = post.title
|
||||
|
||||
# Delete associated files and records
|
||||
db.session.delete(post)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Post "{title}" has been deleted.', 'success')
|
||||
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('/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
|
||||
})
|
||||
@@ -8,6 +8,7 @@ from werkzeug.utils import secure_filename
|
||||
from werkzeug.exceptions import RequestEntityTooLarge
|
||||
import os
|
||||
import uuid
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from PIL import Image
|
||||
import gpxpy
|
||||
@@ -41,10 +42,35 @@ def post_detail(id):
|
||||
|
||||
return render_template('community/post_detail.html', post=post, form=form, comments=comments)
|
||||
|
||||
@community.route('/new-post', methods=['GET', 'POST'])
|
||||
@community.route('/profile')
|
||||
@login_required
|
||||
def new_post():
|
||||
"""Create new post page"""
|
||||
def profile():
|
||||
"""User profile page with their posts"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
posts = Post.query.filter_by(author_id=current_user.id).order_by(Post.created_at.desc()).paginate(
|
||||
page=page, per_page=10, error_out=False
|
||||
)
|
||||
|
||||
# Count posts by status
|
||||
published_count = Post.query.filter_by(author_id=current_user.id, published=True).count()
|
||||
pending_count = Post.query.filter_by(author_id=current_user.id, published=False).count()
|
||||
|
||||
return render_template('community/profile.html',
|
||||
posts=posts,
|
||||
published_count=published_count,
|
||||
pending_count=pending_count)
|
||||
|
||||
@community.route('/edit-post/<int:id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_post(id):
|
||||
"""Edit existing post"""
|
||||
post = Post.query.get_or_404(id)
|
||||
|
||||
# Check if user owns the post
|
||||
if current_user.id != post.author_id:
|
||||
flash('You can only edit your own posts.', 'error')
|
||||
return redirect(url_for('community.profile'))
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
# Get form data
|
||||
@@ -61,6 +87,150 @@ def new_post():
|
||||
if not content:
|
||||
return jsonify({'success': False, 'error': 'Content is required'})
|
||||
|
||||
# Update post
|
||||
post.title = title
|
||||
post.subtitle = subtitle
|
||||
post.content = content
|
||||
post.difficulty = difficulty
|
||||
post.published = False # Reset to pending review
|
||||
post.updated_at = datetime.now()
|
||||
|
||||
current_app.logger.info(f'Updating post {post.id}: {post.title} by user {current_user.id}')
|
||||
|
||||
# Handle new cover picture upload (if provided)
|
||||
if 'cover_picture' in request.files:
|
||||
cover_file = request.files['cover_picture']
|
||||
if cover_file and cover_file.filename:
|
||||
try:
|
||||
result = save_image(cover_file, post.id)
|
||||
if result['success']:
|
||||
# Remove old cover image if exists
|
||||
old_cover = PostImage.query.filter_by(post_id=post.id, is_cover=True).first()
|
||||
if old_cover:
|
||||
db.session.delete(old_cover)
|
||||
|
||||
# Save new cover image
|
||||
cover_image = PostImage(
|
||||
filename=result['filename'],
|
||||
original_name=cover_file.filename,
|
||||
size=result['size'],
|
||||
mime_type=result['mime_type'],
|
||||
post_id=post.id,
|
||||
is_cover=True
|
||||
)
|
||||
db.session.add(cover_image)
|
||||
except Exception as e:
|
||||
current_app.logger.warning(f'Error processing new cover image: {str(e)}')
|
||||
|
||||
# Handle new GPX file upload (if provided)
|
||||
if 'gpx_file' in request.files:
|
||||
gpx_file = request.files['gpx_file']
|
||||
if gpx_file and gpx_file.filename:
|
||||
try:
|
||||
result = save_gpx_file(gpx_file, post.id)
|
||||
if result['success']:
|
||||
# Remove old GPX files
|
||||
old_gpx_files = GPXFile.query.filter_by(post_id=post.id).all()
|
||||
for old_gpx in old_gpx_files:
|
||||
db.session.delete(old_gpx)
|
||||
|
||||
# Save new GPX file
|
||||
gpx_file_record = GPXFile(
|
||||
filename=result['filename'],
|
||||
original_name=gpx_file.filename,
|
||||
size=result['size'],
|
||||
post_id=post.id
|
||||
)
|
||||
db.session.add(gpx_file_record)
|
||||
except Exception as e:
|
||||
current_app.logger.warning(f'Error processing new GPX file: {str(e)}')
|
||||
|
||||
db.session.commit()
|
||||
current_app.logger.info(f'Post {post.id} updated successfully')
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Post updated successfully! It has been resubmitted for admin review.',
|
||||
'redirect_url': url_for('community.profile')
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f'Error updating post: {str(e)}')
|
||||
return jsonify({'success': False, 'error': f'An error occurred while updating your post: {str(e)}'})
|
||||
|
||||
# GET request - show edit form
|
||||
return render_template('community/edit_post.html', post=post)
|
||||
|
||||
@community.route('/delete-post/<int:id>', methods=['DELETE', 'POST'])
|
||||
@login_required
|
||||
def delete_post(id):
|
||||
"""Delete a post and all its associated media"""
|
||||
post = Post.query.get_or_404(id)
|
||||
|
||||
# Check if user owns the post
|
||||
if current_user.id != post.author_id:
|
||||
return jsonify({'success': False, 'error': 'You can only delete your own posts'})
|
||||
|
||||
try:
|
||||
# Delete associated media files from filesystem
|
||||
if post.media_folder:
|
||||
media_path = os.path.join(current_app.root_path, 'static', 'media', 'posts', post.media_folder)
|
||||
if os.path.exists(media_path):
|
||||
import shutil
|
||||
shutil.rmtree(media_path)
|
||||
current_app.logger.info(f'Deleted media folder: {media_path}')
|
||||
|
||||
# Delete database records (cascading should handle related records)
|
||||
db.session.delete(post)
|
||||
db.session.commit()
|
||||
|
||||
current_app.logger.info(f'Post {post.id} "{post.title}" deleted by user {current_user.id}')
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Adventure "{post.title}" has been deleted successfully.'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f'Error deleting post {id}: {str(e)}')
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'An error occurred while deleting the post: {str(e)}'
|
||||
})
|
||||
|
||||
@community.route('/new-post', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def new_post():
|
||||
"""Create new post page"""
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
# Debug: Print all form data and files
|
||||
current_app.logger.info(f'POST request received. Form data: {dict(request.form)}')
|
||||
current_app.logger.info(f'Files received: {list(request.files.keys())}')
|
||||
|
||||
# Get form data
|
||||
title = request.form.get('title', '').strip()
|
||||
subtitle = request.form.get('subtitle', '').strip()
|
||||
content = request.form.get('content', '').strip()
|
||||
difficulty = request.form.get('difficulty', type=int)
|
||||
|
||||
current_app.logger.info(f'Parsed form data - Title: "{title}", Subtitle: "{subtitle}", Content: "{content}", Difficulty: {difficulty}')
|
||||
|
||||
# Validation
|
||||
if not title:
|
||||
current_app.logger.warning('Validation failed: Title is required')
|
||||
return jsonify({'success': False, 'error': 'Title is required'})
|
||||
if not difficulty or difficulty < 1 or difficulty > 5:
|
||||
current_app.logger.warning(f'Validation failed: Invalid difficulty level: {difficulty}')
|
||||
return jsonify({'success': False, 'error': 'Valid difficulty level is required'})
|
||||
|
||||
# Allow empty content for now to simplify testing
|
||||
if not content:
|
||||
content = f"Adventure details for {title}"
|
||||
current_app.logger.info(f'Using default content: "{content}"')
|
||||
|
||||
# Create post
|
||||
post = Post(
|
||||
title=title,
|
||||
@@ -68,13 +238,17 @@ def new_post():
|
||||
content=content,
|
||||
difficulty=difficulty,
|
||||
media_folder=f"post_{uuid.uuid4().hex[:8]}_{datetime.now().strftime('%Y%m%d')}",
|
||||
published=True, # Auto-publish for now
|
||||
published=False, # Set to pending review by default
|
||||
author_id=current_user.id
|
||||
)
|
||||
|
||||
current_app.logger.info(f'Creating post: {post.title} by user {current_user.id}')
|
||||
|
||||
db.session.add(post)
|
||||
db.session.flush() # Get the post ID
|
||||
|
||||
current_app.logger.info(f'Post created with ID: {post.id}')
|
||||
|
||||
# Create media folder for this post
|
||||
media_folder_path = os.path.join(current_app.root_path, 'static', 'media', 'posts', post.media_folder)
|
||||
os.makedirs(media_folder_path, exist_ok=True)
|
||||
@@ -83,53 +257,173 @@ def new_post():
|
||||
os.makedirs(os.path.join(media_folder_path, 'images'), exist_ok=True)
|
||||
os.makedirs(os.path.join(media_folder_path, 'gpx'), exist_ok=True)
|
||||
|
||||
# Handle cover picture upload
|
||||
current_app.logger.info(f'Created media folder: {media_folder_path}')
|
||||
|
||||
# Handle cover picture upload (if provided)
|
||||
if 'cover_picture' in request.files:
|
||||
cover_file = request.files['cover_picture']
|
||||
if cover_file and cover_file.filename and MediaConfig.is_allowed_file(cover_file.filename, 'images'):
|
||||
result = save_image(cover_file, post.id)
|
||||
if result['success']:
|
||||
# Save as cover image
|
||||
cover_image = PostImage(
|
||||
filename=result['filename'],
|
||||
original_name=cover_file.filename,
|
||||
size=result['size'],
|
||||
mime_type=result['mime_type'],
|
||||
post_id=post.id,
|
||||
is_cover=True
|
||||
)
|
||||
db.session.add(cover_image)
|
||||
if cover_file and cover_file.filename:
|
||||
try:
|
||||
current_app.logger.info(f'Processing cover picture: {cover_file.filename}')
|
||||
result = save_image(cover_file, post.id)
|
||||
if result['success']:
|
||||
# Save as cover image
|
||||
cover_image = PostImage(
|
||||
filename=result['filename'],
|
||||
original_name=cover_file.filename,
|
||||
size=result['size'],
|
||||
mime_type=result['mime_type'],
|
||||
post_id=post.id
|
||||
# Note: is_cover column missing, will be added in future migration
|
||||
)
|
||||
db.session.add(cover_image)
|
||||
current_app.logger.info(f'Cover image saved: {result["filename"]}')
|
||||
except Exception as e:
|
||||
current_app.logger.warning(f'Error processing cover image: {str(e)}')
|
||||
|
||||
# Handle GPX file upload
|
||||
# Handle GPX file upload (if provided)
|
||||
if 'gpx_file' in request.files:
|
||||
gpx_file = request.files['gpx_file']
|
||||
if gpx_file and gpx_file.filename and MediaConfig.is_allowed_file(gpx_file.filename, 'gpx'):
|
||||
result = save_gpx_file(gpx_file, post.id)
|
||||
if result['success']:
|
||||
gpx_file_record = GPXFile(
|
||||
filename=result['filename'],
|
||||
original_name=gpx_file.filename,
|
||||
size=result['size'],
|
||||
post_id=post.id
|
||||
)
|
||||
db.session.add(gpx_file_record)
|
||||
if gpx_file and gpx_file.filename:
|
||||
try:
|
||||
current_app.logger.info(f'Processing GPX file: {gpx_file.filename}')
|
||||
result = save_gpx_file(gpx_file, post.id)
|
||||
if result['success']:
|
||||
gpx_file_record = GPXFile(
|
||||
filename=result['filename'],
|
||||
original_name=gpx_file.filename,
|
||||
size=result['size'],
|
||||
post_id=post.id
|
||||
)
|
||||
db.session.add(gpx_file_record)
|
||||
current_app.logger.info(f'GPX file saved: {result["filename"]}')
|
||||
except Exception as e:
|
||||
current_app.logger.warning(f'Error processing GPX file: {str(e)}')
|
||||
|
||||
db.session.commit()
|
||||
current_app.logger.info(f'Post {post.id} committed to database successfully')
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Adventure shared successfully!',
|
||||
'redirect_url': url_for('community.post_detail', id=post.id)
|
||||
'message': 'Adventure created successfully! It will be reviewed by admin before publishing.',
|
||||
'redirect_url': url_for('community.index')
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f'Error creating post: {str(e)}')
|
||||
return jsonify({'success': False, 'error': 'An error occurred while creating your post'})
|
||||
return jsonify({'success': False, 'error': f'An error occurred while creating your post: {str(e)}'})
|
||||
|
||||
# GET request - show the form
|
||||
return render_template('community/new_post.html')
|
||||
|
||||
@community.route('/migrate-db')
|
||||
@login_required
|
||||
def migrate_database():
|
||||
"""Run database migration - for admin use"""
|
||||
if not current_user.is_admin:
|
||||
flash('Unauthorized access.', 'error')
|
||||
return redirect(url_for('community.index'))
|
||||
|
||||
try:
|
||||
from sqlalchemy import text
|
||||
|
||||
# Check if is_cover column exists in post_images table
|
||||
result = db.session.execute(text('PRAGMA table_info(post_images)'))
|
||||
columns = [row[1] for row in result.fetchall()]
|
||||
|
||||
if 'is_cover' not in columns:
|
||||
# Add the is_cover column
|
||||
db.session.execute(text('ALTER TABLE post_images ADD COLUMN is_cover BOOLEAN DEFAULT 0'))
|
||||
db.session.commit()
|
||||
flash('Successfully added is_cover column to post_images table', 'success')
|
||||
else:
|
||||
flash('is_cover column already exists', 'info')
|
||||
|
||||
return redirect(url_for('admin.index'))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f'Migration error: {str(e)}')
|
||||
flash(f'Migration error: {str(e)}', 'error')
|
||||
return redirect(url_for('admin.index'))
|
||||
|
||||
@community.route('/simple-form', methods=['GET'])
|
||||
@login_required
|
||||
def simple_form():
|
||||
"""Simple form for testing post creation"""
|
||||
return render_template('community/simple_form.html')
|
||||
|
||||
@community.route('/simple-test', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def simple_test():
|
||||
"""Very simple test post creation"""
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
# Simple form submission with minimal data
|
||||
title = request.form.get('title', 'Test Post').strip()
|
||||
|
||||
post = Post(
|
||||
title=title,
|
||||
subtitle="Simple test post",
|
||||
content="This is a simple test post to verify the system.",
|
||||
difficulty=3,
|
||||
media_folder=f"test_{uuid.uuid4().hex[:8]}",
|
||||
published=False,
|
||||
author_id=current_user.id
|
||||
)
|
||||
|
||||
db.session.add(post)
|
||||
db.session.commit()
|
||||
|
||||
flash('Test post created successfully!', 'success')
|
||||
return redirect(url_for('community.index'))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f'Error: {str(e)}', 'error')
|
||||
|
||||
return '''
|
||||
<form method="POST">
|
||||
<input type="text" name="title" placeholder="Post title" required>
|
||||
<button type="submit">Create Test Post</button>
|
||||
</form>
|
||||
'''
|
||||
@login_required
|
||||
def test_post():
|
||||
"""Simple test post creation for debugging"""
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
# Create a simple test post
|
||||
post = Post(
|
||||
title="Test Post - " + datetime.now().strftime('%H:%M:%S'),
|
||||
subtitle="Test subtitle",
|
||||
content="This is a test post created to verify the system is working.",
|
||||
difficulty=3,
|
||||
published=False, # Pending review
|
||||
author_id=current_user.id
|
||||
)
|
||||
|
||||
db.session.add(post)
|
||||
db.session.commit()
|
||||
|
||||
flash('Test post created successfully!', 'success')
|
||||
return redirect(url_for('community.index'))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f'Error creating test post: {str(e)}', 'error')
|
||||
return redirect(url_for('community.test_post'))
|
||||
|
||||
# Simple test form
|
||||
return '''
|
||||
<h2>Test Post Creation</h2>
|
||||
<form method="POST">
|
||||
<button type="submit">Create Test Post</button>
|
||||
</form>
|
||||
<a href="/community/">Back to Community</a>
|
||||
'''
|
||||
|
||||
@community.route('/post/<int:id>/comment', methods=['POST'])
|
||||
@login_required
|
||||
def add_comment(id):
|
||||
@@ -417,8 +711,9 @@ def serve_thumbnail(post_folder, filename):
|
||||
return serve_image(post_folder, filename)
|
||||
|
||||
@community.route('/media/posts/<post_folder>/gpx/<filename>')
|
||||
@login_required
|
||||
def serve_gpx(post_folder, filename):
|
||||
"""Serve GPX files for download"""
|
||||
"""Serve GPX files for download - requires login"""
|
||||
try:
|
||||
gpx_path = MediaConfig.get_media_path(current_app, post_folder, 'gpx')
|
||||
file_path = os.path.join(gpx_path, filename)
|
||||
|
||||
Reference in New Issue
Block a user