pdf-quiz-generator/backend/scripts/seed_sample_content.py
Daniel 3279e14bb2 refactor: remove the LMS
There will be no courses. What was there: one draft called "jk" with two empty
lessons, and 4,000 lines of code around it — courses, modules, lessons,
enrolments, per-lesson progress, SCORM, BigBlueButton, completion certificates,
three React pages, a router, two models.

Its real cost was everywhere else. Every query that measured practice had to
remember `Quiz.course_id.is_(None)`, and forgetting it in one place would have
silently mixed course attempts into a learner's analytics; the bank predicate
carried a subquery to exclude a course's own questions from every search,
recommendation and share; quiz access had a second, parallel rule about
enrolment. All of that is gone, so the remaining rules say what they mean.

`quizzes.allow_review` goes with it. It was only ever enforced for a course
quiz, so it had become a promise nothing keeps — the public session page was
still offering "no answer review" about sessions that review fine.

The fixtures' question 5 lived in a course quiz and stood for "a question that
exists but is not in your bank". There is no such thing now — a question is in
the bank unless it is deleted — so the counts it kept out of the numbers are
back in, and the tests that turned on it now turn on deletion or on the
attempt that actually holds a question.

Files the LMS uploaded stay on disk and stay protected: LEGACY_LMS_PREFIXES in
app/utils/upload_access.py is what keeps them unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-12 23:27:51 +02:00

223 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Seed clearly-marked sample articles, cards and lab deep links for demonstration.
Every created row is labeled as sample content and links existing bank questions,
cards and cited lab references so the linking features are visible end to end.
Idempotent: existing sample rows are left untouched.
"""
import sys
from app.database import SessionLocal
from app.models.article import Article, QuestionArticleLink
from app.services import article_service
from app.models.flashcard import Flashcard, FlashcardDeck, FlashcardArticleLink, FlashcardQuestionLink
from app.models.lab_reference import LabReference, LabReferenceCardLink
from app.models.question import Question
from app.models.user import User
SAMPLE_NOTE = "Sample content for demonstration — replace with educator-reviewed material."
SECTION_JAUNDICE = "000000000000000000000000000000a1"
SECTION_JAUNDICE_BILI = "000000000000000000000000000000a2"
SECTION_SEIZURE = "000000000000000000000000000000b1"
SECTION_SEIZURE_LABS = "000000000000000000000000000000b2"
def main():
db = SessionLocal()
try:
moderator = db.query(User.id).filter(User.role.in_(["admin", "moderator"])).order_by(User.id).first()
moderator = moderator[0] if moderator else None
# ── Sample articles ────────────────────────────────────────────
articles = []
for slug, title, summary, content, sections, category_hint in [
("sample-febrile-seizures", "Febrile seizures — sample", SAMPLE_NOTE,
"This **sample article** demonstrates article sections, question links and card links.",
[
{"id": SECTION_SEIZURE, "slug": "definition", "title": "Definition and evaluation",
"content": "A febrile seizure is a seizure associated with fever in a child 6 months5 years without CNS infection or metabolic cause.\n\n**Sample text** — verify against your local guidance."},
{"id": SECTION_SEIZURE_LABS, "slug": "reference-values", "title": "Reference values",
"content": "Linked laboratory reference values appear in the quiz lab panel and deep-link back to this section.\n\n**Sample text** — verify against your local guidance."},
], None),
("sample-neonatal-jaundice", "Neonatal jaundice — sample", SAMPLE_NOTE,
"This **sample article** demonstrates deep links from lab references into a specific section.",
[
{"id": SECTION_JAUNDICE, "slug": "overview", "title": "Overview",
"content": "Physiologic jaundice appears after 24 hours of life and peaks at 35 days in term infants.\n\n**Sample text** — verify against your local guidance."},
{"id": SECTION_JAUNDICE_BILI, "slug": "bilirubin-reference-values", "title": "Bilirubin reference values",
"content": "Total bilirubin reference values used in the lab panel link to this section.\n\n**Sample text** — verify against your local guidance."},
], None),
]:
if not db.query(Article.id).filter(Article.slug == slug).first():
article = Article(slug=slug, title=title, summary=summary, content=content,
sections=sections, user_id=moderator, status="published")
db.add(article)
db.flush()
articles.append(article)
print(f"Created sample article: {title}")
db.commit()
# Seeded content has to be findable by what it says, like anything else.
for article in articles:
article_service.reindex(db, article)
# ── Link bank questions ─────────────────────────────────────────
from app.services.quiz_builder import shareable_question_predicate
bank = db.query(Question.id).filter(
shareable_question_predicate(),
Question.source_quiz_id.is_(None),
).order_by(Question.id).limit(6).all()
bank_ids = [row[0] for row in bank]
by_slug = {a.slug: a for a in db.query(Article).filter(Article.slug.in_(
["sample-febrile-seizures", "sample-neonatal-jaundice"])).all()}
pairs = [
("sample-febrile-seizures", None, 0), ("sample-febrile-seizures", SECTION_SEIZURE, 1),
("sample-febrile-seizures", SECTION_SEIZURE_LABS, 2),
("sample-neonatal-jaundice", None, 3), ("sample-neonatal-jaundice", SECTION_JAUNDICE, 4),
("sample-neonatal-jaundice", SECTION_JAUNDICE_BILI, 5),
]
for slug, section_id, bank_index in pairs:
article = by_slug.get(slug)
if not article or not bank_ids:
continue
question_id = bank_ids[bank_index % len(bank_ids)]
if not db.query(QuestionArticleLink.id).filter_by(
question_id=question_id, article_id=article.id, section_id=section_id).first():
db.add(QuestionArticleLink(question_id=question_id, article_id=article.id,
section_id=section_id, user_id=moderator))
print(f"Linked question {question_id} -> {slug} {section_id or 'whole article'}")
db.commit()
# Sample question demonstrating key points that smart-link to the sample articles.
sample_q = db.query(Question).filter(Question.question_text.like("Sample: febrile seizure definition%")).first()
if not sample_q:
sample_q = Question(
question_text="Sample: febrile seizure definition — which feature defines a SIMPLE febrile seizure?",
question_type="mcq",
options=["Generalized, lasts under 15 minutes, once in 24 hours",
"Focal seizure lasting 25 minutes",
"Two seizures within 24 hours",
"Seizure with CSF pleocytosis"],
correct_answer="Generalized, lasts under 15 minutes, once in 24 hours",
explanation="**Sample content — verify clinically.** A simple febrile seizure is generalized, brief (<15 min), "
"and does not recur within 24 hours, in a child 6 months5 years with fever and no CNS infection.",
option_explanations={
"Generalized, lasts under 15 minutes, once in 24 hours": "This matches the definition of a simple febrile seizure.",
"Focal seizure lasting 25 minutes": "Focal or prolonged seizures are complex febrile seizures.",
"Two seizures within 24 hours": "Recurrence within 24 hours makes the seizure complex.",
"Seizure with CSF pleocytosis": "CSF pleocytosis suggests CNS infection, not a febrile seizure.",
},
user_id=moderator, is_shared=1,
)
db.add(sample_q)
db.flush()
print("Created sample smart-link question")
seizure_article = by_slug.get("sample-febrile-seizures")
jaundice_article = by_slug.get("sample-neonatal-jaundice")
sample_q.key_points = [
{"text": "A febrile seizure is a seizure with fever in a child 6 months5 years without CNS infection.",
"article_id": seizure_article.id if seizure_article else None,
"article_section_id": SECTION_SEIZURE},
{"text": "Physiologic jaundice appears after 24 hours and peaks at 35 days in term infants.",
"article_id": jaundice_article.id if jaundice_article else None,
"article_section_id": SECTION_JAUNDICE},
]
db.commit()
# Sample quiz demonstrating the full study feedback (option explanations,
# key points with article links, linked cards).
from app.models.quiz import Quiz
from app.models.quiz_question_link import QuizQuestionLink
sample_quiz = db.query(Quiz).filter(Quiz.title == "Sample — smart links demo").first()
if not sample_quiz:
sample_quiz = Quiz(title="Sample — smart links demo", user_id=moderator, mode="learning",
questions_count=1, is_published=1, origin="sample")
db.add(sample_quiz)
db.flush()
if not db.query(QuizQuestionLink.question_id).filter_by(quiz_id=sample_quiz.id, question_id=sample_q.id).first():
db.add(QuizQuestionLink(quiz_id=sample_quiz.id, question_id=sample_q.id, position=0))
db.commit()
print("Created sample smart-links quiz")
# ── Sample deck and cards ───────────────────────────────────────
deck = db.query(FlashcardDeck).filter_by(title="Sample cards", user_id=moderator).first()
if not deck:
deck = FlashcardDeck(title="Sample cards", user_id=moderator, card_count=0, is_shared=1)
db.add(deck)
db.flush()
sample_cards = [
("Sample: febrile seizure definition",
"Seizure with fever in a child 6 months5 years without CNS infection or metabolic cause. **Sample content** — verify clinically."),
("Sample: jaundice timeline",
"Physiologic jaundice appears after 24 h and peaks at 35 days in term infants. **Sample content** — verify clinically."),
]
created_cards = []
for front, back in sample_cards:
card = db.query(Flashcard).filter_by(deck_id=deck.id, front=front).first()
if not card:
card = Flashcard(deck_id=deck.id, front=front, back=back)
db.add(card)
db.flush()
created_cards.append(card)
if created_cards:
deck.card_count = db.query(Flashcard).filter(Flashcard.deck_id == deck.id).count()
db.commit()
print(f"Sample deck cards: {len(created_cards)}")
cards = db.query(Flashcard).filter_by(deck_id=deck.id).order_by(Flashcard.id).all()
seizure_article = by_slug.get("sample-febrile-seizures")
jaundice_article = by_slug.get("sample-neonatal-jaundice")
if cards and seizure_article and bank_ids:
card = cards[0]
if not db.query(FlashcardArticleLink.id).filter_by(
flashcard_id=card.id, article_id=seizure_article.id, article_section_id=SECTION_SEIZURE).first():
db.add(FlashcardArticleLink(flashcard_id=card.id, article_id=seizure_article.id,
article_section_id=SECTION_SEIZURE))
if not db.query(FlashcardQuestionLink.id).filter_by(flashcard_id=card.id, question_id=bank_ids[0]).first():
db.add(FlashcardQuestionLink(flashcard_id=card.id, question_id=bank_ids[0]))
print(f"Card {card.id} linked to question {bank_ids[0]}")
if len(cards) > 1 and jaundice_article:
card = cards[1]
if not db.query(FlashcardArticleLink.id).filter_by(
flashcard_id=card.id, article_id=jaundice_article.id, article_section_id=SECTION_JAUNDICE_BILI).first():
db.add(FlashcardArticleLink(flashcard_id=card.id, article_id=jaundice_article.id,
article_section_id=SECTION_JAUNDICE_BILI))
if bank_ids and not db.query(FlashcardQuestionLink.id).filter_by(
flashcard_id=card.id, question_id=bank_ids[1 % len(bank_ids)]).first():
db.add(FlashcardQuestionLink(flashcard_id=card.id, question_id=bank_ids[1 % len(bank_ids)]))
# Link the first sample card to the sample smart-link question as well.
if cards and not db.query(FlashcardQuestionLink.id).filter_by(
flashcard_id=cards[0].id, question_id=sample_q.id).first():
db.add(FlashcardQuestionLink(flashcard_id=cards[0].id, question_id=sample_q.id))
print(f"Sample card linked to the sample question")
db.commit()
# ── Deep-link lab references to the sample sections ─────────────
lab_targets = [
("Bilirubin (total)", SECTION_JAUNDICE_BILI, "sample-neonatal-jaundice"),
("CSF white cell count", SECTION_SEIZURE_LABS, "sample-febrile-seizures"),
("CSF protein", SECTION_SEIZURE_LABS, "sample-febrile-seizures"),
]
for name, section_id, slug in lab_targets:
article = by_slug.get(slug)
if not article:
continue
for entry in db.query(LabReference).filter(LabReference.name == name).all():
if entry.article_id is None:
entry.article_id = article.id
entry.article_section_id = section_id
print(f"Lab '{name}' deep-links to {slug}#{section_id[:8]}")
# Link a sample card to one lab reference.
lab = db.query(LabReference).filter(LabReference.name == "CSF white cell count").first()
if lab and cards:
if not db.query(LabReferenceCardLink.id).filter_by(
lab_reference_id=lab.id, flashcard_id=cards[0].id).first():
db.add(LabReferenceCardLink(lab_reference_id=lab.id, flashcard_id=cards[0].id))
print(f"Lab '{lab.name}' linked to sample card")
db.commit()
print("Sample content ready.")
finally:
db.close()
if __name__ == "__main__":
sys.exit(main())