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