Compare commits
2
Commits
cea03048b8
...
9755417899
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9755417899
|
||
|
|
beec860a2c
|
+21
@@ -676,6 +676,27 @@ pub(crate) enum ReportCommand {
|
||||
/// Leave out the hint written for the option the student chose.
|
||||
#[arg(long)]
|
||||
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.
|
||||
///
|
||||
/// Written for you rather than for them, so it reads clinically next to
|
||||
|
||||
@@ -498,6 +498,13 @@ pub(crate) fn report(cli: &Cli, sub: &ReportCommand) -> Result<Outcome> {
|
||||
no_questions,
|
||||
no_feedback,
|
||||
no_hints,
|
||||
no_levels,
|
||||
no_objectives,
|
||||
no_strengths,
|
||||
no_focus,
|
||||
no_dropped_questions,
|
||||
no_review_lectures,
|
||||
no_study,
|
||||
misconceptions,
|
||||
solutions,
|
||||
template,
|
||||
@@ -548,7 +555,31 @@ pub(crate) fn report(cli: &Cli, sub: &ReportCommand) -> Result<Outcome> {
|
||||
let analysis =
|
||||
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 opts = diagnostic::Options {
|
||||
comparison: !no_comparison,
|
||||
|
||||
@@ -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.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -378,6 +430,11 @@ pub struct RenderConfig {
|
||||
#[serde(default = "yes")]
|
||||
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.
|
||||
///
|
||||
/// This is the escape hatch that keeps the crate out of your layout
|
||||
@@ -410,6 +467,7 @@ impl RenderConfig {
|
||||
stimulus: StimulusMode::Inline,
|
||||
fields: Fields::default(),
|
||||
number_from_record: true,
|
||||
student_sections: StudentSections::default(),
|
||||
extra: BTreeMap::new(),
|
||||
};
|
||||
match variant {
|
||||
@@ -560,6 +618,9 @@ pub struct Overrides {
|
||||
/// See [`RenderConfig::fields`].
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
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`].
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub number_from_record: Option<bool>,
|
||||
@@ -603,6 +664,9 @@ impl Overrides {
|
||||
if let Some(v) = &self.fields {
|
||||
config.fields = v.clone();
|
||||
}
|
||||
if let Some(v) = &self.student_sections {
|
||||
config.student_sections = v.clone();
|
||||
}
|
||||
if let Some(v) = self.number_from_record {
|
||||
config.number_from_record = v;
|
||||
}
|
||||
@@ -668,6 +732,20 @@ variants:
|
||||
|
||||
answer-sheet:
|
||||
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`.
|
||||
|
||||
@@ -169,10 +169,16 @@ fn extra_value(config: &RenderConfig) -> 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
|
||||
///
|
||||
/// * `diagnostic` - the assembled diagnostic.
|
||||
/// * `config` - the render config, for markup handling.
|
||||
/// * `config` - the render config, for markup handling and section toggles.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
@@ -206,11 +212,15 @@ pub fn student_value(diagnostic: &StudentDiagnostic, config: &RenderConfig) -> V
|
||||
out.insert("standing", value);
|
||||
}
|
||||
|
||||
let levels = if config.student_sections.levels {
|
||||
diagnostic.levels.as_slice()
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
out.insert(
|
||||
"levels",
|
||||
Value::Array(
|
||||
diagnostic
|
||||
.levels
|
||||
levels
|
||||
.iter()
|
||||
.map(|level| {
|
||||
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(
|
||||
"objectives",
|
||||
Value::Array(
|
||||
diagnostic
|
||||
.objectives
|
||||
objectives
|
||||
.iter()
|
||||
.map(|objective| {
|
||||
let mut value = Value::dict();
|
||||
@@ -278,8 +292,22 @@ pub fn student_value(diagnostic: &StudentDiagnostic, config: &RenderConfig) -> V
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
out.insert("strengths", objective_refs(&diagnostic.strengths));
|
||||
out.insert("focus", objective_refs(&diagnostic.focus));
|
||||
out.insert(
|
||||
"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(
|
||||
"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(
|
||||
"dropped-questions",
|
||||
Value::Array(
|
||||
diagnostic
|
||||
.dropped_questions
|
||||
dropped_questions
|
||||
.iter()
|
||||
.map(|dropped| {
|
||||
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(
|
||||
"review-lectures",
|
||||
Value::Array(
|
||||
diagnostic
|
||||
.review_lectures
|
||||
review_lectures
|
||||
.iter()
|
||||
.map(|lecture| {
|
||||
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(
|
||||
"study",
|
||||
Value::Array(
|
||||
diagnostic
|
||||
.study
|
||||
study
|
||||
.iter()
|
||||
.map(|group| study_value(group, content))
|
||||
.collect(),
|
||||
|
||||
@@ -749,7 +749,7 @@
|
||||
|
||||
#for group in study [
|
||||
#block(breakable: false, above: entry-gap)[
|
||||
== #markup(group.text)
|
||||
=== #markup(group.text)
|
||||
|
||||
#for reading in group.readings [
|
||||
#block(inset: (left: 0.8em), above: step)[
|
||||
|
||||
Reference in New Issue
Block a user