headless_lms_models/
user_exercise_slide_states.rs

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}