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}