1use derive_more::Display;
2use futures::future::BoxFuture;
3use itertools::Itertools;
4use tracing::info;
5use url::Url;
6use utoipa::ToSchema;
7
8use crate::{
9 exams, exercise_reset_logs,
10 exercise_service_info::ExerciseServiceInfoApi,
11 exercise_slide_submissions::{
12 ExerciseSlideSubmission, get_exercise_slide_submission_counts_for_exercise_user,
13 },
14 exercise_slides::{self, CourseMaterialExerciseSlide},
15 exercise_tasks,
16 peer_or_self_review_configs::CourseMaterialPeerOrSelfReviewConfig,
17 peer_or_self_review_question_submissions::PeerOrSelfReviewQuestionSubmission,
18 peer_or_self_review_questions::PeerOrSelfReviewQuestion,
19 peer_or_self_review_submissions::PeerOrSelfReviewSubmissionWithSubmissionOwner,
20 peer_review_queue_entries::PeerReviewQueueEntry,
21 prelude::*,
22 teacher_grading_decisions::{TeacherDecisionType, TeacherGradingDecision},
23 user_chapter_locking_statuses,
24 user_course_exercise_service_variables::UserCourseExerciseServiceVariable,
25 user_course_settings,
26 user_exercise_states::{self, ReviewingStage, UserExerciseState},
27};
28use std::collections::{HashMap, HashSet};
29
30#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
31
32pub struct Exercise {
33 pub id: Uuid,
34 pub created_at: DateTime<Utc>,
35 pub updated_at: DateTime<Utc>,
36 pub name: String,
37 pub course_id: Option<Uuid>,
38 pub exam_id: Option<Uuid>,
39 pub page_id: Uuid,
40 pub chapter_id: Option<Uuid>,
41 pub deadline: Option<DateTime<Utc>>,
42 pub deleted_at: Option<DateTime<Utc>>,
43 pub score_maximum: i32,
44 pub order_number: i32,
45 pub copied_from: Option<Uuid>,
46 pub max_tries_per_slide: Option<i32>,
47 pub limit_number_of_tries: bool,
48 pub needs_peer_review: bool,
49 pub needs_self_review: bool,
50 pub use_course_default_peer_or_self_review_config: bool,
51 pub exercise_language_group_id: Option<Uuid>,
52 pub teacher_reviews_answer_after_locking: bool,
53}
54
55impl Exercise {
56 pub fn get_course_id(&self) -> ModelResult<Uuid> {
57 self.course_id.ok_or_else(|| {
58 ModelError::new(
59 ModelErrorType::Generic,
60 "Exercise is not related to a course.".to_string(),
61 None,
62 )
63 })
64 }
65}
66
67#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
68
69pub struct ExerciseGradingStatus {
70 pub exercise_id: Uuid,
71 pub exercise_name: String,
72 pub score_maximum: i32,
73 pub score_given: Option<f32>,
74 pub teacher_decision: Option<TeacherDecisionType>,
75 pub submission_id: Uuid,
76 pub updated_at: DateTime<Utc>,
77}
78
79#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
80
81pub struct ExerciseStatusSummaryForUser {
82 pub exercise: Exercise,
83 pub user_exercise_state: Option<UserExerciseState>,
84 pub exercise_slide_submissions: Vec<ExerciseSlideSubmission>,
85 pub given_peer_or_self_review_submissions: Vec<PeerOrSelfReviewSubmissionWithSubmissionOwner>,
86 pub given_peer_or_self_review_question_submissions: Vec<PeerOrSelfReviewQuestionSubmission>,
87 pub received_peer_or_self_review_submissions:
88 Vec<PeerOrSelfReviewSubmissionWithSubmissionOwner>,
89 pub received_peer_or_self_review_question_submissions: Vec<PeerOrSelfReviewQuestionSubmission>,
90 pub peer_review_queue_entry: Option<PeerReviewQueueEntry>,
91 pub teacher_grading_decision: Option<TeacherGradingDecision>,
92 pub peer_or_self_review_questions: Vec<PeerOrSelfReviewQuestion>,
93}
94
95#[derive(Debug, Serialize, Deserialize, ToSchema)]
96
97pub struct CourseMaterialExercise {
98 pub exercise: Exercise,
99 pub can_post_submission: bool,
100 pub current_exercise_slide: CourseMaterialExerciseSlide,
101 pub exercise_status: Option<ExerciseStatus>,
103 pub exercise_slide_submission_counts: HashMap<Uuid, i64>,
104 pub peer_or_self_review_config: Option<CourseMaterialPeerOrSelfReviewConfig>,
105 pub previous_exercise_slide_submission: Option<ExerciseSlideSubmission>,
106 pub user_course_instance_exercise_service_variables: Vec<UserCourseExerciseServiceVariable>,
107 pub should_show_reset_message: Option<String>,
108}
109
110impl CourseMaterialExercise {
111 pub fn clear_grading_information(&mut self) {
112 self.exercise_status = None;
113 self.current_exercise_slide
114 .exercise_tasks
115 .iter_mut()
116 .for_each(|task| {
117 task.model_solution_spec = None;
118 task.previous_submission_grading = None;
119 });
120 }
121
122 pub fn clear_model_solution_specs(&mut self) {
123 self.current_exercise_slide
124 .exercise_tasks
125 .iter_mut()
126 .for_each(|task| {
127 task.model_solution_spec = None;
128 });
129 }
130}
131
132#[derive(
138 Debug,
139 Serialize,
140 Deserialize,
141 PartialEq,
142 Eq,
143 Clone,
144 Copy,
145 Default,
146 Display,
147 sqlx::Type,
148 ToSchema,
149)]
150#[sqlx(type_name = "activity_progress", rename_all = "kebab-case")]
151pub enum ActivityProgress {
152 #[default]
154 Initialized,
155 Started,
157 InProgress,
159 Submitted,
161 Completed,
163}
164
165#[derive(
172 Clone,
173 Copy,
174 Debug,
175 Deserialize,
176 Eq,
177 Serialize,
178 Ord,
179 PartialEq,
180 PartialOrd,
181 Display,
182 sqlx::Type,
183 ToSchema,
184)]
185#[sqlx(type_name = "grading_progress", rename_all = "kebab-case")]
186pub enum GradingProgress {
187 Failed,
189 NotReady,
191 PendingManual,
193 Pending,
195 FullyGraded,
197}
198
199impl GradingProgress {
200 pub fn is_complete(self) -> bool {
201 self == Self::FullyGraded || self == Self::Failed
202 }
203}
204
205#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
206
207pub struct ExerciseStatus {
208 pub score_given: Option<f32>,
210 pub activity_progress: ActivityProgress,
211 pub grading_progress: GradingProgress,
212 pub reviewing_stage: ReviewingStage,
213}
214
215#[allow(clippy::too_many_arguments)]
216pub async fn insert(
217 conn: &mut PgConnection,
218 pkey_policy: PKeyPolicy<Uuid>,
219 course_id: Uuid,
220 name: &str,
221 page_id: Uuid,
222 chapter_id: Uuid,
223 order_number: i32,
224) -> ModelResult<Uuid> {
225 let course = crate::courses::get_course(conn, course_id).await?;
226 let exercise_language_group_id = crate::exercise_language_groups::insert(
227 conn,
228 PKeyPolicy::Generate,
229 course.course_language_group_id,
230 )
231 .await?;
232
233 let res = sqlx::query!(
234 "
235INSERT INTO exercises (
236 id,
237 course_id,
238 name,
239 page_id,
240 chapter_id,
241 order_number,
242 exercise_language_group_id
243 )
244VALUES ($1, $2, $3, $4, $5, $6, $7)
245RETURNING id
246 ",
247 pkey_policy.into_uuid(),
248 course_id,
249 name,
250 page_id,
251 chapter_id,
252 order_number,
253 exercise_language_group_id,
254 )
255 .fetch_one(conn)
256 .await?;
257 Ok(res.id)
258}
259
260pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<Exercise> {
261 let exercise = sqlx::query_as!(
262 Exercise,
263 "
264SELECT *
265FROM exercises
266WHERE id = $1
267",
268 id
269 )
270 .fetch_one(conn)
271 .await?;
272 Ok(exercise)
273}
274
275pub async fn get_exercise_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<Exercise> {
276 let exercise = sqlx::query_as!(Exercise, "SELECT * FROM exercises WHERE id = $1;", id)
277 .fetch_one(conn)
278 .await?;
279 Ok(exercise)
280}
281
282pub async fn get_exercises_by_course_id(
283 conn: &mut PgConnection,
284 course_id: Uuid,
285) -> ModelResult<Vec<Exercise>> {
286 let exercises = sqlx::query_as!(
287 Exercise,
288 r#"
289SELECT *
290FROM exercises
291WHERE course_id = $1
292 AND deleted_at IS NULL
293"#,
294 course_id
295 )
296 .fetch_all(&mut *conn)
297 .await?;
298 Ok(exercises)
299}
300
301pub async fn get_exercise_submissions_and_status_by_course_instance_id(
302 conn: &mut PgConnection,
303 course_instance_id: Uuid,
304 user_id: Uuid,
305) -> ModelResult<Vec<ExerciseGradingStatus>> {
306 let exercises = sqlx::query_as!(
307 ExerciseGradingStatus,
308 r#"
309 SELECT
310 e.id as exercise_id,
311 e.name as exercise_name,
312 e.score_maximum,
313 ues.score_given,
314 tgd.teacher_decision as "teacher_decision: _",
315 ess.id as submission_id,
316 ess.updated_at
317 FROM exercises e
318 LEFT JOIN user_exercise_states ues on e.id = ues.exercise_id
319 LEFT JOIN teacher_grading_decisions tgd on tgd.user_exercise_state_id = ues.id
320 LEFT JOIN exercise_slide_submissions ess on e.id = ess.exercise_id
321 WHERE e.course_id = (
322 SELECT course_id
323 FROM course_instances
324 WHERE id = $1
325 )
326 AND e.deleted_at IS NULL
327 AND ess.user_id = $2
328 AND ues.user_id = $2
329 ORDER BY e.order_number ASC;
330"#,
331 course_instance_id,
332 user_id
333 )
334 .fetch_all(conn)
335 .await?;
336 Ok(exercises)
337}
338
339pub async fn get_exercises_by_chapter_id(
340 conn: &mut PgConnection,
341 chapter_id: Uuid,
342) -> ModelResult<Vec<Exercise>> {
343 let exercises = sqlx::query_as!(
344 Exercise,
345 r#"
346SELECT *
347FROM exercises
348WHERE chapter_id = $1
349 AND deleted_at IS NULL
350"#,
351 chapter_id
352 )
353 .fetch_all(&mut *conn)
354 .await?;
355 Ok(exercises)
356}
357
358pub async fn get_exercises_by_chapter_ids(
359 conn: &mut PgConnection,
360 chapter_ids: &[Uuid],
361) -> ModelResult<Vec<Exercise>> {
362 if chapter_ids.is_empty() {
363 return Ok(Vec::new());
364 }
365 let exercises = sqlx::query_as!(
366 Exercise,
367 r#"
368SELECT *
369FROM exercises
370WHERE chapter_id = ANY($1)
371 AND deleted_at IS NULL
372"#,
373 chapter_ids as &[Uuid]
374 )
375 .fetch_all(&mut *conn)
376 .await?;
377 Ok(exercises)
378}
379
380pub async fn get_exercises_by_page_id(
381 conn: &mut PgConnection,
382 page_id: Uuid,
383) -> ModelResult<Vec<Exercise>> {
384 let exercises = sqlx::query_as!(
385 Exercise,
386 r#"
387SELECT *
388 FROM exercises
389WHERE page_id = $1
390 AND deleted_at IS NULL;
391"#,
392 page_id,
393 )
394 .fetch_all(&mut *conn)
395 .await?;
396 Ok(exercises)
397}
398
399pub async fn get_exercises_by_exam_id(
400 conn: &mut PgConnection,
401 exam_id: Uuid,
402) -> ModelResult<Vec<Exercise>> {
403 let exercises = sqlx::query_as!(
404 Exercise,
405 r#"
406SELECT *
407FROM exercises
408WHERE exam_id = $1
409 AND deleted_at IS NULL
410"#,
411 exam_id,
412 )
413 .fetch_all(&mut *conn)
414 .await?;
415 Ok(exercises)
416}
417
418pub async fn get_course_or_exam_id(
419 conn: &mut PgConnection,
420 id: Uuid,
421) -> ModelResult<CourseOrExamId> {
422 let res = sqlx::query!(
423 "
424SELECT course_id,
425 exam_id
426FROM exercises
427WHERE id = $1
428",
429 id
430 )
431 .fetch_one(conn)
432 .await?;
433 CourseOrExamId::from_course_and_exam_ids(res.course_id, res.exam_id)
434}
435
436pub async fn get_course_material_exercise(
437 conn: &mut PgConnection,
438 user_id: Option<Uuid>,
439 exercise_id: Uuid,
440 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
441) -> ModelResult<CourseMaterialExercise> {
442 let mut exercise = get_by_id(conn, exercise_id).await?;
443 if exercise.deadline.is_none()
444 && let Some(chapter_id) = exercise.chapter_id
445 {
446 let chapter = crate::chapters::get_chapter(conn, chapter_id).await?;
447 exercise.deadline = chapter.deadline;
448 }
449 let (current_exercise_slide, instance_or_exam_id) =
450 get_or_select_exercise_slide(&mut *conn, user_id, &exercise, fetch_service_info).await?;
451 info!(
452 "Current exercise slide id: {:#?}",
453 current_exercise_slide.id
454 );
455
456 let user_exercise_state = match (user_id, instance_or_exam_id) {
457 (Some(user_id), Some(course_or_exam_id)) => {
458 user_exercise_states::get_user_exercise_state_if_exists(
459 conn,
460 user_id,
461 exercise.id,
462 course_or_exam_id,
463 )
464 .await?
465 }
466 _ => None,
467 };
468
469 let can_post_submission =
470 determine_can_post_submission(&mut *conn, user_id, &exercise, &user_exercise_state).await?;
471
472 let previous_exercise_slide_submission = match user_id {
473 Some(user_id) => {
474 crate::exercise_slide_submissions::try_to_get_users_latest_exercise_slide_submission(
475 conn,
476 current_exercise_slide.id,
477 user_id,
478 )
479 .await?
480 }
481 _ => None,
482 };
483
484 let exercise_status = user_exercise_state.map(|user_exercise_state| ExerciseStatus {
485 score_given: user_exercise_state.score_given,
486 activity_progress: user_exercise_state.activity_progress,
487 grading_progress: user_exercise_state.grading_progress,
488 reviewing_stage: user_exercise_state.reviewing_stage,
489 });
490
491 let exercise_slide_submission_counts = if let Some(user_id) = user_id {
492 if let Some(cioreid) = instance_or_exam_id {
493 get_exercise_slide_submission_counts_for_exercise_user(
494 conn,
495 exercise_id,
496 cioreid,
497 user_id,
498 )
499 .await?
500 } else {
501 HashMap::new()
502 }
503 } else {
504 HashMap::new()
505 };
506
507 let peer_or_self_review_config = if let Some(course_id) = exercise.course_id {
508 if exercise.needs_peer_review || exercise.needs_self_review {
509 let prc = crate::peer_or_self_review_configs::get_by_exercise_or_course_id(
510 conn, &exercise, course_id,
511 )
512 .await
513 .optional()?;
514 prc.map(|prc| CourseMaterialPeerOrSelfReviewConfig {
515 id: prc.id,
516 course_id: prc.course_id,
517 exercise_id: prc.exercise_id,
518 peer_reviews_to_give: prc.peer_reviews_to_give,
519 peer_reviews_to_receive: prc.peer_reviews_to_receive,
520 })
521 } else {
522 None
523 }
524 } else {
525 None
526 };
527
528 let user_course_instance_exercise_service_variables = match (user_id, instance_or_exam_id) {
529 (Some(user_id), Some(course_or_exam_id)) => {
530 Some(crate::user_course_exercise_service_variables::get_all_variables_for_user_and_course_or_exam(conn, user_id, course_or_exam_id).await?)
531 }
532 _ => None,
533 }.unwrap_or_default();
534
535 let should_show_reset_message = if let Some(user_id) = user_id {
536 crate::exercise_reset_logs::user_should_see_reset_message_for_exercise(
537 conn,
538 user_id,
539 exercise_id,
540 )
541 .await?
542 } else {
543 None
544 };
545
546 Ok(CourseMaterialExercise {
547 exercise,
548 can_post_submission,
549 current_exercise_slide,
550 exercise_status,
551 exercise_slide_submission_counts,
552 peer_or_self_review_config,
553 user_course_instance_exercise_service_variables,
554 previous_exercise_slide_submission,
555 should_show_reset_message,
556 })
557}
558
559async fn determine_can_post_submission(
560 conn: &mut PgConnection,
561 user_id: Option<Uuid>,
562 exercise: &Exercise,
563 user_exercise_state: &Option<UserExerciseState>,
564) -> Result<bool, ModelError> {
565 if let Some(user_exercise_state) = user_exercise_state {
566 if user_exercise_state.reviewing_stage != ReviewingStage::NotStarted {
568 return Ok(false);
569 }
570 }
571
572 let can_post_submission = if let Some(user_id) = user_id {
573 if let Some(exam_id) = exercise.exam_id {
574 exams::verify_exam_submission_can_be_made(conn, exam_id, user_id).await?
575 } else {
576 true
577 }
578 } else {
579 false
580 };
581 Ok(can_post_submission)
582}
583
584pub async fn get_or_select_exercise_slide(
585 conn: &mut PgConnection,
586 user_id: Option<Uuid>,
587 exercise: &Exercise,
588 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
589) -> ModelResult<(CourseMaterialExerciseSlide, Option<CourseOrExamId>)> {
590 match (user_id, exercise.course_id, exercise.exam_id) {
591 (None, ..) => {
592 let random_slide =
594 exercise_slides::get_random_exercise_slide_for_exercise(conn, exercise.id).await?;
595 let random_slide_tasks = exercise_tasks::get_course_material_exercise_tasks(
596 conn,
597 random_slide.id,
598 None,
599 fetch_service_info,
600 )
601 .await?;
602 Ok((
603 CourseMaterialExerciseSlide {
604 id: random_slide.id,
605 exercise_tasks: random_slide_tasks,
606 },
607 None,
608 ))
609 }
610 (Some(user_id), Some(course_id), None) => {
611 let user_course_settings = user_course_settings::get_user_course_settings_by_course_id(
613 conn, user_id, course_id,
614 )
615 .await?;
616 match user_course_settings {
617 Some(settings) if settings.current_course_id == course_id => {
618 let course_or_exam_id: CourseOrExamId = exercise.try_into()?;
620 let tasks =
621 exercise_tasks::get_or_select_user_exercise_slide_for_course_or_exam(
622 conn,
623 user_id,
624 exercise.id,
625 course_or_exam_id,
626 fetch_service_info,
627 )
628 .await?;
629 Ok((tasks, Some(CourseOrExamId::Course(course_id))))
630 }
631 Some(_) => {
632 let exercise_tasks =
635 exercise_tasks::get_existing_users_exercise_slide_for_course(
636 conn,
637 user_id,
638 exercise.id,
639 course_id,
640 &fetch_service_info,
641 )
642 .await?;
643 if let Some(exercise_tasks) = exercise_tasks {
644 Ok((exercise_tasks, Some(CourseOrExamId::Course(course_id))))
645 } else {
646 let random_slide = exercise_slides::get_random_exercise_slide_for_exercise(
648 conn,
649 exercise.id,
650 )
651 .await?;
652 let random_tasks = exercise_tasks::get_course_material_exercise_tasks(
653 conn,
654 random_slide.id,
655 Some(user_id),
656 &fetch_service_info,
657 )
658 .await?;
659
660 Ok((
661 CourseMaterialExerciseSlide {
662 id: random_slide.id,
663 exercise_tasks: random_tasks,
664 },
665 None,
666 ))
667 }
668 }
669 None => {
670 Err(ModelError::new(
673 ModelErrorType::PreconditionFailed,
674 "User must be enrolled to the course".to_string(),
675 None,
676 ))
677 }
678 }
679 }
680 (Some(user_id), _, Some(exam_id)) => {
681 info!("selecting exam task");
682 let tasks = exercise_tasks::get_or_select_user_exercise_slide_for_course_or_exam(
684 conn,
685 user_id,
686 exercise.id,
687 CourseOrExamId::Exam(exam_id),
688 fetch_service_info,
689 )
690 .await?;
691 info!("selecting exam task {:#?}", tasks);
692 Ok((tasks, Some(CourseOrExamId::Exam(exam_id))))
693 }
694 (Some(_), ..) => Err(ModelError::new(
695 ModelErrorType::Generic,
696 "The selected exercise is not attached to any course or exam".to_string(),
697 None,
698 )),
699 }
700}
701
702pub async fn delete_exercises_by_page_id(
703 conn: &mut PgConnection,
704 page_id: Uuid,
705) -> ModelResult<Vec<Uuid>> {
706 let deleted_ids = sqlx::query!(
707 "
708UPDATE exercises
709SET deleted_at = now()
710WHERE page_id = $1
711AND deleted_at IS NULL
712RETURNING id;
713 ",
714 page_id
715 )
716 .fetch_all(conn)
717 .await?
718 .into_iter()
719 .map(|x| x.id)
720 .collect();
721 Ok(deleted_ids)
722}
723
724pub async fn update_teacher_reviews_answer_after_locking(
725 conn: &mut PgConnection,
726 exercise_id: Uuid,
727 teacher_reviews_answer_after_locking: bool,
728) -> ModelResult<Exercise> {
729 let exercise = sqlx::query_as!(
730 Exercise,
731 r#"
732UPDATE exercises
733SET teacher_reviews_answer_after_locking = $2
734WHERE id = $1
735 AND deleted_at IS NULL
736RETURNING *
737 "#,
738 exercise_id,
739 teacher_reviews_answer_after_locking
740 )
741 .fetch_one(conn)
742 .await?;
743
744 Ok(exercise)
745}
746
747pub async fn set_exercise_to_use_exercise_specific_peer_or_self_review_config(
748 conn: &mut PgConnection,
749 exercise_id: Uuid,
750 needs_peer_review: bool,
751 needs_self_review: bool,
752 use_course_default_peer_or_self_review_config: bool,
753) -> ModelResult<Uuid> {
754 let id = sqlx::query!(
755 "
756UPDATE exercises
757SET use_course_default_peer_or_self_review_config = $1,
758 needs_peer_review = $2,
759 needs_self_review = $3
760WHERE id = $4
761RETURNING id;
762 ",
763 use_course_default_peer_or_self_review_config,
764 needs_peer_review,
765 needs_self_review,
766 exercise_id
767 )
768 .fetch_one(conn)
769 .await?;
770
771 Ok(id.id)
772}
773
774pub async fn get_all_exercise_statuses_by_user_id_and_course_id(
775 conn: &mut PgConnection,
776 course_id: Uuid,
777 user_id: Uuid,
778) -> ModelResult<Vec<ExerciseStatusSummaryForUser>> {
779 let course_or_exam_id = CourseOrExamId::Course(course_id);
780 let exercises = crate::exercises::get_exercises_by_course_id(&mut *conn, course_id).await?;
782 let mut user_exercise_states =
783 crate::user_exercise_states::get_all_for_user_and_course_or_exam(
784 &mut *conn,
785 user_id,
786 course_or_exam_id,
787 )
788 .await?
789 .into_iter()
790 .map(|ues| (ues.exercise_id, ues))
791 .collect::<HashMap<_, _>>();
792 let mut exercise_slide_submissions =
793 crate::exercise_slide_submissions::get_users_all_submissions_for_course_or_exam(
794 &mut *conn,
795 user_id,
796 course_or_exam_id,
797 )
798 .await?
799 .into_iter()
800 .into_group_map_by(|o| o.exercise_id);
801 let mut given_peer_or_self_review_submissions = crate::peer_or_self_review_submissions::get_all_given_peer_or_self_review_submissions_for_user_and_course(&mut *conn, user_id, course_id).await?.into_iter()
802 .into_group_map_by(|o| o.exercise_id);
803 let given_submission_ids: Vec<Uuid> = given_peer_or_self_review_submissions
804 .values()
805 .flatten()
806 .map(|prs| prs.exercise_slide_submission_id)
807 .collect();
808 let submission_owner_user_ids =
809 crate::exercise_slide_submissions::get_user_ids_by_submission_ids(
810 &mut *conn,
811 &given_submission_ids,
812 )
813 .await?;
814 let mut received_peer_or_self_review_submissions = crate::peer_or_self_review_submissions::get_all_received_peer_or_self_review_submissions_for_user_and_course(&mut *conn, user_id, course_id).await?.into_iter()
815 .into_group_map_by(|o| o.exercise_id);
816 let given_peer_or_self_review_submission_ids = given_peer_or_self_review_submissions
817 .values()
818 .flatten()
819 .map(|x| x.id)
820 .collect::<Vec<_>>();
821 let mut given_peer_or_self_review_question_submissions = crate::peer_or_self_review_question_submissions::get_question_submissions_from_from_peer_or_self_review_submission_ids(&mut *conn, &given_peer_or_self_review_submission_ids).await?
822 .into_iter()
823 .into_group_map_by(|o| {
824 let peer_review_submission = given_peer_or_self_review_submissions.clone().into_iter()
825 .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
826 .unwrap_or_else(|| (Uuid::nil(), vec![]));
827 peer_review_submission.0
828 });
829 let received_peer_or_self_review_submission_ids = received_peer_or_self_review_submissions
830 .values()
831 .flatten()
832 .map(|x| x.id)
833 .collect::<Vec<_>>();
834 let mut received_peer_or_self_review_question_submissions = crate::peer_or_self_review_question_submissions::get_question_submissions_from_from_peer_or_self_review_submission_ids(&mut *conn, &received_peer_or_self_review_submission_ids).await?.into_iter()
835 .into_group_map_by(|o| {
836 let peer_review_submission = received_peer_or_self_review_submissions.clone().into_iter()
837 .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
838 .unwrap_or_else(|| (Uuid::nil(), vec![]));
839 peer_review_submission.0
840 });
841 let mut peer_review_queue_entries =
842 crate::peer_review_queue_entries::get_all_by_user_and_course_id(
843 &mut *conn, user_id, course_id,
844 )
845 .await?
846 .into_iter()
847 .map(|x| (x.exercise_id, x))
848 .collect::<HashMap<_, _>>();
849 let mut teacher_grading_decisions = crate::teacher_grading_decisions::get_all_latest_grading_decisions_by_user_id_and_course_id(&mut *conn, user_id, course_id).await?.into_iter()
850 .filter_map(|tgd| {
851 let user_exercise_state = user_exercise_states.clone().into_iter()
852 .find(|(_exercise_id, ues)| ues.id == tgd.user_exercise_state_id)?;
853 Some((user_exercise_state.0, tgd))
854 }).collect::<HashMap<_, _>>();
855 let all_peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
856 .iter()
857 .chain(received_peer_or_self_review_question_submissions.iter())
858 .flat_map(|(_exercise_id, prqs)| prqs.iter().map(|p| p.peer_or_self_review_question_id))
859 .collect::<Vec<_>>();
860 let all_peer_or_self_review_questions = crate::peer_or_self_review_questions::get_by_ids(
861 &mut *conn,
862 &all_peer_or_self_review_question_ids,
863 )
864 .await?;
865
866 let res = exercises
872 .into_iter()
873 .map(|exercise| {
874 let user_exercise_state = user_exercise_states.remove(&exercise.id);
875 let exercise_slide_submissions = exercise_slide_submissions
876 .remove(&exercise.id)
877 .unwrap_or_default();
878 let given_peer_or_self_review_submissions = given_peer_or_self_review_submissions
879 .remove(&exercise.id)
880 .unwrap_or_default()
881 .into_iter()
882 .map(|prs| {
883 let submission_owner_user_id = submission_owner_user_ids
884 .get(&prs.exercise_slide_submission_id)
885 .copied();
886 PeerOrSelfReviewSubmissionWithSubmissionOwner {
887 submission: prs,
888 submission_owner_user_id,
889 }
890 })
891 .collect();
892 let received_peer_or_self_review_submissions = received_peer_or_self_review_submissions
893 .remove(&exercise.id)
894 .unwrap_or_default()
895 .into_iter()
896 .map(|prs| PeerOrSelfReviewSubmissionWithSubmissionOwner {
897 submission: prs,
898 submission_owner_user_id: None,
899 })
900 .collect();
901 let given_peer_or_self_review_question_submissions =
902 given_peer_or_self_review_question_submissions
903 .remove(&exercise.id)
904 .unwrap_or_default();
905 let received_peer_or_self_review_question_submissions =
906 received_peer_or_self_review_question_submissions
907 .remove(&exercise.id)
908 .unwrap_or_default();
909 let peer_review_queue_entry = peer_review_queue_entries.remove(&exercise.id);
910 let teacher_grading_decision = teacher_grading_decisions.remove(&exercise.id);
911 let peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
912 .iter()
913 .chain(received_peer_or_self_review_question_submissions.iter())
914 .map(|prqs| prqs.peer_or_self_review_question_id)
915 .unique()
916 .collect::<Vec<_>>();
917 let peer_or_self_review_questions = all_peer_or_self_review_questions
918 .iter()
919 .filter(|prq| peer_or_self_review_question_ids.contains(&prq.id))
920 .cloned()
921 .collect::<Vec<_>>();
922 ExerciseStatusSummaryForUser {
923 exercise,
924 user_exercise_state,
925 exercise_slide_submissions,
926 given_peer_or_self_review_submissions,
927 received_peer_or_self_review_submissions,
928 given_peer_or_self_review_question_submissions,
929 received_peer_or_self_review_question_submissions,
930 peer_review_queue_entry,
931 teacher_grading_decision,
932 peer_or_self_review_questions,
933 }
934 })
935 .collect::<Vec<_>>();
936 Ok(res)
937}
938
939pub async fn get_exercises_by_module_containing_exercise_type(
940 conn: &mut PgConnection,
941 exercise_type: &str,
942 course_module_id: Uuid,
943) -> ModelResult<Vec<Exercise>> {
944 let res: Vec<Exercise> = sqlx::query_as!(
945 Exercise,
946 r#"
947SELECT DISTINCT(ex.*)
948FROM exercises ex
949 JOIN exercise_slides slides ON ex.id = slides.exercise_id
950 JOIN exercise_tasks tasks ON slides.id = tasks.exercise_slide_id
951 JOIN chapters c ON ex.chapter_id = c.id
952where tasks.exercise_type = $1
953 AND c.course_module_id = $2
954 AND ex.deleted_at IS NULL
955 AND tasks.deleted_at IS NULL
956 and c.deleted_at IS NULL
957 and slides.deleted_at IS NULL
958 "#,
959 exercise_type,
960 course_module_id
961 )
962 .fetch_all(conn)
963 .await?;
964 Ok(res)
965}
966
967pub async fn collect_user_ids_and_exercise_ids_for_reset(
969 conn: &mut PgConnection,
970 user_ids: &[Uuid],
971 exercise_ids: &[Uuid],
972 threshold: Option<f64>,
973 reset_all_below_max: bool,
974 reset_only_locked_reviews: bool,
975) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
976 let results = sqlx::query!(
977 r#"
978SELECT DISTINCT ues.user_id,
979 ues.exercise_id
980FROM user_exercise_states ues
981 LEFT JOIN exercises e ON ues.exercise_id = e.id
982WHERE ues.user_id = ANY($1)
983 AND ues.exercise_id = ANY($2)
984 AND ues.deleted_at IS NULL
985 AND (
986 $3 = FALSE
987 OR ues.score_given < e.score_maximum
988 )
989 AND (
990 $4::FLOAT IS NULL
991 OR ues.score_given < $4::FLOAT
992 )
993 AND (
994 $5 = FALSE
995 OR ues.reviewing_stage = 'reviewed_and_locked'
996 )
997 "#,
998 user_ids,
999 exercise_ids,
1000 reset_all_below_max,
1001 threshold,
1002 reset_only_locked_reviews
1003 )
1004 .fetch_all(&mut *conn)
1005 .await?;
1006
1007 let mut user_exercise_map: HashMap<Uuid, Vec<Uuid>> = HashMap::new();
1008 for row in &results {
1009 user_exercise_map
1010 .entry(row.user_id)
1011 .or_default()
1012 .push(row.exercise_id);
1013 }
1014
1015 Ok(user_exercise_map.into_iter().collect())
1016}
1017
1018async fn get_chapter_ids_for_exercises_in_course(
1020 conn: &mut PgConnection,
1021 exercise_ids: &[Uuid],
1022 course_id: Uuid,
1023) -> ModelResult<Vec<Uuid>> {
1024 let mut chapter_ids = HashSet::new();
1025 for exercise_id in exercise_ids {
1026 let exercise = get_exercise_by_id(conn, *exercise_id).await?;
1027 if exercise.course_id == Some(course_id)
1028 && let Some(chapter_id) = exercise.chapter_id
1029 {
1030 chapter_ids.insert(chapter_id);
1031 }
1032 }
1033 Ok(chapter_ids.into_iter().collect())
1034}
1035
1036pub async fn reset_exercises_for_selected_users(
1038 conn: &mut PgConnection,
1039 users_and_exercises: &[(Uuid, Vec<Uuid>)],
1040 reset_by: Option<Uuid>,
1041 course_id: Uuid,
1042 reason: Option<String>,
1043) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
1044 let mut successful_resets = Vec::new();
1045 let mut tx = conn.begin().await?;
1046 for (user_id, exercise_ids) in users_and_exercises {
1047 sqlx::query!(
1048 r#"
1049UPDATE exercise_slide_submissions
1050SET deleted_at = NOW()
1051WHERE user_id = $1
1052 AND exercise_id = ANY($2)
1053 AND deleted_at IS NULL
1054 "#,
1055 user_id,
1056 exercise_ids
1057 )
1058 .execute(&mut *tx)
1059 .await?;
1060
1061 sqlx::query!(
1062 r#"
1063UPDATE exercise_task_submissions
1064SET deleted_at = NOW()
1065WHERE exercise_slide_submission_id IN (
1066 SELECT id
1067 FROM exercise_slide_submissions
1068 WHERE user_id = $1
1069 AND exercise_id = ANY($2)
1070 )
1071 AND deleted_at IS NULL
1072 "#,
1073 user_id,
1074 exercise_ids
1075 )
1076 .execute(&mut *tx)
1077 .await?;
1078
1079 sqlx::query!(
1080 r#"
1081UPDATE peer_review_queue_entries
1082SET deleted_at = NOW()
1083WHERE user_id = $1
1084 AND exercise_id = ANY($2)
1085 AND deleted_at IS NULL
1086 "#,
1087 user_id,
1088 exercise_ids
1089 )
1090 .execute(&mut *tx)
1091 .await?;
1092
1093 sqlx::query!(
1094 r#"
1095UPDATE exercise_task_gradings
1096SET deleted_at = NOW()
1097WHERE exercise_task_submission_id IN (
1098 SELECT id
1099 FROM exercise_task_submissions
1100 WHERE exercise_slide_submission_id IN (
1101 SELECT id
1102 FROM exercise_slide_submissions
1103 WHERE user_id = $1
1104 AND exercise_id = ANY($2)
1105 )
1106 )
1107 AND deleted_at IS NULL
1108 "#,
1109 user_id,
1110 exercise_ids
1111 )
1112 .execute(&mut *tx)
1113 .await?;
1114
1115 sqlx::query!(
1116 r#"
1117UPDATE user_exercise_states
1118SET deleted_at = NOW()
1119WHERE user_id = $1
1120 AND exercise_id = ANY($2)
1121 AND deleted_at IS NULL
1122 "#,
1123 user_id,
1124 exercise_ids
1125 )
1126 .execute(&mut *tx)
1127 .await?;
1128
1129 sqlx::query!(
1130 r#"
1131UPDATE user_exercise_task_states
1132SET deleted_at = NOW()
1133WHERE user_exercise_slide_state_id IN (
1134 SELECT id
1135 FROM user_exercise_slide_states
1136 WHERE user_exercise_state_id IN (
1137 SELECT id
1138 FROM user_exercise_states
1139 WHERE user_id = $1
1140 AND exercise_id = ANY($2)
1141 )
1142 )
1143 AND deleted_at IS NULL
1144 "#,
1145 user_id,
1146 exercise_ids
1147 )
1148 .execute(&mut *tx)
1149 .await?;
1150
1151 sqlx::query!(
1152 r#"
1153UPDATE user_exercise_slide_states
1154SET deleted_at = NOW()
1155WHERE user_exercise_state_id IN (
1156 SELECT id
1157 FROM user_exercise_states
1158 WHERE user_id = $1
1159 AND exercise_id = ANY($2)
1160 )
1161 AND deleted_at IS NULL
1162 "#,
1163 user_id,
1164 exercise_ids
1165 )
1166 .execute(&mut *tx)
1167 .await?;
1168
1169 sqlx::query!(
1170 r#"
1171UPDATE teacher_grading_decisions
1172SET deleted_at = NOW()
1173WHERE user_exercise_state_id IN (
1174 SELECT id
1175 FROM user_exercise_states
1176 WHERE user_id = $1
1177 AND exercise_id = ANY($2)
1178 )
1179 AND deleted_at IS NULL
1180 "#,
1181 user_id,
1182 exercise_ids
1183 )
1184 .execute(&mut *tx)
1185 .await?;
1186
1187 exercise_reset_logs::log_exercise_reset(
1189 &mut tx,
1190 reset_by,
1191 *user_id,
1192 exercise_ids,
1193 course_id,
1194 reason.clone(),
1195 )
1196 .await?;
1197
1198 let chapter_ids =
1199 get_chapter_ids_for_exercises_in_course(&mut tx, exercise_ids, course_id).await?;
1200 user_chapter_locking_statuses::unlock_chapters_for_user(
1202 &mut tx,
1203 *user_id,
1204 course_id,
1205 &chapter_ids,
1206 )
1207 .await?;
1208
1209 successful_resets.push((*user_id, exercise_ids.to_vec()));
1210 }
1211 tx.commit().await?;
1212 Ok(successful_resets)
1213}
1214
1215#[cfg(test)]
1216mod test {
1217 use super::*;
1218 use crate::{
1219 chapters,
1220 course_instance_enrollments::{self, NewCourseInstanceEnrollment},
1221 courses,
1222 exercise_service_info::{self, PathInfo},
1223 exercise_services::{self, ExerciseServiceNewOrUpdate},
1224 test_helper::Conn,
1225 test_helper::*,
1226 user_chapter_locking_statuses::{self, ChapterLockingStatus},
1227 user_exercise_states,
1228 };
1229 use chrono::TimeZone;
1230 use sqlx::PgConnection;
1231
1232 async fn insert_exercise_service_with_info(tx: &mut PgConnection) {
1233 let exercise_service = exercise_services::insert_exercise_service(
1234 tx,
1235 &ExerciseServiceNewOrUpdate {
1236 name: "text-exercise".to_string(),
1237 slug: TEST_HELPER_EXERCISE_SERVICE_NAME.to_string(),
1238 public_url: "https://example.com".to_string(),
1239 internal_url: None,
1240 max_reprocessing_submissions_at_once: 1,
1241 },
1242 )
1243 .await
1244 .unwrap();
1245 exercise_service_info::insert(
1246 tx,
1247 &PathInfo {
1248 exercise_service_id: exercise_service.id,
1249 user_interface_iframe_path: "/iframe".to_string(),
1250 grade_endpoint_path: "/grade".to_string(),
1251 public_spec_endpoint_path: "/public-spec".to_string(),
1252 model_solution_spec_endpoint_path: "test-only-empty-path".to_string(),
1253 has_custom_view: false,
1254 },
1255 )
1256 .await
1257 .unwrap();
1258 }
1259
1260 #[tokio::test]
1261 async fn selects_course_material_exercise_for_enrolled_student() {
1262 insert_data!(
1263 :tx,
1264 user: user_id,
1265 org: _organization_id,
1266 course: course_id,
1267 instance: course_instance,
1268 :course_module,
1269 chapter: chapter_id,
1270 page: _page_id,
1271 exercise: exercise_id,
1272 slide: exercise_slide_id,
1273 task: exercise_task_id
1274 );
1275 insert_exercise_service_with_info(tx.as_mut()).await;
1276 course_instance_enrollments::insert_enrollment_and_set_as_current(
1277 tx.as_mut(),
1278 NewCourseInstanceEnrollment {
1279 course_id,
1280 course_instance_id: course_instance.id,
1281 user_id,
1282 },
1283 )
1284 .await
1285 .unwrap();
1286
1287 let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1288 tx.as_mut(),
1289 user_id,
1290 exercise_id,
1291 CourseOrExamId::Course(course_id),
1292 )
1293 .await
1294 .unwrap();
1295 assert!(user_exercise_state.is_none());
1296
1297 let exercise = get_course_material_exercise(
1298 tx.as_mut(),
1299 Some(user_id),
1300 exercise_id,
1301 |_| unimplemented!(),
1302 )
1303 .await
1304 .unwrap();
1305 assert_eq!(
1306 exercise
1307 .current_exercise_slide
1308 .exercise_tasks
1309 .first()
1310 .unwrap()
1311 .id,
1312 exercise_task_id
1313 );
1314
1315 let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1316 tx.as_mut(),
1317 user_id,
1318 exercise_id,
1319 CourseOrExamId::Course(course_id),
1320 )
1321 .await
1322 .unwrap();
1323 assert_eq!(
1324 user_exercise_state
1325 .unwrap()
1326 .selected_exercise_slide_id
1327 .unwrap(),
1328 exercise_slide_id
1329 );
1330 }
1331
1332 #[tokio::test]
1333 async fn course_material_exercise_inherits_chapter_deadline() {
1334 insert_data!(
1335 :tx,
1336 user: user_id,
1337 org: organization_id,
1338 course: course_id,
1339 instance: course_instance,
1340 :course_module,
1341 chapter: chapter_id,
1342 page: page_id,
1343 exercise: exercise_id,
1344 slide: exercise_slide_id,
1345 task: _exercise_task_id
1346 );
1347 insert_exercise_service_with_info(tx.as_mut()).await;
1348 course_instance_enrollments::insert_enrollment_and_set_as_current(
1349 tx.as_mut(),
1350 NewCourseInstanceEnrollment {
1351 course_id,
1352 course_instance_id: course_instance.id,
1353 user_id,
1354 },
1355 )
1356 .await
1357 .unwrap();
1358
1359 let chapter_deadline = Utc.with_ymd_and_hms(2125, 1, 1, 23, 59, 59).unwrap();
1360 let chapter = chapters::get_chapter(tx.as_mut(), chapter_id)
1361 .await
1362 .unwrap();
1363 chapters::update_chapter(
1364 tx.as_mut(),
1365 chapter_id,
1366 chapters::ChapterUpdate {
1367 name: chapter.name,
1368 color: chapter.color,
1369 front_page_id: chapter.front_page_id,
1370 deadline: Some(chapter_deadline),
1371 opens_at: chapter.opens_at,
1372 course_module_id: Some(chapter.course_module_id),
1373 },
1374 )
1375 .await
1376 .unwrap();
1377
1378 let exercise = get_course_material_exercise(
1379 tx.as_mut(),
1380 Some(user_id),
1381 exercise_id,
1382 |_| unimplemented!(),
1383 )
1384 .await
1385 .unwrap();
1386
1387 assert_eq!(exercise.exercise.deadline, Some(chapter_deadline));
1388 }
1389
1390 #[tokio::test]
1391 async fn resetting_exercise_unlocks_its_chapter_for_user() {
1392 insert_data!(
1393 :tx,
1394 user: user_id,
1395 org: _organization_id,
1396 course: course_id,
1397 instance: _course_instance,
1398 :course_module,
1399 chapter: chapter_id,
1400 page: _page_id,
1401 exercise: exercise_id,
1402 slide: _exercise_slide_id,
1403 task: _exercise_task_id
1404 );
1405
1406 let existing_course = courses::get_course(tx.as_mut(), course_id).await.unwrap();
1407 courses::update_course(
1408 tx.as_mut(),
1409 course_id,
1410 courses::CourseUpdate {
1411 name: existing_course.name,
1412 description: existing_course.description,
1413 is_draft: existing_course.is_draft,
1414 is_test_mode: existing_course.is_test_mode,
1415 can_add_chatbot: existing_course.can_add_chatbot,
1416 is_unlisted: existing_course.is_unlisted,
1417 is_joinable_by_code_only: existing_course.is_joinable_by_code_only,
1418 ask_marketing_consent: existing_course.ask_marketing_consent,
1419 flagged_answers_threshold: existing_course.flagged_answers_threshold.unwrap_or(1),
1420 flagged_answers_skip_manual_review_and_allow_retry: existing_course
1421 .flagged_answers_skip_manual_review_and_allow_retry,
1422 closed_at: existing_course.closed_at,
1423 closed_additional_message: existing_course.closed_additional_message,
1424 closed_course_successor_id: existing_course.closed_course_successor_id,
1425 chapter_locking_enabled: true,
1426 },
1427 )
1428 .await
1429 .unwrap();
1430
1431 user_chapter_locking_statuses::complete_and_lock_chapter(
1432 tx.as_mut(),
1433 user_id,
1434 chapter_id,
1435 course_id,
1436 )
1437 .await
1438 .unwrap();
1439
1440 reset_exercises_for_selected_users(
1441 tx.as_mut(),
1442 &[(user_id, vec![exercise_id])],
1443 Some(user_id),
1444 course_id,
1445 Some("test-reset".to_string()),
1446 )
1447 .await
1448 .unwrap();
1449
1450 let status = user_chapter_locking_statuses::get_or_init_status(
1451 tx.as_mut(),
1452 user_id,
1453 chapter_id,
1454 Some(course_id),
1455 Some(true),
1456 )
1457 .await
1458 .unwrap();
1459
1460 assert_eq!(status, Some(ChapterLockingStatus::Unlocked));
1461 }
1462}