headless_lms_models/
exercises.rs

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