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