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

305
app/routes/instructions.py Normal file
View File

@@ -0,0 +1,305 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
from flask_login import login_required, current_user
from werkzeug.utils import secure_filename
from app import db
from app.models.set import Set
from app.models.instruction import Instruction
from app.services.file_handler import FileHandler
import os
instructions_bp = Blueprint('instructions', __name__, url_prefix='/instructions')
@instructions_bp.route('/upload/<int:set_id>', methods=['GET', 'POST'])
@login_required
def upload(set_id):
"""Upload instruction files for a specific set."""
lego_set = Set.query.get_or_404(set_id)
if request.method == 'POST':
# Check if files were uploaded
if 'files[]' not in request.files:
flash('No files selected.', 'danger')
return redirect(request.url)
files = request.files.getlist('files[]')
uploaded_count = 0
for file in files:
if file and file.filename and FileHandler.allowed_file(file.filename):
try:
# Determine file type
file_type = FileHandler.get_file_type(file.filename)
# Save file and generate thumbnail
file_path, file_size, thumbnail_path = FileHandler.save_file(
file,
lego_set.set_number,
file_type
)
# Determine page number for images
page_number = 1
if file_type == 'IMAGE':
# Get the highest page number for this set
max_page = db.session.query(
db.func.max(Instruction.page_number)
).filter_by(
set_id=set_id,
file_type='IMAGE'
).scalar()
page_number = (max_page or 0) + 1
# Create instruction record
instruction = Instruction(
set_id=set_id,
file_type=file_type,
file_path=file_path,
file_name=secure_filename(file.filename),
file_size=file_size,
page_number=page_number,
thumbnail_path=thumbnail_path,
user_id=current_user.id
)
db.session.add(instruction)
uploaded_count += 1
except Exception as e:
flash(f'Error uploading {file.filename}: {str(e)}', 'danger')
continue
if uploaded_count > 0:
db.session.commit()
flash(f'Successfully uploaded {uploaded_count} file(s)!', 'success')
else:
flash('No files were uploaded.', 'warning')
return redirect(url_for('sets.view_set', set_id=set_id))
return render_template('instructions/upload.html', set=lego_set)
@instructions_bp.route('/<int:instruction_id>/delete', methods=['POST'])
@login_required
def delete(instruction_id):
"""Delete an instruction file."""
instruction = Instruction.query.get_or_404(instruction_id)
set_id = instruction.set_id
# Delete the physical file
FileHandler.delete_file(instruction.file_path)
# Delete thumbnail if exists
if instruction.thumbnail_path:
FileHandler.delete_file(instruction.thumbnail_path)
# Delete the database record
db.session.delete(instruction)
db.session.commit()
flash('Instruction file deleted successfully.', 'success')
return redirect(url_for('sets.view_set', set_id=set_id))
@instructions_bp.route('/delete-all-images/<int:set_id>', methods=['POST'])
@login_required
def delete_all_images(set_id):
"""Delete all image instructions for a set."""
lego_set = Set.query.get_or_404(set_id)
# Get all image instructions
image_instructions = Instruction.query.filter_by(
set_id=set_id,
file_type='IMAGE'
).all()
count = len(image_instructions)
# Delete each one
for instruction in image_instructions:
# Delete physical file
FileHandler.delete_file(instruction.file_path)
# Delete thumbnail if exists
if instruction.thumbnail_path:
FileHandler.delete_file(instruction.thumbnail_path)
# Delete database record
db.session.delete(instruction)
db.session.commit()
flash(f'Successfully deleted {count} image instruction(s).', 'success')
return redirect(url_for('sets.view_set', set_id=set_id))
@instructions_bp.route('/<int:instruction_id>/view')
@login_required
def view(instruction_id):
"""View a specific instruction file."""
instruction = Instruction.query.get_or_404(instruction_id)
from flask import current_app
file_path = os.path.join(
current_app.config['UPLOAD_FOLDER'],
instruction.file_path
)
if not os.path.exists(file_path):
flash('File not found.', 'danger')
return redirect(url_for('sets.view_set', set_id=instruction.set_id))
return send_file(file_path)
@instructions_bp.route('/<int:set_id>/reorder', methods=['POST'])
@login_required
def reorder(set_id):
"""Reorder image instructions for a set."""
lego_set = Set.query.get_or_404(set_id)
# Get new order from request
new_order = request.json.get('order', [])
if not new_order:
return jsonify({'error': 'No order provided'}), 400
try:
# Update page numbers
for index, instruction_id in enumerate(new_order, start=1):
instruction = Instruction.query.get(instruction_id)
if instruction and instruction.set_id == set_id:
instruction.page_number = index
db.session.commit()
return jsonify({'success': True})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@instructions_bp.route('/bulk-upload/<int:set_id>', methods=['POST'])
@login_required
def bulk_upload(set_id):
"""Handle bulk upload via AJAX."""
lego_set = Set.query.get_or_404(set_id)
if 'file' not in request.files:
return jsonify({'error': 'No file provided'}), 400
file = request.files['file']
if not file or not file.filename:
return jsonify({'error': 'Invalid file'}), 400
if not FileHandler.allowed_file(file.filename):
return jsonify({'error': 'File type not allowed'}), 400
try:
# Determine file type
file_type = FileHandler.get_file_type(file.filename)
# Save file
file_path, file_size = FileHandler.save_file(
file,
lego_set.set_number,
file_type
)
# Determine page number for images
page_number = 1
if file_type == 'IMAGE':
max_page = db.session.query(
db.func.max(Instruction.page_number)
).filter_by(
set_id=set_id,
file_type='IMAGE'
).scalar()
page_number = (max_page or 0) + 1
# Create instruction record
instruction = Instruction(
set_id=set_id,
file_type=file_type,
file_path=file_path,
file_name=secure_filename(file.filename),
file_size=file_size,
page_number=page_number,
user_id=current_user.id
)
db.session.add(instruction)
db.session.commit()
return jsonify({
'success': True,
'instruction': instruction.to_dict()
})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@instructions_bp.route('/viewer/<int:set_id>')
@login_required
def image_viewer(set_id):
"""View image instructions in a scrollable PDF-like viewer."""
lego_set = Set.query.get_or_404(set_id)
# Get all image instructions sorted by page number
image_instructions = lego_set.image_instructions
if not image_instructions:
flash('No image instructions available for this set.', 'info')
return redirect(url_for('sets.view_set', set_id=set_id))
return render_template('instructions/viewer.html',
set=lego_set,
images=image_instructions)
@instructions_bp.route('/debug/<int:set_id>')
@login_required
def debug_paths(set_id):
"""Debug endpoint to check instruction paths."""
from flask import current_app
lego_set = Set.query.get_or_404(set_id)
debug_info = {
'set_number': lego_set.set_number,
'set_name': lego_set.set_name,
'upload_folder': current_app.config['UPLOAD_FOLDER'],
'instructions': []
}
for instruction in lego_set.instructions:
file_path = instruction.file_path.replace('\\', '/')
full_path = os.path.join(current_app.config['UPLOAD_FOLDER'], instruction.file_path)
info = {
'id': instruction.id,
'file_name': instruction.file_name,
'file_type': instruction.file_type,
'page_number': instruction.page_number,
'db_path': instruction.file_path,
'clean_path': file_path,
'full_disk_path': full_path,
'file_exists': os.path.exists(full_path),
'web_url': f'/static/uploads/{file_path}'
}
if instruction.thumbnail_path:
thumb_clean = instruction.thumbnail_path.replace('\\', '/')
thumb_full = os.path.join(current_app.config['UPLOAD_FOLDER'], instruction.thumbnail_path)
info['thumbnail_db'] = instruction.thumbnail_path
info['thumbnail_clean'] = thumb_clean
info['thumbnail_full'] = thumb_full
info['thumbnail_exists'] = os.path.exists(thumb_full)
info['thumbnail_url'] = f'/static/uploads/{thumb_clean}'
debug_info['instructions'].append(info)
return jsonify(debug_info)