Skip to main content

headless_lms_models/library/entity_snapshots/
loading.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use super::{
5    ChapterSnapshot, ContentEntity, ContentEntityType, CourseAudienceSnapshot,
6    CourseDefaultReviewConfigSnapshot, CourseSettingsSnapshot, EntityKey, EntitySnapshot,
7    GlossaryTermSnapshot, ModuleSnapshot, PageSnapshot, ReferenceSnapshot, with_snapshot_type,
8};
9use crate::{course_modules, pages::PageVisibility, prelude::*};
10
11/// The course's default review config, if it has one.
12pub async fn load_course_default_review_config(
13    conn: &mut PgConnection,
14    course_id: Uuid,
15) -> ModelResult<Option<CourseDefaultReviewConfigSnapshot>> {
16    let config =
17        crate::peer_or_self_review_configs::get_default_for_course_by_course_id(conn, course_id)
18            .await
19            .optional()?;
20    Ok(config.map(CourseDefaultReviewConfigSnapshot::from))
21}
22
23/// The live state of each entity, `None` for one that does not exist or is deleted. Pages that
24/// belong to an exam are treated as not existing: changesets only hold course content.
25pub async fn load_live_many(
26    conn: &mut PgConnection,
27    keys: &[EntityKey],
28) -> ModelResult<HashMap<EntityKey, Option<EntitySnapshot>>> {
29    let mut snapshots: HashMap<EntityKey, Option<EntitySnapshot>> =
30        keys.iter().map(|key| (*key, None)).collect();
31    let mut ids_by_type: HashMap<ContentEntityType, Vec<Uuid>> = HashMap::new();
32    for EntityKey {
33        entity_type,
34        entity_id,
35    } in snapshots.keys()
36    {
37        ids_by_type
38            .entry(*entity_type)
39            .or_default()
40            .push(*entity_id);
41    }
42    for (entity_type, ids) in ids_by_type {
43        with_snapshot_type!(entity_type, T => {
44            for (id, snapshot) in T::load_live(conn, &ids).await? {
45                snapshots.insert(EntityKey::new(entity_type, id), Some(snapshot.into()));
46            }
47        });
48    }
49    Ok(snapshots)
50}
51
52/// [`load_live_many`] of one entity.
53pub async fn load_live(
54    conn: &mut PgConnection,
55    key: EntityKey,
56) -> ModelResult<Option<EntitySnapshot>> {
57    Ok(load_live_many(conn, &[key]).await?.remove(&key).flatten())
58}
59
60/// The live page, as [`load_live_many`] has it.
61pub async fn load_page(
62    conn: &mut PgConnection,
63    page_id: Uuid,
64) -> ModelResult<Option<PageSnapshot>> {
65    Ok(load_pages(conn, &[page_id]).await?.remove(&page_id))
66}
67
68/// The live pages, as [`load_live_many`] has them, by id; the pages that don't exist are left out.
69pub async fn load_pages(
70    conn: &mut PgConnection,
71    page_ids: &[Uuid],
72) -> ModelResult<HashMap<Uuid, PageSnapshot>> {
73    if page_ids.is_empty() {
74        return Ok(HashMap::new());
75    }
76    let pages: Vec<_> =
77        crate::pages::get_by_ids_and_visibility(conn, page_ids, PageVisibility::Any)
78            .await?
79            .into_iter()
80            .filter(|page| page.exam_id.is_none())
81            .filter_map(|page| Some((page.course_id?, page)))
82            .collect();
83    let mut exercises = crate::pages::exercises_of_pages(
84        conn,
85        &pages.iter().map(|(_, page)| page.id).collect::<Vec<_>>(),
86    )
87    .await?;
88    let mut snapshots = HashMap::with_capacity(pages.len());
89    for (course_id, page) in pages {
90        let page_exercises = exercises.remove(&page.id).unwrap_or_default();
91        let mut snapshot = PageSnapshot {
92            id: page.id,
93            course_id,
94            chapter_id: page.chapter_id,
95            url_path: page.url_path,
96            title: page.title,
97            hidden: page.hidden,
98            order_number: page.order_number,
99            content: Arc::new(serde_json::from_value(page.content)?),
100            exercises: page_exercises.exercises,
101            exercise_slides: page_exercises.exercise_slides,
102            exercise_tasks: page_exercises.exercise_tasks,
103            joins_page_language_group_id: None,
104        };
105        snapshot.sort();
106        snapshots.insert(snapshot.id, snapshot);
107    }
108    Ok(snapshots)
109}
110
111impl From<course_modules::CourseModule> for ModuleSnapshot {
112    fn from(module: course_modules::CourseModule) -> Self {
113        Self {
114            id: module.id,
115            course_id: module.course_id,
116            name: module.name,
117            order_number: module.order_number,
118            completion_policy: module.completion_policy,
119            certification_enabled: module.certification_enabled,
120            uh_course_code: module.uh_course_code,
121            ects_credits: module.ects_credits,
122            completion_registration_link_override: module.completion_registration_link_override,
123            completions_can_be_registered_to_sisu: module.completions_can_be_registered_to_sisu,
124        }
125    }
126}
127
128impl From<crate::chapters::DatabaseChapter> for ChapterSnapshot {
129    fn from(chapter: crate::chapters::DatabaseChapter) -> Self {
130        Self {
131            id: chapter.id,
132            course_id: chapter.course_id,
133            course_module_id: chapter.course_module_id,
134            name: chapter.name,
135            color: chapter.color,
136            chapter_number: chapter.chapter_number,
137            front_page_id: chapter.front_page_id,
138            opens_at: chapter.opens_at,
139            deadline: chapter.deadline,
140            chapter_image_path: chapter.chapter_image_path,
141        }
142    }
143}
144
145/// The settings of a fetched course; only its audiences are read.
146pub async fn course_settings_of(
147    conn: &mut PgConnection,
148    course: &crate::courses::Course,
149) -> ModelResult<CourseSettingsSnapshot> {
150    let mut audiences: Vec<CourseAudienceSnapshot> =
151        crate::course_audiences::get_edit_course_audiences_by_course_id(conn, course.id)
152            .await?
153            .into_iter()
154            .map(|audience| CourseAudienceSnapshot {
155                id: audience.id,
156                audience: audience.audience,
157            })
158            .collect();
159    audiences.sort_by_key(|audience| audience.id);
160    Ok(CourseSettingsSnapshot {
161        course_id: course.id,
162        name: course.name.clone(),
163        description: course.description.clone(),
164        is_draft: course.is_draft,
165        is_test_mode: course.is_test_mode,
166        is_unlisted: course.is_unlisted,
167        can_add_chatbot: course.can_add_chatbot,
168        is_joinable_by_code_only: course.is_joinable_by_code_only,
169        ask_marketing_consent: course.ask_marketing_consent,
170        flagged_answers_threshold: course.flagged_answers_threshold,
171        flagged_answers_skip_manual_review_and_allow_retry: course
172            .flagged_answers_skip_manual_review_and_allow_retry,
173        closed_at: course.closed_at,
174        closed_additional_message: course.closed_additional_message.clone(),
175        closed_course_successor_id: course.closed_course_successor_id,
176        chapter_locking_enabled: course.chapter_locking_enabled,
177        ai_policy: course.ai_policy,
178        course_material_ai_instructions: course.course_material_ai_instructions,
179        base_module_completion_requires_n_submodule_completions: course
180            .base_module_completion_requires_n_submodule_completions,
181        audiences,
182    })
183}
184
185impl From<crate::glossary::Term> for GlossaryTermSnapshot {
186    fn from(term: crate::glossary::Term) -> Self {
187        Self {
188            id: term.id,
189            course_id: term.course_id,
190            term: term.term,
191            definition: term.definition,
192        }
193    }
194}
195
196impl From<crate::material_references::MaterialReference> for ReferenceSnapshot {
197    fn from(reference: crate::material_references::MaterialReference) -> Self {
198        Self {
199            id: reference.id,
200            course_id: reference.course_id,
201            citation_key: reference.citation_key,
202            reference: reference.reference,
203        }
204    }
205}
206
207impl From<crate::peer_or_self_review_configs::PeerOrSelfReviewConfig>
208    for CourseDefaultReviewConfigSnapshot
209{
210    fn from(config: crate::peer_or_self_review_configs::PeerOrSelfReviewConfig) -> Self {
211        Self {
212            id: config.id,
213            course_id: config.course_id,
214            review_instructions: config.review_instructions,
215        }
216    }
217}