Skip to main content

headless_lms_models/
peer_or_self_review_submissions.rs

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/// Same as PeerOrSelfReviewSubmission with optional submission owner (user who received the review). Used for "given" reviews.
21#[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
138/// How many peer and self reviews students have given on each exercise; exercises with none are
139/// left out.
140pub 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}