headless_lms_server/programs/
peer_review_updater.rs1use 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    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    let pass_automatically_cutoff = earliest_manual_review_cutoff - chrono::Duration::days(90);
74
75    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    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    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}