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_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 None
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
660AND deleted_at IS NULL
661RETURNING id;
662 ",
663 page_id
664 )
665 .fetch_all(conn)
666 .await?
667 .into_iter()
668 .map(|x| x.id)
669 .collect();
670 Ok(deleted_ids)
671}
672
673pub async fn set_exercise_to_use_exercise_specific_peer_or_self_review_config(
674 conn: &mut PgConnection,
675 exercise_id: Uuid,
676 needs_peer_review: bool,
677 needs_self_review: bool,
678 use_course_default_peer_or_self_review_config: bool,
679) -> ModelResult<Uuid> {
680 let id = sqlx::query!(
681 "
682UPDATE exercises
683SET use_course_default_peer_or_self_review_config = $1,
684 needs_peer_review = $2,
685 needs_self_review = $3
686WHERE id = $4
687RETURNING id;
688 ",
689 use_course_default_peer_or_self_review_config,
690 needs_peer_review,
691 needs_self_review,
692 exercise_id
693 )
694 .fetch_one(conn)
695 .await?;
696
697 Ok(id.id)
698}
699
700pub async fn get_all_exercise_statuses_by_user_id_and_course_id(
701 conn: &mut PgConnection,
702 course_id: Uuid,
703 user_id: Uuid,
704) -> ModelResult<Vec<ExerciseStatusSummaryForUser>> {
705 let course_or_exam_id = CourseOrExamId::Course(course_id);
706 let exercises = crate::exercises::get_exercises_by_course_id(&mut *conn, course_id).await?;
708 let mut user_exercise_states =
709 crate::user_exercise_states::get_all_for_user_and_course_or_exam(
710 &mut *conn,
711 user_id,
712 course_or_exam_id,
713 )
714 .await?
715 .into_iter()
716 .map(|ues| (ues.exercise_id, ues))
717 .collect::<HashMap<_, _>>();
718 let mut exercise_slide_submissions =
719 crate::exercise_slide_submissions::get_users_all_submissions_for_course_or_exam(
720 &mut *conn,
721 user_id,
722 course_or_exam_id,
723 )
724 .await?
725 .into_iter()
726 .into_group_map_by(|o| o.exercise_id);
727 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()
728 .into_group_map_by(|o| o.exercise_id);
729 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()
730 .into_group_map_by(|o| o.exercise_id);
731 let given_peer_or_self_review_submission_ids = given_peer_or_self_review_submissions
732 .values()
733 .flatten()
734 .map(|x| x.id)
735 .collect::<Vec<_>>();
736 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?
737 .into_iter()
738 .into_group_map_by(|o| {
739 let peer_review_submission = given_peer_or_self_review_submissions.clone().into_iter()
740 .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
741 .unwrap_or_else(|| (Uuid::nil(), vec![]));
742 peer_review_submission.0
743 });
744 let received_peer_or_self_review_submission_ids = received_peer_or_self_review_submissions
745 .values()
746 .flatten()
747 .map(|x| x.id)
748 .collect::<Vec<_>>();
749 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()
750 .into_group_map_by(|o| {
751 let peer_review_submission = received_peer_or_self_review_submissions.clone().into_iter()
752 .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id))
753 .unwrap_or_else(|| (Uuid::nil(), vec![]));
754 peer_review_submission.0
755 });
756 let mut peer_review_queue_entries =
757 crate::peer_review_queue_entries::get_all_by_user_and_course_id(
758 &mut *conn, user_id, course_id,
759 )
760 .await?
761 .into_iter()
762 .map(|x| (x.exercise_id, x))
763 .collect::<HashMap<_, _>>();
764 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()
765 .filter_map(|tgd| {
766 let user_exercise_state = user_exercise_states.clone().into_iter()
767 .find(|(_exercise_id, ues)| ues.id == tgd.user_exercise_state_id)?;
768 Some((user_exercise_state.0, tgd))
769 }).collect::<HashMap<_, _>>();
770 let all_peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
771 .iter()
772 .chain(received_peer_or_self_review_question_submissions.iter())
773 .flat_map(|(_exercise_id, prqs)| prqs.iter().map(|p| p.peer_or_self_review_question_id))
774 .collect::<Vec<_>>();
775 let all_peer_or_self_review_questions = crate::peer_or_self_review_questions::get_by_ids(
776 &mut *conn,
777 &all_peer_or_self_review_question_ids,
778 )
779 .await?;
780
781 let res = exercises
787 .into_iter()
788 .map(|exercise| {
789 let user_exercise_state = user_exercise_states.remove(&exercise.id);
790 let exercise_slide_submissions = exercise_slide_submissions
791 .remove(&exercise.id)
792 .unwrap_or_default();
793 let given_peer_or_self_review_submissions = given_peer_or_self_review_submissions
794 .remove(&exercise.id)
795 .unwrap_or_default();
796 let received_peer_or_self_review_submissions = received_peer_or_self_review_submissions
797 .remove(&exercise.id)
798 .unwrap_or_default();
799 let given_peer_or_self_review_question_submissions =
800 given_peer_or_self_review_question_submissions
801 .remove(&exercise.id)
802 .unwrap_or_default();
803 let received_peer_or_self_review_question_submissions =
804 received_peer_or_self_review_question_submissions
805 .remove(&exercise.id)
806 .unwrap_or_default();
807 let peer_review_queue_entry = peer_review_queue_entries.remove(&exercise.id);
808 let teacher_grading_decision = teacher_grading_decisions.remove(&exercise.id);
809 let peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions
810 .iter()
811 .chain(received_peer_or_self_review_question_submissions.iter())
812 .map(|prqs| prqs.peer_or_self_review_question_id)
813 .unique()
814 .collect::<Vec<_>>();
815 let peer_or_self_review_questions = all_peer_or_self_review_questions
816 .iter()
817 .filter(|prq| peer_or_self_review_question_ids.contains(&prq.id))
818 .cloned()
819 .collect::<Vec<_>>();
820 ExerciseStatusSummaryForUser {
821 exercise,
822 user_exercise_state,
823 exercise_slide_submissions,
824 given_peer_or_self_review_submissions,
825 received_peer_or_self_review_submissions,
826 given_peer_or_self_review_question_submissions,
827 received_peer_or_self_review_question_submissions,
828 peer_review_queue_entry,
829 teacher_grading_decision,
830 peer_or_self_review_questions,
831 }
832 })
833 .collect::<Vec<_>>();
834 Ok(res)
835}
836
837pub async fn get_exercises_by_module_containing_exercise_type(
838 conn: &mut PgConnection,
839 exercise_type: &str,
840 course_module_id: Uuid,
841) -> ModelResult<Vec<Exercise>> {
842 let res: Vec<Exercise> = sqlx::query_as!(
843 Exercise,
844 r#"
845SELECT DISTINCT(ex.*)
846FROM exercises ex
847 JOIN exercise_slides slides ON ex.id = slides.exercise_id
848 JOIN exercise_tasks tasks ON slides.id = tasks.exercise_slide_id
849 JOIN chapters c ON ex.chapter_id = c.id
850where tasks.exercise_type = $1
851 AND c.course_module_id = $2
852 AND ex.deleted_at IS NULL
853 AND tasks.deleted_at IS NULL
854 and c.deleted_at IS NULL
855 and slides.deleted_at IS NULL
856 "#,
857 exercise_type,
858 course_module_id
859 )
860 .fetch_all(conn)
861 .await?;
862 Ok(res)
863}
864
865pub async fn collect_user_ids_and_exercise_ids_for_reset(
867 conn: &mut PgConnection,
868 user_ids: &[Uuid],
869 exercise_ids: &[Uuid],
870 threshold: Option<f64>,
871 reset_all_below_max: bool,
872 reset_only_locked_reviews: bool,
873) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
874 let results = sqlx::query!(
875 r#"
876SELECT DISTINCT ues.user_id,
877 ues.exercise_id
878FROM user_exercise_states ues
879 LEFT JOIN exercises e ON ues.exercise_id = e.id
880WHERE ues.user_id = ANY($1)
881 AND ues.exercise_id = ANY($2)
882 AND ues.deleted_at IS NULL
883 AND (
884 $3 = FALSE
885 OR ues.score_given < e.score_maximum
886 )
887 AND (
888 $4::FLOAT IS NULL
889 OR ues.score_given < $4::FLOAT
890 )
891 AND (
892 $5 = FALSE
893 OR ues.reviewing_stage = 'reviewed_and_locked'
894 )
895 "#,
896 user_ids,
897 exercise_ids,
898 reset_all_below_max,
899 threshold,
900 reset_only_locked_reviews
901 )
902 .fetch_all(&mut *conn)
903 .await?;
904
905 let mut user_exercise_map: HashMap<Uuid, Vec<Uuid>> = HashMap::new();
906 for row in &results {
907 user_exercise_map
908 .entry(row.user_id)
909 .or_default()
910 .push(row.exercise_id);
911 }
912
913 Ok(user_exercise_map.into_iter().collect())
914}
915
916pub async fn reset_exercises_for_selected_users(
918 conn: &mut PgConnection,
919 users_and_exercises: &[(Uuid, Vec<Uuid>)],
920 reset_by: Option<Uuid>,
921 course_id: Uuid,
922 reason: Option<String>,
923) -> ModelResult<Vec<(Uuid, Vec<Uuid>)>> {
924 let mut successful_resets = Vec::new();
925 let mut tx = conn.begin().await?;
926 for (user_id, exercise_ids) in users_and_exercises {
927 sqlx::query!(
928 r#"
929UPDATE exercise_slide_submissions
930SET deleted_at = NOW()
931WHERE user_id = $1
932 AND exercise_id = ANY($2)
933 AND deleted_at IS NULL
934 "#,
935 user_id,
936 exercise_ids
937 )
938 .execute(&mut *tx)
939 .await?;
940
941 sqlx::query!(
942 r#"
943UPDATE exercise_task_submissions
944SET deleted_at = NOW()
945WHERE exercise_slide_submission_id IN (
946 SELECT id
947 FROM exercise_slide_submissions
948 WHERE user_id = $1
949 AND exercise_id = ANY($2)
950 )
951 AND deleted_at IS NULL
952 "#,
953 user_id,
954 exercise_ids
955 )
956 .execute(&mut *tx)
957 .await?;
958
959 sqlx::query!(
960 r#"
961UPDATE peer_review_queue_entries
962SET deleted_at = NOW()
963WHERE user_id = $1
964 AND exercise_id = ANY($2)
965 AND deleted_at IS NULL
966 "#,
967 user_id,
968 exercise_ids
969 )
970 .execute(&mut *tx)
971 .await?;
972
973 sqlx::query!(
974 r#"
975UPDATE exercise_task_gradings
976SET deleted_at = NOW()
977WHERE exercise_task_submission_id IN (
978 SELECT id
979 FROM exercise_task_submissions
980 WHERE exercise_slide_submission_id IN (
981 SELECT id
982 FROM exercise_slide_submissions
983 WHERE user_id = $1
984 AND exercise_id = ANY($2)
985 )
986 )
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 user_exercise_states
998SET deleted_at = NOW()
999WHERE user_id = $1
1000 AND exercise_id = ANY($2)
1001 AND deleted_at IS NULL
1002 "#,
1003 user_id,
1004 exercise_ids
1005 )
1006 .execute(&mut *tx)
1007 .await?;
1008
1009 sqlx::query!(
1010 r#"
1011UPDATE user_exercise_task_states
1012SET deleted_at = NOW()
1013WHERE user_exercise_slide_state_id IN (
1014 SELECT id
1015 FROM user_exercise_slide_states
1016 WHERE user_exercise_state_id IN (
1017 SELECT id
1018 FROM user_exercise_states
1019 WHERE user_id = $1
1020 AND exercise_id = ANY($2)
1021 )
1022 )
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_slide_states
1034SET deleted_at = NOW()
1035WHERE user_exercise_state_id IN (
1036 SELECT id
1037 FROM user_exercise_states
1038 WHERE user_id = $1
1039 AND exercise_id = ANY($2)
1040 )
1041 AND deleted_at IS NULL
1042 "#,
1043 user_id,
1044 exercise_ids
1045 )
1046 .execute(&mut *tx)
1047 .await?;
1048
1049 sqlx::query!(
1050 r#"
1051UPDATE teacher_grading_decisions
1052SET deleted_at = NOW()
1053WHERE user_exercise_state_id IN (
1054 SELECT id
1055 FROM user_exercise_states
1056 WHERE user_id = $1
1057 AND exercise_id = ANY($2)
1058 )
1059 AND deleted_at IS NULL
1060 "#,
1061 user_id,
1062 exercise_ids
1063 )
1064 .execute(&mut *tx)
1065 .await?;
1066
1067 exercise_reset_logs::log_exercise_reset(
1069 &mut tx,
1070 reset_by,
1071 *user_id,
1072 exercise_ids,
1073 course_id,
1074 reason.clone(),
1075 )
1076 .await?;
1077
1078 successful_resets.push((*user_id, exercise_ids.to_vec()));
1079 }
1080 tx.commit().await?;
1081 Ok(successful_resets)
1082}
1083
1084#[cfg(test)]
1085mod test {
1086 use super::*;
1087 use crate::{
1088 course_instance_enrollments::{self, NewCourseInstanceEnrollment},
1089 exercise_service_info::{self, PathInfo},
1090 exercise_services::{self, ExerciseServiceNewOrUpdate},
1091 test_helper::Conn,
1092 test_helper::*,
1093 user_exercise_states,
1094 };
1095
1096 #[tokio::test]
1097 async fn selects_course_material_exercise_for_enrolled_student() {
1098 insert_data!(
1099 :tx,
1100 user: user_id,
1101 org: organization_id,
1102 course: course_id,
1103 instance: course_instance,
1104 :course_module,
1105 chapter: chapter_id,
1106 page: page_id,
1107 exercise: exercise_id,
1108 slide: exercise_slide_id,
1109 task: exercise_task_id
1110 );
1111 let exercise_service = exercise_services::insert_exercise_service(
1112 tx.as_mut(),
1113 &ExerciseServiceNewOrUpdate {
1114 name: "text-exercise".to_string(),
1115 slug: TEST_HELPER_EXERCISE_SERVICE_NAME.to_string(),
1116 public_url: "https://example.com".to_string(),
1117 internal_url: None,
1118 max_reprocessing_submissions_at_once: 1,
1119 },
1120 )
1121 .await
1122 .unwrap();
1123 let _exercise_service_info = exercise_service_info::insert(
1124 tx.as_mut(),
1125 &PathInfo {
1126 exercise_service_id: exercise_service.id,
1127 user_interface_iframe_path: "/iframe".to_string(),
1128 grade_endpoint_path: "/grade".to_string(),
1129 public_spec_endpoint_path: "/public-spec".to_string(),
1130 model_solution_spec_endpoint_path: "test-only-empty-path".to_string(),
1131 has_custom_view: false,
1132 },
1133 )
1134 .await
1135 .unwrap();
1136 course_instance_enrollments::insert_enrollment_and_set_as_current(
1137 tx.as_mut(),
1138 NewCourseInstanceEnrollment {
1139 course_id,
1140 course_instance_id: course_instance.id,
1141 user_id,
1142 },
1143 )
1144 .await
1145 .unwrap();
1146
1147 let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1148 tx.as_mut(),
1149 user_id,
1150 exercise_id,
1151 CourseOrExamId::Course(course_id),
1152 )
1153 .await
1154 .unwrap();
1155 assert!(user_exercise_state.is_none());
1156
1157 let exercise = get_course_material_exercise(
1158 tx.as_mut(),
1159 Some(user_id),
1160 exercise_id,
1161 |_| unimplemented!(),
1162 )
1163 .await
1164 .unwrap();
1165 assert_eq!(
1166 exercise
1167 .current_exercise_slide
1168 .exercise_tasks
1169 .first()
1170 .unwrap()
1171 .id,
1172 exercise_task_id
1173 );
1174
1175 let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
1176 tx.as_mut(),
1177 user_id,
1178 exercise_id,
1179 CourseOrExamId::Course(course_id),
1180 )
1181 .await
1182 .unwrap();
1183 assert_eq!(
1184 user_exercise_state
1185 .unwrap()
1186 .selected_exercise_slide_id
1187 .unwrap(),
1188 exercise_slide_id
1189 );
1190 }
1191}