feat: cooked up something fierce
This commit is contained in:
+55
-10
@@ -403,6 +403,15 @@ fn validate_item(
|
||||
if seen.contains(&o.id.as_str()) {
|
||||
issues.push(format!("option {pos}: duplicate option id `{}`", o.id));
|
||||
}
|
||||
if let Some(retirement) = &o.retired {
|
||||
if retirement.reason.trim().is_empty() {
|
||||
issues.push(format!(
|
||||
"option {pos}: retired without a reason. The reason is the finding — what \
|
||||
the option did or failed to do — and it is the only part of a retirement \
|
||||
that is worth anything later."
|
||||
));
|
||||
}
|
||||
}
|
||||
seen.push(&o.id);
|
||||
|
||||
let credit = o.credit();
|
||||
@@ -442,14 +451,27 @@ fn validate_item(
|
||||
}
|
||||
|
||||
// --- key ---
|
||||
// Counted over the pool that can still be drawn: a retired option is a
|
||||
// record, not an offer.
|
||||
let (live_keys, live_distractors) = it.pool();
|
||||
let keys = it.key_indices();
|
||||
match it.format {
|
||||
Format::SingleBestAnswer => {
|
||||
if keys.len() != 1 {
|
||||
issues.push(format!(
|
||||
"single_best_answer needs exactly one keyed option, has {}",
|
||||
keys.len()
|
||||
));
|
||||
// Several defensible keys is a pool, not a bug — it is what lets you
|
||||
// test whether "fourth" or "last of the four" is doing the work.
|
||||
// Exactly one of them reaches a student, and that is the
|
||||
// placement's business: see
|
||||
// [`crate::catalog::Catalog::validate_record`].
|
||||
if live_keys.is_empty() {
|
||||
issues.push("single_best_answer needs at least one keyed option".into());
|
||||
}
|
||||
if live_distractors.is_empty() {
|
||||
let retired = it.options.iter().any(|o| o.retired.is_some());
|
||||
issues.push(if retired {
|
||||
"every distractor is retired, so nothing can be drawn against the key".into()
|
||||
} else {
|
||||
"has no option that is not keyed correct, so it asks nothing".to_string()
|
||||
});
|
||||
}
|
||||
}
|
||||
Format::MultipleResponse => {
|
||||
@@ -780,20 +802,43 @@ mod tests {
|
||||
options:
|
||||
- { id: A, text: a, correct: true }
|
||||
- { id: B, text: b, correct: true }
|
||||
- id: q-a-003
|
||||
status: draft
|
||||
level: 1
|
||||
format: single_best_answer
|
||||
stem: s
|
||||
options:
|
||||
- { id: o-key-one, text: a, correct: true }
|
||||
- { id: o-key-two, text: b, correct: true }
|
||||
- { id: o-wrong-one, text: c }
|
||||
- { id: o-wrong-two, text: d }
|
||||
"#,
|
||||
);
|
||||
let issues = b.validate(None);
|
||||
// No key at all is still a bank problem: nothing can be drawn from it.
|
||||
assert!(
|
||||
issues
|
||||
.iter()
|
||||
.any(|i| i.contains("exactly one keyed option"))
|
||||
.any(|i| i.starts_with("q-a-001") && i.contains("at least one keyed option")),
|
||||
"{issues:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
// Keying every option is still a bank problem, for the older reason: a
|
||||
// question with nothing to choose against asks nothing.
|
||||
assert!(
|
||||
issues
|
||||
.iter()
|
||||
.filter(|i| i.contains("exactly one keyed option"))
|
||||
.count(),
|
||||
2
|
||||
.any(|i| i.starts_with("q-a-002") && i.contains("asks nothing")),
|
||||
"{issues:?}"
|
||||
);
|
||||
// Two defensible keys alongside real distractors is not a problem. It
|
||||
// is the pool doing its job — it is what lets you test whether the
|
||||
// wording of the key is what students are answering. Exactly one of
|
||||
// them reaches a student, and that is checked against the placement
|
||||
// that administers it, in `Catalog::validate_placement`.
|
||||
assert!(
|
||||
!issues.iter().any(|i| i.starts_with("q-a-003")
|
||||
&& (i.contains("keyed option") || i.contains("asks nothing"))),
|
||||
"{issues:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user