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_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_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_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_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_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_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_id, deleted_at) DO
86UPDATE
87SET peer_review_priority = $4
88RETURNING *
89 ",
90 user_id,
91 exercise_id,
92 course_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
164async fn get_by_receiving_peer_reviews_submission_and_course_ids(
165 conn: &mut PgConnection,
166 receiving_peer_reviews_exercise_slide_submission_id: Uuid,
167 course_id: Uuid,
168) -> ModelResult<PeerReviewQueueEntry> {
169 let res = sqlx::query_as!(
170 PeerReviewQueueEntry,
171 "
172SELECT *
173FROM peer_review_queue_entries
174WHERE receiving_peer_reviews_exercise_slide_submission_id = $1
175 AND course_id = $2
176 AND deleted_at IS NULL
177 ",
178 receiving_peer_reviews_exercise_slide_submission_id,
179 course_id
180 )
181 .fetch_one(conn)
182 .await?;
183 Ok(res)
184}
185
186pub async fn try_to_get_by_receiving_submission_and_course_ids(
187 conn: &mut PgConnection,
188 receiving_peer_reviews_exercise_slide_submission_id: Uuid,
189 course_id: Uuid,
190) -> ModelResult<Option<PeerReviewQueueEntry>> {
191 get_by_receiving_peer_reviews_submission_and_course_ids(
192 conn,
193 receiving_peer_reviews_exercise_slide_submission_id,
194 course_id,
195 )
196 .await
197 .optional()
198}
199
200pub async fn get_by_user_and_exercise_and_course_ids(
201 conn: &mut PgConnection,
202 user_id: Uuid,
203 exercise_id: Uuid,
204 course_id: Uuid,
205) -> ModelResult<PeerReviewQueueEntry> {
206 let res = sqlx::query_as!(
207 PeerReviewQueueEntry,
208 "
209SELECT *
210FROM peer_review_queue_entries
211WHERE user_id = $1
212 AND exercise_id = $2
213 AND course_id = $3
214 AND deleted_at IS NULL
215 ",
216 user_id,
217 exercise_id,
218 course_id,
219 )
220 .fetch_one(conn)
221 .await?;
222 Ok(res)
223}
224
225pub async fn try_to_get_by_user_and_exercise_and_course_ids(
226 conn: &mut PgConnection,
227 user_id: Uuid,
228 exercise_id: Uuid,
229 course_id: Uuid,
230) -> ModelResult<Option<PeerReviewQueueEntry>> {
231 get_by_user_and_exercise_and_course_ids(conn, user_id, exercise_id, course_id)
232 .await
233 .optional()
234}
235
236pub async fn get_any_including_not_needing_review(
240 conn: &mut PgConnection,
241 exercise_id: Uuid,
242 excluded_user_id: Uuid,
243 excluded_submissions_ids: &[Uuid],
244 count: i64,
245) -> ModelResult<Vec<PeerReviewQueueEntry>> {
246 let res = sqlx::query_as!(
247 PeerReviewQueueEntry,
248 "
249SELECT *
250FROM peer_review_queue_entries
251WHERE exercise_id = $1
252 AND user_id <> $2
253 AND receiving_peer_reviews_exercise_slide_submission_id <> ALL($3)
254 AND deleted_at IS NULL
255ORDER BY created_at DESC
256LIMIT $4
257 ",
258 exercise_id,
259 excluded_user_id,
260 excluded_submissions_ids,
261 count,
262 )
263 .fetch_all(conn)
264 .await?;
265 Ok(res)
266}
267
268pub async fn get_many_that_need_peer_reviews_by_exercise_id_and_review_priority(
273 conn: &mut PgConnection,
274 exercise_id: Uuid,
275 excluded_user_id: Uuid,
276 excluded_submissions_ids: &[Uuid],
277 count: i64,
278) -> ModelResult<Vec<PeerReviewQueueEntry>> {
279 let res = sqlx::query_as!(
280 PeerReviewQueueEntry,
281 "
282SELECT *
283FROM peer_review_queue_entries
284WHERE exercise_id = $1
285 AND user_id <> $2
286 AND receiving_peer_reviews_exercise_slide_submission_id <> ALL($3)
287 AND received_enough_peer_reviews = 'false'
288 AND removed_from_queue_for_unusual_reason = 'false'
289 AND deleted_at IS NULL
290ORDER BY peer_review_priority DESC
291LIMIT $4
292 ",
293 exercise_id,
294 excluded_user_id,
295 excluded_submissions_ids,
296 count,
297 )
298 .fetch_all(conn)
299 .await?;
300 Ok(res)
301}
302
303pub async fn get_all_that_need_peer_reviews_by_exercise_id(
308 conn: &mut PgConnection,
309 exercise_id: Uuid,
310) -> ModelResult<Vec<PeerReviewQueueEntry>> {
311 let res = sqlx::query_as!(
312 PeerReviewQueueEntry,
313 "
314SELECT *
315FROM peer_review_queue_entries
316WHERE exercise_id = $1
317 AND received_enough_peer_reviews = 'false'
318 AND deleted_at IS NULL
319 ",
320 exercise_id,
321 )
322 .fetch_all(conn)
323 .await?;
324 Ok(res)
325}
326
327pub async fn increment_peer_review_priority(
328 conn: &mut PgConnection,
329 peer_review_queue_entry: PeerReviewQueueEntry,
330) -> ModelResult<PeerReviewQueueEntry> {
331 let res = sqlx::query_as!(
332 PeerReviewQueueEntry,
333 "
334UPDATE peer_review_queue_entries
335SET peer_review_priority = $1
336WHERE id = $2
337 AND deleted_at IS NULL
338RETURNING *
339 ",
340 peer_review_queue_entry.peer_review_priority + 1,
341 peer_review_queue_entry.id
342 )
343 .fetch_one(conn)
344 .await?;
345 Ok(res)
346}
347
348pub async fn remove_queue_entries_for_unusual_reason(
349 conn: &mut PgConnection,
350 user_id: Uuid,
351 exercise_id: Uuid,
352 course_id: Uuid,
353) -> ModelResult<()> {
354 sqlx::query!(
355 "
356UPDATE peer_review_queue_entries
357SET removed_from_queue_for_unusual_reason = TRUE
358WHERE user_id = $1
359 AND exercise_id = $2
360 AND course_id = $3
361 AND deleted_at IS NULL
362 ",
363 user_id,
364 exercise_id,
365 course_id
366 )
367 .execute(conn)
368 .await?;
369 Ok(())
370}
371
372pub async fn get_entries_that_need_reviews_and_are_older_than(
373 conn: &mut PgConnection,
374 course_id: Uuid,
375 timestamp: DateTime<Utc>,
376) -> ModelResult<Vec<PeerReviewQueueEntry>> {
377 let res = sqlx::query_as!(
378 PeerReviewQueueEntry,
379 "
380SELECT *
381FROM peer_review_queue_entries
382WHERE course_id = $1
383 AND received_enough_peer_reviews = FALSE
384 AND removed_from_queue_for_unusual_reason = FALSE
385 AND created_at < $2
386 AND deleted_at IS NULL
387 ",
388 course_id,
389 timestamp
390 )
391 .fetch_all(&mut *conn)
392 .await?;
393 Ok(res)
394}
395
396pub async fn get_entries_that_need_teacher_review_and_are_older_than_with_course_id(
398 conn: &mut PgConnection,
399 course_id: Uuid,
400 timestamp: DateTime<Utc>,
401) -> ModelResult<Vec<PeerReviewQueueEntry>> {
402 let res = sqlx::query_as!(
403 PeerReviewQueueEntry,
404 "
405SELECT prqe.*
406FROM peer_review_queue_entries prqe
407 JOIN user_exercise_states ues ON (
408 prqe.user_id = ues.user_id
409 AND prqe.exercise_id = ues.exercise_id
410 AND prqe.course_id = ues.course_id
411 )
412WHERE prqe.course_id = $1
413 AND ues.reviewing_stage = 'waiting_for_manual_grading'
414 AND prqe.created_at < $2
415 AND prqe.deleted_at IS NULL
416 AND ues.deleted_at IS NULL
417 ",
418 course_id,
419 timestamp
420 )
421 .fetch_all(conn)
422 .await?;
423 Ok(res)
424}
425
426pub async fn get_entries_that_need_reviews_and_are_older_than_with_exercise_id(
427 conn: &mut PgConnection,
428 exercise_id: Uuid,
429 timestamp: DateTime<Utc>,
430) -> ModelResult<Vec<PeerReviewQueueEntry>> {
431 let res = sqlx::query_as!(
432 PeerReviewQueueEntry,
433 "
434SELECT *
435FROM peer_review_queue_entries
436WHERE exercise_id = $1
437 AND received_enough_peer_reviews = FALSE
438 AND removed_from_queue_for_unusual_reason = FALSE
439 AND created_at < $2
440 AND deleted_at IS NULL
441 ",
442 exercise_id,
443 timestamp
444 )
445 .fetch_all(&mut *conn)
446 .await?;
447 Ok(res)
448}
449
450pub async fn remove_from_queue_and_add_to_manual_review(
451 conn: &mut PgConnection,
452 peer_review_queue_entry: &PeerReviewQueueEntry,
453) -> ModelResult<PeerReviewQueueEntry> {
454 let mut tx = conn.begin().await?;
455 let res = remove_from_queue(&mut tx, peer_review_queue_entry).await?;
456
457 let _ues = user_exercise_states::update_reviewing_stage(
458 &mut tx,
459 peer_review_queue_entry.user_id,
460 CourseOrExamId::Course(peer_review_queue_entry.course_id),
461 peer_review_queue_entry.exercise_id,
462 ReviewingStage::WaitingForManualGrading,
463 )
464 .await?;
465 tx.commit().await?;
466 Ok(res)
467}
468
469pub async fn remove_from_queue_and_give_full_points(
470 conn: &mut PgConnection,
471 peer_review_queue_entry: &PeerReviewQueueEntry,
472) -> ModelResult<PeerReviewQueueEntry> {
473 let mut tx = conn.begin().await?;
474 let res = remove_from_queue(&mut tx, peer_review_queue_entry).await?;
475 let exercise = exercises::get_by_id(&mut tx, peer_review_queue_entry.exercise_id).await?;
476 let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
477 &mut tx,
478 peer_review_queue_entry.user_id,
479 peer_review_queue_entry.exercise_id,
480 CourseOrExamId::Course(peer_review_queue_entry.course_id),
481 )
482 .await?;
483 if let Some(user_exercise_state) = user_exercise_state {
484 teacher_grading_decisions::add_teacher_grading_decision(
485 &mut tx,
486 user_exercise_state.id,
487 teacher_grading_decisions::TeacherDecisionType::FullPoints,
488 exercise.score_maximum as f32,
489 None,
491 None,
492 false,
493 )
494 .await?;
495 user_exercise_state_updater::update_user_exercise_state(&mut tx, user_exercise_state.id)
496 .await?;
497 } else {
498 return Err(ModelError::new(
499 ModelErrorType::InvalidRequest,
500 "User exercise state not found".to_string(),
501 None,
502 ));
503 }
504
505 tx.commit().await?;
506 Ok(res)
507}
508
509async fn remove_from_queue(
510 conn: &mut PgConnection,
511 peer_review_queue_entry: &PeerReviewQueueEntry,
512) -> ModelResult<PeerReviewQueueEntry> {
513 let res = sqlx::query_as!(
514 PeerReviewQueueEntry,
515 "
516UPDATE peer_review_queue_entries
517SET removed_from_queue_for_unusual_reason = TRUE
518WHERE id = $1
519RETURNING *
520 ",
521 peer_review_queue_entry.id
522 )
523 .fetch_one(&mut *conn)
524 .await?;
525
526 Ok(res)
527}
528
529pub async fn delete_by_receiving_peer_reviews_exercise_slide_submission_id(
530 conn: &mut PgConnection,
531 receiving_peer_reviews_exercise_slide_submission_id: Uuid,
532) -> ModelResult<()> {
533 sqlx::query_as!(
534 PeerReviewQueueEntry,
535 "
536UPDATE peer_review_queue_entries
537SET deleted_at = now()
538WHERE receiving_peer_reviews_exercise_slide_submission_id = $1
539AND deleted_at is NULL
540 ",
541 receiving_peer_reviews_exercise_slide_submission_id
542 )
543 .execute(&mut *conn)
544 .await?;
545
546 Ok(())
547}
548
549pub async fn get_by_receiving_peer_reviews_exercise_slide_submission_id(
550 conn: &mut PgConnection,
551 receiving_peer_reviews_exercise_slide_submission_id: Uuid,
552) -> ModelResult<PeerReviewQueueEntry> {
553 let res = sqlx::query_as!(
554 PeerReviewQueueEntry,
555 "
556SELECT *
557FROM peer_review_queue_entries
558WHERE receiving_peer_reviews_exercise_slide_submission_id = $1
559 AND deleted_at IS NULL
560",
561 receiving_peer_reviews_exercise_slide_submission_id
562 )
563 .fetch_one(conn)
564 .await?;
565 Ok(res)
566}
567
568pub async fn get_all_by_user_and_course_id(
569 conn: &mut PgConnection,
570 user_id: Uuid,
571 course_id: Uuid,
572) -> ModelResult<Vec<PeerReviewQueueEntry>> {
573 let res = sqlx::query_as!(
574 PeerReviewQueueEntry,
575 "
576SELECT *
577FROM peer_review_queue_entries
578WHERE user_id = $1
579 AND course_id = $2
580 AND deleted_at IS NULL
581 ",
582 user_id,
583 course_id,
584 )
585 .fetch_all(conn)
586 .await?;
587 Ok(res)
588}
589
590pub async fn try_to_get_all_by_user_and_course_id(
591 conn: &mut PgConnection,
592 user_id: Uuid,
593 course_id: Uuid,
594) -> ModelResult<Option<Vec<PeerReviewQueueEntry>>> {
595 get_all_by_user_and_course_id(conn, user_id, course_id)
596 .await
597 .optional()
598}