headless_lms_models/
exercises.rs

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