diff --git a/app/routes/sets.py b/app/routes/sets.py index 63e1497..1944c14 100644 --- a/app/routes/sets.py +++ b/app/routes/sets.py @@ -3,6 +3,9 @@ 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 @@ -46,8 +49,8 @@ def list_sets(): year_filter = request.args.get('year', type=int) search_query = request.args.get('q', '') - # Build query - query = Set.query + # Only what this user is allowed to see. + query = Set.visible_to(current_user) # Apply filters if theme_filter: @@ -79,12 +82,18 @@ def list_sets(): 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] + # 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, @@ -107,10 +116,22 @@ def view_set(set_id): 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) + 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']) @@ -162,6 +183,7 @@ def add_set(): is_moc=is_moc, moc_designer=moc_designer, moc_description=moc_description, + visibility=SiteSetting.get('default_visibility', 'private'), user_id=current_user.id ) @@ -272,3 +294,76 @@ def search_brickset(): }) 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))