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