Skip to main content

headless_lms_models/
page_history.rs

1use std::collections::HashMap;
2
3use serde_json::Value;
4use utoipa::ToSchema;
5
6use crate::{
7    pages::{CmsPageExercise, CmsPageExerciseSlide, CmsPageExerciseTask},
8    peer_or_self_review_configs::CmsPeerOrSelfReviewConfig,
9    peer_or_self_review_questions::CmsPeerOrSelfReviewQuestion,
10    prelude::*,
11};
12
13#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type, ToSchema)]
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, ToSchema)]
22
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
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 get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<PageHistory> {
114    let res = sqlx::query_as!(
115        PageHistory,
116        r#"
117SELECT id,
118  title,
119  content,
120  created_at,
121  history_change_reason as "history_change_reason: HistoryChangeReason",
122  restored_from_id,
123  author_user_id,
124  page_id
125FROM page_history
126WHERE id = $1
127  AND deleted_at IS NULL
128        "#,
129        id
130    )
131    .fetch_one(conn)
132    .await?;
133    Ok(res)
134}
135
136pub async fn history(
137    conn: &mut PgConnection,
138    page_id: Uuid,
139    pagination: Pagination,
140) -> ModelResult<Vec<PageHistory>> {
141    let res = sqlx::query_as!(
142        PageHistory,
143        r#"
144SELECT id,
145  title,
146  content,
147  created_at,
148  history_change_reason as "history_change_reason: HistoryChangeReason",
149  restored_from_id,
150  author_user_id,
151  page_id
152FROM page_history
153WHERE page_id = $1
154AND deleted_at IS NULL
155ORDER BY created_at DESC, id
156LIMIT $2
157OFFSET $3
158"#,
159        page_id,
160        pagination.limit(),
161        pagination.offset()
162    )
163    .fetch_all(conn)
164    .await?;
165    Ok(res)
166}
167
168pub async fn history_count(conn: &mut PgConnection, page_id: Uuid) -> ModelResult<i64> {
169    let res = sqlx::query!(
170        "
171SELECT COUNT(*) AS count
172FROM page_history
173WHERE page_id = $1
174AND deleted_at IS NULL
175",
176        page_id
177    )
178    .fetch_one(conn)
179    .await?;
180    Ok(res.count.unwrap_or_default())
181}
182
183/// Latest non-deleted `page_history` row id per page for pages in the given courses.
184pub async fn get_latest_page_history_ids_by_course_ids(
185    conn: &mut PgConnection,
186    course_ids: &[Uuid],
187) -> ModelResult<HashMap<Uuid, Uuid>> {
188    let rows = sqlx::query!(
189        r#"
190SELECT DISTINCT ON (ph.page_id)
191  ph.id,
192  ph.page_id
193FROM page_history ph
194  INNER JOIN pages p ON p.id = ph.page_id
195WHERE p.course_id = ANY($1)
196  AND ph.deleted_at IS NULL
197ORDER BY ph.page_id,
198  ph.created_at DESC,
199  ph.id DESC
200"#,
201        course_ids
202    )
203    .fetch_all(conn)
204    .await?;
205
206    Ok(rows.into_iter().map(|row| (row.page_id, row.id)).collect())
207}