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#[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(&mut *conn)
82 .await?;
83 let spec_files: Vec<Uuid> = content
87 .exercise_tasks
88 .iter()
89 .flat_map(|task| task.private_spec_files.iter().copied())
90 .collect();
91 crate::page_history_spec_files::insert_many(conn, res.id, &spec_files).await?;
92 Ok(res.id)
93}
94
95pub struct PageHistoryData {
96 pub content: PageHistoryContent,
97 pub title: String,
98 pub exam_id: Option<Uuid>,
99}
100
101pub async fn get_history_data(conn: &mut PgConnection, id: Uuid) -> ModelResult<PageHistoryData> {
102 let record = sqlx::query!(
103 "
104SELECT page_history.content,
105 page_history.title,
106 pages.exam_id
107FROM page_history
108 JOIN pages ON pages.id = page_history.page_id
109WHERE page_history.id = $1
110 AND pages.deleted_at IS NULL
111 AND page_history.deleted_at IS NULL
112 ",
113 id,
114 )
115 .fetch_one(conn)
116 .await?;
117 Ok(PageHistoryData {
118 content: serde_json::from_value(record.content)?,
119 title: record.title,
120 exam_id: record.exam_id,
121 })
122}
123
124pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<PageHistory> {
125 let res = sqlx::query_as!(
126 PageHistory,
127 r#"
128SELECT *
129FROM page_history
130WHERE id = $1
131 AND deleted_at IS NULL
132 "#,
133 id
134 )
135 .fetch_one(conn)
136 .await?;
137 Ok(res)
138}
139
140pub async fn history(
141 conn: &mut PgConnection,
142 page_id: Uuid,
143 pagination: Pagination,
144) -> ModelResult<Vec<PageHistory>> {
145 let res = sqlx::query_as!(
146 PageHistory,
147 r#"
148SELECT *
149FROM page_history
150WHERE page_id = $1
151AND deleted_at IS NULL
152ORDER BY created_at DESC, id
153LIMIT $2
154OFFSET $3
155"#,
156 page_id,
157 pagination.limit(),
158 pagination.offset()
159 )
160 .fetch_all(conn)
161 .await?;
162 Ok(res)
163}
164
165pub async fn history_count(conn: &mut PgConnection, page_id: Uuid) -> ModelResult<i64> {
166 let res = sqlx::query!(
167 "
168SELECT COUNT(*) AS count
169FROM page_history
170WHERE page_id = $1
171AND deleted_at IS NULL
172",
173 page_id
174 )
175 .fetch_one(conn)
176 .await?;
177 Ok(res.count.unwrap_or_default())
178}
179
180pub async fn get_latest_page_history_ids_by_course_ids(
182 conn: &mut PgConnection,
183 course_ids: &[Uuid],
184) -> ModelResult<HashMap<Uuid, Uuid>> {
185 let rows = sqlx::query!(
186 r#"
187SELECT DISTINCT ON (ph.page_id)
188 ph.id,
189 ph.page_id
190FROM page_history ph
191 INNER JOIN pages p ON p.id = ph.page_id
192WHERE p.course_id = ANY($1)
193 AND ph.deleted_at IS NULL
194ORDER BY ph.page_id,
195 ph.created_at DESC,
196 ph.id DESC
197"#,
198 course_ids
199 )
200 .fetch_all(conn)
201 .await?;
202
203 Ok(rows.into_iter().map(|row| (row.page_id, row.id)).collect())
204}