1use crate::{
2 exercises,
3 library::user_exercise_state_updater,
4 prelude::*,
5 teacher_grading_decisions,
6 user_exercise_states::{self, ReviewingStage},
7};
8
9#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq)]
10#[cfg_attr(feature = "ts_rs", derive(TS))]
11pub struct PeerReviewQueueEntry {
12 pub id: Uuid,
13 pub created_at: DateTime<Utc>,
14 pub updated_at: DateTime<Utc>,
15 pub deleted_at: Option<DateTime<Utc>>,
16 pub user_id: Uuid,
17 pub exercise_id: Uuid,
18 pub course_instance_id: Uuid,
19 pub receiving_peer_reviews_exercise_slide_submission_id: Uuid,
20 pub received_enough_peer_reviews: bool,
21 pub peer_review_priority: i32,
22 pub removed_from_queue_for_unusual_reason: bool,
23}
24
25pub async fn insert(
26 conn: &mut PgConnection,
27 pkey_policy: PKeyPolicy<Uuid>,
28 user_id: Uuid,
29 exercise_id: Uuid,
30 course_instance_id: Uuid,
31 receiving_peer_reviews_exercise_slide_submission_id: Uuid,
32 peer_review_priority: i32,
33) -> ModelResult<Uuid> {
34 let res = sqlx::query!(
35 "
36INSERT INTO peer_review_queue_entries (
37 id,
38 user_id,
39 exercise_id,
40 course_instance_id,
41 receiving_peer_reviews_exercise_slide_submission_id,
42 peer_review_priority
43 )
44VALUES ($1, $2, $3, $4, $5, $6)
45RETURNING id
46 ",
47 pkey_policy.into_uuid(),
48 user_id,
49 exercise_id,
50 course_instance_id,
51 receiving_peer_reviews_exercise_slide_submission_id,
52 peer_review_priority,
53 )
54 .fetch_one(conn)
55 .await?;
56 Ok(res.id)
57}
58
59pub async fn upsert_peer_review_priority(
66 conn: &mut PgConnection,
67 user_id: Uuid,
68 exercise_id: Uuid,
69 course_instance_id: Uuid,
70 peer_review_priority: i32,
71 receiving_peer_reviews_exercise_slide_submission_id: Uuid,
72 received_enough_peer_reviews: bool,
73) -> ModelResult<PeerReviewQueueEntry> {
74 let res = sqlx::query_as!(
75 PeerReviewQueueEntry,
76 "
77INSERT INTO peer_review_queue_entries (
78 user_id,
79 exercise_id,
80 course_instance_id,
81 peer_review_priority,
82 receiving_peer_reviews_exercise_slide_submission_id,
83 received_enough_peer_reviews
84 )
85VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (user_id, exercise_id, course_instance_id, deleted_at) DO
86UPDATE
87SET peer_review_priority = $4
88RETURNING *
89 ",
90 user_id,
91 exercise_id,
92 course_instance_id,
93 peer_review_priority,
94 receiving_peer_reviews_exercise_slide_submission_id,
95 received_enough_peer_reviews,
96 )
97 .fetch_one(conn)
98 .await?;
99 Ok(res)
100}
101
102pub async fn update_received_enough_peer_reviews(
103 conn: &mut PgConnection,
104 id: Uuid,
105 received_enough_peer_reviews: bool,
106) -> ModelResult<PeerReviewQueueEntry> {
107 let res = sqlx::query_as!(
108 PeerReviewQueueEntry,
109 "
110UPDATE peer_review_queue_entries
111SET received_enough_peer_reviews = $1
112WHERE id = $2
113 AND deleted_at IS NULL
114RETURNING *;
115 ",
116 received_enough_peer_reviews,
117 id,
118 )
119 .fetch_one(conn)
120 .await?;
121 Ok(res)
122}
123
124pub async fn update(
125 conn: &mut PgConnection,
126 id: Uuid,
127 receiving_peer_reviews_exercise_slide_submission_id: Uuid,
128 peer_review_priority: i32,
129) -> ModelResult<PeerReviewQueueEntry> {
130 let res = sqlx::query_as!(
131 PeerReviewQueueEntry,
132 "
133UPDATE peer_review_queue_entries
134SET receiving_peer_reviews_exercise_slide_submission_id = $1,
135 peer_review_priority = $2
136WHERE id = $3
137RETURNING *
138 ",
139 receiving_peer_reviews_exercise_slide_submission_id,
140 peer_review_priority,
141 id
142 )
143 .fetch_one(conn)
144 .await?;
145 Ok(res)
146}
147
148pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<PeerReviewQueueEntry> {
149 let res = sqlx::query_as!(
150 PeerReviewQueueEntry,
151 "
152SELECT *
153FROM peer_review_queue_entries
154WHERE id = $1
155 AND deleted_at IS NULL
156 ",
157 id
158 )
159 .fetch_one(conn)
160 .await?;
161 Ok(res)
162}
163
164pub async fn get_by_user_and_exercise_and_course_instance_ids(
165 conn: &mut PgConnection,
166 user_id: Uuid,
167 exercise_id: Uuid,
168 course_instance_id: Uuid,
169) -> ModelResult<PeerReviewQueueEntry> {
170 let res = sqlx::query_as!(
171 PeerReviewQueueEntry,
172 "
173SELECT *
174FROM peer_review_queue_entries
175WHERE user_id = $1
176 AND exercise_id = $2
177 AND course_instance_id = $3
178 AND deleted_at IS NULL
179 ",
180 user_id,
181 exercise_id,
182 course_instance_id,
183 )
184 .fetch_one(conn)
185 .await?;
186 Ok(res)
187}
188
189pub async fn try_to_get_by_user_and_exercise_and_course_instance_ids(
190 conn: &mut PgConnection,
191 user_id: Uuid,
192 exercise_id: Uuid,
193 course_instance_id: Uuid,
194) -> ModelResult<Option<PeerReviewQueueEntry>> {
195 get_by_user_and_exercise_and_course_instance_ids(conn, user_id, exercise_id, course_instance_id)
196 .await
197 .optional()
198}
199
200pub async fn get_any_including_not_needing_review(
204 conn: &mut PgConnection,
205 exercise_id: Uuid,
206 excluded_user_id: Uuid,
207 excluded_submissions_ids: &[Uuid],
208 count: i64,
209) -> ModelResult<Vec<PeerReviewQueueEntry>> {
210 let res = sqlx::query_as!(
211 PeerReviewQueueEntry,
212 "
213SELECT *
214FROM peer_review_queue_entries
215WHERE exercise_id = $1
216 AND user_id <> $2
217 AND receiving_peer_reviews_exercise_slide_submission_id <> ALL($3)
218 AND deleted_at IS NULL
219ORDER BY created_at DESC
220LIMIT $4
221 ",
222 exercise_id,
223 excluded_user_id,
224 excluded_submissions_ids,
225 count,
226 )
227 .fetch_all(conn)
228 .await?;
229 Ok(res)
230}
231
232pub async fn get_many_that_need_peer_reviews_by_exercise_id_and_review_priority(
237 conn: &mut PgConnection,
238 exercise_id: Uuid,
239 excluded_user_id: Uuid,
240 excluded_submissions_ids: &[Uuid],
241 count: i64,
242) -> ModelResult<Vec<PeerReviewQueueEntry>> {
243 let res = sqlx::query_as!(
244 PeerReviewQueueEntry,
245 "
246SELECT *
247FROM peer_review_queue_entries
248WHERE exercise_id = $1
249 AND user_id <> $2
250 AND receiving_peer_reviews_exercise_slide_submission_id <> ALL($3)
251 AND received_enough_peer_reviews = 'false'
252 AND removed_from_queue_for_unusual_reason = 'false'
253 AND deleted_at IS NULL
254ORDER BY peer_review_priority DESC
255LIMIT $4
256 ",
257 exercise_id,
258 excluded_user_id,
259 excluded_submissions_ids,
260 count,
261 )
262 .fetch_all(conn)
263 .await?;
264 Ok(res)
265}
266
267pub async fn get_all_that_need_peer_reviews_by_exercise_id(
272 conn: &mut PgConnection,
273 exercise_id: Uuid,
274) -> ModelResult<Vec<PeerReviewQueueEntry>> {
275 let res = sqlx::query_as!(
276 PeerReviewQueueEntry,
277 "
278SELECT *
279FROM peer_review_queue_entries
280WHERE exercise_id = $1
281 AND received_enough_peer_reviews = 'false'
282 AND deleted_at IS NULL
283 ",
284 exercise_id,
285 )
286 .fetch_all(conn)
287 .await?;
288 Ok(res)
289}
290
291pub async fn increment_peer_review_priority(
292 conn: &mut PgConnection,
293 peer_review_queue_entry: PeerReviewQueueEntry,
294) -> ModelResult<PeerReviewQueueEntry> {
295 let res = sqlx::query_as!(
296 PeerReviewQueueEntry,
297 "
298UPDATE peer_review_queue_entries
299SET peer_review_priority = $1
300WHERE id = $2
301 AND deleted_at IS NULL
302RETURNING *
303 ",
304 peer_review_queue_entry.peer_review_priority + 1,
305 peer_review_queue_entry.id
306 )
307 .fetch_one(conn)
308 .await?;
309 Ok(res)
310}
311
312pub async fn remove_queue_entries_for_unusual_reason(
313 conn: &mut PgConnection,
314 user_id: Uuid,
315 exercise_id: Uuid,
316 course_instance_id: Uuid,
317) -> ModelResult<()> {
318 sqlx::query!(
319 "
320UPDATE peer_review_queue_entries
321SET removed_from_queue_for_unusual_reason = TRUE
322WHERE user_id = $1
323 AND exercise_id = $2
324 AND course_instance_id = $3
325 AND deleted_at IS NULL
326 ",
327 user_id,
328 exercise_id,
329 course_instance_id
330 )
331 .execute(conn)
332 .await?;
333 Ok(())
334}
335
336pub async fn get_entries_that_need_reviews_and_are_older_than(
337 conn: &mut PgConnection,
338 course_instance_id: Uuid,
339 timestamp: DateTime<Utc>,
340) -> ModelResult<Vec<PeerReviewQueueEntry>> {
341 let res = sqlx::query_as!(
342 PeerReviewQueueEntry,
343 "
344SELECT *
345FROM peer_review_queue_entries
346WHERE course_instance_id = $1
347 AND received_enough_peer_reviews = FALSE
348 AND removed_from_queue_for_unusual_reason = FALSE
349 AND created_at < $2
350 AND deleted_at IS NULL
351 ",
352 course_instance_id,
353 timestamp
354 )
355 .fetch_all(&mut *conn)
356 .await?;
357 Ok(res)
358}
359
360pub async fn get_entries_that_need_teacher_review_and_are_older_than_with_course_instance_id(
362 conn: &mut PgConnection,
363 course_instance_id: Uuid,
364 timestamp: DateTime<Utc>,
365) -> ModelResult<Vec<PeerReviewQueueEntry>> {
366 let res = sqlx::query_as!(
367 PeerReviewQueueEntry,
368 "
369SELECT prqe.*
370FROM peer_review_queue_entries prqe
371 JOIN user_exercise_states ues ON (
372 prqe.user_id = ues.user_id
373 AND prqe.exercise_id = ues.exercise_id
374 AND prqe.course_instance_id = ues.course_instance_id
375 )
376WHERE prqe.course_instance_id = $1
377 AND ues.reviewing_stage = 'waiting_for_manual_grading'
378 AND prqe.created_at < $2
379 AND prqe.deleted_at IS NULL
380 AND ues.deleted_at IS NULL
381 ",
382 course_instance_id,
383 timestamp
384 )
385 .fetch_all(conn)
386 .await?;
387 Ok(res)
388}
389
390pub async fn get_entries_that_need_reviews_and_are_older_than_with_exercise_id(
391 conn: &mut PgConnection,
392 exercise_id: Uuid,
393 timestamp: DateTime<Utc>,
394) -> ModelResult<Vec<PeerReviewQueueEntry>> {
395 let res = sqlx::query_as!(
396 PeerReviewQueueEntry,
397 "
398SELECT *
399FROM peer_review_queue_entries
400WHERE exercise_id = $1
401 AND received_enough_peer_reviews = FALSE
402 AND removed_from_queue_for_unusual_reason = FALSE
403 AND created_at < $2
404 AND deleted_at IS NULL
405 ",
406 exercise_id,
407 timestamp
408 )
409 .fetch_all(&mut *conn)
410 .await?;
411 Ok(res)
412}
413
414pub async fn remove_from_queue_and_add_to_manual_review(
415 conn: &mut PgConnection,
416 peer_review_queue_entry: &PeerReviewQueueEntry,
417) -> ModelResult<PeerReviewQueueEntry> {
418 let mut tx = conn.begin().await?;
419 let res = remove_from_queue(&mut tx, peer_review_queue_entry).await?;
420
421 let _ues = user_exercise_states::update_reviewing_stage(
422 &mut tx,
423 peer_review_queue_entry.user_id,
424 user_exercise_states::CourseInstanceOrExamId::Instance(
425 peer_review_queue_entry.course_instance_id,
426 ),
427 peer_review_queue_entry.exercise_id,
428 ReviewingStage::WaitingForManualGrading,
429 )
430 .await?;
431 tx.commit().await?;
432 Ok(res)
433}
434
435pub async fn remove_from_queue_and_give_full_points(
436 conn: &mut PgConnection,
437 peer_review_queue_entry: &PeerReviewQueueEntry,
438) -> ModelResult<PeerReviewQueueEntry> {
439 let mut tx = conn.begin().await?;
440 let res = remove_from_queue(&mut tx, peer_review_queue_entry).await?;
441 let exercise = exercises::get_by_id(&mut tx, peer_review_queue_entry.exercise_id).await?;
442 let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
443 &mut tx,
444 peer_review_queue_entry.user_id,
445 peer_review_queue_entry.exercise_id,
446 user_exercise_states::CourseInstanceOrExamId::Instance(
447 peer_review_queue_entry.course_instance_id,
448 ),
449 )
450 .await?;
451 if let Some(user_exercise_state) = user_exercise_state {
452 teacher_grading_decisions::add_teacher_grading_decision(
453 &mut tx,
454 user_exercise_state.id,
455 teacher_grading_decisions::TeacherDecisionType::FullPoints,
456 exercise.score_maximum as f32,
457 None,
459 None,
460 false,
461 )
462 .await?;
463 user_exercise_state_updater::update_user_exercise_state(&mut tx, user_exercise_state.id)
464 .await?;
465 } else {
466 return Err(ModelError::new(
467 ModelErrorType::InvalidRequest,
468 "User exercise state not found".to_string(),
469 None,
470 ));
471 }
472
473 tx.commit().await?;
474 Ok(res)
475}
476
477async fn remove_from_queue(
478 conn: &mut PgConnection,
479 peer_review_queue_entry: &PeerReviewQueueEntry,
480) -> ModelResult<PeerReviewQueueEntry> {
481 let res = sqlx::query_as!(
482 PeerReviewQueueEntry,
483 "
484UPDATE peer_review_queue_entries
485SET removed_from_queue_for_unusual_reason = TRUE
486WHERE id = $1
487RETURNING *
488 ",
489 peer_review_queue_entry.id
490 )
491 .fetch_one(&mut *conn)
492 .await?;
493
494 Ok(res)
495}
496
497pub async fn delete_by_receiving_peer_reviews_exercise_slide_submission_id(
498 conn: &mut PgConnection,
499 receiving_peer_reviews_exercise_slide_submission_id: Uuid,
500) -> ModelResult<()> {
501 sqlx::query_as!(
502 PeerReviewQueueEntry,
503 "
504UPDATE peer_review_queue_entries
505SET deleted_at = now()
506WHERE receiving_peer_reviews_exercise_slide_submission_id = $1
507AND deleted_at is NULL
508 ",
509 receiving_peer_reviews_exercise_slide_submission_id
510 )
511 .execute(&mut *conn)
512 .await?;
513
514 Ok(())
515}
516
517pub async fn get_by_receiving_peer_reviews_exercise_slide_submission_id(
518 conn: &mut PgConnection,
519 receiving_peer_reviews_exercise_slide_submission_id: Uuid,
520) -> ModelResult<PeerReviewQueueEntry> {
521 let res = sqlx::query_as!(
522 PeerReviewQueueEntry,
523 "
524SELECT *
525FROM peer_review_queue_entries
526WHERE receiving_peer_reviews_exercise_slide_submission_id = $1
527 AND deleted_at IS NULL
528",
529 receiving_peer_reviews_exercise_slide_submission_id
530 )
531 .fetch_one(conn)
532 .await?;
533 Ok(res)
534}
535
536pub async fn get_all_by_user_and_course_instance_ids(
537 conn: &mut PgConnection,
538 user_id: Uuid,
539 course_instance_id: Uuid,
540) -> ModelResult<Vec<PeerReviewQueueEntry>> {
541 let res = sqlx::query_as!(
542 PeerReviewQueueEntry,
543 "
544SELECT *
545FROM peer_review_queue_entries
546WHERE user_id = $1
547 AND course_instance_id = $2
548 AND deleted_at IS NULL
549 ",
550 user_id,
551 course_instance_id,
552 )
553 .fetch_all(conn)
554 .await?;
555 Ok(res)
556}
557
558pub async fn try_to_get_all_by_user_and_course_instance_ids(
559 conn: &mut PgConnection,
560 user_id: Uuid,
561 course_instance_id: Uuid,
562) -> ModelResult<Option<Vec<PeerReviewQueueEntry>>> {
563 get_all_by_user_and_course_instance_ids(conn, user_id, course_instance_id)
564 .await
565 .optional()
566}