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