Skip to main content

headless_lms_models/
user_exercise_slide_states.rs

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}