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(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
171pub 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}