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
137    let prev_user_exercise_state = required_data.current_user_exercise_state.clone();
138
139    let derived_user_exercise_state = state_deriver::derive_new_user_exercise_state(required_data)?;
140
141    // Try to avoid updating if nothing changed
142    if derived_user_exercise_state
143        == (UserExerciseStateUpdate {
144            id: prev_user_exercise_state.id,
145            score_given: prev_user_exercise_state.score_given,
146            activity_progress: prev_user_exercise_state.activity_progress,
147            reviewing_stage: prev_user_exercise_state.reviewing_stage,
148            grading_progress: prev_user_exercise_state.grading_progress,
149        })
150    {
151        info!("Update resulting in no changes, not updating the database.");
152        return Ok(prev_user_exercise_state);
153    }
154
155    let new_saved_user_exercise_state =
156        user_exercise_states::update(conn, derived_user_exercise_state).await?;
157
158    // Always when the user_exercise_state updates, we need to also check if the user has completed the course.
159    if let Some(course_instance_id) = new_saved_user_exercise_state.course_instance_id {
160        let course_module = course_modules::get_by_exercise_id(conn, exercise_id).await?;
161        super::progressing::update_automatic_completion_status_and_grant_if_eligible(
162            conn,
163            &course_module,
164            course_instance_id,
165            new_saved_user_exercise_state.user_id,
166        )
167        .await?;
168    }
169    Ok(new_saved_user_exercise_state)
170}