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_instance_id(
21            conn,
22            course_instance.id,
23        )
24        .await?;
25
26    for exercise in all_exercises_in_course_instance.iter() {
27        if !exercise.needs_peer_review {
28            continue;
29        }
30        let course_id = exercise.course_id;
31        if let Some(course_id) = course_id {
32            let exercise_config =
33                headless_lms_models::peer_or_self_review_configs::get_by_exercise_or_course_id(
34                    conn, exercise, course_id,
35                )
36                .await
37                .optional()?;
38
39            if let Some(exercise_config) = exercise_config {
40                let manual_review_cutoff_in_days = exercise_config.manual_review_cutoff_in_days;
41                let timestamp = now - chrono::Duration::days(manual_review_cutoff_in_days.into());
42
43                if timestamp < earliest_manual_review_cutoff {
44                    earliest_manual_review_cutoff = timestamp;
45                }
46
47                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?;
48                if !should_be_added_to_manual_review.is_empty() {
49                    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);
50
51                    for peer_review_queue_entry in should_be_added_to_manual_review {
52                        if let Err(err) =
53                            peer_review_queue_entries::remove_from_queue_and_add_to_manual_review(
54                                conn,
55                                &peer_review_queue_entry,
56                            )
57                            .await
58                        {
59                            error!(
60                                peer_review_queue_entry = ?peer_review_queue_entry,
61                                "Failed to remove entry from queue and add to manual review: {:?}",
62                                err
63                            );
64                            continue;
65                        }
66                        moved_to_manual_review += 1;
67                    }
68                }
69            } else {
70                warn!(exercise.id = ?exercise.id, "No peer review config found for exercise {:?}", exercise.id);
71            }
72        }
73    }
74
75    // 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.
76    let pass_automatically_cutoff = earliest_manual_review_cutoff - chrono::Duration::days(90);
77
78    // Process automatic pass cases
79    let should_pass = headless_lms_models::peer_review_queue_entries::get_entries_that_need_teacher_review_and_are_older_than_with_course_instance_id(conn, course_instance.id, pass_automatically_cutoff).await?;
80    if !should_pass.is_empty() {
81        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);
82        for peer_review_queue_entry in should_pass {
83            if let Err(err) = peer_review_queue_entries::remove_from_queue_and_give_full_points(
84                conn,
85                &peer_review_queue_entry,
86            )
87            .await
88            {
89                error!(
90                    peer_review_queue_entry = ?peer_review_queue_entry,
91                    "Failed to remove entry from queue and give full points: {:?}",
92                    err
93                );
94                continue;
95            }
96            given_full_points += 1;
97        }
98    }
99
100    Ok((moved_to_manual_review, given_full_points))
101}
102
103pub async fn main() -> anyhow::Result<()> {
104    // TODO: Audit that the environment access only happens in single-threaded code.
105    unsafe { env::set_var("RUST_LOG", "info,actix_web=info,sqlx=warn") };
106    dotenv().ok();
107    setup_tracing()?;
108    let db_url = env::var("DATABASE_URL")
109        .unwrap_or_else(|_| "postgres://localhost/headless_lms_dev".to_string());
110    let mut conn = PgConnection::connect(&db_url).await?;
111    let now = chrono::offset::Utc::now();
112
113    info!("Peer review updater started");
114    // 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
115    let all_course_instances =
116        headless_lms_models::course_instances::get_all_course_instances(&mut conn).await?;
117    info!(
118        "Processing {:?} course instances",
119        all_course_instances.len()
120    );
121
122    info!(
123        earliest_manual_review_cutoff = ?now - chrono::Duration::days(7 * 3),
124        "Finding answers to move to manual review"
125    );
126
127    let mut total_moved_to_manual_review = 0;
128    let mut total_given_full_points = 0;
129
130    for course_instance in all_course_instances.iter() {
131        let (moved_to_manual_review, given_full_points) =
132            process_course_instance(&mut conn, course_instance, now).await?;
133
134        total_moved_to_manual_review += moved_to_manual_review;
135        total_given_full_points += given_full_points;
136    }
137
138    info!(
139        "Total answers moved to manual review: {:?}",
140        total_moved_to_manual_review
141    );
142    info!(
143        "Total answers given full points: {:?}",
144        total_given_full_points
145    );
146    info!("All done!");
147    Ok(())
148}