Quiz management area (AMBOSS parity, verified against next.amboss.com): - GET /quizzes/sessions returns one management row per accessible quiz — attempt state, live answered/total from Redis, last score and activity — so the page no longer fans out per-quiz requests. - QuizzesPage rebuilt as a session list grouped by day with a progress bar, a state-aware primary action (Start / Resume / Review) and an action menu matching AMBOSS: Analysis, Repeat, Rename, Share, Edit, Category, Delete. Rename and delete confirm inline; no browser popups. - Sessions / Library / Categories tabs replace the flat card grid. - QuizPage honours ?restart=1 so Repeat always begins a fresh attempt. Question manager (new moderator page at /questions/manage): - GET /questions/manage/summary counts editorial gaps; /questions/bank gains a `needs` filter (category / explanation / difficulty / private) so the health tiles double as one-click filters. - POST /questions/bulk applies category, difficulty, sharing or delete to up to 500 checked questions in one call, moderator-only. - Question edit/create modals extracted to components/QuestionEditors.jsx and shared by the question bank and the manager instead of being duplicated. Showcase articles: - scripts/seed_showcase_articles.py seeds eight short starter articles across the main pediatric systems, each filed under a real category, with stable hex section IDs and links to bank questions from the same category. Mobile: dedicated stylesheets for both pages — rows stack, the action menu becomes a bottom sheet and the bulk bar docks to the bottom edge. Tests: 9 new backend tests (session feed states, ordering, Redis-outage degradation, visibility; bulk actions, gap filters, moderator gate) and 9 new frontend tests. Full suites green: 72 backend, 106 frontend, build clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014yhHB8Pc7oQqyqn2Vo9DXA
221 lines
11 KiB
Python
221 lines
11 KiB
Python
"""Seed a small set of showcase topic articles across the main pediatric systems.
|
||
|
||
Purpose is demonstration: every article is short, clearly marked as starter
|
||
content for an educator to review, filed under a real category, and linked to
|
||
bank questions from the same category so the article -> question -> card flow
|
||
is visible end to end.
|
||
|
||
Idempotent — an article whose slug already exists is left untouched.
|
||
|
||
docker compose exec backend python -m scripts.seed_showcase_articles
|
||
"""
|
||
import hashlib
|
||
import sys
|
||
|
||
from app.database import SessionLocal
|
||
from app.models.article import Article, QuestionArticleLink
|
||
from app.models.question import Question
|
||
from app.models.question_category import QuestionCategory
|
||
from app.models.user import User
|
||
|
||
REVIEW_NOTE = "Starter article for demonstration — review and expand before teaching from it."
|
||
|
||
# (slug, title, category name, summary, intro, [(section slug, section title, body)])
|
||
SHOWCASE = [
|
||
(
|
||
"bronchiolitis", "Bronchiolitis", "Pulmonology",
|
||
"Common lower respiratory infection of infants under 2 years.",
|
||
"Bronchiolitis is the most common lower respiratory tract infection in infants, usually caused by "
|
||
"respiratory syncytial virus (RSV). Care is supportive.",
|
||
[
|
||
("presentation", "Presentation",
|
||
"Preceded by 1–3 days of coryza, then cough, tachypnea, wheeze and crackles. Peak severity is "
|
||
"typically day 3–5 of illness. Apnea can be the presenting sign in young or preterm infants."),
|
||
("evaluation", "Evaluation",
|
||
"The diagnosis is clinical. Routine chest radiographs and viral testing are not needed for typical "
|
||
"cases. Assess work of breathing, hydration and feeding, and oxygen saturation."),
|
||
("management", "Management",
|
||
"Supportive care: suctioning, hydration and oxygen when saturations are persistently low. "
|
||
"Bronchodilators, corticosteroids and antibiotics are not routinely indicated."),
|
||
],
|
||
),
|
||
(
|
||
"acute-otitis-media", "Acute otitis media", "Otolaryngology",
|
||
"Middle-ear infection — one of the commonest reasons for antibiotics in children.",
|
||
"Acute otitis media (AOM) is a middle-ear effusion with acute signs of inflammation.",
|
||
[
|
||
("diagnosis", "Diagnosis",
|
||
"Requires a bulging tympanic membrane, or new otorrhea not due to otitis externa, together with "
|
||
"acute onset of symptoms. A cloudy, immobile drum supports effusion."),
|
||
("management", "Management",
|
||
"Analgesia for every child. Antibiotics are indicated for young infants, severe symptoms, or "
|
||
"bilateral disease in the very young; otherwise observation with follow-up is an option."),
|
||
("complications", "Complications",
|
||
"Uncommon but important: mastoiditis, tympanic membrane perforation, hearing loss from persistent "
|
||
"effusion, and rarely intracranial extension."),
|
||
],
|
||
),
|
||
(
|
||
"kawasaki-disease", "Kawasaki disease", "Cardiology",
|
||
"Medium-vessel vasculitis of childhood with coronary artery risk.",
|
||
"Kawasaki disease is an acute, self-limited vasculitis of young children; the key concern is coronary "
|
||
"artery aneurysm.",
|
||
[
|
||
("features", "Clinical features",
|
||
"Fever lasting at least 5 days plus features such as bilateral non-exudative conjunctivitis, oral "
|
||
"mucosal changes, polymorphous rash, extremity changes, and cervical lymphadenopathy."),
|
||
("evaluation", "Evaluation",
|
||
"Inflammatory markers are raised. Echocardiography assesses the coronary arteries at diagnosis and "
|
||
"on follow-up."),
|
||
("management", "Management",
|
||
"Intravenous immunoglobulin with aspirin, given as early as possible, reduces the risk of coronary "
|
||
"aneurysms."),
|
||
],
|
||
),
|
||
(
|
||
"type-1-diabetes-presentation", "Type 1 diabetes — presentation", "Endocrinology",
|
||
"Recognising new-onset diabetes and diabetic ketoacidosis in children.",
|
||
"Type 1 diabetes results from autoimmune destruction of pancreatic beta cells and often presents acutely "
|
||
"in children.",
|
||
[
|
||
("presentation", "Presentation",
|
||
"Polyuria, polydipsia and weight loss over days to weeks. Nocturnal enuresis in a previously dry "
|
||
"child is a useful clue."),
|
||
("dka", "Diabetic ketoacidosis",
|
||
"Hyperglycemia with metabolic acidosis and ketosis. Kussmaul breathing, vomiting and abdominal pain "
|
||
"are common; cerebral edema is the most feared complication."),
|
||
("initial-management", "Initial management",
|
||
"Restore circulation, correct fluid and electrolyte deficits gradually, and start an insulin "
|
||
"infusion after fluids per local protocol."),
|
||
],
|
||
),
|
||
(
|
||
"iron-deficiency-anemia", "Iron deficiency anemia", "Hematology-Oncology",
|
||
"The commonest cause of anemia in young children.",
|
||
"Iron deficiency is usually nutritional in toddlers and related to menstrual loss in adolescents.",
|
||
[
|
||
("risk-factors", "Risk factors",
|
||
"Excessive cow's milk intake in toddlers, prematurity, low dietary iron, and chronic blood loss."),
|
||
("laboratory", "Laboratory findings",
|
||
"Microcytic, hypochromic anemia with a raised red cell distribution width, low ferritin and low "
|
||
"transferrin saturation."),
|
||
("management", "Management",
|
||
"Oral iron with dietary counselling; recheck the response after several weeks. Look for a source of "
|
||
"blood loss when the history does not fit."),
|
||
],
|
||
),
|
||
(
|
||
"urinary-tract-infection", "Urinary tract infection", "Nephrology & Urology",
|
||
"A frequent cause of unexplained fever in infants.",
|
||
"Urinary tract infection should be considered in any young child with unexplained fever.",
|
||
[
|
||
("presentation", "Presentation",
|
||
"Infants present with fever, irritability, vomiting or poor feeding. Older children may describe "
|
||
"dysuria, frequency and flank pain."),
|
||
("diagnosis", "Diagnosis",
|
||
"Requires both pyuria and a positive urine culture from an appropriately collected specimen. "
|
||
"Bag specimens are unreliable for culture."),
|
||
("management", "Management",
|
||
"Antibiotics guided by local resistance patterns, adjusted once sensitivities return. Imaging "
|
||
"decisions follow age and recurrence per local guidance."),
|
||
],
|
||
),
|
||
(
|
||
"asthma-in-children", "Asthma in children", "Pulmonology",
|
||
"Chronic airway inflammation with reversible obstruction.",
|
||
"Childhood asthma presents with recurrent wheeze, cough and breathlessness that respond to bronchodilators.",
|
||
[
|
||
("diagnosis", "Diagnosis",
|
||
"A pattern of recurrent, variable symptoms with documented reversible airflow obstruction where "
|
||
"spirometry is feasible (usually from about age 5)."),
|
||
("acute-exacerbation", "Acute exacerbation",
|
||
"Inhaled short-acting beta agonists, systemic corticosteroids and oxygen as needed. Reassess the "
|
||
"response and escalate for poor responders."),
|
||
("chronic-control", "Chronic control",
|
||
"Step therapy based on symptom control, with inhaler technique and adherence reviewed at every "
|
||
"visit before stepping up."),
|
||
],
|
||
),
|
||
(
|
||
"developmental-milestones", "Developmental milestones", "Developmental & Behavioral",
|
||
"A practical framework for surveillance and red flags.",
|
||
"Developmental surveillance at every health visit, with standardised screening at set ages, is the basis "
|
||
"of early identification.",
|
||
[
|
||
("domains", "Domains",
|
||
"Gross motor, fine motor, language and social/adaptive development are tracked separately; delay in "
|
||
"one domain does not always imply global delay."),
|
||
("red-flags", "Red flags",
|
||
"Loss of previously acquired skills at any age is always a red flag and warrants prompt evaluation."),
|
||
("next-steps", "Next steps",
|
||
"Confirm with a standardised tool, check hearing and vision, and refer to early intervention "
|
||
"services while the evaluation proceeds."),
|
||
],
|
||
),
|
||
]
|
||
|
||
|
||
def _section_id(article_slug: str, section_slug: str) -> str:
|
||
"""Stable 32-hex section ID (the format the articles API validates)."""
|
||
return hashlib.md5(f"{article_slug}/{section_slug}".encode()).hexdigest()
|
||
|
||
|
||
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
|
||
categories = {c.name: c.id for c in db.query(QuestionCategory).filter(QuestionCategory.parent_id.is_(None)).all()}
|
||
|
||
created = 0
|
||
for slug, title, category_name, summary, intro, sections in SHOWCASE:
|
||
if db.query(Article.id).filter(Article.slug == slug).first():
|
||
print(f"skip (exists): {slug}")
|
||
continue
|
||
category_id = categories.get(category_name)
|
||
if category_id is None:
|
||
print(f"warning: category {category_name!r} not found; filing {slug} uncategorised")
|
||
article = Article(
|
||
slug=slug,
|
||
title=title,
|
||
summary=f"{summary} {REVIEW_NOTE}",
|
||
content=intro,
|
||
sections=[
|
||
{"id": _section_id(slug, sec_slug), "slug": sec_slug, "title": sec_title, "content": body}
|
||
for sec_slug, sec_title, body in sections
|
||
],
|
||
category_id=category_id,
|
||
user_id=moderator,
|
||
status="published",
|
||
)
|
||
db.add(article)
|
||
created += 1
|
||
print(f"created: {title} ({category_name})")
|
||
db.commit()
|
||
|
||
# Link up to three bank questions from the same category to each article.
|
||
linked = 0
|
||
for slug, title, category_name, *_ in SHOWCASE:
|
||
article = db.query(Article).filter(Article.slug == slug).first()
|
||
if not article or not article.category_id:
|
||
continue
|
||
question_ids = [
|
||
row[0] for row in db.query(Question.id)
|
||
.filter(Question.question_category_id == article.category_id)
|
||
.order_by(Question.id).limit(3).all()
|
||
]
|
||
for question_id in question_ids:
|
||
exists = db.query(QuestionArticleLink.id).filter_by(
|
||
question_id=question_id, article_id=article.id, section_id=None).first()
|
||
if not exists:
|
||
db.add(QuestionArticleLink(question_id=question_id, article_id=article.id,
|
||
section_id=None, user_id=moderator))
|
||
linked += 1
|
||
db.commit()
|
||
print(f"Showcase articles ready: {created} created, {linked} question links added.")
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|