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