1use std::collections::HashMap;
2
3use crate::prelude::*;
4use utoipa::ToSchema;
5
6#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, ToSchema)]
7
8pub struct PeerOrSelfReviewSubmission {
9 pub id: Uuid,
10 pub created_at: DateTime<Utc>,
11 pub updated_at: DateTime<Utc>,
12 pub deleted_at: Option<DateTime<Utc>>,
13 pub user_id: Uuid,
14 pub exercise_id: Uuid,
15 pub course_id: Uuid,
16 pub peer_or_self_review_config_id: Uuid,
17 pub exercise_slide_submission_id: Uuid,
18}
19
20#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, ToSchema)]
22
23pub struct PeerOrSelfReviewSubmissionWithSubmissionOwner {
24 #[serde(flatten)]
25 pub submission: PeerOrSelfReviewSubmission,
26 pub submission_owner_user_id: Option<Uuid>,
27}
28
29pub async fn insert(
30 conn: &mut PgConnection,
31 pkey_policy: PKeyPolicy<Uuid>,
32 user_id: Uuid,
33 exercise_id: Uuid,
34 course_id: Uuid,
35 peer_or_self_review_config_id: Uuid,
36 exercise_slide_submission_id: Uuid,
37) -> ModelResult<Uuid> {
38 let res = sqlx::query!(
39 "
40INSERT INTO peer_or_self_review_submissions (
41 id,
42 user_id,
43 exercise_id,
44 course_id,
45 peer_or_self_review_config_id,
46 exercise_slide_submission_id
47 )
48VALUES ($1, $2, $3, $4, $5, $6)
49RETURNING *
50 ",
51 pkey_policy.into_uuid(),
52 user_id,
53 exercise_id,
54 course_id,
55 peer_or_self_review_config_id,
56 exercise_slide_submission_id,
57 )
58 .fetch_one(conn)
59 .await?;
60 Ok(res.id)
61}
62
63#[allow(clippy::too_many_arguments)]
64pub async fn insert_for_submission_context(
65 conn: &mut PgConnection,
66 pkey_policy: PKeyPolicy<Uuid>,
67 reviewer_user_id: Uuid,
68 receiver_user_id: Uuid,
69 exercise_id: Uuid,
70 course_id: Uuid,
71 peer_or_self_review_config_id: Uuid,
72 exercise_slide_submission_id: Uuid,
73 reviewer_user_exercise_state_id: Uuid,
74 receiver_user_exercise_state_id: Uuid,
75) -> ModelResult<PeerOrSelfReviewSubmission> {
76 let res = sqlx::query_as!(
77 PeerOrSelfReviewSubmission,
78 r#"
79INSERT INTO peer_or_self_review_submissions (
80 id,
81 user_id,
82 exercise_id,
83 course_id,
84 peer_or_self_review_config_id,
85 exercise_slide_submission_id
86)
87SELECT
88 $1,
89 reviewer_state.user_id,
90 exercise.id,
91 exercise.course_id,
92 config.id,
93 receiver_submission.id
94FROM exercises exercise
95 JOIN peer_or_self_review_configs config
96 ON config.id = $5
97 AND config.course_id = $4
98 AND (config.exercise_id = exercise.id OR config.exercise_id IS NULL)
99 AND config.deleted_at IS NULL
100 JOIN exercise_slide_submissions receiver_submission
101 ON receiver_submission.id = $6
102 AND receiver_submission.exercise_id = exercise.id
103 AND receiver_submission.course_id = $4
104 AND receiver_submission.user_id = $3
105 AND receiver_submission.deleted_at IS NULL
106 JOIN user_exercise_states reviewer_state
107 ON reviewer_state.id = $7
108 AND reviewer_state.user_id = $2
109 AND reviewer_state.exercise_id = exercise.id
110 AND reviewer_state.course_id = $4
111 AND reviewer_state.deleted_at IS NULL
112 JOIN user_exercise_states receiver_state
113 ON receiver_state.id = $8
114 AND receiver_state.user_id = $3
115 AND receiver_state.exercise_id = exercise.id
116 AND receiver_state.course_id = $4
117 AND receiver_state.deleted_at IS NULL
118WHERE exercise.id = $9
119 AND exercise.course_id = $4
120 AND exercise.deleted_at IS NULL
121RETURNING *
122 "#,
123 pkey_policy.into_uuid(),
124 reviewer_user_id,
125 receiver_user_id,
126 course_id,
127 peer_or_self_review_config_id,
128 exercise_slide_submission_id,
129 reviewer_user_exercise_state_id,
130 receiver_user_exercise_state_id,
131 exercise_id
132 )
133 .fetch_one(conn)
134 .await?;
135 Ok(res)
136}
137
138pub async fn count_by_exercise_ids(
141 conn: &mut PgConnection,
142 exercise_ids: &[Uuid],
143) -> ModelResult<HashMap<Uuid, i64>> {
144 if exercise_ids.is_empty() {
145 return Ok(HashMap::new());
146 }
147 let counts = sqlx::query!(
148 r#"
149SELECT exercise_id,
150 COUNT(*) AS "count!"
151FROM peer_or_self_review_submissions
152WHERE exercise_id = ANY($1)
153 AND deleted_at IS NULL
154GROUP BY exercise_id
155 "#,
156 exercise_ids
157 )
158 .fetch_all(conn)
159 .await?;
160 Ok(counts
161 .into_iter()
162 .map(|row| (row.exercise_id, row.count))
163 .collect())
164}
165
166pub async fn get_by_id(
167 conn: &mut PgConnection,
168 id: Uuid,
169) -> ModelResult<PeerOrSelfReviewSubmission> {
170 let res = sqlx::query_as!(
171 PeerOrSelfReviewSubmission,
172 "
173SELECT *
174FROM peer_or_self_review_submissions
175WHERE id = $1
176 AND deleted_at IS NULL
177 ",
178 id
179 )
180 .fetch_one(conn)
181 .await?;
182 Ok(res)
183}
184
185pub async fn get_by_ids(
186 conn: &mut PgConnection,
187 ids: &[Uuid],
188) -> ModelResult<Vec<PeerOrSelfReviewSubmission>> {
189 let res = sqlx::query_as!(
190 PeerOrSelfReviewSubmission,
191 "
192SELECT *
193FROM peer_or_self_review_submissions
194WHERE id = ANY($1)
195 AND deleted_at IS NULL
196 ",
197 ids
198 )
199 .fetch_all(conn)
200 .await?;
201 Ok(res)
202}
203
204pub async fn get_users_submission_ids_for_exercise_and_course_instance(
205 conn: &mut PgConnection,
206 user_id: Uuid,
207 exercise_id: Uuid,
208 course_id: Uuid,
209) -> ModelResult<Vec<Uuid>> {
210 let res = sqlx::query!(
211 "
212SELECT exercise_slide_submission_id
213FROM peer_or_self_review_submissions
214WHERE user_id = $1
215 AND exercise_id = $2
216 AND course_id = $3
217 AND deleted_at IS NULL
218 ",
219 user_id,
220 exercise_id,
221 course_id
222 )
223 .fetch_all(conn)
224 .await?
225 .into_iter()
226 .map(|record| record.exercise_slide_submission_id)
227 .collect();
228 Ok(res)
229}
230
231pub async fn get_all_received_peer_or_self_review_submissions_for_user_and_course(
232 conn: &mut PgConnection,
233 user_id: Uuid,
234 course_id: Uuid,
235) -> ModelResult<Vec<PeerOrSelfReviewSubmission>> {
236 let res = sqlx::query_as!(
237 PeerOrSelfReviewSubmission,
238 "
239SELECT prs.*
240FROM exercise_slide_submissions ess
241INNER JOIN peer_or_self_review_submissions prs ON (ess.id = prs.exercise_slide_submission_id)
242WHERE ess.user_id = $1
243 AND ess.course_id = $2
244 AND ess.deleted_at IS NULL
245 AND prs.deleted_at IS NULL
246 ",
247 user_id,
248 course_id
249 )
250 .fetch_all(conn)
251 .await?;
252 Ok(res)
253}
254
255pub async fn get_all_given_peer_or_self_review_submissions_for_user_and_course(
256 conn: &mut PgConnection,
257 user_id: Uuid,
258 course_id: Uuid,
259) -> ModelResult<Vec<PeerOrSelfReviewSubmission>> {
260 let res = sqlx::query_as!(
261 PeerOrSelfReviewSubmission,
262 "
263SELECT *
264FROM peer_or_self_review_submissions
265WHERE user_id = $1
266 AND course_id = $2
267 AND deleted_at IS NULL
268 ",
269 user_id,
270 course_id
271 )
272 .fetch_all(conn)
273 .await?;
274 Ok(res)
275}
276
277pub async fn get_num_peer_reviews_given_by_user_and_course_instance_and_exercise(
278 conn: &mut PgConnection,
279 user_id: Uuid,
280 course_id: Uuid,
281 exercise_id: Uuid,
282) -> ModelResult<i64> {
283 let res = sqlx::query!(
284 "
285SELECT COUNT(*)
286FROM peer_or_self_review_submissions
287WHERE user_id = $1
288 AND exercise_id = $3
289 AND course_id = $2
290 AND deleted_at IS NULL
291 ",
292 user_id,
293 course_id,
294 exercise_id
295 )
296 .fetch_one(conn)
297 .await?;
298 Ok(res.count.unwrap_or(0))
299}
300
301pub async fn get_peer_reviews_given_by_user_and_course_instance_and_exercise(
302 conn: &mut PgConnection,
303 user_id: Uuid,
304 course_id: Uuid,
305 exercise_id: Uuid,
306) -> ModelResult<Vec<PeerOrSelfReviewSubmission>> {
307 let res = sqlx::query_as!(
308 PeerOrSelfReviewSubmission,
309 "
310SELECT *
311FROM peer_or_self_review_submissions
312WHERE user_id = $1
313 AND exercise_id = $3
314 AND course_id = $2
315 AND deleted_at IS NULL
316 ",
317 user_id,
318 course_id,
319 exercise_id
320 )
321 .fetch_all(conn)
322 .await?;
323 Ok(res)
324}
325
326pub async fn get_users_submission_count_for_exercise_and_course_instance(
327 conn: &mut PgConnection,
328 user_id: Uuid,
329 exercise_id: Uuid,
330 course_id: Uuid,
331) -> ModelResult<u32> {
332 let res = sqlx::query!(
333 "
334SELECT COUNT(*) AS count
335FROM peer_or_self_review_submissions
336WHERE user_id = $1
337 AND exercise_id = $2
338 AND course_id = $3
339 AND deleted_at IS NULL
340 ",
341 user_id,
342 exercise_id,
343 course_id
344 )
345 .fetch_one(conn)
346 .await?;
347 Ok(res.count.unwrap_or(0).try_into()?)
348}
349
350pub async fn get_last_time_user_submitted_peer_review(
351 conn: &mut PgConnection,
352 user_id: Uuid,
353 exercise_id: Uuid,
354 course_id: Uuid,
355) -> ModelResult<Option<DateTime<Utc>>> {
356 let res = sqlx::query!(
357 "
358SELECT MAX(created_at) as latest_submission_time
359FROM peer_or_self_review_submissions
360WHERE user_id = $1
361 AND exercise_id = $2
362 AND course_id = $3
363 AND deleted_at IS NULL
364 ",
365 user_id,
366 exercise_id,
367 course_id
368 )
369 .fetch_optional(conn)
370 .await?;
371 Ok(res.and_then(|o| o.latest_submission_time))
372}
373
374pub async fn count_peer_or_self_review_submissions_for_exercise_slide_submission(
375 conn: &mut PgConnection,
376 exercise_slide_submission_id: Uuid,
377 exclude_user_ids: &[Uuid],
378) -> ModelResult<u32> {
379 let res = sqlx::query!(
380 "
381SELECT COUNT(*) AS count
382FROM peer_or_self_review_submissions
383WHERE exercise_slide_submission_id = $1
384 AND user_id != ALL($2)
385 AND deleted_at IS NULL
386 ",
387 exercise_slide_submission_id,
388 exclude_user_ids
389 )
390 .fetch_one(conn)
391 .await?;
392 Ok(res.count.unwrap_or(0).try_into()?)
393}
394
395pub async fn get_self_review_submission_by_user_and_exercise(
396 conn: &mut PgConnection,
397 user_id: Uuid,
398 exercise_id: Uuid,
399 course_id: Uuid,
400) -> ModelResult<Option<PeerOrSelfReviewSubmission>> {
401 let res = sqlx::query_as!(
402 PeerOrSelfReviewSubmission,
403 "
404SELECT prs.*
405FROM peer_or_self_review_submissions prs
406JOIN exercise_slide_submissions ess ON (ess.id = prs.exercise_slide_submission_id)
407WHERE ess.user_id = $1
408 AND prs.exercise_id = $2
409 AND prs.course_id = $3
410 AND prs.deleted_at IS NULL
411 AND ess.deleted_at IS NULL
412 ",
413 user_id,
414 exercise_id,
415 course_id
416 )
417 .fetch_optional(conn)
418 .await?;
419 Ok(res)
420}
421
422pub async fn get_received_peer_or_self_review_submissions_for_user_by_peer_or_self_review_config_id_and_exercise_slide_submission(
423 conn: &mut PgConnection,
424 user_id: Uuid,
425 exercise_slide_submission_id: Uuid,
426 peer_or_self_review_config_id: Uuid,
427) -> ModelResult<Vec<PeerOrSelfReviewSubmission>> {
428 let res = sqlx::query_as!(
429 PeerOrSelfReviewSubmission,
430 "
431SELECT prs.*
432FROM peer_or_self_review_submissions prs
433 JOIN exercise_slide_submissions ess ON (ess.id = prs.exercise_slide_submission_id)
434WHERE ess.user_id = $1
435 AND ess.id = $2
436 AND prs.peer_or_self_review_config_id = $3
437 AND prs.deleted_at IS NULL
438 AND ess.deleted_at IS NULL
439 ",
440 user_id,
441 exercise_slide_submission_id,
442 peer_or_self_review_config_id
443 )
444 .fetch_all(conn)
445 .await?;
446 Ok(res)
447}
448
449pub async fn get_mapping_from_peer_or_self_review_submission_ids_to_peer_review_giver_user_ids(
450 conn: &mut PgConnection,
451 peer_or_self_review_submission_ids: &[Uuid],
452) -> ModelResult<HashMap<Uuid, Uuid>> {
453 let full = get_by_ids(conn, peer_or_self_review_submission_ids).await?;
454 Ok(full
455 .into_iter()
456 .map(|submission: PeerOrSelfReviewSubmission| (submission.id, submission.user_id))
457 .collect())
458}