Skip to main content

headless_lms_mcp/engine/course_structure/
mod.rs

1//! `patch_course_structure`: modules, chapters and pages, their order, names, titles, paths and
2//! chapter dates. Paths, chapter numbers and redirects are derived once at the end of a call.
3
4mod editor;
5mod fields;
6mod model;
7mod outline;
8mod paths;
9
10use std::collections::{BTreeMap, HashMap};
11
12use headless_lms_models::url_redirections::CreatedRedirect;
13use headless_lms_utils::document_schema_processor::GutenbergBlock;
14use serde::Serialize;
15use uuid::Uuid;
16
17use editor::StructureEditor;
18pub use model::{CourseStructure, StructureChapter, StructureModule, StructurePage};
19pub(crate) use model::{EntityRef, ListRef, Loc, RENDERED_KEYS, parse_structure_path};
20pub use outline::{StructureOutline, outline};
21pub use paths::LinkToOldPath;
22use paths::find_links_to_old_paths;
23
24use super::error::{EditError, Warning};
25use super::op::Op;
26use super::{CreatedIds, PageRef};
27use crate::ids::IdGenerator;
28
29/// A chapter whose number changed because chapters before it moved.
30#[derive(Debug, Clone, PartialEq, Serialize)]
31pub struct ChapterRenumber {
32    pub chapter_id: Uuid,
33    pub from: i32,
34    pub to: i32,
35}
36
37/// A page `copy` created; the caller copies the source page's content to it.
38#[derive(Debug, Clone, PartialEq, Serialize)]
39pub struct PageCopy {
40    pub source_page_id: Uuid,
41    pub new_page_id: Uuid,
42}
43
44/// The new state after a successful call. Nothing is stored until the caller stages it.
45#[derive(Debug, Clone, PartialEq)]
46pub struct StructureResult {
47    pub structure: CourseStructure,
48    pub created: Vec<CreatedIds>,
49    pub labels: BTreeMap<String, Uuid>,
50    pub warnings: Vec<Warning>,
51    /// Pages that still exist and whose path changed, as the redirects publishing creates from
52    /// their old paths.
53    pub changed_paths: Vec<CreatedRedirect>,
54    pub renumbered_chapters: Vec<ChapterRenumber>,
55    /// Blocks of the pages `add` created, by page id; empty when no `blocks` were given.
56    pub new_pages: HashMap<Uuid, Vec<GutenbergBlock>>,
57    /// Pages created by `copy`; the caller copies content with new block and exercise ids.
58    pub page_copies: Vec<PageCopy>,
59    /// Pages `add` created with `pair_with`, by new page id: the page whose language group each
60    /// joins, which the caller checks.
61    pub page_pairings: BTreeMap<Uuid, Uuid>,
62}
63
64impl StructureResult {
65    /// Links in `pages` to a page path the call changed, written `…/<course_slug><old path>`.
66    pub fn links_to_old_paths(&self, pages: &[PageRef], course_slug: &str) -> Vec<LinkToOldPath> {
67        find_links_to_old_paths(pages, course_slug, &self.changed_paths, &self.structure)
68    }
69}
70
71/// Applies `ops` in order to `doc`. In a live course (not a draft, not in test mode) new pages
72/// without content start hidden.
73pub fn apply(
74    doc: &CourseStructure,
75    ops: &[Op],
76    course_is_live: bool,
77    ids: &dyn IdGenerator,
78) -> Result<StructureResult, EditError> {
79    let mut editor = StructureEditor::new(doc, ops, course_is_live, ids);
80    for (index, op) in ops.iter().enumerate() {
81        editor.ids.set_op(index);
82        let first_warning = editor.warnings.len();
83        editor.apply_op(op).map_err(|e| e.at_op(index))?;
84        for warning in &mut editor.warnings[first_warning..] {
85            warning.op = Some(index);
86        }
87    }
88    editor.finish(doc)
89}
90
91#[cfg(test)]
92mod tests;