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 regrading = regradings::get_by_id(&mut *conn, regrading_submission.regrading_id).await?;
291 apply_grading_result_to_user_state(
292 conn,
293 exercise,
294 &slide_submission,
295 exercise_task_grading,
296 exercise_task_grading_result,
297 regrading.user_points_update_strategy,
298 )
299 .await?;
300 Ok(())
301}
302
303pub async fn apply_grading_update(
309 conn: &mut PgConnection,
310 exercise: &Exercise,
311 task_submission: &ExerciseTaskSubmission,
312 exercise_task_grading: &ExerciseTaskGrading,
313 exercise_task_grading_result: &ExerciseTaskGradingResult,
314) -> ModelResult<UserExerciseState> {
315 let slide_submission = exercise_slide_submissions::get_by_id(
316 &mut *conn,
317 task_submission.exercise_slide_submission_id,
318 )
319 .await?;
320 apply_grading_result_to_user_state(
321 conn,
322 exercise,
323 &slide_submission,
324 exercise_task_grading,
325 exercise_task_grading_result,
326 slide_submission.user_points_update_strategy,
327 )
328 .await
329}
330
331async fn apply_grading_result_to_user_state(
332 conn: &mut PgConnection,
333 exercise: &Exercise,
334 slide_submission: &ExerciseSlideSubmission,
335 exercise_task_grading: &ExerciseTaskGrading,
336 exercise_task_grading_result: &ExerciseTaskGradingResult,
337 user_points_update_strategy: UserPointsUpdateStrategy,
338) -> ModelResult<UserExerciseState> {
339 let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
340 conn,
341 slide_submission.user_id,
342 exercise.id,
343 slide_submission.course_id,
344 slide_submission.exam_id,
345 )
346 .await?;
347 let user_exercise_slide_state = user_exercise_slide_states::get_or_insert_by_unique_index(
348 &mut *conn,
349 user_exercise_state.id,
350 slide_submission.exercise_slide_id,
351 )
352 .await?;
353 propagate_user_exercise_state_update_from_exercise_task_grading_result(
354 conn,
355 exercise,
356 exercise_task_grading,
357 exercise_task_grading_result,
358 user_exercise_slide_state,
359 user_points_update_strategy,
360 )
361 .await
362}
363
364pub enum GradingPolicy {
365 Default,
367 Fixed(HashMap<Uuid, ExerciseTaskGradingResult>),
369}
370
371#[allow(clippy::too_many_arguments)]
372pub async fn grade_user_submission(
373 conn: &mut PgConnection,
374 exercise_with_user_state: &mut ExerciseWithUserState,
375 user_exercise_slide_submission: &StudentExerciseSlideSubmission,
376 grading_policy: GradingPolicy,
377 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
378 send_grading_request: impl Fn(
379 Url,
380 &ExerciseTask,
381 &ExerciseTaskSubmission,
382 ) -> BoxFuture<'static, ModelResult<ExerciseTaskGradingResult>>,
383 file_store: &dyn FileStore,
384 app_conf: &ApplicationConfiguration,
385) -> ModelResult<StudentExerciseSlideSubmissionResult> {
386 let mut tx = conn.begin().await?;
387
388 let ExerciseSlideSubmissionWithTasks {
389 exercise_slide_submission,
390 exercise_slide_submission_tasks,
391 } = create_user_exercise_slide_submission(
392 &mut tx,
393 exercise_with_user_state,
394 user_exercise_slide_submission,
395 file_store,
396 app_conf,
397 )
398 .await?;
399 let user_exercise_state = exercise_with_user_state.user_exercise_state();
400 let user_exercise_slide_state = user_exercise_slide_states::get_or_insert_by_unique_index(
401 &mut tx,
402 user_exercise_state.id,
403 exercise_slide_submission.exercise_slide_id,
404 )
405 .await?;
406 let results = match grading_policy {
407 GradingPolicy::Default => {
408 let mut results = Vec::with_capacity(exercise_slide_submission_tasks.len());
409 for task_submission in exercise_slide_submission_tasks {
410 let submission = grade_user_submission_task(
411 &mut tx,
412 &task_submission,
413 exercise_with_user_state.exercise(),
414 user_exercise_slide_state.id,
415 user_exercise_state,
416 &fetch_service_info,
417 &send_grading_request,
418 file_store,
419 app_conf,
420 )
421 .await;
422 match submission {
423 Ok(submission) => results.push(submission),
424 Err(err) => {
425 let (http_status_code, error_message, response_body) =
427 match err.error_type() {
428 crate::error::ModelErrorType::HttpRequest {
429 status_code,
430 response_body,
431 } => (
432 Some(*status_code as i32),
433 Some(response_body.clone()),
434 Some(response_body.clone()),
435 ),
436 crate::error::ModelErrorType::HttpError {
437 error_type: crate::error::HttpErrorType::ResponseDecodeFailed,
438 response_body,
439 status_code,
440 ..
441 } => (
442 status_code.map(|s| s as i32),
443 Some(err.to_string()),
444 response_body.clone(),
445 ),
446 _ => (None, Some(err.to_string()), None),
447 };
448
449 tx.rollback().await?;
452 let mut tx = conn.begin().await?;
453
454 let _ = crate::rejected_exercise_slide_submissions::insert_rejected_exercise_slide_submission(
455 &mut tx,
456 user_exercise_slide_submission,
457 user_exercise_state.user_id,
458 http_status_code,
459 error_message,
460 response_body,
461 ).await;
462
463 tx.commit().await?;
464
465 return Err(err);
466 }
467 }
468 }
469 results
470 }
471 GradingPolicy::Fixed(fixed_results) => {
472 let mut results = Vec::with_capacity(exercise_slide_submission_tasks.len());
473 for task_submission in exercise_slide_submission_tasks {
474 let fixed_result = fixed_results
475 .get(&task_submission.exercise_task_id)
476 .ok_or_else(|| {
477 ModelError::new(
478 ModelErrorType::Generic,
479 "Could not find fixed test result for testing".to_string(),
480 None,
481 )
482 })?
483 .clone();
484 let submission = create_fixed_grading_for_submission_task(
485 &mut tx,
486 &task_submission,
487 exercise_with_user_state.exercise(),
488 user_exercise_slide_state.id,
489 &fixed_result,
490 file_store,
491 app_conf,
492 )
493 .await?;
494 results.push(submission);
495 }
496 results
497 }
498 };
499 let user_exercise_state = update_user_exercise_slide_state_and_user_exercise_state(
500 &mut tx,
501 user_exercise_slide_state,
502 exercise_slide_submission.user_points_update_strategy,
503 )
504 .await?;
505
506 let course_or_exam_id = CourseOrExamId::from_course_and_exam_ids(
507 user_exercise_state.course_id,
508 user_exercise_state.exam_id,
509 )?;
510
511 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?;
512
513 let result = StudentExerciseSlideSubmissionResult {
514 exercise_status: Some(ExerciseStatus {
515 score_given: user_exercise_state.score_given,
516 activity_progress: user_exercise_state.activity_progress,
517 grading_progress: user_exercise_state.grading_progress,
518 reviewing_stage: user_exercise_state.reviewing_stage,
519 }),
520 exercise_task_submission_results: results,
521 user_course_instance_exercise_service_variables,
522 };
523 exercise_with_user_state.set_user_exercise_state(user_exercise_state)?;
524 tx.commit().await?;
525 Ok(result)
526}
527
528#[allow(clippy::too_many_arguments)]
529async fn grade_user_submission_task(
530 conn: &mut PgConnection,
531 submission: &ExerciseTaskSubmission,
532 exercise: &Exercise,
533 user_exercise_slide_state_id: Uuid,
534 user_exercise_state: &UserExerciseState,
535 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
536 send_grading_request: impl Fn(
537 Url,
538 &ExerciseTask,
539 &ExerciseTaskSubmission,
540 ) -> BoxFuture<'static, ModelResult<ExerciseTaskGradingResult>>,
541 file_store: &dyn FileStore,
542 app_conf: &ApplicationConfiguration,
543) -> ModelResult<StudentExerciseTaskSubmissionResult> {
544 let grading = exercise_task_gradings::new_grading(conn, exercise, submission).await?;
545 exercise_task_submissions::set_grading_id(conn, grading.id, submission.id).await?;
546 let updated_submission =
547 exercise_task_submissions::get_by_id(conn, submission.id, file_store, app_conf).await?;
548 let exercise_task =
549 exercise_tasks::get_exercise_task_by_id(conn, submission.exercise_task_id).await?;
550 let grading = exercise_task_gradings::grade_submission(
551 conn,
552 submission,
553 &exercise_task,
554 exercise,
555 &grading,
556 user_exercise_state,
557 fetch_service_info,
558 send_grading_request,
559 )
560 .await?;
561 user_exercise_task_states::upsert_with_grading(conn, user_exercise_slide_state_id, &grading)
562 .await?;
563 let model_solution_spec = exercise_tasks::get_exercise_task_model_solution_spec_by_id(
564 conn,
565 submission.exercise_task_id,
566 )
567 .await?;
568
569 Ok(StudentExerciseTaskSubmissionResult {
570 submission: updated_submission,
571 grading: Some(grading),
572 model_solution_spec,
573 exercise_task_exercise_service_slug: exercise_task.exercise_type,
574 })
575}
576
577async fn create_fixed_grading_for_submission_task(
578 conn: &mut PgConnection,
579 submission: &ExerciseTaskSubmission,
580 exercise: &Exercise,
581 user_exercise_slide_state_id: Uuid,
582 fixed_result: &ExerciseTaskGradingResult,
583 file_store: &dyn FileStore,
584 app_conf: &ApplicationConfiguration,
585) -> ModelResult<StudentExerciseTaskSubmissionResult> {
586 let grading = exercise_task_gradings::new_grading(conn, exercise, submission).await?;
587 exercise_task_submissions::set_grading_id(conn, grading.id, submission.id).await?;
588 let updated_submission =
589 exercise_task_submissions::get_by_id(conn, submission.id, file_store, app_conf).await?;
590 let updated_grading =
591 exercise_task_gradings::update_grading(conn, &grading, fixed_result, exercise).await?;
592 user_exercise_task_states::upsert_with_grading(
593 conn,
594 user_exercise_slide_state_id,
595 &updated_grading,
596 )
597 .await?;
598 let exercise_task =
599 exercise_tasks::get_exercise_task_by_id(conn, submission.exercise_task_id).await?;
600 let model_solution_spec = exercise_task.model_solution_spec;
601
602 Ok(StudentExerciseTaskSubmissionResult {
603 submission: updated_submission,
604 grading: Some(grading),
605 model_solution_spec,
606 exercise_task_exercise_service_slug: exercise_task.exercise_type,
607 })
608}
609
610async fn update_user_exercise_slide_state_and_user_exercise_state(
613 conn: &mut PgConnection,
614 user_exercise_slide_state: UserExerciseSlideState,
615 user_points_update_strategy: UserPointsUpdateStrategy,
616) -> ModelResult<UserExerciseState> {
617 update_user_exercise_slide_state(
618 conn,
619 &user_exercise_slide_state,
620 user_points_update_strategy,
621 )
622 .await?;
623 let user_exercise_state = user_exercise_state_updater::update_user_exercise_state(
624 conn,
625 user_exercise_slide_state.user_exercise_state_id,
626 )
627 .await?;
628
629 Ok(user_exercise_state)
630}
631
632async fn update_user_exercise_slide_state(
633 conn: &mut PgConnection,
634 user_exercise_slide_state: &UserExerciseSlideState,
635 user_points_update_strategy: UserPointsUpdateStrategy,
636) -> ModelResult<()> {
637 let (points_from_tasks, grading_progress) =
638 user_exercise_task_states::get_grading_summary_by_user_exercise_slide_state_id(
639 conn,
640 user_exercise_slide_state.id,
641 )
642 .await?;
643 let new_score_given = user_exercise_task_states::figure_out_new_score_given(
644 user_exercise_slide_state.score_given,
645 points_from_tasks,
646 user_points_update_strategy,
647 );
648 let changes = user_exercise_slide_states::update(
649 conn,
650 user_exercise_slide_state.id,
651 new_score_given,
652 grading_progress,
653 )
654 .await?;
655 info!(
656 "Updating user exercise slide state {} affected {} rows.",
657 user_exercise_slide_state.id, changes
658 );
659 Ok(())
660}
661
662pub async fn propagate_user_exercise_state_update_from_exercise_task_grading_result(
665 conn: &mut PgConnection,
666 exercise: &Exercise,
667 exercise_task_grading: &ExerciseTaskGrading,
668 exercise_task_grading_result: &ExerciseTaskGradingResult,
669 user_exercise_slide_state: UserExerciseSlideState,
670 user_points_update_strategy: UserPointsUpdateStrategy,
671) -> ModelResult<UserExerciseState> {
672 let updated_exercise_task_grading = exercise_task_gradings::update_grading(
673 conn,
674 exercise_task_grading,
675 exercise_task_grading_result,
676 exercise,
677 )
678 .await?;
679 exercise_task_submissions::set_grading_id(
680 conn,
681 updated_exercise_task_grading.id,
682 updated_exercise_task_grading.exercise_task_submission_id,
683 )
684 .await?;
685 let user_exercise_task_state = user_exercise_task_states::upsert_with_grading(
686 conn,
687 user_exercise_slide_state.id,
688 &updated_exercise_task_grading,
689 )
690 .await?;
691 let user_exercise_slide_state = user_exercise_slide_states::get_by_id(
692 conn,
693 user_exercise_task_state.user_exercise_slide_state_id,
694 )
695 .await?;
696 let user_exercise_state = update_user_exercise_slide_state_and_user_exercise_state(
697 conn,
698 user_exercise_slide_state,
699 user_points_update_strategy,
700 )
701 .await?;
702 Ok(user_exercise_state)
703}
704
705#[derive(Debug, Serialize, ToSchema)]
706
707pub struct AnswersRequiringAttention {
708 pub exercise_max_points: i32,
709 pub data: Vec<AnswerRequiringAttentionWithTasks>,
710 pub total_pages: u32,
711}
712
713#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
714
715pub struct AnswerRequiringAttentionWithTasks {
716 pub id: Uuid,
717 pub user_id: Uuid,
718 pub created_at: DateTime<Utc>,
719 pub updated_at: DateTime<Utc>,
720 pub deleted_at: Option<DateTime<Utc>>,
721 pub answer_kind: AnswerKind,
722 pub data_json: Option<serde_json::Value>,
725 #[serde(skip_serializing_if = "Option::is_none")]
727 pub data_files: Option<Vec<AnswerFile>>,
728 pub grading_progress: GradingProgress,
729 pub score_given: Option<f32>,
730 pub submission_id: Uuid,
731 pub exercise_id: Uuid,
732 pub tasks: Vec<CourseMaterialExerciseTask>,
733 pub given_peer_reviews: Vec<PeerReviewWithQuestionsAndAnswers>,
734 pub received_peer_or_self_reviews: Vec<PeerReviewWithQuestionsAndAnswers>,
735 pub received_peer_review_flagging_reports: Vec<FlaggedAnswer>,
736}
737
738pub async fn get_paginated_answers_requiring_attention_for_exercise(
740 conn: &mut PgConnection,
741 exercise_id: Uuid,
742 pagination: Pagination,
743 viewer_user_id: Uuid,
744 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
745 file_store: &dyn FileStore,
746 app_conf: &ApplicationConfiguration,
747) -> ModelResult<AnswersRequiringAttention> {
748 let exercise = exercises::get_exercise_by_id(conn, exercise_id).await?;
749 let answer_requiring_attention_count =
750 exercise_slide_submissions::answer_requiring_attention_count(conn, exercise_id).await?;
751 let data = exercise_slide_submissions::get_all_answers_requiring_attention(
752 conn,
753 exercise.id,
754 pagination,
755 file_store,
756 app_conf,
757 )
758 .await?;
759 let mut answers = Vec::with_capacity(data.len());
760 for answer in &data {
761 let tasks = exercise_task_submissions::get_exercise_task_submission_info_by_exercise_slide_submission_id(
762 conn,
763 answer.submission_id,
764 viewer_user_id,
765 &fetch_service_info,
766 false,
767 file_store,
768 app_conf,
769 )
770 .await?;
771 let given_peer_reviews = peer_or_self_review_question_submissions::get_given_peer_reviews(
772 conn,
773 answer.user_id,
774 answer.exercise_id,
775 )
776 .await?;
777
778 let received_peer_or_self_reviews =
779 peer_or_self_review_question_submissions::get_questions_and_answers_by_submission_id(
780 conn,
781 answer.submission_id,
782 )
783 .await?;
784 let received_peer_review_flagging_reports: Vec<FlaggedAnswer> =
785 flagged_answers::get_flagged_answers_by_submission_id(conn, answer.submission_id)
786 .await?;
787 let answer_fields = AnswerFields::from(answer.answer.to_owned());
788 let new_answer = AnswerRequiringAttentionWithTasks {
789 id: answer.id,
790 user_id: answer.user_id,
791 created_at: answer.created_at,
792 updated_at: answer.updated_at,
793 deleted_at: answer.deleted_at,
794 answer_kind: answer_fields.answer_kind,
795 data_json: answer_fields.data_json,
796 data_files: answer_fields.data_files,
797 grading_progress: answer.grading_progress,
798 score_given: answer.score_given,
799 submission_id: answer.submission_id,
800 exercise_id: answer.exercise_id,
801 tasks,
802 given_peer_reviews,
803 received_peer_or_self_reviews,
804 received_peer_review_flagging_reports,
805 };
806 answers.push(new_answer);
807 }
808 Ok(AnswersRequiringAttention {
809 exercise_max_points: exercise.score_maximum,
810 data: answers,
811 total_pages: pagination.total_pages(answer_requiring_attention_count),
812 })
813}