Elicit Step wise medical history in rheumatoid arthritis

| System | Symptoms to Ask |
|---|---|
| Constitutional | Fatigue (often pronounced), weight loss, low-grade fever, malaise |
| Skin | Subcutaneous nodules (firm, non-tender, extensor surfaces - forearms, olecranon) |
| Eyes | Dry eyes (Sicca/keratoconjunctivitis sicca), red eye (episcleritis, scleritis), visual changes |
| Cardiovascular | Chest pain, palpitations, breathlessness (pericarditis, pleuritis, premature CAD) |
| Respiratory | Dry cough, breathlessness (ILD, pleural effusion, rheumatoid nodules in lungs, rarely bronchiolitis obliterans) |
| Neurological | Tingling/numbness in hands (carpal tunnel syndrome - very common in RA), weakness in legs (cervical myelopathy from C1-C2 subluxation), stocking-glove neuropathy |
| ENT | Hoarseness, throat fullness (cricoarytenoid joint involvement) |
| Haematological | Pallor, easy bruising (anaemia of chronic disease; in Felty's syndrome: splenomegaly + neutropenia) |
| Urinary/Renal | Ankle swelling (amyloidosis in long-standing RA) |
| Domain | Score |
|---|---|
| Joint involvement: 1 large joint = 0; 2-10 large joints = 1; 1-3 small joints = 2; 4-10 small joints = 3; >10 joints (at least 1 small) = 5 | 0-5 |
| Serology: RF or ACPA negative = 0; low positive = 2; high positive (>3x ULN) = 3 | 0-3 |
| Duration of synovitis: <6 weeks = 0; ≥6 weeks = 1 | 0-1 |
| Acute phase reactants: Normal CRP and ESR = 0; Abnormal = 1 | 0-1 |
Generate a detailed patient history template for RA initial assessment
mkdir -p /home/daytona/workspace/ra-template && ls /home/daytona/workspace/ra-template
~/ra-template/generate_ra_template.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import os
OUTPUT_PATH = "/home/daytona/workspace/ra-template/RA_Initial_Assessment_Template.pdf"
# ── Colour palette ────────────────────────────────────────────────────────────
DEEP_BLUE = colors.HexColor("#1A3A5C")
MID_BLUE = colors.HexColor("#2E6DA4")
LIGHT_BLUE = colors.HexColor("#D6E8F7")
ACCENT_RED = colors.HexColor("#C0392B")
LIGHT_GREY = colors.HexColor("#F4F6F8")
MID_GREY = colors.HexColor("#BDC3C7")
DARK_GREY = colors.HexColor("#2C3E50")
WHITE = colors.white
FIELD_BG = colors.HexColor("#FAFBFC")
SECTION_HDR = colors.HexColor("#EBF3FB")
W, H = A4 # 210 x 297 mm
doc = SimpleDocTemplate(
OUTPUT_PATH,
pagesize=A4,
topMargin=18*mm,
bottomMargin=18*mm,
leftMargin=18*mm,
rightMargin=18*mm,
title="Rheumatoid Arthritis – Initial Patient Assessment",
author="Rheumatology Department",
)
styles = getSampleStyleSheet()
# ── Custom paragraph styles ───────────────────────────────────────────────────
def PS(name, parent="Normal", **kw):
return ParagraphStyle(name, parent=styles[parent], **kw)
TITLE_STYLE = PS("DocTitle", fontSize=18, textColor=WHITE, leading=22, alignment=TA_CENTER, spaceAfter=2)
SUBTITLE_S = PS("DocSub", fontSize=10, textColor=LIGHT_BLUE, leading=13, alignment=TA_CENTER, spaceAfter=0)
SEC_HEAD = PS("SecHead", fontSize=10, textColor=WHITE, leading=14, alignment=TA_LEFT, spaceBefore=4, spaceAfter=2, fontName="Helvetica-Bold")
SUB_HEAD = PS("SubHead", fontSize=9, textColor=MID_BLUE, leading=12, alignment=TA_LEFT, spaceBefore=3, spaceAfter=1, fontName="Helvetica-Bold")
LABEL = PS("Label", fontSize=8, textColor=DARK_GREY, leading=10, alignment=TA_LEFT, fontName="Helvetica-Bold")
BODY = PS("Body", fontSize=8, textColor=DARK_GREY, leading=11, alignment=TA_LEFT)
SMALL = PS("Small", fontSize=7, textColor=colors.HexColor("#7F8C8D"), leading=9, alignment=TA_LEFT)
NOTE = PS("Note", fontSize=7.5, textColor=ACCENT_RED, leading=10, alignment=TA_LEFT, fontName="Helvetica-Oblique")
FIELD_LABEL = PS("FLabel", fontSize=7.5, textColor=colors.HexColor("#5D6D7E"), leading=10, fontName="Helvetica-Bold")
PAGE_W = W - 36*mm # usable width
# ── Helper: blank write-in line ───────────────────────────────────────────────
def field_row(label, lines=1, height=7*mm):
"""Single labelled field with underline."""
inner = [
[Paragraph(label, FIELD_LABEL), ""],
]
col_w = PAGE_W / 2
t = Table(inner, colWidths=[col_w*0.38, col_w*0.62])
t.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "BOTTOM"),
("LINEBELOW", (1,0), (1,0), 0.5, MID_GREY),
("BOTTOMPADDING",(0,0), (-1,-1), 2),
("TOPPADDING", (0,0), (-1,-1), 2),
]))
return t
def two_fields(lbl1, lbl2):
"""Two labelled fields side by side."""
col = PAGE_W / 2
data = [[Paragraph(lbl1, FIELD_LABEL), "", Paragraph(lbl2, FIELD_LABEL), ""]]
t = Table(data, colWidths=[col*0.38, col*0.62, col*0.38, col*0.62])
t.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "BOTTOM"),
("LINEBELOW", (1,0), (1,0), 0.5, MID_GREY),
("LINEBELOW", (3,0), (3,0), 0.5, MID_GREY),
("BOTTOMPADDING",(0,0), (-1,-1), 2),
("TOPPADDING", (0,0), (-1,-1), 2),
]))
return t
def three_fields(lbl1, lbl2, lbl3):
"""Three labelled fields side by side."""
col = PAGE_W / 3
data = [[Paragraph(lbl1, FIELD_LABEL), "", Paragraph(lbl2, FIELD_LABEL), "", Paragraph(lbl3, FIELD_LABEL), ""]]
t = Table(data, colWidths=[col*0.42, col*0.58, col*0.42, col*0.58, col*0.42, col*0.58])
t.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "BOTTOM"),
("LINEBELOW", (1,0), (1,0), 0.5, MID_GREY),
("LINEBELOW", (3,0), (3,0), 0.5, MID_GREY),
("LINEBELOW", (5,0), (5,0), 0.5, MID_GREY),
("BOTTOMPADDING",(0,0), (-1,-1), 2),
("TOPPADDING", (0,0), (-1,-1), 2),
]))
return t
def section_header(title, icon=""):
"""Blue section header band."""
data = [[Paragraph(f"{icon} {title}", SEC_HEAD)]]
t = Table(data, colWidths=[PAGE_W])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), DEEP_BLUE),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
]))
return t
def sub_header(title):
"""Light-blue sub-section header."""
data = [[Paragraph(title, SUB_HEAD)]]
t = Table(data, colWidths=[PAGE_W])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), SECTION_HDR),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING",(0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 6),
("LINEBELOW", (0,0), (-1,-1), 0.8, MID_BLUE),
]))
return t
def checkbox_row(items, cols=3):
"""Row of tick-box options."""
# Pad to fill cols
while len(items) % cols != 0:
items.append("")
rows = [items[i:i+cols] for i in range(0, len(items), cols)]
cell_w = PAGE_W / cols
data = []
for row in rows:
data.append([Paragraph(f"☐ {item}" if item else "", BODY) for item in row])
t = Table(data, colWidths=[cell_w]*cols)
t.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING",(0,0), (-1,-1), 2),
("LEFTPADDING", (0,0), (-1,-1), 4),
]))
return t
def text_box(label, height=14*mm):
"""Large multi-line text entry box."""
data = [
[Paragraph(label, FIELD_LABEL)],
[""],
]
t = Table(data, colWidths=[PAGE_W], rowHeights=[None, height])
t.setStyle(TableStyle([
("BOX", (0,0), (-1,-1), 0.5, MID_GREY),
("BACKGROUND", (0,0), (-1,-1), FIELD_BG),
("TOPPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
return t
def spacer(h=3*mm):
return Spacer(1, h)
def hr():
return HRFlowable(width=PAGE_W, thickness=0.4, color=MID_GREY, spaceAfter=2, spaceBefore=2)
# ── VAS / NRS scale ───────────────────────────────────────────────────────────
def pain_scale():
"""0-10 NRS pain scale boxes."""
boxes = [str(i) for i in range(11)]
col_w = PAGE_W / 11
data = [boxes]
t = Table(data, colWidths=[col_w]*11, rowHeights=[8*mm])
style = [
("ALIGN", (0,0), (-1,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("FONTNAME", (0,0), (-1,-1), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 9),
("BOX", (0,0), (-1,-1), 0.8, DEEP_BLUE),
("INNERGRID", (0,0), (-1,-1), 0.5, DEEP_BLUE),
]
# Colour: green→yellow→red
green_cols = [0,1,2,3]
yellow_cols = [4,5,6]
red_cols = [7,8,9,10]
for c in green_cols:
style.append(("BACKGROUND", (c,0), (c,0), colors.HexColor("#D5F5E3")))
for c in yellow_cols:
style.append(("BACKGROUND", (c,0), (c,0), colors.HexColor("#FEF9E7")))
for c in red_cols:
style.append(("BACKGROUND", (c,0), (c,0), colors.HexColor("#FADBD8")))
t.setStyle(TableStyle(style))
return t
# ── Joint diagram table ───────────────────────────────────────────────────────
def joint_involvement_table():
joints = [
"MCP joints (2-5)",
"PIP joints",
"Wrists",
"Elbows",
"Shoulders",
"Knees",
"Ankles",
"MTP joints",
"Cervical spine",
"Hips",
"TMJ",
"Cricoarytenoid",
]
header = [
Paragraph("Joint", FIELD_LABEL),
Paragraph("Right", FIELD_LABEL),
Paragraph("Left", FIELD_LABEL),
Paragraph("Bilateral", FIELD_LABEL),
Paragraph("Tenderness", FIELD_LABEL),
Paragraph("Swelling", FIELD_LABEL),
Paragraph("Warmth", FIELD_LABEL),
Paragraph("Deformity", FIELD_LABEL),
]
col_widths = [PAGE_W*0.20, PAGE_W*0.08, PAGE_W*0.08, PAGE_W*0.10,
PAGE_W*0.14, PAGE_W*0.10, PAGE_W*0.10, PAGE_W*0.10]
rows = [header]
for i, j in enumerate(joints):
bg = LIGHT_GREY if i % 2 == 0 else WHITE
rows.append([
Paragraph(j, BODY), "☐", "☐", "☐", "☐", "☐", "☐", "☐"
])
t = Table(rows, colWidths=col_widths)
ts = TableStyle([
("BACKGROUND", (0,0), (-1,0), SECTION_HDR),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 8),
("ALIGN", (1,0), (-1,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("ROWBACKGROUNDS",(0,1),(-1,-1), [LIGHT_GREY, WHITE]),
("INNERGRID", (0,0), (-1,-1), 0.3, MID_GREY),
("BOX", (0,0), (-1,-1), 0.8, MID_BLUE),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING",(0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
])
t.setStyle(ts)
return t
# ── Extra-articular checklist table ──────────────────────────────────────────
def extra_articular_table():
systems = [
("Constitutional", "Fatigue ☐ Weight loss ☐ Low-grade fever ☐ Malaise ☐"),
("Skin / Nodules", "Rheumatoid nodules ☐ Vasculitis ☐ Pyoderma gangrenosum ☐"),
("Eyes", "Dry eyes (sicca) ☐ Episcleritis ☐ Scleritis ☐ Visual blurring ☐"),
("Cardiovascular", "Chest pain ☐ Palpitations ☐ Breathlessness on exertion ☐ Pericarditis hx ☐"),
("Respiratory", "Dry cough ☐ Breathlessness at rest ☐ Pleuritic pain ☐ Haemoptysis ☐"),
("Neurological", "Carpal tunnel syndrome ☐ Tarsal tunnel ☐ Cervical myelopathy ☐ Peripheral neuropathy ☐"),
("ENT", "Hoarseness ☐ Throat fullness ☐ Stridor ☐ Jaw pain (TMJ) ☐"),
("Haematological", "Pallor ☐ Splenomegaly ☐ Lymphadenopathy ☐ Recurrent infections ☐"),
("Renal", "Oedema ☐ Frothy urine (proteinuria) ☐ Reduced urine output ☐"),
("Gastrointestinal", "Nausea/vomiting ☐ GI bleeding ☐ Mouth ulcers ☐ Abdominal pain ☐"),
]
header = [Paragraph("System", FIELD_LABEL), Paragraph("Symptoms — circle / tick all present", FIELD_LABEL)]
rows = [header]
for s, sym in systems:
rows.append([Paragraph(s, BODY), Paragraph(sym, BODY)])
col_widths = [PAGE_W*0.20, PAGE_W*0.80]
t = Table(rows, colWidths=col_widths)
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), SECTION_HDR),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 7.5),
("VALIGN", (0,0), (-1,-1), "TOP"),
("ROWBACKGROUNDS",(0,1),(-1,-1), [LIGHT_GREY, WHITE]),
("INNERGRID", (0,0), (-1,-1), 0.3, MID_GREY),
("BOX", (0,0), (-1,-1), 0.8, MID_BLUE),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
]))
return t
# ── ACR/EULAR scoring table ───────────────────────────────────────────────────
def acr_eular_table():
data = [
[Paragraph("Domain", FIELD_LABEL), Paragraph("Criteria", FIELD_LABEL), Paragraph("Score", FIELD_LABEL), Paragraph("Tick", FIELD_LABEL)],
[Paragraph("Joint Involvement\n(select highest)", BODY),
Paragraph("1 medium-large joint", BODY), Paragraph("0", BODY), "☐"],
["", Paragraph("2-10 medium-large joints", BODY), Paragraph("1", BODY), "☐"],
["", Paragraph("1-3 small joints", BODY), Paragraph("2", BODY), "☐"],
["", Paragraph("4-10 small joints", BODY), Paragraph("3", BODY), "☐"],
["", Paragraph(">10 joints incl. ≥1 small joint", BODY), Paragraph("5", BODY), "☐"],
[Paragraph("Serology\n(select highest)", BODY),
Paragraph("RF negative AND ACPA negative", BODY), Paragraph("0", BODY), "☐"],
["", Paragraph("RF or ACPA low positive (≤3× ULN)", BODY), Paragraph("2", BODY), "☐"],
["", Paragraph("RF or ACPA high positive (>3× ULN)", BODY), Paragraph("3", BODY), "☐"],
[Paragraph("Duration of\nSynovitis", BODY),
Paragraph("<6 weeks", BODY), Paragraph("0", BODY), "☐"],
["", Paragraph("≥6 weeks", BODY), Paragraph("1", BODY), "☐"],
[Paragraph("Acute Phase\nReactants", BODY),
Paragraph("Normal CRP and ESR", BODY), Paragraph("0", BODY), "☐"],
["", Paragraph("Abnormal CRP or ESR", BODY), Paragraph("1", BODY), "☐"],
[Paragraph("TOTAL SCORE", FIELD_LABEL), Paragraph("Score ≥6 = Definite RA", NOTE),
Paragraph("__ / 10", FIELD_LABEL), ""],
]
col_widths = [PAGE_W*0.22, PAGE_W*0.52, PAGE_W*0.14, PAGE_W*0.12]
t = Table(data, colWidths=col_widths)
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), SECTION_HDR),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 8),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("ALIGN", (2,0), (3,-1), "CENTER"),
("SPAN", (0,1), (0,5)),
("SPAN", (0,6), (0,8)),
("SPAN", (0,9), (0,10)),
("SPAN", (0,11),(0,12)),
("INNERGRID", (0,0), (-1,-1), 0.3, MID_GREY),
("BOX", (0,0), (-1,-1), 0.8, MID_BLUE),
("BACKGROUND", (0,-1),(-1,-1), colors.HexColor("#EBF3FB")),
("FONTNAME", (0,-1),(-1,-1), "Helvetica-Bold"),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 5),
("ROWBACKGROUNDS",(0,1), (-1,-2), [LIGHT_GREY, WHITE]),
]))
return t
# ── Disease activity scores ───────────────────────────────────────────────────
def das28_table():
data = [
[Paragraph("DAS28 Component", FIELD_LABEL),
Paragraph("Value", FIELD_LABEL),
Paragraph("DAS28 Interpretation", FIELD_LABEL),
Paragraph("Threshold", FIELD_LABEL)],
[Paragraph("Tender joint count (28)", BODY), "____",
Paragraph("Remission", BODY), Paragraph("< 2.6", BODY)],
[Paragraph("Swollen joint count (28)", BODY), "____",
Paragraph("Low disease activity", BODY), Paragraph("2.6 – 3.2", BODY)],
[Paragraph("ESR (mm/hr) or CRP (mg/L)", BODY), "____",
Paragraph("Moderate disease activity", BODY), Paragraph("3.2 – 5.1", BODY)],
[Paragraph("Patient global VAS (0-100 mm)", BODY), "____",
Paragraph("High disease activity", BODY), Paragraph("> 5.1", BODY)],
[Paragraph("DAS28 Score:", FIELD_LABEL), "____________",
Paragraph("Interpretation:", FIELD_LABEL), "_______________"],
]
col_widths = [PAGE_W*0.35, PAGE_W*0.15, PAGE_W*0.32, PAGE_W*0.18]
t = Table(data, colWidths=col_widths)
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), SECTION_HDR),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 8),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("INNERGRID", (0,0), (-1,-1), 0.3, MID_GREY),
("BOX", (0,0), (-1,-1), 0.8, MID_BLUE),
("ROWBACKGROUNDS",(0,1),(-1,-2), [LIGHT_GREY, WHITE]),
("BACKGROUND", (0,-1),(-1,-1), colors.HexColor("#EBF3FB")),
("FONTNAME", (0,-1),(-1,-1), "Helvetica-Bold"),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 5),
]))
return t
# ─────────────────────────────────────────────────────────────────────────────
# TITLE PAGE / HEADER
# ─────────────────────────────────────────────────────────────────────────────
def build_header():
title_data = [
[Paragraph("RHEUMATOID ARTHRITIS", TITLE_STYLE)],
[Paragraph("Initial Patient Assessment Template", SUBTITLE_S)],
[Paragraph("ACR/EULAR 2010 Classification Criteria | DAS28 | Disease Activity Monitoring", SUBTITLE_S)],
]
t = Table(title_data, colWidths=[PAGE_W])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), DEEP_BLUE),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING",(0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 10),
]))
return t
# ─────────────────────────────────────────────────────────────────────────────
# BUILD DOCUMENT
# ─────────────────────────────────────────────────────────────────────────────
story = []
# Title
story.append(build_header())
story.append(spacer(4*mm))
# ── SECTION 1: PATIENT DEMOGRAPHICS ─────────────────────────────────────────
story.append(section_header("1. PATIENT DEMOGRAPHICS"))
story.append(spacer(2*mm))
story.append(three_fields("Patient Name:", "Date of Birth:", "Age (yrs):"))
story.append(spacer(2*mm))
story.append(three_fields("Hospital / MRN:", "Date of Assessment:", "Clinician:"))
story.append(spacer(2*mm))
story.append(two_fields("Sex: ☐ Male ☐ Female ☐ Other", "Marital Status:"))
story.append(spacer(2*mm))
story.append(two_fields("Occupation:", "Educational level:"))
story.append(spacer(2*mm))
story.append(two_fields("Address / Contact:", "Referred by:"))
story.append(spacer(4*mm))
# ── SECTION 2: CHIEF COMPLAINTS ─────────────────────────────────────────────
story.append(section_header("2. CHIEF COMPLAINTS (with duration)"))
story.append(spacer(2*mm))
complaints = [
"Joint pain (arthralgia / arthritis)", "Joint swelling",
"Morning stiffness", "Difficulty using hands / reduced grip",
"Fatigue / generalised weakness", "Joint deformity",
"Other:"
]
for i, c in enumerate(complaints):
data = [[
Paragraph(f"{i+1}. {c}", BODY),
Paragraph("Duration:", FIELD_LABEL),
"",
]]
t = Table(data, colWidths=[PAGE_W*0.55, PAGE_W*0.15, PAGE_W*0.30])
t.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "BOTTOM"),
("LINEBELOW", (2,0), (2,0), 0.5, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING",(0,0), (-1,-1), 2),
]))
story.append(t)
story.append(spacer(4*mm))
# ── SECTION 3: HISTORY OF PRESENTING ILLNESS ────────────────────────────────
story.append(section_header("3. HISTORY OF PRESENTING ILLNESS (HOPI)"))
story.append(spacer(2*mm))
# 3A – Joint Pain
story.append(sub_header("A. Joint Pain"))
story.append(spacer(1*mm))
story.append(two_fields("Site (which joints first involved)?", "Current joints involved:"))
story.append(spacer(2*mm))
story.append(Paragraph("Onset:", FIELD_LABEL))
story.append(checkbox_row(["Insidious (gradual)", "Acute / sudden", "Palindromic (episodic, migratory)", "Polyarticular from start", "Oligoarticular from start"], cols=3))
story.append(spacer(2*mm))
story.append(Paragraph("Character of pain:", FIELD_LABEL))
story.append(checkbox_row(["Dull aching", "Throbbing", "Burning", "Sharp", "Deep boring"], cols=5))
story.append(spacer(2*mm))
story.append(Paragraph("Pattern of joint involvement:", FIELD_LABEL))
story.append(checkbox_row(["Symmetric bilateral", "Asymmetric", "Additive (new joints added)", "Migratory (moves joint to joint)", "Persistent in same joints"], cols=3))
story.append(spacer(2*mm))
story.append(Paragraph("Aggravating factors:", FIELD_LABEL))
story.append(checkbox_row(["Activity / movement", "Rest", "Cold / rainy weather", "New / full moon", "Morning / after inactivity", "Stress"], cols=3))
story.append(spacer(2*mm))
story.append(Paragraph("Relieving factors:", FIELD_LABEL))
story.append(checkbox_row(["Warmth / heat", "Gentle movement", "NSAIDs / analgesics", "Steroids", "Rest"], cols=5))
story.append(spacer(2*mm))
story.append(Paragraph("Radiation:", FIELD_LABEL))
story.append(checkbox_row(["No radiation", "Neck → arm (cervical involvement)", "Wrist → hand (carpal tunnel)", "Other:"], cols=4))
story.append(spacer(2*mm))
story.append(Paragraph("Pain severity — NRS (circle number):", FIELD_LABEL))
story.append(spacer(1*mm))
story.append(pain_scale())
story.append(Paragraph("0 = No pain | 10 = Worst imaginable pain", SMALL))
story.append(spacer(3*mm))
# 3B – Stiffness
story.append(sub_header("B. Morning Stiffness — KEY DIAGNOSTIC FEATURE"))
story.append(spacer(1*mm))
story.append(Paragraph("⚠ In RA, morning stiffness typically lasts >1 hour. Duration differentiates RA (>1 hr) from OA (<30 min).", NOTE))
story.append(spacer(2*mm))
data = [
[Paragraph("Duration of morning stiffness:", FIELD_LABEL), "", ""],
[Paragraph("☐ <30 minutes", BODY), Paragraph("☐ 30–60 minutes", BODY), Paragraph("☐ >1 hour — specify: ___ hrs ___ mins", BODY)],
]
t = Table(data, colWidths=[PAGE_W/3]*3)
t.setStyle(TableStyle([
("SPAN", (0,0), (2,0)),
("LINEBELOW", (0,0), (2,0), 0.5, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING",(0,0), (-1,-1), 2),
]))
story.append(t)
story.append(spacer(2*mm))
story.append(Paragraph("Is stiffness relieved by: ☐ Warmth ☐ Movement ☐ Hot shower ☐ Other: _______________", BODY))
story.append(spacer(3*mm))
# 3C – Swelling
story.append(sub_header("C. Joint Swelling"))
story.append(spacer(1*mm))
story.append(Paragraph("Nature of swelling: ☐ Soft (effusion) ☐ Boggy (synovial proliferation/pannus) ☐ Bony / hard (late disease)", BODY))
story.append(spacer(2*mm))
story.append(Paragraph("Distribution: ☐ Symmetric ☐ Asymmetric ☐ Migratory", BODY))
story.append(spacer(3*mm))
# 3D – Functional Impact
story.append(sub_header("D. Functional Impairment (HAQ proxy)"))
story.append(spacer(1*mm))
story.append(Paragraph("Can the patient perform the following? (☐ Yes ☐ No ☐ With difficulty)", FIELD_LABEL))
story.append(spacer(1*mm))
functional_items = [
"Dress / button clothes", "Grip objects / open jar", "Walk on flat ground",
"Climb stairs", "Turn taps / doorknobs", "Write / type",
"Rise from chair unaided", "Carry shopping bag", "Sleep without pain waking them"
]
story.append(checkbox_row(functional_items, cols=3))
story.append(spacer(4*mm))
# ── SECTION 4: EXTRA-ARTICULAR MANIFESTATIONS ────────────────────────────────
story.append(section_header("4. EXTRA-ARTICULAR MANIFESTATIONS (review of systems)"))
story.append(spacer(2*mm))
story.append(Paragraph("More common in RF-positive / ACPA-positive seropositive patients. Tick all that are present.", SMALL))
story.append(spacer(2*mm))
story.append(extra_articular_table())
story.append(spacer(4*mm))
# ── SECTION 5: JOINT INVOLVEMENT MAP ─────────────────────────────────────────
story.append(section_header("5. JOINT INVOLVEMENT MAP"))
story.append(spacer(2*mm))
story.append(Paragraph("Tick each joint involved. Note: RA characteristically SPARES DIP joints — involvement suggests PsA or OA.", NOTE))
story.append(spacer(2*mm))
story.append(joint_involvement_table())
story.append(spacer(2*mm))
story.append(two_fields("Total tender joint count (TJC):", "Total swollen joint count (SJC):"))
story.append(spacer(4*mm))
# ── SECTION 6: PAST MEDICAL HISTORY ──────────────────────────────────────────
story.append(section_header("6. PAST MEDICAL HISTORY"))
story.append(spacer(2*mm))
pmh_items = [
("Previous episodes of joint pain / swelling?", "☐ Yes ☐ No"),
("Tuberculosis?", "☐ Yes ☐ No"),
("Gonorrhoea / sexually transmitted infection?", "☐ Yes ☐ No"),
("Hepatitis B or C?", "☐ Yes ☐ No"),
("Cardiovascular disease (IHD, HF, stroke)?", "☐ Yes ☐ No"),
("Diabetes mellitus?", "☐ Yes ☐ No"),
("Hypertension?", "☐ Yes ☐ No"),
("Osteoporosis / osteopenia?", "☐ Yes ☐ No"),
("Hypothyroidism?", "☐ Yes ☐ No"),
("Sicca syndrome / Sjogren's?", "☐ Yes ☐ No"),
("Malignancy?", "☐ Yes ☐ No"),
("Previous joint surgery / arthroplasty?", "☐ Yes ☐ No"),
("Serious infections (hospitalisation)?", "☐ Yes ☐ No"),
("Other: __________________________________", "☐ Yes ☐ No"),
]
pmh_rows = [[Paragraph(q, BODY), Paragraph(a, BODY)] for q, a in pmh_items]
t = Table(pmh_rows, colWidths=[PAGE_W*0.75, PAGE_W*0.25])
t.setStyle(TableStyle([
("FONTSIZE", (0,0), (-1,-1), 8),
("ROWBACKGROUNDS",(0,0),(-1,-1), [LIGHT_GREY, WHITE]),
("INNERGRID", (0,0), (-1,-1), 0.3, MID_GREY),
("BOX", (0,0), (-1,-1), 0.8, MID_BLUE),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(t)
story.append(spacer(4*mm))
# ── SECTION 7: DRUG / TREATMENT HISTORY ──────────────────────────────────────
story.append(section_header("7. DRUG AND TREATMENT HISTORY"))
story.append(spacer(2*mm))
tx_headers = [
Paragraph("Drug / Treatment", FIELD_LABEL),
Paragraph("Dose", FIELD_LABEL),
Paragraph("Duration", FIELD_LABEL),
Paragraph("Response", FIELD_LABEL),
Paragraph("Side Effects", FIELD_LABEL),
]
tx_rows = [tx_headers]
for _ in range(7):
tx_rows.append(["", "", "", "", ""])
t = Table(tx_rows, colWidths=[PAGE_W*0.28, PAGE_W*0.14, PAGE_W*0.14, PAGE_W*0.22, PAGE_W*0.22],
rowHeights=[None] + [8*mm]*7)
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), SECTION_HDR),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 8),
("INNERGRID", (0,0), (-1,-1), 0.3, MID_GREY),
("BOX", (0,0), (-1,-1), 0.8, MID_BLUE),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("ROWBACKGROUNDS",(0,1),(-1,-1), [LIGHT_GREY, WHITE]),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
]))
story.append(t)
story.append(spacer(2*mm))
story.append(Paragraph("Vaccination status (essential before biologic therapy):", FIELD_LABEL))
story.append(spacer(1*mm))
story.append(checkbox_row(["Hepatitis B ☐ Done ☐ Not done", "Pneumococcal ☐ Done ☐ Not done",
"Influenza (annual) ☐ Done ☐ Not done",
"COVID-19 ☐ Done ☐ Not done",
"Varicella Zoster ☐ Done ☐ Not done",
"Herpes Zoster ☐ Done ☐ Not done"], cols=3))
story.append(spacer(4*mm))
# ── SECTION 8: ALLERGIES ─────────────────────────────────────────────────────
story.append(section_header("8. DRUG ALLERGIES AND ADVERSE REACTIONS"))
story.append(spacer(2*mm))
story.append(text_box("Drug / Agent → Type of reaction → Date (if known):", height=12*mm))
story.append(spacer(4*mm))
# ── SECTION 9: FAMILY HISTORY ────────────────────────────────────────────────
story.append(section_header("9. FAMILY HISTORY"))
story.append(spacer(2*mm))
fh_items = [
("Rheumatoid arthritis (first-degree relatives)?", "☐ Yes ☐ No ☐ Unknown"),
("Other autoimmune disease (SLE, psoriasis, IBD, thyroid)?", "☐ Yes ☐ No ☐ Unknown"),
("Gout?", "☐ Yes ☐ No ☐ Unknown"),
("Tuberculosis?", "☐ Yes ☐ No ☐ Unknown"),
("Premature cardiovascular disease?", "☐ Yes ☐ No ☐ Unknown"),
("Malignancy?", "☐ Yes ☐ No ☐ Unknown"),
("Periodontal disease (gum disease)?", "☐ Yes ☐ No ☐ Unknown"),
]
fh_rows = [[Paragraph(q, BODY), Paragraph(a, BODY)] for q, a in fh_items]
t = Table(fh_rows, colWidths=[PAGE_W*0.70, PAGE_W*0.30])
t.setStyle(TableStyle([
("FONTSIZE", (0,0), (-1,-1), 8),
("ROWBACKGROUNDS",(0,0),(-1,-1), [LIGHT_GREY, WHITE]),
("INNERGRID", (0,0), (-1,-1), 0.3, MID_GREY),
("BOX", (0,0), (-1,-1), 0.8, MID_BLUE),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(t)
story.append(spacer(2*mm))
story.append(text_box("Additional family history details:", height=10*mm))
story.append(spacer(4*mm))
# ── SECTION 10: PERSONAL / SOCIAL HISTORY ────────────────────────────────────
story.append(section_header("10. PERSONAL AND SOCIAL HISTORY"))
story.append(spacer(2*mm))
story.append(Paragraph("Smoking status:", FIELD_LABEL))
story.append(Paragraph(
"☐ Never smoked ☐ Ex-smoker — Quit date: ___________ Pack-years: ______ ☐ Current smoker — Pack-years: ______",
BODY))
story.append(Paragraph("Note: Smoking is a major environmental risk factor for seropositive RA (ACPA/RF). Promotes PAD enzyme-mediated citrullination.", NOTE))
story.append(spacer(2*mm))
story.append(Paragraph("Alcohol:", FIELD_LABEL))
story.append(Paragraph(
"☐ None ☐ Occasional (<14 units/week) ☐ Regular (>14 units/week) "
" Units/week: ______ (Critical: interacts with methotrexate — hepatotoxicity risk)",
BODY))
story.append(spacer(2*mm))
story.append(two_fields("Occupation (current / previous):", "Occupational exposures:"))
story.append(spacer(2*mm))
story.append(Paragraph("Physical activity:", FIELD_LABEL))
story.append(checkbox_row(["Sedentary", "Light activity", "Moderate exercise", "Vigorous exercise"], cols=4))
story.append(spacer(2*mm))
story.append(two_fields("Diet (Mediterranean? Obesity?):", "BMI: ____ kg/m²"))
story.append(spacer(2*mm))
story.append(Paragraph("Living situation:", FIELD_LABEL))
story.append(checkbox_row(["Lives alone", "Lives with family", "Carer available at home",
"Independent in ADLs", "Requires ADL assistance"], cols=3))
story.append(spacer(2*mm))
story.append(two_fields("Socioeconomic status:", "Mental health / Depression screening:"))
story.append(spacer(4*mm))
# ── SECTION 11: MENSTRUAL / OBSTETRIC HISTORY (women) ────────────────────────
story.append(section_header("11. MENSTRUAL AND OBSTETRIC HISTORY (women only)"))
story.append(spacer(2*mm))
story.append(Paragraph("Note: RA classically IMPROVES during pregnancy (2nd/3rd trimester) and FLARES postpartum.", NOTE))
story.append(spacer(2*mm))
story.append(three_fields("Menarche age:", "Menopause age:", "LMP:"))
story.append(spacer(2*mm))
story.append(two_fields("Gravida: ____ Para: ____", "Miscarriages: ____"))
story.append(spacer(2*mm))
story.append(Paragraph("Oral contraceptive use: ☐ Never ☐ Past ☐ Current (may be protective against RA onset)", BODY))
story.append(spacer(2*mm))
story.append(Paragraph("Postpartum flare experienced? ☐ Yes ☐ No", BODY))
story.append(spacer(2*mm))
story.append(two_fields("Current contraception (if relevant):", "Planning pregnancy? ☐ Yes ☐ No"))
story.append(spacer(4*mm))
# ── SECTION 12: ACR/EULAR CLASSIFICATION CRITERIA ───────────────────────────
story.append(section_header("12. ACR/EULAR 2010 CLASSIFICATION CRITERIA (Score ≥6 = Definite RA)"))
story.append(spacer(2*mm))
story.append(acr_eular_table())
story.append(spacer(4*mm))
# ── SECTION 13: DISEASE ACTIVITY ASSESSMENT ──────────────────────────────────
story.append(section_header("13. DISEASE ACTIVITY ASSESSMENT — DAS28"))
story.append(spacer(2*mm))
story.append(das28_table())
story.append(spacer(2*mm))
story.append(two_fields("CDAI score:", "SDAI score:"))
story.append(spacer(4*mm))
# ── SECTION 14: INVESTIGATIONS REQUESTED ─────────────────────────────────────
story.append(section_header("14. INVESTIGATIONS REQUESTED"))
story.append(spacer(2*mm))
story.append(sub_header("Serology / Immunology"))
story.append(checkbox_row([
"Rheumatoid Factor (RF)", "Anti-CCP (ACPA)", "ANA",
"Anti-dsDNA", "Anti-Sm", "ANCA",
"Complement C3/C4", "Immunoglobulins", "Cryoglobulins",
], cols=3))
story.append(spacer(2*mm))
story.append(sub_header("Haematology / Biochemistry"))
story.append(checkbox_row([
"Full Blood Count (FBC)", "ESR", "CRP",
"Urea + Creatinine + eGFR", "Liver Function Tests", "Uric acid",
"Fasting glucose / HbA1c", "Thyroid function (TSH)", "Lipid profile",
"Serum calcium / phosphate", "Vitamin D (25-OH)", "Urine dipstick / PCR",
], cols=3))
story.append(spacer(2*mm))
story.append(sub_header("Imaging"))
story.append(checkbox_row([
"X-ray hands / wrists (PA)", "X-ray feet (DP)", "X-ray cervical spine (lateral flexion/extension)",
"Ultrasound joints (specify): ___________", "MRI (specify): ___________",
"DEXA scan (bone density)",
"Chest X-ray", "HRCT chest (ILD screen)", "Echocardiogram",
], cols=3))
story.append(spacer(2*mm))
story.append(sub_header("Pre-biologic / Pre-DMARD Screening"))
story.append(checkbox_row([
"Mantoux / Quantiferon-TB Gold (TB screening)", "Hepatitis B sAg + sAb + cAb",
"Hepatitis C antibody", "HIV (if indicated)",
"VDRL / syphilis screen (if indicated)", "Varicella IgG titres",
], cols=2))
story.append(spacer(4*mm))
# ── SECTION 15: CLINICIAN ASSESSMENT & PLAN ──────────────────────────────────
story.append(section_header("15. CLINICIAN ASSESSMENT AND MANAGEMENT PLAN"))
story.append(spacer(2*mm))
story.append(text_box("Diagnosis / Working Diagnosis:", height=10*mm))
story.append(spacer(2*mm))
story.append(text_box("Differential Diagnoses:", height=10*mm))
story.append(spacer(2*mm))
story.append(text_box("Management Plan (DMARDs, biologics, NSAIDs, steroids, physio, OT, surgical referral):", height=20*mm))
story.append(spacer(2*mm))
story.append(text_box("Patient Education Points Discussed:", height=12*mm))
story.append(spacer(2*mm))
story.append(two_fields("Review date:", "Rheumatologist signature:"))
story.append(spacer(4*mm))
# ── FOOTER NOTE ───────────────────────────────────────────────────────────────
story.append(hr())
story.append(Paragraph(
"Template based on ACR/EULAR 2010 RA Classification Criteria | Aletaha D et al. Arthritis Rheum 2010;62:2569–2581 | "
"Goldman-Cecil Medicine | Firestein & Kelley's Textbook of Rheumatology | S Das Manual on Clinical Surgery",
SMALL
))
story.append(Paragraph("For clinical use only. Not a substitute for individual clinical judgement.", SMALL))
# ── BUILD ──────────────────────────────────────────────────────────────────────
doc.build(story)
print(f"PDF generated: {OUTPUT_PATH}")
cd /home/daytona/workspace/ra-template && python generate_ra_template.py 2>&1
ls -lh /home/daytona/workspace/ra-template/RA_Initial_Assessment_Template.pdf
RA Initial Assessment Template
PDF Document · PDF
| # | Section | Highlights |
|---|---|---|
| 1 | Patient Demographics | Name, DOB, MRN, clinician, referral |
| 2 | Chief Complaints | 7 common RA complaints with duration fields |
| 3 | HOPI | Joint pain (site, onset, character, pattern, aggravating/relieving), NRS pain scale (colour-coded 0-10), morning stiffness with tick-boxes, swelling characterisation, HAQ-proxy functional checklist |
| 4 | Extra-articular Manifestations | 10-system table (constitutional, skin, eyes, CVS, respiratory, neuro, ENT, haem, renal, GI) with tick-box symptoms |
| 5 | Joint Involvement Map | 12-joint grid with columns for R/L/bilateral, tenderness, swelling, warmth, deformity; TJC and SJC fields |
| 6 | Past Medical History | 14-item checklist (TB, hepatitis, CV disease, osteoporosis, thyroid, malignancy etc.) |
| 7 | Drug & Treatment History | 7-row medication table (drug, dose, duration, response, side effects) + vaccination status |
| 8 | Allergies | Free-text reaction log |
| 9 | Family History | RA, autoimmune, gout, TB, premature CVD, periodontal disease |
| 10 | Personal / Social History | Smoking (pack-years), alcohol (MTX interaction warning), diet/BMI, occupation, ADL independence |
| 11 | Menstrual / Obstetric History | Pregnancy improvement / postpartum flare note, OCP use, contraception |
| 12 | ACR/EULAR 2010 Criteria | Scoring table for joint involvement, serology, synovitis duration, acute phase reactants (score ≥6 = definite RA) |
| 13 | Disease Activity — DAS28 | TJC/SJC/ESR/global VAS entry fields with DAS28 interpretation thresholds |
| 14 | Investigations | Serology, haematology, imaging, pre-biologic/pre-DMARD screening checklists |
| 15 | Assessment & Plan | Diagnosis, differentials, management plan, patient education, review date |