1use futures::future::BoxFuture;
4use std::collections::HashMap;
5use url::Url;
6use utoipa::ToSchema;
7
8use crate::{
9 exercise_service_info::ExerciseServiceInfoApi,
10 exercise_slide_submissions::{self, ExerciseSlideSubmission, NewExerciseSlideSubmission},
11 exercise_task_gradings::{
12 self, ExerciseTaskGrading, ExerciseTaskGradingResult, UserPointsUpdateStrategy,
13 },
14 exercise_task_regrading_submissions::ExerciseTaskRegradingSubmission,
15 exercise_task_submissions::{
16 self, AnswerFields, AnswerFile, AnswerKind, ExerciseTaskSubmission,
17 },
18 exercise_tasks::{self, CourseMaterialExerciseTask, ExerciseTask},
19 exercises::{self, Exercise, ExerciseStatus, GradingProgress},
20 flagged_answers::{self, FlaggedAnswer},
21 peer_or_self_review_configs::PeerReviewProcessingStrategy,
22 peer_or_self_review_question_submissions::{
23 self, PeerOrSelfReviewQuestionSubmission, PeerReviewWithQuestionsAndAnswers,
24 },
25 prelude::*,
26 regradings,
27 user_course_exercise_service_variables::UserCourseExerciseServiceVariable,
28 user_exercise_slide_states::{self, UserExerciseSlideState},
29 user_exercise_states::{self, ExerciseWithUserState, UserExerciseState},
30 user_exercise_task_states,
31};
32
33use super::user_exercise_state_updater;
34
35#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
37
38pub struct StudentExerciseSlideSubmission {
39 pub exercise_slide_id: Uuid,
40 pub exercise_task_submissions: Vec<StudentExerciseTaskSubmission>,
41}
42
43#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
44
45pub struct StudentExerciseSlideSubmissionResult {
46 pub exercise_status: Option<ExerciseStatus>,
47 pub exercise_task_submission_results: Vec<StudentExerciseTaskSubmissionResult>,
48 pub user_course_instance_exercise_service_variables: Vec<UserCourseExerciseServiceVariable>,
49}
50
51impl StudentExerciseSlideSubmissionResult {
52 pub fn clear_grading_information(&mut self) {
53 self.exercise_status = None;
54 self.exercise_task_submission_results
55 .iter_mut()
56 .for_each(|result| {
57 result.grading = None;
58 result.model_solution_spec = None;
59 });
60 }
61
62 pub fn clear_model_solution_specs(&mut self) {
63 self.exercise_task_submission_results
64 .iter_mut()
65 .for_each(|result| {
66 result.model_solution_spec = None;
67 })
68 }
69}
70
71#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
72
73pub struct StudentExerciseTaskSubmission {
74 pub exercise_task_id: Uuid,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub answer_kind: Option<AnswerKind>,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub data_json: Option<serde_json::Value>,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub data_files: Option<Vec<Uuid>>,
86}
87
88impl StudentExerciseTaskSubmission {
89 pub fn json(exercise_task_id: Uuid, data: serde_json::Value) -> Self {
91 Self {
92 exercise_task_id,
93 answer_kind: Some(AnswerKind::Json),
94 data_json: Some(data),
95 data_files: None,
96 }
97 }
98
99 pub fn files(
101 exercise_task_id: Uuid,
102 data_files: Vec<Uuid>,
103 data_json: Option<serde_json::Value>,
104 ) -> Self {
105 Self {
106 exercise_task_id,
107 answer_kind: Some(AnswerKind::File),
108 data_json,
109 data_files: Some(data_files),
110 }
111 }
112
113 pub fn named_file_ids(&self) -> &[Uuid] {
115 match self.answer_kind {
116 Some(AnswerKind::File) => self.data_files.as_deref().unwrap_or_default(),
117 _ => &[],
118 }
119 }
120
121 pub fn to_submitted_answer(&self) -> ModelResult<SubmittedAnswer> {
124 match self.answer_kind.unwrap_or(AnswerKind::Json) {
125 AnswerKind::Json => {
126 if self.data_files.as_ref().is_some_and(|ids| !ids.is_empty()) {
127 return Err(model_err!(
128 InvalidRequest,
129 "A json answer cannot name uploaded files. Send answer_kind 'file' to submit files.".to_string()
130 ));
131 }
132 Ok(SubmittedAnswer::Json {
133 data: self.data_json.clone().unwrap_or(serde_json::Value::Null),
134 })
135 }
136 AnswerKind::File => Ok(SubmittedAnswer::File {
137 file_upload_ids: self.data_files.clone().unwrap_or_default(),
138 metadata: self.data_json.clone(),
139 }),
140 }
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150#[serde(tag = "kind", rename_all = "snake_case")]
151pub enum SubmittedAnswer {
152 Json {
153 data: serde_json::Value,
154 },
155 File {
156 file_upload_ids: Vec<Uuid>,
159 metadata: Option<serde_json::Value>,
161 },
162}
163
164#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
165
166pub struct StudentExerciseTaskSubmissionResult {
167 pub submission: ExerciseTaskSubmission,
168 pub grading: Option<ExerciseTaskGrading>,
169 pub model_solution_spec: Option<serde_json::Value>,
170 pub exercise_task_exercise_service_slug: String,
171}
172
173#[derive(Debug)]
174pub struct ExerciseSlideSubmissionWithTasks {
175 pub exercise_slide_submission: ExerciseSlideSubmission,
176 pub exercise_slide_submission_tasks: Vec<ExerciseTaskSubmission>,
177}
178
179#[derive(Debug)]
181pub struct ExerciseStateUpdateNeedToUpdatePeerReviewStatusWithThis {
182 pub given_enough_peer_reviews: bool,
183 pub received_enough_peer_reviews: bool,
184 pub peer_review_processing_strategy: PeerReviewProcessingStrategy,
185 pub peer_review_accepting_threshold: f32,
186 pub received_peer_or_self_review_question_submissions: Vec<PeerOrSelfReviewQuestionSubmission>,
188}
189
190pub async fn create_user_exercise_slide_submission(
193 conn: &mut PgConnection,
194 exercise_with_user_state: &ExerciseWithUserState,
195 user_exercise_slide_submission: &StudentExerciseSlideSubmission,
196 file_store: &dyn FileStore,
197 app_conf: &ApplicationConfiguration,
198) -> ModelResult<ExerciseSlideSubmissionWithTasks> {
199 let selected_exercise_slide_id = exercise_with_user_state
200 .user_exercise_state()
201 .selected_exercise_slide_id
202 .ok_or_else(|| {
203 ModelError::new(
204 ModelErrorType::PreconditionFailed,
205 "Exercise slide not selected for the student.".to_string(),
206 None,
207 )
208 })?;
209 let exercise_tasks: HashMap<Uuid, ExerciseTask> =
210 exercise_tasks::get_exercise_tasks_by_exercise_slide_id(conn, &selected_exercise_slide_id)
211 .await?;
212 let user_points_update_strategy = if exercise_with_user_state.is_exam_exercise() {
213 UserPointsUpdateStrategy::CanAddPointsAndCanRemovePoints
214 } else {
215 UserPointsUpdateStrategy::CanAddPointsButCannotRemovePoints
216 };
217
218 let mut tx = conn.begin().await?;
219
220 let exercise_slide_submission = exercise_slide_submissions::insert_exercise_slide_submission(
221 &mut tx,
222 NewExerciseSlideSubmission {
223 exercise_slide_id: selected_exercise_slide_id,
224 course_id: exercise_with_user_state.exercise().course_id,
225 exam_id: exercise_with_user_state.exercise().exam_id,
226 exercise_id: exercise_with_user_state.exercise().id,
227 user_id: exercise_with_user_state.user_exercise_state().user_id,
228 user_points_update_strategy,
229 },
230 )
231 .await?;
232 let user_exercise_task_submissions = &user_exercise_slide_submission.exercise_task_submissions;
233 let mut exercise_slide_submission_tasks =
234 Vec::with_capacity(user_exercise_task_submissions.len());
235 for task_submission in user_exercise_task_submissions {
236 let exercise_task = exercise_tasks
237 .get(&task_submission.exercise_task_id)
238 .ok_or_else(|| {
239 ModelError::new(
240 ModelErrorType::PreconditionFailed,
241 "Attempting to submit exercise for illegal exercise_task_id.".to_string(),
242 None,
243 )
244 })?;
245 let submission_id = exercise_task_submissions::insert(
246 &mut tx,
247 PKeyPolicy::Generate,
248 exercise_slide_submission.id,
249 exercise_task.exercise_slide_id,
250 exercise_task.id,
251 &task_submission.to_submitted_answer()?,
252 )
253 .await?;
254 let submission =
255 exercise_task_submissions::get_by_id(&mut tx, submission_id, file_store, app_conf)
256 .await?;
257 exercise_slide_submission_tasks.push(submission)
258 }
259
260 tx.commit().await?;
261 Ok(ExerciseSlideSubmissionWithTasks {
262 exercise_slide_submission,
263 exercise_slide_submission_tasks,
264 })
265}
266
267pub async fn update_grading_with_single_regrading_result(
270 conn: &mut PgConnection,
271 exercise: &Exercise,
272 regrading_submission: &ExerciseTaskRegradingSubmission,
273 exercise_task_grading: &ExerciseTaskGrading,
274 exercise_task_grading_result: &ExerciseTaskGradingResult,
275 file_store: &dyn FileStore,
276 app_conf: &ApplicationConfiguration,
277) -> ModelResult<()> {
278 let task_submission = exercise_task_submissions::get_by_id(
279 &mut *conn,
280 regrading_submission.exercise_task_submission_id,
281 file_store,
282 app_conf,
283 )
284 .await?;
285 let slide_submission = exercise_slide_submissions::get_by_id(
286 &mut *conn,
287 task_submission.exercise_slide_submission_id,
288 )
289 .await?;
290 let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
291 conn,
292 slide_submission.user_id,
293 exercise.id,
294 slide_submission.course_id,
295 slide_submission.exam_id,
296 )
297 .await?;
298 let user_exercise_slide_state = user_exercise_slide_states::get_or_insert_by_unique_index(
299 &mut *conn,
300 user_exercise_state.id,
301 slide_submission.exercise_slide_id,
302 )
303 .await?;
304 let regrading = regradings::get_by_id(&mut *conn, regrading_submission.regrading_id).await?;
305 propagate_user_exercise_state_update_from_exercise_task_grading_result(
306 conn,
307 exercise,
308 exercise_task_grading,
309 exercise_task_grading_result,
310 user_exercise_slide_state,
311 regrading.user_points_update_strategy,
312 )
313 .await?;
314 Ok(())
315}
316
317pub enum GradingPolicy {
318 Default,
320 Fixed(HashMap<Uuid, ExerciseTaskGradingResult>),
322}
323
324#[allow(clippy::too_many_arguments)]
325pub async fn grade_user_submission(
326 conn: &mut PgConnection,
327 exercise_with_user_state: &mut ExerciseWithUserState,
328 user_exercise_slide_submission: &StudentExerciseSlideSubmission,
329 grading_policy: GradingPolicy,
330 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
331 send_grading_request: impl Fn(
332 Url,
333 &ExerciseTask,
334 &ExerciseTaskSubmission,
335 ) -> BoxFuture<'static, ModelResult<ExerciseTaskGradingResult>>,
336 file_store: &dyn FileStore,
337 app_conf: &ApplicationConfiguration,
338) -> ModelResult<StudentExerciseSlideSubmissionResult> {
339 let mut tx = conn.begin().await?;
340
341 let ExerciseSlideSubmissionWithTasks {
342 exercise_slide_submission,
343 exercise_slide_submission_tasks,
344 } = create_user_exercise_slide_submission(
345 &mut tx,
346 exercise_with_user_state,
347 user_exercise_slide_submission,
348 file_store,
349 app_conf,
350 )
351 .await?;
352 let user_exercise_state = exercise_with_user_state.user_exercise_state();
353 let user_exercise_slide_state = user_exercise_slide_states::get_or_insert_by_unique_index(
354 &mut tx,
355 user_exercise_state.id,
356 exercise_slide_submission.exercise_slide_id,
357 )
358 .await?;
359 let results = match grading_policy {
360 GradingPolicy::Default => {
361 let mut results = Vec::with_capacity(exercise_slide_submission_tasks.len());
362 for task_submission in exercise_slide_submission_tasks {
363 let submission = grade_user_submission_task(
364 &mut tx,
365 &task_submission,
366 exercise_with_user_state.exercise(),
367 user_exercise_slide_state.id,
368 user_exercise_state,
369 &fetch_service_info,
370 &send_grading_request,
371 file_store,
372 app_conf,
373 )
374 .await;
375 match submission {
376 Ok(submission) => results.push(submission),
377 Err(err) => {
378 let (http_status_code, error_message, response_body) =
380 match err.error_type() {
381 crate::error::ModelErrorType::HttpRequest {
382 status_code,
383 response_body,
384 } => (
385 Some(*status_code as i32),
386 Some(response_body.clone()),
387 Some(response_body.clone()),
388 ),
389 crate::error::ModelErrorType::HttpError {
390 error_type: crate::error::HttpErrorType::ResponseDecodeFailed,
391 response_body,
392 status_code,
393 ..
394 } => (
395 status_code.map(|s| s as i32),
396 Some(err.to_string()),
397 response_body.clone(),
398 ),
399 _ => (None, Some(err.to_string()), None),
400 };
401
402 tx.rollback().await?;
405 let mut tx = conn.begin().await?;
406
407 let _ = crate::rejected_exercise_slide_submissions::insert_rejected_exercise_slide_submission(
408 &mut tx,
409 user_exercise_slide_submission,
410 user_exercise_state.user_id,
411 http_status_code,
412 error_message,
413 response_body,
414 ).await;
415
416 tx.commit().await?;
417
418 return Err(err);
419 }
420 }
421 }
422 results
423 }
424 GradingPolicy::Fixed(fixed_results) => {
425 let mut results = Vec::with_capacity(exercise_slide_submission_tasks.len());
426 for task_submission in exercise_slide_submission_tasks {
427 let fixed_result = fixed_results
428 .get(&task_submission.exercise_task_id)
429 .ok_or_else(|| {
430 ModelError::new(
431 ModelErrorType::Generic,
432 "Could not find fixed test result for testing".to_string(),
433 None,
434 )
435 })?
436 .clone();
437 let submission = create_fixed_grading_for_submission_task(
438 &mut tx,
439 &task_submission,
440 exercise_with_user_state.exercise(),
441 user_exercise_slide_state.id,
442 &fixed_result,
443 file_store,
444 app_conf,
445 )
446 .await?;
447 results.push(submission);
448 }
449 results
450 }
451 };
452 let user_exercise_state = update_user_exercise_slide_state_and_user_exercise_state(
453 &mut tx,
454 user_exercise_slide_state,
455 exercise_slide_submission.user_points_update_strategy,
456 )
457 .await?;
458
459 let course_or_exam_id = CourseOrExamId::from_course_and_exam_ids(
460 user_exercise_state.course_id,
461 user_exercise_state.exam_id,
462 )?;
463
464 let user_course_instance_exercise_service_variables = crate::user_course_exercise_service_variables::get_all_variables_for_user_and_course_or_exam(&mut tx, user_exercise_state.user_id, course_or_exam_id).await?;
465
466 let result = StudentExerciseSlideSubmissionResult {
467 exercise_status: Some(ExerciseStatus {
468 score_given: user_exercise_state.score_given,
469 activity_progress: user_exercise_state.activity_progress,
470 grading_progress: user_exercise_state.grading_progress,
471 reviewing_stage: user_exercise_state.reviewing_stage,
472 }),
473 exercise_task_submission_results: results,
474 user_course_instance_exercise_service_variables,
475 };
476 exercise_with_user_state.set_user_exercise_state(user_exercise_state)?;
477 tx.commit().await?;
478 Ok(result)
479}
480
481#[allow(clippy::too_many_arguments)]
482async fn grade_user_submission_task(
483 conn: &mut PgConnection,
484 submission: &ExerciseTaskSubmission,
485 exercise: &Exercise,
486 user_exercise_slide_state_id: Uuid,
487 user_exercise_state: &UserExerciseState,
488 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
489 send_grading_request: impl Fn(
490 Url,
491 &ExerciseTask,
492 &ExerciseTaskSubmission,
493 ) -> BoxFuture<'static, ModelResult<ExerciseTaskGradingResult>>,
494 file_store: &dyn FileStore,
495 app_conf: &ApplicationConfiguration,
496) -> ModelResult<StudentExerciseTaskSubmissionResult> {
497 let grading = exercise_task_gradings::new_grading(conn, exercise, submission).await?;
498 exercise_task_submissions::set_grading_id(conn, grading.id, submission.id).await?;
499 let updated_submission =
500 exercise_task_submissions::get_by_id(conn, submission.id, file_store, app_conf).await?;
501 let exercise_task =
502 exercise_tasks::get_exercise_task_by_id(conn, submission.exercise_task_id).await?;
503 let grading = exercise_task_gradings::grade_submission(
504 conn,
505 submission,
506 &exercise_task,
507 exercise,
508 &grading,
509 user_exercise_state,
510 fetch_service_info,
511 send_grading_request,
512 )
513 .await?;
514 user_exercise_task_states::upsert_with_grading(conn, user_exercise_slide_state_id, &grading)
515 .await?;
516 let model_solution_spec = exercise_tasks::get_exercise_task_model_solution_spec_by_id(
517 conn,
518 submission.exercise_task_id,
519 )
520 .await?;
521
522 Ok(StudentExerciseTaskSubmissionResult {
523 submission: updated_submission,
524 grading: Some(grading),
525 model_solution_spec,
526 exercise_task_exercise_service_slug: exercise_task.exercise_type,
527 })
528}
529
530async fn create_fixed_grading_for_submission_task(
531 conn: &mut PgConnection,
532 submission: &ExerciseTaskSubmission,
533 exercise: &Exercise,
534 user_exercise_slide_state_id: Uuid,
535 fixed_result: &ExerciseTaskGradingResult,
536 file_store: &dyn FileStore,
537 app_conf: &ApplicationConfiguration,
538) -> ModelResult<StudentExerciseTaskSubmissionResult> {
539 let grading = exercise_task_gradings::new_grading(conn, exercise, submission).await?;
540 exercise_task_submissions::set_grading_id(conn, grading.id, submission.id).await?;
541 let updated_submission =
542 exercise_task_submissions::get_by_id(conn, submission.id, file_store, app_conf).await?;
543 let updated_grading =
544 exercise_task_gradings::update_grading(conn, &grading, fixed_result, exercise).await?;
545 user_exercise_task_states::upsert_with_grading(
546 conn,
547 user_exercise_slide_state_id,
548 &updated_grading,
549 )
550 .await?;
551 let exercise_task =
552 exercise_tasks::get_exercise_task_by_id(conn, submission.exercise_task_id).await?;
553 let model_solution_spec = exercise_task.model_solution_spec;
554
555 Ok(StudentExerciseTaskSubmissionResult {
556 submission: updated_submission,
557 grading: Some(grading),
558 model_solution_spec,
559 exercise_task_exercise_service_slug: exercise_task.exercise_type,
560 })
561}
562
563async fn update_user_exercise_slide_state_and_user_exercise_state(
566 conn: &mut PgConnection,
567 user_exercise_slide_state: UserExerciseSlideState,
568 user_points_update_strategy: UserPointsUpdateStrategy,
569) -> ModelResult<UserExerciseState> {
570 update_user_exercise_slide_state(
571 conn,
572 &user_exercise_slide_state,
573 user_points_update_strategy,
574 )
575 .await?;
576 let user_exercise_state = user_exercise_state_updater::update_user_exercise_state(
577 conn,
578 user_exercise_slide_state.user_exercise_state_id,
579 )
580 .await?;
581
582 Ok(user_exercise_state)
583}
584
585async fn update_user_exercise_slide_state(
586 conn: &mut PgConnection,
587 user_exercise_slide_state: &UserExerciseSlideState,
588 user_points_update_strategy: UserPointsUpdateStrategy,
589) -> ModelResult<()> {
590 let (points_from_tasks, grading_progress) =
591 user_exercise_task_states::get_grading_summary_by_user_exercise_slide_state_id(
592 conn,
593 user_exercise_slide_state.id,
594 )
595 .await?;
596 let new_score_given = user_exercise_task_states::figure_out_new_score_given(
597 user_exercise_slide_state.score_given,
598 points_from_tasks,
599 user_points_update_strategy,
600 );
601 let changes = user_exercise_slide_states::update(
602 conn,
603 user_exercise_slide_state.id,
604 new_score_given,
605 grading_progress,
606 )
607 .await?;
608 info!(
609 "Updating user exercise slide state {} affected {} rows.",
610 user_exercise_slide_state.id, changes
611 );
612 Ok(())
613}
614
615pub async fn propagate_user_exercise_state_update_from_exercise_task_grading_result(
618 conn: &mut PgConnection,
619 exercise: &Exercise,
620 exercise_task_grading: &ExerciseTaskGrading,
621 exercise_task_grading_result: &ExerciseTaskGradingResult,
622 user_exercise_slide_state: UserExerciseSlideState,
623 user_points_update_strategy: UserPointsUpdateStrategy,
624) -> ModelResult<UserExerciseState> {
625 let updated_exercise_task_grading = exercise_task_gradings::update_grading(
626 conn,
627 exercise_task_grading,
628 exercise_task_grading_result,
629 exercise,
630 )
631 .await?;
632 exercise_task_submissions::set_grading_id(
633 conn,
634 updated_exercise_task_grading.id,
635 updated_exercise_task_grading.exercise_task_submission_id,
636 )
637 .await?;
638 let user_exercise_task_state = user_exercise_task_states::upsert_with_grading(
639 conn,
640 user_exercise_slide_state.id,
641 &updated_exercise_task_grading,
642 )
643 .await?;
644 let user_exercise_slide_state = user_exercise_slide_states::get_by_id(
645 conn,
646 user_exercise_task_state.user_exercise_slide_state_id,
647 )
648 .await?;
649 let user_exercise_state = update_user_exercise_slide_state_and_user_exercise_state(
650 conn,
651 user_exercise_slide_state,
652 user_points_update_strategy,
653 )
654 .await?;
655 Ok(user_exercise_state)
656}
657
658#[derive(Debug, Serialize, ToSchema)]
659
660pub struct AnswersRequiringAttention {
661 pub exercise_max_points: i32,
662 pub data: Vec<AnswerRequiringAttentionWithTasks>,
663 pub total_pages: u32,
664}
665
666#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
667
668pub struct AnswerRequiringAttentionWithTasks {
669 pub id: Uuid,
670 pub user_id: Uuid,
671 pub created_at: DateTime<Utc>,
672 pub updated_at: DateTime<Utc>,
673 pub deleted_at: Option<DateTime<Utc>>,
674 pub answer_kind: AnswerKind,
675 pub data_json: Option<serde_json::Value>,
678 #[serde(skip_serializing_if = "Option::is_none")]
680 pub data_files: Option<Vec<AnswerFile>>,
681 pub grading_progress: GradingProgress,
682 pub score_given: Option<f32>,
683 pub submission_id: Uuid,
684 pub exercise_id: Uuid,
685 pub tasks: Vec<CourseMaterialExerciseTask>,
686 pub given_peer_reviews: Vec<PeerReviewWithQuestionsAndAnswers>,
687 pub received_peer_or_self_reviews: Vec<PeerReviewWithQuestionsAndAnswers>,
688 pub received_peer_review_flagging_reports: Vec<FlaggedAnswer>,
689}
690
691pub async fn get_paginated_answers_requiring_attention_for_exercise(
693 conn: &mut PgConnection,
694 exercise_id: Uuid,
695 pagination: Pagination,
696 viewer_user_id: Uuid,
697 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
698 file_store: &dyn FileStore,
699 app_conf: &ApplicationConfiguration,
700) -> ModelResult<AnswersRequiringAttention> {
701 let exercise = exercises::get_exercise_by_id(conn, exercise_id).await?;
702 let answer_requiring_attention_count =
703 exercise_slide_submissions::answer_requiring_attention_count(conn, exercise_id).await?;
704 let data = exercise_slide_submissions::get_all_answers_requiring_attention(
705 conn,
706 exercise.id,
707 pagination,
708 file_store,
709 app_conf,
710 )
711 .await?;
712 let mut answers = Vec::with_capacity(data.len());
713 for answer in &data {
714 let tasks = exercise_task_submissions::get_exercise_task_submission_info_by_exercise_slide_submission_id(
715 conn,
716 answer.submission_id,
717 viewer_user_id,
718 &fetch_service_info,
719 false,
720 file_store,
721 app_conf,
722 )
723 .await?;
724 let given_peer_reviews = peer_or_self_review_question_submissions::get_given_peer_reviews(
725 conn,
726 answer.user_id,
727 answer.exercise_id,
728 )
729 .await?;
730
731 let received_peer_or_self_reviews =
732 peer_or_self_review_question_submissions::get_questions_and_answers_by_submission_id(
733 conn,
734 answer.submission_id,
735 )
736 .await?;
737 let received_peer_review_flagging_reports: Vec<FlaggedAnswer> =
738 flagged_answers::get_flagged_answers_by_submission_id(conn, answer.submission_id)
739 .await?;
740 let answer_fields = AnswerFields::from(answer.answer.to_owned());
741 let new_answer = AnswerRequiringAttentionWithTasks {
742 id: answer.id,
743 user_id: answer.user_id,
744 created_at: answer.created_at,
745 updated_at: answer.updated_at,
746 deleted_at: answer.deleted_at,
747 answer_kind: answer_fields.answer_kind,
748 data_json: answer_fields.data_json,
749 data_files: answer_fields.data_files,
750 grading_progress: answer.grading_progress,
751 score_given: answer.score_given,
752 submission_id: answer.submission_id,
753 exercise_id: answer.exercise_id,
754 tasks,
755 given_peer_reviews,
756 received_peer_or_self_reviews,
757 received_peer_review_flagging_reports,
758 };
759 answers.push(new_answer);
760 }
761 Ok(AnswersRequiringAttention {
762 exercise_max_points: exercise.score_maximum,
763 data: answers,
764 total_pages: pagination.total_pages(answer_requiring_attention_count),
765 })
766}