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