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.set_share import SetShare from app.models.site_setting import SiteSetting, VISIBILITIES from app.models.user import User 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/') @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', '') # Only what this user is allowed to see. query = Set.visible_to(current_user) # 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) # Filter options limited to visible sets too, so the dropdowns don't # leak the existence of themes only present in private libraries. visible_ids = [row.id for row in Set.visible_to(current_user).with_entities(Set.id).all()] if visible_ids: themes = [t[0] for t in db.session.query(Set.theme).filter( Set.id.in_(visible_ids)).distinct().order_by(Set.theme).all()] years = [y[0] for y in db.session.query(Set.year_released).filter( Set.id.in_(visible_ids)).distinct().order_by( Set.year_released.desc()).all()] else: themes, 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('/') @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 can_manage = lego_set.can_manage(current_user) shared_with = lego_set.shared_with() if can_manage else [] shareable_users = [] if can_manage: shared_ids = {u.id for u in shared_with} shareable_users = [u for u in User.query.order_by(User.username).all() if u.id != lego_set.user_id and u.id not in shared_ids] return render_template('sets/detail.html', set=lego_set, pdf_instructions=pdf_instructions, image_instructions=image_instructions, can_manage=can_manage, shared_with=shared_with, shareable_users=shareable_users, visibilities=VISIBILITIES) @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, visibility=SiteSetting.get('default_visibility', 'private'), 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('//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('//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) @sets_bp.route('//visibility', methods=['POST']) @login_required def set_visibility(set_id): """Change who can see a set. Owner or admin only (enforced by the guard).""" lego_set = Set.query.get_or_404(set_id) visibility = request.form.get('visibility', '').strip() if visibility not in VISIBILITIES: flash('Unknown visibility setting.', 'danger') return redirect(url_for('sets.view_set', set_id=set_id)) lego_set.visibility = visibility db.session.commit() note = { 'private': 'Only you and administrators can see this set now.', 'shared': 'This set is now visible to the people you share it with.', 'public': 'Every logged-in user can see this set now.', }[visibility] flash(note, 'success') return redirect(url_for('sets.view_set', set_id=set_id)) @sets_bp.route('//share', methods=['POST']) @login_required def share_set(set_id): """Grant another user access to a shared set.""" lego_set = Set.query.get_or_404(set_id) user_id = request.form.get('user_id', type=int) user = User.query.get(user_id) if user_id else None if not user: flash('Pick a user to share with.', 'warning') return redirect(url_for('sets.view_set', set_id=set_id)) if user.id == lego_set.user_id: flash('That user already owns this set.', 'info') return redirect(url_for('sets.view_set', set_id=set_id)) existing = SetShare.query.filter_by(set_id=set_id, user_id=user.id).first() if existing: flash(f'Already shared with {user.username}.', 'info') return redirect(url_for('sets.view_set', set_id=set_id)) db.session.add(SetShare(set_id=set_id, user_id=user.id)) # Sharing implies shared visibility — otherwise the share does nothing. if lego_set.visibility == 'private': lego_set.visibility = 'shared' flash(f'Shared with {user.username}. Visibility changed to Shared.', 'success') else: flash(f'Shared with {user.username}.', 'success') db.session.commit() return redirect(url_for('sets.view_set', set_id=set_id)) @sets_bp.route('//unshare/', methods=['POST']) @login_required def unshare_set(set_id, user_id): """Revoke a user's access to a shared set.""" share = SetShare.query.filter_by(set_id=set_id, user_id=user_id).first() if not share: flash('That share no longer exists.', 'warning') return redirect(url_for('sets.view_set', set_id=set_id)) username = share.user.username if share.user else 'user' db.session.delete(share) db.session.commit() flash(f'Removed access for {username}.', 'success') return redirect(url_for('sets.view_set', set_id=set_id))