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