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 id
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 get_by_id(
139    conn: &mut PgConnection,
140    id: Uuid,
141) -> ModelResult<PeerOrSelfReviewSubmission> {
142    let res = sqlx::query_as!(
143        PeerOrSelfReviewSubmission,
144        "
145SELECT *
146FROM peer_or_self_review_submissions
147WHERE id = $1
148  AND deleted_at IS NULL
149        ",
150        id
151    )
152    .fetch_one(conn)
153    .await?;
154    Ok(res)
155}
156
157pub async fn get_by_ids(
158    conn: &mut PgConnection,
159    ids: &[Uuid],
160) -> ModelResult<Vec<PeerOrSelfReviewSubmission>> {
161    let res = sqlx::query_as!(
162        PeerOrSelfReviewSubmission,
163        "
164SELECT *
165FROM peer_or_self_review_submissions
166WHERE id = ANY($1)
167  AND deleted_at IS NULL
168        ",
169        ids
170    )
171    .fetch_all(conn)
172    .await?;
173    Ok(res)
174}
175
176pub async fn get_users_submission_ids_for_exercise_and_course_instance(
177    conn: &mut PgConnection,
178    user_id: Uuid,
179    exercise_id: Uuid,
180    course_id: Uuid,
181) -> ModelResult<Vec<Uuid>> {
182    let res = sqlx::query!(
183        "
184SELECT exercise_slide_submission_id
185FROM peer_or_self_review_submissions
186WHERE user_id = $1
187  AND exercise_id = $2
188  AND course_id = $3
189  AND deleted_at IS NULL
190    ",
191        user_id,
192        exercise_id,
193        course_id
194    )
195    .fetch_all(conn)
196    .await?
197    .into_iter()
198    .map(|record| record.exercise_slide_submission_id)
199    .collect();
200    Ok(res)
201}
202
203pub async fn get_all_received_peer_or_self_review_submissions_for_user_and_course(
204    conn: &mut PgConnection,
205    user_id: Uuid,
206    course_id: Uuid,
207) -> ModelResult<Vec<PeerOrSelfReviewSubmission>> {
208    let res = sqlx::query_as!(
209        PeerOrSelfReviewSubmission,
210        "
211SELECT prs.*
212FROM exercise_slide_submissions ess
213INNER JOIN peer_or_self_review_submissions prs ON (ess.id = prs.exercise_slide_submission_id)
214WHERE ess.user_id = $1
215  AND ess.course_id = $2
216  AND ess.deleted_at IS NULL
217  AND prs.deleted_at IS NULL
218    ",
219        user_id,
220        course_id
221    )
222    .fetch_all(conn)
223    .await?;
224    Ok(res)
225}
226
227pub async fn get_all_given_peer_or_self_review_submissions_for_user_and_course(
228    conn: &mut PgConnection,
229    user_id: Uuid,
230    course_id: Uuid,
231) -> ModelResult<Vec<PeerOrSelfReviewSubmission>> {
232    let res = sqlx::query_as!(
233        PeerOrSelfReviewSubmission,
234        "
235SELECT *
236FROM peer_or_self_review_submissions
237WHERE user_id = $1
238  AND course_id = $2
239  AND deleted_at IS NULL
240    ",
241        user_id,
242        course_id
243    )
244    .fetch_all(conn)
245    .await?;
246    Ok(res)
247}
248
249pub async fn get_num_peer_reviews_given_by_user_and_course_instance_and_exercise(
250    conn: &mut PgConnection,
251    user_id: Uuid,
252    course_id: Uuid,
253    exercise_id: Uuid,
254) -> ModelResult<i64> {
255    let res = sqlx::query!(
256        "
257SELECT COUNT(*)
258FROM peer_or_self_review_submissions
259WHERE user_id = $1
260  AND exercise_id = $3
261  AND course_id = $2
262  AND deleted_at IS NULL
263    ",
264        user_id,
265        course_id,
266        exercise_id
267    )
268    .fetch_one(conn)
269    .await?;
270    Ok(res.count.unwrap_or(0))
271}
272
273pub async fn get_peer_reviews_given_by_user_and_course_instance_and_exercise(
274    conn: &mut PgConnection,
275    user_id: Uuid,
276    course_id: Uuid,
277    exercise_id: Uuid,
278) -> ModelResult<Vec<PeerOrSelfReviewSubmission>> {
279    let res = sqlx::query_as!(
280        PeerOrSelfReviewSubmission,
281        "
282SELECT *
283FROM peer_or_self_review_submissions
284WHERE user_id = $1
285  AND exercise_id = $3
286  AND course_id = $2
287  AND deleted_at IS NULL
288    ",
289        user_id,
290        course_id,
291        exercise_id
292    )
293    .fetch_all(conn)
294    .await?;
295    Ok(res)
296}
297
298pub async fn get_users_submission_count_for_exercise_and_course_instance(
299    conn: &mut PgConnection,
300    user_id: Uuid,
301    exercise_id: Uuid,
302    course_id: Uuid,
303) -> ModelResult<u32> {
304    let res = sqlx::query!(
305        "
306SELECT COUNT(*) AS count
307FROM peer_or_self_review_submissions
308WHERE user_id = $1
309  AND exercise_id = $2
310  AND course_id = $3
311  AND deleted_at IS NULL
312        ",
313        user_id,
314        exercise_id,
315        course_id
316    )
317    .fetch_one(conn)
318    .await?;
319    Ok(res.count.unwrap_or(0).try_into()?)
320}
321
322pub async fn get_last_time_user_submitted_peer_review(
323    conn: &mut PgConnection,
324    user_id: Uuid,
325    exercise_id: Uuid,
326    course_id: Uuid,
327) -> ModelResult<Option<DateTime<Utc>>> {
328    let res = sqlx::query!(
329        "
330SELECT MAX(created_at) as latest_submission_time
331FROM peer_or_self_review_submissions
332WHERE user_id = $1
333  AND exercise_id = $2
334  AND course_id = $3
335  AND deleted_at IS NULL
336        ",
337        user_id,
338        exercise_id,
339        course_id
340    )
341    .fetch_optional(conn)
342    .await?;
343    Ok(res.and_then(|o| o.latest_submission_time))
344}
345
346pub async fn count_peer_or_self_review_submissions_for_exercise_slide_submission(
347    conn: &mut PgConnection,
348    exercise_slide_submission_id: Uuid,
349    exclude_user_ids: &[Uuid],
350) -> ModelResult<u32> {
351    let res = sqlx::query!(
352        "
353SELECT COUNT(*) AS count
354FROM peer_or_self_review_submissions
355WHERE exercise_slide_submission_id = $1
356  AND user_id != ALL($2)
357  AND deleted_at IS NULL
358        ",
359        exercise_slide_submission_id,
360        exclude_user_ids
361    )
362    .fetch_one(conn)
363    .await?;
364    Ok(res.count.unwrap_or(0).try_into()?)
365}
366
367pub async fn get_self_review_submission_by_user_and_exercise(
368    conn: &mut PgConnection,
369    user_id: Uuid,
370    exercise_id: Uuid,
371    course_id: Uuid,
372) -> ModelResult<Option<PeerOrSelfReviewSubmission>> {
373    let res = sqlx::query_as!(
374        PeerOrSelfReviewSubmission,
375        "
376SELECT prs.*
377FROM peer_or_self_review_submissions prs
378JOIN exercise_slide_submissions ess ON (ess.id = prs.exercise_slide_submission_id)
379WHERE ess.user_id = $1
380  AND prs.exercise_id = $2
381  AND prs.course_id = $3
382  AND prs.deleted_at IS NULL
383  AND ess.deleted_at IS NULL
384        ",
385        user_id,
386        exercise_id,
387        course_id
388    )
389    .fetch_optional(conn)
390    .await?;
391    Ok(res)
392}
393
394pub async fn get_received_peer_or_self_review_submissions_for_user_by_peer_or_self_review_config_id_and_exercise_slide_submission(
395    conn: &mut PgConnection,
396    user_id: Uuid,
397    exercise_slide_submission_id: Uuid,
398    peer_or_self_review_config_id: Uuid,
399) -> ModelResult<Vec<PeerOrSelfReviewSubmission>> {
400    let res = sqlx::query_as!(
401        PeerOrSelfReviewSubmission,
402        "
403SELECT prs.*
404FROM peer_or_self_review_submissions prs
405  JOIN exercise_slide_submissions ess ON (ess.id = prs.exercise_slide_submission_id)
406WHERE ess.user_id = $1
407  AND ess.id = $2
408  AND prs.peer_or_self_review_config_id = $3
409  AND prs.deleted_at IS NULL
410  AND ess.deleted_at IS NULL
411        ",
412        user_id,
413        exercise_slide_submission_id,
414        peer_or_self_review_config_id
415    )
416    .fetch_all(conn)
417    .await?;
418    Ok(res)
419}
420
421pub async fn get_mapping_from_peer_or_self_review_submission_ids_to_peer_review_giver_user_ids(
422    conn: &mut PgConnection,
423    peer_or_self_review_submission_ids: &[Uuid],
424) -> ModelResult<HashMap<Uuid, Uuid>> {
425    let full = get_by_ids(conn, peer_or_self_review_submission_ids).await?;
426    Ok(full
427        .into_iter()
428        .map(|submission: PeerOrSelfReviewSubmission| (submission.id, submission.user_id))
429        .collect())
430}