headless_lms_models/library/agent_changesets/course_state/
blockers.rs1use std::collections::{BTreeMap, HashSet};
2
3use utoipa::ToSchema;
4
5use super::CourseState;
6use crate::{
7 chapters,
8 library::entity_snapshots::{ContentEntityType, EntityKey, EntityRef},
9 prelude::*,
10};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
15#[serde(tag = "kind", rename_all = "snake_case")]
16pub enum PublishBlocker {
17 PageInMissingChapter { page: EntityRef, chapter_id: Uuid },
19 DuplicatePath {
21 url_path: String,
22 pages: Vec<EntityRef>,
23 },
24 DuplicatePageOrderNumber {
27 chapter: Option<EntityRef>,
28 order_number: i32,
29 pages: Vec<EntityRef>,
30 },
31 ChapterInMissingModule { chapter: EntityRef, module_id: Uuid },
33 MissingFrontPage {
35 chapter: EntityRef,
36 front_page_id: Uuid,
37 },
38 FrontPageInOtherChapter {
40 chapter: EntityRef,
41 front_page: EntityRef,
42 },
43 FrontPageOffPath {
45 chapter: EntityRef,
46 front_page: EntityRef,
47 url_path: String,
48 expected_url_path: String,
49 },
50 DuplicateChapterNumber {
52 chapter_number: i32,
53 chapters: Vec<EntityRef>,
54 },
55 DuplicateModuleOrderNumber {
57 order_number: i32,
58 modules: Vec<EntityRef>,
59 },
60}
61
62impl PublishBlocker {
63 fn unlabelled(mut self) -> Self {
65 let clear = |entity: &mut EntityRef| entity.label.clear();
66 match &mut self {
67 Self::PageInMissingChapter { page, .. } => clear(page),
68 Self::DuplicatePath { pages, .. } => pages.iter_mut().for_each(clear),
69 Self::DuplicatePageOrderNumber { chapter, pages, .. } => {
70 chapter.iter_mut().chain(pages.iter_mut()).for_each(clear)
71 }
72 Self::ChapterInMissingModule { chapter, .. }
73 | Self::MissingFrontPage { chapter, .. } => clear(chapter),
74 Self::FrontPageInOtherChapter {
75 chapter,
76 front_page,
77 }
78 | Self::FrontPageOffPath {
79 chapter,
80 front_page,
81 ..
82 } => {
83 clear(chapter);
84 clear(front_page);
85 }
86 Self::DuplicateChapterNumber { chapters, .. } => chapters.iter_mut().for_each(clear),
87 Self::DuplicateModuleOrderNumber { modules, .. } => modules.iter_mut().for_each(clear),
88 }
89 self
90 }
91}
92
93fn sorted(mut entities: Vec<EntityRef>) -> Vec<EntityRef> {
94 entities.sort_by_key(|entity| entity.entity_id);
95 entities
96}
97
98impl CourseState {
99 pub fn publish_blockers(&self) -> Vec<PublishBlocker> {
101 let mut blockers = Vec::new();
102 let chapter_ids: HashSet<Uuid> = self.chapters.iter().map(|c| c.id).collect();
103 let module_ids: HashSet<Uuid> = self.modules.iter().map(|m| m.id).collect();
104 let mut pages_by_path: BTreeMap<&str, Vec<EntityRef>> = BTreeMap::new();
105 let mut pages_by_position: BTreeMap<(Option<Uuid>, i32), Vec<EntityRef>> = BTreeMap::new();
106 for page in &self.pages {
107 let page_ref = page.entity_ref();
108 if let Some(chapter_id) = page.chapter_id
109 && !chapter_ids.contains(&chapter_id)
110 {
111 blockers.push(PublishBlocker::PageInMissingChapter {
112 page: page_ref.clone(),
113 chapter_id,
114 });
115 }
116 pages_by_path
117 .entry(&page.url_path)
118 .or_default()
119 .push(page_ref.clone());
120 pages_by_position
121 .entry((page.chapter_id, page.order_number))
122 .or_default()
123 .push(page_ref);
124 }
125 for (url_path, pages) in pages_by_path {
126 if pages.len() > 1 {
127 blockers.push(PublishBlocker::DuplicatePath {
128 url_path: url_path.to_string(),
129 pages: sorted(pages),
130 });
131 }
132 }
133 for ((chapter_id, order_number), pages) in pages_by_position {
134 if pages.len() > 1 {
135 blockers.push(PublishBlocker::DuplicatePageOrderNumber {
136 chapter: chapter_id
137 .map(|id| self.entity_ref(EntityKey::new(ContentEntityType::Chapter, id))),
138 order_number,
139 pages: sorted(pages),
140 });
141 }
142 }
143 let mut chapters_by_number: BTreeMap<i32, Vec<EntityRef>> = BTreeMap::new();
144 for chapter in &self.chapters {
145 let chapter_ref =
146 self.entity_ref(EntityKey::new(ContentEntityType::Chapter, chapter.id));
147 if !module_ids.contains(&chapter.course_module_id) {
148 blockers.push(PublishBlocker::ChapterInMissingModule {
149 chapter: chapter_ref.clone(),
150 module_id: chapter.course_module_id,
151 });
152 }
153 if let Some(front_page_id) = chapter.front_page_id {
154 let expected_url_path = chapters::front_page_path(chapter.chapter_number);
155 match self.page(front_page_id) {
156 None => blockers.push(PublishBlocker::MissingFrontPage {
157 chapter: chapter_ref.clone(),
158 front_page_id,
159 }),
160 Some(page) if page.chapter_id != Some(chapter.id) => {
161 blockers.push(PublishBlocker::FrontPageInOtherChapter {
162 chapter: chapter_ref.clone(),
163 front_page: page.entity_ref(),
164 })
165 }
166 Some(page) if page.url_path != expected_url_path => {
167 blockers.push(PublishBlocker::FrontPageOffPath {
168 chapter: chapter_ref.clone(),
169 front_page: page.entity_ref(),
170 url_path: page.url_path.clone(),
171 expected_url_path,
172 })
173 }
174 Some(_) => {}
175 }
176 }
177 chapters_by_number
178 .entry(chapter.chapter_number)
179 .or_default()
180 .push(chapter_ref);
181 }
182 for (chapter_number, chapters) in chapters_by_number {
183 if chapters.len() > 1 {
184 blockers.push(PublishBlocker::DuplicateChapterNumber {
185 chapter_number,
186 chapters: sorted(chapters),
187 });
188 }
189 }
190 let mut modules_by_order_number: BTreeMap<i32, Vec<EntityRef>> = BTreeMap::new();
191 for module in &self.modules {
192 modules_by_order_number
193 .entry(module.order_number)
194 .or_default()
195 .push(self.entity_ref(EntityKey::new(ContentEntityType::Module, module.id)));
196 }
197 for (order_number, modules) in modules_by_order_number {
198 if modules.len() > 1 {
199 blockers.push(PublishBlocker::DuplicateModuleOrderNumber {
200 order_number,
201 modules: sorted(modules),
202 });
203 }
204 }
205 blockers
206 }
207
208 pub fn blockers_added_since(&self, before: &CourseState) -> Vec<PublishBlocker> {
211 let existing: Vec<PublishBlocker> = before
212 .publish_blockers()
213 .into_iter()
214 .map(PublishBlocker::unlabelled)
215 .collect();
216 self.publish_blockers()
217 .into_iter()
218 .filter(|blocker| !existing.contains(&blocker.clone().unlabelled()))
219 .collect()
220 }
221}