headless_lms_models/
exercise_slide_submissions.rs

1use std::collections::HashMap;
2
3use chrono::NaiveDate;
4use futures::future::BoxFuture;
5use rand::prelude::SliceRandom;
6use url::Url;
7
8use crate::{
9    CourseOrExamId,
10    courses::Course,
11    exams::{self, ExamEnrollment},
12    exercise_service_info::ExerciseServiceInfoApi,
13    exercise_task_gradings::UserPointsUpdateStrategy,
14    exercise_tasks::CourseMaterialExerciseTask,
15    exercises::{self, Exercise, GradingProgress},
16    prelude::*,
17    teacher_grading_decisions::{self, TeacherGradingDecision},
18    user_exercise_states::{self, UserExerciseState},
19};
20
21#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
22#[cfg_attr(feature = "ts_rs", derive(TS))]
23pub struct AnswerRequiringAttention {
24    pub id: Uuid,
25    pub user_id: Uuid,
26    pub created_at: DateTime<Utc>,
27    pub updated_at: DateTime<Utc>,
28    pub deleted_at: Option<DateTime<Utc>>,
29    pub data_json: Option<serde_json::Value>,
30    pub grading_progress: GradingProgress,
31    pub score_given: Option<f32>,
32    pub submission_id: Uuid,
33    pub exercise_id: Uuid,
34}
35#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
36#[cfg_attr(feature = "ts_rs", derive(TS))]
37pub struct NewExerciseSlideSubmission {
38    pub exercise_slide_id: Uuid,
39    pub course_id: Option<Uuid>,
40    pub exam_id: Option<Uuid>,
41    pub user_id: Uuid,
42    pub exercise_id: Uuid,
43    pub user_points_update_strategy: UserPointsUpdateStrategy,
44}
45
46#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
47#[cfg_attr(feature = "ts_rs", derive(TS))]
48pub struct ExerciseSlideSubmission {
49    pub id: Uuid,
50    pub created_at: DateTime<Utc>,
51    pub updated_at: DateTime<Utc>,
52    pub deleted_at: Option<DateTime<Utc>>,
53    pub exercise_slide_id: Uuid,
54    pub course_id: Option<Uuid>,
55    pub exam_id: Option<Uuid>,
56    pub exercise_id: Uuid,
57    pub user_id: Uuid,
58    pub user_points_update_strategy: UserPointsUpdateStrategy,
59    pub flag_count: Option<i32>,
60}
61
62impl ExerciseSlideSubmission {
63    pub fn get_course_id(&self) -> ModelResult<Uuid> {
64        self.course_id.ok_or_else(|| {
65            ModelError::new(
66                ModelErrorType::Generic,
67                "Submission is not related to a course.".to_string(),
68                None,
69            )
70        })
71    }
72}
73
74#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
75#[cfg_attr(feature = "ts_rs", derive(TS))]
76pub struct ExerciseAnswersInCourseRequiringAttentionCount {
77    pub id: Uuid,
78    pub name: String,
79    pub page_id: Uuid,
80    pub chapter_id: Option<Uuid>,
81    pub order_number: i32,
82    pub count: Option<i32>,
83}
84
85#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
86#[cfg_attr(feature = "ts_rs", derive(TS))]
87pub struct ExerciseSlideSubmissionCount {
88    pub date: Option<NaiveDate>,
89    pub count: Option<i32>,
90}
91
92#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
93#[cfg_attr(feature = "ts_rs", derive(TS))]
94pub struct ExerciseSlideSubmissionCountByExercise {
95    pub exercise_id: Uuid,
96    pub count: Option<i32>,
97    pub exercise_name: String,
98}
99
100#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
101#[cfg_attr(feature = "ts_rs", derive(TS))]
102pub struct ExerciseSlideSubmissionCountByWeekAndHour {
103    pub isodow: Option<i32>,
104    pub hour: Option<i32>,
105    pub count: Option<i32>,
106}
107
108#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
109#[cfg_attr(feature = "ts_rs", derive(TS))]
110pub struct ExerciseSlideSubmissionInfo {
111    pub tasks: Vec<CourseMaterialExerciseTask>,
112    pub exercise: Exercise,
113    pub exercise_slide_submission: ExerciseSlideSubmission,
114    pub user_exercise_state: Option<UserExerciseState>,
115}
116
117#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
118#[cfg_attr(feature = "ts_rs", derive(TS))]
119pub struct ExerciseSlideSubmissionAndUserExerciseState {
120    pub exercise: Exercise,
121    pub exercise_slide_submission: ExerciseSlideSubmission,
122    pub user_exercise_state: UserExerciseState,
123    pub teacher_grading_decision: Option<TeacherGradingDecision>,
124    pub user_exam_enrollment: ExamEnrollment,
125}
126
127#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
128#[cfg_attr(feature = "ts_rs", derive(TS))]
129pub struct ExerciseSlideSubmissionAndUserExerciseStateList {
130    pub data: Vec<ExerciseSlideSubmissionAndUserExerciseState>,
131    pub total_pages: u32,
132}
133
134pub async fn insert_exercise_slide_submission(
135    conn: &mut PgConnection,
136    exercise_slide_submission: NewExerciseSlideSubmission,
137) -> ModelResult<ExerciseSlideSubmission> {
138    let res = sqlx::query_as!(
139        ExerciseSlideSubmission,
140        r#"
141INSERT INTO exercise_slide_submissions (
142    exercise_slide_id,
143    course_id,
144    exam_id,
145    exercise_id,
146    user_id,
147    user_points_update_strategy
148  )
149VALUES ($1, $2, $3, $4, $5, $6)
150RETURNING id,
151  created_at,
152  updated_at,
153  deleted_at,
154  exercise_slide_id,
155  course_id,
156  exam_id,
157  exercise_id,
158  user_id,
159  user_points_update_strategy AS "user_points_update_strategy: _",
160  flag_count
161        "#,
162        exercise_slide_submission.exercise_slide_id,
163        exercise_slide_submission.course_id,
164        exercise_slide_submission.exam_id,
165        exercise_slide_submission.exercise_id,
166        exercise_slide_submission.user_id,
167        exercise_slide_submission.user_points_update_strategy as UserPointsUpdateStrategy,
168    )
169    .fetch_one(conn)
170    .await?;
171    Ok(res)
172}
173
174pub async fn insert_exercise_slide_submission_with_id(
175    conn: &mut PgConnection,
176    id: Uuid,
177    exercise_slide_submission: &NewExerciseSlideSubmission,
178) -> ModelResult<ExerciseSlideSubmission> {
179    let res = sqlx::query_as!(
180        ExerciseSlideSubmission,
181        r#"
182INSERT INTO exercise_slide_submissions (
183    id,
184    exercise_slide_id,
185    course_id,
186    exam_id,
187    exercise_id,
188    user_id,
189    user_points_update_strategy
190  )
191VALUES ($1, $2, $3, $4, $5, $6, $7)
192RETURNING id,
193  created_at,
194  updated_at,
195  deleted_at,
196  exercise_slide_id,
197  course_id,
198  exam_id,
199  exercise_id,
200  user_id,
201  user_points_update_strategy AS "user_points_update_strategy: _",
202  flag_count
203        "#,
204        id,
205        exercise_slide_submission.exercise_slide_id,
206        exercise_slide_submission.course_id,
207        exercise_slide_submission.exam_id,
208        exercise_slide_submission.exercise_id,
209        exercise_slide_submission.user_id,
210        exercise_slide_submission.user_points_update_strategy as UserPointsUpdateStrategy,
211    )
212    .fetch_one(conn)
213    .await?;
214    Ok(res)
215}
216
217pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<ExerciseSlideSubmission> {
218    let exercise_slide_submission = sqlx::query_as!(
219        ExerciseSlideSubmission,
220        r#"
221SELECT id,
222created_at,
223updated_at,
224deleted_at,
225exercise_slide_id,
226course_id,
227exam_id,
228exercise_id,
229user_id,
230user_points_update_strategy AS "user_points_update_strategy: _",
231flag_count
232FROM exercise_slide_submissions
233WHERE id = $1
234  AND deleted_at IS NULL;
235        "#,
236        id
237    )
238    .fetch_one(conn)
239    .await?;
240    Ok(exercise_slide_submission)
241}
242
243/// Returns a map of exercise_slide_submission id -> user_id for the given submission ids.
244pub async fn get_user_ids_by_submission_ids(
245    conn: &mut PgConnection,
246    submission_ids: &[Uuid],
247) -> ModelResult<HashMap<Uuid, Uuid>> {
248    if submission_ids.is_empty() {
249        return Ok(HashMap::new());
250    }
251    let rows = sqlx::query!(
252        r#"
253SELECT id,
254  user_id
255FROM exercise_slide_submissions
256WHERE id = ANY($1)
257  AND deleted_at IS NULL
258        "#,
259        submission_ids
260    )
261    .fetch_all(conn)
262    .await?;
263    Ok(rows.into_iter().map(|r| (r.id, r.user_id)).collect())
264}
265
266/// Attempts to find a single random `ExerciseSlideSubmission` that is not related to the provided user.
267///
268/// This function is mostly provided for very specific peer review purposes. Used only as a last resort if no other candidates are found to be peer reviewed.
269pub async fn try_to_get_random_filtered_by_user_and_submissions(
270    conn: &mut PgConnection,
271    exercise_id: Uuid,
272    excluded_user_id: Uuid,
273    excluded_ids: &[Uuid],
274) -> ModelResult<Option<ExerciseSlideSubmission>> {
275    let mut res = sqlx::query_as!(
276        ExerciseSlideSubmission,
277        r#"
278SELECT DISTINCT ON (user_id)
279  ess.id,
280  ess.created_at,
281  ess.updated_at,
282  ess.deleted_at,
283  ess.exercise_slide_id,
284  ess.course_id,
285  ess.exam_id,
286  ess.exercise_id,
287  ess.user_id,
288  ess.user_points_update_strategy AS "user_points_update_strategy: _",
289  ess.flag_count
290FROM exercise_slide_submissions AS ess
291JOIN courses AS c
292  ON ess.course_id = c.id
293WHERE ess.exercise_id = $1
294  AND ess.id <> ALL($2)
295  AND ess.user_id <> $3
296  AND ess.deleted_at IS NULL
297  AND ess.flag_count < c.flagged_answers_threshold
298ORDER BY ess.user_id, ess.created_at DESC
299        "#,
300        exercise_id,
301        excluded_ids,
302        excluded_user_id,
303    )
304    .fetch_all(conn)
305    .await?;
306    // shuffle the res vec
307    let mut rng = rand::rng();
308    res.shuffle(&mut rng);
309    Ok(res.into_iter().next())
310}
311
312pub async fn get_by_exercise_id(
313    conn: &mut PgConnection,
314    exercise_id: Uuid,
315    pagination: Pagination,
316) -> ModelResult<Vec<ExerciseSlideSubmission>> {
317    let submissions = sqlx::query_as!(
318        ExerciseSlideSubmission,
319        r#"
320SELECT id,
321  created_at,
322  updated_at,
323  deleted_at,
324  exercise_slide_id,
325  course_id,
326  exam_id,
327  exercise_id,
328  user_id,
329  user_points_update_strategy AS "user_points_update_strategy: _",
330  flag_count
331FROM exercise_slide_submissions
332WHERE exercise_id = $1
333  AND deleted_at IS NULL
334LIMIT $2 OFFSET $3;
335        "#,
336        exercise_id,
337        pagination.limit(),
338        pagination.offset(),
339    )
340    .fetch_all(conn)
341    .await?;
342    Ok(submissions)
343}
344
345pub async fn get_users_all_submissions_for_course_or_exam(
346    conn: &mut PgConnection,
347    user_id: Uuid,
348    course_or_exam_id: CourseOrExamId,
349) -> ModelResult<Vec<ExerciseSlideSubmission>> {
350    let (course_id, exam_id) = course_or_exam_id.to_course_and_exam_ids();
351    let submissions = sqlx::query_as!(
352        ExerciseSlideSubmission,
353        r#"
354SELECT id,
355  created_at,
356  updated_at,
357  deleted_at,
358  exercise_slide_id,
359  course_id,
360  exam_id,
361  exercise_id,
362  user_id,
363  user_points_update_strategy AS "user_points_update_strategy: _",
364  flag_count
365FROM exercise_slide_submissions
366WHERE user_id = $1
367  AND (course_id = $2 OR exam_id = $3)
368  AND deleted_at IS NULL
369        "#,
370        user_id,
371        course_id,
372        exam_id,
373    )
374    .fetch_all(conn)
375    .await?;
376    Ok(submissions)
377}
378
379pub async fn get_users_submissions_for_exercise(
380    conn: &mut PgConnection,
381    user_id: Uuid,
382    exercise_id: Uuid,
383) -> ModelResult<Vec<ExerciseSlideSubmission>> {
384    let submissions = sqlx::query_as!(
385        ExerciseSlideSubmission,
386        r#"
387SELECT id,
388  created_at,
389  updated_at,
390  deleted_at,
391  exercise_slide_id,
392  course_id,
393  exam_id,
394  exercise_id,
395  user_id,
396  user_points_update_strategy AS "user_points_update_strategy: _",
397  flag_count
398FROM exercise_slide_submissions
399WHERE user_id = $1
400  AND exercise_id = $2
401  AND deleted_at IS NULL
402ORDER BY created_at DESC
403        "#,
404        user_id,
405        exercise_id,
406    )
407    .fetch_all(conn)
408    .await?;
409    Ok(submissions)
410}
411
412pub async fn get_users_latest_exercise_slide_submission(
413    conn: &mut PgConnection,
414    exercise_slide_id: Uuid,
415    user_id: Uuid,
416) -> ModelResult<ExerciseSlideSubmission> {
417    let res = sqlx::query_as!(
418        ExerciseSlideSubmission,
419        r#"
420SELECT id,
421  created_at,
422  updated_at,
423  deleted_at,
424  exercise_slide_id,
425  course_id,
426  exam_id,
427  exercise_id,
428  user_id,
429  user_points_update_strategy AS "user_points_update_strategy: _",
430  flag_count
431FROM exercise_slide_submissions
432WHERE exercise_slide_id = $1
433  AND user_id = $2
434  AND deleted_at IS NULL
435ORDER BY created_at DESC
436LIMIT 1
437        "#,
438        exercise_slide_id,
439        user_id
440    )
441    .fetch_one(conn)
442    .await?;
443    Ok(res)
444}
445
446pub async fn try_to_get_users_latest_exercise_slide_submission(
447    conn: &mut PgConnection,
448    exercise_slide_id: Uuid,
449    user_id: Uuid,
450) -> ModelResult<Option<ExerciseSlideSubmission>> {
451    get_users_latest_exercise_slide_submission(conn, exercise_slide_id, user_id)
452        .await
453        .optional()
454}
455
456pub async fn get_course_and_exam_id(
457    conn: &mut PgConnection,
458    id: Uuid,
459) -> ModelResult<CourseOrExamId> {
460    let res = sqlx::query!(
461        "
462SELECT course_id,
463  exam_id
464FROM exercise_slide_submissions
465WHERE id = $1
466  AND deleted_at IS NULL
467        ",
468        id
469    )
470    .fetch_one(conn)
471    .await?;
472    CourseOrExamId::from_course_and_exam_ids(res.course_id, res.exam_id)
473}
474
475pub async fn exercise_slide_submission_count(
476    conn: &mut PgConnection,
477    exercise_id: Uuid,
478) -> ModelResult<u32> {
479    let count = sqlx::query!(
480        "
481SELECT COUNT(*) as count
482FROM exercise_slide_submissions
483WHERE exercise_id = $1
484AND deleted_at IS NULL
485",
486        exercise_id,
487    )
488    .fetch_one(conn)
489    .await?;
490    Ok(count.count.unwrap_or(0).try_into()?)
491}
492
493pub async fn exercise_slide_submissions(
494    conn: &mut PgConnection,
495    exercise_id: Uuid,
496    pagination: Pagination,
497) -> ModelResult<Vec<ExerciseSlideSubmission>> {
498    let submissions = sqlx::query_as!(
499        ExerciseSlideSubmission,
500        r#"
501SELECT id,
502  created_at,
503  updated_at,
504  deleted_at,
505  exercise_slide_id,
506  course_id,
507  exam_id,
508  exercise_id,
509  user_id,
510  user_points_update_strategy AS "user_points_update_strategy: _",
511  flag_count
512FROM exercise_slide_submissions
513WHERE exercise_id = $1
514  AND deleted_at IS NULL
515ORDER BY created_at DESC
516LIMIT $2 OFFSET $3
517        "#,
518        exercise_id,
519        pagination.limit(),
520        pagination.offset(),
521    )
522    .fetch_all(conn)
523    .await?;
524    Ok(submissions)
525}
526
527pub async fn exercise_slide_submission_count_with_exam_id(
528    conn: &mut PgConnection,
529    exam_id: Uuid,
530) -> ModelResult<u32> {
531    let count = sqlx::query!(
532        "
533SELECT COUNT(*) as count
534FROM exercise_slide_submissions
535WHERE exam_id = $1
536AND deleted_at IS NULL
537",
538        exam_id,
539    )
540    .fetch_one(conn)
541    .await?;
542    Ok(count.count.unwrap_or(0).try_into()?)
543}
544
545pub async fn exercise_slide_submission_count_with_exercise_id(
546    conn: &mut PgConnection,
547    exercise_id: Uuid,
548) -> ModelResult<u32> {
549    let count = sqlx::query!(
550        "
551SELECT COUNT(*) as count
552FROM exercise_slide_submissions
553WHERE exercise_id = $1
554AND deleted_at IS NULL
555",
556        exercise_id,
557    )
558    .fetch_one(conn)
559    .await?;
560    Ok(count.count.unwrap_or(0).try_into()?)
561}
562
563pub async fn get_latest_exercise_slide_submissions_and_user_exercise_state_list_with_exercise_id(
564    conn: &mut PgConnection,
565    exercise_id: Uuid,
566    pagination: Pagination,
567) -> ModelResult<Vec<ExerciseSlideSubmissionAndUserExerciseState>> {
568    let submissions = sqlx::query_as!(
569        ExerciseSlideSubmission,
570        r#"
571    SELECT DISTINCT ON (user_id)
572        id,
573        created_at,
574        updated_at,
575        deleted_at,
576        exercise_slide_id,
577        course_id,
578        exam_id,
579        exercise_id,
580        user_id,
581        user_points_update_strategy AS "user_points_update_strategy: _",
582  flag_count
583FROM exercise_slide_submissions
584WHERE exercise_id = $1
585      AND deleted_at IS NULL
586ORDER BY user_id, created_at DESC
587LIMIT $2 OFFSET $3
588        "#,
589        exercise_id,
590        pagination.limit(),
591        pagination.offset(),
592    )
593    .fetch_all(&mut *conn)
594    .await?;
595
596    let user_ids = submissions
597        .iter()
598        .map(|sub| sub.user_id)
599        .collect::<Vec<_>>();
600
601    let exercise = exercises::get_by_id(conn, exercise_id).await?;
602    let exam_id = exercise.exam_id;
603    let course_id = exercise.course_id;
604
605    let user_exercise_states_list =
606        user_exercise_states::get_or_create_user_exercise_state_for_users(
607            conn,
608            &user_ids,
609            exercise_id,
610            course_id,
611            exam_id,
612        )
613        .await?;
614
615    let mut user_exercise_state_id_list: Vec<Uuid> = Vec::new();
616
617    for (_key, value) in user_exercise_states_list.clone().into_iter() {
618        user_exercise_state_id_list.push(value.id);
619    }
620
621    let exercise = exercises::get_by_id(conn, exercise_id).await?;
622    let exam_id = exercise
623        .exam_id
624        .ok_or_else(|| ModelError::new(ModelErrorType::Generic, "No exam id found", None))?;
625
626    let teacher_grading_decisions_list = teacher_grading_decisions::try_to_get_latest_grading_decision_by_user_exercise_state_id_for_users(conn, &user_exercise_state_id_list).await?;
627
628    let user_exam_enrollments_list =
629        exams::get_exam_enrollments_for_users(conn, exam_id, &user_ids).await?;
630
631    let mut list: Vec<ExerciseSlideSubmissionAndUserExerciseState> = Vec::new();
632    for sub in submissions {
633        let user_exercise_state = user_exercise_states_list
634            .get(&sub.user_id)
635            .ok_or_else(|| ModelError::new(ModelErrorType::Generic, "No user found", None))?;
636
637        let teacher_grading_decision = teacher_grading_decisions_list.get(&user_exercise_state.id);
638        let user_exam_enrollment =
639            user_exam_enrollments_list
640                .get(&sub.user_id)
641                .ok_or_else(|| {
642                    ModelError::new(
643                        ModelErrorType::Generic,
644                        "No users exam_enrollment found",
645                        None,
646                    )
647                })?;
648
649        //Add submissions to the list only if the students exam time has ended
650        if user_exam_enrollment.ended_at.is_some() {
651            let data = ExerciseSlideSubmissionAndUserExerciseState {
652                exercise: exercise.clone(),
653                exercise_slide_submission: sub,
654                user_exercise_state: user_exercise_state.clone(),
655                teacher_grading_decision: teacher_grading_decision.cloned(),
656                user_exam_enrollment: user_exam_enrollment.clone(),
657            };
658            list.push(data);
659        }
660    }
661
662    Ok(list)
663}
664
665pub async fn get_course_daily_slide_submission_counts(
666    conn: &mut PgConnection,
667    course: &Course,
668) -> ModelResult<Vec<ExerciseSlideSubmissionCount>> {
669    let res = sqlx::query_as!(
670        ExerciseSlideSubmissionCount,
671        r#"
672SELECT DATE(created_at) date, count(*)::integer
673FROM exercise_slide_submissions
674WHERE course_id = $1
675AND deleted_at IS NULL
676GROUP BY date
677ORDER BY date;
678          "#,
679        course.id
680    )
681    .fetch_all(conn)
682    .await?;
683    Ok(res)
684}
685
686pub async fn get_course_daily_user_counts_with_submissions(
687    conn: &mut PgConnection,
688    course: &Course,
689) -> ModelResult<Vec<ExerciseSlideSubmissionCount>> {
690    let res = sqlx::query_as!(
691        ExerciseSlideSubmissionCount,
692        r#"
693SELECT DATE(created_at) date, count(DISTINCT user_id)::integer
694FROM exercise_slide_submissions
695WHERE course_id = $1
696AND deleted_at IS NULL
697GROUP BY date
698ORDER BY date;
699          "#,
700        course.id
701    )
702    .fetch_all(conn)
703    .await?;
704    Ok(res)
705}
706
707pub async fn answer_requiring_attention_count(
708    conn: &mut PgConnection,
709    exercise_id: Uuid,
710) -> ModelResult<u32> {
711    let count = sqlx::query!(
712        r#"
713        SELECT
714        COUNT(DISTINCT us_state.user_id) as count
715    FROM user_exercise_states AS us_state
716    JOIN exercise_task_submissions AS t_submission
717        ON us_state.selected_exercise_slide_id =
718            t_submission.exercise_slide_id
719    JOIN exercise_slide_submissions AS s_submission
720            ON t_submission.exercise_slide_submission_id =
721                s_submission.id
722    WHERE us_state.selected_exercise_slide_id =
723            t_submission.exercise_slide_id
724    AND us_state.user_id = s_submission.user_id
725    AND us_state.exercise_id = $1
726    AND us_state.reviewing_stage = 'waiting_for_manual_grading'
727    AND us_state.deleted_at IS NULL
728    AND s_submission.deleted_at IS NULL
729    AND t_submission.deleted_at IS NULL"#,
730        exercise_id,
731    )
732    .fetch_one(conn)
733    .await?;
734    Ok(count.count.unwrap_or(0).try_into()?)
735}
736
737pub async fn get_count_of_answers_requiring_attention_in_exercise_by_course_id(
738    conn: &mut PgConnection,
739    course_id: Uuid,
740) -> ModelResult<Vec<ExerciseAnswersInCourseRequiringAttentionCount>> {
741    let count_list = sqlx::query_as!(
742        ExerciseAnswersInCourseRequiringAttentionCount,
743        r#"
744SELECT exercises.id,
745  (
746    SELECT COUNT(DISTINCT us_state.user_id)::integer AS COUNT
747    FROM exercises AS exercises2
748      LEFT JOIN user_exercise_states AS us_state ON us_state.exercise_id = exercises2.id
749      LEFT JOIN exercise_slide_submissions AS s_submission ON us_state.selected_exercise_slide_id = s_submission.exercise_slide_id
750      LEFT JOIN exercise_task_submissions AS t_submission ON s_submission.id = t_submission.exercise_slide_submission_id
751    WHERE us_state.selected_exercise_slide_id = t_submission.exercise_slide_id
752      AND us_state.user_id = s_submission.user_id
753      AND us_state.reviewing_stage = 'waiting_for_manual_grading'
754      AND us_state.deleted_at IS NULL
755      AND s_submission.deleted_at IS NULL
756      AND t_submission.deleted_at IS NULL
757      AND exercises2.course_id = $1
758      AND exercises.id = exercises2.id
759    GROUP BY exercises2.id
760  ),
761  exercises.order_number,
762  exercises.name,
763  exercises.page_id,
764  exercises.chapter_id
765FROM exercises
766WHERE exercises.course_id = $1
767  AND exercises.deleted_at IS NULL
768GROUP BY exercises.id;
769"#,
770        course_id,
771    )
772    .fetch_all(conn)
773    .await?;
774    Ok(count_list)
775}
776
777pub async fn exercise_slide_submissions_for_answers_requiring_attention(
778    conn: &mut PgConnection,
779    exercise_id: Uuid,
780    pagination: Pagination,
781) -> ModelResult<Vec<ExerciseSlideSubmission>> {
782    let submissions = sqlx::query_as!(
783        ExerciseSlideSubmission,
784        r#"
785SELECT id,
786  created_at,
787  updated_at,
788  deleted_at,
789  exercise_slide_id,
790  course_id,
791  exam_id,
792  exercise_id,
793  user_id,
794  user_points_update_strategy AS "user_points_update_strategy: _",
795  flag_count
796FROM exercise_slide_submissions
797WHERE exercise_id = $1
798  AND deleted_at IS NULL
799LIMIT $2 OFFSET $3
800        "#,
801        exercise_id,
802        pagination.limit(),
803        pagination.offset(),
804    )
805    .fetch_all(conn)
806    .await?;
807    Ok(submissions)
808}
809
810pub async fn get_all_answers_requiring_attention(
811    conn: &mut PgConnection,
812    exercise_id: Uuid,
813    pagination: Pagination,
814) -> ModelResult<Vec<AnswerRequiringAttention>> {
815    let submissions = sqlx::query_as!(
816        AnswerRequiringAttention,
817        r#"
818        SELECT DISTINCT ON (us_state.user_id)
819        us_state.id,
820        us_state.user_id,
821        us_state.exercise_id,
822        us_state.score_given,
823        us_state.grading_progress as "grading_progress: _",
824        t_submission.data_json,
825        s_submission.created_at,
826        s_submission.updated_at,
827        s_submission.deleted_at,
828        s_submission.id AS submission_id
829    FROM user_exercise_states AS us_state
830    JOIN exercise_task_submissions AS t_submission
831        ON us_state.selected_exercise_slide_id =
832            t_submission.exercise_slide_id
833    JOIN exercise_slide_submissions AS s_submission
834            ON t_submission.exercise_slide_submission_id =
835                s_submission.id
836    WHERE us_state.selected_exercise_slide_id =
837            t_submission.exercise_slide_id
838    AND us_state.user_id = s_submission.user_id
839    AND us_state.exercise_id = $1
840    AND us_state.reviewing_stage = 'waiting_for_manual_grading'
841    AND us_state.deleted_at IS NULL
842    AND s_submission.deleted_at IS NULL
843    AND t_submission.deleted_at IS NULL
844    ORDER BY us_state.user_id, s_submission.created_at DESC
845    LIMIT $2 OFFSET $3;"#,
846        exercise_id,
847        pagination.limit(),
848        pagination.offset(),
849    )
850    .fetch_all(conn)
851    .await?;
852    Ok(submissions)
853}
854
855pub async fn get_course_exercise_slide_submission_counts_by_weekday_and_hour(
856    conn: &mut PgConnection,
857    course: &Course,
858) -> ModelResult<Vec<ExerciseSlideSubmissionCountByWeekAndHour>> {
859    let res = sqlx::query_as!(
860        ExerciseSlideSubmissionCountByWeekAndHour,
861        r#"
862SELECT date_part('isodow', created_at)::integer isodow,
863  date_part('hour', created_at)::integer "hour",
864  count(*)::integer
865FROM exercise_slide_submissions
866WHERE course_id = $1
867AND deleted_at IS NULL
868GROUP BY isodow,
869  "hour"
870ORDER BY isodow,
871  hour;
872          "#,
873        course.id
874    )
875    .fetch_all(conn)
876    .await?;
877    Ok(res)
878}
879
880pub async fn get_course_exercise_slide_submission_counts_by_exercise(
881    conn: &mut PgConnection,
882    course: &Course,
883) -> ModelResult<Vec<ExerciseSlideSubmissionCountByExercise>> {
884    let res = sqlx::query_as!(
885        ExerciseSlideSubmissionCountByExercise,
886        r#"
887SELECT counts.*, exercises.name exercise_name
888    FROM (
889        SELECT exercise_id, count(*)::integer count
890        FROM exercise_slide_submissions
891        WHERE course_id = $1
892        AND deleted_at IS NULL
893        GROUP BY exercise_id
894    ) counts
895    JOIN exercises ON (counts.exercise_id = exercises.id);
896          "#,
897        course.id
898    )
899    .fetch_all(conn)
900    .await?;
901    Ok(res)
902}
903
904pub async fn get_exercise_slide_submission_counts_for_exercise_user(
905    conn: &mut PgConnection,
906    exercise_id: Uuid,
907    course_id_or_exam_id: CourseOrExamId,
908    user_id: Uuid,
909) -> ModelResult<HashMap<Uuid, i64>> {
910    let ci_id_or_e_id = match course_id_or_exam_id {
911        CourseOrExamId::Course(id) => id,
912        CourseOrExamId::Exam(id) => id,
913    };
914    let res = sqlx::query!(
915        r#"
916SELECT exercise_slide_id,
917  COUNT(*) as count
918FROM exercise_slide_submissions
919WHERE exercise_id = $1
920  AND (course_id = $2 OR exam_id = $2)
921  AND user_id = $3
922  AND deleted_at IS NULL
923GROUP BY exercise_slide_id;
924    "#,
925        exercise_id,
926        ci_id_or_e_id,
927        user_id
928    )
929    .fetch_all(conn)
930    .await?
931    .iter()
932    .map(|row| (row.exercise_slide_id, row.count.unwrap_or(0)))
933    .collect::<HashMap<Uuid, i64>>();
934
935    Ok(res)
936}
937
938pub async fn get_exercise_slide_submission_info(
939    conn: &mut PgConnection,
940    exercise_slide_submission_id: Uuid,
941    user_id: Uuid,
942    fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
943    include_deleted_tasks: bool,
944) -> ModelResult<ExerciseSlideSubmissionInfo> {
945    let exercise_slide_submission = get_by_id(&mut *conn, exercise_slide_submission_id).await?;
946    let exercise =
947        crate::exercises::get_by_id(&mut *conn, exercise_slide_submission.exercise_id).await?;
948    let tasks = crate::exercise_task_submissions::get_exercise_task_submission_info_by_exercise_slide_submission_id(&mut *conn, exercise_slide_submission_id, user_id, fetch_service_info, include_deleted_tasks).await?;
949    let user_exercise_state = crate::user_exercise_states::get_user_exercise_state_if_exists(
950        &mut *conn,
951        user_id,
952        exercise_slide_submission.exercise_id,
953        CourseOrExamId::from_course_and_exam_ids(
954            exercise_slide_submission.course_id,
955            exercise_slide_submission.exam_id,
956        )?,
957    )
958    .await?;
959
960    Ok(ExerciseSlideSubmissionInfo {
961        exercise,
962        tasks,
963        exercise_slide_submission,
964        user_exercise_state,
965    })
966}
967
968pub async fn delete_exercise_submissions_with_exam_id_and_user_id(
969    conn: &mut PgConnection,
970    exam_id: Uuid,
971    user_id: Uuid,
972) -> ModelResult<()> {
973    sqlx::query!(
974        "
975UPDATE exercise_slide_submissions
976SET deleted_at = now()
977WHERE exam_id = $1 AND user_id = $2
978AND deleted_at IS NULL
979    ",
980        exam_id,
981        user_id,
982    )
983    .execute(&mut *conn)
984    .await?;
985    Ok(())
986}