headless_lms_models/
page_history.rs

1use std::collections::HashMap;
2
3use serde_json::Value;
4
5use crate::{
6    pages::{CmsPageExercise, CmsPageExerciseSlide, CmsPageExerciseTask},
7    peer_or_self_review_configs::CmsPeerOrSelfReviewConfig,
8    peer_or_self_review_questions::CmsPeerOrSelfReviewQuestion,
9    prelude::*,
10};
11
12#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type)]
13#[cfg_attr(feature = "ts_rs", derive(TS))]
14#[sqlx(type_name = "history_change_reason", rename_all = "kebab-case")]
15pub enum HistoryChangeReason {
16    PageSaved,
17    HistoryRestored,
18    PageDeleted,
19}
20
21#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
22#[cfg_attr(feature = "ts_rs", derive(TS))]
23pub struct PageHistory {
24    pub id: Uuid,
25    pub created_at: DateTime<Utc>,
26    pub title: String,
27    pub content: Value,
28    pub history_change_reason: HistoryChangeReason,
29    pub restored_from_id: Option<Uuid>,
30    pub author_user_id: Uuid,
31    pub page_id: Uuid,
32}
33
34#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
35#[cfg_attr(feature = "ts_rs", derive(TS))]
36pub struct PageHistoryContent {
37    pub content: serde_json::Value,
38    pub exercises: Vec<CmsPageExercise>,
39    pub exercise_slides: Vec<CmsPageExerciseSlide>,
40    pub exercise_tasks: Vec<CmsPageExerciseTask>,
41    pub peer_or_self_review_configs: Vec<CmsPeerOrSelfReviewConfig>,
42    pub peer_or_self_review_questions: Vec<CmsPeerOrSelfReviewQuestion>,
43}
44
45// Batch refactor pushed past the limit
46#[allow(clippy::too_many_arguments)]
47pub async fn insert(
48    conn: &mut PgConnection,
49    pkey_policy: PKeyPolicy<Uuid>,
50    page_id: Uuid,
51    title: &str,
52    content: &PageHistoryContent,
53    history_change_reason: HistoryChangeReason,
54    author_user_id: Uuid,
55    restored_from_id: Option<Uuid>,
56) -> ModelResult<Uuid> {
57    let res = sqlx::query!(
58        "
59INSERT INTO page_history (
60    id,
61    page_id,
62    title,
63    content,
64    history_change_reason,
65    author_user_id,
66    restored_from_id
67  )
68VALUES ($1, $2, $3, $4, $5, $6, $7)
69RETURNING id
70        ",
71        pkey_policy.into_uuid(),
72        page_id,
73        title,
74        serde_json::to_value(content)?,
75        history_change_reason as HistoryChangeReason,
76        author_user_id,
77        restored_from_id
78    )
79    .fetch_one(conn)
80    .await?;
81    Ok(res.id)
82}
83
84pub struct PageHistoryData {
85    pub content: PageHistoryContent,
86    pub title: String,
87    pub exam_id: Option<Uuid>,
88}
89
90pub async fn get_history_data(conn: &mut PgConnection, id: Uuid) -> ModelResult<PageHistoryData> {
91    let record = sqlx::query!(
92        "
93SELECT page_history.content,
94  page_history.title,
95  pages.exam_id
96FROM page_history
97  JOIN pages ON pages.id = page_history.page_id
98WHERE page_history.id = $1
99  AND pages.deleted_at IS NULL
100  AND page_history.deleted_at IS NULL
101        ",
102        id,
103    )
104    .fetch_one(conn)
105    .await?;
106    Ok(PageHistoryData {
107        content: serde_json::from_value(record.content)?,
108        title: record.title,
109        exam_id: record.exam_id,
110    })
111}
112
113pub async fn history(
114    conn: &mut PgConnection,
115    page_id: Uuid,
116    pagination: Pagination,
117) -> ModelResult<Vec<PageHistory>> {
118    let res = sqlx::query_as!(
119        PageHistory,
120        r#"
121SELECT id,
122  title,
123  content,
124  created_at,
125  history_change_reason as "history_change_reason: HistoryChangeReason",
126  restored_from_id,
127  author_user_id,
128  page_id
129FROM page_history
130WHERE page_id = $1
131AND deleted_at IS NULL
132ORDER BY created_at DESC, id
133LIMIT $2
134OFFSET $3
135"#,
136        page_id,
137        pagination.limit(),
138        pagination.offset()
139    )
140    .fetch_all(conn)
141    .await?;
142    Ok(res)
143}
144
145pub async fn history_count(conn: &mut PgConnection, page_id: Uuid) -> ModelResult<i64> {
146    let res = sqlx::query!(
147        "
148SELECT COUNT(*) AS count
149FROM page_history
150WHERE page_id = $1
151AND deleted_at IS NULL
152",
153        page_id
154    )
155    .fetch_one(conn)
156    .await?;
157    Ok(res.count.unwrap_or_default())
158}
159
160pub async fn get_latest_history_entries_for_pages_by_course_ids(
161    conn: &mut PgConnection,
162    course_ids: &[Uuid],
163) -> ModelResult<HashMap<Uuid, PageHistory>> {
164    let res = sqlx::query_as!(
165        PageHistory,
166        r#"
167WITH ranked_history AS (
168  SELECT ph.*,
169    ROW_NUMBER() OVER (
170      PARTITION BY ph.page_id
171      ORDER BY ph.created_at DESC
172    ) AS rn
173  FROM page_history ph
174    JOIN pages p ON p.id = ph.page_id
175  WHERE p.course_id = ANY($1)
176    AND ph.deleted_at IS NULL
177)
178SELECT id,
179  title,
180  content,
181  created_at,
182  history_change_reason AS "history_change_reason: HistoryChangeReason",
183  restored_from_id,
184  author_user_id,
185  page_id
186FROM ranked_history
187WHERE rn = 1
188"#,
189        course_ids
190    )
191    .fetch_all(conn)
192    .await?
193    .into_iter()
194    .fold(HashMap::new(), |mut map, history| {
195        map.insert(history.page_id, history);
196        map
197    });
198
199    Ok(res)
200}