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