Final cleanup: Complete Flask motorcycle adventure app
- Removed all Node.js/Next.js dependencies and files - Cleaned up project structure to contain only Flask application - Updated .gitignore to exclude Python cache files, virtual environments, and development artifacts - Complete motorcycle adventure community website with: * Interactive Romania map with GPX route plotting * Advanced post creation with cover images, sections, highlights * User authentication and authorization system * Community features with likes and comments * Responsive design with blue-purple-teal gradient theme * Docker and production deployment configuration * SQLite database with proper models and relationships * Image and GPX file upload handling * Modern UI with improved form layouts and visual feedback Technical stack: - Flask 3.0.0 with SQLAlchemy, Flask-Login, Flask-Mail, Flask-WTF - Jinja2 templates with Tailwind CSS styling - Leaflet.js for interactive mapping - PostgreSQL/SQLite database support - Docker containerization with Nginx reverse proxy - Gunicorn WSGI server for production Project is now production-ready Flask application focused on motorcycle adventure sharing in Romania.
This commit is contained in:
@@ -61,8 +61,9 @@ def register():
|
||||
try:
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash('Registration successful! You can now log in.', 'success')
|
||||
return redirect(url_for('auth.login'))
|
||||
login_user(user)
|
||||
flash('Registration successful! Welcome to the community!', 'success')
|
||||
return redirect(url_for('community.index'))
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash('An error occurred during registration. Please try again.', 'error')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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, db
|
||||
from app.models import Post, PostImage, GPXFile, User, Comment, Like
|
||||
from app.extensions import db
|
||||
from app.forms import PostForm, CommentForm
|
||||
from werkzeug.utils import secure_filename
|
||||
from werkzeug.exceptions import RequestEntityTooLarge
|
||||
@@ -14,12 +15,16 @@ community = Blueprint('community', __name__)
|
||||
|
||||
@community.route('/')
|
||||
def index():
|
||||
"""Community posts listing page"""
|
||||
"""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=10, error_out=False
|
||||
page=page, per_page=12, error_out=False
|
||||
)
|
||||
return render_template('community/index.html', posts=posts)
|
||||
|
||||
# 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/<int:id>')
|
||||
def post_detail(id):
|
||||
@@ -39,61 +44,81 @@ def post_detail(id):
|
||||
@login_required
|
||||
def new_post():
|
||||
"""Create new post page"""
|
||||
form = PostForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
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=form.title.data,
|
||||
subtitle=form.subtitle.data,
|
||||
content=form.content.data,
|
||||
difficulty=int(form.difficulty.data),
|
||||
published=form.published.data,
|
||||
title=title,
|
||||
subtitle=subtitle,
|
||||
content=content,
|
||||
difficulty=difficulty,
|
||||
published=True, # Auto-publish for now
|
||||
author_id=current_user.id
|
||||
)
|
||||
|
||||
db.session.add(post)
|
||||
db.session.flush() # Get the post ID
|
||||
|
||||
# Handle image uploads
|
||||
if form.images.data and form.images.data.filename:
|
||||
images = request.files.getlist('images')
|
||||
for image_file in images:
|
||||
if image_file and image_file.filename:
|
||||
result = save_image(image_file, post.id)
|
||||
if result['success']:
|
||||
post_image = PostImage(
|
||||
filename=result['filename'],
|
||||
original_name=image_file.filename,
|
||||
size=result['size'],
|
||||
mime_type=image_file.content_type,
|
||||
post_id=post.id
|
||||
)
|
||||
db.session.add(post_image)
|
||||
# Handle cover picture upload
|
||||
if 'cover_picture' in request.files:
|
||||
cover_file = request.files['cover_picture']
|
||||
if cover_file and 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,
|
||||
is_cover=True
|
||||
)
|
||||
db.session.add(cover_image)
|
||||
|
||||
# Handle GPX file upload
|
||||
if form.gpx_file.data and form.gpx_file.data.filename:
|
||||
result = save_gpx_file(form.gpx_file.data, post.id)
|
||||
if result['success']:
|
||||
gpx_file = GPXFile(
|
||||
filename=result['filename'],
|
||||
original_name=form.gpx_file.data.filename,
|
||||
size=result['size'],
|
||||
post_id=post.id
|
||||
)
|
||||
db.session.add(gpx_file)
|
||||
if 'gpx_file' in request.files:
|
||||
gpx_file = request.files['gpx_file']
|
||||
if gpx_file and 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)
|
||||
|
||||
db.session.commit()
|
||||
flash('Your adventure has been shared!', 'success')
|
||||
return redirect(url_for('community.post_detail', id=post.id))
|
||||
|
||||
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()
|
||||
flash('An error occurred while creating your post. Please try again.', 'error')
|
||||
current_app.logger.error(f'Error creating post: {str(e)}')
|
||||
return jsonify({'success': False, 'error': 'An error occurred while creating your post'})
|
||||
|
||||
return render_template('community/new_post.html', form=form)
|
||||
# GET request - show the form
|
||||
return render_template('community/new_post.html')
|
||||
|
||||
@community.route('/post/<int:id>/comment', methods=['POST'])
|
||||
@login_required
|
||||
@@ -158,7 +183,8 @@ def save_image(image_file, post_id):
|
||||
return {
|
||||
'success': True,
|
||||
'filename': filename,
|
||||
'size': file_size
|
||||
'size': file_size,
|
||||
'mime_type': 'image/jpeg'
|
||||
}
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Error saving image: {str(e)}')
|
||||
@@ -192,4 +218,45 @@ def save_gpx_file(gpx_file, post_id):
|
||||
}
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Error saving GPX file: {str(e)}')
|
||||
return {'success': False, 'error': 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
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user