// 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 { 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 { 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 { let ids: Vec = 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) }