1use derive_more::Display;
2use futures::future::BoxFuture;
3use itertools::Itertools;
4use url::Url;
5
6use crate::{
7 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_exercise_service_variables::UserCourseExerciseServiceVariable,
22 user_course_settings,
23 user_exercise_states::{self, 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: Vec<UserCourseExerciseServiceVariable>,
103 pub should_show_reset_message: Option<String>,
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_exercise_submissions_and_status_by_course_instance_id(
279 conn: &mut PgConnection,
280 course_instance_id: Uuid,
281 user_id: Uuid,
282) -> ModelResult<Vec<ExerciseGradingStatus>> {
283 let exercises = sqlx::query_as!(
284 ExerciseGradingStatus,
285 r#"
286 SELECT
287 e.id as exercise_id,
288 e.name as exercise_name,
289 e.score_maximum,
290 ues.score_given,
291 tgd.teacher_decision as "teacher_decision: _",
292 ess.id as submission_id,
293 ess.updated_at
294 FROM exercises e
295 LEFT JOIN user_exercise_states ues on e.id = ues.exercise_id
296 LEFT JOIN teacher_grading_decisions tgd on tgd.user_exercise_state_id = ues.id
297 LEFT JOIN exercise_slide_submissions ess on e.id = ess.exercise_id
298 WHERE e.course_id = (
299 SELECT course_id
300 FROM course_instances
301 WHERE id = $1
302 )
303 AND e.deleted_at IS NULL
304 AND ess.user_id = $2
305 AND ues.user_id = $2
306 ORDER BY e.order_number ASC;
307"#,
308 course_instance_id,
309 user_id
310 )
311 .fetch_all(conn)
312 .await?;
313 Ok(exercises)
314}
315
316pub async fn get_exercises_by_chapter_id(
317 conn: &mut PgConnection,
318 chapter_id: Uuid,
319) -> ModelResult<Vec<Exercise>> {
320 let exercises = sqlx::query_as!(
321 Exercise,
322 r#"
323SELECT *
324FROM exercises
325WHERE chapter_id = $1
326 AND deleted_at IS NULL
327"#,
328 chapter_id
329 )
330 .fetch_all(&mut *conn)
331 .await?;
332 Ok(exercises)
333}
334
335pub async fn get_exercises_by_chapter_ids(
336 conn: &mut PgConnection,
337 chapter_ids: &[Uuid],
338) -> ModelResult<Vec<Exercise>> {
339 if chapter_ids.is_empty() {
340 return Ok(Vec::new());
341 }
342 let exercises = sqlx::query_as!(
343 Exercise,
344 r#"
345SELECT *
346FROM exercises
347WHERE chapter_id = ANY($1)
348 AND deleted_at IS NULL
349"#,
350 chapter_ids as &[Uuid]
351 )
352 .fetch_all(&mut *conn)
353 .await?;
354 Ok(exercises)
355}
356
357pub async fn get_exercises_by_page_id(
358 conn: &mut PgConnection,
359 page_id: Uuid,
360) -> ModelResult<Vec<Exercise>> {
361 let exercises = sqlx::query_as!(
362 Exercise,
363 r#"
364SELECT *
365 FROM exercises
366WHERE page_id = $1
367 AND deleted_at IS NULL;
368"#,
369 page_id,
370 )
371 .fetch_all(&mut *conn)
372 .await?;
373 Ok(exercises)
374}
375
376pub async fn get_exercises_by_exam_id(
377 conn: &mut PgConnection,
378 exam_id: Uuid,
379) -> ModelResult<Vec<Exercise>> {
380 let exercises = sqlx::query_as!(
381 Exercise,
382 r#"
383SELECT *
384FROM exercises
385WHERE exam_id = $1
386 AND deleted_at IS NULL
387"#,
388 exam_id,
389 )
390 .fetch_all(&mut *conn)
391 .await?;
392 Ok(exercises)
393}
394
395pub async fn get_course_or_exam_id(
396 conn: &mut PgConnection,
397 id: Uuid,
398) -> ModelResult<CourseOrExamId> {
399 let res = sqlx::query!(
400 "
401SELECT course_id,
402 exam_id
403FROM exercises
404WHERE id = $1
405",
406 id
407 )
408 .fetch_one(conn)
409 .await?;
410 CourseOrExamId::from_course_and_exam_ids(res.course_id, res.exam_id)
411}
412
413pub async fn get_course_material_exercise(
414 conn: &mut PgConnection,
415 user_id: Option<Uuid>,
416 exercise_id: Uuid,
417 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
418) -> ModelResult<CourseMaterialExercise> {
419 let exercise = get_by_id(conn, exercise_id).await?;
420 let (current_exercise_slide, instance_or_exam_id) =
421 get_or_select_exercise_slide(&mut *conn, user_id, &exercise, fetch_service_info).await?;
422 info!(
423 "Current exercise slide id: {:#?}",
424 current_exercise_slide.id
425 );
426
427 let user_exercise_state = match (user_id, instance_or_exam_id) {
428 (Some(user_id), Some(course_or_exam_id)) => {
429 user_exercise_states::get_user_exercise_state_if_exists(
430 conn,
431 user_id,
432 exercise.id,
433 course_or_exam_id,
434 )
435 .await?
436 }
437 _ => None,
438 };
439
440 let can_post_submission =
441 determine_can_post_submission(&mut *conn, user_id, &exercise, &user_exercise_state).await?;
442
443 let previous_exercise_slide_submission = match user_id {
444 Some(user_id) => {
445 crate::exercise_slide_submissions::try_to_get_users_latest_exercise_slide_submission(
446 conn,
447 current_exercise_slide.id,
448 user_id,
449 )
450 .await?
451 }
452 _ => None,
453 };
454
455 let exercise_status = user_exercise_state.map(|user_exercise_state| ExerciseStatus {
456 score_given: user_exercise_state.score_given,
457 activity_progress: user_exercise_state.activity_progress,
458 grading_progress: user_exercise_state.grading_progress,
459 reviewing_stage: user_exercise_state.reviewing_stage,
460 });
461
462 let exercise_slide_submission_counts = if let Some(user_id) = user_id {
463 if let Some(cioreid) = instance_or_exam_id {
464 get_exercise_slide_submission_counts_for_exercise_user(
465 conn,
466 exercise_id,
467 cioreid,
468 user_id,
469 )
470 .await?
471 } else {
472 HashMap::new()
473 }
474 } else {
475 HashMap::new()
476 };
477
478 let peer_or_self_review_config = if let Some(course_id) = exercise.course_id {
479 if exercise.needs_peer_review || exercise.needs_self_review {
480 let prc = crate::peer_or_self_review_configs::get_by_exercise_or_course_id(
481 conn, &exercise, course_id,
482 )
483 .await
484 .optional()?;
485 prc.map(|prc| CourseMaterialPeerOrSelfReviewConfig {
486 id: prc.id,
487 course_id: prc.course_id,
488 exercise_id: prc.exercise_id,
489 peer_reviews_to_give: prc.peer_reviews_to_give,
490 peer_reviews_to_receive: prc.peer_reviews_to_receive,
491 })
492 } else {
493 None
494 }
495 } else {
496 None
497 };
498
499 let user_course_instance_exercise_service_variables = match (user_id, instance_or_exam_id) {
500 (Some(user_id), Some(course_or_exam_id)) => {
501 Some(crate::user_course_exercise_service_variables::get_all_variables_for_user_and_course_or_exam(conn, user_id, course_or_exam_id).await?)
502 }
503 _ => None,
504 }.unwrap_or_default();
505
506 let should_show_reset_message = if let Some(user_id) = user_id {
507 crate::exercise_reset_logs::user_should_see_reset_message_for_exercise(
508 conn,
509 user_id,
510 exercise_id,
511 )
512 .await?
513 } else {
514 None
515 };
516
517 Ok(CourseMaterialExercise {
518 exercise,
519 can_post_submission,
520 current_exercise_slide,
521 exercise_status,
522 exercise_slide_submission_counts,
523 peer_or_self_review_config,
524 user_course_instance_exercise_service_variables,
525 previous_exercise_slide_submission,
526 should_show_reset_message,
527 })
528}
529
530async fn determine_can_post_submission(
531 conn: &mut PgConnection,
532 user_id: Option<Uuid>,
533 exercise: &Exercise,
534 user_exercise_state: &Option<UserExerciseState>,
535) -> Result<bool, ModelError> {
536 if let Some(user_exercise_state) = user_exercise_state {
537 if user_exercise_state.reviewing_stage != ReviewingStage::NotStarted {
539 return Ok(false);
540 }
541 }
542
543 let can_post_submission = if let Some(user_id) = user_id {
544 if let Some(exam_id) = exercise.exam_id {
545 exams::verify_exam_submission_can_be_made(conn, exam_id, user_id).await?
546 } else {
547 true
548 }
549 } else {
550 false
551 };
552 Ok(can_post_submission)
553}
554
555pub async fn get_or_select_exercise_slide(
556 conn: &mut PgConnection,
557 user_id: Option<Uuid>,
558 exercise: &Exercise,
559 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
560) -> ModelResult<(CourseMaterialExerciseSlide, Option<CourseOrExamId>)> {
561 match (user_id, exercise.course_id, exercise.exam_id) {
562 (None, ..) => {
563 let random_slide =
565 exercise_slides::get_random_exercise_slide_for_exercise(conn, exercise.id).await?;
566 let random_slide_tasks = exercise_tasks::get_course_material_exercise_tasks(
567 conn,
568 random_slide.id,
569 None,
570 fetch_service_info,
571 )
572 .await?;
573 Ok((
574 CourseMaterialExerciseSlide {
575 id: random_slide.id,
576 exercise_tasks: random_slide_tasks,
577 },
578 None,
579 ))
580 }
581 (Some(user_id), Some(course_id), None) => {
582 let user_course_settings = user_course_settings::get_user_course_settings_by_course_id(
584 conn, user_id, course_id,
585 )
586 .await?;
587 match user_course_settings {
588 Some(settings) if settings.current_course_id == course_id => {
589 let course_or_exam_id: CourseOrExamId = exercise.try_into()?;
591 let tasks =
592 exercise_tasks::get_or_select_user_exercise_slide_for_course_or_exam(
593 conn,
594 user_id,
595 exercise.id,
596 course_or_exam_id,
597 fetch_service_info,
598 )
599 .await?;
600 Ok((tasks, Some(CourseOrExamId::Course(course_id))))
601 }
602 Some(_) => {
603 let exercise_tasks =
606 exercise_tasks::get_existing_users_exercise_slide_for_course(
607 conn,
608 user_id,
609 exercise.id,
610 course_id,
611 &fetch_service_info,
612 )
613 .await?;
614 if let Some(exercise_tasks) = exercise_tasks {
615 Ok((exercise_tasks, Some(CourseOrExamId::Course(course_id))))
616 } else {
617 let random_slide = exercise_slides::get_random_exercise_slide_for_exercise(
619 conn,
620 exercise.id,
621 )
622 .await?;
623 let random_tasks = exercise_tasks::get_course_material_exercise_tasks(
624 conn,
625 random_slide.id,
626 Some(user_id),
627 &fetch_service_info,
628 )
629 .await?;
630
631 Ok((
632 CourseMaterialExerciseSlide {
633 id: random_slide.id,
634 exercise_tasks: random_tasks,
635 },
636 None,
637 ))
638 }
639 }
640 None => {
641 Err(ModelError::new(
644 ModelErrorType::PreconditionFailed,
645 "User must be enrolled to the course".to_string(),
646 None,
647 ))
648 }
649 }
650 }
651 (Some(user_id), _, Some(exam_id)) => {
652 info!("selecting exam task");
653 let tasks = exercise_tasks::get_or_select_user_exercise_slide_for_course_or_exam(
655 conn,
656 user_id,
657 exercise.id,
658 CourseOrExamId::Exam(exam_id),
659 fetch_service_info,
660 )
661 .await?;
662 info!("selecting exam task {:#?}", tasks);
663 Ok((tasks, Some(CourseOrExamId::Exam(exam_id))))
664 }
665 (Some(_), ..) => Err(ModelError::new(
666 ModelErrorType::Generic,
667 "The selected exercise is not attached to any course or exam".to_string(),
668 None,
669 )),
670 }
671}
672
673pub async fn delete_exercises_by_page_id(
674 conn: &mut PgConnection,
675 page_id: Uuid,
676) -> ModelResult<Vec<Uuid>> {
677 let deleted_ids = sqlx::query!(
678 "
679UPDATE exercises
680SET deleted_at = now()
681WHERE page_id = $1
682AND deleted_at IS NULL
683RETURNING id;
684 ",
685 page_id
686 )
687 .fetch_all(conn)
688 .await?
689 .into_iter()
690 .map(|x| x.id)
691 .collect();
692 Ok(deleted_ids)
693}
694
695pub async fn set_exercise_to_use_exercise_specific_peer_or_self_review_config(
696 conn: &mut PgConnection,
697 exercise_id: Uuid,
698 needs_peer_review: bool,
699 needs_self_review: bool,
700 use_course_default_peer_or_self_review_config: bool,
701) -> ModelResult<Uuid> {
702 let id = sqlx::query!(
703 "
704UPDATE exercises
705SET use_course_default_peer_or_self_review_config = $1,
706 needs_peer_review = $2,
707 needs_self_review = $3
708WHERE id = $4
709RETURNING id;
710 ",
711 use_course_default_peer_or_self_review_config,
712 needs_peer_review,
713 needs_self_review,
714 exercise_id
715 )
716 .fetch_one(conn)
717 .await?;
718
719 Ok(id.id)
720}
721
722pub async fn get_all_exercise_statuses_by_user_id_and_course_id(
723 conn: &mut PgConnection,
724 course_id: Uuid,
725 user_id: Uuid,
726) -> ModelResult<Vec<ExerciseStatusSummaryForUser>> {
727 let course_or_exam_id = CourseOrExamId::Course(course_id);
728 let exercises = crate::exercises::get_exercises_by_course_id(&mut *conn, course_id).await?;
730 let mut user_exercise_states =
731 crate::user_exercise_states::get_all_for_user_and_course_or_exam(
732 &mut *conn,
733 user_id,
734 course_or_exam_id,
735 )
736 .await?
737 .into_iter()
738 .map(|ues| (ues.exercise_id, ues))
739 .collect::<HashMap<_, _>>();
740 let mut exercise_slide_submissions =
741 crate::exercise_slide_submissions::get_users_all_submissions_for_course_or_exam(
742 &mut *conn,
743 user_id,
744 course_or_exam_id,
745 )
746 .await?
747 .into_iter()
748 .into_group_map_by(|o| o.exercise_id);
749 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()
750 .into_group_map_by(|o| o.exercise_id);
751 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()
752 .into_group_map_by(|o| o.exercise_id);
753 let given_peer_or_self_review_submission_ids = given_peer_or_self_review_submissions
754 .values()
755 .flatten()
756 .map(|x| x.id)
757 .collect::<Vec<_>>();
758 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?
759 .into_iter()
760 .into_group_map_by(|o| {
761 let peer_review_submission = given_peer_or_self_review_submissions.clone().into_iter()
762 .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
763 .unwrap_or_else(|| (Uuid::nil(), vec![]));
764 peer_review_submission.0
765 });
766 let received_peer_or_self_review_submission_ids = received_peer_or_self_review_submissions
767 .values()
768 .flatten()
769 .map(|x| x.id)
770 .collect::<Vec<_>>();
771 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()
772 .into_group_map_by(|o| {
773 let peer_review_submission = received_peer_or_self_review_submissions.clone().into_iter()
774 .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
775 .unwrap_or_else(|| (Uuid::nil(), vec![]));
776 peer_review_submission.0
777 });
778 let mut peer_review_queue_entries =
779 crate::peer_review_queue_entries::get_all_by_user_and_course_id(
780 &mut *conn, user_id, course_id,
781 )
782 .await?
783 .into_iter()
784 .map(|x| (x.exercise_id, x))
785 .collect::<HashMap<_, _>>();
786 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()
787 .filter_map(|tgd| {
788 let user_exercise_state = user_exercise_states.clone().into_iter()
789 .find(|(_exercise_id, ues)| ues.id == tgd.user_exercise_state_id)?;
790 Some((user_exercise_state.0, tgd))
791 }).collect::<HashMap<_, _>>();
792 let all_peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
793 .iter()
794 .chain(received_peer_or_self_review_question_submissions.iter())
795 .flat_map(|(_exercise_id, prqs)| prqs.iter().map(|p| p.peer_or_self_review_question_id))
796 .collect::<Vec<_>>();
797 let all_peer_or_self_review_questions = crate::peer_or_self_review_questions::get_by_ids(
798 &mut *conn,
799 &all_peer_or_self_review_question_ids,
800 )
801 .await?;
802
803 let res = exercises
809 .into_iter()
810 .map(|exercise| {
811 let user_exercise_state = user_exercise_states.remove(&exercise.id);
812 let exercise_slide_submissions = exercise_slide_submissions
813 .remove(&exercise.id)
814 .unwrap_or_default();
815 let given_peer_or_self_review_submissions = given_peer_or_self_review_submissions
816 .remove(&exercise.id)
817 .unwrap_or_default();
818 let received_peer_or_self_review_submissions = received_peer_or_self_review_submissions
819 .remove(&exercise.id)
820 .unwrap_or_default();
821 let given_peer_or_self_review_question_submissions =
822 given_peer_or_self_review_question_submissions
823 .remove(&exercise.id)
824 .unwrap_or_default();
825 let received_peer_or_self_review_question_submissions =
826 received_peer_or_self_review_question_submissions
827 .remove(&exercise.id)
828 .unwrap_or_default();
829 let peer_review_queue_entry = peer_review_queue_entries.remove(&exercise.id);
830 let teacher_grading_decision = teacher_grading_decisions.remove(&exercise.id);
831 let peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
832 .iter()
833 .chain(received_peer_or_self_review_question_submissions.iter())
834 .map(|prqs| prqs.peer_or_self_review_question_id)
835 .unique()
836 .collect::<Vec<_>>();
837 let peer_or_self_review_questions = all_peer_or_self_review_questions
838 .iter()
839 .filter(|prq| peer_or_self_review_question_ids.contains(&prq.id))
840 .cloned()
841 .collect::<Vec<_>>();
842 ExerciseStatusSummaryForUser {
843 exercise,
844 user_exercise_state,
845 exercise_slide_submissions,
846 given_peer_or_self_review_submissions,
847 received_peer_or_self_review_submissions,
848 given_peer_or_self_review_question_submissions,
849 received_peer_or_self_review_question_submissions,
850 peer_review_queue_entry,
851 teacher_grading_decision,
852 peer_or_self_review_questions,
853 }
854 })
855 .collect::<Vec<_>>();
856 Ok(res)
857}
858
859pub async fn get_exercises_by_module_containing_exercise_type(
860 conn: &mut PgConnection,
861 exercise_type: &str,
862 course_module_id: Uuid,
863) -> ModelResult<Vec<Exercise>> {
864 let res: Vec<Exercise> = sqlx::query_as!(
865 Exercise,
866 r#"
867SELECT DISTINCT(ex.*)
868FROM exercises ex
869 JOIN exercise_slides slides ON ex.id = slides.exercise_id
870 JOIN exercise_tasks tasks ON slides.id = tasks.exercise_slide_id
871 JOIN chapters c ON ex.chapter_id = c.id
872where tasks.exercise_type = $1
873 AND c.course_module_id = $2
874 AND ex.deleted_at IS NULL
875 AND tasks.deleted_at IS NULL
876 and c.deleted_at IS NULL
877 and slides.deleted_at IS NULL
878 "#,
879 exercise_type,
880 course_module_id
881 )
882 .fetch_all(conn)
883 .await?;
884 Ok(res)
885}
886
887pub async fn collect_user_ids_and_exercise_ids_for_reset(
889 conn: &mut PgConnection,
890 user_ids: &[Uuid],
891 exercise_ids: &[Uuid],
892 threshold: Option<f64>,
893 reset_all_below_max: bool,
894 reset_only_locked_reviews: bool,
895) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
896 let results = sqlx::query!(
897 r#"
898SELECT DISTINCT ues.user_id,
899 ues.exercise_id
900FROM user_exercise_states ues
901 LEFT JOIN exercises e ON ues.exercise_id = e.id
902WHERE ues.user_id = ANY($1)
903 AND ues.exercise_id = ANY($2)
904 AND ues.deleted_at IS NULL
905 AND (
906 $3 = FALSE
907 OR ues.score_given < e.score_maximum
908 )
909 AND (
910 $4::FLOAT IS NULL
911 OR ues.score_given < $4::FLOAT
912 )
913 AND (
914 $5 = FALSE
915 OR ues.reviewing_stage = 'reviewed_and_locked'
916 )
917 "#,
918 user_ids,
919 exercise_ids,
920 reset_all_below_max,
921 threshold,
922 reset_only_locked_reviews
923 )
924 .fetch_all(&mut *conn)
925 .await?;
926
927 let mut user_exercise_map: HashMap<Uuid, Vec<Uuid>> = HashMap::new();
928 for row in &results {
929 user_exercise_map
930 .entry(row.user_id)
931 .or_default()
932 .push(row.exercise_id);
933 }
934
935 Ok(user_exercise_map.into_iter().collect())
936}
937
938pub async fn reset_exercises_for_selected_users(
940 conn: &mut PgConnection,
941 users_and_exercises: &[(Uuid, Vec<Uuid>)],
942 reset_by: Option<Uuid>,
943 course_id: Uuid,
944 reason: Option<String>,
945) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
946 let mut successful_resets = Vec::new();
947 let mut tx = conn.begin().await?;
948 for (user_id, exercise_ids) in users_and_exercises {
949 sqlx::query!(
950 r#"
951UPDATE exercise_slide_submissions
952SET deleted_at = NOW()
953WHERE user_id = $1
954 AND exercise_id = ANY($2)
955 AND deleted_at IS NULL
956 "#,
957 user_id,
958 exercise_ids
959 )
960 .execute(&mut *tx)
961 .await?;
962
963 sqlx::query!(
964 r#"
965UPDATE exercise_task_submissions
966SET deleted_at = NOW()
967WHERE exercise_slide_submission_id IN (
968 SELECT id
969 FROM exercise_slide_submissions
970 WHERE user_id = $1
971 AND exercise_id = ANY($2)
972 )
973 AND deleted_at IS NULL
974 "#,
975 user_id,
976 exercise_ids
977 )
978 .execute(&mut *tx)
979 .await?;
980
981 sqlx::query!(
982 r#"
983UPDATE peer_review_queue_entries
984SET deleted_at = NOW()
985WHERE user_id = $1
986 AND exercise_id = ANY($2)
987 AND deleted_at IS NULL
988 "#,
989 user_id,
990 exercise_ids
991 )
992 .execute(&mut *tx)
993 .await?;
994
995 sqlx::query!(
996 r#"
997UPDATE exercise_task_gradings
998SET deleted_at = NOW()
999WHERE exercise_task_submission_id IN (
1000 SELECT id
1001 FROM exercise_task_submissions
1002 WHERE exercise_slide_submission_id IN (
1003 SELECT id
1004 FROM exercise_slide_submissions
1005 WHERE user_id = $1
1006 AND exercise_id = ANY($2)
1007 )
1008 )
1009 AND deleted_at IS NULL
1010 "#,
1011 user_id,
1012 exercise_ids
1013 )
1014 .execute(&mut *tx)
1015 .await?;
1016
1017 sqlx::query!(
1018 r#"
1019UPDATE user_exercise_states
1020SET deleted_at = NOW()
1021WHERE user_id = $1
1022 AND exercise_id = ANY($2)
1023 AND deleted_at IS NULL
1024 "#,
1025 user_id,
1026 exercise_ids
1027 )
1028 .execute(&mut *tx)
1029 .await?;
1030
1031 sqlx::query!(
1032 r#"
1033UPDATE user_exercise_task_states
1034SET deleted_at = NOW()
1035WHERE user_exercise_slide_state_id IN (
1036 SELECT id
1037 FROM user_exercise_slide_states
1038 WHERE user_exercise_state_id IN (
1039 SELECT id
1040 FROM user_exercise_states
1041 WHERE user_id = $1
1042 AND exercise_id = ANY($2)
1043 )
1044 )
1045 AND deleted_at IS NULL
1046 "#,
1047 user_id,
1048 exercise_ids
1049 )
1050 .execute(&mut *tx)
1051 .await?;
1052
1053 sqlx::query!(
1054 r#"
1055UPDATE user_exercise_slide_states
1056SET deleted_at = NOW()
1057WHERE user_exercise_state_id IN (
1058 SELECT id
1059 FROM user_exercise_states
1060 WHERE user_id = $1
1061 AND exercise_id = ANY($2)
1062 )
1063 AND deleted_at IS NULL
1064 "#,
1065 user_id,
1066 exercise_ids
1067 )
1068 .execute(&mut *tx)
1069 .await?;
1070
1071 sqlx::query!(
1072 r#"
1073UPDATE teacher_grading_decisions
1074SET deleted_at = NOW()
1075WHERE user_exercise_state_id IN (
1076 SELECT id
1077 FROM user_exercise_states
1078 WHERE user_id = $1
1079 AND exercise_id = ANY($2)
1080 )
1081 AND deleted_at IS NULL
1082 "#,
1083 user_id,
1084 exercise_ids
1085 )
1086 .execute(&mut *tx)
1087 .await?;
1088
1089 exercise_reset_logs::log_exercise_reset(
1091 &mut tx,
1092 reset_by,
1093 *user_id,
1094 exercise_ids,
1095 course_id,
1096 reason.clone(),
1097 )
1098 .await?;
1099
1100 successful_resets.push((*user_id, exercise_ids.to_vec()));
1101 }
1102 tx.commit().await?;
1103 Ok(successful_resets)
1104}
1105
1106#[cfg(test)]
1107mod test {
1108 use super::*;
1109 use crate::{
1110 course_instance_enrollments::{self, NewCourseInstanceEnrollment},
1111 exercise_service_info::{self, PathInfo},
1112 exercise_services::{self, ExerciseServiceNewOrUpdate},
1113 test_helper::Conn,
1114 test_helper::*,
1115 user_exercise_states,
1116 };
1117
1118 #[tokio::test]
1119 async fn selects_course_material_exercise_for_enrolled_student() {
1120 insert_data!(
1121 :tx,
1122 user: user_id,
1123 org: organization_id,
1124 course: course_id,
1125 instance: course_instance,
1126 :course_module,
1127 chapter: chapter_id,
1128 page: page_id,
1129 exercise: exercise_id,
1130 slide: exercise_slide_id,
1131 task: exercise_task_id
1132 );
1133 let exercise_service = exercise_services::insert_exercise_service(
1134 tx.as_mut(),
1135 &ExerciseServiceNewOrUpdate {
1136 name: "text-exercise".to_string(),
1137 slug: TEST_HELPER_EXERCISE_SERVICE_NAME.to_string(),
1138 public_url: "https://example.com".to_string(),
1139 internal_url: None,
1140 max_reprocessing_submissions_at_once: 1,
1141 },
1142 )
1143 .await
1144 .unwrap();
1145 let _exercise_service_info = exercise_service_info::insert(
1146 tx.as_mut(),
1147 &PathInfo {
1148 exercise_service_id: exercise_service.id,
1149 user_interface_iframe_path: "/iframe".to_string(),
1150 grade_endpoint_path: "/grade".to_string(),
1151 public_spec_endpoint_path: "/public-spec".to_string(),
1152 model_solution_spec_endpoint_path: "test-only-empty-path".to_string(),
1153 has_custom_view: false,
1154 },
1155 )
1156 .await
1157 .unwrap();
1158 course_instance_enrollments::insert_enrollment_and_set_as_current(
1159 tx.as_mut(),
1160 NewCourseInstanceEnrollment {
1161 course_id,
1162 course_instance_id: course_instance.id,
1163 user_id,
1164 },
1165 )
1166 .await
1167 .unwrap();
1168
1169 let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1170 tx.as_mut(),
1171 user_id,
1172 exercise_id,
1173 CourseOrExamId::Course(course_id),
1174 )
1175 .await
1176 .unwrap();
1177 assert!(user_exercise_state.is_none());
1178
1179 let exercise = get_course_material_exercise(
1180 tx.as_mut(),
1181 Some(user_id),
1182 exercise_id,
1183 |_| unimplemented!(),
1184 )
1185 .await
1186 .unwrap();
1187 assert_eq!(
1188 exercise
1189 .current_exercise_slide
1190 .exercise_tasks
1191 .first()
1192 .unwrap()
1193 .id,
1194 exercise_task_id
1195 );
1196
1197 let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1198 tx.as_mut(),
1199 user_id,
1200 exercise_id,
1201 CourseOrExamId::Course(course_id),
1202 )
1203 .await
1204 .unwrap();
1205 assert_eq!(
1206 user_exercise_state
1207 .unwrap()
1208 .selected_exercise_slide_id
1209 .unwrap(),
1210 exercise_slide_id
1211 );
1212 }
1213}