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::{self, ExerciseTaskSubmission},
16 exercise_tasks::{self, CourseMaterialExerciseTask, ExerciseTask},
17 exercises::{self, Exercise, ExerciseStatus, GradingProgress},
18 flagged_answers::{self, FlaggedAnswer},
19 peer_or_self_review_configs::PeerReviewProcessingStrategy,
20 peer_or_self_review_question_submissions::{
21 self, PeerOrSelfReviewQuestionSubmission, PeerReviewWithQuestionsAndAnswers,
22 },
23 prelude::*,
24 regradings,
25 user_course_exercise_service_variables::UserCourseExerciseServiceVariable,
26 user_exercise_slide_states::{self, UserExerciseSlideState},
27 user_exercise_states::{self, ExerciseWithUserState, UserExerciseState},
28 user_exercise_task_states,
29};
30
31use super::user_exercise_state_updater;
32
33#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
35
36pub struct StudentExerciseSlideSubmission {
37 pub exercise_slide_id: Uuid,
38 pub exercise_task_submissions: Vec<StudentExerciseTaskSubmission>,
39}
40
41#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
42
43pub struct StudentExerciseSlideSubmissionResult {
44 pub exercise_status: Option<ExerciseStatus>,
45 pub exercise_task_submission_results: Vec<StudentExerciseTaskSubmissionResult>,
46 pub user_course_instance_exercise_service_variables: Vec<UserCourseExerciseServiceVariable>,
47}
48
49impl StudentExerciseSlideSubmissionResult {
50 pub fn clear_grading_information(&mut self) {
51 self.exercise_status = None;
52 self.exercise_task_submission_results
53 .iter_mut()
54 .for_each(|result| {
55 result.grading = None;
56 result.model_solution_spec = None;
57 });
58 }
59
60 pub fn clear_model_solution_specs(&mut self) {
61 self.exercise_task_submission_results
62 .iter_mut()
63 .for_each(|result| {
64 result.model_solution_spec = None;
65 })
66 }
67}
68
69#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
70
71pub struct StudentExerciseTaskSubmission {
72 pub exercise_task_id: Uuid,
73 pub data_json: serde_json::Value,
74}
75
76#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
77
78pub struct StudentExerciseTaskSubmissionResult {
79 pub submission: ExerciseTaskSubmission,
80 pub grading: Option<ExerciseTaskGrading>,
81 pub model_solution_spec: Option<serde_json::Value>,
82 pub exercise_task_exercise_service_slug: String,
83}
84
85#[derive(Debug)]
86pub struct ExerciseSlideSubmissionWithTasks {
87 pub exercise_slide_submission: ExerciseSlideSubmission,
88 pub exercise_slide_submission_tasks: Vec<ExerciseTaskSubmission>,
89}
90
91#[derive(Debug)]
93pub struct ExerciseStateUpdateNeedToUpdatePeerReviewStatusWithThis {
94 pub given_enough_peer_reviews: bool,
95 pub received_enough_peer_reviews: bool,
96 pub peer_review_processing_strategy: PeerReviewProcessingStrategy,
97 pub peer_review_accepting_threshold: f32,
98 pub received_peer_or_self_review_question_submissions: Vec<PeerOrSelfReviewQuestionSubmission>,
100}
101
102pub async fn create_user_exercise_slide_submission(
105 conn: &mut PgConnection,
106 exercise_with_user_state: &ExerciseWithUserState,
107 user_exercise_slide_submission: &StudentExerciseSlideSubmission,
108) -> ModelResult<ExerciseSlideSubmissionWithTasks> {
109 let selected_exercise_slide_id = exercise_with_user_state
110 .user_exercise_state()
111 .selected_exercise_slide_id
112 .ok_or_else(|| {
113 ModelError::new(
114 ModelErrorType::PreconditionFailed,
115 "Exercise slide not selected for the student.".to_string(),
116 None,
117 )
118 })?;
119 let exercise_tasks: HashMap<Uuid, ExerciseTask> =
120 exercise_tasks::get_exercise_tasks_by_exercise_slide_id(conn, &selected_exercise_slide_id)
121 .await?;
122 let user_points_update_strategy = if exercise_with_user_state.is_exam_exercise() {
123 UserPointsUpdateStrategy::CanAddPointsAndCanRemovePoints
124 } else {
125 UserPointsUpdateStrategy::CanAddPointsButCannotRemovePoints
126 };
127
128 let mut tx = conn.begin().await?;
129
130 let exercise_slide_submission = exercise_slide_submissions::insert_exercise_slide_submission(
131 &mut tx,
132 NewExerciseSlideSubmission {
133 exercise_slide_id: selected_exercise_slide_id,
134 course_id: exercise_with_user_state.exercise().course_id,
135 exam_id: exercise_with_user_state.exercise().exam_id,
136 exercise_id: exercise_with_user_state.exercise().id,
137 user_id: exercise_with_user_state.user_exercise_state().user_id,
138 user_points_update_strategy,
139 },
140 )
141 .await?;
142 let user_exercise_task_submissions = &user_exercise_slide_submission.exercise_task_submissions;
143 let mut exercise_slide_submission_tasks =
144 Vec::with_capacity(user_exercise_task_submissions.len());
145 for task_submission in user_exercise_task_submissions {
146 let exercise_task = exercise_tasks
147 .get(&task_submission.exercise_task_id)
148 .ok_or_else(|| {
149 ModelError::new(
150 ModelErrorType::PreconditionFailed,
151 "Attempting to submit exercise for illegal exercise_task_id.".to_string(),
152 None,
153 )
154 })?;
155 let submission_id = exercise_task_submissions::insert(
156 &mut tx,
157 PKeyPolicy::Generate,
158 exercise_slide_submission.id,
159 exercise_task.exercise_slide_id,
160 exercise_task.id,
161 &task_submission.data_json,
162 )
163 .await?;
164 let submission = exercise_task_submissions::get_by_id(&mut tx, submission_id).await?;
165 exercise_slide_submission_tasks.push(submission)
166 }
167
168 tx.commit().await?;
169 Ok(ExerciseSlideSubmissionWithTasks {
170 exercise_slide_submission,
171 exercise_slide_submission_tasks,
172 })
173}
174
175pub async fn update_grading_with_single_regrading_result(
178 conn: &mut PgConnection,
179 exercise: &Exercise,
180 regrading_submission: &ExerciseTaskRegradingSubmission,
181 exercise_task_grading: &ExerciseTaskGrading,
182 exercise_task_grading_result: &ExerciseTaskGradingResult,
183) -> ModelResult<()> {
184 let task_submission = exercise_task_submissions::get_by_id(
185 &mut *conn,
186 regrading_submission.exercise_task_submission_id,
187 )
188 .await?;
189 let slide_submission = exercise_slide_submissions::get_by_id(
190 &mut *conn,
191 task_submission.exercise_slide_submission_id,
192 )
193 .await?;
194 let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
195 conn,
196 slide_submission.user_id,
197 exercise.id,
198 slide_submission.course_id,
199 slide_submission.exam_id,
200 )
201 .await?;
202 let user_exercise_slide_state = user_exercise_slide_states::get_or_insert_by_unique_index(
203 &mut *conn,
204 user_exercise_state.id,
205 slide_submission.exercise_slide_id,
206 )
207 .await?;
208 let regrading = regradings::get_by_id(&mut *conn, regrading_submission.regrading_id).await?;
209 propagate_user_exercise_state_update_from_exercise_task_grading_result(
210 conn,
211 exercise,
212 exercise_task_grading,
213 exercise_task_grading_result,
214 user_exercise_slide_state,
215 regrading.user_points_update_strategy,
216 )
217 .await?;
218 Ok(())
219}
220
221pub enum GradingPolicy {
222 Default,
224 Fixed(HashMap<Uuid, ExerciseTaskGradingResult>),
226}
227
228pub async fn grade_user_submission(
229 conn: &mut PgConnection,
230 exercise_with_user_state: &mut ExerciseWithUserState,
231 user_exercise_slide_submission: &StudentExerciseSlideSubmission,
232 grading_policy: GradingPolicy,
233 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
234 send_grading_request: impl Fn(
235 Url,
236 &ExerciseTask,
237 &ExerciseTaskSubmission,
238 ) -> BoxFuture<'static, ModelResult<ExerciseTaskGradingResult>>,
239) -> ModelResult<StudentExerciseSlideSubmissionResult> {
240 let mut tx = conn.begin().await?;
241
242 let ExerciseSlideSubmissionWithTasks {
243 exercise_slide_submission,
244 exercise_slide_submission_tasks,
245 } = create_user_exercise_slide_submission(
246 &mut tx,
247 exercise_with_user_state,
248 user_exercise_slide_submission,
249 )
250 .await?;
251 let user_exercise_state = exercise_with_user_state.user_exercise_state();
252 let user_exercise_slide_state = user_exercise_slide_states::get_or_insert_by_unique_index(
253 &mut tx,
254 user_exercise_state.id,
255 exercise_slide_submission.exercise_slide_id,
256 )
257 .await?;
258 let results = match grading_policy {
259 GradingPolicy::Default => {
260 let mut results = Vec::with_capacity(exercise_slide_submission_tasks.len());
261 for task_submission in exercise_slide_submission_tasks {
262 let submission = grade_user_submission_task(
263 &mut tx,
264 &task_submission,
265 exercise_with_user_state.exercise(),
266 user_exercise_slide_state.id,
267 user_exercise_state,
268 &fetch_service_info,
269 &send_grading_request,
270 )
271 .await;
272 match submission {
273 Ok(submission) => results.push(submission),
274 Err(err) => {
275 let (http_status_code, error_message, response_body) =
277 match err.error_type() {
278 crate::error::ModelErrorType::HttpRequest {
279 status_code,
280 response_body,
281 } => (
282 Some(*status_code as i32),
283 Some(response_body.clone()),
284 Some(response_body.clone()),
285 ),
286 crate::error::ModelErrorType::HttpError {
287 error_type: crate::error::HttpErrorType::ResponseDecodeFailed,
288 response_body,
289 status_code,
290 ..
291 } => (
292 status_code.map(|s| s as i32),
293 Some(err.to_string()),
294 response_body.clone(),
295 ),
296 _ => (None, Some(err.to_string()), None),
297 };
298
299 tx.rollback().await?;
302 let mut tx = conn.begin().await?;
303
304 let _ = crate::rejected_exercise_slide_submissions::insert_rejected_exercise_slide_submission(
305 &mut tx,
306 user_exercise_slide_submission,
307 user_exercise_state.user_id,
308 http_status_code,
309 error_message,
310 response_body,
311 ).await;
312
313 tx.commit().await?;
314
315 return Err(err);
316 }
317 }
318 }
319 results
320 }
321 GradingPolicy::Fixed(fixed_results) => {
322 let mut results = Vec::with_capacity(exercise_slide_submission_tasks.len());
323 for task_submission in exercise_slide_submission_tasks {
324 let fixed_result = fixed_results
325 .get(&task_submission.exercise_task_id)
326 .ok_or_else(|| {
327 ModelError::new(
328 ModelErrorType::Generic,
329 "Could not find fixed test result for testing".to_string(),
330 None,
331 )
332 })?
333 .clone();
334 let submission = create_fixed_grading_for_submission_task(
335 &mut tx,
336 &task_submission,
337 exercise_with_user_state.exercise(),
338 user_exercise_slide_state.id,
339 &fixed_result,
340 )
341 .await?;
342 results.push(submission);
343 }
344 results
345 }
346 };
347 let user_exercise_state = update_user_exercise_slide_state_and_user_exercise_state(
348 &mut tx,
349 user_exercise_slide_state,
350 exercise_slide_submission.user_points_update_strategy,
351 )
352 .await?;
353
354 let course_or_exam_id = CourseOrExamId::from_course_and_exam_ids(
355 user_exercise_state.course_id,
356 user_exercise_state.exam_id,
357 )?;
358
359 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?;
360
361 let result = StudentExerciseSlideSubmissionResult {
362 exercise_status: Some(ExerciseStatus {
363 score_given: user_exercise_state.score_given,
364 activity_progress: user_exercise_state.activity_progress,
365 grading_progress: user_exercise_state.grading_progress,
366 reviewing_stage: user_exercise_state.reviewing_stage,
367 }),
368 exercise_task_submission_results: results,
369 user_course_instance_exercise_service_variables,
370 };
371 exercise_with_user_state.set_user_exercise_state(user_exercise_state)?;
372 tx.commit().await?;
373 Ok(result)
374}
375
376async fn grade_user_submission_task(
377 conn: &mut PgConnection,
378 submission: &ExerciseTaskSubmission,
379 exercise: &Exercise,
380 user_exercise_slide_state_id: Uuid,
381 user_exercise_state: &UserExerciseState,
382 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
383 send_grading_request: impl Fn(
384 Url,
385 &ExerciseTask,
386 &ExerciseTaskSubmission,
387 ) -> BoxFuture<'static, ModelResult<ExerciseTaskGradingResult>>,
388) -> ModelResult<StudentExerciseTaskSubmissionResult> {
389 let grading = exercise_task_gradings::new_grading(conn, exercise, submission).await?;
390 let updated_submission =
391 exercise_task_submissions::set_grading_id(conn, grading.id, submission.id).await?;
392 let exercise_task =
393 exercise_tasks::get_exercise_task_by_id(conn, submission.exercise_task_id).await?;
394 let grading = exercise_task_gradings::grade_submission(
395 conn,
396 submission,
397 &exercise_task,
398 exercise,
399 &grading,
400 user_exercise_state,
401 fetch_service_info,
402 send_grading_request,
403 )
404 .await?;
405 user_exercise_task_states::upsert_with_grading(conn, user_exercise_slide_state_id, &grading)
406 .await?;
407 let model_solution_spec = exercise_tasks::get_exercise_task_model_solution_spec_by_id(
408 conn,
409 submission.exercise_task_id,
410 )
411 .await?;
412
413 Ok(StudentExerciseTaskSubmissionResult {
414 submission: updated_submission,
415 grading: Some(grading),
416 model_solution_spec,
417 exercise_task_exercise_service_slug: exercise_task.exercise_type,
418 })
419}
420
421async fn create_fixed_grading_for_submission_task(
422 conn: &mut PgConnection,
423 submission: &ExerciseTaskSubmission,
424 exercise: &Exercise,
425 user_exercise_slide_state_id: Uuid,
426 fixed_result: &ExerciseTaskGradingResult,
427) -> ModelResult<StudentExerciseTaskSubmissionResult> {
428 let grading = exercise_task_gradings::new_grading(conn, exercise, submission).await?;
429 let updated_submission =
430 exercise_task_submissions::set_grading_id(conn, grading.id, submission.id).await?;
431 let updated_grading =
432 exercise_task_gradings::update_grading(conn, &grading, fixed_result, exercise).await?;
433 user_exercise_task_states::upsert_with_grading(
434 conn,
435 user_exercise_slide_state_id,
436 &updated_grading,
437 )
438 .await?;
439 let exercise_task =
440 exercise_tasks::get_exercise_task_by_id(conn, submission.exercise_task_id).await?;
441 let model_solution_spec = exercise_task.model_solution_spec;
442
443 Ok(StudentExerciseTaskSubmissionResult {
444 submission: updated_submission,
445 grading: Some(grading),
446 model_solution_spec,
447 exercise_task_exercise_service_slug: exercise_task.exercise_type,
448 })
449}
450
451async fn update_user_exercise_slide_state_and_user_exercise_state(
454 conn: &mut PgConnection,
455 user_exercise_slide_state: UserExerciseSlideState,
456 user_points_update_strategy: UserPointsUpdateStrategy,
457) -> ModelResult<UserExerciseState> {
458 update_user_exercise_slide_state(
459 conn,
460 &user_exercise_slide_state,
461 user_points_update_strategy,
462 )
463 .await?;
464 let user_exercise_state = user_exercise_state_updater::update_user_exercise_state(
465 conn,
466 user_exercise_slide_state.user_exercise_state_id,
467 )
468 .await?;
469
470 Ok(user_exercise_state)
471}
472
473async fn update_user_exercise_slide_state(
474 conn: &mut PgConnection,
475 user_exercise_slide_state: &UserExerciseSlideState,
476 user_points_update_strategy: UserPointsUpdateStrategy,
477) -> ModelResult<()> {
478 let (points_from_tasks, grading_progress) =
479 user_exercise_task_states::get_grading_summary_by_user_exercise_slide_state_id(
480 conn,
481 user_exercise_slide_state.id,
482 )
483 .await?;
484 let new_score_given = user_exercise_task_states::figure_out_new_score_given(
485 user_exercise_slide_state.score_given,
486 points_from_tasks,
487 user_points_update_strategy,
488 );
489 let changes = user_exercise_slide_states::update(
490 conn,
491 user_exercise_slide_state.id,
492 new_score_given,
493 grading_progress,
494 )
495 .await?;
496 info!(
497 "Updating user exercise slide state {} affected {} rows.",
498 user_exercise_slide_state.id, changes
499 );
500 Ok(())
501}
502
503pub async fn propagate_user_exercise_state_update_from_exercise_task_grading_result(
506 conn: &mut PgConnection,
507 exercise: &Exercise,
508 exercise_task_grading: &ExerciseTaskGrading,
509 exercise_task_grading_result: &ExerciseTaskGradingResult,
510 user_exercise_slide_state: UserExerciseSlideState,
511 user_points_update_strategy: UserPointsUpdateStrategy,
512) -> ModelResult<UserExerciseState> {
513 let updated_exercise_task_grading = exercise_task_gradings::update_grading(
514 conn,
515 exercise_task_grading,
516 exercise_task_grading_result,
517 exercise,
518 )
519 .await?;
520 exercise_task_submissions::set_grading_id(
521 conn,
522 updated_exercise_task_grading.id,
523 updated_exercise_task_grading.exercise_task_submission_id,
524 )
525 .await?;
526 let user_exercise_task_state = user_exercise_task_states::upsert_with_grading(
527 conn,
528 user_exercise_slide_state.id,
529 &updated_exercise_task_grading,
530 )
531 .await?;
532 let user_exercise_slide_state = user_exercise_slide_states::get_by_id(
533 conn,
534 user_exercise_task_state.user_exercise_slide_state_id,
535 )
536 .await?;
537 let user_exercise_state = update_user_exercise_slide_state_and_user_exercise_state(
538 conn,
539 user_exercise_slide_state,
540 user_points_update_strategy,
541 )
542 .await?;
543 Ok(user_exercise_state)
544}
545
546#[derive(Debug, Serialize, ToSchema)]
547
548pub struct AnswersRequiringAttention {
549 pub exercise_max_points: i32,
550 pub data: Vec<AnswerRequiringAttentionWithTasks>,
551 pub total_pages: u32,
552}
553
554#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
555
556pub struct AnswerRequiringAttentionWithTasks {
557 pub id: Uuid,
558 pub user_id: Uuid,
559 pub created_at: DateTime<Utc>,
560 pub updated_at: DateTime<Utc>,
561 pub deleted_at: Option<DateTime<Utc>>,
562 pub data_json: Option<serde_json::Value>,
563 pub grading_progress: GradingProgress,
564 pub score_given: Option<f32>,
565 pub submission_id: Uuid,
566 pub exercise_id: Uuid,
567 pub tasks: Vec<CourseMaterialExerciseTask>,
568 pub given_peer_reviews: Vec<PeerReviewWithQuestionsAndAnswers>,
569 pub received_peer_or_self_reviews: Vec<PeerReviewWithQuestionsAndAnswers>,
570 pub received_peer_review_flagging_reports: Vec<FlaggedAnswer>,
571}
572
573pub async fn get_paginated_answers_requiring_attention_for_exercise(
575 conn: &mut PgConnection,
576 exercise_id: Uuid,
577 pagination: Pagination,
578 viewer_user_id: Uuid,
579 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
580) -> ModelResult<AnswersRequiringAttention> {
581 let exercise = exercises::get_exercise_by_id(conn, exercise_id).await?;
582 let answer_requiring_attention_count =
583 exercise_slide_submissions::answer_requiring_attention_count(conn, exercise_id).await?;
584 let data = exercise_slide_submissions::get_all_answers_requiring_attention(
585 conn,
586 exercise.id,
587 pagination,
588 )
589 .await?;
590 let mut answers = Vec::with_capacity(data.len());
591 for answer in &data {
592 let tasks = exercise_task_submissions::get_exercise_task_submission_info_by_exercise_slide_submission_id(
593 conn,
594 answer.submission_id,
595 viewer_user_id,
596 &fetch_service_info,
597 false,
598 )
599 .await?;
600 let given_peer_reviews = peer_or_self_review_question_submissions::get_given_peer_reviews(
601 conn,
602 answer.user_id,
603 answer.exercise_id,
604 )
605 .await?;
606
607 let received_peer_or_self_reviews =
608 peer_or_self_review_question_submissions::get_questions_and_answers_by_submission_id(
609 conn,
610 answer.submission_id,
611 )
612 .await?;
613 let received_peer_review_flagging_reports: Vec<FlaggedAnswer> =
614 flagged_answers::get_flagged_answers_by_submission_id(conn, answer.submission_id)
615 .await?;
616 let new_answer = AnswerRequiringAttentionWithTasks {
617 id: answer.id,
618 user_id: answer.user_id,
619 created_at: answer.created_at,
620 updated_at: answer.updated_at,
621 deleted_at: answer.deleted_at,
622 data_json: answer.data_json.to_owned(),
623 grading_progress: answer.grading_progress,
624 score_given: answer.score_given,
625 submission_id: answer.submission_id,
626 exercise_id: answer.exercise_id,
627 tasks,
628 given_peer_reviews,
629 received_peer_or_self_reviews,
630 received_peer_review_flagging_reports,
631 };
632 answers.push(new_answer);
633 }
634 Ok(AnswersRequiringAttention {
635 exercise_max_points: exercise.score_maximum,
636 data: answers,
637 total_pages: pagination.total_pages(answer_requiring_attention_count),
638 })
639}