Skip to main content

headless_lms_models/library/agent_changesets/course_state/
view.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2
3use super::{CourseParts, CourseState, ExerciseRecord, load_live_course};
4use crate::{
5    agent_changeset_entities::{self, AgentChangesetEntity},
6    agent_changesets::{self, AgentChangeset},
7    courses::Course,
8    library::entity_snapshots::{
9        self, ContentEntity, ContentEntityType, EntityKey, PageSnapshot, exercise_bundles,
10    },
11    prelude::*,
12};
13
14/// The published course and the user's staged view of it.
15#[derive(Debug, Clone)]
16pub struct CourseView {
17    pub course: Course,
18    pub live: CourseState,
19    pub staged: CourseState,
20    pub changeset: Option<AgentChangeset>,
21    pub staged_entities: BTreeMap<EntityKey, AgentChangesetEntity>,
22}
23
24impl CourseView {
25    /// The changeset's row for the entity; `None` if it isn't staged.
26    pub fn staged_row(&self, key: EntityKey) -> Option<&AgentChangesetEntity> {
27        self.staged_entities.get(&key)
28    }
29
30    /// The page holding the exercise as the user sees it, else the one its live record names.
31    pub fn exercise_page_id(&self, exercise_id: Uuid) -> Option<Uuid> {
32        self.staged.exercise_page_id(exercise_id).or_else(|| {
33            self.live
34                .exercise(exercise_id)
35                .map(|exercise| exercise.page_id)
36        })
37    }
38
39    /// The page's full staged snapshot: `Some(None)` when the changeset deletes it, `None` when
40    /// the changeset doesn't have it.
41    pub fn staged_page_snapshot(&self, page_id: Uuid) -> Option<Option<&PageSnapshot>> {
42        let row = self.staged_row(EntityKey::new(ContentEntityType::Page, page_id))?;
43        Some(row.after_snapshot.as_ref().and_then(PageSnapshot::narrow))
44    }
45
46    /// The pages' full snapshots as the user sees them, exercises included, by id; the pages
47    /// that don't exist for the user are left out.
48    pub async fn current_page_snapshots(
49        &self,
50        conn: &mut PgConnection,
51        page_ids: &[Uuid],
52    ) -> ModelResult<HashMap<Uuid, PageSnapshot>> {
53        let mut snapshots = HashMap::new();
54        let mut unstaged = Vec::new();
55        for page_id in page_ids {
56            match self.staged_page_snapshot(*page_id) {
57                Some(staged) => {
58                    if let Some(page) = staged {
59                        snapshots.insert(*page_id, page.clone());
60                    }
61                }
62                None => unstaged.push(*page_id),
63            }
64        }
65        snapshots.extend(entity_snapshots::load_pages(conn, &unstaged).await?);
66        Ok(snapshots)
67    }
68
69    /// Loads the exercise records of both states, unless they are loaded already. The view must
70    /// hold every part of the course.
71    pub async fn load_exercises(&mut self, conn: &mut PgConnection) -> ModelResult<()> {
72        let page_ids: Vec<Uuid> = self.live.pages.iter().map(|page| page.id).collect();
73        self.load_exercise_records(conn, &page_ids).await
74    }
75
76    /// Loads into both states the records of the exercises on `page_ids`, staged or published,
77    /// and maybe others; does nothing if records are already loaded. The view must hold every
78    /// part of the course.
79    pub async fn load_exercises_on_pages(
80        &mut self,
81        conn: &mut PgConnection,
82        page_ids: &[Uuid],
83    ) -> ModelResult<()> {
84        if self.live.exercises.is_some() {
85            return Ok(());
86        }
87        let live_pages = self.live.exercise_page_ids();
88        let mut pages: HashSet<Uuid> = page_ids.iter().copied().collect();
89        let staged_exercises = page_ids
90            .iter()
91            .filter_map(|page_id| self.staged.page(*page_id))
92            .flat_map(|page| page.content.iter().filter_map(|block| block.exercise_id()));
93        pages.extend(staged_exercises.filter_map(|exercise_id| live_pages.get(&exercise_id)));
94        let pages: Vec<Uuid> = pages.into_iter().collect();
95        self.load_exercise_records(conn, &pages).await
96    }
97
98    /// Loads the records of the live pages `page_ids` into both states, then lays the staged
99    /// pages' over the staged state.
100    async fn load_exercise_records(
101        &mut self,
102        conn: &mut PgConnection,
103        page_ids: &[Uuid],
104    ) -> ModelResult<()> {
105        if self.live.exercises.is_some() {
106            return Ok(());
107        }
108        let records = load_exercise_records(conn, page_ids).await?;
109        let staged_pages: HashMap<Uuid, Option<&PageSnapshot>> = self
110            .staged_entities
111            .values()
112            .filter(|row| row.entity_type == ContentEntityType::Page)
113            .map(|row| {
114                let page = row.after_snapshot.as_ref().and_then(PageSnapshot::narrow);
115                (row.entity_id, page)
116            })
117            .collect();
118        let mut staged = records.clone();
119        staged.retain(|_, exercise| !staged_pages.contains_key(&exercise.page_id));
120        for exercise in staged_pages
121            .values()
122            .flatten()
123            .flat_map(|page| ExerciseRecord::of_page(page))
124        {
125            staged.insert(exercise.id, exercise);
126        }
127        self.live.exercises = Some(records);
128        self.staged.exercises = Some(staged);
129        Ok(())
130    }
131}
132
133/// The records of the live pages' exercises, by exercise id.
134async fn load_exercise_records(
135    conn: &mut PgConnection,
136    page_ids: &[Uuid],
137) -> ModelResult<BTreeMap<Uuid, ExerciseRecord>> {
138    let mut records = BTreeMap::new();
139    for (page_id, exercises) in crate::pages::exercises_of_pages(conn, page_ids).await? {
140        for bundle in exercise_bundles(
141            &exercises.exercises,
142            &exercises.exercise_slides,
143            &exercises.exercise_tasks,
144        ) {
145            records.insert(
146                bundle.exercise.id,
147                ExerciseRecord::of_bundle(page_id, &bundle),
148            );
149        }
150    }
151    Ok(records)
152}
153
154/// Loads the parts of the published course and the user's open changeset for it.
155pub async fn load_view(
156    conn: &mut PgConnection,
157    user_id: Uuid,
158    course: Course,
159    parts: CourseParts,
160) -> ModelResult<CourseView> {
161    let live = load_live_course(conn, &course, parts).await?;
162    let changeset = agent_changesets::get_open(conn, user_id, course.id).await?;
163    let staged_entities: BTreeMap<EntityKey, AgentChangesetEntity> = match &changeset {
164        Some(changeset) => agent_changeset_entities::get_by_changeset_id(conn, changeset.id)
165            .await?
166            .into_iter()
167            .map(|row| (row.key(), row))
168            .collect(),
169        None => BTreeMap::new(),
170    };
171    let staged = live.overlay(staged_entities.values(), parts);
172    Ok(CourseView {
173        course,
174        live,
175        staged,
176        changeset,
177        staged_entities,
178    })
179}