"PREP" is the American Academy of Pediatrics' trademark for their own product. The plans here are our own sets of questions grouped by year, so they are now named for what they are: Board Review 2021, and Mixed Review for the plan that draws from every year at once. Renamed in the database as well as the code — 13 plans, 14 quizzes a learner had already generated from a block, and the 12 year tags, which appear in the question bank's filters and are as visible as the plans. The seeder matches both the old and new names so a fresh import still finds its material, and the tagger mints the new one so the next run cannot undo this. Prompts and comments that described the source PDFs by that name now describe them by what they are. The generation run's 377 failures were not a bug Every call was reserving the model's full 64k output ceiling, and OpenRouter refuses the whole request when the balance is below the reservation — "you requested up to 64000 tokens, but can only afford 52017" — however short the answer would actually be. `_call_model` now takes a max_tokens, and the article writer asks for 4000, which is comfortable for three views of one topic and keeps each request small enough to be affordable. 98 articles were written before the balance ran down; 158 exist in total. Generation is paused at the user's request while credits are topped up. 208 backend, 243 frontend green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
108 lines
4.2 KiB
Python
108 lines
4.2 KiB
Python
"""Rename the study plans away from the vendor's programme name.
|
|
|
|
"PREP" is the American Academy of Pediatrics' trademark for its own product.
|
|
The plans here are our own sets of questions grouped by year, so they get names
|
|
that describe what they are: "Board Review 2021", and "Mixed Review" for the
|
|
plan that draws from every year at once.
|
|
|
|
Slugs change with them, which is safe because a study plan is reached by id and
|
|
the slug is not a public address. Quizzes a learner already generated from a
|
|
block are renamed too, so a title in their history matches the plan it came from
|
|
rather than referring to something that no longer exists. So are the year tags,
|
|
which appear in the question bank's filters and are as visible as the plans.
|
|
|
|
docker compose exec backend python -m scripts.rename_study_plans
|
|
docker compose exec backend python -m scripts.rename_study_plans --apply
|
|
"""
|
|
import re
|
|
import sys
|
|
|
|
from sqlalchemy import text as sa_text
|
|
|
|
from app.database import SessionLocal
|
|
|
|
YEAR_NAME = "Board Review {year}"
|
|
YEAR_SLUG = "board-review-{year}"
|
|
MIXED_NAME = "Mixed Review"
|
|
MIXED_SLUG = "mixed-review"
|
|
|
|
|
|
def planned(db):
|
|
"""(id, old name, new name, old slug, new slug) for everything to rename."""
|
|
rows = db.execute(sa_text(
|
|
"SELECT id, slug, name, kind FROM study_plans ORDER BY sort_order, name")).fetchall()
|
|
changes = []
|
|
for plan in rows:
|
|
year = re.search(r"(\d{4})", plan.name)
|
|
if plan.kind == "mixed" or "mixed" in plan.name.lower():
|
|
new_name, new_slug = MIXED_NAME, MIXED_SLUG
|
|
elif year:
|
|
new_name = YEAR_NAME.format(year=year.group(1))
|
|
new_slug = YEAR_SLUG.format(year=year.group(1))
|
|
else:
|
|
continue
|
|
if (new_name, new_slug) != (plan.name, plan.slug):
|
|
changes.append((plan.id, plan.name, new_name, plan.slug, new_slug))
|
|
return changes
|
|
|
|
|
|
def main():
|
|
apply_changes = "--apply" in sys.argv
|
|
db = SessionLocal()
|
|
try:
|
|
# No early return when the plans are already done: the quizzes and the
|
|
# year tags are renamed by the same pass, and a second run has to be able
|
|
# to finish what a first one left.
|
|
changes = planned(db)
|
|
print(f" plans to rename: {len(changes)}\n")
|
|
for _pid, old_name, new_name, old_slug, new_slug in changes:
|
|
print(f" {old_name:<16} -> {new_name:<22} ({old_slug} -> {new_slug})")
|
|
|
|
quizzes = db.execute(sa_text(
|
|
"SELECT id, title FROM quizzes WHERE title LIKE '%PREP%'")).fetchall()
|
|
print(f"\n quizzes already generated from a block: {len(quizzes)}")
|
|
|
|
tags = db.execute(sa_text(
|
|
"SELECT id, name FROM question_tags WHERE name ~ '^PREP [0-9]{4}$'")).fetchall()
|
|
print(f" year tags shown in the bank's filters : {len(tags)}")
|
|
|
|
if not apply_changes:
|
|
print("\n Re-run with --apply to rename them.")
|
|
return 0
|
|
|
|
for plan_id, _old_name, new_name, _old_slug, new_slug in changes:
|
|
db.execute(sa_text("UPDATE study_plans SET name = :n, slug = :s WHERE id = :i"),
|
|
{"n": new_name, "s": new_slug, "i": plan_id})
|
|
|
|
renamed = 0
|
|
for quiz in quizzes:
|
|
title = quiz.title
|
|
year = re.search(r"PREP (\d{4})", title)
|
|
if year:
|
|
title = title.replace(f"PREP {year.group(1)}", YEAR_NAME.format(year=year.group(1)))
|
|
else:
|
|
title = title.replace("PREP Mixed", MIXED_NAME).replace("PREP", "Board Review")
|
|
if title != quiz.title:
|
|
db.execute(sa_text("UPDATE quizzes SET title = :t WHERE id = :i"),
|
|
{"t": title, "i": quiz.id})
|
|
renamed += 1
|
|
retagged = 0
|
|
for tag in tags:
|
|
year = re.search(r"(\d{4})", tag.name)
|
|
if not year:
|
|
continue
|
|
db.execute(sa_text("UPDATE question_tags SET name = :n WHERE id = :i"),
|
|
{"n": YEAR_NAME.format(year=year.group(1)), "i": tag.id})
|
|
retagged += 1
|
|
|
|
db.commit()
|
|
print(f"\n plans renamed : {len(changes)}")
|
|
print(f" quizzes renamed : {renamed}")
|
|
print(f" tags renamed : {retagged}")
|
|
finally:
|
|
db.close()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|