{
const scripts = Array.from(
document.querySelectorAll("script.fcpython-ojs-quiz-config")
);
const script = scripts.find(
(node) => node.dataset.fcpythonRendered !== "true"
);
if (!script) {
return html`<div class="fcpython-quiz fcpython-quiz-warning">
Quiz configuration was not found.
</div>`;
}
script.dataset.fcpythonRendered = "true";
const quiz = JSON.parse(script.textContent);
const container = html`<div class="fcpython-quiz"></div>`;
const title = document.createElement("h3");
title.textContent = quiz.title;
container.appendChild(title);
const instructions = document.createElement("p");
instructions.textContent = quiz.instructions;
container.appendChild(instructions);
const progress = document.createElement("div");
progress.className = "fcpython-quiz-progress";
const counter = document.createElement("p");
counter.className = "fcpython-quiz-counter";
counter.setAttribute("aria-live", "polite");
progress.appendChild(counter);
const tabs = document.createElement("div");
tabs.className = "fcpython-quiz-steps";
tabs.setAttribute("role", "tablist");
tabs.setAttribute("aria-label", "Quiz questions");
progress.appendChild(tabs);
container.appendChild(progress);
const questions = document.createElement("div");
questions.className = "fcpython-quiz-questions";
const feedbackNodes = [];
const questionPanels = [];
const stepButtons = [];
let currentQuestion = 0;
function setStepStatus(questionIndex, status) {
const step = stepButtons[questionIndex];
const question = quiz.questions[questionIndex];
const statuses = {
answered: "answered",
correct: "correct",
incorrect: "incorrect",
unanswered: "not answered",
};
step.classList.remove(
"is-answered",
"is-correct",
"is-incorrect",
"is-unanswered"
);
if (status) {
step.classList.add(`is-${status}`);
}
const statusLabel = status ? `, ${statuses[status]}` : "";
step.setAttribute(
"aria-label",
`Question ${questionIndex + 1}: ${question.prompt}${statusLabel}`
);
}
function showQuestion(questionIndex, focusPanel = false) {
currentQuestion = Math.max(
0,
Math.min(questionIndex, quiz.questions.length - 1)
);
questionPanels.forEach((panel, index) => {
panel.hidden = index !== currentQuestion;
});
stepButtons.forEach((step, index) => {
const isCurrent = index === currentQuestion;
step.classList.toggle("is-current", isCurrent);
step.setAttribute("aria-selected", String(isCurrent));
step.tabIndex = isCurrent ? 0 : -1;
});
counter.textContent = `Question ${currentQuestion + 1} of ${quiz.questions.length}`;
const selected = questionPanels[currentQuestion].querySelector(
"input[type='radio']:checked"
);
const isLastQuestion = currentQuestion === quiz.questions.length - 1;
previous.hidden = currentQuestion === 0;
next.hidden = isLastQuestion || !selected;
check.hidden = !isLastQuestion;
if (focusPanel) {
questionPanels[currentQuestion].focus();
}
}
quiz.questions.forEach((question, questionIndex) => {
const tabId = `${quiz.id}-question-tab-${questionIndex + 1}`;
const panelId = `${quiz.id}-question-panel-${questionIndex + 1}`;
const step = document.createElement("button");
step.type = "button";
step.className = "fcpython-quiz-step";
step.id = tabId;
step.textContent = String(questionIndex + 1);
step.setAttribute("role", "tab");
step.setAttribute("aria-controls", panelId);
step.setAttribute("aria-selected", "false");
step.tabIndex = -1;
step.addEventListener("click", () => showQuestion(questionIndex));
step.addEventListener("keydown", (event) => {
let destination = null;
if (event.key === "ArrowRight") {
destination = (questionIndex + 1) % quiz.questions.length;
} else if (event.key === "ArrowLeft") {
destination =
(questionIndex - 1 + quiz.questions.length) % quiz.questions.length;
} else if (event.key === "Home") {
destination = 0;
} else if (event.key === "End") {
destination = quiz.questions.length - 1;
}
if (destination !== null) {
event.preventDefault();
showQuestion(destination);
stepButtons[destination].focus();
}
});
stepButtons.push(step);
tabs.appendChild(step);
setStepStatus(questionIndex, "");
const panel = document.createElement("div");
panel.className = "fcpython-quiz-panel";
panel.id = panelId;
panel.setAttribute("role", "tabpanel");
panel.setAttribute("aria-labelledby", tabId);
panel.tabIndex = -1;
const fieldset = document.createElement("fieldset");
fieldset.className = "fcpython-quiz-question";
const legend = document.createElement("legend");
legend.textContent = question.prompt;
fieldset.appendChild(legend);
question.options.forEach((option, optionIndex) => {
const label = document.createElement("label");
label.className = "fcpython-quiz-option";
const input = document.createElement("input");
input.type = "radio";
input.name = `${quiz.id}-${question.id}`;
input.value = String(optionIndex);
input.addEventListener("change", () => {
setStepStatus(questionIndex, "answered");
feedbackNodes[questionIndex].textContent = "";
feedbackNodes[questionIndex].className = "fcpython-quiz-feedback";
score.textContent = "";
showQuestion(questionIndex);
});
const text = document.createElement("span");
text.textContent = option;
label.appendChild(input);
label.appendChild(text);
fieldset.appendChild(label);
});
const feedback = document.createElement("p");
feedback.className = "fcpython-quiz-feedback";
feedback.setAttribute("aria-live", "polite");
feedbackNodes.push(feedback);
fieldset.appendChild(feedback);
panel.appendChild(fieldset);
questionPanels.push(panel);
questions.appendChild(panel);
});
container.appendChild(questions);
const actions = document.createElement("div");
actions.className = "fcpython-quiz-actions";
const previous = document.createElement("button");
previous.type = "button";
previous.className = "fcpython-quiz-secondary";
previous.textContent = "Previous";
previous.addEventListener("click", () => {
showQuestion(currentQuestion - 1, true);
});
const next = document.createElement("button");
next.type = "button";
next.textContent = "Next question";
next.addEventListener("click", () => {
showQuestion(currentQuestion + 1, true);
});
const check = document.createElement("button");
check.type = "button";
check.textContent = "Check answers";
const reset = document.createElement("button");
reset.type = "button";
reset.className = "fcpython-quiz-secondary fcpython-quiz-reset";
reset.textContent = "Reset";
const score = document.createElement("p");
score.className = "fcpython-quiz-score";
score.setAttribute("aria-live", "polite");
check.addEventListener("click", () => {
let correctCount = 0;
let firstQuestionToReview = null;
quiz.questions.forEach((question, questionIndex) => {
const selected = container.querySelector(
`input[name="${quiz.id}-${question.id}"]:checked`
);
const feedback = feedbackNodes[questionIndex];
if (!selected) {
feedback.textContent = "Choose an answer before checking.";
feedback.className = "fcpython-quiz-feedback";
setStepStatus(questionIndex, "unanswered");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
return;
}
const selectedIndex = Number(selected.value);
if (selectedIndex === question.answer_index) {
correctCount += 1;
feedback.textContent = `✅ Correct. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-correct";
setStepStatus(questionIndex, "correct");
} else {
const answer = question.options[question.answer_index];
feedback.textContent = `❌ Not yet. Correct answer: ${answer}. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-incorrect";
setStepStatus(questionIndex, "incorrect");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
}
});
score.textContent = `Score: ${correctCount}/${quiz.questions.length}`;
if (firstQuestionToReview !== null) {
showQuestion(firstQuestionToReview, true);
}
});
reset.addEventListener("click", () => {
container.querySelectorAll("input[type='radio']").forEach((input) => {
input.checked = false;
});
feedbackNodes.forEach((feedback) => {
feedback.textContent = "";
feedback.className = "fcpython-quiz-feedback";
});
stepButtons.forEach((_, questionIndex) => {
setStepStatus(questionIndex, "");
});
score.textContent = "";
showQuestion(0, true);
});
actions.appendChild(previous);
actions.appendChild(next);
actions.appendChild(check);
actions.appendChild(reset);
container.appendChild(actions);
container.appendChild(score);
showQuestion(0);
return container;
}
FreeCampus Python
Unit Challenge: Repair and Explain a Broken Study Planner
Repair a complete beginner Python program, prove its behavior with assertions, document its decisions, and explain every important line.
python-foundations
python-syntax
unit-challenge
Course progress
0%
1. Your task: repair the study planner
A study-planner program was damaged while being copied from a message. Its purpose is simple, but Python cannot parse it. Repair it in stages until it:
- preserves the required names and input values;
- calculates focused time;
- chooses the correct session label;
- stores a progress comparison;
- displays a readable summary;
- begins with a useful module docstring;
- contains one comment explaining why the break is excluded;
- passes every supplied assertion; and
- still works after one requirement change.
The challenge intentionally combines independent and cascading mistakes. The first repair may reveal another error. Make one change, rerun, and record what the new result teaches you.
NoteWork independently, but use the support deliberately
Attempt each stage before opening a hint. A hint should answer a specific question you can state, not replace reading the source and error report.
2. Understand the acceptance example
The repaired program starts with these facts:
| Name | Value | Meaning |
|---|---|---|
course_title |
"Python Foundations" |
course being studied |
session_minutes |
45 |
scheduled session length |
break_minutes |
10 |
scheduled break |
completed_lessons |
3 |
lessons completed so far |
target_lessons |
7 |
current target |
It must calculate:
| Name | Expected value |
|---|---|
focused_minutes |
35 |
session_label |
"focused" |
is_target_reached |
False |
Expected output:
Constraints
- Keep every required name exactly as shown.
- Keep the initial input values unchanged until the controlled-change stage.
- Use one module docstring as the first statement.
- Keep the
if/elseblock; do not replace the decision with a fixed string. - Write the focused-time calculation across multiple lines inside parentheses.
- Use a comment to explain why the break is subtracted.
- Do not edit expected assertion values merely to create a pass.
- Do not use
compile(),exec(), exception handling, functions, or classes.
Before editing, annotate the starter source on paper or in Markdown:
- circle literals;
- underline intended identifiers;
- box keywords;
- mark operators;
- pair delimiters where possible;
- draw a line along each intended indentation level.
3. Start from the contract
Copy this starter into one code cell. It is intentionally invalid.
"""Do study stuff."""
course title = "Python Foundations"
session_minutes = 45
break-minutes = 10
completed_lessons = 3
target_lessons = 7
# Subtract the break.
focused_minutes = (
session_minutes
- break-minutes
if focused_minutes >= 30
session_label = "focused"
else:
session_label = "short"
is_target_reached = completed_lessons >= target_lessons
print("Course:" course title)
print(
"Focused minutes:",
focused_minutes,
]
print("Session:", session_label)
print("Target reached:", is_target_reached)Do not replace the entire program with a new one. Repair the starter so each change remains connected to an observed problem.
Stage A: make every intended name valid
Inspect the two intended multiword names that do not follow identifier rules. Repair their definitions and every later use.
After your edit, state:
- why a space cannot occur inside one identifier;
- why a hyphen is read as an operator;
- why changing only the assignment would leave a later lookup inconsistent.
Run again. A different syntax report is progress.
Stage B: complete the multiline expression
Pair the opening parenthesis in the focused_minutes assignment. Keep the calculation spread across readable physical lines.
Replace the existing comment with one that explains the reporting rule rather than translating subtraction. For example, it should answer:
Why does this report exclude the break?
Run again and read the next report.
Stage C: repair the decision block
The if header, body, else header, and second body must form one visible structure.
Before editing, draw the intended shape:
Then repair the missing delimiter and indentation. Do not change the comparison or label values.
Stage D: repair both output calls
One call is missing an argument separator. Another closes with the wrong delimiter. Repair the smallest relevant token in each call.
Once the program parses, compare its four output lines with the acceptance example. Valid syntax is not enough; spelling, order, and values must match.
Stage E: document the program’s promise
Replace """Do study stuff.""" with a one-sentence module docstring that describes the program’s actual purpose. Keep it as the first statement.
Review the focused-time comment. It should explain the decision to exclude the break, not say only “subtract the break.”
4. Build in small stages
Use this repair sequence:
- Run the unchanged starter and preserve the first report.
- Classify the nearby issue as name, quote, delimiter, comma, header, or indentation.
- State one broken rule.
- Make one smallest edit.
- Rerun immediately.
- Record whether the same error, a new error, or normal output appears.
- Continue until the program parses.
- Compare normal output with the acceptance example.
- Run the assertions in groups.
- Restart the runtime and run every challenge cell from the top.
A useful progress log is:
| Run | Reported evidence | Rule you believe is broken | One edit | New evidence |
|---|---|---|---|---|
| 1 | Copy the error type, line, and message | One grammatical rule | One change | Same, new, or resolved |
| 2 | … | … | … | … |
WarningDo not fix by deleting the difficult feature
Removing the multiline expression, if block, documentation, or assertions would avoid practicing the unit skill. Repair the required structure instead.
5. Run progressive assertions
Run each group only after the repaired program reaches normal output.
Group 1: required input bindings
If one fails, inspect the corresponding assignment. Do not edit the expected value.
Group 2: calculated values
The first and third checks restate relationships. The second and fourth verify the acceptance example.
Group 3: decision result
The first check verifies the current input. The second protects the allowed vocabulary.
Group 4: required source qualities
Verify these by reading the source:
- the first statement is the module docstring;
- the focused-time comment explains why the break is excluded;
- the focused-time expression remains multiline and parenthesized;
- the two branch bodies use four spaces;
elsealigns withif;- each
print()call has matching parentheses and comma-separated arguments.
Assertions can verify values. They cannot prove that a comment is useful.
6. Use the hint ladder only when needed
Hint 1: classify the remaining problems
Work from top to bottom.
- The intended course name contains whitespace.
- The intended break name contains an operator.
- The focused-time grouping never closes.
- The decision header lacks its closing delimiter.
- The two branch headers and bodies do not form matching levels.
- The first output call lacks an argument separator.
- The multiline output call closes with a square bracket.
Fix only the first unresolved category, then rerun.
Hint 2: compare each broken shape with a valid shape
Valid identifier and continuation shapes:
Valid decision shape:
Valid two-argument call:
Use these shapes to locate a mismatch; do not copy new behavior into the program.
Hint 3: expected repaired structure
The repaired executable statements, without the documentation wording, should have this order:
The focused calculation is 45 - 10. The decision compares that result with 30. The progress comparison checks 3 >= 7.
If those relationships exist and the syntax is repaired, the acceptance values follow.
7. Keep debugging evidence
Choose one failure that required thought. Record:
| Field | Your evidence |
|---|---|
| Input or source state | What exact code did you run? |
| Error type and message | What did Python report? |
| Reported location | Which line and token were highlighted? |
| Nearby cause | Was the actual cause on that line or earlier? |
| Hypothesis | Which one syntax rule explained it? |
| Controlled change | What one edit did you make? |
| Verified rerun | What changed after rerunning? |
| Explanation | Why did that edit resolve this failure? |
A useful record contains exact source and observed text. “It was broken, then I fixed it” is not enough to reuse the method later.
8. Test a changed requirement
After all initial checks pass, change only:
Before running, predict:
focused_minutes;session_label;is_target_reached;- all four output lines.
The expected changed values are:
If the old labels remain, check whether a derived assignment was run before or after the changed input. Restart and run from the top.
Then restore the original input values and rerun every initial assertion. The program should support both examples without changing its decision logic.
9. Explain the repaired program
Write one sentence for each important line or group:
- What value or name appears?
- What lookup or calculation does Python perform?
- What binding changes?
- What block owns the line?
- What output or stored result should follow?
Your explanation should distinguish:
- the docstring from the comment;
- assignment
=from comparison>=; - the multiline expression from the decision block;
- the
ifbody from theelsebody; - calculated values from displayed values.
10. Compare with a solution path
Reveal after your assertions pass or all three hints have been used
"""Display a study-session summary and current lesson-target status."""
course_title = "Python Foundations"
session_minutes = 45
break_minutes = 10
completed_lessons = 3
target_lessons = 7
# The report measures active study, so the scheduled break is excluded.
focused_minutes = (
session_minutes
- break_minutes
)
if focused_minutes >= 30:
session_label = "focused"
else:
session_label = "short"
is_target_reached = completed_lessons >= target_lessons
print("Course:", course_title)
print(
"Focused minutes:",
focused_minutes,
)
print("Session:", session_label)
print("Target reached:", is_target_reached)Passing checks:
assert course_title == "Python Foundations"
assert session_minutes == 45
assert break_minutes == 10
assert completed_lessons == 3
assert target_lessons == 7
assert focused_minutes == session_minutes - break_minutes
assert focused_minutes == 35
assert session_label == "focused"
assert is_target_reached == (completed_lessons >= target_lessons)
assert is_target_reached is FalseYour docstring and comment may use different words if they accurately describe the same purpose and decision.
11. Check your understanding
12. Before you finish
Evidence rubric
| Evidence | Ready to record when |
|---|---|
| Syntax | The program runs from a clean state with no syntax or indentation errors. |
| Values | All initial and changed-example assertions pass without editing expected values. |
| Structure | Names, delimiters, continuation, and block levels follow the required shapes. |
| Documentation | The first statement is a useful docstring and the comment explains a real decision. |
| Debugging | One record connects an exact failure to one hypothesis, edit, and rerun. |
| Explanation | Every important line can be described as a value, lookup, expression, statement, block, or output action. |
This button stores a self-reported marker only in this browser. It does not submit work, grade the program, verify identity, or issue a certificate.
Not yet recorded.
Key points
- Repair syntax from the earliest clear failure, one controlled edit at a time.
- Passing assertions verify relationships and values; source review verifies readability and documentation.
- A changed example tests whether the program represents a rule rather than one memorized output.
- A clean rerun and line-by-line explanation complete the challenge.