Skip to main content

headless_lms_server/programs/
peer_review_updater.rs

1use crate::config::program_config::ProgramConfig;
2use crate::setup_tracing;
3use dotenvy::dotenv;
4use headless_lms_models::error::TryToOptional;
5use headless_lms_models::peer_review_queue_entries;
6use sqlx::{Connection, PgConnection};
7
8async fn process_course_instance(
9    conn: &mut PgConnection,
10    course_instance: &headless_lms_models::course_instances::CourseInstance,
11    now: chrono::DateTime<chrono::Utc>,
12) -> anyhow::Result<(i32, i32)> {
13    let mut moved_to_manual_review = 0;
14    let mut given_full_points = 0;
15
16    let mut earliest_manual_review_cutoff = now - chrono::Duration::days(7 * 3);
17
18    // Process manual review cases
19    let all_exercises_in_course_instance =
20        headless_lms_models::exercises::get_exercises_by_course_id(conn, course_instance.course_id)
21            .await?;
22
23    for exercise in all_exercises_in_course_instance.iter() {
24        if !exercise.needs_peer_review {
25            continue;
26        }
27        let course_id = exercise.course_id;
28        if let Some(course_id) = course_id {
29            let exercise_config =
30                headless_lms_models::peer_or_self_review_configs::get_by_exercise_or_course_id(
31                    conn, exercise, course_id,
32                )
33                .await
34                .optional()?;
35
36            if let Some(exercise_config) = exercise_config {
37                let manual_review_cutoff_in_days = exercise_config.manual_review_cutoff_in_days;
38                let timestamp = now - chrono::Duration::days(manual_review_cutoff_in_days.into());
39
40                if timestamp < earliest_manual_review_cutoff {
41                    earliest_manual_review_cutoff = timestamp;
42                }
43
44                let should_be_added_to_manual_review = headless_lms_models::peer_review_queue_entries::get_entries_that_need_reviews_and_are_older_than_with_exercise_id(conn, exercise.id, timestamp).await?;
45                if !should_be_added_to_manual_review.is_empty() {
46                    info!(exercise.id = ?exercise.id, "Found {:?} answers that have been added to the peer review queue before {:?} and have not received enough peer reviews or have not been reviewed manually. Adding them to be manually reviewed by the teachers.", should_be_added_to_manual_review.len(), timestamp);
47
48                    for peer_review_queue_entry in should_be_added_to_manual_review {
49                        if let Err(err) =
50                            peer_review_queue_entries::remove_from_queue_and_add_to_manual_review(
51                                conn,
52                                &peer_review_queue_entry,
53                            )
54                            .await
55                        {
56                            error!(
57                                peer_review_queue_entry = ?peer_review_queue_entry,
58                                "Failed to remove entry from queue and add to manual review: {:?}",
59                                err
60                            );
61                            continue;
62                        }
63                        moved_to_manual_review += 1;
64                    }
65                }
66            } else {
67                warn!(exercise.id = ?exercise.id, "No peer review config found for exercise {:?}", exercise.id);
68            }
69        }
70    }
71
72    // After this date, we will assume that the teacher has given up on reviewing answers, so we will consider queue entries older than this to have passed the peer review.
73    let pass_automatically_cutoff = earliest_manual_review_cutoff - chrono::Duration::days(90);
74
75    // Process automatic pass cases
76    let should_pass = headless_lms_models::peer_review_queue_entries::get_entries_that_need_teacher_review_and_are_older_than_with_course_id(conn, course_instance.course_id, pass_automatically_cutoff).await?;
77    if !should_pass.is_empty() {
78        info!(course_instance_id = ?course_instance.id, "Found {:?} answers that have been added to the peer review queue before {:?}. The teacher has not reviewed the answers manually after 3 months. Giving them full points.", should_pass.len(), pass_automatically_cutoff);
79        for peer_review_queue_entry in should_pass {
80            if let Err(err) = peer_review_queue_entries::remove_from_queue_and_give_full_points(
81                conn,
82                &peer_review_queue_entry,
83            )
84            .await
85            {
86                error!(
87                    peer_review_queue_entry = ?peer_review_queue_entry,
88                    "Failed to remove entry from queue and give full points: {:?}",
89                    err
90                );
91                continue;
92            }
93            given_full_points += 1;
94        }
95    }
96
97    Ok((moved_to_manual_review, given_full_points))
98}
99
100pub async fn main() -> anyhow::Result<()> {
101    dotenv().ok();
102    ProgramConfig::ensure_default_rust_log_for_workers();
103    setup_tracing()?;
104    let db_url = ProgramConfig::database_url_with_default();
105    let mut conn = PgConnection::connect(&db_url).await?;
106    let now = chrono::offset::Utc::now();
107
108    info!("Peer review updater started");
109    // Doing the update in small parts so that we don't end up constructing too heavy queries and so that we can get more frequeent log messages about the progress
110    let all_course_instances =
111        headless_lms_models::course_instances::get_all_course_instances(&mut conn).await?;
112    info!(
113        "Processing {:?} course instances",
114        all_course_instances.len()
115    );
116
117    info!(
118        earliest_manual_review_cutoff = ?now - chrono::Duration::days(7 * 3),
119        "Finding answers to move to manual review"
120    );
121
122    let mut total_moved_to_manual_review = 0;
123    let mut total_given_full_points = 0;
124
125    for course_instance in all_course_instances.iter() {
126        let (moved_to_manual_review, given_full_points) =
127            process_course_instance(&mut conn, course_instance, now).await?;
128
129        total_moved_to_manual_review += moved_to_manual_review;
130        total_given_full_points += given_full_points;
131    }
132
133    info!(
134        "Total answers moved to manual review: {:?}",
135        total_moved_to_manual_review
136    );
137    info!(
138        "Total answers given full points: {:?}",
139        total_given_full_points
140    );
141    info!("All done!");
142    Ok(())
143}