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