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/', 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('//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/', 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('//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('//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/', 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/') @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/') @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)