diff --git a/services/headless-lms/migrations/20231030114344_add-peer-review-cutoff-time.down.sql b/services/headless-lms/migrations/20231030114344_add-peer-review-cutoff-time.down.sql new file mode 100644 index 000000000000..710d2fc2e594 --- /dev/null +++ b/services/headless-lms/migrations/20231030114344_add-peer-review-cutoff-time.down.sql @@ -0,0 +1 @@ +ALTER TABLE peer_review_configs DROP COLUMN manual_review_cutoff_in_days; diff --git a/services/headless-lms/migrations/20231030114344_add-peer-review-cutoff-time.up.sql b/services/headless-lms/migrations/20231030114344_add-peer-review-cutoff-time.up.sql new file mode 100644 index 000000000000..e67dc488e0fd --- /dev/null +++ b/services/headless-lms/migrations/20231030114344_add-peer-review-cutoff-time.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE peer_review_configs +ADD COLUMN manual_review_cutoff_in_days INTEGER NOT NULL DEFAULT 21; +COMMENT ON COLUMN peer_review_configs.manual_review_cutoff_in_days IS 'Number of days that needs to pass for the exercise submission to move to manual review'; diff --git a/services/headless-lms/models/.sqlx/query-76d4f28b04383543bc7dbee4815ab0c0d63ded052387db1dfeeaae943e02ea77.json b/services/headless-lms/models/.sqlx/query-6650edcb98f7af3bfb5afa37038a578b6e653bb45552d6a9f41bf631464badc5.json similarity index 81% rename from services/headless-lms/models/.sqlx/query-76d4f28b04383543bc7dbee4815ab0c0d63ded052387db1dfeeaae943e02ea77.json rename to services/headless-lms/models/.sqlx/query-6650edcb98f7af3bfb5afa37038a578b6e653bb45552d6a9f41bf631464badc5.json index b65eea9ce070..916e1acd222f 100644 --- a/services/headless-lms/models/.sqlx/query-76d4f28b04383543bc7dbee4815ab0c0d63ded052387db1dfeeaae943e02ea77.json +++ b/services/headless-lms/models/.sqlx/query-6650edcb98f7af3bfb5afa37038a578b6e653bb45552d6a9f41bf631464badc5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n accepting_strategy AS \"accepting_strategy: _\"\nFROM peer_review_configs\nWHERE id = $1\n AND deleted_at IS NULL\n ", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n accepting_strategy AS \"accepting_strategy: _\",\n manual_review_cutoff_in_days\nFROM peer_review_configs\nWHERE course_id = $1\n AND exercise_id IS NULL\n AND deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -63,12 +63,17 @@ } } } + }, + { + "ordinal": 10, + "name": "manual_review_cutoff_in_days", + "type_info": "Int4" } ], "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, false, true, false, true, false, false, false, false] + "nullable": [false, false, false, true, false, true, false, false, false, false, false] }, - "hash": "76d4f28b04383543bc7dbee4815ab0c0d63ded052387db1dfeeaae943e02ea77" + "hash": "6650edcb98f7af3bfb5afa37038a578b6e653bb45552d6a9f41bf631464badc5" } diff --git a/services/headless-lms/models/.sqlx/query-cad525e2bf541f11cb2e5844bd461e0c9d784ab0777f6b850d68a5ad5ec242ed.json b/services/headless-lms/models/.sqlx/query-6a832462ea028810931a4d859c0e03581ba5bef183c84aaf27428b90ed48611f.json similarity index 81% rename from services/headless-lms/models/.sqlx/query-cad525e2bf541f11cb2e5844bd461e0c9d784ab0777f6b850d68a5ad5ec242ed.json rename to services/headless-lms/models/.sqlx/query-6a832462ea028810931a4d859c0e03581ba5bef183c84aaf27428b90ed48611f.json index dc5a44f8cb78..ed6dccdd5a7d 100644 --- a/services/headless-lms/models/.sqlx/query-cad525e2bf541f11cb2e5844bd461e0c9d784ab0777f6b850d68a5ad5ec242ed.json +++ b/services/headless-lms/models/.sqlx/query-6a832462ea028810931a4d859c0e03581ba5bef183c84aaf27428b90ed48611f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n accepting_strategy AS \"accepting_strategy: _\"\nFROM peer_review_configs\nWHERE exercise_id = $1\n AND deleted_at IS NULL\n ", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n accepting_strategy AS \"accepting_strategy: _\",\n manual_review_cutoff_in_days\nFROM peer_review_configs\nWHERE exercise_id = $1\n AND deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -63,12 +63,17 @@ } } } + }, + { + "ordinal": 10, + "name": "manual_review_cutoff_in_days", + "type_info": "Int4" } ], "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, false, true, false, true, false, false, false, false] + "nullable": [false, false, false, true, false, true, false, false, false, false, false] }, - "hash": "cad525e2bf541f11cb2e5844bd461e0c9d784ab0777f6b850d68a5ad5ec242ed" + "hash": "6a832462ea028810931a4d859c0e03581ba5bef183c84aaf27428b90ed48611f" } diff --git a/services/headless-lms/models/.sqlx/query-55495b3872d09432230c60f846f69c5967fb9d3a41508aa2e4b8c823bd762fc3.json b/services/headless-lms/models/.sqlx/query-7fc6ae6c78377544002371ff340856064f3e2592498d27e37ac53ca4e74eec10.json similarity index 82% rename from services/headless-lms/models/.sqlx/query-55495b3872d09432230c60f846f69c5967fb9d3a41508aa2e4b8c823bd762fc3.json rename to services/headless-lms/models/.sqlx/query-7fc6ae6c78377544002371ff340856064f3e2592498d27e37ac53ca4e74eec10.json index 1193cd35a0cb..de5c96696e44 100644 --- a/services/headless-lms/models/.sqlx/query-55495b3872d09432230c60f846f69c5967fb9d3a41508aa2e4b8c823bd762fc3.json +++ b/services/headless-lms/models/.sqlx/query-7fc6ae6c78377544002371ff340856064f3e2592498d27e37ac53ca4e74eec10.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n accepting_strategy AS \"accepting_strategy: _\"\nFROM peer_review_configs\nWHERE course_id = $1\n AND exercise_id IS NULL\n AND deleted_at IS NULL;\n ", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n accepting_strategy AS \"accepting_strategy: _\",\n manual_review_cutoff_in_days\nFROM peer_review_configs\nWHERE id = $1\n AND deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -63,12 +63,17 @@ } } } + }, + { + "ordinal": 10, + "name": "manual_review_cutoff_in_days", + "type_info": "Int4" } ], "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, false, true, false, true, false, false, false, false] + "nullable": [false, false, false, true, false, true, false, false, false, false, false] }, - "hash": "55495b3872d09432230c60f846f69c5967fb9d3a41508aa2e4b8c823bd762fc3" + "hash": "7fc6ae6c78377544002371ff340856064f3e2592498d27e37ac53ca4e74eec10" } diff --git a/services/headless-lms/models/.sqlx/query-e3ec9d0aa135be4b608d62c47ca80a8076dcab3284c11d69e49eea12caadcb1c.json b/services/headless-lms/models/.sqlx/query-e3ec9d0aa135be4b608d62c47ca80a8076dcab3284c11d69e49eea12caadcb1c.json new file mode 100644 index 000000000000..ec1a0bc6f897 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-e3ec9d0aa135be4b608d62c47ca80a8076dcab3284c11d69e49eea12caadcb1c.json @@ -0,0 +1,68 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT *\nFROM peer_review_queue_entries\nWHERE exercise_id = $1\n AND received_enough_peer_reviews = FALSE\n AND removed_from_queue_for_unusual_reason = FALSE\n AND created_at < $2\n AND deleted_at IS NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "exercise_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "course_instance_id", + "type_info": "Uuid" + }, + { + "ordinal": 7, + "name": "receiving_peer_reviews_exercise_slide_submission_id", + "type_info": "Uuid" + }, + { + "ordinal": 8, + "name": "received_enough_peer_reviews", + "type_info": "Bool" + }, + { + "ordinal": 9, + "name": "peer_review_priority", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "removed_from_queue_for_unusual_reason", + "type_info": "Bool" + } + ], + "parameters": { + "Left": ["Uuid", "Timestamptz"] + }, + "nullable": [false, false, false, true, false, false, false, false, false, false, false] + }, + "hash": "e3ec9d0aa135be4b608d62c47ca80a8076dcab3284c11d69e49eea12caadcb1c" +} diff --git a/services/headless-lms/models/src/library/user_exercise_state_updater/state_deriver.rs b/services/headless-lms/models/src/library/user_exercise_state_updater/state_deriver.rs index 56ac259f3a6d..6ad5514df6b2 100644 --- a/services/headless-lms/models/src/library/user_exercise_state_updater/state_deriver.rs +++ b/services/headless-lms/models/src/library/user_exercise_state_updater/state_deriver.rs @@ -690,6 +690,7 @@ mod tests { peer_reviews_to_receive: 2, accepting_threshold: 2.1, accepting_strategy, + manual_review_cutoff_in_days: 21, } } diff --git a/services/headless-lms/models/src/peer_review_configs.rs b/services/headless-lms/models/src/peer_review_configs.rs index b699e32650a1..b32d666d74e1 100644 --- a/services/headless-lms/models/src/peer_review_configs.rs +++ b/services/headless-lms/models/src/peer_review_configs.rs @@ -26,6 +26,7 @@ pub struct PeerReviewConfig { pub peer_reviews_to_receive: i32, pub accepting_threshold: f32, pub accepting_strategy: PeerReviewAcceptingStrategy, + pub manual_review_cutoff_in_days: i32, } /// Like `PeerReviewConfig` but only the fields it's fine to show to all users. @@ -157,7 +158,8 @@ SELECT id, peer_reviews_to_give, peer_reviews_to_receive, accepting_threshold, - accepting_strategy AS "accepting_strategy: _" + accepting_strategy AS "accepting_strategy: _", + manual_review_cutoff_in_days FROM peer_review_configs WHERE id = $1 AND deleted_at IS NULL @@ -173,7 +175,7 @@ WHERE id = $1 pub async fn get_by_exercise_id( conn: &mut PgConnection, exercise_id: Uuid, -) -> ModelResult { +) -> ModelResult> { let res = sqlx::query_as!( PeerReviewConfig, r#" @@ -186,14 +188,15 @@ SELECT id, peer_reviews_to_give, peer_reviews_to_receive, accepting_threshold, - accepting_strategy AS "accepting_strategy: _" + accepting_strategy AS "accepting_strategy: _", + manual_review_cutoff_in_days FROM peer_review_configs WHERE exercise_id = $1 AND deleted_at IS NULL "#, exercise_id ) - .fetch_one(conn) + .fetch_optional(conn) .await?; Ok(res) } @@ -207,7 +210,12 @@ pub async fn get_by_exercise_or_course_id( if exercise.use_course_default_peer_review_config { get_default_for_course_by_course_id(conn, course_id).await } else { - get_by_exercise_id(conn, exercise.id).await + let config = get_by_exercise_id(conn, exercise.id).await?; + if let Some(config) = config { + Ok(config) + } else { + get_default_for_course_by_course_id(conn, course_id).await + } } } @@ -227,7 +235,8 @@ SELECT id, peer_reviews_to_give, peer_reviews_to_receive, accepting_threshold, - accepting_strategy AS "accepting_strategy: _" + accepting_strategy AS "accepting_strategy: _", + manual_review_cutoff_in_days FROM peer_review_configs WHERE course_id = $1 AND exercise_id IS NULL diff --git a/services/headless-lms/models/src/peer_review_queue_entries.rs b/services/headless-lms/models/src/peer_review_queue_entries.rs index d3756776ca43..7048e3d71dc1 100644 --- a/services/headless-lms/models/src/peer_review_queue_entries.rs +++ b/services/headless-lms/models/src/peer_review_queue_entries.rs @@ -393,6 +393,30 @@ WHERE course_instance_id = $1 Ok(res) } +pub async fn get_entries_that_need_reviews_and_are_older_than_with_exercise_id( + conn: &mut PgConnection, + exercise_id: Uuid, + timestamp: DateTime, +) -> ModelResult> { + let res = sqlx::query_as!( + PeerReviewQueueEntry, + " +SELECT * +FROM peer_review_queue_entries +WHERE exercise_id = $1 + AND received_enough_peer_reviews = FALSE + AND removed_from_queue_for_unusual_reason = FALSE + AND created_at < $2 + AND deleted_at IS NULL + ", + exercise_id, + timestamp + ) + .fetch_all(&mut *conn) + .await?; + Ok(res) +} + pub async fn remove_from_queue_and_add_to_manual_review( conn: &mut PgConnection, peer_review_queue_entry: &PeerReviewQueueEntry, diff --git a/services/headless-lms/server/src/programs/doc_file_generator/mod.rs b/services/headless-lms/server/src/programs/doc_file_generator/mod.rs index 7ce2a205be82..ac810b200158 100644 --- a/services/headless-lms/server/src/programs/doc_file_generator/mod.rs +++ b/services/headless-lms/server/src/programs/doc_file_generator/mod.rs @@ -764,6 +764,7 @@ fn models() { peer_reviews_to_receive: 2, accepting_threshold: 3.0, accepting_strategy: PeerReviewAcceptingStrategy::AutomaticallyAcceptOrManualReviewByAverage, + manual_review_cutoff_in_days: 21, }); doc!( T, diff --git a/services/headless-lms/server/src/programs/peer_review_updater.rs b/services/headless-lms/server/src/programs/peer_review_updater.rs index ed68088184d0..acce3742ec6a 100644 --- a/services/headless-lms/server/src/programs/peer_review_updater.rs +++ b/services/headless-lms/server/src/programs/peer_review_updater.rs @@ -32,18 +32,49 @@ pub async fn main() -> anyhow::Result<()> { let mut moved_to_manual_review = 0; for course_instance in all_course_instances.iter() { - let should_be_added_to_manual_review = headless_lms_models::peer_review_queue_entries::get_entries_that_need_reviews_and_are_older_than(&mut conn, course_instance.id, manual_review_cutoff).await?; - if should_be_added_to_manual_review.is_empty() { - continue; - } - info!(course_instance_id = ?course_instance.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(), manual_review_cutoff); - for peer_review_queue_entry in should_be_added_to_manual_review { - peer_review_queue_entries::remove_from_queue_and_add_to_manual_review( + //List of exercises in a course instance + let all_exercises_in_course_instance = + headless_lms_models::exercises::get_exercises_by_course_instance_id( &mut conn, - &peer_review_queue_entry, + course_instance.id, ) .await?; - moved_to_manual_review += 1 + + for exercise in all_exercises_in_course_instance.iter() { + if !exercise.needs_peer_review { + continue; + } + + let course_id = exercise.course_id; + if course_id.is_some() { + let exercise_config = + headless_lms_models::peer_review_configs::get_by_exercise_or_course_id( + &mut conn, + exercise, + course_id.unwrap(), + ) + .await?; + + let manual_review_cutoff_in_days = exercise_config.manual_review_cutoff_in_days; + + let timestamp = now - chrono::Duration::days(manual_review_cutoff_in_days.into()); + + 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(&mut conn, exercise.id, timestamp).await?; + if should_be_added_to_manual_review.is_empty() { + continue; + } + + 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); + + for peer_review_queue_entry in should_be_added_to_manual_review { + peer_review_queue_entries::remove_from_queue_and_add_to_manual_review( + &mut conn, + &peer_review_queue_entry, + ) + .await?; + moved_to_manual_review += 1 + } + } } } diff --git a/shared-module/src/bindings.guard.ts b/shared-module/src/bindings.guard.ts index f762457d31af..f6350cf8f1df 100644 --- a/shared-module/src/bindings.guard.ts +++ b/shared-module/src/bindings.guard.ts @@ -2265,7 +2265,8 @@ export function isPeerReviewConfig(obj: unknown): obj is PeerReviewConfig { typeof typedObj["peer_reviews_to_give"] === "number" && typeof typedObj["peer_reviews_to_receive"] === "number" && typeof typedObj["accepting_threshold"] === "number" && - (isPeerReviewAcceptingStrategy(typedObj["accepting_strategy"]) as boolean) + (isPeerReviewAcceptingStrategy(typedObj["accepting_strategy"]) as boolean) && + typeof typedObj["manual_review_cutoff_in_days"] === "number" ) } diff --git a/shared-module/src/bindings.ts b/shared-module/src/bindings.ts index 9d804c7709db..c5f18d1ff164 100644 --- a/shared-module/src/bindings.ts +++ b/shared-module/src/bindings.ts @@ -1236,6 +1236,7 @@ export interface PeerReviewConfig { peer_reviews_to_receive: number accepting_threshold: number accepting_strategy: PeerReviewAcceptingStrategy + manual_review_cutoff_in_days: number } export interface PeerReviewSubmission {