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