headless_lms_models/library/entity_snapshots/
content_entity.rs1use std::collections::HashMap;
2
3use super::{
4 ChapterSnapshot, ContentEntityType, CourseDefaultReviewConfigSnapshot, CourseSettingsSnapshot,
5 EntityKey, EntitySnapshot, GlossaryTermSnapshot, ModuleSnapshot, PageSnapshot,
6 ReferenceSnapshot, course_settings_of, load_pages,
7};
8use crate::{course_modules, prelude::*};
9
10pub(crate) trait ContentEntity: Clone + PartialEq + Into<EntitySnapshot> {
13 const ENTITY_TYPE: ContentEntityType;
14
15 fn id(&self) -> Uuid;
17
18 fn course_id(&self) -> Uuid;
19
20 fn label(&self) -> &str;
22
23 fn parent(&self) -> Option<EntityKey> {
25 None
26 }
27
28 fn narrow(snapshot: &EntitySnapshot) -> Option<&Self>;
30
31 async fn load_live(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<HashMap<Uuid, Self>>;
33
34 async fn lock_rows(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<()>;
38}
39
40impl ContentEntity for PageSnapshot {
41 const ENTITY_TYPE: ContentEntityType = ContentEntityType::Page;
42
43 fn id(&self) -> Uuid {
44 self.id
45 }
46
47 fn course_id(&self) -> Uuid {
48 self.course_id
49 }
50
51 fn label(&self) -> &str {
52 &self.title
53 }
54
55 fn parent(&self) -> Option<EntityKey> {
56 self.chapter_id
57 .map(|chapter_id| EntityKey::new(ContentEntityType::Chapter, chapter_id))
58 }
59
60 fn narrow(snapshot: &EntitySnapshot) -> Option<&Self> {
61 match snapshot {
62 EntitySnapshot::Page(page) => Some(page),
63 _ => None,
64 }
65 }
66
67 async fn load_live(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<HashMap<Uuid, Self>> {
68 load_pages(conn, ids).await
69 }
70
71 async fn lock_rows(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<()> {
72 if !ids.is_empty() {
73 sqlx::query!(
74 "SELECT id FROM pages WHERE id = ANY($1) ORDER BY id FOR UPDATE",
75 ids
76 )
77 .fetch_all(conn)
78 .await?;
79 }
80 Ok(())
81 }
82}
83
84impl ContentEntity for ChapterSnapshot {
85 const ENTITY_TYPE: ContentEntityType = ContentEntityType::Chapter;
86
87 fn id(&self) -> Uuid {
88 self.id
89 }
90
91 fn course_id(&self) -> Uuid {
92 self.course_id
93 }
94
95 fn label(&self) -> &str {
96 &self.name
97 }
98
99 fn parent(&self) -> Option<EntityKey> {
100 Some(EntityKey::new(
101 ContentEntityType::Module,
102 self.course_module_id,
103 ))
104 }
105
106 fn narrow(snapshot: &EntitySnapshot) -> Option<&Self> {
107 match snapshot {
108 EntitySnapshot::Chapter(chapter) => Some(chapter),
109 _ => None,
110 }
111 }
112
113 async fn load_live(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<HashMap<Uuid, Self>> {
114 if ids.is_empty() {
115 return Ok(HashMap::new());
116 }
117 let chapters = crate::chapters::get_by_ids(conn, ids).await?;
118 Ok(chapters
119 .into_iter()
120 .map(|chapter| (chapter.id, ChapterSnapshot::from(chapter)))
121 .collect())
122 }
123
124 async fn lock_rows(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<()> {
125 if !ids.is_empty() {
126 sqlx::query!(
127 "SELECT id FROM chapters WHERE id = ANY($1) ORDER BY id FOR UPDATE",
128 ids
129 )
130 .fetch_all(conn)
131 .await?;
132 }
133 Ok(())
134 }
135}
136
137impl ContentEntity for ModuleSnapshot {
138 const ENTITY_TYPE: ContentEntityType = ContentEntityType::Module;
139
140 fn id(&self) -> Uuid {
141 self.id
142 }
143
144 fn course_id(&self) -> Uuid {
145 self.course_id
146 }
147
148 fn label(&self) -> &str {
149 self.name.as_deref().unwrap_or_default()
150 }
151
152 fn narrow(snapshot: &EntitySnapshot) -> Option<&Self> {
153 match snapshot {
154 EntitySnapshot::Module(module) => Some(module),
155 _ => None,
156 }
157 }
158
159 async fn load_live(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<HashMap<Uuid, Self>> {
160 if ids.is_empty() {
161 return Ok(HashMap::new());
162 }
163 Ok(course_modules::get_by_ids(conn, ids)
164 .await?
165 .into_iter()
166 .map(|module| (module.id, module.into()))
167 .collect())
168 }
169
170 async fn lock_rows(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<()> {
171 if !ids.is_empty() {
172 sqlx::query!(
173 "SELECT id FROM course_modules WHERE id = ANY($1) ORDER BY id FOR UPDATE",
174 ids
175 )
176 .fetch_all(conn)
177 .await?;
178 }
179 Ok(())
180 }
181}
182
183impl ContentEntity for CourseSettingsSnapshot {
184 const ENTITY_TYPE: ContentEntityType = ContentEntityType::CourseSettings;
185
186 fn id(&self) -> Uuid {
187 self.course_id
188 }
189
190 fn course_id(&self) -> Uuid {
191 self.course_id
192 }
193
194 fn label(&self) -> &str {
195 &self.name
196 }
197
198 fn narrow(snapshot: &EntitySnapshot) -> Option<&Self> {
199 match snapshot {
200 EntitySnapshot::CourseSettings(settings) => Some(settings),
201 _ => None,
202 }
203 }
204
205 async fn load_live(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<HashMap<Uuid, Self>> {
206 let mut settings = HashMap::with_capacity(ids.len());
207 for course in crate::courses::get_by_ids(conn, ids).await? {
208 settings.insert(course.id, course_settings_of(conn, &course).await?);
209 }
210 Ok(settings)
211 }
212
213 async fn lock_rows(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<()> {
214 if !ids.is_empty() {
215 sqlx::query!(
216 "SELECT id FROM courses WHERE id = ANY($1) ORDER BY id FOR UPDATE",
217 ids
218 )
219 .fetch_all(conn)
220 .await?;
221 }
222 Ok(())
223 }
224}
225
226impl ContentEntity for GlossaryTermSnapshot {
227 const ENTITY_TYPE: ContentEntityType = ContentEntityType::GlossaryTerm;
228
229 fn id(&self) -> Uuid {
230 self.id
231 }
232
233 fn course_id(&self) -> Uuid {
234 self.course_id
235 }
236
237 fn label(&self) -> &str {
238 &self.term
239 }
240
241 fn narrow(snapshot: &EntitySnapshot) -> Option<&Self> {
242 match snapshot {
243 EntitySnapshot::GlossaryTerm(term) => Some(term),
244 _ => None,
245 }
246 }
247
248 async fn load_live(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<HashMap<Uuid, Self>> {
249 if ids.is_empty() {
250 return Ok(HashMap::new());
251 }
252 let terms = crate::glossary::get_by_ids(conn, ids).await?;
253 Ok(terms
254 .into_iter()
255 .map(|term| (term.id, GlossaryTermSnapshot::from(term)))
256 .collect())
257 }
258
259 async fn lock_rows(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<()> {
260 if !ids.is_empty() {
261 sqlx::query!(
262 "SELECT id FROM glossary WHERE id = ANY($1) ORDER BY id FOR UPDATE",
263 ids
264 )
265 .fetch_all(conn)
266 .await?;
267 }
268 Ok(())
269 }
270}
271
272impl ContentEntity for ReferenceSnapshot {
273 const ENTITY_TYPE: ContentEntityType = ContentEntityType::Reference;
274
275 fn id(&self) -> Uuid {
276 self.id
277 }
278
279 fn course_id(&self) -> Uuid {
280 self.course_id
281 }
282
283 fn label(&self) -> &str {
284 &self.citation_key
285 }
286
287 fn narrow(snapshot: &EntitySnapshot) -> Option<&Self> {
288 match snapshot {
289 EntitySnapshot::Reference(reference) => Some(reference),
290 _ => None,
291 }
292 }
293
294 async fn load_live(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<HashMap<Uuid, Self>> {
295 if ids.is_empty() {
296 return Ok(HashMap::new());
297 }
298 let references = crate::material_references::get_by_ids(conn, ids).await?;
299 Ok(references
300 .into_iter()
301 .map(|reference| (reference.id, ReferenceSnapshot::from(reference)))
302 .collect())
303 }
304
305 async fn lock_rows(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<()> {
306 if !ids.is_empty() {
307 sqlx::query!(
308 "SELECT id FROM material_references WHERE id = ANY($1) ORDER BY id FOR UPDATE",
309 ids
310 )
311 .fetch_all(conn)
312 .await?;
313 }
314 Ok(())
315 }
316}
317
318impl ContentEntity for CourseDefaultReviewConfigSnapshot {
319 const ENTITY_TYPE: ContentEntityType = ContentEntityType::CourseDefaultReviewConfig;
320
321 fn id(&self) -> Uuid {
322 self.id
323 }
324
325 fn course_id(&self) -> Uuid {
326 self.course_id
327 }
328
329 fn label(&self) -> &str {
330 ""
331 }
332
333 fn narrow(snapshot: &EntitySnapshot) -> Option<&Self> {
334 match snapshot {
335 EntitySnapshot::CourseDefaultReviewConfig(config) => Some(config),
336 _ => None,
337 }
338 }
339
340 async fn load_live(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<HashMap<Uuid, Self>> {
341 if ids.is_empty() {
342 return Ok(HashMap::new());
343 }
344 let configs = crate::peer_or_self_review_configs::get_by_ids(conn, ids).await?;
345 Ok(configs
346 .into_iter()
347 .filter(|config| config.exercise_id.is_none())
348 .map(|config| (config.id, CourseDefaultReviewConfigSnapshot::from(config)))
349 .collect())
350 }
351
352 async fn lock_rows(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<()> {
353 if !ids.is_empty() {
354 sqlx::query!(
355 "SELECT id FROM peer_or_self_review_configs WHERE id = ANY($1) ORDER BY id FOR UPDATE",
356 ids
357 )
358 .fetch_all(conn)
359 .await?;
360 }
361 Ok(())
362 }
363}