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: bool,
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        false
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
660RETURNING id;
661        ",
662        page_id
663    )
664    .fetch_all(conn)
665    .await?
666    .into_iter()
667    .map(|x| x.id)
668    .collect();
669    Ok(deleted_ids)
670}
671
672pub async fn set_exercise_to_use_exercise_specific_peer_or_self_review_config(
673    conn: &mut PgConnection,
674    exercise_id: Uuid,
675    needs_peer_review: bool,
676    needs_self_review: bool,
677    use_course_default_peer_or_self_review_config: bool,
678) -> ModelResult<Uuid> {
679    let id = sqlx::query!(
680        "
681UPDATE exercises
682SET use_course_default_peer_or_self_review_config = $1,
683  needs_peer_review = $2,
684  needs_self_review = $3
685WHERE id = $4
686RETURNING id;
687        ",
688        use_course_default_peer_or_self_review_config,
689        needs_peer_review,
690        needs_self_review,
691        exercise_id
692    )
693    .fetch_one(conn)
694    .await?;
695
696    Ok(id.id)
697}
698
699pub async fn get_all_exercise_statuses_by_user_id_and_course_id(
700    conn: &mut PgConnection,
701    course_id: Uuid,
702    user_id: Uuid,
703) -> ModelResult<Vec<ExerciseStatusSummaryForUser>> {
704    let course_or_exam_id = CourseOrExamId::Course(course_id);
705    // Load all the data for this user from all the exercises to memory, and group most of them to HashMaps by exercise id
706    let exercises = crate::exercises::get_exercises_by_course_id(&mut *conn, course_id).await?;
707    let mut user_exercise_states =
708        crate::user_exercise_states::get_all_for_user_and_course_or_exam(
709            &mut *conn,
710            user_id,
711            course_or_exam_id,
712        )
713        .await?
714        .into_iter()
715        .map(|ues| (ues.exercise_id, ues))
716        .collect::<HashMap<_, _>>();
717    let mut exercise_slide_submissions =
718        crate::exercise_slide_submissions::get_users_all_submissions_for_course_or_exam(
719            &mut *conn,
720            user_id,
721            course_or_exam_id,
722        )
723        .await?
724        .into_iter()
725        .into_group_map_by(|o| o.exercise_id);
726    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()
727        .into_group_map_by(|o| o.exercise_id);
728    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()
729        .into_group_map_by(|o| o.exercise_id);
730    let given_peer_or_self_review_submission_ids = given_peer_or_self_review_submissions
731        .values()
732        .flatten()
733        .map(|x| x.id)
734        .collect::<Vec<_>>();
735    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?
736        .into_iter()
737        .into_group_map_by(|o| {
738            let peer_review_submission = given_peer_or_self_review_submissions.clone().into_iter()
739                .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
740                .unwrap_or_else(|| (Uuid::nil(), vec![]));
741            peer_review_submission.0
742    });
743    let received_peer_or_self_review_submission_ids = received_peer_or_self_review_submissions
744        .values()
745        .flatten()
746        .map(|x| x.id)
747        .collect::<Vec<_>>();
748    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()
749    .into_group_map_by(|o| {
750        let peer_review_submission = received_peer_or_self_review_submissions.clone().into_iter()
751            .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
752            .unwrap_or_else(|| (Uuid::nil(), vec![]));
753        peer_review_submission.0
754    });
755    let mut peer_review_queue_entries =
756        crate::peer_review_queue_entries::get_all_by_user_and_course_id(
757            &mut *conn, user_id, course_id,
758        )
759        .await?
760        .into_iter()
761        .map(|x| (x.exercise_id, x))
762        .collect::<HashMap<_, _>>();
763    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()
764    .filter_map(|tgd| {
765        let user_exercise_state = user_exercise_states.clone().into_iter()
766            .find(|(_exercise_id, ues)|  ues.id == tgd.user_exercise_state_id)?;
767        Some((user_exercise_state.0, tgd))
768    }).collect::<HashMap<_, _>>();
769    let all_peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
770        .iter()
771        .chain(received_peer_or_self_review_question_submissions.iter())
772        .flat_map(|(_exercise_id, prqs)| prqs.iter().map(|p| p.peer_or_self_review_question_id))
773        .collect::<Vec<_>>();
774    let all_peer_or_self_review_questions = crate::peer_or_self_review_questions::get_by_ids(
775        &mut *conn,
776        &all_peer_or_self_review_question_ids,
777    )
778    .await?;
779
780    // Map all the data for all the exercises to be summaries of the data for each exercise.
781    //
782    // Since all data is in hashmaps grouped by exercise id, and we iterate though every
783    // exercise id exactly once, we can just remove the data for the exercise from the
784    // hashmaps and avoid extra copying.
785    let res = exercises
786        .into_iter()
787        .map(|exercise| {
788            let user_exercise_state = user_exercise_states.remove(&exercise.id);
789            let exercise_slide_submissions = exercise_slide_submissions
790                .remove(&exercise.id)
791                .unwrap_or_default();
792            let given_peer_or_self_review_submissions = given_peer_or_self_review_submissions
793                .remove(&exercise.id)
794                .unwrap_or_default();
795            let received_peer_or_self_review_submissions = received_peer_or_self_review_submissions
796                .remove(&exercise.id)
797                .unwrap_or_default();
798            let given_peer_or_self_review_question_submissions =
799                given_peer_or_self_review_question_submissions
800                    .remove(&exercise.id)
801                    .unwrap_or_default();
802            let received_peer_or_self_review_question_submissions =
803                received_peer_or_self_review_question_submissions
804                    .remove(&exercise.id)
805                    .unwrap_or_default();
806            let peer_review_queue_entry = peer_review_queue_entries.remove(&exercise.id);
807            let teacher_grading_decision = teacher_grading_decisions.remove(&exercise.id);
808            let peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
809                .iter()
810                .chain(received_peer_or_self_review_question_submissions.iter())
811                .map(|prqs| prqs.peer_or_self_review_question_id)
812                .unique()
813                .collect::<Vec<_>>();
814            let peer_or_self_review_questions = all_peer_or_self_review_questions
815                .iter()
816                .filter(|prq| peer_or_self_review_question_ids.contains(&prq.id))
817                .cloned()
818                .collect::<Vec<_>>();
819            ExerciseStatusSummaryForUser {
820                exercise,
821                user_exercise_state,
822                exercise_slide_submissions,
823                given_peer_or_self_review_submissions,
824                received_peer_or_self_review_submissions,
825                given_peer_or_self_review_question_submissions,
826                received_peer_or_self_review_question_submissions,
827                peer_review_queue_entry,
828                teacher_grading_decision,
829                peer_or_self_review_questions,
830            }
831        })
832        .collect::<Vec<_>>();
833    Ok(res)
834}
835
836pub async fn get_exercises_by_module_containing_exercise_type(
837    conn: &mut PgConnection,
838    exercise_type: &str,
839    course_module_id: Uuid,
840) -> ModelResult<Vec<Exercise>> {
841    let res: Vec<Exercise> = sqlx::query_as!(
842        Exercise,
843        r#"
844SELECT DISTINCT(ex.*)
845FROM exercises ex
846  JOIN exercise_slides slides ON ex.id = slides.exercise_id
847  JOIN exercise_tasks tasks ON slides.id = tasks.exercise_slide_id
848  JOIN chapters c ON ex.chapter_id = c.id
849where tasks.exercise_type = $1
850  AND c.course_module_id = $2
851  AND ex.deleted_at IS NULL
852  AND tasks.deleted_at IS NULL
853  and c.deleted_at IS NULL
854  and slides.deleted_at IS NULL
855        "#,
856        exercise_type,
857        course_module_id
858    )
859    .fetch_all(conn)
860    .await?;
861    Ok(res)
862}
863
864/// Collects user_ids and related exercise_ids according to given filters
865pub async fn collect_user_ids_and_exercise_ids_for_reset(
866    conn: &mut PgConnection,
867    user_ids: &[Uuid],
868    exercise_ids: &[Uuid],
869    threshold: Option<f64>,
870    reset_all_below_max: bool,
871    reset_only_locked_reviews: bool,
872) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
873    let results = sqlx::query!(
874        r#"
875SELECT DISTINCT ues.user_id,
876  ues.exercise_id
877FROM user_exercise_states ues
878  LEFT JOIN exercises e ON ues.exercise_id = e.id
879WHERE ues.user_id = ANY($1)
880  AND ues.exercise_id = ANY($2)
881  AND ues.deleted_at IS NULL
882  AND (
883    $3 = FALSE
884    OR ues.score_given < e.score_maximum
885  )
886  AND (
887    $4::FLOAT IS NULL
888    OR ues.score_given < $4::FLOAT
889  )
890  AND (
891    $5 = FALSE
892    OR ues.reviewing_stage = 'reviewed_and_locked'
893  )
894            "#,
895        user_ids,
896        exercise_ids,
897        reset_all_below_max,
898        threshold,
899        reset_only_locked_reviews
900    )
901    .fetch_all(&mut *conn)
902    .await?;
903
904    let mut user_exercise_map: HashMap<Uuid, Vec<Uuid>> = HashMap::new();
905    for row in &results {
906        user_exercise_map
907            .entry(row.user_id)
908            .or_default()
909            .push(row.exercise_id);
910    }
911
912    Ok(user_exercise_map.into_iter().collect())
913}
914
915/// Resets all related tables for selected users and related exercises
916pub async fn reset_exercises_for_selected_users(
917    conn: &mut PgConnection,
918    users_and_exercises: &[(Uuid, Vec<Uuid>)],
919    reset_by: Uuid,
920    course_id: Uuid,
921) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
922    let mut successful_resets = Vec::new();
923    let mut tx = conn.begin().await?;
924    for (user_id, exercise_ids) in users_and_exercises {
925        sqlx::query!(
926            r#"
927UPDATE exercise_slide_submissions
928SET deleted_at = NOW()
929WHERE user_id = $1
930  AND exercise_id = ANY($2)
931  AND deleted_at IS NULL
932            "#,
933            user_id,
934            exercise_ids
935        )
936        .execute(&mut *tx)
937        .await?;
938
939        sqlx::query!(
940            r#"
941UPDATE exercise_task_submissions
942SET deleted_at = NOW()
943WHERE exercise_slide_submission_id IN (
944    SELECT id
945    FROM exercise_slide_submissions
946    WHERE user_id = $1
947      AND exercise_id = ANY($2)
948  )
949  AND deleted_at IS NULL
950            "#,
951            user_id,
952            exercise_ids
953        )
954        .execute(&mut *tx)
955        .await?;
956
957        sqlx::query!(
958            r#"
959UPDATE peer_review_queue_entries
960SET deleted_at = NOW()
961WHERE user_id = $1
962  AND exercise_id = ANY($2)
963  AND deleted_at IS NULL
964            "#,
965            user_id,
966            exercise_ids
967        )
968        .execute(&mut *tx)
969        .await?;
970
971        sqlx::query!(
972            r#"
973UPDATE exercise_task_gradings
974SET deleted_at = NOW()
975WHERE exercise_task_submission_id IN (
976    SELECT id
977    FROM exercise_task_submissions
978    WHERE exercise_slide_submission_id IN (
979        SELECT id
980        FROM exercise_slide_submissions
981        WHERE user_id = $1
982          AND exercise_id = ANY($2)
983      )
984  )
985  AND deleted_at IS NULL
986            "#,
987            user_id,
988            exercise_ids
989        )
990        .execute(&mut *tx)
991        .await?;
992
993        sqlx::query!(
994            r#"
995UPDATE user_exercise_states
996SET deleted_at = NOW()
997WHERE user_id = $1
998  AND exercise_id = ANY($2)
999  AND deleted_at IS NULL
1000            "#,
1001            user_id,
1002            exercise_ids
1003        )
1004        .execute(&mut *tx)
1005        .await?;
1006
1007        sqlx::query!(
1008            r#"
1009UPDATE user_exercise_task_states
1010SET deleted_at = NOW()
1011WHERE user_exercise_slide_state_id IN (
1012    SELECT id
1013    FROM user_exercise_slide_states
1014    WHERE user_exercise_state_id IN (
1015        SELECT id
1016        FROM user_exercise_states
1017        WHERE user_id = $1
1018          AND exercise_id = ANY($2)
1019      )
1020  )
1021  AND deleted_at IS NULL
1022            "#,
1023            user_id,
1024            exercise_ids
1025        )
1026        .execute(&mut *tx)
1027        .await?;
1028
1029        sqlx::query!(
1030            r#"
1031UPDATE user_exercise_slide_states
1032SET deleted_at = NOW()
1033WHERE user_exercise_state_id IN (
1034    SELECT id
1035    FROM user_exercise_states
1036    WHERE user_id = $1
1037      AND exercise_id = ANY($2)
1038  )
1039  AND deleted_at IS NULL
1040            "#,
1041            user_id,
1042            exercise_ids
1043        )
1044        .execute(&mut *tx)
1045        .await?;
1046
1047        sqlx::query!(
1048            r#"
1049UPDATE teacher_grading_decisions
1050SET deleted_at = NOW()
1051WHERE user_exercise_state_id IN (
1052    SELECT id
1053    FROM user_exercise_states
1054    WHERE user_id = $1
1055      AND exercise_id = ANY($2)
1056  )
1057  AND deleted_at IS NULL
1058            "#,
1059            user_id,
1060            exercise_ids
1061        )
1062        .execute(&mut *tx)
1063        .await?;
1064
1065        // Adds a log of a reset exercise for a user
1066        exercise_reset_logs::log_exercise_reset(
1067            &mut tx,
1068            reset_by,
1069            *user_id,
1070            exercise_ids,
1071            course_id,
1072        )
1073        .await?;
1074
1075        successful_resets.push((*user_id, exercise_ids.to_vec()));
1076    }
1077    tx.commit().await?;
1078    Ok(successful_resets)
1079}
1080
1081#[cfg(test)]
1082mod test {
1083    use super::*;
1084    use crate::{
1085        course_instance_enrollments::{self, NewCourseInstanceEnrollment},
1086        exercise_service_info::{self, PathInfo},
1087        exercise_services::{self, ExerciseServiceNewOrUpdate},
1088        test_helper::Conn,
1089        test_helper::*,
1090        user_exercise_states,
1091    };
1092
1093    #[tokio::test]
1094    async fn selects_course_material_exercise_for_enrolled_student() {
1095        insert_data!(
1096            :tx,
1097            user: user_id,
1098            org: organization_id,
1099            course: course_id,
1100            instance: course_instance,
1101            :course_module,
1102            chapter: chapter_id,
1103            page: page_id,
1104            exercise: exercise_id,
1105            slide: exercise_slide_id,
1106            task: exercise_task_id
1107        );
1108        let exercise_service = exercise_services::insert_exercise_service(
1109            tx.as_mut(),
1110            &ExerciseServiceNewOrUpdate {
1111                name: "text-exercise".to_string(),
1112                slug: TEST_HELPER_EXERCISE_SERVICE_NAME.to_string(),
1113                public_url: "https://example.com".to_string(),
1114                internal_url: None,
1115                max_reprocessing_submissions_at_once: 1,
1116            },
1117        )
1118        .await
1119        .unwrap();
1120        let _exercise_service_info = exercise_service_info::insert(
1121            tx.as_mut(),
1122            &PathInfo {
1123                exercise_service_id: exercise_service.id,
1124                user_interface_iframe_path: "/iframe".to_string(),
1125                grade_endpoint_path: "/grade".to_string(),
1126                public_spec_endpoint_path: "/public-spec".to_string(),
1127                model_solution_spec_endpoint_path: "test-only-empty-path".to_string(),
1128                has_custom_view: false,
1129            },
1130        )
1131        .await
1132        .unwrap();
1133        course_instance_enrollments::insert_enrollment_and_set_as_current(
1134            tx.as_mut(),
1135            NewCourseInstanceEnrollment {
1136                course_id,
1137                course_instance_id: course_instance.id,
1138                user_id,
1139            },
1140        )
1141        .await
1142        .unwrap();
1143
1144        let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1145            tx.as_mut(),
1146            user_id,
1147            exercise_id,
1148            CourseOrExamId::Course(course_id),
1149        )
1150        .await
1151        .unwrap();
1152        assert!(user_exercise_state.is_none());
1153
1154        let exercise = get_course_material_exercise(
1155            tx.as_mut(),
1156            Some(user_id),
1157            exercise_id,
1158            |_| unimplemented!(),
1159        )
1160        .await
1161        .unwrap();
1162        assert_eq!(
1163            exercise
1164                .current_exercise_slide
1165                .exercise_tasks
1166                .first()
1167                .unwrap()
1168                .id,
1169            exercise_task_id
1170        );
1171
1172        let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1173            tx.as_mut(),
1174            user_id,
1175            exercise_id,
1176            CourseOrExamId::Course(course_id),
1177        )
1178        .await
1179        .unwrap();
1180        assert_eq!(
1181            user_exercise_state
1182                .unwrap()
1183                .selected_exercise_slide_id
1184                .unwrap(),
1185            exercise_slide_id
1186        );
1187    }
1188}