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
243pub 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
266pub 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 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 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}