feat: support readings and lecture rendering
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
// 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 objective.
|
||||
//!
|
||||
//! 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 objective.
|
||||
///
|
||||
/// Returns [`Outcome::Findings`] when an assessed objective 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_objectives(l)
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
None => course.objectives_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, |o| { course.objective_text(o) })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let gaps = course.objectives_without_readings();
|
||||
if gaps.is_empty() {
|
||||
if !quiet {
|
||||
println!("\nevery assessed objective has a reading behind it");
|
||||
}
|
||||
return Ok(Outcome::Ok);
|
||||
}
|
||||
println!(
|
||||
"\n{} assessed objective(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)
|
||||
}
|
||||
@@ -183,13 +183,14 @@ fn merge_gitignore(existing: &str) -> GitignoreMerge {
|
||||
|
||||
/// Writes the `.gitignore`, merging into one that is already there.
|
||||
///
|
||||
/// `init` is often run in a repository that already has a `.gitignore`.
|
||||
/// An existing file keeps everything it had and gains only the patterns
|
||||
/// it was missing, at the top where they are easy to see in the diff.
|
||||
/// `init` is often run in a repository that already has a `.gitignore`, and the
|
||||
/// first version of this overwrote it. An existing file now keeps everything it
|
||||
/// had and gains only the patterns it was missing, at the top where they are easy
|
||||
/// to see in the diff.
|
||||
fn write_gitignore(path: &Path) -> Result<()> {
|
||||
let existing = match fs::read_to_string(path) {
|
||||
Ok(text) => text,
|
||||
// Nothing to merge with. Stay quiet about it.
|
||||
// Nothing to merge with. Stay quiet about it, the way this always has.
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
return yaml::write_text(path, GITIGNORE);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user