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#[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}