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