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