1use crate::{
2 exercise_task_gradings::{ExerciseTaskGrading, UserPointsUpdateStrategy},
3 exercises::{ActivityProgress, GradingProgress},
4 prelude::*,
5};
6
7#[derive(Clone, Debug, Deserialize, Serialize)]
8
9pub struct UserExerciseTaskState {
10 pub exercise_task_id: Uuid,
11 pub user_exercise_slide_state_id: Uuid,
12 pub created_at: DateTime<Utc>,
13 pub updated_at: DateTime<Utc>,
14 pub deleted_at: Option<DateTime<Utc>>,
15 pub score_given: Option<f32>,
16 pub grading_progress: GradingProgress,
17}
18
19pub async fn insert(
20 conn: &mut PgConnection,
21 exercise_task_id: Uuid,
22 user_exercise_slide_state_id: Uuid,
23 grading_progress: GradingProgress,
24) -> ModelResult<()> {
25 sqlx::query!(
26 "
27INSERT INTO user_exercise_task_states (
28 exercise_task_id,
29 user_exercise_slide_state_id,
30 grading_progress
31 )
32VALUES ($1, $2, $3)
33 ",
34 exercise_task_id,
35 user_exercise_slide_state_id,
36 grading_progress as GradingProgress,
37 )
38 .execute(conn)
39 .await?;
40 Ok(())
41}
42
43pub async fn upsert_with_grading(
46 conn: &mut PgConnection,
47 user_exercise_slide_state_id: Uuid,
48 exercise_task_grading: &ExerciseTaskGrading,
49) -> ModelResult<UserExerciseTaskState> {
50 upsert_with_grading_status(
51 conn,
52 exercise_task_grading.exercise_task_id,
53 user_exercise_slide_state_id,
54 exercise_task_grading.score_given,
55 exercise_task_grading.grading_progress,
56 )
57 .await
58}
59
60async fn upsert_with_grading_status(
61 conn: &mut PgConnection,
62 exercise_task_id: Uuid,
63 user_exercise_slide_state_id: Uuid,
64 score_given: Option<f32>,
65 grading_progress: GradingProgress,
66) -> ModelResult<UserExerciseTaskState> {
67 let res = sqlx::query_as!(
68 UserExerciseTaskState,
69 r#"
70INSERT INTO user_exercise_task_states (
71 exercise_task_id,
72 user_exercise_slide_state_id,
73 score_given,
74 grading_progress
75 )
76VALUES ($1, $2, $3, $4) ON CONFLICT (exercise_task_id, user_exercise_slide_state_id) DO
77UPDATE
78SET deleted_at = NULL,
79 score_given = $3,
80 grading_progress = $4
81RETURNING *
82 "#,
83 exercise_task_id,
84 user_exercise_slide_state_id,
85 score_given,
86 grading_progress as GradingProgress,
87 )
88 .fetch_one(conn)
89 .await?;
90 Ok(res)
91}
92
93pub async fn get(
94 conn: &mut PgConnection,
95 exercise_task_id: Uuid,
96 user_exercise_state_id: Uuid,
97) -> ModelResult<UserExerciseTaskState> {
98 let res = sqlx::query_as!(
99 UserExerciseTaskState,
100 r#"
101SELECT *
102FROM user_exercise_task_states
103WHERE exercise_task_id = $1
104 AND user_exercise_slide_state_id = $2
105 AND deleted_at IS NULL
106 "#,
107 exercise_task_id,
108 user_exercise_state_id,
109 )
110 .fetch_one(conn)
111 .await?;
112 Ok(res)
113}
114
115pub async fn get_grading_summary_by_user_exercise_slide_state_id(
116 conn: &mut PgConnection,
117 user_exercise_slide_state_id: Uuid,
118) -> ModelResult<(Option<f32>, GradingProgress)> {
119 let res = sqlx::query!(
120 r#"
121SELECT *
122FROM user_exercise_task_states
123WHERE user_exercise_slide_state_id = $1
124 AND deleted_at IS NULL
125 "#,
126 user_exercise_slide_state_id
127 )
128 .fetch_all(conn)
129 .await?;
130 let total_score_given = res
131 .iter()
132 .filter_map(|x| x.score_given)
133 .reduce(|acc, next| acc + next);
134 let least_significant_grading_progress = res
135 .iter()
136 .map(|x| x.grading_progress)
137 .min()
138 .unwrap_or(GradingProgress::NotReady);
139 Ok((total_score_given, least_significant_grading_progress))
140}
141
142pub async fn delete(
143 conn: &mut PgConnection,
144 exercise_task_id: Uuid,
145 user_exercise_slide_state_id: Uuid,
146) -> ModelResult<()> {
147 sqlx::query!(
148 "
149UPDATE user_exercise_task_states
150SET deleted_at = now()
151WHERE exercise_task_id = $1
152 AND user_exercise_slide_state_id = $2
153 AND deleted_at IS NULL
154 ",
155 exercise_task_id,
156 user_exercise_slide_state_id,
157 )
158 .execute(conn)
159 .await?;
160 Ok(())
161}
162
163pub fn figure_out_new_activity_progress(
171 current_activity_progress: ActivityProgress,
172) -> ActivityProgress {
173 if current_activity_progress == ActivityProgress::Completed {
174 return ActivityProgress::Completed;
175 }
176
177 ActivityProgress::Completed
180}
181
182pub fn figure_out_new_grading_progress(
195 current_grading_progress: Option<GradingProgress>,
196 grading_grading_progress: GradingProgress,
197) -> GradingProgress {
198 match current_grading_progress {
199 Some(GradingProgress::FullyGraded) => GradingProgress::FullyGraded,
200 _ => grading_grading_progress,
201 }
202}
203
204pub fn figure_out_new_score_given(
205 current_score_given: Option<f32>,
206 grading_score_given: Option<f32>,
207 user_points_update_strategy: UserPointsUpdateStrategy,
208) -> Option<f32> {
209 let current_score_given = if let Some(current_score_given) = current_score_given {
210 current_score_given
211 } else {
212 info!(
213 "Current state has no score, using score from grading ({:?})",
214 grading_score_given
215 );
216 return grading_score_given;
217 };
218 let grading_score_given = if let Some(grading_score_given) = grading_score_given {
219 grading_score_given
220 } else {
221 info!(
222 "Grading has no score, using score from current state ({:?})",
223 current_score_given
224 );
225 return Some(current_score_given);
226 };
227
228 let new_score = match user_points_update_strategy {
229 UserPointsUpdateStrategy::CanAddPointsButCannotRemovePoints => {
230 if current_score_given >= grading_score_given {
231 info!(
232 "Not updating score ({:?} >= {:?})",
233 current_score_given, grading_score_given
234 );
235 current_score_given
236 } else {
237 info!(
238 "Updating score from {:?} to {:?}",
239 current_score_given, grading_score_given
240 );
241 grading_score_given
242 }
243 }
244 UserPointsUpdateStrategy::CanAddPointsAndCanRemovePoints => {
245 info!(
246 "Updating score from {:?} to {:?}",
247 current_score_given, grading_score_given
248 );
249 grading_score_given
250 }
251 };
252 Some(new_score)
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::test_helper::*;
259
260 mod get_grading_summary_by_user_exercise_slide_state_id {
261 use headless_lms_utils::numbers::f32_approx_eq;
262 use serde_json::Value;
263
264 use crate::{
265 chapters::{self, NewChapter},
266 exercise_slides,
267 exercise_tasks::{self, NewExerciseTask},
268 exercises,
269 pages::{self, NewCoursePage},
270 user_exercise_slide_states, user_exercise_states,
271 };
272
273 use super::*;
274
275 #[tokio::test]
276 async fn initial_values() {
277 insert_data!(:tx);
278 let (user_exercise_slide_state_id, task_1, task_2, task_3) =
279 create_test_data(&mut tx).await.unwrap();
280 insert(
281 tx.as_mut(),
282 task_1,
283 user_exercise_slide_state_id,
284 GradingProgress::NotReady,
285 )
286 .await
287 .unwrap();
288 insert(
289 tx.as_mut(),
290 task_2,
291 user_exercise_slide_state_id,
292 GradingProgress::NotReady,
293 )
294 .await
295 .unwrap();
296 insert(
297 tx.as_mut(),
298 task_3,
299 user_exercise_slide_state_id,
300 GradingProgress::NotReady,
301 )
302 .await
303 .unwrap();
304
305 let (score_given, grading_progress) =
306 get_grading_summary_by_user_exercise_slide_state_id(
307 tx.as_mut(),
308 user_exercise_slide_state_id,
309 )
310 .await
311 .unwrap();
312 assert_eq!(score_given, None);
313 assert_eq!(grading_progress, GradingProgress::NotReady);
314 }
315
316 #[tokio::test]
317 async fn single_task() {
318 insert_data!(:tx);
319 let (user_exercise_slide_state_id, task_1, task_2, task_3) =
320 create_test_data(&mut tx).await.unwrap();
321 upsert_with_grading_status(
322 tx.as_mut(),
323 task_1,
324 user_exercise_slide_state_id,
325 None,
326 GradingProgress::NotReady,
327 )
328 .await
329 .unwrap();
330 upsert_with_grading_status(
331 tx.as_mut(),
332 task_2,
333 user_exercise_slide_state_id,
334 None,
335 GradingProgress::NotReady,
336 )
337 .await
338 .unwrap();
339 upsert_with_grading_status(
340 tx.as_mut(),
341 task_3,
342 user_exercise_slide_state_id,
343 Some(1.0),
344 GradingProgress::FullyGraded,
345 )
346 .await
347 .unwrap();
348
349 let (score_given, grading_progress) =
350 get_grading_summary_by_user_exercise_slide_state_id(
351 tx.as_mut(),
352 user_exercise_slide_state_id,
353 )
354 .await
355 .unwrap();
356 assert!(f32_approx_eq(score_given.unwrap(), 1.0));
357 assert_eq!(grading_progress, GradingProgress::NotReady);
358 }
359
360 #[tokio::test]
361 async fn all_tasks() {
362 insert_data!(:tx);
363 let (user_exercise_slide_state_id, task_1, task_2, task_3) =
364 create_test_data(&mut tx).await.unwrap();
365 upsert_with_grading_status(
366 tx.as_mut(),
367 task_1,
368 user_exercise_slide_state_id,
369 Some(1.0),
370 GradingProgress::FullyGraded,
371 )
372 .await
373 .unwrap();
374 upsert_with_grading_status(
375 tx.as_mut(),
376 task_2,
377 user_exercise_slide_state_id,
378 Some(1.0),
379 GradingProgress::FullyGraded,
380 )
381 .await
382 .unwrap();
383 upsert_with_grading_status(
384 tx.as_mut(),
385 task_3,
386 user_exercise_slide_state_id,
387 Some(1.0),
388 GradingProgress::FullyGraded,
389 )
390 .await
391 .unwrap();
392
393 let (score_given, grading_progress) =
394 get_grading_summary_by_user_exercise_slide_state_id(
395 tx.as_mut(),
396 user_exercise_slide_state_id,
397 )
398 .await
399 .unwrap();
400 assert!(f32_approx_eq(score_given.unwrap(), 3.0));
401 assert_eq!(grading_progress, GradingProgress::FullyGraded);
402 }
403
404 async fn create_test_data(tx: &mut Tx<'_>) -> ModelResult<(Uuid, Uuid, Uuid, Uuid)> {
405 insert_data!(tx: tx; :user, :org, :course, instance: _instance, :course_module);
406 let chapter_id = chapters::insert(
407 tx.as_mut(),
408 PKeyPolicy::Generate,
409 &NewChapter {
410 name: "chapter".to_string(),
411 color: Some("#065853".to_string()),
412 course_id: course,
413 chapter_number: 1,
414 front_page_id: None,
415 opens_at: None,
416 deadline: None,
417 course_module_id: Some(course_module.id),
418 },
419 )
420 .await?;
421
422 let (page_id, _history) = pages::insert_course_page(
423 tx.as_mut(),
424 &NewCoursePage::new(course, 1, "/test", "test"),
425 user,
426 )
427 .await?;
428 let exercise_id = exercises::insert(
429 tx.as_mut(),
430 PKeyPolicy::Generate,
431 course,
432 "course",
433 page_id,
434 chapter_id,
435 1,
436 )
437 .await?;
438 let slide_id =
439 exercise_slides::insert(tx.as_mut(), PKeyPolicy::Generate, exercise_id, 1).await?;
440 let task_1 = exercise_tasks::insert(
441 tx.as_mut(),
442 PKeyPolicy::Generate,
443 NewExerciseTask {
444 exercise_slide_id: slide_id,
445 exercise_type: "test-exercise".to_string(),
446 assignment: vec![],
447 public_spec: Some(Value::Null),
448 private_spec: Some(Value::Null),
449 model_solution_spec: Some(Value::Null),
450 order_number: 1,
451 },
452 )
453 .await?;
454 let task_2 = exercise_tasks::insert(
455 tx.as_mut(),
456 PKeyPolicy::Generate,
457 NewExerciseTask {
458 exercise_slide_id: slide_id,
459 exercise_type: "test-exercise".to_string(),
460 assignment: vec![],
461 public_spec: Some(Value::Null),
462 private_spec: Some(Value::Null),
463 model_solution_spec: Some(Value::Null),
464 order_number: 2,
465 },
466 )
467 .await?;
468 let task_3 = exercise_tasks::insert(
469 tx.as_mut(),
470 PKeyPolicy::Generate,
471 NewExerciseTask {
472 exercise_slide_id: slide_id,
473 exercise_type: "test-exercise".to_string(),
474 assignment: vec![],
475 public_spec: Some(Value::Null),
476 private_spec: Some(Value::Null),
477 model_solution_spec: Some(Value::Null),
478 order_number: 3,
479 },
480 )
481 .await?;
482 let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
483 tx.as_mut(),
484 user,
485 exercise_id,
486 Some(course),
487 None,
488 )
489 .await?;
490 user_exercise_states::upsert_selected_exercise_slide_id(
491 tx.as_mut(),
492 user,
493 exercise_id,
494 Some(course),
495 None,
496 Some(slide_id),
497 )
498 .await?;
499 let user_exercise_slide_state_id = user_exercise_slide_states::insert(
500 tx.as_mut(),
501 PKeyPolicy::Generate,
502 user_exercise_state.id,
503 slide_id,
504 )
505 .await?;
506 Ok((user_exercise_slide_state_id, task_1, task_2, task_3))
507 }
508 }
509
510 mod figure_out_new_activity_progress {
511 use super::*;
512
513 #[test]
514 fn it_works() {
515 assert_eq!(
516 figure_out_new_activity_progress(ActivityProgress::Initialized),
517 ActivityProgress::Completed
518 );
519 }
520 }
521
522 mod figure_out_new_grading_progress {
523 use super::*;
524
525 const ALL_GRADING_PROGRESSES: [GradingProgress; 5] = [
526 GradingProgress::FullyGraded,
527 GradingProgress::Pending,
528 GradingProgress::PendingManual,
529 GradingProgress::Failed,
530 GradingProgress::NotReady,
531 ];
532
533 #[test]
534 fn current_fully_graded_progress_always_retains() {
535 let current_grading_progress = GradingProgress::FullyGraded;
536 for grading_grading_progress in ALL_GRADING_PROGRESSES {
537 let new_grading_progress = figure_out_new_grading_progress(
538 Some(current_grading_progress),
539 grading_grading_progress,
540 );
541 assert_eq!(new_grading_progress, current_grading_progress);
542 }
543 }
544
545 #[test]
546 fn uses_value_from_grading_if_not_completed() {
547 for grading_grading_progress in ALL_GRADING_PROGRESSES {
548 let current_grading_progresses = vec![
549 None,
550 Some(GradingProgress::Pending),
551 Some(GradingProgress::PendingManual),
552 Some(GradingProgress::Failed),
553 Some(GradingProgress::NotReady),
554 ];
555 for current_grading_progress in current_grading_progresses {
556 let new_grading_progress = figure_out_new_grading_progress(
557 current_grading_progress,
558 grading_grading_progress,
559 );
560 assert_eq!(new_grading_progress, grading_grading_progress);
561 }
562 }
563 }
564 }
565
566 mod figure_out_new_score_given {
567 use headless_lms_utils::numbers::{f32_approx_eq, f32_max};
568
569 use super::*;
570
571 #[test]
572 fn strategy_can_add_points_and_can_remove_points_works() {
573 let test_cases = vec![(1.1, 1.1), (1.1, 20.9), (20.9, 1.1)];
574 for (current, new) in test_cases {
575 let result = figure_out_new_score_given(
576 Some(current),
577 Some(new),
578 UserPointsUpdateStrategy::CanAddPointsAndCanRemovePoints,
579 )
580 .unwrap();
581 assert!(f32_approx_eq(result, new));
582 }
583 }
584
585 #[test]
586 fn strategy_can_add_points_but_cannot_remove_points_works() {
587 let test_cases = vec![(1.1, 1.1), (1.1, 20.9), (20.9, 1.1)];
588 for (current, new) in test_cases {
589 let result = figure_out_new_score_given(
590 Some(current),
591 Some(new),
592 UserPointsUpdateStrategy::CanAddPointsButCannotRemovePoints,
593 )
594 .unwrap();
595 assert!(f32_approx_eq(result, f32_max(current, new)))
596 }
597 }
598
599 #[test]
600 fn it_handles_nones() {
601 let user_points_update_strategies = vec![
602 UserPointsUpdateStrategy::CanAddPointsAndCanRemovePoints,
603 UserPointsUpdateStrategy::CanAddPointsButCannotRemovePoints,
604 ];
605 for update_strategy in user_points_update_strategies {
606 assert_eq!(
607 figure_out_new_score_given(None, None, update_strategy),
608 None
609 );
610 assert!(f32_approx_eq(
611 figure_out_new_score_given(None, Some(1.1), update_strategy).unwrap(),
612 1.1
613 ));
614 assert!(f32_approx_eq(
615 figure_out_new_score_given(Some(1.1), None, update_strategy).unwrap(),
616 1.1
617 ));
618 }
619 }
620 }
621}