""" CLI commands for checking and fixing image instruction page order. Lives in its own module rather than in cli.py so the page-ordering work stays together: the logic is in app/services/page_order.py and the commands that drive it are here. flask pages audit which sets have broken numbering flask pages fix --dry-run what would be repaired flask pages fix repair every set that needs it flask pages fix --set 60123 repair one set flask pages sort 60123 --by filename reorder one set flask pages sort 60123 --by page_hint --reverse Created by: Jess Rogerson (yelling commands at Claude.AI) """ import click from flask.cli import AppGroup pages_cli = AppGroup( 'pages', help='Check and fix the page order of image instructions.') def _sets_with_images(): """Every set that has at least one image page, in set-number order.""" from app import db from app.models.set import Set from app.models.instruction import Instruction ids = [row[0] for row in db.session.query(Instruction.set_id) .filter(Instruction.file_type == 'IMAGE').distinct()] return Set.query.filter(Set.id.in_(ids)).order_by(Set.set_number).all() @pages_cli.command('audit') @click.option('--all/--problems', 'show_all', default=False, help='List every set, or only the ones with a numbering problem ' '(default).') def pages_audit(show_all): """Report duplicate, missing or unnumbered pages across the library.""" from app.services import page_order problems = 0 for lego_set in _sets_with_images(): report = page_order.audit(lego_set.id) if report['is_clean'] and not show_all: continue if not report['is_clean']: problems += 1 state = 'ok' if report['is_clean'] else 'NEEDS FIXING' colour = 'green' if report['is_clean'] else 'yellow' click.secho(f"{lego_set.set_number:<12} {report['total']:>4} pages " f"{state}", fg=colour) if report['duplicates']: click.echo(f" duplicates : {report['duplicates']}") if report['gaps']: click.echo(f" gaps : {report['gaps']}") if report['unnumbered']: click.echo(f" unnumbered : {report['unnumbered']}") click.echo(f"\n{problems} set(s) need fixing.") @pages_cli.command('fix') @click.option('--set', 'set_number', default=None, help='One set number. Omit to fix every set that needs it.') @click.option('--dry-run', is_flag=True, help='Report without writing.') def pages_fix(set_number, dry_run): """Renumber pages 1..n, closing gaps and clearing duplicates.""" from app.models.set import Set from app.services import page_order if set_number: targets = Set.query.filter_by(set_number=set_number).all() if not targets: raise click.ClickException(f'No set numbered {set_number}') else: targets = _sets_with_images() fixed = 0 for lego_set in targets: report = page_order.audit(lego_set.id) if report['is_clean']: continue fixed += 1 if dry_run: click.echo(f"would fix {lego_set.set_number} " f"({report['total']} pages)") continue page_order.renumber(lego_set.id) click.secho(f"fixed {lego_set.set_number} ({report['total']} pages)", fg='green') click.echo(f"{fixed} set(s) {'would be ' if dry_run else ''}fixed.") @pages_cli.command('sort') @click.argument('set_number') @click.option('--by', 'strategy', default='filename', help='filename, page_hint, uploaded or size.') @click.option('--reverse', is_flag=True, help='Sort the other way round.') def pages_sort(set_number, strategy, reverse): """Reorder one set's image pages by filename, upload order or size.""" from app.models.set import Set from app.services import page_order lego_set = Set.query.filter_by(set_number=set_number).first() if lego_set is None: raise click.ClickException(f'No set numbered {set_number}') try: pages = page_order.auto_sort(lego_set.id, strategy, reverse=reverse) except ValueError as e: raise click.ClickException(str(e)) for page in pages: click.echo(f" {page.page_number:>4} {page.file_name}") click.secho(f'Sorted {len(pages)} pages.', fg='green') def register_pages_cli(app): """Attach the `flask pages` group to `app`.""" app.cli.add_command(pages_cli)