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