Initial commit - LEGO Instructions Manager v1.5.0

This commit is contained in:
2025-12-09 17:20:41 +11:00
commit 63496b1ccd
68 changed files with 9131 additions and 0 deletions

274
app/routes/sets.py Normal file
View File

@@ -0,0 +1,274 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, current_app
from flask_login import login_required, current_user
import os
from app import db
from app.models.set import Set
from app.models.instruction import Instruction
from app.services.brickset_api import BricksetAPI
from app.services.file_handler import FileHandler
sets_bp = Blueprint('sets', __name__, url_prefix='/sets')
@sets_bp.route('/debug/<int:set_id>')
@login_required
def debug_set_image(set_id):
"""Debug route to check image paths."""
lego_set = Set.query.get_or_404(set_id)
debug_info = {
'set_number': lego_set.set_number,
'set_name': lego_set.set_name,
'cover_image_db': lego_set.cover_image,
'image_url_db': lego_set.image_url,
'get_image_result': lego_set.get_image(),
'cover_image_exists': False,
'cover_image_full_path': None
}
# Check if file actually exists
if lego_set.cover_image:
from flask import current_app
full_path = os.path.join(current_app.config['UPLOAD_FOLDER'], lego_set.cover_image)
debug_info['cover_image_full_path'] = full_path
debug_info['cover_image_exists'] = os.path.exists(full_path)
return jsonify(debug_info)
@sets_bp.route('/')
@login_required
def list_sets():
"""List all LEGO sets with sorting and filtering."""
page = request.args.get('page', 1, type=int)
sort_by = request.args.get('sort', 'set_number')
theme_filter = request.args.get('theme', '')
year_filter = request.args.get('year', type=int)
search_query = request.args.get('q', '')
# Build query
query = Set.query
# Apply filters
if theme_filter:
query = query.filter(Set.theme == theme_filter)
if year_filter:
query = query.filter(Set.year_released == year_filter)
if search_query:
query = query.filter(
db.or_(
Set.set_name.ilike(f'%{search_query}%'),
Set.set_number.ilike(f'%{search_query}%')
)
)
# Apply sorting
if sort_by == 'set_number':
query = query.order_by(Set.set_number)
elif sort_by == 'name':
query = query.order_by(Set.set_name)
elif sort_by == 'theme':
query = query.order_by(Set.theme, Set.set_number)
elif sort_by == 'year':
query = query.order_by(Set.year_released.desc(), Set.set_number)
elif sort_by == 'newest':
query = query.order_by(Set.created_at.desc())
# Paginate
from flask import current_app
per_page = current_app.config.get('SETS_PER_PAGE', 20)
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
# Get unique themes and years for filters
themes = db.session.query(Set.theme).distinct().order_by(Set.theme).all()
themes = [t[0] for t in themes]
years = db.session.query(Set.year_released).distinct().order_by(Set.year_released.desc()).all()
years = [y[0] for y in years]
return render_template('sets/list.html',
sets=pagination.items,
pagination=pagination,
themes=themes,
years=years,
current_theme=theme_filter,
current_year=year_filter,
current_sort=sort_by,
search_query=search_query)
@sets_bp.route('/<int:set_id>')
@login_required
def view_set(set_id):
"""View detailed information about a specific set."""
lego_set = Set.query.get_or_404(set_id)
# Get instructions grouped by type
pdf_instructions = lego_set.pdf_instructions
image_instructions = lego_set.image_instructions
return render_template('sets/detail.html',
set=lego_set,
pdf_instructions=pdf_instructions,
image_instructions=image_instructions)
@sets_bp.route('/add', methods=['GET', 'POST'])
@login_required
def add_set():
"""Add a new LEGO set or MOC."""
if request.method == 'POST':
set_number = request.form.get('set_number', '').strip()
set_name = request.form.get('set_name', '').strip()
theme = request.form.get('theme', '').strip()
year_released = request.form.get('year_released', type=int)
piece_count = request.form.get('piece_count', type=int)
image_url = request.form.get('image_url', '').strip()
# MOC fields
is_moc = request.form.get('is_moc') == 'on'
moc_designer = request.form.get('moc_designer', '').strip() if is_moc else None
moc_description = request.form.get('moc_description', '').strip() if is_moc else None
# Validation
if not all([set_number, set_name, theme, year_released]):
flash('Set number, name, theme, and year are required.', 'danger')
return render_template('sets/add.html')
# Check if set already exists
if Set.query.filter_by(set_number=set_number).first():
flash(f'Set {set_number} already exists in the database.', 'warning')
return redirect(url_for('sets.list_sets'))
# Handle cover image upload
cover_image_path = None
if 'cover_image' in request.files:
file = request.files['cover_image']
if file and file.filename and FileHandler.allowed_file(file.filename):
try:
cover_image_path, _ = FileHandler.save_cover_image(file, set_number)
except Exception as e:
flash(f'Error uploading cover image: {str(e)}', 'warning')
# Create new set
new_set = Set(
set_number=set_number,
set_name=set_name,
theme=theme,
year_released=year_released,
piece_count=piece_count,
image_url=image_url,
cover_image=cover_image_path,
is_moc=is_moc,
moc_designer=moc_designer,
moc_description=moc_description,
user_id=current_user.id
)
db.session.add(new_set)
db.session.commit()
set_type = "MOC" if is_moc else "Set"
flash(f'{set_type} {set_number}: {set_name} added successfully!', 'success')
return redirect(url_for('sets.view_set', set_id=new_set.id))
return render_template('sets/add.html')
@sets_bp.route('/<int:set_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_set(set_id):
"""Edit an existing LEGO set or MOC."""
lego_set = Set.query.get_or_404(set_id)
if request.method == 'POST':
lego_set.set_name = request.form.get('set_name', '').strip()
lego_set.theme = request.form.get('theme', '').strip()
lego_set.year_released = request.form.get('year_released', type=int)
lego_set.piece_count = request.form.get('piece_count', type=int)
lego_set.image_url = request.form.get('image_url', '').strip()
# Update MOC fields
lego_set.is_moc = request.form.get('is_moc') == 'on'
lego_set.moc_designer = request.form.get('moc_designer', '').strip() if lego_set.is_moc else None
lego_set.moc_description = request.form.get('moc_description', '').strip() if lego_set.is_moc else None
# Handle cover image upload
if 'cover_image' in request.files:
file = request.files['cover_image']
if file and file.filename and FileHandler.allowed_file(file.filename):
try:
# Delete old cover image if exists
if lego_set.cover_image:
old_path = os.path.join(current_app.config['UPLOAD_FOLDER'], lego_set.cover_image)
if os.path.exists(old_path):
os.remove(old_path)
# Save new cover image
cover_image_path, _ = FileHandler.save_cover_image(file, lego_set.set_number)
lego_set.cover_image = cover_image_path
except Exception as e:
flash(f'Error uploading cover image: {str(e)}', 'warning')
# Option to remove cover image
if request.form.get('remove_cover_image') == 'on':
if lego_set.cover_image:
old_path = os.path.join(current_app.config['UPLOAD_FOLDER'], lego_set.cover_image)
if os.path.exists(old_path):
os.remove(old_path)
lego_set.cover_image = None
db.session.commit()
flash('Set updated successfully!', 'success')
return redirect(url_for('sets.view_set', set_id=set_id))
return render_template('sets/edit.html', set=lego_set)
@sets_bp.route('/<int:set_id>/delete', methods=['POST'])
@login_required
def delete_set(set_id):
"""Delete a LEGO set and all its instructions."""
lego_set = Set.query.get_or_404(set_id)
# Delete associated files
from app.services.file_handler import FileHandler
for instruction in lego_set.instructions:
FileHandler.delete_file(instruction.file_path)
db.session.delete(lego_set)
db.session.commit()
flash(f'Set {lego_set.set_number} deleted successfully.', 'success')
return redirect(url_for('sets.list_sets'))
@sets_bp.route('/search-brickset')
@login_required
def search_brickset():
"""Search for sets using Brickset API."""
query = request.args.get('q', '').strip()
if not query:
return jsonify({'error': 'Search query is required'}), 400
api = BricksetAPI()
# Try to search by set number first, then by name
results = api.search_sets(set_number=query)
if not results:
results = api.search_sets(query=query)
# Format results for JSON response
formatted_results = []
for result in results[:10]: # Limit to 10 results
formatted_results.append({
'setNumber': result.get('number'),
'name': result.get('name'),
'theme': result.get('theme'),
'year': result.get('year'),
'pieces': result.get('pieces'),
'imageUrl': result.get('image', {}).get('imageURL') if result.get('image') else None
})
return jsonify(formatted_results)