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_non_deleted_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<Exercise> {
276 let exercise = sqlx::query_as!(
277 Exercise,
278 "
279SELECT *
280FROM exercises
281WHERE id = $1
282 AND deleted_at IS NULL
283",
284 id
285 )
286 .fetch_one(conn)
287 .await?;
288 Ok(exercise)
289}
290
291pub async fn get_non_deleted_by_ids(
292 conn: &mut PgConnection,
293 ids: &[Uuid],
294) -> ModelResult<Vec<Exercise>> {
295 let exercises = sqlx::query_as!(
296 Exercise,
297 "
298SELECT *
299FROM exercises
300WHERE id = ANY($1)
301 AND deleted_at IS NULL
302",
303 ids
304 )
305 .fetch_all(conn)
306 .await?;
307 Ok(exercises)
308}
309
310pub async fn get_exercise_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<Exercise> {
311 let exercise = sqlx::query_as!(Exercise, "SELECT * FROM exercises WHERE id = $1;", id)
312 .fetch_one(conn)
313 .await?;
314 Ok(exercise)
315}
316
317pub async fn get_exercises_by_course_id(
318 conn: &mut PgConnection,
319 course_id: Uuid,
320) -> ModelResult<Vec<Exercise>> {
321 let exercises = sqlx::query_as!(
322 Exercise,
323 r#"
324SELECT *
325FROM exercises
326WHERE course_id = $1
327 AND deleted_at IS NULL
328"#,
329 course_id
330 )
331 .fetch_all(&mut *conn)
332 .await?;
333 Ok(exercises)
334}
335
336pub async fn get_exercise_submissions_and_status_by_course_instance_id(
337 conn: &mut PgConnection,
338 course_instance_id: Uuid,
339 user_id: Uuid,
340) -> ModelResult<Vec<ExerciseGradingStatus>> {
341 let exercises = sqlx::query_as!(
342 ExerciseGradingStatus,
343 r#"
344 SELECT
345 e.id as exercise_id,
346 e.name as exercise_name,
347 e.score_maximum,
348 ues.score_given,
349 tgd.teacher_decision as "teacher_decision: _",
350 ess.id as submission_id,
351 ess.updated_at
352 FROM exercises e
353 LEFT JOIN user_exercise_states ues on e.id = ues.exercise_id
354 LEFT JOIN teacher_grading_decisions tgd on tgd.user_exercise_state_id = ues.id
355 LEFT JOIN exercise_slide_submissions ess on e.id = ess.exercise_id
356 WHERE e.course_id = (
357 SELECT course_id
358 FROM course_instances
359 WHERE id = $1
360 )
361 AND e.deleted_at IS NULL
362 AND ess.user_id = $2
363 AND ues.user_id = $2
364 ORDER BY e.order_number ASC;
365"#,
366 course_instance_id,
367 user_id
368 )
369 .fetch_all(conn)
370 .await?;
371 Ok(exercises)
372}
373
374pub async fn get_exercises_by_chapter_id(
375 conn: &mut PgConnection,
376 chapter_id: Uuid,
377) -> ModelResult<Vec<Exercise>> {
378 let exercises = sqlx::query_as!(
379 Exercise,
380 r#"
381SELECT *
382FROM exercises
383WHERE chapter_id = $1
384 AND deleted_at IS NULL
385"#,
386 chapter_id
387 )
388 .fetch_all(&mut *conn)
389 .await?;
390 Ok(exercises)
391}
392
393pub async fn get_exercises_by_chapter_ids(
394 conn: &mut PgConnection,
395 chapter_ids: &[Uuid],
396) -> ModelResult<Vec<Exercise>> {
397 if chapter_ids.is_empty() {
398 return Ok(Vec::new());
399 }
400 let exercises = sqlx::query_as!(
401 Exercise,
402 r#"
403SELECT *
404FROM exercises
405WHERE chapter_id = ANY($1)
406 AND deleted_at IS NULL
407"#,
408 chapter_ids as &[Uuid]
409 )
410 .fetch_all(&mut *conn)
411 .await?;
412 Ok(exercises)
413}
414
415pub async fn get_exercises_by_page_id(
416 conn: &mut PgConnection,
417 page_id: Uuid,
418) -> ModelResult<Vec<Exercise>> {
419 let exercises = sqlx::query_as!(
420 Exercise,
421 r#"
422SELECT *
423 FROM exercises
424WHERE page_id = $1
425 AND deleted_at IS NULL;
426"#,
427 page_id,
428 )
429 .fetch_all(&mut *conn)
430 .await?;
431 Ok(exercises)
432}
433
434pub async fn get_exercises_by_exam_id(
435 conn: &mut PgConnection,
436 exam_id: Uuid,
437) -> ModelResult<Vec<Exercise>> {
438 let exercises = sqlx::query_as!(
439 Exercise,
440 r#"
441SELECT *
442FROM exercises
443WHERE exam_id = $1
444 AND deleted_at IS NULL
445"#,
446 exam_id,
447 )
448 .fetch_all(&mut *conn)
449 .await?;
450 Ok(exercises)
451}
452
453pub async fn get_course_or_exam_id(
454 conn: &mut PgConnection,
455 id: Uuid,
456) -> ModelResult<CourseOrExamId> {
457 let res = sqlx::query!(
458 "
459SELECT course_id,
460 exam_id
461FROM exercises
462WHERE id = $1
463",
464 id
465 )
466 .fetch_one(conn)
467 .await?;
468 CourseOrExamId::from_course_and_exam_ids(res.course_id, res.exam_id)
469}
470
471pub async fn get_course_material_exercise(
472 conn: &mut PgConnection,
473 user_id: Option<Uuid>,
474 exercise_id: Uuid,
475 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
476) -> ModelResult<CourseMaterialExercise> {
477 let mut exercise = get_by_id(conn, exercise_id).await?;
478 if exercise.deadline.is_none()
479 && let Some(chapter_id) = exercise.chapter_id
480 {
481 let chapter = crate::chapters::get_chapter(conn, chapter_id).await?;
482 exercise.deadline = chapter.deadline;
483 }
484 let (current_exercise_slide, instance_or_exam_id) =
485 get_or_select_exercise_slide(&mut *conn, user_id, &exercise, fetch_service_info).await?;
486 info!(
487 "Current exercise slide id: {:#?}",
488 current_exercise_slide.id
489 );
490
491 let user_exercise_state = match (user_id, instance_or_exam_id) {
492 (Some(user_id), Some(course_or_exam_id)) => {
493 user_exercise_states::get_user_exercise_state_if_exists(
494 conn,
495 user_id,
496 exercise.id,
497 course_or_exam_id,
498 )
499 .await?
500 }
501 _ => None,
502 };
503
504 let can_post_submission =
505 determine_can_post_submission(&mut *conn, user_id, &exercise, &user_exercise_state).await?;
506
507 let previous_exercise_slide_submission = match user_id {
508 Some(user_id) => {
509 crate::exercise_slide_submissions::try_to_get_users_latest_exercise_slide_submission(
510 conn,
511 current_exercise_slide.id,
512 user_id,
513 )
514 .await?
515 }
516 _ => None,
517 };
518
519 let exercise_status = user_exercise_state.map(|user_exercise_state| ExerciseStatus {
520 score_given: user_exercise_state.score_given,
521 activity_progress: user_exercise_state.activity_progress,
522 grading_progress: user_exercise_state.grading_progress,
523 reviewing_stage: user_exercise_state.reviewing_stage,
524 });
525
526 let exercise_slide_submission_counts = if let Some(user_id) = user_id {
527 if let Some(cioreid) = instance_or_exam_id {
528 get_exercise_slide_submission_counts_for_exercise_user(
529 conn,
530 exercise_id,
531 cioreid,
532 user_id,
533 )
534 .await?
535 } else {
536 HashMap::new()
537 }
538 } else {
539 HashMap::new()
540 };
541
542 let peer_or_self_review_config = if let Some(course_id) = exercise.course_id {
543 if exercise.needs_peer_review || exercise.needs_self_review {
544 let prc = crate::peer_or_self_review_configs::get_by_exercise_or_course_id(
545 conn, &exercise, course_id,
546 )
547 .await
548 .optional()?;
549 prc.map(|prc| CourseMaterialPeerOrSelfReviewConfig {
550 id: prc.id,
551 course_id: prc.course_id,
552 exercise_id: prc.exercise_id,
553 peer_reviews_to_give: prc.peer_reviews_to_give,
554 peer_reviews_to_receive: prc.peer_reviews_to_receive,
555 })
556 } else {
557 None
558 }
559 } else {
560 None
561 };
562
563 let user_course_instance_exercise_service_variables = match (user_id, instance_or_exam_id) {
564 (Some(user_id), Some(course_or_exam_id)) => {
565 Some(crate::user_course_exercise_service_variables::get_all_variables_for_user_and_course_or_exam(conn, user_id, course_or_exam_id).await?)
566 }
567 _ => None,
568 }.unwrap_or_default();
569
570 let should_show_reset_message = if let Some(user_id) = user_id {
571 crate::exercise_reset_logs::user_should_see_reset_message_for_exercise(
572 conn,
573 user_id,
574 exercise_id,
575 )
576 .await?
577 } else {
578 None
579 };
580
581 Ok(CourseMaterialExercise {
582 exercise,
583 can_post_submission,
584 current_exercise_slide,
585 exercise_status,
586 exercise_slide_submission_counts,
587 peer_or_self_review_config,
588 user_course_instance_exercise_service_variables,
589 previous_exercise_slide_submission,
590 should_show_reset_message,
591 })
592}
593
594async fn determine_can_post_submission(
595 conn: &mut PgConnection,
596 user_id: Option<Uuid>,
597 exercise: &Exercise,
598 user_exercise_state: &Option<UserExerciseState>,
599) -> Result<bool, ModelError> {
600 if let Some(user_exercise_state) = user_exercise_state {
601 if user_exercise_state.reviewing_stage != ReviewingStage::NotStarted {
603 return Ok(false);
604 }
605 }
606
607 let can_post_submission = if let Some(user_id) = user_id {
608 if let Some(exam_id) = exercise.exam_id {
609 exams::verify_exam_submission_can_be_made(conn, exam_id, user_id).await?
610 } else {
611 true
612 }
613 } else {
614 false
615 };
616 Ok(can_post_submission)
617}
618
619pub async fn get_or_select_exercise_slide(
620 conn: &mut PgConnection,
621 user_id: Option<Uuid>,
622 exercise: &Exercise,
623 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
624) -> ModelResult<(CourseMaterialExerciseSlide, Option<CourseOrExamId>)> {
625 match (user_id, exercise.course_id, exercise.exam_id) {
626 (None, ..) => {
627 let random_slide =
629 exercise_slides::get_random_exercise_slide_for_exercise(conn, exercise.id).await?;
630 let random_slide_tasks = exercise_tasks::get_course_material_exercise_tasks(
631 conn,
632 random_slide.id,
633 None,
634 fetch_service_info,
635 )
636 .await?;
637 Ok((
638 CourseMaterialExerciseSlide {
639 id: random_slide.id,
640 exercise_tasks: random_slide_tasks,
641 },
642 None,
643 ))
644 }
645 (Some(user_id), Some(course_id), None) => {
646 let user_course_settings = user_course_settings::get_user_course_settings_by_course_id(
648 conn, user_id, course_id,
649 )
650 .await?;
651 match user_course_settings {
652 Some(settings) if settings.current_course_id == course_id => {
653 let course_or_exam_id: CourseOrExamId = exercise.try_into()?;
655 let tasks =
656 exercise_tasks::get_or_select_user_exercise_slide_for_course_or_exam(
657 conn,
658 user_id,
659 exercise.id,
660 course_or_exam_id,
661 fetch_service_info,
662 )
663 .await?;
664 Ok((tasks, Some(CourseOrExamId::Course(course_id))))
665 }
666 Some(_) => {
667 let exercise_tasks =
670 exercise_tasks::get_existing_users_exercise_slide_for_course(
671 conn,
672 user_id,
673 exercise.id,
674 course_id,
675 &fetch_service_info,
676 )
677 .await?;
678 if let Some(exercise_tasks) = exercise_tasks {
679 Ok((exercise_tasks, Some(CourseOrExamId::Course(course_id))))
680 } else {
681 let random_slide = exercise_slides::get_random_exercise_slide_for_exercise(
683 conn,
684 exercise.id,
685 )
686 .await?;
687 let random_tasks = exercise_tasks::get_course_material_exercise_tasks(
688 conn,
689 random_slide.id,
690 Some(user_id),
691 &fetch_service_info,
692 )
693 .await?;
694
695 Ok((
696 CourseMaterialExerciseSlide {
697 id: random_slide.id,
698 exercise_tasks: random_tasks,
699 },
700 None,
701 ))
702 }
703 }
704 None => {
705 Err(ModelError::new(
708 ModelErrorType::PreconditionFailed,
709 "User must be enrolled to the course".to_string(),
710 None,
711 ))
712 }
713 }
714 }
715 (Some(user_id), _, Some(exam_id)) => {
716 info!("selecting exam task");
717 let tasks = exercise_tasks::get_or_select_user_exercise_slide_for_course_or_exam(
719 conn,
720 user_id,
721 exercise.id,
722 CourseOrExamId::Exam(exam_id),
723 fetch_service_info,
724 )
725 .await?;
726 info!("selecting exam task {:#?}", tasks);
727 Ok((tasks, Some(CourseOrExamId::Exam(exam_id))))
728 }
729 (Some(_), ..) => Err(ModelError::new(
730 ModelErrorType::Generic,
731 "The selected exercise is not attached to any course or exam".to_string(),
732 None,
733 )),
734 }
735}
736
737pub async fn delete_exercises_by_page_id(
738 conn: &mut PgConnection,
739 page_id: Uuid,
740) -> ModelResult<Vec<Uuid>> {
741 let deleted_ids = sqlx::query!(
742 "
743UPDATE exercises
744SET deleted_at = now()
745WHERE page_id = $1
746AND deleted_at IS NULL
747RETURNING id;
748 ",
749 page_id
750 )
751 .fetch_all(conn)
752 .await?
753 .into_iter()
754 .map(|x| x.id)
755 .collect();
756 Ok(deleted_ids)
757}
758
759pub async fn update_teacher_reviews_answer_after_locking(
760 conn: &mut PgConnection,
761 exercise_id: Uuid,
762 teacher_reviews_answer_after_locking: bool,
763) -> ModelResult<Exercise> {
764 let exercise = sqlx::query_as!(
765 Exercise,
766 r#"
767UPDATE exercises
768SET teacher_reviews_answer_after_locking = $2
769WHERE id = $1
770 AND deleted_at IS NULL
771RETURNING *
772 "#,
773 exercise_id,
774 teacher_reviews_answer_after_locking
775 )
776 .fetch_one(conn)
777 .await?;
778
779 Ok(exercise)
780}
781
782pub async fn set_exercise_to_use_exercise_specific_peer_or_self_review_config(
783 conn: &mut PgConnection,
784 exercise_id: Uuid,
785 needs_peer_review: bool,
786 needs_self_review: bool,
787 use_course_default_peer_or_self_review_config: bool,
788) -> ModelResult<Uuid> {
789 let id = sqlx::query!(
790 "
791UPDATE exercises
792SET use_course_default_peer_or_self_review_config = $1,
793 needs_peer_review = $2,
794 needs_self_review = $3
795WHERE id = $4
796RETURNING id;
797 ",
798 use_course_default_peer_or_self_review_config,
799 needs_peer_review,
800 needs_self_review,
801 exercise_id
802 )
803 .fetch_one(conn)
804 .await?;
805
806 Ok(id.id)
807}
808
809pub async fn get_all_exercise_statuses_by_user_id_and_course_id(
810 conn: &mut PgConnection,
811 course_id: Uuid,
812 user_id: Uuid,
813) -> ModelResult<Vec<ExerciseStatusSummaryForUser>> {
814 let course_or_exam_id = CourseOrExamId::Course(course_id);
815 let exercises = crate::exercises::get_exercises_by_course_id(&mut *conn, course_id).await?;
817 let mut user_exercise_states =
818 crate::user_exercise_states::get_all_for_user_and_course_or_exam(
819 &mut *conn,
820 user_id,
821 course_or_exam_id,
822 )
823 .await?
824 .into_iter()
825 .map(|ues| (ues.exercise_id, ues))
826 .collect::<HashMap<_, _>>();
827 let mut exercise_slide_submissions =
828 crate::exercise_slide_submissions::get_users_all_submissions_for_course_or_exam(
829 &mut *conn,
830 user_id,
831 course_or_exam_id,
832 )
833 .await?
834 .into_iter()
835 .into_group_map_by(|o| o.exercise_id);
836 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()
837 .into_group_map_by(|o| o.exercise_id);
838 let given_submission_ids: Vec<Uuid> = given_peer_or_self_review_submissions
839 .values()
840 .flatten()
841 .map(|prs| prs.exercise_slide_submission_id)
842 .collect();
843 let submission_owner_user_ids =
844 crate::exercise_slide_submissions::get_user_ids_by_submission_ids(
845 &mut *conn,
846 &given_submission_ids,
847 )
848 .await?;
849 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()
850 .into_group_map_by(|o| o.exercise_id);
851 let given_peer_or_self_review_submission_ids = given_peer_or_self_review_submissions
852 .values()
853 .flatten()
854 .map(|x| x.id)
855 .collect::<Vec<_>>();
856 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?
857 .into_iter()
858 .into_group_map_by(|o| {
859 let peer_review_submission = given_peer_or_self_review_submissions.clone().into_iter()
860 .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
861 .unwrap_or_else(|| (Uuid::nil(), vec![]));
862 peer_review_submission.0
863 });
864 let received_peer_or_self_review_submission_ids = received_peer_or_self_review_submissions
865 .values()
866 .flatten()
867 .map(|x| x.id)
868 .collect::<Vec<_>>();
869 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()
870 .into_group_map_by(|o| {
871 let peer_review_submission = received_peer_or_self_review_submissions.clone().into_iter()
872 .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
873 .unwrap_or_else(|| (Uuid::nil(), vec![]));
874 peer_review_submission.0
875 });
876 let mut peer_review_queue_entries =
877 crate::peer_review_queue_entries::get_all_by_user_and_course_id(
878 &mut *conn, user_id, course_id,
879 )
880 .await?
881 .into_iter()
882 .map(|x| (x.exercise_id, x))
883 .collect::<HashMap<_, _>>();
884 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()
885 .filter_map(|tgd| {
886 let user_exercise_state = user_exercise_states.clone().into_iter()
887 .find(|(_exercise_id, ues)| ues.id == tgd.user_exercise_state_id)?;
888 Some((user_exercise_state.0, tgd))
889 }).collect::<HashMap<_, _>>();
890 let all_peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
891 .iter()
892 .chain(received_peer_or_self_review_question_submissions.iter())
893 .flat_map(|(_exercise_id, prqs)| prqs.iter().map(|p| p.peer_or_self_review_question_id))
894 .collect::<Vec<_>>();
895 let all_peer_or_self_review_questions = crate::peer_or_self_review_questions::get_by_ids(
896 &mut *conn,
897 &all_peer_or_self_review_question_ids,
898 )
899 .await?;
900
901 let res = exercises
907 .into_iter()
908 .map(|exercise| {
909 let user_exercise_state = user_exercise_states.remove(&exercise.id);
910 let exercise_slide_submissions = exercise_slide_submissions
911 .remove(&exercise.id)
912 .unwrap_or_default();
913 let given_peer_or_self_review_submissions = given_peer_or_self_review_submissions
914 .remove(&exercise.id)
915 .unwrap_or_default()
916 .into_iter()
917 .map(|prs| {
918 let submission_owner_user_id = submission_owner_user_ids
919 .get(&prs.exercise_slide_submission_id)
920 .copied();
921 PeerOrSelfReviewSubmissionWithSubmissionOwner {
922 submission: prs,
923 submission_owner_user_id,
924 }
925 })
926 .collect();
927 let received_peer_or_self_review_submissions = received_peer_or_self_review_submissions
928 .remove(&exercise.id)
929 .unwrap_or_default()
930 .into_iter()
931 .map(|prs| PeerOrSelfReviewSubmissionWithSubmissionOwner {
932 submission: prs,
933 submission_owner_user_id: None,
934 })
935 .collect();
936 let given_peer_or_self_review_question_submissions =
937 given_peer_or_self_review_question_submissions
938 .remove(&exercise.id)
939 .unwrap_or_default();
940 let received_peer_or_self_review_question_submissions =
941 received_peer_or_self_review_question_submissions
942 .remove(&exercise.id)
943 .unwrap_or_default();
944 let peer_review_queue_entry = peer_review_queue_entries.remove(&exercise.id);
945 let teacher_grading_decision = teacher_grading_decisions.remove(&exercise.id);
946 let peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
947 .iter()
948 .chain(received_peer_or_self_review_question_submissions.iter())
949 .map(|prqs| prqs.peer_or_self_review_question_id)
950 .unique()
951 .collect::<Vec<_>>();
952 let peer_or_self_review_questions = all_peer_or_self_review_questions
953 .iter()
954 .filter(|prq| peer_or_self_review_question_ids.contains(&prq.id))
955 .cloned()
956 .collect::<Vec<_>>();
957 ExerciseStatusSummaryForUser {
958 exercise,
959 user_exercise_state,
960 exercise_slide_submissions,
961 given_peer_or_self_review_submissions,
962 received_peer_or_self_review_submissions,
963 given_peer_or_self_review_question_submissions,
964 received_peer_or_self_review_question_submissions,
965 peer_review_queue_entry,
966 teacher_grading_decision,
967 peer_or_self_review_questions,
968 }
969 })
970 .collect::<Vec<_>>();
971 Ok(res)
972}
973
974pub async fn get_exercises_by_module_containing_exercise_type(
975 conn: &mut PgConnection,
976 exercise_type: &str,
977 course_module_id: Uuid,
978) -> ModelResult<Vec<Exercise>> {
979 let res: Vec<Exercise> = sqlx::query_as!(
980 Exercise,
981 r#"
982SELECT DISTINCT(ex.*)
983FROM exercises ex
984 JOIN exercise_slides slides ON ex.id = slides.exercise_id
985 JOIN exercise_tasks tasks ON slides.id = tasks.exercise_slide_id
986 JOIN chapters c ON ex.chapter_id = c.id
987where tasks.exercise_type = $1
988 AND c.course_module_id = $2
989 AND ex.deleted_at IS NULL
990 AND tasks.deleted_at IS NULL
991 and c.deleted_at IS NULL
992 and slides.deleted_at IS NULL
993 "#,
994 exercise_type,
995 course_module_id
996 )
997 .fetch_all(conn)
998 .await?;
999 Ok(res)
1000}
1001
1002pub async fn collect_user_ids_and_exercise_ids_for_reset(
1004 conn: &mut PgConnection,
1005 user_ids: &[Uuid],
1006 exercise_ids: &[Uuid],
1007 threshold: Option<f64>,
1008 reset_all_below_max: bool,
1009 reset_only_locked_reviews: bool,
1010) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
1011 let results = sqlx::query!(
1012 r#"
1013SELECT DISTINCT ues.user_id,
1014 ues.exercise_id
1015FROM user_exercise_states ues
1016 LEFT JOIN exercises e ON ues.exercise_id = e.id
1017WHERE ues.user_id = ANY($1)
1018 AND ues.exercise_id = ANY($2)
1019 AND ues.deleted_at IS NULL
1020 AND (
1021 $3 = FALSE
1022 OR ues.score_given < e.score_maximum
1023 )
1024 AND (
1025 $4::FLOAT IS NULL
1026 OR ues.score_given < $4::FLOAT
1027 )
1028 AND (
1029 $5 = FALSE
1030 OR ues.reviewing_stage = 'reviewed_and_locked'
1031 )
1032 "#,
1033 user_ids,
1034 exercise_ids,
1035 reset_all_below_max,
1036 threshold,
1037 reset_only_locked_reviews
1038 )
1039 .fetch_all(&mut *conn)
1040 .await?;
1041
1042 let mut user_exercise_map: HashMap<Uuid, Vec<Uuid>> = HashMap::new();
1043 for row in &results {
1044 user_exercise_map
1045 .entry(row.user_id)
1046 .or_default()
1047 .push(row.exercise_id);
1048 }
1049
1050 Ok(user_exercise_map.into_iter().collect())
1051}
1052
1053async fn get_chapter_ids_for_exercises_in_course(
1055 conn: &mut PgConnection,
1056 exercise_ids: &[Uuid],
1057 course_id: Uuid,
1058) -> ModelResult<Vec<Uuid>> {
1059 let mut chapter_ids = HashSet::new();
1060 for exercise_id in exercise_ids {
1061 let exercise = get_exercise_by_id(conn, *exercise_id).await?;
1062 if exercise.course_id == Some(course_id)
1063 && let Some(chapter_id) = exercise.chapter_id
1064 {
1065 chapter_ids.insert(chapter_id);
1066 }
1067 }
1068 Ok(chapter_ids.into_iter().collect())
1069}
1070
1071pub async fn reset_exercises_for_selected_users(
1073 conn: &mut PgConnection,
1074 users_and_exercises: &[(Uuid, Vec<Uuid>)],
1075 reset_by: Option<Uuid>,
1076 course_id: Uuid,
1077 reason: Option<String>,
1078) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
1079 let mut successful_resets = Vec::new();
1080 let mut tx = conn.begin().await?;
1081 for (user_id, exercise_ids) in users_and_exercises {
1082 sqlx::query!(
1083 r#"
1084UPDATE exercise_slide_submissions
1085SET deleted_at = NOW()
1086WHERE user_id = $1
1087 AND exercise_id = ANY($2)
1088 AND deleted_at IS NULL
1089 "#,
1090 user_id,
1091 exercise_ids
1092 )
1093 .execute(&mut *tx)
1094 .await?;
1095
1096 sqlx::query!(
1097 r#"
1098UPDATE exercise_task_submissions
1099SET deleted_at = NOW()
1100WHERE 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 AND deleted_at IS NULL
1107 "#,
1108 user_id,
1109 exercise_ids
1110 )
1111 .execute(&mut *tx)
1112 .await?;
1113
1114 sqlx::query!(
1115 r#"
1116UPDATE peer_review_queue_entries
1117SET deleted_at = NOW()
1118WHERE user_id = $1
1119 AND exercise_id = ANY($2)
1120 AND deleted_at IS NULL
1121 "#,
1122 user_id,
1123 exercise_ids
1124 )
1125 .execute(&mut *tx)
1126 .await?;
1127
1128 sqlx::query!(
1129 r#"
1130UPDATE exercise_task_gradings
1131SET deleted_at = NOW()
1132WHERE exercise_task_submission_id IN (
1133 SELECT id
1134 FROM exercise_task_submissions
1135 WHERE exercise_slide_submission_id IN (
1136 SELECT id
1137 FROM exercise_slide_submissions
1138 WHERE user_id = $1
1139 AND exercise_id = ANY($2)
1140 )
1141 )
1142 AND deleted_at IS NULL
1143 "#,
1144 user_id,
1145 exercise_ids
1146 )
1147 .execute(&mut *tx)
1148 .await?;
1149
1150 sqlx::query!(
1151 r#"
1152UPDATE user_exercise_states
1153SET deleted_at = NOW()
1154WHERE user_id = $1
1155 AND exercise_id = ANY($2)
1156 AND deleted_at IS NULL
1157 "#,
1158 user_id,
1159 exercise_ids
1160 )
1161 .execute(&mut *tx)
1162 .await?;
1163
1164 sqlx::query!(
1165 r#"
1166UPDATE user_exercise_task_states
1167SET deleted_at = NOW()
1168WHERE user_exercise_slide_state_id IN (
1169 SELECT id
1170 FROM user_exercise_slide_states
1171 WHERE user_exercise_state_id IN (
1172 SELECT id
1173 FROM user_exercise_states
1174 WHERE user_id = $1
1175 AND exercise_id = ANY($2)
1176 )
1177 )
1178 AND deleted_at IS NULL
1179 "#,
1180 user_id,
1181 exercise_ids
1182 )
1183 .execute(&mut *tx)
1184 .await?;
1185
1186 sqlx::query!(
1187 r#"
1188UPDATE user_exercise_slide_states
1189SET deleted_at = NOW()
1190WHERE user_exercise_state_id IN (
1191 SELECT id
1192 FROM user_exercise_states
1193 WHERE user_id = $1
1194 AND exercise_id = ANY($2)
1195 )
1196 AND deleted_at IS NULL
1197 "#,
1198 user_id,
1199 exercise_ids
1200 )
1201 .execute(&mut *tx)
1202 .await?;
1203
1204 sqlx::query!(
1205 r#"
1206UPDATE teacher_grading_decisions
1207SET deleted_at = NOW()
1208WHERE user_exercise_state_id IN (
1209 SELECT id
1210 FROM user_exercise_states
1211 WHERE user_id = $1
1212 AND exercise_id = ANY($2)
1213 )
1214 AND deleted_at IS NULL
1215 "#,
1216 user_id,
1217 exercise_ids
1218 )
1219 .execute(&mut *tx)
1220 .await?;
1221
1222 exercise_reset_logs::log_exercise_reset(
1224 &mut tx,
1225 reset_by,
1226 *user_id,
1227 exercise_ids,
1228 course_id,
1229 reason.clone(),
1230 )
1231 .await?;
1232
1233 let chapter_ids =
1234 get_chapter_ids_for_exercises_in_course(&mut tx, exercise_ids, course_id).await?;
1235 user_chapter_locking_statuses::unlock_chapters_for_user(
1237 &mut tx,
1238 *user_id,
1239 course_id,
1240 &chapter_ids,
1241 )
1242 .await?;
1243
1244 successful_resets.push((*user_id, exercise_ids.to_vec()));
1245 }
1246 tx.commit().await?;
1247 Ok(successful_resets)
1248}
1249
1250pub async fn reset_progress_by_course_id_user_ids_and_exercise_ids(
1251 conn: &mut PgConnection,
1252 course_id: Uuid,
1253 user_ids: &[Uuid],
1254 exercise_ids: &[Uuid],
1255 reset_by: Option<Uuid>,
1256 reason: Option<String>,
1257) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
1258 let mut successful_resets = Vec::new();
1259 let mut tx = conn.begin().await?;
1260 let validated_exercise_ids = sqlx::query!(
1261 r#"
1262SELECT id
1263FROM exercises
1264WHERE id = ANY($1)
1265 AND course_id = $2
1266 AND deleted_at IS NULL
1267 "#,
1268 exercise_ids,
1269 course_id
1270 )
1271 .fetch_all(&mut *tx)
1272 .await?
1273 .into_iter()
1274 .map(|row| row.id)
1275 .collect::<Vec<_>>();
1276
1277 if validated_exercise_ids.is_empty() {
1278 tx.commit().await?;
1279 return Ok(successful_resets);
1280 }
1281
1282 for user_id in user_ids {
1283 sqlx::query!(
1284 r#"
1285UPDATE exercise_slide_submissions
1286SET deleted_at = NOW()
1287WHERE user_id = $1
1288 AND course_id = $2
1289 AND exercise_id = ANY($3)
1290 AND deleted_at IS NULL
1291 "#,
1292 user_id,
1293 course_id,
1294 &validated_exercise_ids
1295 )
1296 .execute(&mut *tx)
1297 .await?;
1298
1299 sqlx::query!(
1300 r#"
1301UPDATE exercise_task_submissions
1302SET deleted_at = NOW()
1303WHERE exercise_slide_submission_id IN (
1304 SELECT id
1305 FROM exercise_slide_submissions
1306 WHERE user_id = $1
1307 AND course_id = $2
1308 AND exercise_id = ANY($3)
1309 )
1310 AND deleted_at IS NULL
1311 "#,
1312 user_id,
1313 course_id,
1314 &validated_exercise_ids
1315 )
1316 .execute(&mut *tx)
1317 .await?;
1318
1319 sqlx::query!(
1320 r#"
1321UPDATE peer_review_queue_entries
1322SET deleted_at = NOW()
1323WHERE user_id = $1
1324 AND exercise_id = ANY($2)
1325 AND deleted_at IS NULL
1326 "#,
1327 user_id,
1328 &validated_exercise_ids
1329 )
1330 .execute(&mut *tx)
1331 .await?;
1332
1333 sqlx::query!(
1334 r#"
1335UPDATE exercise_task_gradings
1336SET deleted_at = NOW()
1337WHERE exercise_task_submission_id IN (
1338 SELECT ets.id
1339 FROM exercise_task_submissions ets
1340 JOIN exercise_slide_submissions ess
1341 ON ess.id = ets.exercise_slide_submission_id
1342 WHERE ess.user_id = $1
1343 AND ess.course_id = $2
1344 AND ess.exercise_id = ANY($3)
1345 )
1346 AND deleted_at IS NULL
1347 "#,
1348 user_id,
1349 course_id,
1350 &validated_exercise_ids
1351 )
1352 .execute(&mut *tx)
1353 .await?;
1354
1355 sqlx::query!(
1356 r#"
1357UPDATE teacher_grading_decisions
1358SET deleted_at = NOW()
1359WHERE user_exercise_state_id IN (
1360 SELECT id
1361 FROM user_exercise_states
1362 WHERE user_id = $1
1363 AND course_id = $2
1364 AND exercise_id = ANY($3)
1365 AND deleted_at IS NULL
1366 )
1367 AND deleted_at IS NULL
1368 "#,
1369 user_id,
1370 course_id,
1371 &validated_exercise_ids
1372 )
1373 .execute(&mut *tx)
1374 .await?;
1375
1376 sqlx::query!(
1377 r#"
1378UPDATE user_exercise_task_states
1379SET deleted_at = NOW()
1380WHERE user_exercise_slide_state_id IN (
1381 SELECT uess.id
1382 FROM user_exercise_slide_states uess
1383 JOIN user_exercise_states ues ON ues.id = uess.user_exercise_state_id
1384 WHERE ues.user_id = $1
1385 AND ues.course_id = $2
1386 AND ues.exercise_id = ANY($3)
1387 )
1388 AND deleted_at IS NULL
1389 "#,
1390 user_id,
1391 course_id,
1392 &validated_exercise_ids
1393 )
1394 .execute(&mut *tx)
1395 .await?;
1396
1397 sqlx::query!(
1398 r#"
1399UPDATE user_exercise_slide_states
1400SET deleted_at = NOW()
1401WHERE user_exercise_state_id IN (
1402 SELECT id
1403 FROM user_exercise_states
1404 WHERE user_id = $1
1405 AND course_id = $2
1406 AND exercise_id = ANY($3)
1407 )
1408 AND deleted_at IS NULL
1409 "#,
1410 user_id,
1411 course_id,
1412 &validated_exercise_ids
1413 )
1414 .execute(&mut *tx)
1415 .await?;
1416
1417 sqlx::query!(
1418 r#"
1419UPDATE user_exercise_states
1420SET deleted_at = NOW()
1421WHERE user_id = $1
1422 AND course_id = $2
1423 AND exercise_id = ANY($3)
1424 AND deleted_at IS NULL
1425 "#,
1426 user_id,
1427 course_id,
1428 &validated_exercise_ids
1429 )
1430 .execute(&mut *tx)
1431 .await?;
1432
1433 exercise_reset_logs::log_exercise_reset(
1434 &mut tx,
1435 reset_by,
1436 *user_id,
1437 &validated_exercise_ids,
1438 course_id,
1439 reason.clone(),
1440 )
1441 .await?;
1442
1443 let chapter_ids =
1444 get_chapter_ids_for_exercises_in_course(&mut tx, &validated_exercise_ids, course_id)
1445 .await?;
1446 user_chapter_locking_statuses::unlock_chapters_for_user(
1447 &mut tx,
1448 *user_id,
1449 course_id,
1450 &chapter_ids,
1451 )
1452 .await?;
1453
1454 successful_resets.push((*user_id, validated_exercise_ids.clone()));
1455 }
1456 tx.commit().await?;
1457 Ok(successful_resets)
1458}
1459
1460#[cfg(test)]
1461mod test {
1462 use super::*;
1463 use crate::{
1464 chapters,
1465 course_instance_enrollments::{self, NewCourseInstanceEnrollment},
1466 courses,
1467 exercise_service_info::{self, PathInfo},
1468 exercise_services::{self, ExerciseServiceNewOrUpdate},
1469 test_helper::Conn,
1470 test_helper::*,
1471 user_chapter_locking_statuses::{self, ChapterLockingStatus},
1472 user_exercise_states,
1473 };
1474 use chrono::TimeZone;
1475 use sqlx::PgConnection;
1476
1477 async fn insert_exercise_service_with_info(tx: &mut PgConnection) {
1478 let exercise_service = exercise_services::insert_exercise_service(
1479 tx,
1480 &ExerciseServiceNewOrUpdate {
1481 name: "text-exercise".to_string(),
1482 slug: TEST_HELPER_EXERCISE_SERVICE_NAME.to_string(),
1483 public_url: "https://example.com".to_string(),
1484 internal_url: None,
1485 max_reprocessing_submissions_at_once: 1,
1486 },
1487 )
1488 .await
1489 .unwrap();
1490 exercise_service_info::insert(
1491 tx,
1492 &PathInfo {
1493 exercise_service_id: exercise_service.id,
1494 user_interface_iframe_path: "/iframe".to_string(),
1495 grade_endpoint_path: "/grade".to_string(),
1496 public_spec_endpoint_path: "/public-spec".to_string(),
1497 model_solution_spec_endpoint_path: "test-only-empty-path".to_string(),
1498 has_custom_view: false,
1499 },
1500 )
1501 .await
1502 .unwrap();
1503 }
1504
1505 #[tokio::test]
1506 async fn selects_course_material_exercise_for_enrolled_student() {
1507 insert_data!(
1508 :tx,
1509 user: user_id,
1510 org: _organization_id,
1511 course: course_id,
1512 instance: course_instance,
1513 :course_module,
1514 chapter: chapter_id,
1515 page: _page_id,
1516 exercise: exercise_id,
1517 slide: exercise_slide_id,
1518 task: exercise_task_id
1519 );
1520 insert_exercise_service_with_info(tx.as_mut()).await;
1521 course_instance_enrollments::insert_enrollment_and_set_as_current(
1522 tx.as_mut(),
1523 NewCourseInstanceEnrollment {
1524 course_id,
1525 course_instance_id: course_instance.id,
1526 user_id,
1527 },
1528 )
1529 .await
1530 .unwrap();
1531
1532 let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1533 tx.as_mut(),
1534 user_id,
1535 exercise_id,
1536 CourseOrExamId::Course(course_id),
1537 )
1538 .await
1539 .unwrap();
1540 assert!(user_exercise_state.is_none());
1541
1542 let exercise = get_course_material_exercise(
1543 tx.as_mut(),
1544 Some(user_id),
1545 exercise_id,
1546 |_| unimplemented!(),
1547 )
1548 .await
1549 .unwrap();
1550 assert_eq!(
1551 exercise
1552 .current_exercise_slide
1553 .exercise_tasks
1554 .first()
1555 .unwrap()
1556 .id,
1557 exercise_task_id
1558 );
1559
1560 let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1561 tx.as_mut(),
1562 user_id,
1563 exercise_id,
1564 CourseOrExamId::Course(course_id),
1565 )
1566 .await
1567 .unwrap();
1568 assert_eq!(
1569 user_exercise_state
1570 .unwrap()
1571 .selected_exercise_slide_id
1572 .unwrap(),
1573 exercise_slide_id
1574 );
1575 }
1576
1577 #[tokio::test]
1578 async fn course_material_exercise_inherits_chapter_deadline() {
1579 insert_data!(
1580 :tx,
1581 user: user_id,
1582 org: organization_id,
1583 course: course_id,
1584 instance: course_instance,
1585 :course_module,
1586 chapter: chapter_id,
1587 page: page_id,
1588 exercise: exercise_id,
1589 slide: exercise_slide_id,
1590 task: _exercise_task_id
1591 );
1592 insert_exercise_service_with_info(tx.as_mut()).await;
1593 course_instance_enrollments::insert_enrollment_and_set_as_current(
1594 tx.as_mut(),
1595 NewCourseInstanceEnrollment {
1596 course_id,
1597 course_instance_id: course_instance.id,
1598 user_id,
1599 },
1600 )
1601 .await
1602 .unwrap();
1603
1604 let chapter_deadline = Utc.with_ymd_and_hms(2125, 1, 1, 23, 59, 59).unwrap();
1605 let chapter = chapters::get_chapter(tx.as_mut(), chapter_id)
1606 .await
1607 .unwrap();
1608 chapters::update_chapter(
1609 tx.as_mut(),
1610 chapter_id,
1611 chapters::ChapterUpdate {
1612 name: chapter.name,
1613 color: chapter.color,
1614 front_page_id: chapter.front_page_id,
1615 deadline: Some(chapter_deadline),
1616 opens_at: chapter.opens_at,
1617 course_module_id: Some(chapter.course_module_id),
1618 },
1619 )
1620 .await
1621 .unwrap();
1622
1623 let exercise = get_course_material_exercise(
1624 tx.as_mut(),
1625 Some(user_id),
1626 exercise_id,
1627 |_| unimplemented!(),
1628 )
1629 .await
1630 .unwrap();
1631
1632 assert_eq!(exercise.exercise.deadline, Some(chapter_deadline));
1633 }
1634
1635 #[tokio::test]
1636 async fn resetting_exercise_unlocks_its_chapter_for_user() {
1637 insert_data!(
1638 :tx,
1639 user: user_id,
1640 org: _organization_id,
1641 course: course_id,
1642 instance: _course_instance,
1643 :course_module,
1644 chapter: chapter_id,
1645 page: _page_id,
1646 exercise: exercise_id,
1647 slide: _exercise_slide_id,
1648 task: _exercise_task_id
1649 );
1650
1651 let existing_course = courses::get_course(tx.as_mut(), course_id).await.unwrap();
1652 courses::update_course(
1653 tx.as_mut(),
1654 course_id,
1655 courses::CourseUpdate {
1656 name: existing_course.name,
1657 description: existing_course.description,
1658 is_draft: existing_course.is_draft,
1659 is_test_mode: existing_course.is_test_mode,
1660 can_add_chatbot: existing_course.can_add_chatbot,
1661 is_unlisted: existing_course.is_unlisted,
1662 is_joinable_by_code_only: existing_course.is_joinable_by_code_only,
1663 ask_marketing_consent: existing_course.ask_marketing_consent,
1664 flagged_answers_threshold: existing_course.flagged_answers_threshold.unwrap_or(1),
1665 flagged_answers_skip_manual_review_and_allow_retry: existing_course
1666 .flagged_answers_skip_manual_review_and_allow_retry,
1667 closed_at: existing_course.closed_at,
1668 closed_additional_message: existing_course.closed_additional_message,
1669 closed_course_successor_id: existing_course.closed_course_successor_id,
1670 chapter_locking_enabled: true,
1671 },
1672 )
1673 .await
1674 .unwrap();
1675
1676 user_chapter_locking_statuses::complete_and_lock_chapter(
1677 tx.as_mut(),
1678 user_id,
1679 chapter_id,
1680 course_id,
1681 )
1682 .await
1683 .unwrap();
1684
1685 reset_exercises_for_selected_users(
1686 tx.as_mut(),
1687 &[(user_id, vec![exercise_id])],
1688 Some(user_id),
1689 course_id,
1690 Some("test-reset".to_string()),
1691 )
1692 .await
1693 .unwrap();
1694
1695 let status = user_chapter_locking_statuses::get_or_init_status(
1696 tx.as_mut(),
1697 user_id,
1698 chapter_id,
1699 Some(course_id),
1700 Some(true),
1701 )
1702 .await
1703 .unwrap();
1704
1705 assert_eq!(status, Some(ChapterLockingStatus::Unlocked));
1706 }
1707}