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_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 let pass_automatically_cutoff = earliest_manual_review_cutoff - chrono::Duration::days(90);
77
78 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 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 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}