Files
coursebank/src/commands/lectures.rs
T
alexm 003340dc6f
Pipeline / check (pull_request) Failing after 2m38s
Pipeline / docs (pull_request) Skipped
Pipeline / nightly (pull_request) Skipped
Pipeline / release (pull_request) Skipped
fix: handle targets
2026-09-21 15:24:16 -04:00

111 lines
3.8 KiB
Rust

// SPDX-License-Identifier: Prosperity-3.0.0
// Copyright Scientific Computing Studio
// Source: https://git.scient.ing/education/coursebank
//! Rendering lecture pages, and checking what backs each learning target.
//!
//! Both handlers here read the course file and nothing else, so neither needs a
//! bank or a single response. That is deliberate: a reading list is useful in week
//! one, before any item exists.
use coursebank::course::CourseFile;
use coursebank::error::Result;
use coursebank::lecture::{objectives_markdown, readings_markdown};
use coursebank::yaml;
use crate::cli::{Cli, LectureCommand};
use crate::commands::Outcome;
/// `lecture`: render a reading list, or report reading coverage.
pub(crate) fn lecture(cli: &Cli, sub: &LectureCommand) -> Result<Outcome> {
let course = CourseFile::load_dir(&cli.course)?;
match sub {
LectureCommand::Readings { id, style, out } => emit(
readings_markdown(&course, id, style.as_style())?,
out.as_deref(),
),
LectureCommand::Objectives { id, style, out } => emit(
objectives_markdown(&course, id, style.as_style())?,
out.as_deref(),
),
LectureCommand::Coverage { lecture: only } => coverage(&course, only.as_deref(), cli.quiet),
}
}
/// Writes rendered Markdown to a file, or to stdout when no path was given.
fn emit(markdown: String, out: Option<&std::path::Path>) -> Result<Outcome> {
match out {
Some(path) => {
yaml::write_text(path, &markdown)?;
println!("wrote {}", path.display());
}
None => print!("{markdown}"),
}
Ok(Outcome::Ok)
}
/// Prints the readings behind each learning target.
///
/// Targets rather than objectives, because that is the tier a reading is cited
/// against: a section of a book backs a specific performance. Returns
/// [`Outcome::Findings`] when an assessed target has no reading, since that is
/// the case where a student report can name what was missed but not where to go
/// and read about it.
fn coverage(course: &CourseFile, lecture: Option<&str>, quiet: bool) -> Result<Outcome> {
let ids: Vec<String> = match lecture {
Some(l) => course
.lecture_targets(l)
.into_iter()
.map(str::to_string)
.collect(),
None => course.targets_in_order(),
};
for id in &ids {
let readings = course.readings_for_objective(id);
println!("{id}");
if readings.is_empty() {
println!(" (no reading)");
continue;
}
for (lecture_id, reading) in readings {
let Some(key) = reading.reference.as_deref() else {
continue;
};
let reference = course.reference(key, lecture_id)?;
let supplemental = match reading.role {
coursebank::course::ReadingRole::Supplemental => " (supplemental)",
coursebank::course::ReadingRole::Assigned => "",
};
println!(
" {lecture_id} {}{supplemental}",
reading.cite(key, reference)
);
if let Some(focus) = &reading.focus {
println!(
" {}",
course.expand_objective_refs(focus, |id| { course.text_for(id) })
);
}
}
}
let gaps = course.targets_without_readings();
if gaps.is_empty() {
if !quiet {
println!("\nevery assessed target has a reading behind it");
}
return Ok(Outcome::Ok);
}
println!(
"\n{} assessed target(s) with no reading, so a student report cannot say \
where to go back to:",
gaps.len()
);
for id in gaps {
println!(" - {id}");
}
Ok(Outcome::Findings)
}