1use std::collections::HashMap;
2
3use serde_json::Value;
4use utoipa::ToSchema;
5
6use crate::{
7 entity_history::HistoryAuthor,
8 pages::{CmsPageExercise, CmsPageExerciseSlide, CmsPageExerciseTask},
9 peer_or_self_review_configs::CmsPeerOrSelfReviewConfig,
10 peer_or_self_review_questions::CmsPeerOrSelfReviewQuestion,
11 prelude::*,
12};
13
14#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type, ToSchema)]
15#[sqlx(type_name = "history_change_reason", rename_all = "kebab-case")]
16pub enum HistoryChangeReason {
17 PageSaved,
18 HistoryRestored,
19 PageDeleted,
20}
21
22#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
23
24pub struct PageHistory {
25 pub id: Uuid,
26 pub created_at: DateTime<Utc>,
27 pub updated_at: DateTime<Utc>,
28 pub deleted_at: Option<DateTime<Utc>>,
29 pub title: String,
30 pub content: Value,
31 pub history_change_reason: HistoryChangeReason,
32 pub restored_from_id: Option<Uuid>,
33 pub author_user_id: Uuid,
34 pub page_id: Uuid,
35 pub agent_client_name: Option<String>,
37}
38
39#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
40
41pub struct PageHistoryContent {
42 pub content: serde_json::Value,
43 pub exercises: Vec<CmsPageExercise>,
44 pub exercise_slides: Vec<CmsPageExerciseSlide>,
45 pub exercise_tasks: Vec<CmsPageExerciseTask>,
46 pub peer_or_self_review_configs: Vec<CmsPeerOrSelfReviewConfig>,
47 pub peer_or_self_review_questions: Vec<CmsPeerOrSelfReviewQuestion>,
48}
49
50#[allow(clippy::too_many_arguments)]
52pub async fn insert(
53 conn: &mut PgConnection,
54 pkey_policy: PKeyPolicy<Uuid>,
55 page_id: Uuid,
56 title: &str,
57 content: &PageHistoryContent,
58 history_change_reason: HistoryChangeReason,
59 author: HistoryAuthor,
60 restored_from_id: Option<Uuid>,
61) -> ModelResult<Uuid> {
62 let res = sqlx::query!(
63 "
64INSERT INTO page_history (
65 id,
66 page_id,
67 title,
68 content,
69 history_change_reason,
70 author_user_id,
71 restored_from_id,
72 agent_call_id
73 )
74VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
75RETURNING id
76 ",
77 pkey_policy.into_uuid(),
78 page_id,
79 title,
80 serde_json::to_value(content)?,
81 history_change_reason as HistoryChangeReason,
82 author.user_id,
83 restored_from_id,
84 author.agent_call_id,
85 )
86 .fetch_one(&mut *conn)
87 .await?;
88 let spec_files: Vec<Uuid> = content
92 .exercise_tasks
93 .iter()
94 .flat_map(|task| task.private_spec_files.iter().copied())
95 .collect();
96 crate::page_history_spec_files::insert_many(conn, res.id, &spec_files).await?;
97 Ok(res.id)
98}
99
100pub struct PageHistoryData {
101 pub content: PageHistoryContent,
102 pub title: String,
103 pub exam_id: Option<Uuid>,
104}
105
106pub async fn get_history_data(conn: &mut PgConnection, id: Uuid) -> ModelResult<PageHistoryData> {
107 let record = sqlx::query!(
108 "
109SELECT page_history.content,
110 page_history.title,
111 pages.exam_id
112FROM page_history
113 JOIN pages ON pages.id = page_history.page_id
114WHERE page_history.id = $1
115 AND pages.deleted_at IS NULL
116 AND page_history.deleted_at IS NULL
117 ",
118 id,
119 )
120 .fetch_one(conn)
121 .await?;
122 Ok(PageHistoryData {
123 content: serde_json::from_value(record.content)?,
124 title: record.title,
125 exam_id: record.exam_id,
126 })
127}
128
129pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<PageHistory> {
130 let res = sqlx::query_as!(
131 PageHistory,
132 r#"
133SELECT h.id AS "id!",
134 h.created_at AS "created_at!",
135 h.updated_at AS "updated_at!",
136 h.deleted_at,
137 h.title AS "title!",
138 h.content AS "content!",
139 h.history_change_reason AS "history_change_reason!",
140 h.restored_from_id,
141 h.author_user_id AS "author_user_id!",
142 h.page_id AS "page_id!",
143 c.client_name AS "agent_client_name?"
144FROM page_history h
145 LEFT JOIN agent_changeset_calls c ON c.id = h.agent_call_id
146WHERE h.id = $1
147 AND h.deleted_at IS NULL
148 "#,
149 id
150 )
151 .fetch_one(conn)
152 .await?;
153 Ok(res)
154}
155
156pub async fn history(
157 conn: &mut PgConnection,
158 page_id: Uuid,
159 pagination: Pagination,
160) -> ModelResult<Vec<PageHistory>> {
161 let res = sqlx::query_as!(
162 PageHistory,
163 r#"
164SELECT h.id AS "id!",
165 h.created_at AS "created_at!",
166 h.updated_at AS "updated_at!",
167 h.deleted_at,
168 h.title AS "title!",
169 h.content AS "content!",
170 h.history_change_reason AS "history_change_reason!",
171 h.restored_from_id,
172 h.author_user_id AS "author_user_id!",
173 h.page_id AS "page_id!",
174 c.client_name AS "agent_client_name?"
175FROM page_history h
176 LEFT JOIN agent_changeset_calls c ON c.id = h.agent_call_id
177WHERE h.page_id = $1
178 AND h.deleted_at IS NULL
179ORDER BY h.created_at DESC,
180 h.id
181LIMIT $2 OFFSET $3
182"#,
183 page_id,
184 pagination.limit(),
185 pagination.offset()
186 )
187 .fetch_all(conn)
188 .await?;
189 Ok(res)
190}
191
192pub async fn history_count(conn: &mut PgConnection, page_id: Uuid) -> ModelResult<i64> {
193 let res = sqlx::query!(
194 "
195SELECT COUNT(*) AS count
196FROM page_history
197WHERE page_id = $1
198AND deleted_at IS NULL
199",
200 page_id
201 )
202 .fetch_one(conn)
203 .await?;
204 Ok(res.count.unwrap_or_default())
205}
206
207pub async fn get_latest_page_history_ids_by_course_ids(
209 conn: &mut PgConnection,
210 course_ids: &[Uuid],
211) -> ModelResult<HashMap<Uuid, Uuid>> {
212 let rows = sqlx::query!(
213 r#"
214SELECT DISTINCT ON (ph.page_id)
215 ph.id,
216 ph.page_id
217FROM page_history ph
218 INNER JOIN pages p ON p.id = ph.page_id
219WHERE p.course_id = ANY($1)
220 AND ph.deleted_at IS NULL
221ORDER BY ph.page_id,
222 ph.created_at DESC,
223 ph.id DESC
224"#,
225 course_ids
226 )
227 .fetch_all(conn)
228 .await?;
229
230 Ok(rows.into_iter().map(|row| (row.page_id, row.id)).collect())
231}