headless_lms_models/library/user_exercise_state_updater/
mod.rs

1//! Always update the user_exercise_state table though this module
2//!
3
4// Internal modules, not public to make sure someone does not accidentally import them and mess things up.
5mod data_loader;
6mod state_deriver;
7mod validation;
8
9use crate::{
10    chapters::DatabaseChapter,
11    course_modules,
12    courses::Course,
13    exercise_slide_submissions::ExerciseSlideSubmission,
14    exercises::Exercise,
15    peer_or_self_review_configs::PeerOrSelfReviewConfig,
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::TeacherGradingDecision,
22    user_exercise_slide_states::UserExerciseSlideStateGradingSummary,
23    user_exercise_states::{self, UserExerciseState, UserExerciseStateUpdate},
24};
25
26use std::default::Default;
27
28/// Visible only in the current module (and submodules) to prevent misuse.
29#[derive(Debug)]
30pub struct UserExerciseStateUpdateRequiredData {
31    pub exercise: Exercise,
32    pub current_user_exercise_state: UserExerciseState,
33    /// None if peer review is not enabled for the exercise
34    pub peer_or_self_review_information:
35        Option<UserExerciseStateUpdateRequiredDataPeerReviewInformation>,
36    /// None if a teacher has not made a grading decision yet.
37    pub latest_teacher_grading_decision: Option<TeacherGradingDecision>,
38    /// The grades summed up from all the user exercise slide states. Note that multiple slides can give points, and they are all aggregated here.
39    pub user_exercise_slide_state_grading_summary: UserExerciseSlideStateGradingSummary,
40    /// Chapter information if the exercise belongs to a chapter. Used to check if chapter_locking_enabled is enabled.
41    pub chapter: Option<DatabaseChapter>,
42    /// Course information. Used to check if chapter_locking_enabled is enabled.
43    pub course: Option<Course>,
44}
45
46/// Visible only in the current module (and submodules) to prevent misuse.
47#[derive(Debug)]
48pub struct UserExerciseStateUpdateRequiredDataPeerReviewInformation {
49    pub given_peer_or_self_review_submissions: Vec<PeerOrSelfReviewSubmission>,
50    pub given_self_review_submission: Option<PeerOrSelfReviewSubmission>,
51    pub latest_exercise_slide_submission_received_peer_or_self_review_question_submissions:
52        Vec<PeerOrSelfReviewQuestionSubmission>,
53    pub peer_review_queue_entry: Option<PeerReviewQueueEntry>,
54    pub peer_or_self_review_config: PeerOrSelfReviewConfig,
55    pub peer_or_self_review_questions: Vec<PeerOrSelfReviewQuestion>,
56}
57
58/**
59Same as `UserExerciseStateUpdateRequiredData` but public and everything is optional. Can be used to pass some already loaded dependencies to the update function.
60*/
61#[derive(Default)]
62pub struct UserExerciseStateUpdateAlreadyLoadedRequiredData {
63    pub exercise: Option<Exercise>,
64    pub current_user_exercise_state: Option<UserExerciseState>,
65    pub peer_or_self_review_information:
66        Option<UserExerciseStateUpdateAlreadyLoadedRequiredDataPeerReviewInformation>,
67    /// The outer option is to indicate whether this cached value is provided or not, and the inner option is to tell whether a teacher has made a grading decision or not.
68    pub latest_teacher_grading_decision: Option<Option<TeacherGradingDecision>>,
69    pub user_exercise_slide_state_grading_summary: Option<UserExerciseSlideStateGradingSummary>,
70    pub chapter: Option<Option<DatabaseChapter>>,
71    pub course: Option<Course>,
72}
73
74/**
75Same as `UserExerciseStateUpdateRequiredDataPeerReviewInformation` but public and everything is optional. Can be used to pass some already loaded dependencies to the update function.
76*/
77#[derive(Default)]
78pub struct UserExerciseStateUpdateAlreadyLoadedRequiredDataPeerReviewInformation {
79    pub given_peer_or_self_review_submissions: Option<Vec<PeerOrSelfReviewSubmission>>,
80    pub given_self_review_submission: Option<Option<PeerOrSelfReviewSubmission>>,
81    pub latest_exercise_slide_submission: Option<ExerciseSlideSubmission>,
82    pub latest_exercise_slide_submission_received_peer_or_self_review_question_submissions:
83        Option<Vec<PeerOrSelfReviewQuestionSubmission>>,
84    /// The outer option is to indicate whether this cached value is provided or not, and the inner option is to tell whether the answer has been added to the the peer review queue or not
85    pub peer_review_queue_entry: Option<Option<PeerReviewQueueEntry>>,
86    pub peer_or_self_review_config: Option<PeerOrSelfReviewConfig>,
87    pub peer_or_self_review_questions: Option<Vec<PeerOrSelfReviewQuestion>>,
88}
89
90/// Loads all required data and updates user_exercise_state. Also creates completions if needed.
91pub async fn update_user_exercise_state(
92    conn: &mut PgConnection,
93    user_exercise_state_id: Uuid,
94) -> ModelResult<UserExerciseState> {
95    update_user_exercise_state_with_some_already_loaded_data(
96        conn,
97        user_exercise_state_id,
98        // Fills all the fields with None so that all the data will be loaded from the database.
99        Default::default(),
100    )
101    .await
102}
103
104/**
105Allows you to pass some data that `update_user_exercise_state` fetches to avoid repeating SQL queries for performance. Note that the caller must be careful that it passes correct data to the function. A good rule of thumb is that this function expects unmodified data directly from the database.
106
107Usage:
108
109```no_run
110# use headless_lms_models::library::user_exercise_state_updater::{update_user_exercise_state_with_some_already_loaded_data, UserExerciseStateUpdateAlreadyLoadedRequiredData};
111# use headless_lms_models::ModelResult;
112#
113# async fn example_function() -> ModelResult<()> {
114# let conn = panic!("Placeholder");
115# let user_exercise_state_id = panic!("Placeholder");
116# let previously_loaded_exercise = panic!("Placeholder");
117update_user_exercise_state_with_some_already_loaded_data(
118    conn,
119    user_exercise_state_id,
120    UserExerciseStateUpdateAlreadyLoadedRequiredData {
121        exercise: previously_loaded_exercise,
122        // Allows us to omit the data we have not manually loaded by setting `None` to all the other fields.
123        ..Default::default()
124    },
125)
126.await?;
127# Ok(())
128# }
129```
130*/
131#[instrument(skip(conn, already_loaded_internal_dependencies))]
132pub async fn update_user_exercise_state_with_some_already_loaded_data(
133    conn: &mut PgConnection,
134    user_exercise_state_id: Uuid,
135    already_loaded_internal_dependencies: UserExerciseStateUpdateAlreadyLoadedRequiredData,
136) -> ModelResult<UserExerciseState> {
137    let required_data = data_loader::load_required_data(
138        conn,
139        user_exercise_state_id,
140        already_loaded_internal_dependencies,
141    )
142    .await?;
143    let exercise_id = required_data.exercise.id;
144    let exercise_is_part_of_exam = required_data.exercise.exam_id.is_some();
145
146    let prev_user_exercise_state = required_data.current_user_exercise_state.clone();
147
148    let derived_user_exercise_state = state_deriver::derive_new_user_exercise_state(required_data)?;
149
150    // Try to avoid updating if nothing changed
151    if derived_user_exercise_state
152        == (UserExerciseStateUpdate {
153            id: prev_user_exercise_state.id,
154            score_given: prev_user_exercise_state.score_given,
155            activity_progress: prev_user_exercise_state.activity_progress,
156            reviewing_stage: prev_user_exercise_state.reviewing_stage,
157            grading_progress: prev_user_exercise_state.grading_progress,
158        })
159    {
160        info!("Update resulting in no changes, not updating the database.");
161        return Ok(prev_user_exercise_state);
162    }
163
164    let new_saved_user_exercise_state =
165        user_exercise_states::update(conn, derived_user_exercise_state).await?;
166
167    // Always when the user_exercise_state updates, we need to also check if the user has completed the course.
168    // Skip when the exercise belongs to an exam, as exams are not tied to course modules.
169    if !exercise_is_part_of_exam {
170        let course_module = course_modules::get_by_exercise_id(conn, exercise_id).await?;
171        super::progressing::update_automatic_completion_status_and_grant_if_eligible(
172            conn,
173            &course_module,
174            new_saved_user_exercise_state.user_id,
175        )
176        .await?;
177    }
178
179    Ok(new_saved_user_exercise_state)
180}