111 lines
3.8 KiB
Rust
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)
|
|
}
|