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