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_page_id(
336    conn: &mut PgConnection,
337    page_id: Uuid,
338) -> ModelResult<Vec<Exercise>> {
339    let exercises = sqlx::query_as!(
340        Exercise,
341        r#"
342SELECT *
343  FROM exercises
344WHERE page_id = $1
345  AND deleted_at IS NULL;
346"#,
347        page_id,
348    )
349    .fetch_all(&mut *conn)
350    .await?;
351    Ok(exercises)
352}
353
354pub async fn get_exercises_by_exam_id(
355    conn: &mut PgConnection,
356    exam_id: Uuid,
357) -> ModelResult<Vec<Exercise>> {
358    let exercises = sqlx::query_as!(
359        Exercise,
360        r#"
361SELECT *
362FROM exercises
363WHERE exam_id = $1
364  AND deleted_at IS NULL
365"#,
366        exam_id,
367    )
368    .fetch_all(&mut *conn)
369    .await?;
370    Ok(exercises)
371}
372
373pub async fn get_course_or_exam_id(
374    conn: &mut PgConnection,
375    id: Uuid,
376) -> ModelResult<CourseOrExamId> {
377    let res = sqlx::query!(
378        "
379SELECT course_id,
380  exam_id
381FROM exercises
382WHERE id = $1
383",
384        id
385    )
386    .fetch_one(conn)
387    .await?;
388    CourseOrExamId::from_course_and_exam_ids(res.course_id, res.exam_id)
389}
390
391pub async fn get_course_material_exercise(
392    conn: &mut PgConnection,
393    user_id: Option<Uuid>,
394    exercise_id: Uuid,
395    fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
396) -> ModelResult<CourseMaterialExercise> {
397    let exercise = get_by_id(conn, exercise_id).await?;
398    let (current_exercise_slide, instance_or_exam_id) =
399        get_or_select_exercise_slide(&mut *conn, user_id, &exercise, fetch_service_info).await?;
400    info!(
401        "Current exercise slide id: {:#?}",
402        current_exercise_slide.id
403    );
404
405    let user_exercise_state = match (user_id, instance_or_exam_id) {
406        (Some(user_id), Some(course_or_exam_id)) => {
407            user_exercise_states::get_user_exercise_state_if_exists(
408                conn,
409                user_id,
410                exercise.id,
411                course_or_exam_id,
412            )
413            .await?
414        }
415        _ => None,
416    };
417
418    let can_post_submission =
419        determine_can_post_submission(&mut *conn, user_id, &exercise, &user_exercise_state).await?;
420
421    let previous_exercise_slide_submission = match user_id {
422        Some(user_id) => {
423            crate::exercise_slide_submissions::try_to_get_users_latest_exercise_slide_submission(
424                conn,
425                current_exercise_slide.id,
426                user_id,
427            )
428            .await?
429        }
430        _ => None,
431    };
432
433    let exercise_status = user_exercise_state.map(|user_exercise_state| ExerciseStatus {
434        score_given: user_exercise_state.score_given,
435        activity_progress: user_exercise_state.activity_progress,
436        grading_progress: user_exercise_state.grading_progress,
437        reviewing_stage: user_exercise_state.reviewing_stage,
438    });
439
440    let exercise_slide_submission_counts = if let Some(user_id) = user_id {
441        if let Some(cioreid) = instance_or_exam_id {
442            get_exercise_slide_submission_counts_for_exercise_user(
443                conn,
444                exercise_id,
445                cioreid,
446                user_id,
447            )
448            .await?
449        } else {
450            HashMap::new()
451        }
452    } else {
453        HashMap::new()
454    };
455
456    let peer_or_self_review_config = if let Some(course_id) = exercise.course_id {
457        if exercise.needs_peer_review || exercise.needs_self_review {
458            let prc = crate::peer_or_self_review_configs::get_by_exercise_or_course_id(
459                conn, &exercise, course_id,
460            )
461            .await
462            .optional()?;
463            prc.map(|prc| CourseMaterialPeerOrSelfReviewConfig {
464                id: prc.id,
465                course_id: prc.course_id,
466                exercise_id: prc.exercise_id,
467                peer_reviews_to_give: prc.peer_reviews_to_give,
468                peer_reviews_to_receive: prc.peer_reviews_to_receive,
469            })
470        } else {
471            None
472        }
473    } else {
474        None
475    };
476
477    let user_course_instance_exercise_service_variables = match (user_id, instance_or_exam_id) {
478        (Some(user_id), Some(course_or_exam_id)) => {
479            Some(crate::user_course_exercise_service_variables::get_all_variables_for_user_and_course_or_exam(conn, user_id, course_or_exam_id).await?)
480        }
481        _ => None,
482    }.unwrap_or_default();
483
484    let should_show_reset_message = if let Some(user_id) = user_id {
485        crate::exercise_reset_logs::user_should_see_reset_message_for_exercise(
486            conn,
487            user_id,
488            exercise_id,
489        )
490        .await?
491    } else {
492        None
493    };
494
495    Ok(CourseMaterialExercise {
496        exercise,
497        can_post_submission,
498        current_exercise_slide,
499        exercise_status,
500        exercise_slide_submission_counts,
501        peer_or_self_review_config,
502        user_course_instance_exercise_service_variables,
503        previous_exercise_slide_submission,
504        should_show_reset_message,
505    })
506}
507
508async fn determine_can_post_submission(
509    conn: &mut PgConnection,
510    user_id: Option<Uuid>,
511    exercise: &Exercise,
512    user_exercise_state: &Option<UserExerciseState>,
513) -> Result<bool, ModelError> {
514    if let Some(user_exercise_state) = user_exercise_state {
515        // 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.
516        if user_exercise_state.reviewing_stage != ReviewingStage::NotStarted {
517            return Ok(false);
518        }
519    }
520
521    let can_post_submission = if let Some(user_id) = user_id {
522        if let Some(exam_id) = exercise.exam_id {
523            exams::verify_exam_submission_can_be_made(conn, exam_id, user_id).await?
524        } else {
525            true
526        }
527    } else {
528        false
529    };
530    Ok(can_post_submission)
531}
532
533pub async fn get_or_select_exercise_slide(
534    conn: &mut PgConnection,
535    user_id: Option<Uuid>,
536    exercise: &Exercise,
537    fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
538) -> ModelResult<(CourseMaterialExerciseSlide, Option<CourseOrExamId>)> {
539    match (user_id, exercise.course_id, exercise.exam_id) {
540        (None, ..) => {
541            // No signed in user. Show random exercise without model solution.
542            let random_slide =
543                exercise_slides::get_random_exercise_slide_for_exercise(conn, exercise.id).await?;
544            let random_slide_tasks = exercise_tasks::get_course_material_exercise_tasks(
545                conn,
546                random_slide.id,
547                None,
548                fetch_service_info,
549            )
550            .await?;
551            Ok((
552                CourseMaterialExerciseSlide {
553                    id: random_slide.id,
554                    exercise_tasks: random_slide_tasks,
555                },
556                None,
557            ))
558        }
559        (Some(user_id), Some(course_id), None) => {
560            // signed in, course exercise
561            let user_course_settings = user_course_settings::get_user_course_settings_by_course_id(
562                conn, user_id, course_id,
563            )
564            .await?;
565            match user_course_settings {
566                Some(settings) if settings.current_course_id == course_id => {
567                    // User is enrolled on an instance of the given course.
568                    let course_or_exam_id: CourseOrExamId = exercise.try_into()?;
569                    let tasks =
570                        exercise_tasks::get_or_select_user_exercise_slide_for_course_or_exam(
571                            conn,
572                            user_id,
573                            exercise.id,
574                            course_or_exam_id,
575                            fetch_service_info,
576                        )
577                        .await?;
578                    Ok((tasks, Some(CourseOrExamId::Course(course_id))))
579                }
580                Some(_) => {
581                    // User is enrolled on a different language version of the course. Show exercise
582                    // slide based on their latest enrollment or a random one.
583                    let exercise_tasks =
584                        exercise_tasks::get_existing_users_exercise_slide_for_course(
585                            conn,
586                            user_id,
587                            exercise.id,
588                            course_id,
589                            &fetch_service_info,
590                        )
591                        .await?;
592                    if let Some(exercise_tasks) = exercise_tasks {
593                        Ok((exercise_tasks, Some(CourseOrExamId::Course(course_id))))
594                    } else {
595                        // no exercise task has been chosen for the user
596                        let random_slide = exercise_slides::get_random_exercise_slide_for_exercise(
597                            conn,
598                            exercise.id,
599                        )
600                        .await?;
601                        let random_tasks = exercise_tasks::get_course_material_exercise_tasks(
602                            conn,
603                            random_slide.id,
604                            Some(user_id),
605                            &fetch_service_info,
606                        )
607                        .await?;
608
609                        Ok((
610                            CourseMaterialExerciseSlide {
611                                id: random_slide.id,
612                                exercise_tasks: random_tasks,
613                            },
614                            None,
615                        ))
616                    }
617                }
618                None => {
619                    // User is not enrolled on any course version. This is not a valid scenario because
620                    // tasks are based on a specific instance.
621                    Err(ModelError::new(
622                        ModelErrorType::PreconditionFailed,
623                        "User must be enrolled to the course".to_string(),
624                        None,
625                    ))
626                }
627            }
628        }
629        (Some(user_id), _, Some(exam_id)) => {
630            info!("selecting exam task");
631            // signed in, exam exercise
632            let tasks = exercise_tasks::get_or_select_user_exercise_slide_for_course_or_exam(
633                conn,
634                user_id,
635                exercise.id,
636                CourseOrExamId::Exam(exam_id),
637                fetch_service_info,
638            )
639            .await?;
640            info!("selecting exam task {:#?}", tasks);
641            Ok((tasks, Some(CourseOrExamId::Exam(exam_id))))
642        }
643        (Some(_), ..) => Err(ModelError::new(
644            ModelErrorType::Generic,
645            "The selected exercise is not attached to any course or exam".to_string(),
646            None,
647        )),
648    }
649}
650
651pub async fn delete_exercises_by_page_id(
652    conn: &mut PgConnection,
653    page_id: Uuid,
654) -> ModelResult<Vec<Uuid>> {
655    let deleted_ids = sqlx::query!(
656        "
657UPDATE exercises
658SET deleted_at = now()
659WHERE page_id = $1
660AND deleted_at IS NULL
661RETURNING id;
662        ",
663        page_id
664    )
665    .fetch_all(conn)
666    .await?
667    .into_iter()
668    .map(|x| x.id)
669    .collect();
670    Ok(deleted_ids)
671}
672
673pub async fn set_exercise_to_use_exercise_specific_peer_or_self_review_config(
674    conn: &mut PgConnection,
675    exercise_id: Uuid,
676    needs_peer_review: bool,
677    needs_self_review: bool,
678    use_course_default_peer_or_self_review_config: bool,
679) -> ModelResult<Uuid> {
680    let id = sqlx::query!(
681        "
682UPDATE exercises
683SET use_course_default_peer_or_self_review_config = $1,
684  needs_peer_review = $2,
685  needs_self_review = $3
686WHERE id = $4
687RETURNING id;
688        ",
689        use_course_default_peer_or_self_review_config,
690        needs_peer_review,
691        needs_self_review,
692        exercise_id
693    )
694    .fetch_one(conn)
695    .await?;
696
697    Ok(id.id)
698}
699
700pub async fn get_all_exercise_statuses_by_user_id_and_course_id(
701    conn: &mut PgConnection,
702    course_id: Uuid,
703    user_id: Uuid,
704) -> ModelResult<Vec<ExerciseStatusSummaryForUser>> {
705    let course_or_exam_id = CourseOrExamId::Course(course_id);
706    // Load all the data for this user from all the exercises to memory, and group most of them to HashMaps by exercise id
707    let exercises = crate::exercises::get_exercises_by_course_id(&mut *conn, course_id).await?;
708    let mut user_exercise_states =
709        crate::user_exercise_states::get_all_for_user_and_course_or_exam(
710            &mut *conn,
711            user_id,
712            course_or_exam_id,
713        )
714        .await?
715        .into_iter()
716        .map(|ues| (ues.exercise_id, ues))
717        .collect::<HashMap<_, _>>();
718    let mut exercise_slide_submissions =
719        crate::exercise_slide_submissions::get_users_all_submissions_for_course_or_exam(
720            &mut *conn,
721            user_id,
722            course_or_exam_id,
723        )
724        .await?
725        .into_iter()
726        .into_group_map_by(|o| o.exercise_id);
727    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()
728        .into_group_map_by(|o| o.exercise_id);
729    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()
730        .into_group_map_by(|o| o.exercise_id);
731    let given_peer_or_self_review_submission_ids = given_peer_or_self_review_submissions
732        .values()
733        .flatten()
734        .map(|x| x.id)
735        .collect::<Vec<_>>();
736    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?
737        .into_iter()
738        .into_group_map_by(|o| {
739            let peer_review_submission = given_peer_or_self_review_submissions.clone().into_iter()
740                .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
741                .unwrap_or_else(|| (Uuid::nil(), vec![]));
742            peer_review_submission.0
743    });
744    let received_peer_or_self_review_submission_ids = received_peer_or_self_review_submissions
745        .values()
746        .flatten()
747        .map(|x| x.id)
748        .collect::<Vec<_>>();
749    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()
750    .into_group_map_by(|o| {
751        let peer_review_submission = received_peer_or_self_review_submissions.clone().into_iter()
752            .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
753            .unwrap_or_else(|| (Uuid::nil(), vec![]));
754        peer_review_submission.0
755    });
756    let mut peer_review_queue_entries =
757        crate::peer_review_queue_entries::get_all_by_user_and_course_id(
758            &mut *conn, user_id, course_id,
759        )
760        .await?
761        .into_iter()
762        .map(|x| (x.exercise_id, x))
763        .collect::<HashMap<_, _>>();
764    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()
765    .filter_map(|tgd| {
766        let user_exercise_state = user_exercise_states.clone().into_iter()
767            .find(|(_exercise_id, ues)|  ues.id == tgd.user_exercise_state_id)?;
768        Some((user_exercise_state.0, tgd))
769    }).collect::<HashMap<_, _>>();
770    let all_peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
771        .iter()
772        .chain(received_peer_or_self_review_question_submissions.iter())
773        .flat_map(|(_exercise_id, prqs)| prqs.iter().map(|p| p.peer_or_self_review_question_id))
774        .collect::<Vec<_>>();
775    let all_peer_or_self_review_questions = crate::peer_or_self_review_questions::get_by_ids(
776        &mut *conn,
777        &all_peer_or_self_review_question_ids,
778    )
779    .await?;
780
781    // Map all the data for all the exercises to be summaries of the data for each exercise.
782    //
783    // Since all data is in hashmaps grouped by exercise id, and we iterate though every
784    // exercise id exactly once, we can just remove the data for the exercise from the
785    // hashmaps and avoid extra copying.
786    let res = exercises
787        .into_iter()
788        .map(|exercise| {
789            let user_exercise_state = user_exercise_states.remove(&exercise.id);
790            let exercise_slide_submissions = exercise_slide_submissions
791                .remove(&exercise.id)
792                .unwrap_or_default();
793            let given_peer_or_self_review_submissions = given_peer_or_self_review_submissions
794                .remove(&exercise.id)
795                .unwrap_or_default();
796            let received_peer_or_self_review_submissions = received_peer_or_self_review_submissions
797                .remove(&exercise.id)
798                .unwrap_or_default();
799            let given_peer_or_self_review_question_submissions =
800                given_peer_or_self_review_question_submissions
801                    .remove(&exercise.id)
802                    .unwrap_or_default();
803            let received_peer_or_self_review_question_submissions =
804                received_peer_or_self_review_question_submissions
805                    .remove(&exercise.id)
806                    .unwrap_or_default();
807            let peer_review_queue_entry = peer_review_queue_entries.remove(&exercise.id);
808            let teacher_grading_decision = teacher_grading_decisions.remove(&exercise.id);
809            let peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
810                .iter()
811                .chain(received_peer_or_self_review_question_submissions.iter())
812                .map(|prqs| prqs.peer_or_self_review_question_id)
813                .unique()
814                .collect::<Vec<_>>();
815            let peer_or_self_review_questions = all_peer_or_self_review_questions
816                .iter()
817                .filter(|prq| peer_or_self_review_question_ids.contains(&prq.id))
818                .cloned()
819                .collect::<Vec<_>>();
820            ExerciseStatusSummaryForUser {
821                exercise,
822                user_exercise_state,
823                exercise_slide_submissions,
824                given_peer_or_self_review_submissions,
825                received_peer_or_self_review_submissions,
826                given_peer_or_self_review_question_submissions,
827                received_peer_or_self_review_question_submissions,
828                peer_review_queue_entry,
829                teacher_grading_decision,
830                peer_or_self_review_questions,
831            }
832        })
833        .collect::<Vec<_>>();
834    Ok(res)
835}
836
837pub async fn get_exercises_by_module_containing_exercise_type(
838    conn: &mut PgConnection,
839    exercise_type: &str,
840    course_module_id: Uuid,
841) -> ModelResult<Vec<Exercise>> {
842    let res: Vec<Exercise> = sqlx::query_as!(
843        Exercise,
844        r#"
845SELECT DISTINCT(ex.*)
846FROM exercises ex
847  JOIN exercise_slides slides ON ex.id = slides.exercise_id
848  JOIN exercise_tasks tasks ON slides.id = tasks.exercise_slide_id
849  JOIN chapters c ON ex.chapter_id = c.id
850where tasks.exercise_type = $1
851  AND c.course_module_id = $2
852  AND ex.deleted_at IS NULL
853  AND tasks.deleted_at IS NULL
854  and c.deleted_at IS NULL
855  and slides.deleted_at IS NULL
856        "#,
857        exercise_type,
858        course_module_id
859    )
860    .fetch_all(conn)
861    .await?;
862    Ok(res)
863}
864
865/// Collects user_ids and related exercise_ids according to given filters
866pub async fn collect_user_ids_and_exercise_ids_for_reset(
867    conn: &mut PgConnection,
868    user_ids: &[Uuid],
869    exercise_ids: &[Uuid],
870    threshold: Option<f64>,
871    reset_all_below_max: bool,
872    reset_only_locked_reviews: bool,
873) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
874    let results = sqlx::query!(
875        r#"
876SELECT DISTINCT ues.user_id,
877  ues.exercise_id
878FROM user_exercise_states ues
879  LEFT JOIN exercises e ON ues.exercise_id = e.id
880WHERE ues.user_id = ANY($1)
881  AND ues.exercise_id = ANY($2)
882  AND ues.deleted_at IS NULL
883  AND (
884    $3 = FALSE
885    OR ues.score_given < e.score_maximum
886  )
887  AND (
888    $4::FLOAT IS NULL
889    OR ues.score_given < $4::FLOAT
890  )
891  AND (
892    $5 = FALSE
893    OR ues.reviewing_stage = 'reviewed_and_locked'
894  )
895            "#,
896        user_ids,
897        exercise_ids,
898        reset_all_below_max,
899        threshold,
900        reset_only_locked_reviews
901    )
902    .fetch_all(&mut *conn)
903    .await?;
904
905    let mut user_exercise_map: HashMap<Uuid, Vec<Uuid>> = HashMap::new();
906    for row in &results {
907        user_exercise_map
908            .entry(row.user_id)
909            .or_default()
910            .push(row.exercise_id);
911    }
912
913    Ok(user_exercise_map.into_iter().collect())
914}
915
916/// Resets all related tables for selected users and related exercises
917pub async fn reset_exercises_for_selected_users(
918    conn: &mut PgConnection,
919    users_and_exercises: &[(Uuid, Vec<Uuid>)],
920    reset_by: Option<Uuid>,
921    course_id: Uuid,
922    reason: Option<String>,
923) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
924    let mut successful_resets = Vec::new();
925    let mut tx = conn.begin().await?;
926    for (user_id, exercise_ids) in users_and_exercises {
927        sqlx::query!(
928            r#"
929UPDATE exercise_slide_submissions
930SET deleted_at = NOW()
931WHERE user_id = $1
932  AND exercise_id = ANY($2)
933  AND deleted_at IS NULL
934            "#,
935            user_id,
936            exercise_ids
937        )
938        .execute(&mut *tx)
939        .await?;
940
941        sqlx::query!(
942            r#"
943UPDATE exercise_task_submissions
944SET deleted_at = NOW()
945WHERE exercise_slide_submission_id IN (
946    SELECT id
947    FROM exercise_slide_submissions
948    WHERE user_id = $1
949      AND exercise_id = ANY($2)
950  )
951  AND deleted_at IS NULL
952            "#,
953            user_id,
954            exercise_ids
955        )
956        .execute(&mut *tx)
957        .await?;
958
959        sqlx::query!(
960            r#"
961UPDATE peer_review_queue_entries
962SET deleted_at = NOW()
963WHERE user_id = $1
964  AND exercise_id = ANY($2)
965  AND deleted_at IS NULL
966            "#,
967            user_id,
968            exercise_ids
969        )
970        .execute(&mut *tx)
971        .await?;
972
973        sqlx::query!(
974            r#"
975UPDATE exercise_task_gradings
976SET deleted_at = NOW()
977WHERE exercise_task_submission_id IN (
978    SELECT id
979    FROM exercise_task_submissions
980    WHERE exercise_slide_submission_id IN (
981        SELECT id
982        FROM exercise_slide_submissions
983        WHERE user_id = $1
984          AND exercise_id = ANY($2)
985      )
986  )
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 user_exercise_states
998SET deleted_at = NOW()
999WHERE user_id = $1
1000  AND exercise_id = ANY($2)
1001  AND deleted_at IS NULL
1002            "#,
1003            user_id,
1004            exercise_ids
1005        )
1006        .execute(&mut *tx)
1007        .await?;
1008
1009        sqlx::query!(
1010            r#"
1011UPDATE user_exercise_task_states
1012SET deleted_at = NOW()
1013WHERE user_exercise_slide_state_id IN (
1014    SELECT id
1015    FROM user_exercise_slide_states
1016    WHERE user_exercise_state_id IN (
1017        SELECT id
1018        FROM user_exercise_states
1019        WHERE user_id = $1
1020          AND exercise_id = ANY($2)
1021      )
1022  )
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_slide_states
1034SET deleted_at = NOW()
1035WHERE user_exercise_state_id IN (
1036    SELECT id
1037    FROM user_exercise_states
1038    WHERE user_id = $1
1039      AND exercise_id = ANY($2)
1040  )
1041  AND deleted_at IS NULL
1042            "#,
1043            user_id,
1044            exercise_ids
1045        )
1046        .execute(&mut *tx)
1047        .await?;
1048
1049        sqlx::query!(
1050            r#"
1051UPDATE teacher_grading_decisions
1052SET deleted_at = NOW()
1053WHERE user_exercise_state_id IN (
1054    SELECT id
1055    FROM user_exercise_states
1056    WHERE user_id = $1
1057      AND exercise_id = ANY($2)
1058  )
1059  AND deleted_at IS NULL
1060            "#,
1061            user_id,
1062            exercise_ids
1063        )
1064        .execute(&mut *tx)
1065        .await?;
1066
1067        // Adds a log of a reset exercise for a user
1068        exercise_reset_logs::log_exercise_reset(
1069            &mut tx,
1070            reset_by,
1071            *user_id,
1072            exercise_ids,
1073            course_id,
1074            reason.clone(),
1075        )
1076        .await?;
1077
1078        successful_resets.push((*user_id, exercise_ids.to_vec()));
1079    }
1080    tx.commit().await?;
1081    Ok(successful_resets)
1082}
1083
1084#[cfg(test)]
1085mod test {
1086    use super::*;
1087    use crate::{
1088        course_instance_enrollments::{self, NewCourseInstanceEnrollment},
1089        exercise_service_info::{self, PathInfo},
1090        exercise_services::{self, ExerciseServiceNewOrUpdate},
1091        test_helper::Conn,
1092        test_helper::*,
1093        user_exercise_states,
1094    };
1095
1096    #[tokio::test]
1097    async fn selects_course_material_exercise_for_enrolled_student() {
1098        insert_data!(
1099            :tx,
1100            user: user_id,
1101            org: organization_id,
1102            course: course_id,
1103            instance: course_instance,
1104            :course_module,
1105            chapter: chapter_id,
1106            page: page_id,
1107            exercise: exercise_id,
1108            slide: exercise_slide_id,
1109            task: exercise_task_id
1110        );
1111        let exercise_service = exercise_services::insert_exercise_service(
1112            tx.as_mut(),
1113            &ExerciseServiceNewOrUpdate {
1114                name: "text-exercise".to_string(),
1115                slug: TEST_HELPER_EXERCISE_SERVICE_NAME.to_string(),
1116                public_url: "https://example.com".to_string(),
1117                internal_url: None,
1118                max_reprocessing_submissions_at_once: 1,
1119            },
1120        )
1121        .await
1122        .unwrap();
1123        let _exercise_service_info = exercise_service_info::insert(
1124            tx.as_mut(),
1125            &PathInfo {
1126                exercise_service_id: exercise_service.id,
1127                user_interface_iframe_path: "/iframe".to_string(),
1128                grade_endpoint_path: "/grade".to_string(),
1129                public_spec_endpoint_path: "/public-spec".to_string(),
1130                model_solution_spec_endpoint_path: "test-only-empty-path".to_string(),
1131                has_custom_view: false,
1132            },
1133        )
1134        .await
1135        .unwrap();
1136        course_instance_enrollments::insert_enrollment_and_set_as_current(
1137            tx.as_mut(),
1138            NewCourseInstanceEnrollment {
1139                course_id,
1140                course_instance_id: course_instance.id,
1141                user_id,
1142            },
1143        )
1144        .await
1145        .unwrap();
1146
1147        let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1148            tx.as_mut(),
1149            user_id,
1150            exercise_id,
1151            CourseOrExamId::Course(course_id),
1152        )
1153        .await
1154        .unwrap();
1155        assert!(user_exercise_state.is_none());
1156
1157        let exercise = get_course_material_exercise(
1158            tx.as_mut(),
1159            Some(user_id),
1160            exercise_id,
1161            |_| unimplemented!(),
1162        )
1163        .await
1164        .unwrap();
1165        assert_eq!(
1166            exercise
1167                .current_exercise_slide
1168                .exercise_tasks
1169                .first()
1170                .unwrap()
1171                .id,
1172            exercise_task_id
1173        );
1174
1175        let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1176            tx.as_mut(),
1177            user_id,
1178            exercise_id,
1179            CourseOrExamId::Course(course_id),
1180        )
1181        .await
1182        .unwrap();
1183        assert_eq!(
1184            user_exercise_state
1185                .unwrap()
1186                .selected_exercise_slide_id
1187                .unwrap(),
1188            exercise_slide_id
1189        );
1190    }
1191}