diff --git a/examples/curriculum_courses/content.json b/examples/curriculum_courses/content.json
new file mode 100644
index 00000000..18c8b907
--- /dev/null
+++ b/examples/curriculum_courses/content.json
@@ -0,0 +1,444 @@
+{
+ "courses": [
+ {
+ "source_id": "hummingbird-biology",
+ "title": "Hummingbird Biology",
+ "description": "Learn about the fascinating biology of hummingbirds",
+ "units": [
+ {
+ "source_id": "hb-anatomy-physiology",
+ "title": "Anatomy & Physiology",
+ "description": "Explore the unique anatomical features of hummingbirds",
+ "learning_objectives": [
+ "Identify the key bones in a hummingbird skeleton",
+ "Explain how skeletal adaptations enable hovering flight",
+ "Describe the major muscle groups used in flight",
+ "Explain the role of fast-twitch muscles in wing movement",
+ "Describe how the respiratory system supports high metabolism",
+ "Explain the function of air sacs in hummingbird respiration"
+ ],
+ "lessons": [
+ {
+ "source_id": "hb-skeletal-structure",
+ "title": "Skeletal Structure",
+ "learning_objective_indices": [0, 1],
+ "content": "
Hummingbirds possess one of nature's most remarkable skeletal systems, evolved over millions of years to support their unique flight capabilities. Unlike most birds, whose skeletons account for about 5% of body weight, hummingbird bones are extraordinarily lightweight while maintaining exceptional strength.
Bone Structure
The secret lies in their pneumatized bones - hollow structures reinforced with internal struts, similar to the engineering principles used in aircraft wings. This honeycomb-like internal architecture reduces weight by up to 50% compared to solid bone while retaining structural integrity.
Did You Know? A Ruby-throated Hummingbird's entire skeleton weighs less than a paperclip - about 0.9 grams! Yet these delicate bones withstand G-forces that would shatter human bones.
Key Skeletal Features
The hummingbird skeleton includes several specialized adaptations:
Fused vertebrae: Several neck and back vertebrae are fused together, creating a rigid backbone that provides stability during the rapid wing movements of hovering flight.
Enlarged keel bone: The sternum features a dramatically enlarged keel that extends down from the chest. This massive anchor point provides attachment for the powerful flight muscles that make up nearly 30% of body weight.
Ball-and-socket shoulder joints: Perhaps the most crucial adaptation, these specialized joints allow the wings to rotate a full 180 degrees, enabling the unique figure-8 wing pattern essential for hovering.
Reduced leg bones: Since hummingbirds spend most of their lives in flight, their leg bones are minimal - strong enough for perching but lightweight enough not to impede flight.
Species Spotlight: Bee Hummingbird The world's smallest bird, Cuba's Bee Hummingbird (Mellisuga helenae), has a skeleton so tiny that its entire body mass is just 1.6-2 grams - lighter than a US penny. Its femur (thigh bone) is about 5mm long, yet perfectly formed for perching.
Evolutionary Significance
These skeletal adaptations represent millions of years of evolutionary refinement. Each feature works in concert with the others: the fused vertebrae wouldn't be useful without the flexible shoulders, and the enlarged keel would be unnecessary without the powerful muscles it supports.
Human Comparison: If humans had proportionally sized keels like hummingbirds, our breastbones would extend outward about 30 cm (1 foot) from our chests! This would be necessary to anchor flight muscles weighing over 40 pounds on each side.
",
+ "exercise_questions": [
+ {
+ "type": "single_select",
+ "id": "hb-skel-q1",
+ "question": "A biologist discovers a new bird species with solid, dense bones. Based on what you learned about hummingbird skeletal adaptations, what flight behavior would this bird likely be UNABLE to perform?",
+ "correct_answer": "Sustained hovering in place",
+ "all_answers": ["Sustained hovering in place", "Perching on branches", "Walking on the ground", "Sleeping while perched"]
+ },
+ {
+ "type": "multiple_select",
+ "id": "hb-skel-q2",
+ "question": "An engineer is designing a micro-drone inspired by hummingbirds. Which skeletal features should they incorporate into their design for hovering capability?",
+ "correct_answers": ["A rigid central frame like fused vertebrae", "Rotating joints allowing 180-degree wing rotation"],
+ "all_answers": ["A rigid central frame like fused vertebrae", "Rotating joints allowing 180-degree wing rotation", "Heavy landing gear for stability", "Flexible backbone for extra maneuverability"]
+ },
+ {
+ "type": "single_select",
+ "id": "hb-skel-q3",
+ "question": "A Bee Hummingbird weighing 2 grams has flight muscles comprising 30% of its body weight. If its keel bone were reduced to the size of a typical songbird's (proportionally), what problem would the hummingbird face?",
+ "correct_answer": "Insufficient anchor points for powerful flight muscles",
+ "all_answers": ["Insufficient anchor points for powerful flight muscles", "Inability to digest nectar", "Loss of body temperature regulation", "Reduced vision capabilities"]
+ }
+ ]
+ },
+ {
+ "source_id": "hb-muscle-systems",
+ "title": "Muscle Systems",
+ "learning_objective_indices": [2, 3],
+ "content": "
The muscular system of hummingbirds is a marvel of biological engineering, representing the most extreme adaptation for sustained powered flight in the animal kingdom. These tiny birds possess muscles that can contract and relax at frequencies that would cause fatigue within seconds in other animals.
Flight Muscle Composition
Hummingbird flight muscles comprise an astounding 25-30% of total body weight - the highest proportion of any bird species. By comparison, human leg muscles (our primary locomotion muscles) make up only about 20% of body weight. This massive investment in flight capability comes at the cost of leg strength; hummingbirds can barely walk and instead shuffle sideways on their perches.
Did You Know? The supracoracoideus pulley system is unique to birds with powered flight. It's so efficient that hummingbird muscles can contract and relax completely in just 0.01 seconds - that's 100 times per second!
The Two Primary Flight Muscles
Two muscles dominate the hummingbird's flight system:
Pectoralis major: This large breast muscle powers the downstroke, pulling the wing downward with tremendous force. In most birds, the downstroke provides the majority of lift, but in hummingbirds, it contributes only about 50%.
Supracoracoideus: This smaller muscle powers the upstroke through an ingenious pulley system. A tendon from this muscle passes through a hole in the shoulder (the triosseal canal) to attach to the top of the wing bone, allowing it to pull the wing upward - essentially working like a rope through a pulley.
Species Spotlight: Anna's Hummingbird The Anna's Hummingbird (Calypte anna) of western North America has been clocked with wing beats up to 80 times per second during normal hovering. During courtship dives, males reach speeds of 385 body lengths per second - the highest length-specific velocity of any vertebrate!
Fast-Twitch Fiber Dominance
What makes hummingbird muscles truly extraordinary is their composition. They are composed almost entirely of fast-twitch muscle fibers - the same type that powers a sprinter's explosive speed. However, unlike in humans where fast-twitch fibers fatigue quickly, hummingbird fast-twitch muscles have been modified to resist fatigue through:
Extremely high mitochondria density (the cellular powerhouses)
Rich capillary networks delivering oxygen
Elevated myoglobin levels storing oxygen within the muscle
This allows wing beats of 50-80 times per second in normal flight, and up to 200 beats per second during courtship dives - frequencies that would instantly fatigue any other vertebrate muscle.
Human Comparison: If a human sprinter's muscles worked like hummingbird muscles, they could sprint at full speed continuously for hours without tiring. Our fast-twitch fibers deplete their energy in about 10-15 seconds, while hummingbird fast-twitch fibers can work indefinitely thanks to their enhanced oxygen delivery system.
",
+ "exercise_questions": [
+ {
+ "type": "single_select",
+ "id": "hb-musc-q1",
+ "question": "A researcher is studying a 4-gram hummingbird. If they weigh just the flight muscles, approximately what mass should they expect?",
+ "correct_answer": "1.0-1.2 grams (25-30% of body weight)",
+ "all_answers": ["0.2-0.4 grams (5-10%)", "0.6-0.8 grams (15-20%)", "1.0-1.2 grams (25-30% of body weight)", "1.6-2.0 grams (40-50%)"]
+ },
+ {
+ "type": "single_select",
+ "id": "hb-musc-q2",
+ "question": "A veterinarian examining an injured hummingbird notices damage to the tendon passing through the triosseal canal. Which flight motion would be most affected?",
+ "correct_answer": "The upstroke (wing lifting)",
+ "all_answers": ["The downstroke (wing pushing down)", "The upstroke (wing lifting)", "Forward flight only", "Perching ability"]
+ },
+ {
+ "type": "single_select",
+ "id": "hb-musc-q3",
+ "question": "Scientists want to create artificial muscles for a hovering drone. Which cellular feature of hummingbird muscles would be MOST critical to replicate?",
+ "correct_answer": "High mitochondria density for continuous energy",
+ "all_answers": ["Large cell size for more power", "High mitochondria density for continuous energy", "Low oxygen requirements", "Slow contraction speed"]
+ }
+ ]
+ },
+ {
+ "source_id": "hb-respiratory-system",
+ "title": "Respiratory System",
+ "learning_objective_indices": [4, 5],
+ "content": "
To power their extraordinary metabolism, hummingbirds have evolved the most efficient respiratory system of any vertebrate. Their oxygen demands during flight are so extreme that a conventional bird respiratory system would be completely inadequate.
Metabolic Demands
During hovering flight, a hummingbird's metabolic rate increases to approximately 10 times its resting rate. If a human scaled up to this level of exertion, it would be equivalent to running multiple marathons simultaneously. This demands an oxygen delivery system of unparalleled efficiency.
Did You Know? Hummingbird lungs extract oxygen so efficiently that they can thrive at altitudes of 14,000+ feet in the Andes mountains, where humans struggle to breathe. The Hillstar Hummingbird lives permanently at elevations that would give most people severe altitude sickness!
The Air Sac System
Like all birds, hummingbirds possess air sacs - thin-walled extensions of the respiratory system that penetrate throughout the body. However, hummingbirds have optimized this system to its extreme:
Nine air sacs: These balloon-like structures are distributed throughout the body cavity and even extend into some bones.
Unidirectional airflow: Unlike mammalian lungs where air flows in and out, the air sac system creates continuous one-way flow through the lungs. Air passes through the lungs during both inhalation AND exhalation.
Cross-current gas exchange: Blood flows perpendicular to air flow in the lungs, creating the most efficient possible oxygen extraction - up to 10 times more efficient than human lungs.
Species Spotlight: Giant Hummingbird The Giant Hummingbird (Patagona gigas) of South America is the largest hummingbird at 20-24 grams - about the weight of 8 pennies. Despite its \"giant\" size, its respiratory system is so efficient that it can hover at 15,000 feet in the Andes, feeding on high-altitude flowers where the oxygen concentration is only 60% of sea level.
Breathing Rate
At rest, hummingbirds breathe about 250 times per minute - already faster than most small mammals. During flight, this can increase to over 500 breaths per minute. Each breath is shallow but extremely efficient, rapidly cycling fresh air through the system.
Integration with Flight
The respiratory and flight systems work in perfect synchrony. The pumping action of the flight muscles actually assists in moving air through the air sac system, creating a beautiful example of biological integration where two systems enhance each other's performance.
Human Comparison: If humans had hummingbird-style lungs, we could hold our breath for over an hour! Our tidal breathing (air in, air out) wastes about 30% of each breath as \"dead space\" - air that never reaches the gas exchange surfaces. Hummingbird unidirectional flow eliminates this inefficiency entirely.
",
+ "exercise_questions": [
+ {
+ "type": "single_select",
+ "id": "hb-resp-q1",
+ "question": "A biologist is dissecting a hummingbird to study its respiratory anatomy. How many distinct air sac structures should they expect to find throughout the body cavity?",
+ "correct_answer": "Nine air sacs",
+ "all_answers": ["Two air sacs", "Five air sacs", "Nine air sacs", "Twelve air sacs"]
+ },
+ {
+ "type": "multiple_select",
+ "id": "hb-resp-q2",
+ "question": "Engineers designing a high-efficiency ventilation system are studying hummingbird lungs for inspiration. Which features should they replicate?",
+ "correct_answers": ["One-way airflow through the exchange area", "Perpendicular flow between air and fluid for maximum transfer"],
+ "all_answers": ["One-way airflow through the exchange area", "Perpendicular flow between air and fluid for maximum transfer", "Single storage chamber", "Back-and-forth tidal flow"]
+ },
+ {
+ "type": "single_select",
+ "id": "hb-resp-q3",
+ "question": "A Hillstar Hummingbird thrives at 14,000 feet elevation where oxygen is scarce. What primarily enables it to survive where humans would struggle?",
+ "correct_answer": "Continuous airflow through lungs on both inhale and exhale",
+ "all_answers": ["Continuous airflow through lungs on both inhale and exhale", "Unusually large lungs", "Very slow breathing to conserve oxygen", "Ability to hold breath for long periods"]
+ }
+ ]
+ }
+ ],
+ "prepost_questions": [
+ {"id": "hb-anat-pp-1a", "variant": "A", "lo_index": 0, "type": "single_select", "question": "Which bone is enlarged in hummingbirds for muscle attachment?", "correct_answer": "Keel bone (sternum)", "all_answers": ["Keel bone (sternum)", "Femur", "Skull", "Pelvis"]},
+ {"id": "hb-anat-pp-1b", "variant": "B", "lo_index": 0, "type": "single_select", "question": "What type of bones do hummingbirds have to reduce weight?", "correct_answer": "Hollow pneumatized bones", "all_answers": ["Hollow pneumatized bones", "Dense solid bones", "Cartilage only", "No bones"]},
+ {"id": "hb-anat-pp-2a", "variant": "A", "lo_index": 1, "type": "single_select", "question": "How do fused vertebrae help hummingbirds?", "correct_answer": "Provide stability during hovering", "all_answers": ["Provide stability during hovering", "Allow greater flexibility", "Store more fat", "Produce more blood"]},
+ {"id": "hb-anat-pp-2b", "variant": "B", "lo_index": 1, "type": "single_select", "question": "What allows hummingbird wings to rotate 180 degrees?", "correct_answer": "Ball-and-socket shoulder joints", "all_answers": ["Ball-and-socket shoulder joints", "Extra wing bones", "Longer feathers", "Stronger muscles only"]},
+ {"id": "hb-anat-pp-3a", "variant": "A", "lo_index": 2, "type": "single_select", "question": "Which muscle powers the downstroke in hummingbird flight?", "correct_answer": "Pectoralis major", "all_answers": ["Pectoralis major", "Supracoracoideus", "Biceps", "Deltoid"]},
+ {"id": "hb-anat-pp-3b", "variant": "B", "lo_index": 2, "type": "single_select", "question": "Which muscle uses a pulley system to power the upstroke?", "correct_answer": "Supracoracoideus", "all_answers": ["Supracoracoideus", "Pectoralis major", "Triceps", "Trapezius"]},
+ {"id": "hb-anat-pp-4a", "variant": "A", "lo_index": 3, "type": "single_select", "question": "How many times per second can hummingbird wings beat?", "correct_answer": "50-80 times", "all_answers": ["10-20 times", "30-40 times", "50-80 times", "100+ times normally"]},
+ {"id": "hb-anat-pp-4b", "variant": "B", "lo_index": 3, "type": "single_select", "question": "What prevents hummingbird fast-twitch muscles from fatiguing?", "correct_answer": "High mitochondria density and rich capillaries", "all_answers": ["High mitochondria density and rich capillaries", "Slow contraction speed", "Low oxygen demand", "Rest between beats"]},
+ {"id": "hb-anat-pp-5a", "variant": "A", "lo_index": 4, "type": "single_select", "question": "How many breaths per minute during flight?", "correct_answer": "Over 500", "all_answers": ["50", "150", "250", "Over 500"]},
+ {"id": "hb-anat-pp-5b", "variant": "B", "lo_index": 4, "type": "single_select", "question": "Why do hummingbirds need such efficient respiration?", "correct_answer": "Metabolic rate increases 10x during flight", "all_answers": ["Metabolic rate increases 10x during flight", "They live at high altitudes only", "They are very large", "They breathe underwater"]},
+ {"id": "hb-anat-pp-6a", "variant": "A", "lo_index": 5, "type": "single_select", "question": "What do air sacs provide in the respiratory system?", "correct_answer": "Continuous unidirectional airflow", "all_answers": ["Continuous unidirectional airflow", "Blood storage", "Food digestion", "Waste removal"]},
+ {"id": "hb-anat-pp-6b", "variant": "B", "lo_index": 5, "type": "single_select", "question": "How does cross-current gas exchange benefit hummingbirds?", "correct_answer": "Up to 10x more efficient oxygen extraction", "all_answers": ["Up to 10x more efficient oxygen extraction", "Faster digestion", "Better hearing", "Stronger bones"]}
+ ]
+ },
+ {
+ "source_id": "hb-feeding-metabolism",
+ "title": "Feeding & Metabolism",
+ "description": "Understand how hummingbirds fuel their high-energy lifestyle",
+ "learning_objectives": [
+ "Identify the primary food sources for hummingbirds",
+ "Explain the role of nectar in hummingbird diet",
+ "Describe the digestive adaptations of hummingbirds",
+ "Explain how hummingbirds process sugars efficiently",
+ "Describe torpor and its role in energy conservation",
+ "Explain the metabolic rate differences between active and resting states"
+ ],
+ "lessons": [
+ {
+ "source_id": "hb-diet-foraging",
+ "title": "Diet & Foraging",
+ "learning_objective_indices": [0, 1],
+ "content": "
Hummingbirds are specialized nectarivores, having evolved in close partnership with flowering plants over millions of years. This mutualistic relationship has shaped both their behavior and physiology in remarkable ways.
Primary Food Sources
The hummingbird diet consists of two main components:
Nectar: Comprising about 90% of their caloric intake, nectar from flowers provides the simple sugars (primarily sucrose, glucose, and fructose) that fuel their intense metabolism. A hummingbird may consume nectar from 1,000-2,000 flowers every day.
Arthropods: Small insects and spiders provide essential proteins, fats, vitamins, and minerals that nectar cannot supply. Hummingbirds catch insects mid-flight (hawking) or pluck them from leaves and spider webs (gleaning). This protein source is especially crucial during breeding season and for growing chicks.
Did You Know? Hummingbirds have exceptional spatial memory. They remember the exact location of every flower they've visited, when they visited it, and how long each flower takes to refill with nectar. Some researchers call this \"episodic-like memory\" - previously thought to be unique to humans!
Foraging Behavior
Hummingbirds are highly territorial feeders. A single bird may defend a patch of flowers aggressively, investing significant energy in chasing away competitors. This makes sense when you consider that they must consume roughly half their body weight in nectar each day - reliable food sources are worth fighting for.
Species Spotlight: Sword-billed Hummingbird The Sword-billed Hummingbird (Ensifera ensifera) of the Andes has a bill longer than its body (excluding tail) - up to 10cm! This extreme adaptation allows it to feed from deep tubular flowers like Passiflora mixta that no other hummingbird can access. It's the only bird that must preen with its feet because its bill is too long to reach its feathers.
Physical Adaptations for Feeding
Evolution has equipped hummingbirds with specialized feeding apparatus:
Bill shape: Bills vary by species, often matching specific flower types. Some are long and curved to access tubular flowers; others are short and straight for open blooms.
Tongue mechanism: The hummingbird tongue is not a simple straw. It's a forked, flexible structure that extends far beyond the bill tip. The tongue uses capillary action and elastic energy to lap nectar at rates up to 15-20 licks per second.
Memory: Hummingbirds remember which flowers they've visited recently and their refill times, allowing efficient foraging routes.
Human Comparison: If a human had to eat proportionally as much as a hummingbird, we'd need to consume about 150,000 calories per day - roughly 300 Big Macs! And we'd need to do it by visiting tens of thousands of individual food sources, remembering exactly when we last visited each one.
",
+ "exercise_questions": [
+ {"type": "single_select", "id": "hb-diet-q1", "question": "A wildlife researcher is setting up hummingbird feeders in a garden. To match the feeding intensity of wild foraging, approximately how many flower-equivalent feeding opportunities should she provide per hummingbird per day?", "correct_answer": "1,000-2,000 feeding opportunities", "all_answers": ["50-100 feeding opportunities", "200-500 feeding opportunities", "1,000-2,000 feeding opportunities", "5,000+ feeding opportunities"]},
+ {"type": "multiple_select", "id": "hb-diet-q2", "question": "A hummingbird rehabilitation center needs to create a complete diet for recovering birds. Which food sources must they provide?", "correct_answers": ["Sugar water or nectar substitute", "Small insects like fruit flies"], "all_answers": ["Sugar water or nectar substitute", "Small insects like fruit flies", "Seeds and grains", "Leafy vegetables"]},
+ {"type": "single_select", "id": "hb-diet-q3", "question": "A female hummingbird is raising chicks and needs extra protein. Why would she increase her insect hunting rather than simply drinking more nectar?", "correct_answer": "Nectar provides only sugar - insects provide essential proteins for growth", "all_answers": ["Nectar provides only sugar - insects provide essential proteins for growth", "Insects taste better to baby hummingbirds", "Insects are easier to catch than finding flowers", "Nectar has too much protein already"]}
+ ]
+ },
+ {
+ "source_id": "hb-digestive-system",
+ "title": "Digestive System",
+ "learning_objective_indices": [2, 3],
+ "content": "
The hummingbird digestive system is a masterpiece of biological optimization, designed for one primary purpose: extracting and processing sugars with maximum speed and efficiency.
Streamlined for Speed
Unlike most birds, hummingbirds have virtually eliminated food storage. The crop - a pouch in the esophagus where most birds store food - is nearly vestigial in hummingbirds. Food passes almost directly to the intestines because storage would add unnecessary weight and delay energy availability.
Did You Know? Hummingbirds can switch from burning stored fat to burning freshly consumed sugar within minutes. They're essentially running on \"just-in-time\" fuel delivery - like a race car that refuels while still on the track!
Intestinal Adaptations
The small intestine is where the magic happens:
Surface area: The intestinal lining has extensive folds and microvilli, maximizing the surface area for sugar absorption.
Enzyme concentration: Extremely high levels of sucrase (which breaks down sucrose) and other digestive enzymes line the intestinal walls.
Rapid absorption: Specialized transport proteins move glucose and fructose into the bloodstream with remarkable efficiency.
Species Spotlight: Rufous Hummingbird The Rufous Hummingbird (Selasphorus rufus) migrates up to 3,900 miles from Mexico to Alaska - the longest migration relative to body size of any bird. To fuel this journey, its digestive system can process nectar so efficiently that it gains 10% of body weight per day during migration stopovers, building crucial fat reserves.
Processing Speed
The result of these adaptations is astounding: sugars from nectar can be absorbed and available for use within 15-20 minutes of consumption. This is roughly 10 times faster than human sugar digestion. The speed is essential because a hovering hummingbird burns through its available energy in just 15-20 minutes - it quite literally cannot afford slow digestion.
Water Management
Nectar is typically 20-25% sugar and 75-80% water. Consuming half their body weight in nectar means hummingbirds also take in enormous amounts of water. Their kidneys work overtime, producing very dilute urine to expel excess water while retaining essential electrolytes.
Human Comparison: If human kidneys worked as hard as hummingbird kidneys (proportionally), we'd need to process about 150 liters of water per day - roughly 40 gallons, or the equivalent of taking 300 trips to the bathroom!
",
+ "exercise_questions": [
+ {"type": "single_select", "id": "hb-digest-q1", "question": "A hummingbird feeds at 8:00 AM. By what time should the energy from that nectar be available for hovering flight?", "correct_answer": "8:15-8:20 AM (15-20 minutes)", "all_answers": ["8:01-8:02 AM (1-2 minutes)", "8:15-8:20 AM (15-20 minutes)", "9:00-10:00 AM (1-2 hours)", "2:00 PM (6+ hours)"]},
+ {"type": "single_select", "id": "hb-digest-q2", "question": "A biochemist is developing an artificial nectar for hummingbird feeders. The nectar contains sucrose (table sugar). Which enzyme must be present in high concentrations in hummingbird intestines to process this?", "correct_answer": "Sucrase (breaks down sucrose)", "all_answers": ["Sucrase (breaks down sucrose)", "Amylase (breaks down starch)", "Lipase (breaks down fats)", "Pepsin (breaks down proteins)"]},
+ {"type": "single_select", "id": "hb-digest-q3", "question": "An evolutionary biologist compares bird digestive systems. Why would a large crop (food storage organ) actually be HARMFUL for a hummingbird's lifestyle?", "correct_answer": "Added weight slows flight and stored food delays energy availability", "all_answers": ["Added weight slows flight and stored food delays energy availability", "Large crops attract predators", "Crops interfere with wing movement", "Hummingbirds are allergic to crops"]}
+ ]
+ },
+ {
+ "source_id": "hb-energy-metabolism",
+ "title": "Energy & Metabolism",
+ "learning_objective_indices": [4, 5],
+ "content": "
Hummingbirds live on the edge of energy balance. Their metabolic rate during flight is the highest of any vertebrate, yet they have virtually no ability to store energy as fat. This precarious situation has led to fascinating adaptations.
Metabolic Extremes
The numbers are staggering:
Heart rate: Up to 1,200 beats per minute during flight (compared to 60-100 in resting humans)
Body temperature: 40-41°C (104-106°F), among the highest of any bird
Oxygen consumption: About 10 times the resting rate during hovering
Sugar consumption: Equivalent to a human eating 400+ pounds of sugar per day
Did You Know? A hummingbird's heart beats so fast that individual beats blend together into a continuous hum - that's where the name \"hummingbird\" actually comes from (along with their wing sound). A doctor using a stethoscope would hear a buzz, not distinct heartbeats!
The Energy Crisis of Night
With essentially no fat reserves, how do hummingbirds survive overnight without feeding? The answer is torpor - a state of controlled hypothermia that represents one of the most dramatic physiological adaptations in the animal kingdom.
Species Spotlight: Andean Hillstar The Andean Hillstar (Oreotrochilus estella) lives at elevations up to 17,100 feet where nighttime temperatures drop below freezing. It enters torpor almost every night, allowing its body temperature to drop to just 6.8°C (44°F) - the lowest recorded for any bird. This extreme torpor lets it survive in conditions that would kill most other hummingbirds.
Torpor: Controlled Shutdown
As night approaches, hummingbirds can enter torpor:
Metabolic rate: Drops by up to 95%
Heart rate: Slows from 1,200+ to as low as 50 beats per minute
Body temperature: Can drop from 40°C to as low as 18°C (64°F)
Responsiveness: The bird becomes nearly unresponsive, appearing dead to casual observation
Awakening from Torpor
Emerging from torpor takes 20-60 minutes of intense shivering as the bird gradually rewarms. During this vulnerable period, hummingbirds cannot fly or escape predators. The energy savings of torpor must be balanced against these risks, and not all hummingbirds enter torpor every night - it depends on energy reserves and environmental conditions.
Human Comparison: If a human entered a torpor-like state proportional to a hummingbird's, our heart rate would drop from 70 bpm to about 3 bpm, our body temperature would fall to 15°C (59°F), and we'd survive on just 100 calories per day instead of 2,000. Medical science is actually studying torpor to develop suspended animation for long space flights!
",
+ "exercise_questions": [
+ {"type": "single_select", "id": "hb-energy-q1", "question": "A birdwatcher finds a hummingbird on its perch at dawn that appears dead - cold, unresponsive, barely breathing. What is the most likely explanation?", "correct_answer": "The bird is in torpor and will revive as it warms up", "all_answers": ["The bird is in torpor and will revive as it warms up", "The bird is performing a mating display", "The bird is sick and needs veterinary care immediately", "The bird is simply sleeping normally"]},
+ {"type": "single_select", "id": "hb-energy-q2", "question": "A researcher wants to study how much energy torpor saves. If a hummingbird normally uses 100 units of energy per hour while resting, approximately how many units would it use during torpor?", "correct_answer": "About 5 units (95% reduction)", "all_answers": ["About 75 units (25% reduction)", "About 50 units (50% reduction)", "About 25 units (75% reduction)", "About 5 units (95% reduction)"]},
+ {"type": "single_select", "id": "hb-energy-q3", "question": "A scientist is monitoring a hummingbird with a tiny heart rate sensor. During hovering flight, approximately how many heartbeats should register per minute?", "correct_answer": "Around 1,200 beats per minute", "all_answers": ["Around 200 beats per minute", "Around 500 beats per minute", "Around 1,200 beats per minute", "Over 2,000 beats per minute"]}
+ ]
+ }
+ ],
+ "prepost_questions": [
+ {"id": "hb-metab-pp-1a", "variant": "A", "lo_index": 0, "type": "single_select", "question": "What is the primary food source for hummingbirds?", "correct_answer": "Nectar from flowers", "all_answers": ["Nectar from flowers", "Seeds", "Fish", "Leaves"]},
+ {"id": "hb-metab-pp-1b", "variant": "B", "lo_index": 0, "type": "single_select", "question": "What provides protein in the hummingbird diet?", "correct_answer": "Small insects and spiders", "all_answers": ["Small insects and spiders", "Nectar", "Pollen", "Fruit"]},
+ {"id": "hb-metab-pp-2a", "variant": "A", "lo_index": 1, "type": "single_select", "question": "How much of their body weight in nectar do they consume daily?", "correct_answer": "About half", "all_answers": ["10%", "25%", "About half", "Twice their weight"]},
+ {"id": "hb-metab-pp-2b", "variant": "B", "lo_index": 1, "type": "single_select", "question": "What physical adaptation helps extract nectar?", "correct_answer": "Specialized bill and extensible tongue", "all_answers": ["Specialized bill and extensible tongue", "Large crop", "Strong beak for cracking", "Webbed feet"]},
+ {"id": "hb-metab-pp-3a", "variant": "A", "lo_index": 2, "type": "single_select", "question": "Why do hummingbirds have minimal crop storage?", "correct_answer": "To minimize weight and speed digestion", "all_answers": ["To minimize weight and speed digestion", "To store more food", "To improve flight acoustics", "To attract mates"]},
+ {"id": "hb-metab-pp-3b", "variant": "B", "lo_index": 2, "type": "single_select", "question": "Where does most sugar absorption occur?", "correct_answer": "Small intestine", "all_answers": ["Crop", "Stomach", "Small intestine", "Liver"]},
+ {"id": "hb-metab-pp-4a", "variant": "A", "lo_index": 3, "type": "single_select", "question": "What enzyme is key to sugar processing?", "correct_answer": "Sucrase", "all_answers": ["Sucrase", "Lactase", "Maltase", "Pepsin"]},
+ {"id": "hb-metab-pp-4b", "variant": "B", "lo_index": 3, "type": "single_select", "question": "How quickly is sugar absorbed after feeding?", "correct_answer": "15-20 minutes", "all_answers": ["1-2 minutes", "15-20 minutes", "1-2 hours", "6-8 hours"]},
+ {"id": "hb-metab-pp-5a", "variant": "A", "lo_index": 4, "type": "single_select", "question": "Why do hummingbirds enter torpor?", "correct_answer": "To survive overnight without feeding", "all_answers": ["To survive overnight without feeding", "To fly faster", "To attract mates", "To digest food"]},
+ {"id": "hb-metab-pp-5b", "variant": "B", "lo_index": 4, "type": "single_select", "question": "What happens to body temperature during torpor?", "correct_answer": "Drops dramatically (can reach 18°C)", "all_answers": ["Drops dramatically (can reach 18°C)", "Increases", "Stays the same", "Fluctuates rapidly"]},
+ {"id": "hb-metab-pp-6a", "variant": "A", "lo_index": 5, "type": "single_select", "question": "What is the heart rate during active flight?", "correct_answer": "Up to 1,200 beats per minute", "all_answers": ["100 beats per minute", "400 beats per minute", "Up to 1,200 beats per minute", "2,000+ beats per minute"]},
+ {"id": "hb-metab-pp-6b", "variant": "B", "lo_index": 5, "type": "single_select", "question": "How long does it take to emerge from torpor?", "correct_answer": "20-60 minutes of shivering", "all_answers": ["Instantly", "5-10 minutes", "20-60 minutes of shivering", "Several hours"]}
+ ]
+ },
+ {
+ "source_id": "hb-flight-mechanics",
+ "title": "Flight Mechanics",
+ "description": "Discover how hummingbirds achieve their remarkable flight abilities",
+ "learning_objectives": [
+ "Identify the unique aspects of hummingbird wing anatomy",
+ "Explain how wing structure enables hovering",
+ "Describe the mechanics of hovering flight",
+ "Explain how hummingbirds generate lift on both strokes",
+ "Describe the aerial maneuvers hummingbirds can perform",
+ "Explain the physics of backward and sideways flight"
+ ],
+ "lessons": [
+ {
+ "source_id": "hb-wing-anatomy",
+ "title": "Wing Anatomy",
+ "learning_objective_indices": [0, 1],
+ "content": "
Hummingbird wings are unlike those of any other bird. While most birds have wings optimized for soaring, gliding, or powerful flapping flight, hummingbird wings are designed for something far more demanding: sustained hovering in still air.
Structural Differences
Several key features distinguish hummingbird wings:
Proportions: Hummingbird wings are proportionally shorter than those of other birds, with a longer hand section (primaries) relative to the arm section (secondaries). The primaries can make up 80% of wing length.
Rigidity: The wing is more rigid than typical bird wings, functioning more like an insect wing or helicopter blade than a flexible bird wing.
Bone structure: The arm bones are shorter and more robust, while the wrist and hand bones are elongated. This creates a wing that pivots primarily at the shoulder.
Did You Know? Hummingbird wings have evolved to be more similar to insect wings than to other bird wings. In fact, their flight mechanics were so poorly understood that scientists had to use high-speed cameras filming at 1,000+ frames per second to finally figure out how hovering works!
The Ball-and-Socket Shoulder
The most crucial adaptation is the shoulder joint. Unlike other birds with more limited shoulder mobility, hummingbirds have a true ball-and-socket joint that allows:
180-degree rotation of the wing
The ability to flip the wing completely over between upstroke and downstroke
Independent control of each wing
Species Spotlight: Violet Sabrewing The Violet Sabrewing (Campylopterus hemileucurus) of Central America is one of the largest hummingbirds outside South America, with a wingspan of 7 inches. Despite its larger size, its wing structure maintains the same 80% primary ratio, allowing it to hover just as effectively as its smaller relatives - it just beats its wings more slowly (about 15 times per second vs. 80+ for tiny species).
Feather Arrangement
The flight feathers are arranged to create an efficient airfoil that works both right-side-up and upside-down. This symmetrical design is essential for generating lift on both strokes of the wing beat cycle.
Human Comparison: If our arms worked like hummingbird wings, we could rotate them 180 degrees at the shoulder - turning our palms from facing the ceiling to facing the floor by rotating at the shoulder alone, not the wrist. Try it - you'll find humans can only rotate about 90 degrees before having to use the forearm!
",
+ "exercise_questions": [
+ {"type": "single_select", "id": "hb-wing-q1", "question": "A drone engineer wants to build a micro-UAV that can hover like a hummingbird. Based on hummingbird wing design, what characteristics should the drone's 'wings' have?", "correct_answer": "Short and rigid, like helicopter blades", "all_answers": ["Short and rigid, like helicopter blades", "Long and flexible for soaring", "Identical to standard airplane wings", "No wings needed - use propellers only"]},
+ {"type": "single_select", "id": "hb-wing-q2", "question": "A robotics team is designing an articulated arm for a hovering robot. What type of joint would best mimic hummingbird shoulder function?", "correct_answer": "Ball-and-socket joint allowing 180° rotation", "all_answers": ["Hinge joint like a door", "Ball-and-socket joint allowing 180° rotation", "Pivot joint like turning a key", "Fixed joint for stability"]},
+ {"type": "single_select", "id": "hb-wing-q3", "question": "An ornithologist measures a hummingbird wing that is 5cm long. Based on typical hummingbird proportions, approximately how long is the 'hand' section (primaries)?", "correct_answer": "About 4cm (80% of wing length)", "all_answers": ["About 1.5cm (30%)", "About 2.5cm (50%)", "About 4cm (80% of wing length)", "About 4.75cm (95%)"]}
+ ]
+ },
+ {
+ "source_id": "hb-hovering-flight",
+ "title": "Hovering Flight",
+ "learning_objective_indices": [2, 3],
+ "content": "
Hummingbirds are the only birds capable of true sustained hovering in still air. While other birds like kingfishers and kestrels can hover briefly, they require wind assistance. Hummingbirds achieve this feat through a completely unique wing motion.
The Figure-8 Pattern
During hovering, hummingbird wings trace a figure-8 pattern (or more precisely, a horizontal oval) rather than flapping up and down:
Wings sweep forward and backward in nearly horizontal strokes
At the end of each stroke, the wing rotates 180 degrees
This keeps the leading edge of the wing forward throughout the cycle
Did You Know? This wing pattern wasn't fully understood until 2005 when researchers at UC Berkeley used high-speed cameras and robot models to finally crack the code. Before then, scientists couldn't explain how hummingbirds generated enough lift to hover - the physics just didn't seem to work with traditional bird flight models!
Lift on Both Strokes
The genius of this system is that it generates lift during both the forward and backward strokes:
Forward stroke: Wing moves forward with leading edge first, generating downward air pressure and upward lift
Backward stroke: Wing flips over and moves backward, again with leading edge first, generating similar lift
Balance: About 50% of lift comes from each stroke (compared to 70-80% from the downstroke in other birds)
Species Spotlight: Amethyst Woodstar The tiny Amethyst Woodstar (Calliphlox amethystina) of South America weighs only 2.5 grams and has one of the fastest wing beats - up to 80 times per second during hovering! At that speed, each complete figure-8 cycle takes just 0.0125 seconds. The wing tip travels at about 30 mph despite the tiny wingspan.
Speed and Control
Wing beat frequency varies with species size and activity:
Larger species: 12-15 beats per second
Smaller species: 70-80 beats per second
During courtship dives: Up to 200 beats per second
Each wing can be controlled independently, allowing for instantaneous adjustments in position and attitude.
Human Comparison: Imagine waving your arms back and forth 80 times per second while also rotating your hands 180 degrees at each end. That's 160 hand rotations per second! Even Olympic swimmers, who have among the fastest arm movements of any athletes, only complete about 1.5 arm cycles per second.
",
+ "exercise_questions": [
+ {"type": "single_select", "id": "hb-hover-q1", "question": "A slow-motion video of a hovering hummingbird shows wing tip positions. When connected, what shape would these positions form?", "correct_answer": "A figure-8 or horizontal oval pattern", "all_answers": ["A simple circle", "A figure-8 or horizontal oval pattern", "A triangle", "A straight vertical line (up and down)"]},
+ {"type": "multiple_select", "id": "hb-hover-q2", "question": "A physicist analyzing hummingbird flight measures lift force during each wing stroke phase. During which phases should significant lift be detected?", "correct_answers": ["During the forward stroke", "During the backward stroke"], "all_answers": ["During the forward stroke", "During the backward stroke", "Neither phase produces lift", "Only when changing direction"]},
+ {"type": "single_select", "id": "hb-hover-q3", "question": "An animator creating a realistic hummingbird hovering animation asks: what must happen to the wing at the end of each forward and backward stroke?", "correct_answer": "The wing must rotate 180 degrees to keep the leading edge forward", "all_answers": ["The wing should stop completely before reversing", "The wing must rotate 180 degrees to keep the leading edge forward", "The wing should fold against the body momentarily", "Nothing special - it just reverses direction"]}
+ ]
+ },
+ {
+ "source_id": "hb-aerial-maneuvers",
+ "title": "Aerial Maneuvers",
+ "learning_objective_indices": [4, 5],
+ "content": "
Beyond hovering, hummingbirds can perform aerial maneuvers that no other bird can match. Their flight abilities have been described as 'helicopter-like,' but they actually exceed helicopter capabilities in several ways.
Directional Flight
Hummingbirds can fly in any direction:
Forward: Normal flight at speeds up to 30-35 mph
Backward: The only birds capable of sustained backward flight
Sideways: Easy lateral movement in any direction
Upside down: Brief inverted flight, typically during escape maneuvers
Did You Know? Hummingbirds can perform an instant 180-degree turn in less than 0.01 seconds - faster than the blink of a human eye! This escape reflex is so fast that predators like praying mantises and dragonflies, which also have extremely fast reflexes, still usually miss when striking at hummingbirds.
How Backward Flight Works
Backward flight is achieved by tilting the wing stroke plane backward rather than forward. The wing still traces its figure-8 pattern, but the orientation directs thrust backward (pushing the bird in reverse). The tail helps with stability during this unusual maneuver.
Species Spotlight: Costa's Hummingbird Male Costa's Hummingbirds (Calypte costae) of the Sonoran Desert perform one of the most extreme courtship dives. They climb to over 100 feet, then plummet at 60+ mph, pulling up with forces of 9g - more than fighter pilots experience! The dive produces a high-pitched whistle from their tail feathers, tuned to frequencies that female Costa's find most attractive.
Courtship Dives
Male hummingbirds perform spectacular dive displays:
Climb to 60-130 feet in height
Power dive reaching speeds of 60+ mph
Pull up with forces exceeding 9g (more than fighter pilots experience)
Produce sounds with tail feathers during the dive
Escape Maneuvers
When threatened, hummingbirds can execute instant direction changes that would destroy a mechanical aircraft. They can accelerate from zero to 30 mph in less than a meter, or reverse direction almost instantaneously. Their reaction time is among the fastest in the animal kingdom.
Human Comparison: Fighter pilots wear special G-suits and train extensively to handle 9g forces for a few seconds - and they still sometimes black out. At 9g, a 150-pound person feels like they weigh 1,350 pounds. A hummingbird experiences these forces during every courtship dive, multiple times per day, with no ill effects!
",
+ "exercise_questions": [
+ {"type": "multiple_select", "id": "hb-maneuver-q1", "question": "A drone engineer wants to match hummingbird maneuverability. Which flight directions must the drone achieve to equal hummingbird capability?", "correct_answers": ["Forward flight", "Backward flight", "Sideways/lateral flight"], "all_answers": ["Forward flight", "Backward flight", "Sideways/lateral flight", "Forward only (like most drones)"]},
+ {"type": "single_select", "id": "hb-maneuver-q2", "question": "A sports scientist studying extreme accelerations compares a hummingbird courtship dive to human activities. At what speed does the hummingbird dive?", "correct_answer": "60+ mph, faster than most highway speed limits", "all_answers": ["20 mph, like a slow bicycle", "40 mph, like urban driving", "60+ mph, faster than most highway speed limits", "100 mph, like a race car"]},
+ {"type": "single_select", "id": "hb-maneuver-q3", "question": "A videographer films a hummingbird flying backward away from a flower. What is the bird doing to achieve this unique maneuver?", "correct_answer": "Tilting the wing stroke plane backward to redirect thrust", "all_answers": ["Flying in tight circles", "Using only the tail as a propeller", "Tilting the wing stroke plane backward to redirect thrust", "This was video editing - birds can't fly backward"]}
+ ]
+ }
+ ],
+ "prepost_questions": [
+ {"id": "hb-flight-pp-1a", "variant": "A", "lo_index": 0, "type": "single_select", "question": "Which feathers make up most of the wing length?", "correct_answer": "Primaries", "all_answers": ["Primaries", "Secondaries", "Coverts", "Down feathers"]},
+ {"id": "hb-flight-pp-1b", "variant": "B", "lo_index": 0, "type": "single_select", "question": "Hummingbird wings function most similarly to what?", "correct_answer": "Insect wings or helicopter blades", "all_answers": ["Eagle wings", "Insect wings or helicopter blades", "Bat wings", "Fish fins"]},
+ {"id": "hb-flight-pp-2a", "variant": "A", "lo_index": 1, "type": "single_select", "question": "How many degrees can hummingbird wings rotate?", "correct_answer": "180 degrees", "all_answers": ["45 degrees", "90 degrees", "180 degrees", "360 degrees"]},
+ {"id": "hb-flight-pp-2b", "variant": "B", "lo_index": 1, "type": "single_select", "question": "What enables the extreme wing rotation?", "correct_answer": "Ball-and-socket shoulder joint", "all_answers": ["Ball-and-socket shoulder joint", "Extra wing bones", "Longer feathers", "Flexible spine"]},
+ {"id": "hb-flight-pp-3a", "variant": "A", "lo_index": 2, "type": "single_select", "question": "What pattern do wings trace during hovering?", "correct_answer": "Figure-8 (horizontal oval)", "all_answers": ["Circle", "Figure-8 (horizontal oval)", "Up and down", "Random"]},
+ {"id": "hb-flight-pp-3b", "variant": "B", "lo_index": 2, "type": "single_select", "question": "Can any other bird truly hover in still air?", "correct_answer": "No, only hummingbirds", "all_answers": ["No, only hummingbirds", "Yes, eagles", "Yes, all birds", "Yes, kingfishers without wind"]},
+ {"id": "hb-flight-pp-4a", "variant": "A", "lo_index": 3, "type": "single_select", "question": "On how many strokes do hummingbirds generate lift?", "correct_answer": "Both forward and backward strokes", "all_answers": ["Downstroke only", "Upstroke only", "Both forward and backward strokes", "Neither"]},
+ {"id": "hb-flight-pp-4b", "variant": "B", "lo_index": 3, "type": "single_select", "question": "How does lift compare between strokes?", "correct_answer": "About 50% from each", "all_answers": ["Downstroke is much stronger", "Upstroke is much stronger", "About 50% from each", "No lift on either"]},
+ {"id": "hb-flight-pp-5a", "variant": "A", "lo_index": 4, "type": "single_select", "question": "Can hummingbirds fly upside down?", "correct_answer": "Yes, briefly for escape maneuvers", "all_answers": ["No", "Yes, briefly for escape maneuvers", "Yes, for long periods", "Only during sleep"]},
+ {"id": "hb-flight-pp-5b", "variant": "B", "lo_index": 4, "type": "single_select", "question": "What is the forward flight top speed?", "correct_answer": "30-35 mph", "all_answers": ["10 mph", "20 mph", "30-35 mph", "60+ mph"]},
+ {"id": "hb-flight-pp-6a", "variant": "A", "lo_index": 5, "type": "single_select", "question": "How do hummingbirds achieve backward flight?", "correct_answer": "Tilting the wing stroke plane backward", "all_answers": ["Tilting the wing stroke plane backward", "Flying in circles", "Using tail as rudder only", "They cannot fly backward"]},
+ {"id": "hb-flight-pp-6b", "variant": "B", "lo_index": 5, "type": "single_select", "question": "What G-forces do hummingbirds experience in courtship dives?", "correct_answer": "Over 9g", "all_answers": ["1-2g", "3-4g", "Over 9g", "No G-forces"]}
+ ]
+ }
+ ]
+ },
+ {
+ "source_id": "intro-computing",
+ "title": "Introductory Computing",
+ "description": "Learn fundamental programming concepts",
+ "units": [
+ {
+ "source_id": "cs-data-variables",
+ "title": "Data & Variables",
+ "description": "Understand how computers store and manipulate information",
+ "learning_objectives": [
+ "Define what data means in computing",
+ "Identify different types of data",
+ "Explain the purpose of variables",
+ "Describe how to declare and assign variables",
+ "List common data operations",
+ "Apply arithmetic operations to numeric data"
+ ],
+ "lessons": [
+ {
+ "source_id": "cs-what-is-data",
+ "title": "What is Data?",
+ "learning_objective_indices": [0, 1],
+ "content": "
At its core, computing is about processing data - transforming information from one form into another, analyzing it, storing it, and presenting it in useful ways. Before we can write programs, we need to understand what data actually is.
Data: Information for Computers
Data is any information that a computer can store and process. This incredibly broad definition encompasses:
Numbers (your age, a price, a distance)
Text (a name, a message, a book)
Images (photographs, drawings, icons)
Audio (music, voice recordings, sound effects)
Video (movies, animations, video calls)
And much more...
Did You Know? A single 4K video frame contains about 25 million pixels, each with color data. At 60 frames per second, that's 1.5 billion pieces of data every second! Modern computers process this in real-time while you watch YouTube.
Binary: The Language of Computers
While humans think in decimal (base 10) and communicate with complex languages, computers fundamentally work in binary - just 1s and 0s. Every piece of data, no matter how complex, is ultimately represented as sequences of these binary digits (bits).
However, programming would be impossibly tedious if we had to work directly in binary. Instead, programming languages provide higher-level abstractions called data types.
Real-World Application: Video Games When you play a video game, the computer tracks thousands of variables: your position (floats like 123.45, 67.89), your health (integer like 85), your character name (string like \"DragonSlayer99\"), and whether you're jumping (boolean True/False). Every frame, these get updated 60+ times per second!
Common Data Types
Programming languages categorize data into types, each with its own properties and operations:
Integers: Whole numbers like 42, -17, or 0
Floating-point numbers: Decimal numbers like 3.14, -0.001, or 2.0
Strings: Text enclosed in quotes, like \"Hello\" or 'World'
Booleans: True/False values used for logic and decisions
Understanding data types is essential because operations that work on one type might not work on another - you can add numbers, but what does it mean to divide text?
",
+ "exercise_questions": [
+ {"type": "single_select", "id": "cs-data-q1", "question": "A computer scientist explains that your MP3 files, photos, and documents are all stored the same way at the hardware level. What format is that?", "correct_answer": "Binary - sequences of 1s and 0s", "all_answers": ["Binary - sequences of 1s and 0s", "Alphabetic letters", "RGB colors", "Analog sound waves"]},
+ {"type": "multiple_select", "id": "cs-data-q2", "question": "You're building a student database. Which programming data types would you need to store: name, age, GPA, and graduation status?", "correct_answers": ["String for the name", "Integer for the age", "Boolean for graduation status"], "all_answers": ["String for the name", "Integer for the age", "Boolean for graduation status", "Emotion for happiness level"]},
+ {"type": "single_select", "id": "cs-data-q3", "question": "A login system needs to track whether a user is currently logged in. What data type best represents this on/off state?", "correct_answer": "Boolean (True/False)", "all_answers": ["Integer", "String", "Boolean (True/False)", "Float"]}
+ ]
+ },
+ {
+ "source_id": "cs-variables-types",
+ "title": "Variables & Types",
+ "learning_objective_indices": [2, 3],
+ "content": "
If data is the information computers work with, variables are the containers that hold that data. Understanding variables is fundamental to programming - they're one of the first concepts every programmer learns.
What is a Variable?
A variable is a named storage location in the computer's memory. Think of it like a labeled box:
The label is the variable's name
The contents are the variable's value
You can look inside (read the value)
You can change what's inside (update the value)
Did You Know? The term \"variable\" comes from mathematics, but programming variables work differently. In math, x = 5 is a statement of equality. In programming, x = 5 is a command: \"store 5 in the box labeled x.\"
Declaring and Assigning Variables
In most programming languages, you create and use variables in two steps:
Declaration: Telling the computer you want a variable with a specific name
Assignment: Putting a value into that variable
In Python, these often happen together:
age = 25 # Creates 'age' and assigns 25\nname = \"Alice\" # Creates 'name' and assigns \"Alice\"\nis_student = True # Creates 'is_student' and assigns True
The equals sign (=) is the assignment operator - it puts the value on the right into the variable on the left.
Real-World Application: Shopping Cart Online stores use variables constantly:
",
+ "exercise_questions": [
+ {"type": "single_select", "id": "cs-var-q1", "question": "A game developer needs to track a player's current health points, which changes as the player takes damage or heals. What programming concept should they use?", "correct_answer": "A variable (named container that can change)", "all_answers": ["A variable (named container that can change)", "A new computer each time", "A mathematical equation", "A different programming language"]},
+ {"type": "single_select", "id": "cs-var-q2", "question": "Looking at this code: `high_score = 5000`, which part identifies WHERE the value is stored?", "correct_answer": "high_score (the variable name)", "all_answers": ["5000 (the number)", "high_score (the variable name)", "integer (the data type)", "= (the equals sign)"]},
+ {"type": "single_select", "id": "cs-var-q3", "question": "A programmer writes `temperature = 72`. Later they write `temperature = 85`. What happened?", "correct_answer": "The old value (72) was replaced with the new value (85)", "all_answers": ["The code tested if temperature equals 72", "The old value (72) was replaced with the new value (85)", "An error occurred - variables can't change", "Two separate variables were created"]}
+ ]
+ },
+ {
+ "source_id": "cs-operations-data",
+ "title": "Operations on Data",
+ "learning_objective_indices": [4, 5],
+ "content": "
Data becomes useful when we operate on it - when we calculate, compare, combine, and transform. Programming languages provide operators and functions for these operations.
Arithmetic Operations
The basic mathematical operations work as you'd expect:
Addition (+):5 + 3 equals 8
Subtraction (-):10 - 4 equals 6
Multiplication (*):4 * 7 equals 28
Division (/):15 / 3 equals 5.0
Integer division (//):17 // 5 equals 3
Modulo (%):17 % 5 equals 2 (remainder)
Exponentiation (**):2 ** 3 equals 8
Did You Know? The modulo operator (%) is secretly one of the most useful operators! It's used to check if numbers are even/odd, to make things wrap around (like clock arithmetic), and to create patterns in games. 17 % 2 equals 1, so 17 is odd!
Order of Operations
Just like in mathematics, operations follow a specific order (PEMDAS):
Parentheses
Exponents
Multiplication and Division (left to right)
Addition and Subtraction (left to right)
So 10 + 5 * 2 equals 20 (not 30), because multiplication happens first.
",
+ "exercise_questions": [
+ {"type": "multiple_select", "id": "cs-ops-q1", "question": "You're writing a calculator app. Which Python operators would you need to implement basic math functions?", "correct_answers": ["+ for addition", "- for subtraction", "* for multiplication", "/ for division"], "all_answers": ["+ for addition", "- for subtraction", "* for multiplication", "/ for division"]},
+ {"type": "single_select", "id": "cs-ops-q2", "question": "A social media app creates usernames by combining first and last name: first_name + \"_\" + last_name. What is this string operation called?", "correct_answer": "Concatenation (joining strings)", "all_answers": ["Concatenation (joining strings)", "Deletion", "Comparison", "Sorting"]},
+ {"type": "single_select", "id": "cs-ops-q3", "question": "A shopping cart calculates: base_price + shipping * item_count where base_price=10, shipping=5, item_count=2. What's the total due to order of operations?", "correct_answer": "20 (shipping * item_count happens first)", "all_answers": ["15", "20 (shipping * item_count happens first)", "30 (everything happens left to right)", "25"]}
+ ]
+ }
+ ],
+ "prepost_questions": [
+ {"id": "cs-data-pp-1a", "variant": "A", "lo_index": 0, "type": "single_select", "question": "What is data in computing?", "correct_answer": "Information that computers can process", "all_answers": ["Information that computers can process", "Only numbers", "Only text", "Computer hardware"]},
+ {"id": "cs-data-pp-1b", "variant": "B", "lo_index": 0, "type": "single_select", "question": "What does a computer process?", "correct_answer": "Data", "all_answers": ["Data", "Electricity only", "Nothing", "Physical objects"]},
+ {"id": "cs-data-pp-2a", "variant": "A", "lo_index": 1, "type": "single_select", "question": "Which is a numeric data type?", "correct_answer": "Integer", "all_answers": ["Integer", "Boolean", "String", "List"]},
+ {"id": "cs-data-pp-2b", "variant": "B", "lo_index": 1, "type": "single_select", "question": "What data type represents True/False?", "correct_answer": "Boolean", "all_answers": ["Integer", "String", "Boolean", "Float"]},
+ {"id": "cs-data-pp-3a", "variant": "A", "lo_index": 2, "type": "single_select", "question": "Why do we use variables?", "correct_answer": "To store and reuse values", "all_answers": ["To store and reuse values", "To make code slower", "To confuse programmers", "Variables are not useful"]},
+ {"id": "cs-data-pp-3b", "variant": "B", "lo_index": 2, "type": "single_select", "question": "What can a variable hold?", "correct_answer": "A value of some data type", "all_answers": ["A value of some data type", "Another computer", "Hardware components", "Nothing useful"]},
+ {"id": "cs-data-pp-4a", "variant": "A", "lo_index": 3, "type": "single_select", "question": "What does 'x = 5' do?", "correct_answer": "Assigns 5 to variable x", "all_answers": ["Assigns 5 to variable x", "Tests if x equals 5", "Deletes x", "Prints 5"]},
+ {"id": "cs-data-pp-4b", "variant": "B", "lo_index": 3, "type": "single_select", "question": "In 'name = \"Alice\"', what is \"Alice\"?", "correct_answer": "The value being assigned", "all_answers": ["The variable name", "The value being assigned", "An operator", "A function"]},
+ {"id": "cs-data-pp-5a", "variant": "A", "lo_index": 4, "type": "single_select", "question": "What symbol represents multiplication?", "correct_answer": "*", "all_answers": ["+", "-", "*", "/"]},
+ {"id": "cs-data-pp-5b", "variant": "B", "lo_index": 4, "type": "single_select", "question": "What does the % operator return?", "correct_answer": "The remainder of division", "all_answers": ["A percentage", "The remainder of division", "A ratio", "An error"]},
+ {"id": "cs-data-pp-6a", "variant": "A", "lo_index": 5, "type": "single_select", "question": "What is 10 + 5 * 2?", "correct_answer": "20", "all_answers": ["15", "20", "30", "25"]},
+ {"id": "cs-data-pp-6b", "variant": "B", "lo_index": 5, "type": "single_select", "question": "What is (10 + 5) * 2?", "correct_answer": "30", "all_answers": ["20", "25", "30", "35"]}
+ ]
+ },
+ {
+ "source_id": "cs-control-flow",
+ "title": "Control Flow",
+ "description": "Learn how to control program execution",
+ "learning_objectives": [
+ "Explain what conditional statements do",
+ "Write simple if/else statements",
+ "Describe the purpose of loops",
+ "Differentiate between for and while loops",
+ "Combine conditionals and loops",
+ "Trace execution flow through nested structures"
+ ],
+ "lessons": [
+ {
+ "source_id": "cs-conditionals",
+ "title": "Conditionals",
+ "learning_objective_indices": [0, 1],
+ "content": "
Real programs need to make decisions. Should we display an error message? Is the user old enough? Does the password match? Conditional statements let programs choose different paths based on conditions.
The if Statement
The fundamental conditional is the if statement. Its basic form:
if condition:\n # code to run if condition is True\n do_something()
The code inside the if block only runs when the condition evaluates to True. If it's False, that code is skipped entirely.
Did You Know? Every time you unlock your phone, dozens of if statements run: Is the fingerprint valid? Is the PIN correct? Is Face ID recognized? Should notifications be shown? Conditionals are the backbone of all interactive software!
Adding else
Often we want to do one thing if a condition is true and something different if it's false:
if temperature > 30:\n print(\"It's hot!\")\nelse:\n print(\"It's not too hot.\")
The else block runs when the if condition is False.
Python checks each condition in order and runs the first matching block.
Boolean Expressions
Conditions are Boolean expressions - they evaluate to True or False:
Comparison: ==, !=, <, >, <=, >=
Logical: and, or, not
Example: if age >= 18 and has_license:
Try It Yourself:
# A simple recommendation engine\nmood = \"happy\"\nweather = \"sunny\"\n\nif mood == \"happy\" and weather == \"sunny\":\n print(\"Perfect day for a picnic!\")\nelif mood == \"tired\":\n print(\"Maybe get some rest?\")\nelse:\n print(\"How about a movie?\")
",
+ "exercise_questions": [
+ {"type": "single_select", "id": "cs-cond-q1", "question": "A login system should show 'Welcome!' only when the password is correct. What programming concept should the developer use?", "correct_answer": "An if statement that checks if password matches", "all_answers": ["An if statement that checks if password matches", "A loop that repeats the welcome message", "A variable called 'welcome'", "A print statement at the start"]},
+ {"type": "single_select", "id": "cs-cond-q2", "question": "A weather app shows 'Bring an umbrella!' if it's raining, otherwise shows 'Enjoy the sun!'. When does the 'Enjoy the sun!' message display?", "correct_answer": "When the 'if raining' condition is False", "all_answers": ["When the 'if raining' condition is True", "When the 'if raining' condition is False", "Always, regardless of weather", "Never - it's broken code"]},
+ {"type": "single_select", "id": "cs-cond-q3", "question": "A grading system needs to assign A, B, C, D, or F based on score. A developer uses 'if' for A, then multiple '____' for B, C, D, and 'else' for F. What goes in the blank?", "correct_answer": "elif (else if)", "all_answers": ["elif (else if)", "elseloop", "element", "errorif"]}
+ ]
+ },
+ {
+ "source_id": "cs-loops",
+ "title": "Loops",
+ "learning_objective_indices": [2, 3],
+ "content": "
Loops are one of programming's most powerful tools. They let us repeat actions without writing the same code over and over. Need to process 1,000 files? A loop handles it as easily as processing one.
The for Loop
Use a for loop when you know how many times to repeat, or when iterating over a collection:
# Repeat 5 times\nfor i in range(5):\n print(i) # Prints 0, 1, 2, 3, 4\n\n# Process each item in a list\nfor name in [\"Alice\", \"Bob\", \"Charlie\"]:\n print(\"Hello, \" + name)
The loop variable (i, name) automatically takes each value in sequence.
Did You Know? When you scroll through Instagram, a for loop is displaying each post. When Spotify plays your playlist, a for loop is going through each song. Loops are everywhere!
The while Loop
Use a while loop when you don't know exactly how many iterations you need - when you want to continue until some condition changes:
for loop: Known number of iterations, iterating over collections
while loop: Unknown iterations, waiting for conditions
Loop Control
Two special statements control loop execution:
break: Exit the loop immediately
continue: Skip to the next iteration
for i in range(10):\n if i == 5:\n break # Stops at 5\n print(i) # Prints 0-4
Try It Yourself:
# Count down from 5\nfor i in range(5, 0, -1):\n print(i)\nprint(\"Blastoff!\")\n# Output: 5, 4, 3, 2, 1, Blastoff!
",
+ "exercise_questions": [
+ {"type": "single_select", "id": "cs-loop-q1", "question": "A music player needs to play each song in a 50-song playlist. Instead of writing 50 separate 'play' commands, what should the developer use?", "correct_answer": "A loop that repeats the play command for each song", "all_answers": ["A loop that repeats the play command for each song", "50 different if statements", "A single variable", "A print statement"]},
+ {"type": "single_select", "id": "cs-loop-q2", "question": "A chat application keeps checking for new messages until the user logs out. What causes this 'while checking' loop to stop?", "correct_answer": "When the logged_in condition becomes False", "all_answers": ["After checking exactly once", "When the logged_in condition becomes False", "It never stops (runs forever)", "After exactly 10 checks"]},
+ {"type": "single_select", "id": "cs-loop-q3", "question": "A search function loops through items but should stop immediately when it finds what it's looking for. What command stops the loop early?", "correct_answer": "break - exits the loop immediately", "all_answers": ["break - exits the loop immediately", "continue - skips to next item", "restart - starts the loop over", "stop - this isn't a real command"]}
+ ]
+ },
+ {
+ "source_id": "cs-combining-structures",
+ "title": "Combining Structures",
+ "learning_objective_indices": [4, 5],
+ "content": "
Real programs combine conditionals and loops to create sophisticated logic. A search algorithm loops through data while checking conditions. A game loop runs continuously, making decisions each frame.
Conditionals Inside Loops
One common pattern is checking conditions within a loop:
numbers = [1, -2, 3, -4, 5]\n\nfor num in numbers:\n if num > 0:\n print(f\"{num} is positive\")\n else:\n print(f\"{num} is negative or zero\")
Each iteration checks the condition independently.
Loops Inside Conditionals
The reverse is also useful - only looping when certain conditions are met:
if user_wants_report:\n for item in data:\n process(item)\n print_report()
Nested Loops
Loops can contain other loops (be careful - this multiplies iterations!):
for row in range(3):\n for col in range(3):\n print(f\"({row},{col})\", end=\" \")\n print() # New line after each row
When debugging complex nested structures, trace through the code step by step:
Keep track of all variable values
Note which branches are taken
Follow loop iterations carefully
Watch for off-by-one errors
This skill - mentally or on paper executing code - is invaluable for understanding and fixing programs.
",
+ "exercise_questions": [
+ {"type": "single_select", "id": "cs-combine-q1", "question": "A shopping cart loops through all items, but only adds a discount if the item is on sale. What programming pattern is this?", "correct_answer": "An if statement inside a loop - very common!", "all_answers": ["An if statement inside a loop - very common!", "This would cause an error", "This only works in certain languages", "You can only check one item per loop"]},
+ {"type": "single_select", "id": "cs-combine-q2", "question": "A calendar app has code that displays a monthly grid: an outer loop for rows (weeks) containing an inner loop for columns (days). What is this called?", "correct_answer": "Nested code - structures inside other structures", "all_answers": ["Nested code - structures inside other structures", "Broken code with errors", "Code that's too short", "Code that won't execute"]},
+ {"type": "single_select", "id": "cs-combine-q3", "question": "A developer finds a bug where a loop runs one too many times. What technique helps them understand exactly what's happening?", "correct_answer": "Tracing - following code step by step with variable values", "all_answers": ["Tracing - following code step by step with variable values", "Making the code run faster", "Adding more comments", "Tracing isn't useful for debugging"]}
+ ]
+ }
+ ],
+ "prepost_questions": [
+ {"id": "cs-flow-pp-1a", "variant": "A", "lo_index": 0, "type": "single_select", "question": "What do conditionals allow programs to do?", "correct_answer": "Make decisions based on conditions", "all_answers": ["Make decisions based on conditions", "Repeat code", "Store data", "Print output"]},
+ {"id": "cs-flow-pp-1b", "variant": "B", "lo_index": 0, "type": "single_select", "question": "What keyword starts a conditional in Python?", "correct_answer": "if", "all_answers": ["for", "while", "if", "def"]},
+ {"id": "cs-flow-pp-2a", "variant": "A", "lo_index": 1, "type": "single_select", "question": "What runs when 'if x > 5' is True?", "correct_answer": "The code in the if block", "all_answers": ["The code in the if block", "The else block", "Both blocks", "Neither block"]},
+ {"id": "cs-flow-pp-2b", "variant": "B", "lo_index": 1, "type": "single_select", "question": "What keyword provides an alternative path?", "correct_answer": "else", "all_answers": ["if", "else", "for", "while"]},
+ {"id": "cs-flow-pp-3a", "variant": "A", "lo_index": 2, "type": "single_select", "question": "Why use loops?", "correct_answer": "To avoid writing repetitive code", "all_answers": ["To avoid writing repetitive code", "To make code slower", "To delete data", "Loops are not useful"]},
+ {"id": "cs-flow-pp-3b", "variant": "B", "lo_index": 2, "type": "single_select", "question": "How many times can a loop execute?", "correct_answer": "As many times as needed", "all_answers": ["Exactly once", "Exactly twice", "As many times as needed", "Maximum 100"]},
+ {"id": "cs-flow-pp-4a", "variant": "A", "lo_index": 3, "type": "single_select", "question": "Which loop is best for a known number of iterations?", "correct_answer": "for loop", "all_answers": ["for loop", "while loop", "if statement", "All are equal"]},
+ {"id": "cs-flow-pp-4b", "variant": "B", "lo_index": 3, "type": "single_select", "question": "Which loop continues until a condition is false?", "correct_answer": "while loop", "all_answers": ["for loop", "while loop", "Both equally", "Neither"]},
+ {"id": "cs-flow-pp-5a", "variant": "A", "lo_index": 4, "type": "single_select", "question": "Can conditionals be placed inside loops?", "correct_answer": "Yes", "all_answers": ["Yes", "No", "Only sometimes", "Only in certain languages"]},
+ {"id": "cs-flow-pp-5b", "variant": "B", "lo_index": 4, "type": "single_select", "question": "Can loops be placed inside conditionals?", "correct_answer": "Yes", "all_answers": ["Yes", "No", "Only sometimes", "Only in certain languages"]},
+ {"id": "cs-flow-pp-6a", "variant": "A", "lo_index": 5, "type": "single_select", "question": "What is tracing execution flow?", "correct_answer": "Following code logic step by step", "all_answers": ["Following code logic step by step", "Drawing diagrams", "Deleting code", "Writing documentation"]},
+ {"id": "cs-flow-pp-6b", "variant": "B", "lo_index": 5, "type": "single_select", "question": "Why is understanding nested structures important?", "correct_answer": "To predict how the program will behave", "all_answers": ["To predict how the program will behave", "To make code look pretty", "It is not important", "To confuse others"]}
+ ]
+ },
+ {
+ "source_id": "cs-functions-modularity",
+ "title": "Functions & Modularity",
+ "description": "Learn to organize code into reusable pieces",
+ "learning_objectives": [
+ "Define what a function is in programming",
+ "Explain the benefits of using functions",
+ "Use parameters to pass data to functions",
+ "Use return values to get data from functions",
+ "Explain code modularity and reuse",
+ "Organize code into logical functions"
+ ],
+ "lessons": [
+ {
+ "source_id": "cs-defining-functions",
+ "title": "Defining Functions",
+ "learning_objective_indices": [0, 1],
+ "content": "
As programs grow larger, organization becomes crucial. Functions are the primary tool for organizing code into manageable, reusable pieces.
What is a Function?
A function is a named block of code that performs a specific task. Once defined, you can run that code by calling the function by name, as many times as needed.
def greet():\n print(\"Hello!\")\n print(\"Welcome to the program.\")\n\n# Call the function\ngreet() # Prints both lines\ngreet() # Prints them again
Did You Know? The average smartphone app contains thousands of functions! Instagram's app has functions for loading images, applying filters, posting comments, sending DMs, and much more. Without functions, the code would be millions of lines of unmaintainable chaos.
Why Use Functions?
Functions provide several critical benefits:
Reusability: Write code once, use it many times
Organization: Break complex problems into smaller, manageable parts
Readability: Well-named functions make code self-documenting
Maintainability: Fix bugs or make improvements in one place
Testing: Test individual functions independently
Real-World Application: Password Validation
def is_password_strong(password):\n has_upper = any(c.isupper() for c in password)\n has_number = any(c.isdigit() for c in password)\n long_enough = len(password) >= 8\n return has_upper and has_number and long_enough\n\n# Reuse everywhere passwords are needed!\nif is_password_strong(new_password):\n save_password()
Function Anatomy
A function definition includes:
def keyword: Tells Python you're defining a function
Name: How you'll call the function later
Parentheses: For parameters (covered next lesson)
Colon: Indicates the start of the function body
Indented body: The code that runs when you call the function
Calling Functions
To run a function's code, call it by name with parentheses:
greet() # Correct - calls the function\ngreet # Wrong - just references the function object
Try It Yourself:
# Define a simple function\ndef celebrate():\n print(\"🎉 Congratulations!\")\n print(\"You learned functions!\")\n\n# Call it three times\ncelebrate()\ncelebrate()\ncelebrate()
",
+ "exercise_questions": [
+ {"type": "single_select", "id": "cs-func-q1", "question": "Maya notices she's writing the same 5 lines of code to display a welcome message in 8 different places in her program. What programming concept should Maya use to avoid this repetition?", "correct_answer": "Create a function with those 5 lines and call it where needed", "all_answers": ["Create a function with those 5 lines and call it where needed", "Copy-paste is fine since it works", "Create 8 different variables", "Delete most of the welcome messages"]},
+ {"type": "multiple_select", "id": "cs-func-q2", "question": "Alex created a function calculate_shipping() that is used in 15 places throughout their e-commerce application. Which scenarios demonstrate the benefits of this approach?", "correct_answers": ["When shipping rates change, Alex only edits one function", "Alex can test shipping calculations independently", "Team members can understand the code more easily"], "all_answers": ["When shipping rates change, Alex only edits one function", "Alex can test shipping calculations independently", "Team members can understand the code more easily", "The program runs twice as slow"]},
+ {"type": "single_select", "id": "cs-func-q3", "question": "Jordan writes: 'function greet(): print(\"Hello\")' but Python gives an error. Looking at the function anatomy diagram, what did Jordan do wrong?", "correct_answer": "Used 'function' instead of 'def' keyword", "all_answers": ["Used 'function' instead of 'def' keyword", "Forgot the print statement", "The parentheses are wrong", "The message is incorrect"]}
+ ]
+ },
+ {
+ "source_id": "cs-parameters-returns",
+ "title": "Parameters & Return Values",
+ "learning_objective_indices": [2, 3],
+ "content": "
Functions become truly powerful when they can receive input and produce output. Parameters let you pass data in; return values let you get data out.
Parameters: Input for Functions
Parameters are variables that receive values when a function is called:
The parameter name takes whatever value is passed in.
Did You Know? Google's search function takes your query as a parameter and searches billions of web pages! The same function handles \"best pizza near me\" and \"quantum physics\" - the parameter just changes what it searches for.
Multiple Parameters
Functions can have multiple parameters, separated by commas:
def add(a, b):\n result = a + b\n print(result)\n\nadd(3, 5) # Prints: 8\nadd(10, 20) # Prints: 30
Real-World Application: Ride-Sharing App
def calculate_fare(distance_miles, surge_multiplier):\n base_fare = 2.50\n per_mile = 1.75\n fare = base_fare + (distance_miles * per_mile)\n return fare * surge_multiplier\n\n# Normal ride: 5 miles, no surge\nprint(calculate_fare(5, 1.0)) # $11.25\n\n# Busy hour: 5 miles, 2x surge\nprint(calculate_fare(5, 2.0)) # $22.50
Return Values: Output from Functions
The return statement sends a value back to the caller:
def add(a, b):\n return a + b\n\nsum = add(3, 5) # sum is now 8\nprint(sum) # Prints: 8\n\n# Can use return value directly\nprint(add(10, 20)) # Prints: 30
Return Ends the Function
When return executes, the function stops immediately:
def check_positive(num):\n if num > 0:\n return \"Positive\"\n return \"Not positive\" # Only runs if num <= 0
Functions Without return
If a function doesn't explicitly return something, it returns None:
def say_hi():\n print(\"Hi\")\n\nresult = say_hi() # result is None
Try It Yourself:
# Function with parameter and return\ndef double(number):\n return number * 2\n\n# Chain the results!\nresult = double(double(double(2)))\nprint(result) # What do you get?
Answer: 2 → 4 → 8 → 16
",
+ "exercise_questions": [
+ {"type": "single_select", "id": "cs-param-q1", "question": "Taylor writes a function to greet users: 'def greet(name): print(\"Hello, \" + name)'. When Taylor calls greet(\"Sam\"), what happens to the value \"Sam\"?", "correct_answer": "It's passed as the 'name' parameter and used inside the function", "all_answers": ["It's passed as the 'name' parameter and used inside the function", "It's printed before the function runs", "It replaces the function name", "It creates a new function called 'Sam'"]},
+ {"type": "single_select", "id": "cs-param-q2", "question": "A ride-sharing app needs a function that calculates the fare based on distance and time. The app needs to display this fare on screen. Should the function use 'print' or 'return' for the calculated amount?", "correct_answer": "Return, so the app can use the value to display it however needed", "all_answers": ["Return, so the app can use the value to display it however needed", "Print, because that's how you output data", "Both print and return together", "Neither - just save it to a file"]},
+ {"type": "single_select", "id": "cs-param-q3", "question": "Casey creates: 'def calculate_area(width, height): return width * height'. What will 'area = calculate_area(4, 5)' store in the variable 'area'?", "correct_answer": "20", "all_answers": ["20", "None", "The function itself", "An error because you can't store return values"]}
+ ]
+ },
+ {
+ "source_id": "cs-code-organization",
+ "title": "Code Organization",
+ "learning_objective_indices": [4, 5],
+ "content": "
Functions are tools for creating modular, organized code. Good organization makes programs easier to understand, modify, and debug.
If you struggle to name a function or find yourself using \"and\" in the name, it probably does too much.
Did You Know? The UNIX operating system was built on this philosophy: each program does one thing well. The 'ls' command just lists files. The 'cat' command just reads files. The 'grep' command just searches text. By combining these simple tools, you can do incredibly complex things!
Choosing Good Names
Function names should clearly describe what they do:
Use verbs: calculate, get, validate, process
Be specific: get_user_age() not get_data()
Follow conventions: lowercase_with_underscores in Python
Real-World Application: Restaurant Kitchen
A well-organized kitchen has stations: one chef grills, one makes sauces, one plates. Each person has one responsibility. Code works the same way!
# Restaurant order processing\ndef process_order(order):\n validate_order(order) # Check order is valid\n prepare_ingredients(order) # Get items ready\n cook_order(order) # Make the food\n plate_order(order) # Arrange presentation\n notify_waiter(order) # Ready to serve!
Breaking Down Complex Tasks
Large tasks should be decomposed into smaller functions:
# Instead of one huge function:\ndef process_order(order):\n # 200 lines of code...\n\n# Break it into manageable pieces:\ndef process_order(order):\n validate_order(order)\n calculate_total(order)\n apply_discounts(order)\n process_payment(order)\n send_confirmation(order)
Code Reuse
Once you have well-designed functions, reuse them:
If you find yourself copying code, consider making it a function instead.
Try It Yourself:
# Refactor this into smaller functions!\ndef bad_greeting(name, age, city):\n print(\"Hello \" + name)\n if age >= 18:\n print(\"You are an adult\")\n else:\n print(\"You are a minor\")\n print(\"You live in \" + city)\n\n# Better: greet(), describe_age_group(), mention_location()
",
+ "exercise_questions": [
+ {"type": "single_select", "id": "cs-org-q1", "question": "Jamie is reviewing code and finds a function named 'do_everything_for_checkout()' that handles cart validation, payment processing, inventory updates, and email confirmation (250 lines total). What advice should Jamie give?", "correct_answer": "Split it into separate functions, each handling one responsibility", "all_answers": ["Split it into separate functions, each handling one responsibility", "It's fine since it all relates to checkout", "Add more comments to explain the 250 lines", "Rename it to 'checkout_master_function()'"]},
+ {"type": "multiple_select", "id": "cs-org-q2", "question": "A team lead asks Riley to improve the codebase's organization. Which of Riley's changes follow best practices?", "correct_answers": ["Renaming 'process()' to 'validate_user_email()'", "Breaking a 150-line function into 5 smaller functions"], "all_answers": ["Renaming 'process()' to 'validate_user_email()'", "Breaking a 150-line function into 5 smaller functions", "Combining 3 small functions into one large function", "Naming all functions 'helper1()', 'helper2()', 'helper3()'"]},
+ {"type": "single_select", "id": "cs-org-q3", "question": "Sam notices the same 10 lines of code for formatting dates appears in 6 different files. What should Sam do to improve the codebase?", "correct_answer": "Create a format_date() function and call it from all 6 locations", "all_answers": ["Create a format_date() function and call it from all 6 locations", "Leave it because it already works", "Add a comment saying 'duplicated code' in each file", "Delete the date formatting since it's repetitive"]}
+ ]
+ }
+ ],
+ "prepost_questions": [
+ {"id": "cs-func-pp-1a", "variant": "A", "lo_index": 0, "type": "single_select", "question": "What keyword defines a function in Python?", "correct_answer": "def", "all_answers": ["def", "function", "fun", "define"]},
+ {"id": "cs-func-pp-1b", "variant": "B", "lo_index": 0, "type": "single_select", "question": "What is calling a function?", "correct_answer": "Running/executing the function's code", "all_answers": ["Running/executing the function's code", "Defining the function", "Deleting the function", "Naming the function"]},
+ {"id": "cs-func-pp-2a", "variant": "A", "lo_index": 1, "type": "single_select", "question": "Why use functions instead of repeating code?", "correct_answer": "Easier maintenance and fewer bugs", "all_answers": ["Easier maintenance and fewer bugs", "Makes code longer", "Makes programs slower", "No real benefit"]},
+ {"id": "cs-func-pp-2b", "variant": "B", "lo_index": 1, "type": "single_select", "question": "What happens when you fix repeated code?", "correct_answer": "Must fix it in every location", "all_answers": ["Must fix it in every location", "Automatically fixes everywhere", "Cannot fix it", "It fixes itself"]},
+ {"id": "cs-func-pp-3a", "variant": "A", "lo_index": 2, "type": "single_select", "question": "In 'def greet(name)', what is 'name'?", "correct_answer": "A parameter", "all_answers": ["A parameter", "A return value", "The function name", "A keyword"]},
+ {"id": "cs-func-pp-3b", "variant": "B", "lo_index": 2, "type": "single_select", "question": "Can functions have multiple parameters?", "correct_answer": "Yes", "all_answers": ["Yes", "No", "Only two", "Only in Python"]},
+ {"id": "cs-func-pp-4a", "variant": "A", "lo_index": 3, "type": "single_select", "question": "What does 'return 42' do?", "correct_answer": "Sends 42 back to the caller", "all_answers": ["Sends 42 back to the caller", "Prints 42", "Creates variable 42", "Does nothing"]},
+ {"id": "cs-func-pp-4b", "variant": "B", "lo_index": 3, "type": "single_select", "question": "What happens after a return statement?", "correct_answer": "The function stops executing", "all_answers": ["The function stops executing", "The function continues", "The whole program ends", "Nothing special"]},
+ {"id": "cs-func-pp-5a", "variant": "A", "lo_index": 4, "type": "single_select", "question": "What is modularity?", "correct_answer": "Breaking code into independent pieces", "all_answers": ["Breaking code into independent pieces", "Making code longer", "Adding more bugs", "Writing without functions"]},
+ {"id": "cs-func-pp-5b", "variant": "B", "lo_index": 4, "type": "single_select", "question": "Why is code reuse valuable?", "correct_answer": "Saves time and reduces errors", "all_answers": ["Saves time and reduces errors", "Makes code longer", "Adds complexity", "No value"]},
+ {"id": "cs-func-pp-6a", "variant": "A", "lo_index": 5, "type": "single_select", "question": "Should function names describe what they do?", "correct_answer": "Yes, always", "all_answers": ["Yes, always", "No", "Only sometimes", "Names don't matter"]},
+ {"id": "cs-func-pp-6b", "variant": "B", "lo_index": 5, "type": "single_select", "question": "What should you do with a complex task?", "correct_answer": "Break it into smaller functions", "all_answers": ["Break it into smaller functions", "Write one huge function", "Avoid coding it", "Give up"]}
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/examples/curriculum_courses/sushichef.py b/examples/curriculum_courses/sushichef.py
new file mode 100644
index 00000000..efb7d5f5
--- /dev/null
+++ b/examples/curriculum_courses/sushichef.py
@@ -0,0 +1,211 @@
+#!/usr/bin/env python
+"""
+Example sushichef demonstrating curriculum structure nodes (CourseNode, UnitNode, LessonNode).
+
+This script loads course content from content.json and creates a channel with:
+- Two courses: Hummingbird Biology and Introductory Computing
+- Each course has 3 units with 3 lessons per unit
+- Each lesson has a KPUB document and an exercise
+- Each unit has pre/post test questions with learning objectives
+
+Run with:
+ python sushichef.py --token=YOUR_TOKEN_HERE
+"""
+import json
+import os
+import tempfile
+import zipfile
+
+from ricecooker.chefs import SushiChef
+from ricecooker.classes.curriculum import LearningObjective
+from ricecooker.classes.licenses import get_license
+from ricecooker.classes.nodes import CourseNode
+from ricecooker.classes.nodes import DocumentNode
+from ricecooker.classes.nodes import ExerciseNode
+from ricecooker.classes.nodes import LessonNode
+from ricecooker.classes.nodes import UnitNode
+from ricecooker.classes.questions import MultipleSelectQuestion
+from ricecooker.classes.questions import SingleSelectQuestion
+from ricecooker.classes.questions import VARIANT_A
+from ricecooker.classes.questions import VARIANT_B
+
+
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+CONTENT_FILE = os.path.join(SCRIPT_DIR, "content.json")
+
+
+def load_content():
+ """Load course content from JSON file."""
+ with open(CONTENT_FILE, "r", encoding="utf-8") as f:
+ return json.load(f)
+
+
+def create_kpub(html_content, output_path):
+ """
+ Create a KPUB file (zip archive with index.html).
+
+ Args:
+ html_content: HTML string for the lesson content
+ output_path: Path where the .kpub file will be created
+ """
+ full_html = f"""
+
+
+
+ Lesson
+
+
+{html_content}
+
+"""
+
+ with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
+ zf.writestr("index.html", full_html.encode("utf-8"))
+
+
+def create_question(question_data):
+ """
+ Create a question object from JSON data.
+
+ Args:
+ question_data: Dict with type, id, question, answers, etc.
+
+ Returns:
+ SingleSelectQuestion or MultipleSelectQuestion instance
+ """
+ if question_data["type"] == "single_select":
+ return SingleSelectQuestion(
+ id=question_data["id"],
+ question=question_data["question"],
+ correct_answer=question_data["correct_answer"],
+ all_answers=question_data["all_answers"],
+ )
+ elif question_data["type"] == "multiple_select":
+ return MultipleSelectQuestion(
+ id=question_data["id"],
+ question=question_data["question"],
+ correct_answers=question_data["correct_answers"],
+ all_answers=question_data["all_answers"],
+ )
+ else:
+ raise ValueError(f"Unknown question type: {question_data['type']}")
+
+
+class CurriculumCoursesChef(SushiChef):
+ """
+ Sushichef demonstrating curriculum structure with CourseNode, UnitNode, LessonNode.
+ """
+
+ channel_info = {
+ "CHANNEL_TITLE": "Curriculum Courses Example",
+ "CHANNEL_SOURCE_DOMAIN": "learningequality.org",
+ "CHANNEL_SOURCE_ID": "curriculum-courses-example",
+ "CHANNEL_LANGUAGE": "en",
+ "CHANNEL_DESCRIPTION": "Example channel demonstrating curriculum structure nodes",
+ }
+
+ def construct_channel(self, **kwargs):
+ """Build channel structure from content.json."""
+ channel = self.get_channel(**kwargs)
+ content = load_content()
+
+ license = get_license("CC BY", copyright_holder="Learning Equality")
+
+ for course_data in content["courses"]:
+ course_node = self._build_course(course_data, license)
+ channel.add_child(course_node)
+
+ return channel
+
+ def _build_course(self, course_data, license):
+ """Build a CourseNode from JSON data."""
+ course = CourseNode(
+ source_id=course_data["source_id"],
+ title=course_data["title"],
+ description=course_data.get("description", ""),
+ )
+
+ for unit_data in course_data["units"]:
+ unit = self._build_unit(unit_data, license)
+ course.add_child(unit)
+
+ return course
+
+ def _build_unit(self, unit_data, license):
+ """Build a UnitNode with lessons and pre/post questions."""
+ unit = UnitNode(
+ source_id=unit_data["source_id"],
+ title=unit_data["title"],
+ description=unit_data.get("description", ""),
+ )
+
+ # Create LearningObjective objects from the text strings
+ learning_objectives = [
+ LearningObjective(text) for text in unit_data["learning_objectives"]
+ ]
+
+ # Add lessons with their associated learning objectives
+ for lesson_data in unit_data["lessons"]:
+ lesson = self._build_lesson(lesson_data, license)
+ # Get the LearningObjective objects for this lesson
+ lesson_los = [
+ learning_objectives[i]
+ for i in lesson_data["learning_objective_indices"]
+ ]
+ unit.add_child(lesson, lesson_los)
+
+ # Add pre/post test questions
+ for q_data in unit_data["prepost_questions"]:
+ question = create_question(q_data)
+ variant = VARIANT_A if q_data["variant"] == "A" else VARIANT_B
+ # Get the LearningObjective for this question
+ question_los = [learning_objectives[q_data["lo_index"]]]
+ unit.add_question(question, variant, question_los)
+
+ return unit
+
+ def _build_lesson(self, lesson_data, license):
+ """Build a LessonNode with content document and exercise."""
+ lesson = LessonNode(
+ source_id=lesson_data["source_id"],
+ title=lesson_data["title"],
+ )
+
+ # Create KPUB file for the lesson content
+ fd, kpub_path = tempfile.mkstemp(suffix=".kpub")
+ os.close(fd)
+ create_kpub(lesson_data["content"], kpub_path)
+
+ # Add document node with KPUB content
+ doc_node = DocumentNode(
+ source_id=f"{lesson_data['source_id']}-doc",
+ title=f"{lesson_data['title']} - Reading",
+ license=license,
+ language="en",
+ uri=kpub_path,
+ )
+ lesson.add_child(doc_node)
+
+ # Add exercise node with practice questions
+ questions = [create_question(q) for q in lesson_data["exercise_questions"]]
+ exercise = ExerciseNode(
+ source_id=f"{lesson_data['source_id']}-exercise",
+ title=f"{lesson_data['title']} - Practice",
+ license=license,
+ language="en",
+ questions=questions,
+ exercise_data={
+ "mastery_model": "m_of_n",
+ "m": min(3, len(questions)),
+ "n": len(questions),
+ "randomize": True,
+ },
+ )
+ lesson.add_child(exercise)
+
+ return lesson
+
+
+if __name__ == "__main__":
+ chef = CurriculumCoursesChef()
+ chef.main()
diff --git a/ricecooker/classes/curriculum.py b/ricecooker/classes/curriculum.py
new file mode 100644
index 00000000..9c59f6e4
--- /dev/null
+++ b/ricecooker/classes/curriculum.py
@@ -0,0 +1,42 @@
+"""
+Curriculum-related classes for structured educational content.
+
+This module provides classes for representing curriculum concepts like
+learning objectives, which can be associated with lessons and assessment
+questions.
+"""
+import uuid
+
+# Fixed namespace for generating deterministic learning objective UUIDs.
+# Same text will always produce the same UUID.
+LEARNING_OBJECTIVE_NAMESPACE = uuid.UUID("b5e3f3e8-9c7a-4d6b-8f2e-1a5c9d8e7f6b")
+
+
+class LearningObjective:
+ """
+ Represents a learning objective that can be associated with lessons and questions.
+
+ The ID is deterministically generated from the text using UUID5, so identical
+ text always produces the same ID. This prevents accidental duplicates.
+
+ Attributes:
+ text (str): Human-readable description of the learning objective.
+ id (str): Deterministic UUID5 generated from the text.
+ metadata (dict): Optional metadata associated with the objective.
+ """
+
+ def __init__(self, text, metadata=None):
+ """Create a new LearningObjective with deterministic UUID5 from text."""
+ if not text or not text.strip():
+ raise ValueError("LearningObjective text must be non-empty")
+ self.text = text
+ self.id = uuid.uuid5(LEARNING_OBJECTIVE_NAMESPACE, text).hex
+ self.metadata = metadata or {}
+
+ def to_dict(self):
+ """Serialize the learning objective for inclusion in channel data."""
+ return {
+ "id": self.id,
+ "text": self.text,
+ "metadata": self.metadata,
+ }
diff --git a/ricecooker/classes/nodes.py b/ricecooker/classes/nodes.py
index adcdda02..fed5ceda 100644
--- a/ricecooker/classes/nodes.py
+++ b/ricecooker/classes/nodes.py
@@ -3,10 +3,13 @@
import re
import uuid
+from le_utils.constants import completion_criteria
from le_utils.constants import content_kinds
from le_utils.constants import exercises
from le_utils.constants import format_presets
from le_utils.constants import languages
+from le_utils.constants import mastery_criteria
+from le_utils.constants import modalities
from le_utils.constants import roles
from le_utils.constants.labels import accessibility_categories
from le_utils.constants.labels import learning_activities
@@ -19,6 +22,7 @@
from .. import config
from ..exceptions import InvalidNodeException
from ..utils.utils import is_valid_uuid_string
+from .curriculum import LearningObjective
from .files import ExtractedEPubThumbnailFile
from .files import ExtractedHTMLZipThumbnailFile
from .files import ExtractedPdfThumbnailFile
@@ -26,6 +30,8 @@
from .files import SubtitleFile
from .files import YouTubeSubtitleFile
from .licenses import License
+from .questions import VARIANT_A
+from .questions import VARIANT_B
from ricecooker.utils.pipeline.exceptions import ExpectedFileException
from ricecooker.utils.pipeline.exceptions import InvalidFileException
@@ -99,8 +105,11 @@ class Node(object):
kind = None
license = None
language = None
- kind = None
valid = False
+ # Nodes with questions (ExerciseNode, UnitNode) set this True.
+ # This gates Node._validate() to allow a non-empty `questions` list,
+ # which TreeNode.to_dict() serializes for the Studio API.
+ _allows_questions = False
def __init__(
self,
@@ -431,9 +440,10 @@ def _validate(self): # noqa: C901
self.kind != self.__class__.kind,
f"{self.__class__.__name__} must have kind {self.__class__.kind}",
)
- if self.kind != content_kinds.EXERCISE:
+ if not self._allows_questions:
self._validate_values(
- bool(self.questions), f"{self.kind} should not have questions"
+ bool(self.questions),
+ f"{self.__class__.__name__} should not have questions",
)
self._validate_values(not isinstance(self.title, str), "Title is not a string")
@@ -838,9 +848,12 @@ def pipeline(self):
return self._pipeline
def __str__(self):
- metadata = "{0} {1}".format(
- len(self.files), "file" if len(self.files) == 1 else "files"
- )
+ if len(self.files) == 0 and self.uri:
+ metadata = "uri: {}".format(self.uri)
+ else:
+ metadata = "{0} {1}".format(
+ len(self.files), "file" if len(self.files) == 1 else "files"
+ )
return "{title} ({kind}): {metadata}".format(
title=self.title, kind=self.__class__.__name__, metadata=metadata
)
@@ -1036,7 +1049,7 @@ class AudioNode(ContentNode):
class DocumentNode(ContentNode):
"""Model representing documents in channel
- Documents must be in PDF or ePub format
+ Documents must be in PDF, ePub, Bloom, or KPUB format
Attributes:
derive_thumbnail (bool): automatically generate thumbnail (optional)
@@ -1049,6 +1062,7 @@ class DocumentNode(ContentNode):
format_presets.DOCUMENT,
format_presets.EPUB,
format_presets.BLOOMPUB,
+ format_presets.KPUB_ZIP,
)
def generate_thumbnail(self):
@@ -1136,6 +1150,7 @@ class ExerciseNode(ContentNode):
"""
kind = content_kinds.EXERCISE
+ _allows_questions = True
def __init__(self, *args, questions=None, exercise_data=None, **kwargs):
self.questions = questions or []
@@ -1482,3 +1497,258 @@ def to_dict(self):
# add alias for back-compatibility
RemoteContentNode = StudioContentNode
+
+
+class _CurriculumNode(TopicNode):
+ """
+ Internal base class for curriculum-structured topic nodes (Course, Unit, Lesson).
+
+ Provides common functionality for setting modality and validating child types.
+ Not intended to be instantiated directly.
+
+ Attributes:
+ MODALITY: The modality constant from le_utils.constants.modalities
+ CHILD_CLASS: The allowed child node class
+ """
+
+ MODALITY = None # Subclasses must define
+ CHILD_CLASS = None # Subclasses must define
+
+ def __init__(self, *args, **kwargs):
+ kwargs["extra_fields"] = kwargs.get("extra_fields", {})
+ kwargs["extra_fields"]["options"] = kwargs["extra_fields"].get("options", {})
+ kwargs["extra_fields"]["options"]["modality"] = self.MODALITY
+ super().__init__(*args, **kwargs)
+
+ def _validate_child(self, node):
+ """Validate that node is an instance of the allowed child class."""
+ if not isinstance(node, self.CHILD_CLASS):
+ raise InvalidNodeException(
+ f"{self.__class__.__name__} can only have {self.CHILD_CLASS.__name__} children"
+ )
+
+ def add_child(self, node):
+ """Add a child node after validating its type."""
+ self._validate_child(node)
+ super().add_child(node)
+
+
+class LessonNode(_CurriculumNode):
+ """
+ Topic node representing a lesson within a unit.
+
+ Lessons can only contain resource nodes (ContentNode subclasses like
+ VideoNode, DocumentNode, etc.), not other topic nodes.
+
+ Attributes:
+ source_id (str): lesson's original id
+ title (str): lesson's title
+ description (str): description of lesson (optional)
+ thumbnail (str): local path or url to thumbnail image (optional)
+ """
+
+ MODALITY = modalities.LESSON
+ CHILD_CLASS = ContentNode
+
+
+class UnitNode(_CurriculumNode):
+ """
+ Topic node representing a unit within a course.
+
+ Units can only contain LessonNodes as children. Units also manage
+ pre/post test questions and learning objectives.
+
+ Attributes:
+ source_id (str): unit's original id
+ title (str): unit's title
+ description (str): description of unit (optional)
+ thumbnail (str): local path or url to thumbnail image (optional)
+ test_questions (list): list of (question, variant, learning_objectives) tuples
+ lesson_objectives (dict): mapping of source_id to list of LearningObjective
+ """
+
+ MODALITY = modalities.UNIT
+ CHILD_CLASS = LessonNode
+ _allows_questions = True
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.test_questions = [] # List of (question, variant, [LOs])
+ self.lesson_objectives = {} # {source_id: [LOs]}
+
+ def _validate_learning_objectives(self, learning_objectives):
+ """Validate that all items are valid LearningObjective instances."""
+ if not learning_objectives:
+ raise InvalidNodeException("Must have at least one learning objective")
+ for lo in learning_objectives:
+ if not isinstance(lo, LearningObjective):
+ raise InvalidNodeException(
+ f"Expected LearningObjective, got {type(lo).__name__}"
+ )
+
+ def add_child(self, node, learning_objectives):
+ """
+ Add a LessonNode with its associated learning objectives.
+
+ Note: this intentionally has a different signature from
+ _CurriculumNode.add_child(node) — learning_objectives is required.
+ This is safe because add_child is only called during channel
+ construction by chef code, never by internal tree-walking code.
+
+ Args:
+ node: LessonNode to add
+ learning_objectives: List of LearningObjective instances
+ """
+ self._validate_learning_objectives(learning_objectives)
+ if node.source_id in self.lesson_objectives:
+ raise InvalidNodeException(
+ f"Duplicate source_id '{node.source_id}' in {self.__class__.__name__}"
+ )
+ self.lesson_objectives[node.source_id] = learning_objectives
+ super().add_child(node)
+
+ def add_question(self, question, variant, learning_objectives):
+ """
+ Add a pre/post test question.
+
+ Args:
+ question: Question instance (same types as ExerciseNode)
+ variant: VARIANT_A or VARIANT_B
+ learning_objectives: List of LearningObjective instances
+ """
+ if variant not in (VARIANT_A, VARIANT_B):
+ raise InvalidNodeException("variant must be VARIANT_A or VARIANT_B")
+ self._validate_learning_objectives(learning_objectives)
+ self.test_questions.append((question, variant, learning_objectives))
+
+ def _validate(self):
+ """Validate the unit including question balance and LO matching."""
+ # Single pass: validate questions and partition by variant
+ variant_a = []
+ variant_b = []
+ for question, variant, los in self.test_questions:
+ self._validate_values(
+ not question.validate(), "UnitNode has invalid question"
+ )
+ if variant == VARIANT_A:
+ variant_a.append((question, los))
+ else:
+ variant_b.append((question, los))
+
+ # Minimum 2 questions per variant
+ self._validate_values(
+ len(variant_a) < 2, "Must have at least 2 VARIANT_A questions"
+ )
+ self._validate_values(
+ len(variant_b) < 2, "Must have at least 2 VARIANT_B questions"
+ )
+
+ # Equal total counts
+ self._validate_values(
+ len(variant_a) != len(variant_b),
+ "VARIANT_A and VARIANT_B must have equal question counts",
+ )
+
+ # LO sets must match
+ lesson_los = {lo.id for los in self.lesson_objectives.values() for lo in los}
+ question_los = {lo.id for _, los in variant_a + variant_b for lo in los}
+ self._validate_values(
+ lesson_los != question_los,
+ "Learning objectives on lessons must match those on questions",
+ )
+
+ # Each LO equally represented across variants and across LOs
+ lo_totals = {}
+ for lo_id in lesson_los:
+ a_count = sum(
+ 1 for _, los in variant_a if any(lo.id == lo_id for lo in los)
+ )
+ b_count = sum(
+ 1 for _, los in variant_b if any(lo.id == lo_id for lo in los)
+ )
+ self._validate_values(
+ a_count != b_count,
+ "Learning objective must have equal questions in each variant",
+ )
+ lo_totals[lo_id] = a_count + b_count
+
+ # Each LO must have the same total number of questions
+ if lo_totals:
+ expected = next(iter(lo_totals.values()))
+ self._validate_values(
+ any(c != expected for c in lo_totals.values()),
+ "Each learning objective must have the same total number of questions",
+ )
+
+ return super()._validate()
+
+ def _get_mastery_criteria(self):
+ """Build mastery criteria dict for pre/post test."""
+ all_ids = [q.assessment_id for q, _, _ in self.test_questions]
+ a_ids = [q.assessment_id for q, v, _ in self.test_questions if v == VARIANT_A]
+ b_ids = [q.assessment_id for q, v, _ in self.test_questions if v == VARIANT_B]
+
+ return {
+ "mastery_model": mastery_criteria.PRE_POST_TEST,
+ "pre_post_test": {
+ "assessment_item_ids": all_ids,
+ "version_a_item_ids": a_ids,
+ "version_b_item_ids": b_ids,
+ },
+ }
+
+ def _get_learning_objectives_data(self):
+ """Build learning objectives structure per le_utils schema."""
+ # Collect unique LOs from lesson_objectives
+ # (validation guarantees match with questions)
+ all_los = {lo.id: lo for los in self.lesson_objectives.values() for lo in los}
+
+ return {
+ "learning_objectives": [lo.to_dict() for lo in all_los.values()],
+ "assessment_objectives": {
+ q.assessment_id: [lo.id for lo in los]
+ for q, _, los in self.test_questions
+ },
+ "lesson_objectives": {
+ child.get_node_id().hex: [
+ lo.id for lo in self.lesson_objectives[child.source_id]
+ ]
+ for child in self.children
+ if child.source_id in self.lesson_objectives
+ },
+ }
+
+ def to_dict(self):
+ """Serialize UnitNode including mastery model and learning objectives in options."""
+ result = super().to_dict()
+
+ # Parse extra_fields, add mastery model and learning objectives to options
+ extra_fields = json.loads(result["extra_fields"])
+ extra_fields["options"]["completion_criteria"] = {
+ "model": completion_criteria.MASTERY,
+ "threshold": self._get_mastery_criteria(),
+ }
+ extra_fields["options"].update(self._get_learning_objectives_data())
+ result["extra_fields"] = json.dumps(extra_fields)
+
+ # Serialize test questions
+ result["questions"] = [q.to_dict() for q, _, _ in self.test_questions]
+
+ return result
+
+
+class CourseNode(_CurriculumNode):
+ """
+ Topic node representing a course.
+
+ Courses can only contain UnitNodes as children.
+
+ Attributes:
+ source_id (str): course's original id
+ title (str): course's title
+ description (str): description of course (optional)
+ thumbnail (str): local path or url to thumbnail image (optional)
+ """
+
+ MODALITY = modalities.COURSE
+ CHILD_CLASS = UnitNode
diff --git a/ricecooker/classes/questions.py b/ricecooker/classes/questions.py
index 4eb1eb8c..b795f931 100644
--- a/ricecooker/classes/questions.py
+++ b/ricecooker/classes/questions.py
@@ -30,6 +30,10 @@
# match protocol:{{path}} either wrapped in parentheses or quotes (original regex)
MARKDOWN_IMAGE_REGEX = r"!\[([^\]]+)?\]\(([^\)]+?)\)" # match 
+# Pre/post test variant constants for UnitNode questions
+VARIANT_A = "A"
+VARIANT_B = "B"
+
class BaseQuestion:
"""Base model representing exercise questions
@@ -69,6 +73,11 @@ def __init__(
self.randomize = randomize
self.id = uuid.uuid5(uuid.NAMESPACE_DNS, id)
+ @property
+ def assessment_id(self):
+ """Return the assessment ID as a hex string."""
+ return self.id.hex
+
def truncate_fields(self):
if self.source_url and len(self.source_url) > config.MAX_SOURCE_URL_LENGTH:
config.print_truncate(
@@ -82,7 +91,7 @@ def to_dict(self):
Returns: dict of node's data
"""
return {
- "assessment_id": self.id.hex,
+ "assessment_id": self.assessment_id,
"type": self.question_type,
"files": [
f.to_dict() for f in filter(lambda x: x and x.filename, self.files)
diff --git a/ricecooker/commands.py b/ricecooker/commands.py
index 4d7b505a..5a2766d3 100644
--- a/ricecooker/commands.py
+++ b/ricecooker/commands.py
@@ -395,11 +395,21 @@ def walk_tree(parents_path, subtree):
def attach(parent, node_path):
if len(node_path) == 1:
# leaf node
- parent.add_child(node_path[0])
+ try:
+ parent.add_child(node_path[0])
+ except TypeError:
+ raise NotImplementedError(
+ "--sample mode is not supported for channels with curriculum structure nodes"
+ )
else:
child = node_path[0]
if not any(c.source_id == child.source_id for c in parent.children):
- parent.add_child(child)
+ try:
+ parent.add_child(child)
+ except TypeError:
+ raise NotImplementedError(
+ "--sample mode is not supported for channels with curriculum structure nodes"
+ )
attach(child, node_path[1:])
for node_path in sample_paths:
diff --git a/ricecooker/utils/pipeline/convert.py b/ricecooker/utils/pipeline/convert.py
index 4c9e209b..2c3c729a 100644
--- a/ricecooker/utils/pipeline/convert.py
+++ b/ricecooker/utils/pipeline/convert.py
@@ -236,6 +236,31 @@ def read_file_from_archive(self, zf, filepath):
f"File {zf.filename} is not a valid {self.FILE_TYPE} file, {filepath} is missing."
)
+ def _validate_index_html_body(self, zf, path):
+ """Validate that index.html exists and has a non-empty body."""
+ index_html = self.read_file_from_archive(zf, "index.html")
+ try:
+ dom = html5lib.parse(index_html, namespaceHTMLElements=False)
+ body = dom.find("body")
+ if body is None:
+ raise InvalidFileException(
+ f"File {path} is not a valid {self.FILE_TYPE} file, index.html is missing a body element."
+ )
+ # Check that the body has at least one child element
+ # for some reason it seems like comments don't get a string tag attribute
+ body_children = [
+ c for c in body.iter() if isinstance(c.tag, str) and c.tag != "body"
+ ]
+ if not (body.text and body.text.strip()) and not body_children:
+ raise InvalidFileException(
+ f"File {path} is not a valid {self.FILE_TYPE} file, index.html is empty."
+ )
+ return dom
+ except ParseError:
+ raise InvalidFileException(
+ f"File {path} is not a valid {self.FILE_TYPE} file, index.html is not well-formed."
+ )
+
def _read_and_compress_archive_file(
self, filepath, reader, audio_settings=None, video_settings=None, ext=None
):
@@ -291,28 +316,7 @@ class HTML5ConversionHandler(ArchiveProcessingBaseHandler):
def validate_archive(self, path: str):
with self.open_and_verify_archive(path) as zf:
- # Check index.html exists and is valid HTML
- index_html = self.read_file_from_archive(zf, "index.html")
- try:
- dom = html5lib.parse(index_html, namespaceHTMLElements=False)
- body = dom.find("body")
- if body is None:
- raise InvalidFileException(
- f"File {path} is not a valid HTML5 file, index.html is missing a body element."
- )
- # Check that the body has at least one child element
- # for some reason it seems like comments don't get a string tag attribute
- body_children = [
- c for c in body.iter() if isinstance(c.tag, str) and c.tag != "body"
- ]
- if not body.text.strip() and not body_children:
- raise InvalidFileException(
- f"File {path} is not a valid HTML5 file, index.html is empty."
- )
- except ParseError:
- raise InvalidFileException(
- f"File {path} is not a valid HTML5 file, index.html is not well-formed."
- )
+ self._validate_index_html_body(zf, path)
class H5PConversionHandler(ArchiveProcessingBaseHandler):
@@ -404,6 +408,34 @@ def validate_archive(self, path: str):
self._validate_opf(zf, path, opf_path)
+class KPUBConversionHandler(ArchiveProcessingBaseHandler):
+
+ EXTENSIONS = {file_formats.HTML5_ARTICLE}
+ FILE_TYPE = "KPUB"
+
+ def validate_archive(self, path: str):
+ with self.open_and_verify_archive(path) as zf:
+ dom = self._validate_index_html_body(zf, path)
+
+ # Check for inline