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