headless_lms_server/programs/
peer_review_updater.rs1use 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};
7use std::env;
8
9async fn process_course_instance(
10 conn: &mut PgConnection,
11 course_instance: &headless_lms_models::course_instances::CourseInstance,
12 now: chrono::DateTime<chrono::Utc>,
13) -> anyhow::Result<(i32, i32)> {
14 let mut moved_to_manual_review = 0;
15 let mut given_full_points = 0;
16
17 let mut earliest_manual_review_cutoff = now - chrono::Duration::days(7 * 3);
18
19 let all_exercises_in_course_instance =
21 headless_lms_models::exercises::get_exercises_by_course_id(conn, course_instance.course_id)
22 .await?;
23
24 for exercise in all_exercises_in_course_instance.iter() {
25 if !exercise.needs_peer_review {
26 continue;
27 }
28 let course_id = exercise.course_id;
29 if let Some(course_id) = course_id {
30 let exercise_config =
31 headless_lms_models::peer_or_self_review_configs::get_by_exercise_or_course_id(
32 conn, exercise, course_id,
33 )
34 .await
35 .optional()?;
36
37 if let Some(exercise_config) = exercise_config {
38 let manual_review_cutoff_in_days = exercise_config.manual_review_cutoff_in_days;
39 let timestamp = now - chrono::Duration::days(manual_review_cutoff_in_days.into());
40
41 if timestamp < earliest_manual_review_cutoff {
42 earliest_manual_review_cutoff = timestamp;
43 }
44
45 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?;
46 if !should_be_added_to_manual_review.is_empty() {
47 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);
48
49 for peer_review_queue_entry in should_be_added_to_manual_review {
50 if let Err(err) =
51 peer_review_queue_entries::remove_from_queue_and_add_to_manual_review(
52 conn,
53 &peer_review_queue_entry,
54 )
55 .await
56 {
57 error!(
58 peer_review_queue_entry = ?peer_review_queue_entry,
59 "Failed to remove entry from queue and add to manual review: {:?}",
60 err
61 );
62 continue;
63 }
64 moved_to_manual_review += 1;
65 }
66 }
67 } else {
68 warn!(exercise.id = ?exercise.id, "No peer review config found for exercise {:?}", exercise.id);
69 }
70 }
71 }
72
73 let pass_automatically_cutoff = earliest_manual_review_cutoff - chrono::Duration::days(90);
75
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?;
78 if !should_pass.is_empty() {
79 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);
80 for peer_review_queue_entry in should_pass {
81 if let Err(err) = peer_review_queue_entries::remove_from_queue_and_give_full_points(
82 conn,
83 &peer_review_queue_entry,
84 )
85 .await
86 {
87 error!(
88 peer_review_queue_entry = ?peer_review_queue_entry,
89 "Failed to remove entry from queue and give full points: {:?}",
90 err
91 );
92 continue;
93 }
94 given_full_points += 1;
95 }
96 }
97
98 Ok((moved_to_manual_review, given_full_points))
99}
100
101pub async fn main() -> anyhow::Result<()> {
102 unsafe { env::set_var("RUST_LOG", "info,actix_web=info,sqlx=warn") };
104 dotenv().ok();
105 setup_tracing()?;
106 let db_url = ProgramConfig::database_url_with_default();
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}