headless_lms_models/
exercises.rs

1use derive_more::Display;
2use futures::future::BoxFuture;
3use itertools::Itertools;
4use url::Url;
5
6use crate::{
7    exams, exercise_reset_logs,
8    exercise_service_info::ExerciseServiceInfoApi,
9    exercise_slide_submissions::{
10        ExerciseSlideSubmission, get_exercise_slide_submission_counts_for_exercise_user,
11    },
12    exercise_slides::{self, CourseMaterialExerciseSlide},
13    exercise_tasks,
14    peer_or_self_review_configs::CourseMaterialPeerOrSelfReviewConfig,
15    peer_or_self_review_question_submissions::PeerOrSelfReviewQuestionSubmission,
16    peer_or_self_review_questions::PeerOrSelfReviewQuestion,
17    peer_or_self_review_submissions::PeerOrSelfReviewSubmission,
18    peer_review_queue_entries::PeerReviewQueueEntry,
19    prelude::*,
20    teacher_grading_decisions::{TeacherDecisionType, TeacherGradingDecision},
21    user_course_exercise_service_variables::UserCourseExerciseServiceVariable,
22    user_course_settings,
23    user_exercise_states::{self, ReviewingStage, UserExerciseState},
24};
25use std::collections::HashMap;
26
27#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
28#[cfg_attr(feature = "ts_rs", derive(TS))]
29pub struct Exercise {
30    pub id: Uuid,
31    pub created_at: DateTime<Utc>,
32    pub updated_at: DateTime<Utc>,
33    pub name: String,
34    pub course_id: Option<Uuid>,
35    pub exam_id: Option<Uuid>,
36    pub page_id: Uuid,
37    pub chapter_id: Option<Uuid>,
38    pub deadline: Option<DateTime<Utc>>,
39    pub deleted_at: Option<DateTime<Utc>>,
40    pub score_maximum: i32,
41    pub order_number: i32,
42    pub copied_from: Option<Uuid>,
43    pub max_tries_per_slide: Option<i32>,
44    pub limit_number_of_tries: bool,
45    pub needs_peer_review: bool,
46    pub needs_self_review: bool,
47    pub use_course_default_peer_or_self_review_config: bool,
48    pub exercise_language_group_id: Option<Uuid>,
49}
50
51impl Exercise {
52    pub fn get_course_id(&self) -> ModelResult<Uuid> {
53        self.course_id.ok_or_else(|| {
54            ModelError::new(
55                ModelErrorType::Generic,
56                "Exercise is not related to a course.".to_string(),
57                None,
58            )
59        })
60    }
61}
62
63#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
64#[cfg_attr(feature = "ts_rs", derive(TS))]
65pub struct ExerciseGradingStatus {
66    pub exercise_id: Uuid,
67    pub exercise_name: String,
68    pub score_maximum: i32,
69    pub score_given: Option<f32>,
70    pub teacher_decision: Option<TeacherDecisionType>,
71    pub submission_id: Uuid,
72    pub updated_at: DateTime<Utc>,
73}
74
75#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
76#[cfg_attr(feature = "ts_rs", derive(TS))]
77pub struct ExerciseStatusSummaryForUser {
78    pub exercise: Exercise,
79    pub user_exercise_state: Option<UserExerciseState>,
80    pub exercise_slide_submissions: Vec<ExerciseSlideSubmission>,
81    pub given_peer_or_self_review_submissions: Vec<PeerOrSelfReviewSubmission>,
82    pub given_peer_or_self_review_question_submissions: Vec<PeerOrSelfReviewQuestionSubmission>,
83    pub received_peer_or_self_review_submissions: Vec<PeerOrSelfReviewSubmission>,
84    pub received_peer_or_self_review_question_submissions: Vec<PeerOrSelfReviewQuestionSubmission>,
85    pub peer_review_queue_entry: Option<PeerReviewQueueEntry>,
86    pub teacher_grading_decision: Option<TeacherGradingDecision>,
87    pub peer_or_self_review_questions: Vec<PeerOrSelfReviewQuestion>,
88}
89
90#[derive(Debug, Serialize, Deserialize)]
91#[cfg_attr(feature = "ts_rs", derive(TS))]
92pub struct CourseMaterialExercise {
93    pub exercise: Exercise,
94    pub can_post_submission: bool,
95    pub current_exercise_slide: CourseMaterialExerciseSlide,
96    /// None for logged out users.
97    pub exercise_status: Option<ExerciseStatus>,
98    #[cfg_attr(feature = "ts_rs", ts(type = "Record<string, number>"))]
99    pub exercise_slide_submission_counts: HashMap<Uuid, i64>,
100    pub peer_or_self_review_config: Option<CourseMaterialPeerOrSelfReviewConfig>,
101    pub previous_exercise_slide_submission: Option<ExerciseSlideSubmission>,
102    pub user_course_instance_exercise_service_variables: Vec<UserCourseExerciseServiceVariable>,
103    pub should_show_reset_message: Option<String>,
104}
105
106impl CourseMaterialExercise {
107    pub fn clear_grading_information(&mut self) {
108        self.exercise_status = None;
109        self.current_exercise_slide
110            .exercise_tasks
111            .iter_mut()
112            .for_each(|task| {
113                task.model_solution_spec = None;
114                task.previous_submission_grading = None;
115            });
116    }
117
118    pub fn clear_model_solution_specs(&mut self) {
119        self.current_exercise_slide
120            .exercise_tasks
121            .iter_mut()
122            .for_each(|task| {
123                task.model_solution_spec = None;
124            });
125    }
126}
127
128/**
129Indicates what is the user's completion status for a exercise.
130
131As close as possible to LTI's activity progress for compatibility: <https://www.imsglobal.org/spec/lti-ags/v2p0#activityprogress>.
132*/
133#[derive(
134    Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Default, Display, sqlx::Type,
135)]
136#[cfg_attr(feature = "ts_rs", derive(TS))]
137#[sqlx(type_name = "activity_progress", rename_all = "kebab-case")]
138pub enum ActivityProgress {
139    /// The user has not started the activity, or the activity has been reset for that student.
140    #[default]
141    Initialized,
142    /// The activity associated with the exercise has been started by the user to which the result relates.
143    Started,
144    /// The activity is being drafted and is available for comment.
145    InProgress,
146    /// The activity has been submitted at least once by the user but the user is still able make further submissions.
147    Submitted,
148    /// The user has completed the activity associated with the exercise.
149    Completed,
150}
151
152/**
153
154Tells what's the status of the grading progress for a user and exercise.
155
156As close as possible LTI's grading progress for compatibility: <https://www.imsglobal.org/spec/lti-ags/v2p0#gradingprogress>
157*/
158#[derive(
159    Clone, Copy, Debug, Deserialize, Eq, Serialize, Ord, PartialEq, PartialOrd, Display, sqlx::Type,
160)]
161#[cfg_attr(feature = "ts_rs", derive(TS))]
162#[sqlx(type_name = "grading_progress", rename_all = "kebab-case")]
163pub enum GradingProgress {
164    /// The grading could not complete.
165    Failed,
166    /// There is no grading process occurring; for example, the student has not yet made any submission.
167    NotReady,
168    /// Final Grade is pending, and it does require human intervention; if a Score value is present, it indicates the current value is partial and may be updated during the manual grading.
169    PendingManual,
170    /// Final Grade is pending, but does not require manual intervention; if a Score value is present, it indicates the current value is partial and may be updated.
171    Pending,
172    /// The grading process is completed; the score value, if any, represents the current Final Grade;
173    FullyGraded,
174}
175
176impl GradingProgress {
177    pub fn is_complete(self) -> bool {
178        self == Self::FullyGraded || self == Self::Failed
179    }
180}
181
182#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
183#[cfg_attr(feature = "ts_rs", derive(TS))]
184pub struct ExerciseStatus {
185    // None when grading has not completed yet. Max score can be found from the associated exercise.
186    pub score_given: Option<f32>,
187    pub activity_progress: ActivityProgress,
188    pub grading_progress: GradingProgress,
189    pub reviewing_stage: ReviewingStage,
190}
191
192#[allow(clippy::too_many_arguments)]
193pub async fn insert(
194    conn: &mut PgConnection,
195    pkey_policy: PKeyPolicy<Uuid>,
196    course_id: Uuid,
197    name: &str,
198    page_id: Uuid,
199    chapter_id: Uuid,
200    order_number: i32,
201) -> ModelResult<Uuid> {
202    let course = crate::courses::get_course(conn, course_id).await?;
203    let exercise_language_group_id = crate::exercise_language_groups::insert(
204        conn,
205        PKeyPolicy::Generate,
206        course.course_language_group_id,
207    )
208    .await?;
209
210    let res = sqlx::query!(
211        "
212INSERT INTO exercises (
213    id,
214    course_id,
215    name,
216    page_id,
217    chapter_id,
218    order_number,
219    exercise_language_group_id
220  )
221VALUES ($1, $2, $3, $4, $5, $6, $7)
222RETURNING id
223        ",
224        pkey_policy.into_uuid(),
225        course_id,
226        name,
227        page_id,
228        chapter_id,
229        order_number,
230        exercise_language_group_id,
231    )
232    .fetch_one(conn)
233    .await?;
234    Ok(res.id)
235}
236
237pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<Exercise> {
238    let exercise = sqlx::query_as!(
239        Exercise,
240        "
241SELECT *
242FROM exercises
243WHERE id = $1
244",
245        id
246    )
247    .fetch_one(conn)
248    .await?;
249    Ok(exercise)
250}
251
252pub async fn get_exercise_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<Exercise> {
253    let exercise = sqlx::query_as!(Exercise, "SELECT * FROM exercises WHERE id = $1;", id)
254        .fetch_one(conn)
255        .await?;
256    Ok(exercise)
257}
258
259pub async fn get_exercises_by_course_id(
260    conn: &mut PgConnection,
261    course_id: Uuid,
262) -> ModelResult<Vec<Exercise>> {
263    let exercises = sqlx::query_as!(
264        Exercise,
265        r#"
266SELECT *
267FROM exercises
268WHERE course_id = $1
269  AND deleted_at IS NULL
270"#,
271        course_id
272    )
273    .fetch_all(&mut *conn)
274    .await?;
275    Ok(exercises)
276}
277
278pub async fn get_exercise_submissions_and_status_by_course_instance_id(
279    conn: &mut PgConnection,
280    course_instance_id: Uuid,
281    user_id: Uuid,
282) -> ModelResult<Vec<ExerciseGradingStatus>> {
283    let exercises = sqlx::query_as!(
284        ExerciseGradingStatus,
285        r#"
286        SELECT
287        e.id as exercise_id,
288        e.name as exercise_name,
289        e.score_maximum,
290        ues.score_given,
291        tgd.teacher_decision as "teacher_decision: _",
292        ess.id as submission_id,
293        ess.updated_at
294        FROM exercises e
295        LEFT JOIN user_exercise_states ues on e.id = ues.exercise_id
296        LEFT JOIN teacher_grading_decisions tgd on tgd.user_exercise_state_id = ues.id
297        LEFT JOIN exercise_slide_submissions ess on e.id = ess.exercise_id
298        WHERE e.course_id = (
299            SELECT course_id
300            FROM course_instances
301            WHERE id = $1
302          )
303          AND e.deleted_at IS NULL
304          AND ess.user_id = $2
305          AND ues.user_id = $2
306        ORDER BY e.order_number ASC;
307"#,
308        course_instance_id,
309        user_id
310    )
311    .fetch_all(conn)
312    .await?;
313    Ok(exercises)
314}
315
316pub async fn get_exercises_by_chapter_id(
317    conn: &mut PgConnection,
318    chapter_id: Uuid,
319) -> ModelResult<Vec<Exercise>> {
320    let exercises = sqlx::query_as!(
321        Exercise,
322        r#"
323SELECT *
324FROM exercises
325WHERE chapter_id = $1
326  AND deleted_at IS NULL
327"#,
328        chapter_id
329    )
330    .fetch_all(&mut *conn)
331    .await?;
332    Ok(exercises)
333}
334
335pub async fn get_exercises_by_chapter_ids(
336    conn: &mut PgConnection,
337    chapter_ids: &[Uuid],
338) -> ModelResult<Vec<Exercise>> {
339    if chapter_ids.is_empty() {
340        return Ok(Vec::new());
341    }
342    let exercises = sqlx::query_as!(
343        Exercise,
344        r#"
345SELECT *
346FROM exercises
347WHERE chapter_id = ANY($1)
348  AND deleted_at IS NULL
349"#,
350        chapter_ids as &[Uuid]
351    )
352    .fetch_all(&mut *conn)
353    .await?;
354    Ok(exercises)
355}
356
357pub async fn get_exercises_by_page_id(
358    conn: &mut PgConnection,
359    page_id: Uuid,
360) -> ModelResult<Vec<Exercise>> {
361    let exercises = sqlx::query_as!(
362        Exercise,
363        r#"
364SELECT *
365  FROM exercises
366WHERE page_id = $1
367  AND deleted_at IS NULL;
368"#,
369        page_id,
370    )
371    .fetch_all(&mut *conn)
372    .await?;
373    Ok(exercises)
374}
375
376pub async fn get_exercises_by_exam_id(
377    conn: &mut PgConnection,
378    exam_id: Uuid,
379) -> ModelResult<Vec<Exercise>> {
380    let exercises = sqlx::query_as!(
381        Exercise,
382        r#"
383SELECT *
384FROM exercises
385WHERE exam_id = $1
386  AND deleted_at IS NULL
387"#,
388        exam_id,
389    )
390    .fetch_all(&mut *conn)
391    .await?;
392    Ok(exercises)
393}
394
395pub async fn get_course_or_exam_id(
396    conn: &mut PgConnection,
397    id: Uuid,
398) -> ModelResult<CourseOrExamId> {
399    let res = sqlx::query!(
400        "
401SELECT course_id,
402  exam_id
403FROM exercises
404WHERE id = $1
405",
406        id
407    )
408    .fetch_one(conn)
409    .await?;
410    CourseOrExamId::from_course_and_exam_ids(res.course_id, res.exam_id)
411}
412
413pub async fn get_course_material_exercise(
414    conn: &mut PgConnection,
415    user_id: Option<Uuid>,
416    exercise_id: Uuid,
417    fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
418) -> ModelResult<CourseMaterialExercise> {
419    let exercise = get_by_id(conn, exercise_id).await?;
420    let (current_exercise_slide, instance_or_exam_id) =
421        get_or_select_exercise_slide(&mut *conn, user_id, &exercise, fetch_service_info).await?;
422    info!(
423        "Current exercise slide id: {:#?}",
424        current_exercise_slide.id
425    );
426
427    let user_exercise_state = match (user_id, instance_or_exam_id) {
428        (Some(user_id), Some(course_or_exam_id)) => {
429            user_exercise_states::get_user_exercise_state_if_exists(
430                conn,
431                user_id,
432                exercise.id,
433                course_or_exam_id,
434            )
435            .await?
436        }
437        _ => None,
438    };
439
440    let can_post_submission =
441        determine_can_post_submission(&mut *conn, user_id, &exercise, &user_exercise_state).await?;
442
443    let previous_exercise_slide_submission = match user_id {
444        Some(user_id) => {
445            crate::exercise_slide_submissions::try_to_get_users_latest_exercise_slide_submission(
446                conn,
447                current_exercise_slide.id,
448                user_id,
449            )
450            .await?
451        }
452        _ => None,
453    };
454
455    let exercise_status = user_exercise_state.map(|user_exercise_state| ExerciseStatus {
456        score_given: user_exercise_state.score_given,
457        activity_progress: user_exercise_state.activity_progress,
458        grading_progress: user_exercise_state.grading_progress,
459        reviewing_stage: user_exercise_state.reviewing_stage,
460    });
461
462    let exercise_slide_submission_counts = if let Some(user_id) = user_id {
463        if let Some(cioreid) = instance_or_exam_id {
464            get_exercise_slide_submission_counts_for_exercise_user(
465                conn,
466                exercise_id,
467                cioreid,
468                user_id,
469            )
470            .await?
471        } else {
472            HashMap::new()
473        }
474    } else {
475        HashMap::new()
476    };
477
478    let peer_or_self_review_config = if let Some(course_id) = exercise.course_id {
479        if exercise.needs_peer_review || exercise.needs_self_review {
480            let prc = crate::peer_or_self_review_configs::get_by_exercise_or_course_id(
481                conn, &exercise, course_id,
482            )
483            .await
484            .optional()?;
485            prc.map(|prc| CourseMaterialPeerOrSelfReviewConfig {
486                id: prc.id,
487                course_id: prc.course_id,
488                exercise_id: prc.exercise_id,
489                peer_reviews_to_give: prc.peer_reviews_to_give,
490                peer_reviews_to_receive: prc.peer_reviews_to_receive,
491            })
492        } else {
493            None
494        }
495    } else {
496        None
497    };
498
499    let user_course_instance_exercise_service_variables = match (user_id, instance_or_exam_id) {
500        (Some(user_id), Some(course_or_exam_id)) => {
501            Some(crate::user_course_exercise_service_variables::get_all_variables_for_user_and_course_or_exam(conn, user_id, course_or_exam_id).await?)
502        }
503        _ => None,
504    }.unwrap_or_default();
505
506    let should_show_reset_message = if let Some(user_id) = user_id {
507        crate::exercise_reset_logs::user_should_see_reset_message_for_exercise(
508            conn,
509            user_id,
510            exercise_id,
511        )
512        .await?
513    } else {
514        None
515    };
516
517    Ok(CourseMaterialExercise {
518        exercise,
519        can_post_submission,
520        current_exercise_slide,
521        exercise_status,
522        exercise_slide_submission_counts,
523        peer_or_self_review_config,
524        user_course_instance_exercise_service_variables,
525        previous_exercise_slide_submission,
526        should_show_reset_message,
527    })
528}
529
530async fn determine_can_post_submission(
531    conn: &mut PgConnection,
532    user_id: Option<Uuid>,
533    exercise: &Exercise,
534    user_exercise_state: &Option<UserExerciseState>,
535) -> Result<bool, ModelError> {
536    if let Some(user_exercise_state) = user_exercise_state {
537        // Once the user has started peer review or self review, they cannot no longer answer the exercise because they have already seen a model solution in the review instructions and they have seen submissions from other users.
538        if user_exercise_state.reviewing_stage != ReviewingStage::NotStarted {
539            return Ok(false);
540        }
541    }
542
543    let can_post_submission = if let Some(user_id) = user_id {
544        if let Some(exam_id) = exercise.exam_id {
545            exams::verify_exam_submission_can_be_made(conn, exam_id, user_id).await?
546        } else {
547            true
548        }
549    } else {
550        false
551    };
552    Ok(can_post_submission)
553}
554
555pub async fn get_or_select_exercise_slide(
556    conn: &mut PgConnection,
557    user_id: Option<Uuid>,
558    exercise: &Exercise,
559    fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
560) -> ModelResult<(CourseMaterialExerciseSlide, Option<CourseOrExamId>)> {
561    match (user_id, exercise.course_id, exercise.exam_id) {
562        (None, ..) => {
563            // No signed in user. Show random exercise without model solution.
564            let random_slide =
565                exercise_slides::get_random_exercise_slide_for_exercise(conn, exercise.id).await?;
566            let random_slide_tasks = exercise_tasks::get_course_material_exercise_tasks(
567                conn,
568                random_slide.id,
569                None,
570                fetch_service_info,
571            )
572            .await?;
573            Ok((
574                CourseMaterialExerciseSlide {
575                    id: random_slide.id,
576                    exercise_tasks: random_slide_tasks,
577                },
578                None,
579            ))
580        }
581        (Some(user_id), Some(course_id), None) => {
582            // signed in, course exercise
583            let user_course_settings = user_course_settings::get_user_course_settings_by_course_id(
584                conn, user_id, course_id,
585            )
586            .await?;
587            match user_course_settings {
588                Some(settings) if settings.current_course_id == course_id => {
589                    // User is enrolled on an instance of the given course.
590                    let course_or_exam_id: CourseOrExamId = exercise.try_into()?;
591                    let tasks =
592                        exercise_tasks::get_or_select_user_exercise_slide_for_course_or_exam(
593                            conn,
594                            user_id,
595                            exercise.id,
596                            course_or_exam_id,
597                            fetch_service_info,
598                        )
599                        .await?;
600                    Ok((tasks, Some(CourseOrExamId::Course(course_id))))
601                }
602                Some(_) => {
603                    // User is enrolled on a different language version of the course. Show exercise
604                    // slide based on their latest enrollment or a random one.
605                    let exercise_tasks =
606                        exercise_tasks::get_existing_users_exercise_slide_for_course(
607                            conn,
608                            user_id,
609                            exercise.id,
610                            course_id,
611                            &fetch_service_info,
612                        )
613                        .await?;
614                    if let Some(exercise_tasks) = exercise_tasks {
615                        Ok((exercise_tasks, Some(CourseOrExamId::Course(course_id))))
616                    } else {
617                        // no exercise task has been chosen for the user
618                        let random_slide = exercise_slides::get_random_exercise_slide_for_exercise(
619                            conn,
620                            exercise.id,
621                        )
622                        .await?;
623                        let random_tasks = exercise_tasks::get_course_material_exercise_tasks(
624                            conn,
625                            random_slide.id,
626                            Some(user_id),
627                            &fetch_service_info,
628                        )
629                        .await?;
630
631                        Ok((
632                            CourseMaterialExerciseSlide {
633                                id: random_slide.id,
634                                exercise_tasks: random_tasks,
635                            },
636                            None,
637                        ))
638                    }
639                }
640                None => {
641                    // User is not enrolled on any course version. This is not a valid scenario because
642                    // tasks are based on a specific instance.
643                    Err(ModelError::new(
644                        ModelErrorType::PreconditionFailed,
645                        "User must be enrolled to the course".to_string(),
646                        None,
647                    ))
648                }
649            }
650        }
651        (Some(user_id), _, Some(exam_id)) => {
652            info!("selecting exam task");
653            // signed in, exam exercise
654            let tasks = exercise_tasks::get_or_select_user_exercise_slide_for_course_or_exam(
655                conn,
656                user_id,
657                exercise.id,
658                CourseOrExamId::Exam(exam_id),
659                fetch_service_info,
660            )
661            .await?;
662            info!("selecting exam task {:#?}", tasks);
663            Ok((tasks, Some(CourseOrExamId::Exam(exam_id))))
664        }
665        (Some(_), ..) => Err(ModelError::new(
666            ModelErrorType::Generic,
667            "The selected exercise is not attached to any course or exam".to_string(),
668            None,
669        )),
670    }
671}
672
673pub async fn delete_exercises_by_page_id(
674    conn: &mut PgConnection,
675    page_id: Uuid,
676) -> ModelResult<Vec<Uuid>> {
677    let deleted_ids = sqlx::query!(
678        "
679UPDATE exercises
680SET deleted_at = now()
681WHERE page_id = $1
682AND deleted_at IS NULL
683RETURNING id;
684        ",
685        page_id
686    )
687    .fetch_all(conn)
688    .await?
689    .into_iter()
690    .map(|x| x.id)
691    .collect();
692    Ok(deleted_ids)
693}
694
695pub async fn set_exercise_to_use_exercise_specific_peer_or_self_review_config(
696    conn: &mut PgConnection,
697    exercise_id: Uuid,
698    needs_peer_review: bool,
699    needs_self_review: bool,
700    use_course_default_peer_or_self_review_config: bool,
701) -> ModelResult<Uuid> {
702    let id = sqlx::query!(
703        "
704UPDATE exercises
705SET use_course_default_peer_or_self_review_config = $1,
706  needs_peer_review = $2,
707  needs_self_review = $3
708WHERE id = $4
709RETURNING id;
710        ",
711        use_course_default_peer_or_self_review_config,
712        needs_peer_review,
713        needs_self_review,
714        exercise_id
715    )
716    .fetch_one(conn)
717    .await?;
718
719    Ok(id.id)
720}
721
722pub async fn get_all_exercise_statuses_by_user_id_and_course_id(
723    conn: &mut PgConnection,
724    course_id: Uuid,
725    user_id: Uuid,
726) -> ModelResult<Vec<ExerciseStatusSummaryForUser>> {
727    let course_or_exam_id = CourseOrExamId::Course(course_id);
728    // Load all the data for this user from all the exercises to memory, and group most of them to HashMaps by exercise id
729    let exercises = crate::exercises::get_exercises_by_course_id(&mut *conn, course_id).await?;
730    let mut user_exercise_states =
731        crate::user_exercise_states::get_all_for_user_and_course_or_exam(
732            &mut *conn,
733            user_id,
734            course_or_exam_id,
735        )
736        .await?
737        .into_iter()
738        .map(|ues| (ues.exercise_id, ues))
739        .collect::<HashMap<_, _>>();
740    let mut exercise_slide_submissions =
741        crate::exercise_slide_submissions::get_users_all_submissions_for_course_or_exam(
742            &mut *conn,
743            user_id,
744            course_or_exam_id,
745        )
746        .await?
747        .into_iter()
748        .into_group_map_by(|o| o.exercise_id);
749    let mut given_peer_or_self_review_submissions = crate::peer_or_self_review_submissions::get_all_given_peer_or_self_review_submissions_for_user_and_course(&mut *conn, user_id, course_id).await?.into_iter()
750        .into_group_map_by(|o| o.exercise_id);
751    let mut received_peer_or_self_review_submissions = crate::peer_or_self_review_submissions::get_all_received_peer_or_self_review_submissions_for_user_and_course(&mut *conn, user_id, course_id).await?.into_iter()
752        .into_group_map_by(|o| o.exercise_id);
753    let given_peer_or_self_review_submission_ids = given_peer_or_self_review_submissions
754        .values()
755        .flatten()
756        .map(|x| x.id)
757        .collect::<Vec<_>>();
758    let mut given_peer_or_self_review_question_submissions = crate::peer_or_self_review_question_submissions::get_question_submissions_from_from_peer_or_self_review_submission_ids(&mut *conn, &given_peer_or_self_review_submission_ids).await?
759        .into_iter()
760        .into_group_map_by(|o| {
761            let peer_review_submission = given_peer_or_self_review_submissions.clone().into_iter()
762                .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
763                .unwrap_or_else(|| (Uuid::nil(), vec![]));
764            peer_review_submission.0
765    });
766    let received_peer_or_self_review_submission_ids = received_peer_or_self_review_submissions
767        .values()
768        .flatten()
769        .map(|x| x.id)
770        .collect::<Vec<_>>();
771    let mut received_peer_or_self_review_question_submissions = crate::peer_or_self_review_question_submissions::get_question_submissions_from_from_peer_or_self_review_submission_ids(&mut *conn, &received_peer_or_self_review_submission_ids).await?.into_iter()
772    .into_group_map_by(|o| {
773        let peer_review_submission = received_peer_or_self_review_submissions.clone().into_iter()
774            .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
775            .unwrap_or_else(|| (Uuid::nil(), vec![]));
776        peer_review_submission.0
777    });
778    let mut peer_review_queue_entries =
779        crate::peer_review_queue_entries::get_all_by_user_and_course_id(
780            &mut *conn, user_id, course_id,
781        )
782        .await?
783        .into_iter()
784        .map(|x| (x.exercise_id, x))
785        .collect::<HashMap<_, _>>();
786    let mut teacher_grading_decisions = crate::teacher_grading_decisions::get_all_latest_grading_decisions_by_user_id_and_course_id(&mut *conn, user_id, course_id).await?.into_iter()
787    .filter_map(|tgd| {
788        let user_exercise_state = user_exercise_states.clone().into_iter()
789            .find(|(_exercise_id, ues)|  ues.id == tgd.user_exercise_state_id)?;
790        Some((user_exercise_state.0, tgd))
791    }).collect::<HashMap<_, _>>();
792    let all_peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
793        .iter()
794        .chain(received_peer_or_self_review_question_submissions.iter())
795        .flat_map(|(_exercise_id, prqs)| prqs.iter().map(|p| p.peer_or_self_review_question_id))
796        .collect::<Vec<_>>();
797    let all_peer_or_self_review_questions = crate::peer_or_self_review_questions::get_by_ids(
798        &mut *conn,
799        &all_peer_or_self_review_question_ids,
800    )
801    .await?;
802
803    // Map all the data for all the exercises to be summaries of the data for each exercise.
804    //
805    // Since all data is in hashmaps grouped by exercise id, and we iterate though every
806    // exercise id exactly once, we can just remove the data for the exercise from the
807    // hashmaps and avoid extra copying.
808    let res = exercises
809        .into_iter()
810        .map(|exercise| {
811            let user_exercise_state = user_exercise_states.remove(&exercise.id);
812            let exercise_slide_submissions = exercise_slide_submissions
813                .remove(&exercise.id)
814                .unwrap_or_default();
815            let given_peer_or_self_review_submissions = given_peer_or_self_review_submissions
816                .remove(&exercise.id)
817                .unwrap_or_default();
818            let received_peer_or_self_review_submissions = received_peer_or_self_review_submissions
819                .remove(&exercise.id)
820                .unwrap_or_default();
821            let given_peer_or_self_review_question_submissions =
822                given_peer_or_self_review_question_submissions
823                    .remove(&exercise.id)
824                    .unwrap_or_default();
825            let received_peer_or_self_review_question_submissions =
826                received_peer_or_self_review_question_submissions
827                    .remove(&exercise.id)
828                    .unwrap_or_default();
829            let peer_review_queue_entry = peer_review_queue_entries.remove(&exercise.id);
830            let teacher_grading_decision = teacher_grading_decisions.remove(&exercise.id);
831            let peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
832                .iter()
833                .chain(received_peer_or_self_review_question_submissions.iter())
834                .map(|prqs| prqs.peer_or_self_review_question_id)
835                .unique()
836                .collect::<Vec<_>>();
837            let peer_or_self_review_questions = all_peer_or_self_review_questions
838                .iter()
839                .filter(|prq| peer_or_self_review_question_ids.contains(&prq.id))
840                .cloned()
841                .collect::<Vec<_>>();
842            ExerciseStatusSummaryForUser {
843                exercise,
844                user_exercise_state,
845                exercise_slide_submissions,
846                given_peer_or_self_review_submissions,
847                received_peer_or_self_review_submissions,
848                given_peer_or_self_review_question_submissions,
849                received_peer_or_self_review_question_submissions,
850                peer_review_queue_entry,
851                teacher_grading_decision,
852                peer_or_self_review_questions,
853            }
854        })
855        .collect::<Vec<_>>();
856    Ok(res)
857}
858
859pub async fn get_exercises_by_module_containing_exercise_type(
860    conn: &mut PgConnection,
861    exercise_type: &str,
862    course_module_id: Uuid,
863) -> ModelResult<Vec<Exercise>> {
864    let res: Vec<Exercise> = sqlx::query_as!(
865        Exercise,
866        r#"
867SELECT DISTINCT(ex.*)
868FROM exercises ex
869  JOIN exercise_slides slides ON ex.id = slides.exercise_id
870  JOIN exercise_tasks tasks ON slides.id = tasks.exercise_slide_id
871  JOIN chapters c ON ex.chapter_id = c.id
872where tasks.exercise_type = $1
873  AND c.course_module_id = $2
874  AND ex.deleted_at IS NULL
875  AND tasks.deleted_at IS NULL
876  and c.deleted_at IS NULL
877  and slides.deleted_at IS NULL
878        "#,
879        exercise_type,
880        course_module_id
881    )
882    .fetch_all(conn)
883    .await?;
884    Ok(res)
885}
886
887/// Collects user_ids and related exercise_ids according to given filters
888pub async fn collect_user_ids_and_exercise_ids_for_reset(
889    conn: &mut PgConnection,
890    user_ids: &[Uuid],
891    exercise_ids: &[Uuid],
892    threshold: Option<f64>,
893    reset_all_below_max: bool,
894    reset_only_locked_reviews: bool,
895) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
896    let results = sqlx::query!(
897        r#"
898SELECT DISTINCT ues.user_id,
899  ues.exercise_id
900FROM user_exercise_states ues
901  LEFT JOIN exercises e ON ues.exercise_id = e.id
902WHERE ues.user_id = ANY($1)
903  AND ues.exercise_id = ANY($2)
904  AND ues.deleted_at IS NULL
905  AND (
906    $3 = FALSE
907    OR ues.score_given < e.score_maximum
908  )
909  AND (
910    $4::FLOAT IS NULL
911    OR ues.score_given < $4::FLOAT
912  )
913  AND (
914    $5 = FALSE
915    OR ues.reviewing_stage = 'reviewed_and_locked'
916  )
917            "#,
918        user_ids,
919        exercise_ids,
920        reset_all_below_max,
921        threshold,
922        reset_only_locked_reviews
923    )
924    .fetch_all(&mut *conn)
925    .await?;
926
927    let mut user_exercise_map: HashMap<Uuid, Vec<Uuid>> = HashMap::new();
928    for row in &results {
929        user_exercise_map
930            .entry(row.user_id)
931            .or_default()
932            .push(row.exercise_id);
933    }
934
935    Ok(user_exercise_map.into_iter().collect())
936}
937
938/// Resets all related tables for selected users and related exercises
939pub async fn reset_exercises_for_selected_users(
940    conn: &mut PgConnection,
941    users_and_exercises: &[(Uuid, Vec<Uuid>)],
942    reset_by: Option<Uuid>,
943    course_id: Uuid,
944    reason: Option<String>,
945) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
946    let mut successful_resets = Vec::new();
947    let mut tx = conn.begin().await?;
948    for (user_id, exercise_ids) in users_and_exercises {
949        sqlx::query!(
950            r#"
951UPDATE exercise_slide_submissions
952SET deleted_at = NOW()
953WHERE user_id = $1
954  AND exercise_id = ANY($2)
955  AND deleted_at IS NULL
956            "#,
957            user_id,
958            exercise_ids
959        )
960        .execute(&mut *tx)
961        .await?;
962
963        sqlx::query!(
964            r#"
965UPDATE exercise_task_submissions
966SET deleted_at = NOW()
967WHERE exercise_slide_submission_id IN (
968    SELECT id
969    FROM exercise_slide_submissions
970    WHERE user_id = $1
971      AND exercise_id = ANY($2)
972  )
973  AND deleted_at IS NULL
974            "#,
975            user_id,
976            exercise_ids
977        )
978        .execute(&mut *tx)
979        .await?;
980
981        sqlx::query!(
982            r#"
983UPDATE peer_review_queue_entries
984SET deleted_at = NOW()
985WHERE user_id = $1
986  AND exercise_id = ANY($2)
987  AND deleted_at IS NULL
988            "#,
989            user_id,
990            exercise_ids
991        )
992        .execute(&mut *tx)
993        .await?;
994
995        sqlx::query!(
996            r#"
997UPDATE exercise_task_gradings
998SET deleted_at = NOW()
999WHERE exercise_task_submission_id IN (
1000    SELECT id
1001    FROM exercise_task_submissions
1002    WHERE exercise_slide_submission_id IN (
1003        SELECT id
1004        FROM exercise_slide_submissions
1005        WHERE user_id = $1
1006          AND exercise_id = ANY($2)
1007      )
1008  )
1009  AND deleted_at IS NULL
1010            "#,
1011            user_id,
1012            exercise_ids
1013        )
1014        .execute(&mut *tx)
1015        .await?;
1016
1017        sqlx::query!(
1018            r#"
1019UPDATE user_exercise_states
1020SET deleted_at = NOW()
1021WHERE user_id = $1
1022  AND exercise_id = ANY($2)
1023  AND deleted_at IS NULL
1024            "#,
1025            user_id,
1026            exercise_ids
1027        )
1028        .execute(&mut *tx)
1029        .await?;
1030
1031        sqlx::query!(
1032            r#"
1033UPDATE user_exercise_task_states
1034SET deleted_at = NOW()
1035WHERE user_exercise_slide_state_id IN (
1036    SELECT id
1037    FROM user_exercise_slide_states
1038    WHERE user_exercise_state_id IN (
1039        SELECT id
1040        FROM user_exercise_states
1041        WHERE user_id = $1
1042          AND exercise_id = ANY($2)
1043      )
1044  )
1045  AND deleted_at IS NULL
1046            "#,
1047            user_id,
1048            exercise_ids
1049        )
1050        .execute(&mut *tx)
1051        .await?;
1052
1053        sqlx::query!(
1054            r#"
1055UPDATE user_exercise_slide_states
1056SET deleted_at = NOW()
1057WHERE user_exercise_state_id IN (
1058    SELECT id
1059    FROM user_exercise_states
1060    WHERE user_id = $1
1061      AND exercise_id = ANY($2)
1062  )
1063  AND deleted_at IS NULL
1064            "#,
1065            user_id,
1066            exercise_ids
1067        )
1068        .execute(&mut *tx)
1069        .await?;
1070
1071        sqlx::query!(
1072            r#"
1073UPDATE teacher_grading_decisions
1074SET deleted_at = NOW()
1075WHERE user_exercise_state_id IN (
1076    SELECT id
1077    FROM user_exercise_states
1078    WHERE user_id = $1
1079      AND exercise_id = ANY($2)
1080  )
1081  AND deleted_at IS NULL
1082            "#,
1083            user_id,
1084            exercise_ids
1085        )
1086        .execute(&mut *tx)
1087        .await?;
1088
1089        // Adds a log of a reset exercise for a user
1090        exercise_reset_logs::log_exercise_reset(
1091            &mut tx,
1092            reset_by,
1093            *user_id,
1094            exercise_ids,
1095            course_id,
1096            reason.clone(),
1097        )
1098        .await?;
1099
1100        successful_resets.push((*user_id, exercise_ids.to_vec()));
1101    }
1102    tx.commit().await?;
1103    Ok(successful_resets)
1104}
1105
1106#[cfg(test)]
1107mod test {
1108    use super::*;
1109    use crate::{
1110        course_instance_enrollments::{self, NewCourseInstanceEnrollment},
1111        exercise_service_info::{self, PathInfo},
1112        exercise_services::{self, ExerciseServiceNewOrUpdate},
1113        test_helper::Conn,
1114        test_helper::*,
1115        user_exercise_states,
1116    };
1117
1118    #[tokio::test]
1119    async fn selects_course_material_exercise_for_enrolled_student() {
1120        insert_data!(
1121            :tx,
1122            user: user_id,
1123            org: organization_id,
1124            course: course_id,
1125            instance: course_instance,
1126            :course_module,
1127            chapter: chapter_id,
1128            page: page_id,
1129            exercise: exercise_id,
1130            slide: exercise_slide_id,
1131            task: exercise_task_id
1132        );
1133        let exercise_service = exercise_services::insert_exercise_service(
1134            tx.as_mut(),
1135            &ExerciseServiceNewOrUpdate {
1136                name: "text-exercise".to_string(),
1137                slug: TEST_HELPER_EXERCISE_SERVICE_NAME.to_string(),
1138                public_url: "https://example.com".to_string(),
1139                internal_url: None,
1140                max_reprocessing_submissions_at_once: 1,
1141            },
1142        )
1143        .await
1144        .unwrap();
1145        let _exercise_service_info = exercise_service_info::insert(
1146            tx.as_mut(),
1147            &PathInfo {
1148                exercise_service_id: exercise_service.id,
1149                user_interface_iframe_path: "/iframe".to_string(),
1150                grade_endpoint_path: "/grade".to_string(),
1151                public_spec_endpoint_path: "/public-spec".to_string(),
1152                model_solution_spec_endpoint_path: "test-only-empty-path".to_string(),
1153                has_custom_view: false,
1154            },
1155        )
1156        .await
1157        .unwrap();
1158        course_instance_enrollments::insert_enrollment_and_set_as_current(
1159            tx.as_mut(),
1160            NewCourseInstanceEnrollment {
1161                course_id,
1162                course_instance_id: course_instance.id,
1163                user_id,
1164            },
1165        )
1166        .await
1167        .unwrap();
1168
1169        let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1170            tx.as_mut(),
1171            user_id,
1172            exercise_id,
1173            CourseOrExamId::Course(course_id),
1174        )
1175        .await
1176        .unwrap();
1177        assert!(user_exercise_state.is_none());
1178
1179        let exercise = get_course_material_exercise(
1180            tx.as_mut(),
1181            Some(user_id),
1182            exercise_id,
1183            |_| unimplemented!(),
1184        )
1185        .await
1186        .unwrap();
1187        assert_eq!(
1188            exercise
1189                .current_exercise_slide
1190                .exercise_tasks
1191                .first()
1192                .unwrap()
1193                .id,
1194            exercise_task_id
1195        );
1196
1197        let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1198            tx.as_mut(),
1199            user_id,
1200            exercise_id,
1201            CourseOrExamId::Course(course_id),
1202        )
1203        .await
1204        .unwrap();
1205        assert_eq!(
1206            user_exercise_state
1207                .unwrap()
1208                .selected_exercise_slide_id
1209                .unwrap(),
1210            exercise_slide_id
1211        );
1212    }
1213}