Add page organiser routes; fix bulk upload tuple unpack and harden reorder
This commit is contained in:
1 parent
2e2f2d66d6
commit
48c82ab763
1 file changed
+134
-30
+134
-30
@@ -5,6 +5,7 @@ from app import db
|
|||||||
from app.models.set import Set
|
from app.models.set import Set
|
||||||
from app.models.instruction import Instruction
|
from app.models.instruction import Instruction
|
||||||
from app.services.file_handler import FileHandler
|
from app.services.file_handler import FileHandler
|
||||||
|
from app.services import page_order
|
||||||
import os
|
import os
|
||||||
|
|
||||||
instructions_bp = Blueprint('instructions', __name__, url_prefix='/instructions')
|
instructions_bp = Blueprint('instructions', __name__, url_prefix='/instructions')
|
||||||
@@ -41,14 +42,7 @@ def upload(set_id):
|
|||||||
# Determine page number for images
|
# Determine page number for images
|
||||||
page_number = 1
|
page_number = 1
|
||||||
if file_type == 'IMAGE':
|
if file_type == 'IMAGE':
|
||||||
# Get the highest page number for this set
|
page_number = page_order.next_page_number(set_id)
|
||||||
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
|
# Create instruction record
|
||||||
instruction = Instruction(
|
instruction = Instruction(
|
||||||
@@ -156,25 +150,25 @@ def view(instruction_id):
|
|||||||
@instructions_bp.route('/<int:set_id>/reorder', methods=['POST'])
|
@instructions_bp.route('/<int:set_id>/reorder', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def reorder(set_id):
|
def reorder(set_id):
|
||||||
"""Reorder image instructions for a set."""
|
"""Save a new page order for a set's image instructions."""
|
||||||
lego_set = Set.query.get_or_404(set_id)
|
Set.query.get_or_404(set_id)
|
||||||
|
|
||||||
# Get new order from request
|
payload = request.get_json(silent=True) or {}
|
||||||
new_order = request.json.get('order', [])
|
new_order = payload.get('order', [])
|
||||||
|
|
||||||
if not new_order:
|
if not new_order:
|
||||||
return jsonify({'error': 'No order provided'}), 400
|
return jsonify({'error': 'No order provided'}), 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Update page numbers
|
pages = page_order.apply_order(set_id, new_order)
|
||||||
for index, instruction_id in enumerate(new_order, start=1):
|
return jsonify({
|
||||||
instruction = Instruction.query.get(instruction_id)
|
'success': True,
|
||||||
if instruction and instruction.set_id == set_id:
|
'pages': [p.to_dict() for p in pages],
|
||||||
instruction.page_number = index
|
'audit': page_order.audit(set_id),
|
||||||
|
})
|
||||||
db.session.commit()
|
except ValueError as e:
|
||||||
return jsonify({'success': True})
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
@@ -201,8 +195,10 @@ def bulk_upload(set_id):
|
|||||||
# Determine file type
|
# Determine file type
|
||||||
file_type = FileHandler.get_file_type(file.filename)
|
file_type = FileHandler.get_file_type(file.filename)
|
||||||
|
|
||||||
# Save file
|
# Save file (save_file returns three values; the thumbnail was being
|
||||||
file_path, file_size = FileHandler.save_file(
|
# dropped here, which blew up every bulk upload with a tuple-unpack
|
||||||
|
# error before it ever reached the database)
|
||||||
|
file_path, file_size, thumbnail_path = FileHandler.save_file(
|
||||||
file,
|
file,
|
||||||
lego_set.set_number,
|
lego_set.set_number,
|
||||||
file_type
|
file_type
|
||||||
@@ -211,13 +207,7 @@ def bulk_upload(set_id):
|
|||||||
# Determine page number for images
|
# Determine page number for images
|
||||||
page_number = 1
|
page_number = 1
|
||||||
if file_type == 'IMAGE':
|
if file_type == 'IMAGE':
|
||||||
max_page = db.session.query(
|
page_number = page_order.next_page_number(set_id)
|
||||||
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
|
# Create instruction record
|
||||||
instruction = Instruction(
|
instruction = Instruction(
|
||||||
@@ -227,6 +217,7 @@ def bulk_upload(set_id):
|
|||||||
file_name=secure_filename(file.filename),
|
file_name=secure_filename(file.filename),
|
||||||
file_size=file_size,
|
file_size=file_size,
|
||||||
page_number=page_number,
|
page_number=page_number,
|
||||||
|
thumbnail_path=thumbnail_path,
|
||||||
user_id=current_user.id
|
user_id=current_user.id
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -303,3 +294,116 @@ def debug_paths(set_id):
|
|||||||
debug_info['instructions'].append(info)
|
debug_info['instructions'].append(info)
|
||||||
|
|
||||||
return jsonify(debug_info)
|
return jsonify(debug_info)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Page organiser: fixing the order of image instructions after upload.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@instructions_bp.route('/<int:set_id>/organise')
|
||||||
|
@login_required
|
||||||
|
def organise(set_id):
|
||||||
|
"""Drag-and-drop page organiser for a set's image instructions."""
|
||||||
|
lego_set = Set.query.get_or_404(set_id)
|
||||||
|
pages = page_order.image_pages(set_id)
|
||||||
|
|
||||||
|
if not pages:
|
||||||
|
flash('There are no image pages to organise for this set.', 'info')
|
||||||
|
return redirect(url_for('sets.view_set', set_id=set_id))
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'instructions/organise.html',
|
||||||
|
set=lego_set,
|
||||||
|
pages=pages,
|
||||||
|
page_hints={p.id: page_order.page_hint(p.file_name or '') for p in pages},
|
||||||
|
strategies=page_order.strategy_choices(),
|
||||||
|
audit=page_order.audit(set_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@instructions_bp.route('/<int:set_id>/auto-sort', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def auto_sort(set_id):
|
||||||
|
"""Sort a set's pages by filename, detected page number, or upload order."""
|
||||||
|
Set.query.get_or_404(set_id)
|
||||||
|
|
||||||
|
payload = request.get_json(silent=True) or request.form
|
||||||
|
strategy = (payload.get('strategy') or 'filename').strip()
|
||||||
|
reverse = str(payload.get('reverse', '')).lower() in ('1', 'true', 'yes', 'on')
|
||||||
|
|
||||||
|
try:
|
||||||
|
if strategy == page_order.REVERSIBLE:
|
||||||
|
pages = page_order.reverse_order(set_id)
|
||||||
|
message = 'Page order reversed.'
|
||||||
|
else:
|
||||||
|
pages = page_order.auto_sort(set_id, strategy, reverse=reverse)
|
||||||
|
label = dict(page_order.strategy_choices()).get(strategy, strategy)
|
||||||
|
message = f'Sorted {len(pages)} pages by {label.lower()}.'
|
||||||
|
except ValueError as e:
|
||||||
|
if request.is_json:
|
||||||
|
return jsonify({'error': str(e)}), 400
|
||||||
|
flash(str(e), 'danger')
|
||||||
|
return redirect(url_for('instructions.organise', set_id=set_id))
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
if request.is_json:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
flash(f'Could not sort pages: {e}', 'danger')
|
||||||
|
return redirect(url_for('instructions.organise', set_id=set_id))
|
||||||
|
|
||||||
|
if request.is_json:
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': message,
|
||||||
|
'pages': [p.to_dict() for p in pages],
|
||||||
|
})
|
||||||
|
|
||||||
|
flash(message, 'success')
|
||||||
|
return redirect(url_for('instructions.organise', set_id=set_id))
|
||||||
|
|
||||||
|
|
||||||
|
@instructions_bp.route('/<int:set_id>/renumber', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def renumber(set_id):
|
||||||
|
"""Close gaps and clear duplicate page numbers without moving anything."""
|
||||||
|
Set.query.get_or_404(set_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pages = page_order.renumber(set_id)
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
if request.is_json:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
flash(f'Could not renumber pages: {e}', 'danger')
|
||||||
|
return redirect(url_for('instructions.organise', set_id=set_id))
|
||||||
|
|
||||||
|
message = f'Renumbered {len(pages)} pages 1 to {len(pages)}.'
|
||||||
|
|
||||||
|
if request.is_json:
|
||||||
|
return jsonify({'success': True, 'message': message,
|
||||||
|
'pages': [p.to_dict() for p in pages]})
|
||||||
|
|
||||||
|
flash(message, 'success')
|
||||||
|
return redirect(url_for('instructions.organise', set_id=set_id))
|
||||||
|
|
||||||
|
|
||||||
|
@instructions_bp.route('/<int:instruction_id>/move', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def move(instruction_id):
|
||||||
|
"""Move one page to a given position, shuffling the rest around it."""
|
||||||
|
payload = request.get_json(silent=True) or request.form
|
||||||
|
position = payload.get('position')
|
||||||
|
|
||||||
|
try:
|
||||||
|
pages = page_order.move_page(instruction_id, int(position))
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 400
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'pages': [p.to_dict() for p in pages],
|
||||||
|
})
|
||||||
Reference in new issue
Block a user