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