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
208RETURNING id
209 ",
210 id
211 )
212 .fetch_one(conn)
213 .await?;
214 Ok(res.id)
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::test_helper::*;
221
222 mod get_grading_summary_by_user_exercise_state_id {
223 use headless_lms_utils::numbers::f32_approx_eq;
224
225 use crate::{
226 chapters, chapters::NewChapter, exercise_slides, exercises, pages,
227 pages::NewCoursePage, user_exercise_states,
228 };
229
230 use super::*;
231
232 #[tokio::test]
233 async fn initial_values() {
234 insert_data!(:tx);
235 let (user_exercise_state_id, slide_1, slide_2, slide_3) =
236 create_test_data(&mut tx).await.unwrap();
237 insert(
238 tx.as_mut(),
239 PKeyPolicy::Generate,
240 user_exercise_state_id,
241 slide_1,
242 )
243 .await
244 .unwrap();
245 insert(
246 tx.as_mut(),
247 PKeyPolicy::Generate,
248 user_exercise_state_id,
249 slide_2,
250 )
251 .await
252 .unwrap();
253 insert(
254 tx.as_mut(),
255 PKeyPolicy::Generate,
256 user_exercise_state_id,
257 slide_3,
258 )
259 .await
260 .unwrap();
261
262 let UserExerciseSlideStateGradingSummary {
263 score_given,
264 grading_progress,
265 } = get_grading_summary_by_user_exercise_state_id(tx.as_mut(), user_exercise_state_id)
266 .await
267 .unwrap();
268 assert_eq!(score_given, None);
269 assert_eq!(grading_progress, GradingProgress::NotReady);
270 }
271
272 #[tokio::test]
273 async fn single_task() {
274 insert_data!(:tx);
275 let (user_exercise_state_id, slide_1, slide_2, slide_3) =
276 create_test_data(&mut tx).await.unwrap();
277 insert(
278 tx.as_mut(),
279 PKeyPolicy::Generate,
280 user_exercise_state_id,
281 slide_1,
282 )
283 .await
284 .unwrap();
285 insert(
286 tx.as_mut(),
287 PKeyPolicy::Generate,
288 user_exercise_state_id,
289 slide_2,
290 )
291 .await
292 .unwrap();
293 let id_3 = insert(
294 tx.as_mut(),
295 PKeyPolicy::Generate,
296 user_exercise_state_id,
297 slide_3,
298 )
299 .await
300 .unwrap();
301 update(tx.as_mut(), id_3, Some(1.0), GradingProgress::FullyGraded)
302 .await
303 .unwrap();
304
305 let UserExerciseSlideStateGradingSummary {
306 score_given,
307 grading_progress,
308 } = get_grading_summary_by_user_exercise_state_id(tx.as_mut(), user_exercise_state_id)
309 .await
310 .unwrap();
311 assert!(f32_approx_eq(score_given.unwrap(), 1.0));
312 assert_eq!(grading_progress, GradingProgress::NotReady);
313 }
314
315 #[tokio::test]
316 async fn all_tasks() {
317 insert_data!(:tx);
318 let (user_exercise_state_id, slide_1, slide_2, slide_3) =
319 create_test_data(&mut tx).await.unwrap();
320 let id_1 = insert(
321 tx.as_mut(),
322 PKeyPolicy::Generate,
323 user_exercise_state_id,
324 slide_1,
325 )
326 .await
327 .unwrap();
328 update(tx.as_mut(), id_1, Some(1.0), GradingProgress::FullyGraded)
329 .await
330 .unwrap();
331 let id_2 = insert(
332 tx.as_mut(),
333 PKeyPolicy::Generate,
334 user_exercise_state_id,
335 slide_2,
336 )
337 .await
338 .unwrap();
339 update(tx.as_mut(), id_2, Some(1.0), GradingProgress::FullyGraded)
340 .await
341 .unwrap();
342 let id_3 = insert(
343 tx.as_mut(),
344 PKeyPolicy::Generate,
345 user_exercise_state_id,
346 slide_3,
347 )
348 .await
349 .unwrap();
350 update(tx.as_mut(), id_3, Some(1.0), GradingProgress::FullyGraded)
351 .await
352 .unwrap();
353
354 let UserExerciseSlideStateGradingSummary {
355 score_given,
356 grading_progress,
357 } = get_grading_summary_by_user_exercise_state_id(tx.as_mut(), user_exercise_state_id)
358 .await
359 .unwrap();
360 assert!(f32_approx_eq(score_given.unwrap(), 3.0));
361 assert_eq!(grading_progress, GradingProgress::FullyGraded);
362 }
363
364 async fn create_test_data(tx: &mut Tx<'_>) -> ModelResult<(Uuid, Uuid, Uuid, Uuid)> {
365 insert_data!(tx: tx; :user, :org, :course, :instance, :course_module);
366 let chapter_id = chapters::insert(
367 tx.as_mut(),
368 PKeyPolicy::Generate,
369 &NewChapter {
370 name: "chapter".to_string(),
371 color: Some("#065853".to_string()),
372 course_id: course,
373 chapter_number: 1,
374 front_page_id: None,
375 opens_at: None,
376 deadline: None,
377 course_module_id: Some(course_module.id),
378 },
379 )
380 .await?;
381
382 let (page_id, _history) = pages::insert_course_page(
383 tx.as_mut(),
384 &NewCoursePage::new(course, 1, "/test", "test"),
385 user,
386 )
387 .await?;
388 let exercise_id = exercises::insert(
389 tx.as_mut(),
390 PKeyPolicy::Generate,
391 course,
392 "course",
393 page_id,
394 chapter_id,
395 1,
396 )
397 .await?;
398 let slide_1 =
399 exercise_slides::insert(tx.as_mut(), PKeyPolicy::Generate, exercise_id, 1).await?;
400 let slide_2 =
401 exercise_slides::insert(tx.as_mut(), PKeyPolicy::Generate, exercise_id, 2).await?;
402 let slide_3 =
403 exercise_slides::insert(tx.as_mut(), PKeyPolicy::Generate, exercise_id, 3).await?;
404 let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
405 tx.as_mut(),
406 user,
407 exercise_id,
408 Some(instance.id),
409 None,
410 )
411 .await?;
412 Ok((user_exercise_state.id, slide_1, slide_2, slide_3))
413 }
414 }
415}