- Add media_folder field to Post model for organized file storage
- Create MediaConfig class for centralized media management settings
- Update community routes to use post-specific media folders
- Add thumbnail generation for uploaded images
- Implement structured folder layout: app/static/media/posts/{post_folder}/
- Add utility functions for image and GPX file handling
- Create media management script for migration and maintenance
- Add proper file validation and MIME type checking
- Include routes for serving images, thumbnails, and GPX files
- Maintain backward compatibility with existing uploads
- Add comprehensive documentation and migration tools
Each post now gets its own media folder with subfolders for:
- images/ (with thumbnails/ subfolder)
- gpx/
Post content remains in database for optimal query performance while
media files are organized in dedicated folders for better management.
170 lines
6.9 KiB
Python
170 lines
6.9 KiB
Python
from datetime import datetime
|
|
import os
|
|
from flask_login import UserMixin
|
|
from werkzeug.security import generate_password_hash, check_password_hash
|
|
from app.extensions import db
|
|
|
|
class User(UserMixin, db.Model):
|
|
__tablename__ = 'users'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
nickname = db.Column(db.String(80), unique=True, nullable=False)
|
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
|
password_hash = db.Column(db.String(255), nullable=False)
|
|
is_active = db.Column(db.Boolean, default=True, nullable=False)
|
|
is_admin = db.Column(db.Boolean, default=False, nullable=False)
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
# Relationships
|
|
posts = db.relationship('Post', backref='author', lazy='dynamic', cascade='all, delete-orphan')
|
|
comments = db.relationship('Comment', backref='author', lazy='dynamic', cascade='all, delete-orphan')
|
|
likes = db.relationship('Like', backref='user', lazy='dynamic', cascade='all, delete-orphan')
|
|
|
|
def set_password(self, password):
|
|
self.password_hash = generate_password_hash(password)
|
|
|
|
def check_password(self, password):
|
|
return check_password_hash(self.password_hash, password)
|
|
|
|
def __repr__(self):
|
|
return f'<User {self.nickname}>'
|
|
|
|
class Post(db.Model):
|
|
__tablename__ = 'posts'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
title = db.Column(db.String(200), nullable=False)
|
|
subtitle = db.Column(db.String(300))
|
|
content = db.Column(db.Text, nullable=False)
|
|
difficulty = db.Column(db.Integer, default=3, nullable=False) # 1-5 scale
|
|
media_folder = db.Column(db.String(100)) # Folder name for media files
|
|
published = db.Column(db.Boolean, default=False, nullable=False)
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
# Foreign Keys
|
|
author_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
|
|
|
# Relationships
|
|
images = db.relationship('PostImage', backref='post', lazy='dynamic', cascade='all, delete-orphan')
|
|
gpx_files = db.relationship('GPXFile', backref='post', lazy='dynamic', cascade='all, delete-orphan')
|
|
comments = db.relationship('Comment', backref='post', lazy='dynamic', cascade='all, delete-orphan')
|
|
likes = db.relationship('Like', backref='post', lazy='dynamic', cascade='all, delete-orphan')
|
|
|
|
def get_difficulty_label(self):
|
|
labels = ['Very Easy', 'Easy', 'Moderate', 'Hard', 'Very Hard']
|
|
return labels[self.difficulty - 1] if 1 <= self.difficulty <= 5 else 'Unknown'
|
|
|
|
def get_like_count(self):
|
|
return self.likes.count()
|
|
|
|
def get_media_folder_path(self):
|
|
"""Get the full path to the post's media folder"""
|
|
if self.media_folder:
|
|
return os.path.join('static', 'media', 'posts', self.media_folder)
|
|
return None
|
|
|
|
def get_media_url_path(self):
|
|
"""Get the URL path to the post's media folder"""
|
|
if self.media_folder:
|
|
return f'/static/media/posts/{self.media_folder}'
|
|
return None
|
|
|
|
def __repr__(self):
|
|
return f'<Post {self.title}>'
|
|
|
|
class PostImage(db.Model):
|
|
__tablename__ = 'post_images'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
filename = db.Column(db.String(255), nullable=False)
|
|
original_name = db.Column(db.String(255), nullable=False)
|
|
description = db.Column(db.Text)
|
|
size = db.Column(db.Integer, nullable=False)
|
|
mime_type = db.Column(db.String(100), default='image/jpeg', nullable=False)
|
|
is_cover = db.Column(db.Boolean, default=False, nullable=False)
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
|
|
# Foreign Keys
|
|
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'), nullable=False)
|
|
|
|
def get_url(self):
|
|
"""Get the URL path to access this image"""
|
|
if self.post.media_folder:
|
|
return f'/static/media/posts/{self.post.media_folder}/images/{self.filename}'
|
|
return f'/static/uploads/images/{self.filename}' # Fallback for old files
|
|
|
|
def get_thumbnail_url(self):
|
|
"""Get the URL path to access this image's thumbnail"""
|
|
if self.post.media_folder:
|
|
return f'/static/media/posts/{self.post.media_folder}/images/thumbnails/{self.filename}'
|
|
return self.get_url() # Fallback to main image for old files
|
|
|
|
def has_thumbnail(self):
|
|
"""Check if thumbnail exists for this image"""
|
|
if not self.post.media_folder:
|
|
return False
|
|
|
|
from flask import current_app
|
|
thumbnail_path = os.path.join(
|
|
current_app.root_path, 'static', 'media', 'posts',
|
|
self.post.media_folder, 'images', 'thumbnails', self.filename
|
|
)
|
|
return os.path.exists(thumbnail_path)
|
|
|
|
def __repr__(self):
|
|
return f'<PostImage {self.filename}>'
|
|
|
|
class GPXFile(db.Model):
|
|
__tablename__ = 'gpx_files'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
filename = db.Column(db.String(255), nullable=False)
|
|
original_name = db.Column(db.String(255), nullable=False)
|
|
size = db.Column(db.Integer, nullable=False)
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
|
|
# Foreign Keys
|
|
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'), nullable=False)
|
|
|
|
def get_url(self):
|
|
"""Get the URL path to access this GPX file"""
|
|
if self.post.media_folder:
|
|
return f'/static/media/posts/{self.post.media_folder}/gpx/{self.filename}'
|
|
return f'/static/uploads/gpx/{self.filename}' # Fallback for old files
|
|
|
|
def __repr__(self):
|
|
return f'<GPXFile {self.filename}>'
|
|
|
|
class Comment(db.Model):
|
|
__tablename__ = 'comments'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
content = db.Column(db.Text, nullable=False)
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
# Foreign Keys
|
|
author_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
|
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'), nullable=False)
|
|
|
|
def __repr__(self):
|
|
return f'<Comment {self.id}>'
|
|
|
|
class Like(db.Model):
|
|
__tablename__ = 'likes'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
|
|
# Foreign Keys
|
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
|
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'), nullable=False)
|
|
|
|
# Unique constraint
|
|
__table_args__ = (db.UniqueConstraint('user_id', 'post_id', name='unique_user_post_like'),)
|
|
|
|
def __repr__(self):
|
|
return f'<Like {self.user_id}-{self.post_id}>'
|