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