headless_lms_server/programs/
peer_review_updater.rs

1use crate::setup_tracing;
2use dotenv::dotenv;
3use headless_lms_models::error::TryToOptional;
4use headless_lms_models::peer_review_queue_entries;
5use sqlx::{Connection, PgConnection};
6use std::env;
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    // TODO: Audit that the environment access only happens in single-threaded code.
102    unsafe { env::set_var("RUST_LOG", "info,actix_web=info,sqlx=warn") };
103    dotenv().ok();
104    setup_tracing()?;
105    let db_url = env::var("DATABASE_URL")
106        .unwrap_or_else(|_| "postgres://localhost/headless_lms_dev".to_string());
107    let mut conn = PgConnection::connect(&db_url).await?;
108    let now = chrono::offset::Utc::now();
109
110    info!("Peer review updater started");
111    // 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
112    let all_course_instances =
113        headless_lms_models::course_instances::get_all_course_instances(&mut conn).await?;
114    info!(
115        "Processing {:?} course instances",
116        all_course_instances.len()
117    );
118
119    info!(
120        earliest_manual_review_cutoff = ?now - chrono::Duration::days(7 * 3),
121        "Finding answers to move to manual review"
122    );
123
124    let mut total_moved_to_manual_review = 0;
125    let mut total_given_full_points = 0;
126
127    for course_instance in all_course_instances.iter() {
128        let (moved_to_manual_review, given_full_points) =
129            process_course_instance(&mut conn, course_instance, now).await?;
130
131        total_moved_to_manual_review += moved_to_manual_review;
132        total_given_full_points += given_full_points;
133    }
134
135    info!(
136        "Total answers moved to manual review: {:?}",
137        total_moved_to_manual_review
138    );
139    info!(
140        "Total answers given full points: {:?}",
141        total_given_full_points
142    );
143    info!("All done!");
144    Ok(())
145}