1use crate::{exercises::GradingProgress, prelude::*};
2
3#[derive(Clone, Debug, Deserialize, Serialize)]
4
5pub struct UserExerciseSlideState {
6 pub id: Uuid,
7 pub created_at: DateTime<Utc>,
8 pub updated_at: DateTime<Utc>,
9 pub deleted_at: Option<DateTime<Utc>>,
10 pub exercise_slide_id: Uuid,
11 pub user_exercise_state_id: Uuid,
12 pub score_given: Option<f32>,
13 pub grading_progress: GradingProgress,
14}
15
16#[derive(Debug)]
17pub struct UserExerciseSlideStateGradingSummary {
18 pub score_given: Option<f32>,
19 pub grading_progress: GradingProgress,
20}
21
22pub async fn insert(
23 conn: &mut PgConnection,
24 pkey_policy: PKeyPolicy<Uuid>,
25 user_exercise_state_id: Uuid,
26 exercise_slide_id: Uuid,
27) -> ModelResult<Uuid> {
28 let res = sqlx::query!(
29 "
30INSERT INTO user_exercise_slide_states (
31 id,
32 exercise_slide_id,
33 user_exercise_state_id,
34 grading_progress
35 )
36VALUES ($1, $2, $3, $4)
37RETURNING *
38 ",
39 pkey_policy.into_uuid(),
40 exercise_slide_id,
41 user_exercise_state_id,
42 GradingProgress::NotReady as GradingProgress,
43 )
44 .fetch_one(conn)
45 .await?;
46 Ok(res.id)
47}
48
49pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<UserExerciseSlideState> {
50 let res = sqlx::query_as!(
51 UserExerciseSlideState,
52 r#"
53SELECT *
54FROM user_exercise_slide_states
55WHERE id = $1
56 AND deleted_at IS NULL
57 "#,
58 id
59 )
60 .fetch_one(conn)
61 .await?;
62 Ok(res)
63}
64
65pub async fn get_by_unique_index(
66 conn: &mut PgConnection,
67 user_exercise_state_id: Uuid,
68 exercise_slide_id: Uuid,
69) -> ModelResult<Option<UserExerciseSlideState>> {
70 let res = sqlx::query_as!(
71 UserExerciseSlideState,
72 r#"
73SELECT *
74FROM user_exercise_slide_states
75WHERE user_exercise_state_id = $1
76 AND exercise_slide_id = $2
77 AND deleted_at IS NULL
78 "#,
79 user_exercise_state_id,
80 exercise_slide_id,
81 )
82 .fetch_optional(conn)
83 .await?;
84 Ok(res)
85}
86
87pub async fn get_all_by_user_exercise_state_id(
88 conn: &mut PgConnection,
89 user_exercise_state_id: Uuid,
90) -> ModelResult<Vec<UserExerciseSlideState>> {
91 let res = sqlx::query_as!(
92 UserExerciseSlideState,
93 r#"
94SELECT *
95FROM user_exercise_slide_states
96WHERE user_exercise_state_id = $1
97 AND deleted_at IS NULL
98 "#,
99 user_exercise_state_id
100 )
101 .fetch_all(conn)
102 .await?;
103 Ok(res)
104}
105
106pub async fn get_or_insert_by_unique_index(
107 conn: &mut PgConnection,
108 user_exercise_state_id: Uuid,
109 exercise_slide_id: Uuid,
110) -> ModelResult<UserExerciseSlideState> {
111 let user_exercise_slide_state =
112 get_by_unique_index(conn, user_exercise_state_id, exercise_slide_id).await?;
113 if let Some(user_exercise_slide_state) = user_exercise_slide_state {
114 Ok(user_exercise_slide_state)
115 } else {
116 let id = insert(
117 conn,
118 PKeyPolicy::Generate,
119 user_exercise_state_id,
120 exercise_slide_id,
121 )
122 .await?;
123 get_by_id(conn, id).await
124 }
125}
126
127pub async fn get_grading_summary_by_user_exercise_state_id(
128 conn: &mut PgConnection,
129 user_exercise_state_id: Uuid,
130) -> ModelResult<UserExerciseSlideStateGradingSummary> {
131 let res = sqlx::query!(
132 r#"
133SELECT *
134FROM user_exercise_slide_states
135WHERE user_exercise_state_id = $1
136 AND deleted_at IS NULL
137 "#,
138 user_exercise_state_id,
139 )
140 .fetch_all(conn)
141 .await?;
142 let total_score_given = res
143 .iter()
144 .filter_map(|x| x.score_given)
145 .reduce(|acc, next| acc + next);
146 let least_significant_grading_progress = res
147 .iter()
148 .map(|x| x.grading_progress)
149 .min()
150 .unwrap_or(GradingProgress::NotReady);
151 Ok(UserExerciseSlideStateGradingSummary {
152 score_given: total_score_given,
153 grading_progress: least_significant_grading_progress,
154 })
155}
156
157pub async fn update(
158 conn: &mut PgConnection,
159 id: Uuid,
160 score_given: Option<f32>,
161 grading_progress: GradingProgress,
162) -> ModelResult<u64> {
163 let res = sqlx::query!(
164 "
165UPDATE user_exercise_slide_states
166SET score_given = $1,
167 grading_progress = $2
168WHERE id = $3
169 AND deleted_at IS NULL
170 ",
171 score_given,
172 grading_progress as GradingProgress,
173 id,
174 )
175 .execute(conn)
176 .await?;
177 Ok(res.rows_affected())
178}
179
180pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<Uuid> {
181 let res = sqlx::query!(
182 "
183UPDATE user_exercise_slide_states
184SET deleted_at = now()
185WHERE id = $1
186AND deleted_at IS NULL
187RETURNING *
188 ",
189 id
190 )
191 .fetch_one(conn)
192 .await?;
193 Ok(res.id)
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::test_helper::*;
200
201 mod get_grading_summary_by_user_exercise_state_id {
202 use headless_lms_utils::numbers::f32_approx_eq;
203
204 use crate::{
205 chapters, chapters::NewChapter, exercise_slides, exercises, pages,
206 pages::NewCoursePage, user_exercise_states,
207 };
208
209 use super::*;
210
211 #[tokio::test]
212 async fn initial_values() {
213 insert_data!(:tx);
214 let (user_exercise_state_id, slide_1, slide_2, slide_3) =
215 create_test_data(&mut tx).await.unwrap();
216 insert(
217 tx.as_mut(),
218 PKeyPolicy::Generate,
219 user_exercise_state_id,
220 slide_1,
221 )
222 .await
223 .unwrap();
224 insert(
225 tx.as_mut(),
226 PKeyPolicy::Generate,
227 user_exercise_state_id,
228 slide_2,
229 )
230 .await
231 .unwrap();
232 insert(
233 tx.as_mut(),
234 PKeyPolicy::Generate,
235 user_exercise_state_id,
236 slide_3,
237 )
238 .await
239 .unwrap();
240
241 let UserExerciseSlideStateGradingSummary {
242 score_given,
243 grading_progress,
244 } = get_grading_summary_by_user_exercise_state_id(tx.as_mut(), user_exercise_state_id)
245 .await
246 .unwrap();
247 assert_eq!(score_given, None);
248 assert_eq!(grading_progress, GradingProgress::NotReady);
249 }
250
251 #[tokio::test]
252 async fn single_task() {
253 insert_data!(:tx);
254 let (user_exercise_state_id, slide_1, slide_2, slide_3) =
255 create_test_data(&mut tx).await.unwrap();
256 insert(
257 tx.as_mut(),
258 PKeyPolicy::Generate,
259 user_exercise_state_id,
260 slide_1,
261 )
262 .await
263 .unwrap();
264 insert(
265 tx.as_mut(),
266 PKeyPolicy::Generate,
267 user_exercise_state_id,
268 slide_2,
269 )
270 .await
271 .unwrap();
272 let id_3 = insert(
273 tx.as_mut(),
274 PKeyPolicy::Generate,
275 user_exercise_state_id,
276 slide_3,
277 )
278 .await
279 .unwrap();
280 update(tx.as_mut(), id_3, Some(1.0), GradingProgress::FullyGraded)
281 .await
282 .unwrap();
283
284 let UserExerciseSlideStateGradingSummary {
285 score_given,
286 grading_progress,
287 } = get_grading_summary_by_user_exercise_state_id(tx.as_mut(), user_exercise_state_id)
288 .await
289 .unwrap();
290 assert!(f32_approx_eq(score_given.unwrap(), 1.0));
291 assert_eq!(grading_progress, GradingProgress::NotReady);
292 }
293
294 #[tokio::test]
295 async fn all_tasks() {
296 insert_data!(:tx);
297 let (user_exercise_state_id, slide_1, slide_2, slide_3) =
298 create_test_data(&mut tx).await.unwrap();
299 let id_1 = insert(
300 tx.as_mut(),
301 PKeyPolicy::Generate,
302 user_exercise_state_id,
303 slide_1,
304 )
305 .await
306 .unwrap();
307 update(tx.as_mut(), id_1, Some(1.0), GradingProgress::FullyGraded)
308 .await
309 .unwrap();
310 let id_2 = insert(
311 tx.as_mut(),
312 PKeyPolicy::Generate,
313 user_exercise_state_id,
314 slide_2,
315 )
316 .await
317 .unwrap();
318 update(tx.as_mut(), id_2, Some(1.0), GradingProgress::FullyGraded)
319 .await
320 .unwrap();
321 let id_3 = insert(
322 tx.as_mut(),
323 PKeyPolicy::Generate,
324 user_exercise_state_id,
325 slide_3,
326 )
327 .await
328 .unwrap();
329 update(tx.as_mut(), id_3, Some(1.0), GradingProgress::FullyGraded)
330 .await
331 .unwrap();
332
333 let UserExerciseSlideStateGradingSummary {
334 score_given,
335 grading_progress,
336 } = get_grading_summary_by_user_exercise_state_id(tx.as_mut(), user_exercise_state_id)
337 .await
338 .unwrap();
339 assert!(f32_approx_eq(score_given.unwrap(), 3.0));
340 assert_eq!(grading_progress, GradingProgress::FullyGraded);
341 }
342
343 async fn create_test_data(tx: &mut Tx<'_>) -> ModelResult<(Uuid, Uuid, Uuid, Uuid)> {
344 insert_data!(tx: tx; :user, :org, :course, instance: _instance, :course_module);
345 let chapter_id = chapters::insert(
346 tx.as_mut(),
347 PKeyPolicy::Generate,
348 &NewChapter {
349 name: "chapter".to_string(),
350 color: Some("#065853".to_string()),
351 course_id: course,
352 chapter_number: 1,
353 front_page_id: None,
354 opens_at: None,
355 deadline: None,
356 course_module_id: Some(course_module.id),
357 },
358 )
359 .await?;
360
361 let (page_id, _history) = pages::insert_course_page(
362 tx.as_mut(),
363 &NewCoursePage::new(course, 1, "/test", "test"),
364 user,
365 )
366 .await?;
367 let exercise_id = exercises::insert(
368 tx.as_mut(),
369 PKeyPolicy::Generate,
370 course,
371 "course",
372 page_id,
373 chapter_id,
374 1,
375 )
376 .await?;
377 let slide_1 =
378 exercise_slides::insert(tx.as_mut(), PKeyPolicy::Generate, exercise_id, 1).await?;
379 let slide_2 =
380 exercise_slides::insert(tx.as_mut(), PKeyPolicy::Generate, exercise_id, 2).await?;
381 let slide_3 =
382 exercise_slides::insert(tx.as_mut(), PKeyPolicy::Generate, exercise_id, 3).await?;
383 let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
384 tx.as_mut(),
385 user,
386 exercise_id,
387 Some(course),
388 None,
389 )
390 .await?;
391 Ok((user_exercise_state.id, slide_1, slide_2, slide_3))
392 }
393 }
394}