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