Compare commits
2
Commits
cea03048b8
...
9755417899
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9755417899
|
||
|
|
beec860a2c
|
+22
-1
@@ -676,6 +676,27 @@ pub(crate) enum ReportCommand {
|
|||||||
/// Leave out the hint written for the option the student chose.
|
/// Leave out the hint written for the option the student chose.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
no_hints: bool,
|
no_hints: bool,
|
||||||
|
/// Leave the "kinds of thinking" comparison out of the Typst report.
|
||||||
|
#[arg(long)]
|
||||||
|
no_levels: bool,
|
||||||
|
/// Leave the per-objective mastery table out of the Typst report.
|
||||||
|
#[arg(long)]
|
||||||
|
no_objectives: bool,
|
||||||
|
/// Leave the objectives called out as strengths out of the Typst report.
|
||||||
|
#[arg(long)]
|
||||||
|
no_strengths: bool,
|
||||||
|
/// Leave the objectives called out as focus areas out of the Typst report.
|
||||||
|
#[arg(long)]
|
||||||
|
no_focus: bool,
|
||||||
|
/// Leave the dropped-question notice out of the Typst report.
|
||||||
|
#[arg(long)]
|
||||||
|
no_dropped_questions: bool,
|
||||||
|
/// Leave lecture-review suggestions out of the Typst report.
|
||||||
|
#[arg(long)]
|
||||||
|
no_review_lectures: bool,
|
||||||
|
/// Leave suggested study groups and readings out of the Typst report.
|
||||||
|
#[arg(long)]
|
||||||
|
no_study: bool,
|
||||||
/// Name the misconception each chosen distractor was written to catch.
|
/// Name the misconception each chosen distractor was written to catch.
|
||||||
///
|
///
|
||||||
/// Written for you rather than for them, so it reads clinically next to
|
/// Written for you rather than for them, so it reads clinically next to
|
||||||
@@ -778,4 +799,4 @@ mod tests {
|
|||||||
fn the_cli_rejects_an_unknown_subcommand() {
|
fn the_cli_rejects_an_unknown_subcommand() {
|
||||||
assert!(Cli::try_parse_from(["coursebank", "frobnicate"]).is_err());
|
assert!(Cli::try_parse_from(["coursebank", "frobnicate"]).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -498,6 +498,13 @@ pub(crate) fn report(cli: &Cli, sub: &ReportCommand) -> Result<Outcome> {
|
|||||||
no_questions,
|
no_questions,
|
||||||
no_feedback,
|
no_feedback,
|
||||||
no_hints,
|
no_hints,
|
||||||
|
no_levels,
|
||||||
|
no_objectives,
|
||||||
|
no_strengths,
|
||||||
|
no_focus,
|
||||||
|
no_dropped_questions,
|
||||||
|
no_review_lectures,
|
||||||
|
no_study,
|
||||||
misconceptions,
|
misconceptions,
|
||||||
solutions,
|
solutions,
|
||||||
template,
|
template,
|
||||||
@@ -548,7 +555,31 @@ pub(crate) fn report(cli: &Cli, sub: &ReportCommand) -> Result<Outcome> {
|
|||||||
let analysis =
|
let analysis =
|
||||||
classical::analyze(&set, &Thresholds::default(), Some(&record), Some(&catalog));
|
classical::analyze(&set, &Thresholds::default(), Some(&record), Some(&catalog));
|
||||||
|
|
||||||
let config = typst::load_config(&catalog.layout)?.resolve(Variant::StudentReport);
|
let mut config = typst::load_config(&catalog.layout)?.resolve(Variant::StudentReport);
|
||||||
|
// Each flag only turns a section off; leaving it unset keeps whatever
|
||||||
|
// templates/typst.yaml already resolved to, so a CLI run that doesn't
|
||||||
|
// mention a section never overrides a course's saved preference.
|
||||||
|
if *no_levels {
|
||||||
|
config.student_sections.levels = false;
|
||||||
|
}
|
||||||
|
if *no_objectives {
|
||||||
|
config.student_sections.objectives = false;
|
||||||
|
}
|
||||||
|
if *no_strengths {
|
||||||
|
config.student_sections.strengths = false;
|
||||||
|
}
|
||||||
|
if *no_focus {
|
||||||
|
config.student_sections.focus = false;
|
||||||
|
}
|
||||||
|
if *no_dropped_questions {
|
||||||
|
config.student_sections.dropped_questions = false;
|
||||||
|
}
|
||||||
|
if *no_review_lectures {
|
||||||
|
config.student_sections.review_lectures = false;
|
||||||
|
}
|
||||||
|
if *no_study {
|
||||||
|
config.student_sections.study = false;
|
||||||
|
}
|
||||||
let meta = typst_diagnostic::Meta::new(&catalog, &record, cohort.students.len());
|
let meta = typst_diagnostic::Meta::new(&catalog, &record, cohort.students.len());
|
||||||
let opts = diagnostic::Options {
|
let opts = diagnostic::Options {
|
||||||
comparison: !no_comparison,
|
comparison: !no_comparison,
|
||||||
@@ -751,4 +782,4 @@ fn warn_about_drift(
|
|||||||
eprintln!(" {}", finding.message);
|
eprintln!(" {}", finding.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,6 +328,58 @@ impl Default for Fields {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which sections of a student diagnostic are emitted.
|
||||||
|
///
|
||||||
|
/// Everything defaults on. Each flag corresponds to one block that
|
||||||
|
/// [`crate::typst::diagnostic::student_value`] would otherwise write
|
||||||
|
/// unconditionally: turning one off drops it from `cb-data` as an empty array
|
||||||
|
/// rather than omitting the key, so a template that checks `len() > 0` (the
|
||||||
|
/// pattern the bundled templates use) simply renders nothing for that section
|
||||||
|
/// without needing to guard against a missing key.
|
||||||
|
///
|
||||||
|
/// This governs whole sections. Which fields survive *within* a question —
|
||||||
|
/// feedback, hints, misconceptions, worked solutions — is decided when the
|
||||||
|
/// diagnostic itself is built, not here.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct StudentSections {
|
||||||
|
/// The per-level ("kinds of thinking") comparison to the class.
|
||||||
|
#[serde(default = "yes")]
|
||||||
|
pub levels: bool,
|
||||||
|
/// The per-objective mastery table.
|
||||||
|
#[serde(default = "yes")]
|
||||||
|
pub objectives: bool,
|
||||||
|
/// Objectives called out as strengths.
|
||||||
|
#[serde(default = "yes")]
|
||||||
|
pub strengths: bool,
|
||||||
|
/// Objectives called out as focus areas.
|
||||||
|
#[serde(default = "yes")]
|
||||||
|
pub focus: bool,
|
||||||
|
/// Which questions were dropped from scoring.
|
||||||
|
#[serde(default = "yes")]
|
||||||
|
pub dropped_questions: bool,
|
||||||
|
/// Lectures to revisit for missed objectives.
|
||||||
|
#[serde(default = "yes")]
|
||||||
|
pub review_lectures: bool,
|
||||||
|
/// Suggested study groups and their readings.
|
||||||
|
#[serde(default = "yes")]
|
||||||
|
pub study: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StudentSections {
|
||||||
|
fn default() -> StudentSections {
|
||||||
|
StudentSections {
|
||||||
|
levels: true,
|
||||||
|
objectives: true,
|
||||||
|
strengths: true,
|
||||||
|
focus: true,
|
||||||
|
dropped_questions: true,
|
||||||
|
review_lectures: true,
|
||||||
|
study: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The resolved configuration for rendering one variant.
|
/// The resolved configuration for rendering one variant.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
@@ -378,6 +430,11 @@ pub struct RenderConfig {
|
|||||||
#[serde(default = "yes")]
|
#[serde(default = "yes")]
|
||||||
pub number_from_record: bool,
|
pub number_from_record: bool,
|
||||||
|
|
||||||
|
/// Which sections of a student diagnostic are emitted. Ignored by every
|
||||||
|
/// variant except [`Variant::StudentReport`].
|
||||||
|
#[serde(default)]
|
||||||
|
pub student_sections: StudentSections,
|
||||||
|
|
||||||
/// Anything else you want the template to see, carried through untouched.
|
/// Anything else you want the template to see, carried through untouched.
|
||||||
///
|
///
|
||||||
/// This is the escape hatch that keeps the crate out of your layout
|
/// This is the escape hatch that keeps the crate out of your layout
|
||||||
@@ -410,6 +467,7 @@ impl RenderConfig {
|
|||||||
stimulus: StimulusMode::Inline,
|
stimulus: StimulusMode::Inline,
|
||||||
fields: Fields::default(),
|
fields: Fields::default(),
|
||||||
number_from_record: true,
|
number_from_record: true,
|
||||||
|
student_sections: StudentSections::default(),
|
||||||
extra: BTreeMap::new(),
|
extra: BTreeMap::new(),
|
||||||
};
|
};
|
||||||
match variant {
|
match variant {
|
||||||
@@ -560,6 +618,9 @@ pub struct Overrides {
|
|||||||
/// See [`RenderConfig::fields`].
|
/// See [`RenderConfig::fields`].
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub fields: Option<Fields>,
|
pub fields: Option<Fields>,
|
||||||
|
/// See [`RenderConfig::student_sections`].
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub student_sections: Option<StudentSections>,
|
||||||
/// See [`RenderConfig::number_from_record`].
|
/// See [`RenderConfig::number_from_record`].
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub number_from_record: Option<bool>,
|
pub number_from_record: Option<bool>,
|
||||||
@@ -603,6 +664,9 @@ impl Overrides {
|
|||||||
if let Some(v) = &self.fields {
|
if let Some(v) = &self.fields {
|
||||||
config.fields = v.clone();
|
config.fields = v.clone();
|
||||||
}
|
}
|
||||||
|
if let Some(v) = &self.student_sections {
|
||||||
|
config.student_sections = v.clone();
|
||||||
|
}
|
||||||
if let Some(v) = self.number_from_record {
|
if let Some(v) = self.number_from_record {
|
||||||
config.number_from_record = v;
|
config.number_from_record = v;
|
||||||
}
|
}
|
||||||
@@ -668,6 +732,20 @@ variants:
|
|||||||
|
|
||||||
answer-sheet:
|
answer-sheet:
|
||||||
reveal: nothing
|
reveal: nothing
|
||||||
|
|
||||||
|
# Every student-report section defaults on. Uncomment what you don't want;
|
||||||
|
# the CLI's `--no-levels`, `--no-objectives`, `--no-strengths`, `--no-focus`,
|
||||||
|
# `--no-dropped-questions`, `--no-review-lectures`, and `--no-study` flags
|
||||||
|
# set these same fields for a single run without editing this file.
|
||||||
|
# student-report:
|
||||||
|
# student_sections:
|
||||||
|
# levels: false
|
||||||
|
# objectives: false
|
||||||
|
# strengths: false
|
||||||
|
# focus: false
|
||||||
|
# dropped_questions: false
|
||||||
|
# review_lectures: false
|
||||||
|
# study: false
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
/// Serde default: `true`.
|
/// Serde default: `true`.
|
||||||
@@ -791,4 +869,4 @@ mod tests {
|
|||||||
assert_eq!(file.resolve(Variant::Exam).reveal, Reveal::Nothing);
|
assert_eq!(file.resolve(Variant::Exam).reveal, Reveal::Nothing);
|
||||||
assert_eq!(file.resolve(Variant::Key).reveal, Reveal::Everything);
|
assert_eq!(file.resolve(Variant::Key).reveal, Reveal::Everything);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,10 +169,16 @@ fn extra_value(config: &RenderConfig) -> Value {
|
|||||||
|
|
||||||
/// One student's diagnostic as a Typst value.
|
/// One student's diagnostic as a Typst value.
|
||||||
///
|
///
|
||||||
|
/// `config.student_sections` decides which of `levels`, `objectives`,
|
||||||
|
/// `strengths`, `focus`, `dropped-questions`, `review-lectures`, and `study`
|
||||||
|
/// are populated; a section turned off is emitted as an empty array rather
|
||||||
|
/// than left out of the dictionary, so a template need not guard against a
|
||||||
|
/// missing key.
|
||||||
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `diagnostic` - the assembled diagnostic.
|
/// * `diagnostic` - the assembled diagnostic.
|
||||||
/// * `config` - the render config, for markup handling.
|
/// * `config` - the render config, for markup handling and section toggles.
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
@@ -206,11 +212,15 @@ pub fn student_value(diagnostic: &StudentDiagnostic, config: &RenderConfig) -> V
|
|||||||
out.insert("standing", value);
|
out.insert("standing", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let levels = if config.student_sections.levels {
|
||||||
|
diagnostic.levels.as_slice()
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
};
|
||||||
out.insert(
|
out.insert(
|
||||||
"levels",
|
"levels",
|
||||||
Value::Array(
|
Value::Array(
|
||||||
diagnostic
|
levels
|
||||||
.levels
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|level| {
|
.map(|level| {
|
||||||
let mut value = Value::dict();
|
let mut value = Value::dict();
|
||||||
@@ -227,11 +237,15 @@ pub fn student_value(diagnostic: &StudentDiagnostic, config: &RenderConfig) -> V
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let objectives = if config.student_sections.objectives {
|
||||||
|
diagnostic.objectives.as_slice()
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
};
|
||||||
out.insert(
|
out.insert(
|
||||||
"objectives",
|
"objectives",
|
||||||
Value::Array(
|
Value::Array(
|
||||||
diagnostic
|
objectives
|
||||||
.objectives
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|objective| {
|
.map(|objective| {
|
||||||
let mut value = Value::dict();
|
let mut value = Value::dict();
|
||||||
@@ -278,8 +292,22 @@ pub fn student_value(diagnostic: &StudentDiagnostic, config: &RenderConfig) -> V
|
|||||||
.collect(),
|
.collect(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
out.insert("strengths", objective_refs(&diagnostic.strengths));
|
out.insert(
|
||||||
out.insert("focus", objective_refs(&diagnostic.focus));
|
"strengths",
|
||||||
|
objective_refs(if config.student_sections.strengths {
|
||||||
|
&diagnostic.strengths
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
out.insert(
|
||||||
|
"focus",
|
||||||
|
objective_refs(if config.student_sections.focus {
|
||||||
|
&diagnostic.focus
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
out.insert(
|
out.insert(
|
||||||
"questions",
|
"questions",
|
||||||
@@ -349,11 +377,15 @@ pub fn student_value(diagnostic: &StudentDiagnostic, config: &RenderConfig) -> V
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let dropped_questions = if config.student_sections.dropped_questions {
|
||||||
|
diagnostic.dropped_questions.as_slice()
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
};
|
||||||
out.insert(
|
out.insert(
|
||||||
"dropped-questions",
|
"dropped-questions",
|
||||||
Value::Array(
|
Value::Array(
|
||||||
diagnostic
|
dropped_questions
|
||||||
.dropped_questions
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|dropped| {
|
.map(|dropped| {
|
||||||
let mut value = Value::dict();
|
let mut value = Value::dict();
|
||||||
@@ -365,11 +397,15 @@ pub fn student_value(diagnostic: &StudentDiagnostic, config: &RenderConfig) -> V
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let review_lectures = if config.student_sections.review_lectures {
|
||||||
|
diagnostic.review_lectures.as_slice()
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
};
|
||||||
out.insert(
|
out.insert(
|
||||||
"review-lectures",
|
"review-lectures",
|
||||||
Value::Array(
|
Value::Array(
|
||||||
diagnostic
|
review_lectures
|
||||||
.review_lectures
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|lecture| {
|
.map(|lecture| {
|
||||||
let mut value = Value::dict();
|
let mut value = Value::dict();
|
||||||
@@ -414,11 +450,15 @@ pub fn student_value(diagnostic: &StudentDiagnostic, config: &RenderConfig) -> V
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let study = if config.student_sections.study {
|
||||||
|
diagnostic.study.as_slice()
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
};
|
||||||
out.insert(
|
out.insert(
|
||||||
"study",
|
"study",
|
||||||
Value::Array(
|
Value::Array(
|
||||||
diagnostic
|
study
|
||||||
.study
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|group| study_value(group, content))
|
.map(|group| study_value(group, content))
|
||||||
.collect(),
|
.collect(),
|
||||||
@@ -1196,4 +1236,4 @@ mod tests {
|
|||||||
assert!(text.contains("reliability"));
|
assert!(text.contains("reliability"));
|
||||||
assert!(!text.contains("stem"));
|
assert!(!text.contains("stem"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -749,7 +749,7 @@
|
|||||||
|
|
||||||
#for group in study [
|
#for group in study [
|
||||||
#block(breakable: false, above: entry-gap)[
|
#block(breakable: false, above: entry-gap)[
|
||||||
== #markup(group.text)
|
=== #markup(group.text)
|
||||||
|
|
||||||
#for reading in group.readings [
|
#for reading in group.readings [
|
||||||
#block(inset: (left: 0.8em), above: step)[
|
#block(inset: (left: 0.8em), above: step)[
|
||||||
|
|||||||
Reference in New Issue
Block a user