from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify from flask_login import login_required, current_user from app.models import Post, PostImage, GPXFile, User, Comment, Like from app.extensions import db from app.forms import PostForm, CommentForm from app.media_config import MediaConfig from werkzeug.utils import secure_filename from werkzeug.exceptions import RequestEntityTooLarge import os import uuid from datetime import datetime from PIL import Image import gpxpy community = Blueprint('community', __name__) @community.route('/') def index(): """Community main page with map and posts""" page = request.args.get('page', 1, type=int) posts = Post.query.filter_by(published=True).order_by(Post.created_at.desc()).paginate( page=page, per_page=12, error_out=False ) # Get posts with GPX files for map display posts_with_routes = Post.query.filter_by(published=True).join(GPXFile).all() return render_template('community/index.html', posts=posts, posts_with_routes=posts_with_routes) @community.route('/post/') def post_detail(id): """Individual post detail page""" post = Post.query.get_or_404(id) if not post.published and (not current_user.is_authenticated or (current_user.id != post.author_id and not current_user.is_admin)): flash('Post not found.', 'error') return redirect(url_for('community.index')) form = CommentForm() comments = Comment.query.filter_by(post_id=id).order_by(Comment.created_at.asc()).all() return render_template('community/post_detail.html', post=post, form=form, comments=comments) @community.route('/new-post', methods=['GET', 'POST']) @login_required def new_post(): """Create new post page""" if request.method == 'POST': try: # 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) # Validation if not title: return jsonify({'success': False, 'error': 'Title is required'}) if not difficulty or difficulty < 1 or difficulty > 5: return jsonify({'success': False, 'error': 'Valid difficulty level is required'}) if not content: return jsonify({'success': False, 'error': 'Content is required'}) # Create post post = Post( title=title, subtitle=subtitle, content=content, difficulty=difficulty, media_folder=f"post_{uuid.uuid4().hex[:8]}_{datetime.now().strftime('%Y%m%d')}", published=True, # Auto-publish for now author_id=current_user.id ) db.session.add(post) db.session.flush() # Get the 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) # Create subfolders for different media types 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 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) # Handle GPX file upload 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) db.session.commit() return jsonify({ 'success': True, 'message': 'Adventure shared successfully!', 'redirect_url': url_for('community.post_detail', id=post.id) }) 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'}) # GET request - show the form return render_template('community/new_post.html') @community.route('/post//comment', methods=['POST']) @login_required def add_comment(id): """Add comment to post""" post = Post.query.get_or_404(id) form = CommentForm() if form.validate_on_submit(): comment = Comment( content=form.content.data, author_id=current_user.id, post_id=post.id ) db.session.add(comment) db.session.commit() flash('Your comment has been added.', 'success') return redirect(url_for('community.post_detail', id=id)) @community.route('/post//add-images', methods=['POST']) @login_required def add_images_to_post(id): """Add additional images to an existing post""" post = Post.query.get_or_404(id) # Check if user owns the post or is admin if current_user.id != post.author_id and not current_user.is_admin: return jsonify({'success': False, 'error': 'Unauthorized'}) try: uploaded_images = [] # Handle multiple image uploads if 'images' in request.files: files = request.files.getlist('images') for image_file in files: if image_file and image_file.filename and MediaConfig.is_allowed_file(image_file.filename, 'images'): result = save_image(image_file, post.id) if result['success']: # Create PostImage record post_image = PostImage( filename=result['filename'], original_name=image_file.filename, size=result['size'], mime_type=result['mime_type'], post_id=post.id, is_cover=False, description=request.form.get(f'description_{image_file.filename}', '') ) db.session.add(post_image) uploaded_images.append({ 'filename': result['filename'], 'original_name': image_file.filename, 'url': post_image.get_url(), 'thumbnail_url': post_image.get_thumbnail_url() }) db.session.commit() return jsonify({ 'success': True, 'message': f'Successfully uploaded {len(uploaded_images)} images', 'images': uploaded_images }) except Exception as e: db.session.rollback() current_app.logger.error(f'Error adding images to post: {str(e)}') return jsonify({'success': False, 'error': 'An error occurred while uploading images'}) @community.route('/post//like', methods=['POST']) @login_required def toggle_like(id): """Toggle like on post""" post = Post.query.get_or_404(id) existing_like = Like.query.filter_by(user_id=current_user.id, post_id=post.id).first() if existing_like: db.session.delete(existing_like) liked = False else: like = Like(user_id=current_user.id, post_id=post.id) db.session.add(like) liked = True db.session.commit() return jsonify({'liked': liked, 'count': post.get_like_count()}) def save_image(image_file, post_id): """Save uploaded image file with thumbnails""" try: # Get post to access media folder post = Post.query.get(post_id) if not post or not post.media_folder: raise ValueError("Post or media folder not found") # Validate file type and MIME type if not MediaConfig.is_allowed_file(image_file.filename, 'images'): raise ValueError("File type not allowed") # Create upload directory for this post upload_dir = MediaConfig.get_media_path(current_app, post.media_folder, 'images') os.makedirs(upload_dir, exist_ok=True) # Create thumbnails directory thumbnails_dir = os.path.join(upload_dir, 'thumbnails') os.makedirs(thumbnails_dir, exist_ok=True) # Generate unique filename file_ext = image_file.filename.rsplit('.', 1)[1].lower() filename = f"{uuid.uuid4().hex}.{file_ext}" filepath = os.path.join(upload_dir, filename) thumbnail_path = os.path.join(thumbnails_dir, filename) # Open and process image image = Image.open(image_file) if image.mode in ('RGBA', 'LA', 'P'): image = image.convert('RGB') # Resize main image if too large image.thumbnail(MediaConfig.IMAGE_MAX_SIZE, Image.Resampling.LANCZOS) image.save(filepath, 'JPEG', quality=MediaConfig.IMAGE_QUALITY, optimize=True) # Create thumbnail thumbnail = image.copy() thumbnail.thumbnail(MediaConfig.THUMBNAIL_SIZE, Image.Resampling.LANCZOS) thumbnail.save(thumbnail_path, 'JPEG', quality=MediaConfig.IMAGE_QUALITY, optimize=True) file_size = os.path.getsize(filepath) return { 'success': True, 'filename': filename, 'size': file_size, 'mime_type': 'image/jpeg', 'has_thumbnail': True } except Exception as e: current_app.logger.error(f'Error saving image: {str(e)}') return {'success': False, 'error': str(e)} def save_gpx_file(gpx_file, post_id): """Save uploaded GPX file with validation""" try: # Get post to access media folder post = Post.query.get(post_id) if not post or not post.media_folder: raise ValueError("Post or media folder not found") # Validate file type if not MediaConfig.is_allowed_file(gpx_file.filename, 'gpx'): raise ValueError("File type not allowed") # Create upload directory for this post upload_dir = MediaConfig.get_media_path(current_app, post.media_folder, 'gpx') os.makedirs(upload_dir, exist_ok=True) # Generate unique filename file_ext = gpx_file.filename.rsplit('.', 1)[1].lower() filename = f"{uuid.uuid4().hex}.{file_ext}" filepath = os.path.join(upload_dir, filename) # Validate GPX file content gpx_content = gpx_file.read() try: gpx = gpxpy.parse(gpx_content.decode('utf-8')) # Check if GPX has any tracks or waypoints if not gpx.tracks and not gpx.waypoints and not gpx.routes: raise ValueError("GPX file appears to be empty or invalid") except Exception as e: raise ValueError(f"Invalid GPX file: {str(e)}") # Save file with open(filepath, 'wb') as f: f.write(gpx_content) file_size = len(gpx_content) # Extract basic statistics total_distance = 0 total_points = 0 for track in gpx.tracks: total_distance += track.length_2d() or 0 for segment in track.segments: total_points += len(segment.points) return { 'success': True, 'filename': filename, 'size': file_size, 'stats': { 'total_distance': round(total_distance / 1000, 2) if total_distance else 0, # Convert to km 'total_points': total_points, 'tracks_count': len(gpx.tracks), 'waypoints_count': len(gpx.waypoints) } } except Exception as e: current_app.logger.error(f'Error saving GPX file: {str(e)}') return { 'success': False, 'error': str(e) } @community.route('/api/routes') def api_routes(): """API endpoint to get all routes for map display""" posts_with_routes = Post.query.filter_by(published=True).join(GPXFile).all() routes_data = [] for post in posts_with_routes: for gpx_file in post.gpx_files: try: # Read and parse GPX file using new folder structure if post.media_folder: gpx_path = os.path.join(current_app.root_path, 'static', 'media', 'posts', post.media_folder, 'gpx', gpx_file.filename) else: # Fallback to old path for existing files gpx_path = os.path.join(current_app.instance_path, 'uploads', 'gpx', gpx_file.filename) if os.path.exists(gpx_path): with open(gpx_path, 'r') as f: gpx_content = f.read() gpx = gpxpy.parse(gpx_content) # Extract coordinates coordinates = [] for track in gpx.tracks: for segment in track.segments: for point in segment.points: coordinates.append([point.latitude, point.longitude]) if coordinates: routes_data.append({ 'id': post.id, 'title': post.title, 'author': post.author.nickname, 'coordinates': coordinates, 'url': url_for('community.post_detail', id=post.id) }) except Exception as e: current_app.logger.error(f'Error processing GPX file {gpx_file.filename}: {str(e)}') continue return jsonify(routes_data) @community.route('/media/posts//images/') def serve_image(post_folder, filename): """Serve post images with caching headers""" try: image_path = MediaConfig.get_media_path(current_app, post_folder, 'images') file_path = os.path.join(image_path, filename) if not os.path.exists(file_path): flash('Image not found.', 'error') return redirect(url_for('community.index')) from flask import send_file return send_file(file_path, as_attachment=False, cache_timeout=86400) # Cache for 24 hours except Exception as e: current_app.logger.error(f'Error serving image: {str(e)}') flash('Error loading image.', 'error') return redirect(url_for('community.index')) @community.route('/media/posts//images/thumbnails/') def serve_thumbnail(post_folder, filename): """Serve post image thumbnails with caching headers""" try: thumbnail_path = MediaConfig.get_media_path(current_app, post_folder, 'images') thumbnail_path = os.path.join(thumbnail_path, 'thumbnails', filename) if not os.path.exists(thumbnail_path): # Fallback to main image return serve_image(post_folder, filename) from flask import send_file return send_file(thumbnail_path, as_attachment=False, cache_timeout=86400) # Cache for 24 hours except Exception as e: current_app.logger.error(f'Error serving thumbnail: {str(e)}') return serve_image(post_folder, filename) @community.route('/media/posts//gpx/') def serve_gpx(post_folder, filename): """Serve GPX files for download""" try: gpx_path = MediaConfig.get_media_path(current_app, post_folder, 'gpx') file_path = os.path.join(gpx_path, filename) if not os.path.exists(file_path): flash('GPX file not found.', 'error') return redirect(url_for('community.index')) from flask import send_file return send_file(file_path, as_attachment=True, download_name=f'route_{post_folder}.gpx', mimetype='application/gpx+xml') except Exception as e: current_app.logger.error(f'Error serving GPX file: {str(e)}') flash('Error downloading GPX file.', 'error') return redirect(url_for('community.index'))