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
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}