headless_lms_models/
exercise_task_submissions.rs

1use std::collections::HashSet;
2
3use futures::{Stream, future::BoxFuture};
4use serde_json::Value;
5use url::Url;
6
7use crate::{
8    CourseOrExamId,
9    exercise_service_info::{self, ExerciseServiceInfoApi},
10    exercise_services, exercise_slide_submissions,
11    exercise_tasks::{CourseMaterialExerciseTask, ExerciseTask},
12    library::custom_view_exercises::{CustomViewExerciseTaskSubmission, CustomViewExerciseTasks},
13    peer_or_self_review_question_submissions::PeerOrSelfReviewQuestionSubmission,
14    peer_or_self_review_questions::PeerOrSelfReviewQuestion,
15    peer_or_self_review_submissions::PeerOrSelfReviewSubmission,
16    prelude::*,
17};
18
19#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
20#[cfg_attr(feature = "ts_rs", derive(TS))]
21pub struct ExerciseTaskSubmission {
22    pub id: Uuid,
23    pub created_at: DateTime<Utc>,
24    pub updated_at: DateTime<Utc>,
25    pub deleted_at: Option<DateTime<Utc>>,
26    pub exercise_slide_submission_id: Uuid,
27    pub exercise_task_id: Uuid,
28    pub exercise_slide_id: Uuid,
29    pub data_json: Option<serde_json::Value>,
30    pub exercise_task_grading_id: Option<Uuid>,
31    pub metadata: Option<serde_json::Value>,
32}
33
34#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
35#[cfg_attr(feature = "ts_rs", derive(TS))]
36pub struct PeerOrSelfReviewsReceived {
37    pub peer_or_self_review_questions: Vec<PeerOrSelfReviewQuestion>,
38    pub peer_or_self_review_question_submissions: Vec<PeerOrSelfReviewQuestionSubmission>,
39    pub peer_or_self_review_submissions: Vec<PeerOrSelfReviewSubmission>,
40}
41
42#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
43#[cfg_attr(feature = "ts_rs", derive(TS))]
44pub struct SubmissionData {
45    pub exercise_id: Uuid,
46    pub course_id: Uuid,
47    pub exercise_slide_submission_id: Uuid,
48    pub exercise_slide_id: Uuid,
49    pub exercise_task_id: Uuid,
50    pub user_id: Uuid,
51    pub course_instance_id: Uuid,
52    pub data_json: Value,
53    pub id: Uuid,
54}
55
56#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
57#[cfg_attr(feature = "ts_rs", derive(TS))]
58pub struct ExportedSubmission {
59    pub id: Uuid,
60    pub user_id: Uuid,
61    pub created_at: DateTime<Utc>,
62    pub exercise_id: Uuid,
63    pub exercise_task_id: Uuid,
64    pub score_given: Option<f32>,
65    pub data_json: Option<serde_json::Value>,
66}
67
68#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
69#[cfg_attr(feature = "ts_rs", derive(TS))]
70pub struct ExportedCourseSubmission {
71    pub exercise_slide_submission_id: Uuid,
72    pub id: Uuid,
73    pub user_id: Uuid,
74    pub created_at: DateTime<Utc>,
75    pub course_id: Option<Uuid>,
76    pub exercise_id: Uuid,
77    pub exercise_task_id: Uuid,
78    pub score_given: Option<f32>,
79    pub data_json: Option<serde_json::Value>,
80}
81
82/// One row for CSV export: a single attempt at an exercise task for a given exercise_slide_submission (chronological submission order).
83#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
84pub struct ExerciseTaskSubmissionCsvExportData {
85    pub exercise_slide_submission_id: Uuid,
86    pub exercise_task_submission_id: Uuid,
87    pub exercise_task_id: Uuid,
88    pub exercise_id: Uuid,
89    pub user_id: Uuid,
90    pub submitted_at: DateTime<Utc>,
91    pub answer: Option<serde_json::Value>,
92}
93
94pub async fn get_submission(
95    conn: &mut PgConnection,
96    submission_id: Uuid,
97) -> ModelResult<ExerciseTaskSubmission> {
98    let res = sqlx::query_as!(
99        ExerciseTaskSubmission,
100        "
101SELECT *
102FROM exercise_task_submissions
103WHERE id = $1
104",
105        submission_id
106    )
107    .fetch_one(conn)
108    .await?;
109    Ok(res)
110}
111
112// TODO: Merge with the other insert, but need to resolve different parameters.
113pub async fn insert_with_id(
114    conn: &mut PgConnection,
115    submission_data: &SubmissionData,
116) -> ModelResult<Uuid> {
117    let res = sqlx::query!(
118        "
119INSERT INTO exercise_task_submissions (
120    id,
121    exercise_slide_submission_id,
122    exercise_slide_id,
123    exercise_task_id,
124    data_json
125  )
126VALUES ($1, $2, $3, $4, $5)
127RETURNING id
128        ",
129        submission_data.id,
130        submission_data.exercise_slide_submission_id,
131        submission_data.exercise_slide_id,
132        submission_data.exercise_task_id,
133        submission_data.data_json,
134    )
135    .fetch_one(conn)
136    .await?;
137    Ok(res.id)
138}
139
140pub async fn insert(
141    conn: &mut PgConnection,
142    pkey_policy: PKeyPolicy<Uuid>,
143    exercise_slide_submission_id: Uuid,
144    exercise_slide_id: Uuid,
145    exercise_task_id: Uuid,
146    data_json: &Value,
147) -> ModelResult<Uuid> {
148    let res = sqlx::query!(
149        "
150INSERT INTO exercise_task_submissions (
151    id,
152    exercise_slide_submission_id,
153    exercise_slide_id,
154    exercise_task_id,
155    data_json
156  )
157  VALUES ($1, $2, $3, $4, $5)
158  RETURNING id
159        ",
160        pkey_policy.into_uuid(),
161        exercise_slide_submission_id,
162        exercise_slide_id,
163        exercise_task_id,
164        data_json,
165    )
166    .fetch_one(conn)
167    .await?;
168    Ok(res.id)
169}
170
171pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<ExerciseTaskSubmission> {
172    let submission = sqlx::query_as!(
173        ExerciseTaskSubmission,
174        "
175SELECT *
176FROM exercise_task_submissions
177WHERE id = $1
178",
179        id
180    )
181    .fetch_one(conn)
182    .await?;
183    Ok(submission)
184}
185
186pub async fn get_by_exercise_slide_submission_id(
187    conn: &mut PgConnection,
188    exercise_slide_submission_id: Uuid,
189) -> ModelResult<Vec<ExerciseTaskSubmission>> {
190    let submissions = sqlx::query_as!(
191        ExerciseTaskSubmission,
192        "
193SELECT *
194FROM exercise_task_submissions
195WHERE exercise_slide_submission_id = $1
196        ",
197        exercise_slide_submission_id
198    )
199    .fetch_all(conn)
200    .await?;
201    Ok(submissions)
202}
203
204/// Fetches CSV-exportable rows at attempt granularity in intended ordering.
205pub async fn get_csv_export_data_by_exercise_and_task(
206    conn: &mut PgConnection,
207    exercise_id: Uuid,
208    exercise_task_id: Uuid,
209) -> ModelResult<Vec<ExerciseTaskSubmissionCsvExportData>> {
210    let submissions = sqlx::query_as!(
211        ExerciseTaskSubmissionCsvExportData,
212        r#"
213SELECT ets.exercise_slide_submission_id,
214  ets.id AS exercise_task_submission_id,
215  ets.exercise_task_id,
216  ess.exercise_id,
217  ess.user_id,
218  ets.created_at AS submitted_at,
219  ets.data_json AS answer
220FROM exercise_task_submissions ets
221  JOIN exercise_slide_submissions ess ON ets.exercise_slide_submission_id = ess.id
222WHERE ess.exercise_id = $1
223  AND ets.exercise_task_id = $2
224  AND ess.deleted_at IS NULL
225  AND ets.deleted_at IS NULL
226ORDER BY ets.created_at ASC
227        "#,
228        exercise_id,
229        exercise_task_id
230    )
231    .fetch_all(conn)
232    .await?;
233    Ok(submissions)
234}
235
236/// Fetches CSV-exportable rows for the latest submission per user only, ordered by submitted_at.
237pub async fn get_csv_export_data_by_exercise_and_task_latest_per_user(
238    conn: &mut PgConnection,
239    exercise_id: Uuid,
240    exercise_task_id: Uuid,
241) -> ModelResult<Vec<ExerciseTaskSubmissionCsvExportData>> {
242    let submissions = sqlx::query_as!(
243        ExerciseTaskSubmissionCsvExportData,
244        r#"
245WITH latest AS (
246  SELECT DISTINCT ON (ess.user_id) ets.id AS exercise_task_submission_id
247  FROM exercise_task_submissions ets
248  JOIN exercise_slide_submissions ess ON ets.exercise_slide_submission_id = ess.id
249  WHERE ess.exercise_id = $1
250    AND ets.exercise_task_id = $2
251    AND ess.deleted_at IS NULL
252    AND ets.deleted_at IS NULL
253  ORDER BY ess.user_id, ets.created_at DESC
254)
255SELECT ets.exercise_slide_submission_id,
256  ets.id AS exercise_task_submission_id,
257  ets.exercise_task_id,
258  ess.exercise_id,
259  ess.user_id,
260  ets.created_at AS submitted_at,
261  ets.data_json AS answer
262FROM exercise_task_submissions ets
263JOIN exercise_slide_submissions ess ON ets.exercise_slide_submission_id = ess.id
264JOIN latest ON latest.exercise_task_submission_id = ets.id
265WHERE ess.exercise_id = $1
266  AND ets.exercise_task_id = $2
267  AND ess.deleted_at IS NULL
268  AND ets.deleted_at IS NULL
269ORDER BY ets.created_at ASC
270        "#,
271        exercise_id,
272        exercise_task_id
273    )
274    .fetch_all(conn)
275    .await?;
276    Ok(submissions)
277}
278
279pub async fn get_users_latest_exercise_task_submissions_for_exercise_slide(
280    conn: &mut PgConnection,
281    exercise_slide_id: Uuid,
282    user_id: Uuid,
283) -> ModelResult<Option<Vec<ExerciseTaskSubmission>>> {
284    let exercise_slide_submission =
285        exercise_slide_submissions::try_to_get_users_latest_exercise_slide_submission(
286            conn,
287            exercise_slide_id,
288            user_id,
289        )
290        .await?;
291    if let Some(exercise_slide_submission) = exercise_slide_submission {
292        let task_submissions = sqlx::query_as!(
293            ExerciseTaskSubmission,
294            "
295SELECT *
296FROM exercise_task_submissions
297WHERE exercise_slide_submission_id = $1
298  AND deleted_at IS NULL
299            ",
300            exercise_slide_submission.id
301        )
302        .fetch_all(conn)
303        .await?;
304        Ok(Some(task_submissions))
305    } else {
306        Ok(None)
307    }
308}
309
310pub async fn get_course_and_exam_id(
311    conn: &mut PgConnection,
312    id: Uuid,
313) -> ModelResult<CourseOrExamId> {
314    let res = sqlx::query!(
315        "
316SELECT ess.course_id,
317  ess.exam_id
318FROM exercise_task_submissions ets
319  JOIN exercise_slide_submissions ess ON ets.exercise_slide_submission_id = ess.id
320WHERE ets.id = $1
321  AND ets.deleted_at IS NULL
322  AND ess.deleted_at IS NULL
323        ",
324        id
325    )
326    .fetch_one(conn)
327    .await?;
328    CourseOrExamId::from_course_and_exam_ids(res.course_id, res.exam_id)
329}
330
331pub async fn get_peer_reviews_received(
332    conn: &mut PgConnection,
333    exercise_id: Uuid,
334    exercise_slide_submission_id: Uuid,
335    user_id: Uuid,
336) -> ModelResult<PeerOrSelfReviewsReceived> {
337    let exercise = crate::exercises::get_by_id(&mut *conn, exercise_id).await?;
338    let peer_or_self_review_config =
339        crate::peer_or_self_review_configs::get_by_exercise_or_course_id(
340            &mut *conn,
341            &exercise,
342            exercise.course_id.ok_or_else(|| {
343                ModelError::new(
344                    ModelErrorType::InvalidRequest,
345                    "Peer reviews work only on courses (and not, for example, on exams)"
346                        .to_string(),
347                    None,
348                )
349            })?,
350        )
351        .await?;
352    let peer_or_self_review_questions =
353        crate::peer_or_self_review_questions::get_by_peer_or_self_review_configs_id(
354            &mut *conn,
355            peer_or_self_review_config.id,
356        )
357        .await?;
358
359    let peer_or_self_review_question_ids = peer_or_self_review_questions
360        .iter()
361        .map(|x| x.id)
362        .collect::<Vec<_>>();
363
364    let peer_or_self_review_submissions =
365        crate::peer_or_self_review_submissions::get_received_peer_or_self_review_submissions_for_user_by_peer_or_self_review_config_id_and_exercise_slide_submission(
366            &mut *conn,
367            user_id,
368            exercise_slide_submission_id,
369            peer_or_self_review_config.id,
370        )
371        .await?;
372
373    let peer_or_self_review_question_submissions =
374        crate::peer_or_self_review_question_submissions::get_by_peer_reviews_question_ids(
375            &mut *conn,
376            &peer_or_self_review_question_ids,
377            user_id,
378            exercise_slide_submission_id,
379        )
380        .await?;
381
382    Ok(PeerOrSelfReviewsReceived {
383        peer_or_self_review_questions,
384        peer_or_self_review_question_submissions,
385        peer_or_self_review_submissions,
386    })
387}
388
389pub async fn set_grading_id(
390    conn: &mut PgConnection,
391    grading_id: Uuid,
392    submission_id: Uuid,
393) -> ModelResult<ExerciseTaskSubmission> {
394    let res = sqlx::query_as!(
395        ExerciseTaskSubmission,
396        "
397UPDATE exercise_task_submissions
398SET exercise_task_grading_id = $1
399WHERE id = $2
400RETURNING *
401",
402        grading_id,
403        submission_id
404    )
405    .fetch_one(conn)
406    .await?;
407    Ok(res)
408}
409
410pub fn stream_exam_submissions(
411    conn: &mut PgConnection,
412    exam_id: Uuid,
413) -> impl Stream<Item = sqlx::Result<ExportedSubmission>> + '_ {
414    sqlx::query_as!(
415        ExportedSubmission,
416        "
417SELECT exercise_task_submissions.id,
418  user_id,
419  exercise_task_submissions.created_at,
420  exercise_slide_submissions.exercise_id,
421  exercise_task_submissions.exercise_task_id,
422  exercise_task_gradings.score_given,
423  exercise_task_submissions.data_json
424FROM exercise_task_submissions
425  JOIN exercise_slide_submissions ON exercise_task_submissions.exercise_slide_submission_id = exercise_slide_submissions.id
426  JOIN exercise_task_gradings on exercise_task_submissions.exercise_task_grading_id = exercise_task_gradings.id
427  JOIN exercises on exercise_slide_submissions.exercise_id = exercises.id
428WHERE exercise_slide_submissions.exam_id = $1
429  AND exercise_task_submissions.deleted_at IS NULL
430  AND exercise_task_gradings.deleted_at IS NULL
431  AND exercises.deleted_at IS NULL;
432        ",
433        exam_id
434    )
435    .fetch(conn)
436}
437
438pub fn stream_course_submissions(
439    conn: &mut PgConnection,
440    course_id: Uuid,
441) -> impl Stream<Item = sqlx::Result<ExportedCourseSubmission>> + '_ {
442    sqlx::query_as!(
443        ExportedCourseSubmission,
444        "
445SELECT exercise_task_submissions.exercise_slide_submission_id,
446  exercise_task_submissions.id,
447  user_id,
448  exercise_task_submissions.created_at,
449  exercise_slide_submissions.course_id,
450  exercise_slide_submissions.exercise_id,
451  exercise_task_submissions.exercise_task_id,
452  exercise_task_gradings.score_given,
453  exercise_task_submissions.data_json
454FROM exercise_task_submissions
455  JOIN exercise_slide_submissions ON exercise_task_submissions.exercise_slide_submission_id = exercise_slide_submissions.id
456  JOIN exercise_task_gradings ON exercise_task_submissions.exercise_task_grading_id = exercise_task_gradings.id
457  JOIN exercises ON exercise_slide_submissions.exercise_id = exercises.id
458WHERE exercise_slide_submissions.course_id = $1
459  AND exercise_slide_submissions.deleted_at IS NULL
460  AND exercise_task_submissions.deleted_at IS NULL
461  AND exercise_task_gradings.deleted_at IS NULL
462  AND exercises.deleted_at IS NULL;
463        ",
464        course_id
465    )
466    .fetch(conn)
467}
468
469/// Used to get the necessary info for rendering a submission either when we're viewing a submission, or we're conducting a peer review.
470pub async fn get_exercise_task_submission_info_by_exercise_slide_submission_id(
471    conn: &mut PgConnection,
472    exercise_slide_submission_id: Uuid,
473    viewer_user_id: Uuid,
474    fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
475    include_deleted_tasks: bool,
476) -> ModelResult<Vec<CourseMaterialExerciseTask>> {
477    let task_submisssions = crate::exercise_task_submissions::get_by_exercise_slide_submission_id(
478        &mut *conn,
479        exercise_slide_submission_id,
480    )
481    .await?;
482    let exercise_task_gradings =
483        crate::exercise_task_gradings::get_all_gradings_by_exercise_slide_submission_id(
484            &mut *conn,
485            exercise_slide_submission_id,
486        )
487        .await?;
488
489    let exercise_tasks = if include_deleted_tasks {
490        crate::exercise_tasks::get_exercise_tasks_by_exercise_slide_id_including_deleted::<
491            Vec<ExerciseTask>,
492        >(&mut *conn, &task_submisssions[0].exercise_slide_id)
493        .await?
494    } else {
495        crate::exercise_tasks::get_exercise_tasks_by_exercise_slide_id::<Vec<ExerciseTask>>(
496            &mut *conn,
497            &task_submisssions[0].exercise_slide_id,
498        )
499        .await?
500    };
501
502    let mut res = Vec::with_capacity(task_submisssions.len());
503
504    let unique_exercise_service_slugs = exercise_tasks
505        .iter()
506        .cloned()
507        .map(|et| et.exercise_type)
508        .collect::<HashSet<_>>()
509        .into_iter()
510        .collect::<Vec<_>>();
511    let exercise_service_slug_to_service_and_info =
512        exercise_service_info::get_selected_exercise_services_by_type(
513            &mut *conn,
514            &unique_exercise_service_slugs,
515            fetch_service_info,
516        )
517        .await?;
518
519    for ts in task_submisssions {
520        let grading = exercise_task_gradings
521            .iter()
522            .find(|g| Some(g.id) == ts.exercise_task_grading_id)
523            .ok_or_else(|| {
524                ModelError::new(
525                    ModelErrorType::NotFound,
526                    "Grading not found".to_string(),
527                    None,
528                )
529            })?;
530        let task = exercise_tasks
531            .iter()
532            .find(|t| t.id == ts.exercise_task_id)
533            .ok_or_else(|| {
534                ModelError::new(
535                    ModelErrorType::NotFound,
536                    "Exercise task not found".to_string(),
537                    None,
538                )
539            })?;
540        let (exercise_service, service_info) = exercise_service_slug_to_service_and_info
541            .get(&task.exercise_type)
542            .ok_or_else(|| {
543                ModelError::new(
544                    ModelErrorType::InvalidRequest,
545                    "Exercise service not found".to_string(),
546                    None,
547                )
548            })?;
549        let mut exercise_iframe_url =
550            exercise_services::get_exercise_service_externally_preferred_baseurl(exercise_service)?;
551        exercise_iframe_url.set_path(&service_info.user_interface_iframe_path);
552        let course_material_exercise_task = CourseMaterialExerciseTask {
553            id: task.id,
554            exercise_service_slug: task.exercise_type.clone(),
555            exercise_slide_id: task.exercise_slide_id,
556            exercise_iframe_url: Some(exercise_iframe_url.to_string()),
557            pseudonumous_user_id: Some(Uuid::new_v5(
558                &service_info.exercise_service_id,
559                viewer_user_id.as_bytes(),
560            )),
561            assignment: task.assignment.clone(),
562            public_spec: task.public_spec.clone(),
563            model_solution_spec: task.model_solution_spec.clone(),
564            previous_submission: Some(ts),
565            previous_submission_grading: Some(grading.clone()),
566            order_number: task.order_number,
567            deleted_at: task.deleted_at,
568        };
569        res.push(course_material_exercise_task);
570    }
571    Ok(res)
572}
573
574pub async fn get_user_custom_view_exercise_tasks_by_module_and_exercise_type(
575    conn: &mut PgConnection,
576    exercise_type: &str,
577    course_module_id: Uuid,
578    user_id: Uuid,
579    course_id: Uuid,
580) -> ModelResult<CustomViewExerciseTasks> {
581    let task_submissions =
582        crate::exercise_task_submissions::get_user_latest_exercise_task_submissions_by_course_module_and_exercise_type(
583            &mut *conn,
584            user_id,
585            exercise_type,
586            course_module_id,
587            course_id,
588        )
589        .await?;
590    let task_gradings =
591        crate::exercise_task_gradings::get_user_exercise_task_gradings_by_module_and_exercise_type(
592            &mut *conn,
593            user_id,
594            exercise_type,
595            course_module_id,
596            course_id,
597        )
598        .await?;
599
600    let exercise_tasks = crate::exercise_tasks::get_all_exercise_tasks_by_module_and_exercise_type(
601        &mut *conn,
602        exercise_type,
603        course_module_id,
604    )
605    .await?;
606    let res: CustomViewExerciseTasks = CustomViewExerciseTasks {
607        exercise_tasks,
608        task_submissions,
609        task_gradings,
610    };
611    Ok(res)
612}
613
614/// get all submissions for user and course module and exercise type
615pub async fn get_user_latest_exercise_task_submissions_by_course_module_and_exercise_type(
616    conn: &mut PgConnection,
617    user_id: Uuid,
618    exercise_type: &str,
619    module_id: Uuid,
620    course_id: Uuid,
621) -> ModelResult<Vec<CustomViewExerciseTaskSubmission>> {
622    let res: Vec<CustomViewExerciseTaskSubmission> = sqlx::query_as!(
623        CustomViewExerciseTaskSubmission,
624        r#"
625        SELECT DISTINCT ON (g.exercise_task_id)
626        g.id,
627        g.created_at,
628        g.exercise_slide_submission_id,
629        g.exercise_slide_id,
630        g.exercise_task_id,
631        g.exercise_task_grading_id,
632        g.data_json
633      FROM exercise_task_submissions g
634        JOIN exercise_tasks et ON et.id = g.exercise_task_id
635        JOIN exercise_slide_submissions ess ON ess.id = g.exercise_slide_submission_id
636        JOIN exercises e ON e.id = ess.exercise_id
637        JOIN chapters c ON c.id = e.chapter_id
638      WHERE ess.user_id = $1
639      AND ess.course_id = $2
640      AND et.exercise_type = $3
641      AND c.course_module_id = $4
642      AND g.deleted_at IS NULL
643      AND et.deleted_at IS NULL
644      AND ess.deleted_at IS NULL
645      AND e.deleted_at IS NULL
646      AND c.deleted_at IS NULL
647      ORDER BY g.exercise_task_id, g.created_at DESC
648      "#,
649        user_id,
650        course_id,
651        exercise_type,
652        module_id
653    )
654    .fetch_all(conn)
655    .await?;
656    Ok(res)
657}
658
659pub async fn get_ids_by_exercise_id(
660    conn: &mut PgConnection,
661    exercise_id: Uuid,
662) -> ModelResult<Vec<Uuid>> {
663    let res = sqlx::query!(
664        "
665SELECT id
666FROM exercise_task_submissions
667WHERE exercise_slide_submission_id IN (
668    SELECT id
669    FROM exercise_slide_submissions
670    WHERE exercise_id = $1
671)
672AND deleted_at IS NULL
673",
674        &exercise_id
675    )
676    .fetch_all(conn)
677    .await?;
678    Ok(res.iter().map(|x| x.id).collect())
679}
680
681/// Similar to get_ids_by_exercise_id but returns the record with the highest created_at for a user_id
682pub async fn get_latest_submission_ids_by_exercise_id(
683    conn: &mut PgConnection,
684    exercise_id: Uuid,
685) -> ModelResult<Vec<Uuid>> {
686    let res = sqlx::query!(
687        "
688SELECT id
689FROM exercise_task_submissions
690WHERE exercise_slide_submission_id IN (SELECT id
691    FROM (SELECT DISTINCT ON (user_id, exercise_id) *
692        FROM exercise_slide_submissions
693        WHERE exercise_id = $1
694        AND deleted_at IS NULL
695        ORDER BY user_id, exercise_id, created_at DESC) a )
696    AND deleted_at IS NULL
697",
698        &exercise_id
699    )
700    .fetch_all(conn)
701    .await?;
702    Ok(res.iter().map(|x| x.id).collect())
703}