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 results.push(submission);
272 }
273 results
274 }
275 GradingPolicy::Fixed(fixed_results) => {
276 let mut results = Vec::with_capacity(exercise_slide_submission_tasks.len());
277 for task_submission in exercise_slide_submission_tasks {
278 let fixed_result = fixed_results
279 .get(&task_submission.exercise_task_id)
280 .ok_or_else(|| {
281 ModelError::new(
282 ModelErrorType::Generic,
283 "Could not find fixed test result for testing".to_string(),
284 None,
285 )
286 })?
287 .clone();
288 let submission = create_fixed_grading_for_submission_task(
289 &mut tx,
290 &task_submission,
291 exercise_with_user_state.exercise(),
292 user_exercise_slide_state.id,
293 &fixed_result,
294 )
295 .await?;
296 results.push(submission);
297 }
298 results
299 }
300 };
301 let user_exercise_state = update_user_exercise_slide_state_and_user_exercise_state(
302 &mut tx,
303 user_exercise_slide_state,
304 exercise_slide_submission.user_points_update_strategy,
305 )
306 .await?;
307
308 let course_or_exam_id = CourseOrExamId::from_course_and_exam_ids(
309 user_exercise_state.course_id,
310 user_exercise_state.exam_id,
311 )?;
312
313 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?;
314
315 let result = StudentExerciseSlideSubmissionResult {
316 exercise_status: Some(ExerciseStatus {
317 score_given: user_exercise_state.score_given,
318 activity_progress: user_exercise_state.activity_progress,
319 grading_progress: user_exercise_state.grading_progress,
320 reviewing_stage: user_exercise_state.reviewing_stage,
321 }),
322 exercise_task_submission_results: results,
323 user_course_instance_exercise_service_variables,
324 };
325 exercise_with_user_state.set_user_exercise_state(user_exercise_state)?;
326 tx.commit().await?;
327 Ok(result)
328}
329
330async fn grade_user_submission_task(
331 conn: &mut PgConnection,
332 submission: &ExerciseTaskSubmission,
333 exercise: &Exercise,
334 user_exercise_slide_state_id: Uuid,
335 user_exercise_state: &UserExerciseState,
336 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
337 send_grading_request: impl Fn(
338 Url,
339 &ExerciseTask,
340 &ExerciseTaskSubmission,
341 ) -> BoxFuture<'static, ModelResult<ExerciseTaskGradingResult>>,
342) -> ModelResult<StudentExerciseTaskSubmissionResult> {
343 let grading = exercise_task_gradings::new_grading(conn, exercise, submission).await?;
344 let updated_submission =
345 exercise_task_submissions::set_grading_id(conn, grading.id, submission.id).await?;
346 let exercise_task =
347 exercise_tasks::get_exercise_task_by_id(conn, submission.exercise_task_id).await?;
348 let grading = exercise_task_gradings::grade_submission(
349 conn,
350 submission,
351 &exercise_task,
352 exercise,
353 &grading,
354 user_exercise_state,
355 fetch_service_info,
356 send_grading_request,
357 )
358 .await?;
359 user_exercise_task_states::upsert_with_grading(conn, user_exercise_slide_state_id, &grading)
360 .await?;
361 let model_solution_spec = exercise_tasks::get_exercise_task_model_solution_spec_by_id(
362 conn,
363 submission.exercise_task_id,
364 )
365 .await?;
366
367 Ok(StudentExerciseTaskSubmissionResult {
368 submission: updated_submission,
369 grading: Some(grading),
370 model_solution_spec,
371 exercise_task_exercise_service_slug: exercise_task.exercise_type,
372 })
373}
374
375async fn create_fixed_grading_for_submission_task(
376 conn: &mut PgConnection,
377 submission: &ExerciseTaskSubmission,
378 exercise: &Exercise,
379 user_exercise_slide_state_id: Uuid,
380 fixed_result: &ExerciseTaskGradingResult,
381) -> ModelResult<StudentExerciseTaskSubmissionResult> {
382 let grading = exercise_task_gradings::new_grading(conn, exercise, submission).await?;
383 let updated_submission =
384 exercise_task_submissions::set_grading_id(conn, grading.id, submission.id).await?;
385 let updated_grading =
386 exercise_task_gradings::update_grading(conn, &grading, fixed_result, exercise).await?;
387 user_exercise_task_states::upsert_with_grading(
388 conn,
389 user_exercise_slide_state_id,
390 &updated_grading,
391 )
392 .await?;
393 let exercise_task =
394 exercise_tasks::get_exercise_task_by_id(conn, submission.exercise_task_id).await?;
395 let model_solution_spec = exercise_task.model_solution_spec;
396
397 Ok(StudentExerciseTaskSubmissionResult {
398 submission: updated_submission,
399 grading: Some(grading),
400 model_solution_spec,
401 exercise_task_exercise_service_slug: exercise_task.exercise_type,
402 })
403}
404
405async fn update_user_exercise_slide_state_and_user_exercise_state(
408 conn: &mut PgConnection,
409 user_exercise_slide_state: UserExerciseSlideState,
410 user_points_update_strategy: UserPointsUpdateStrategy,
411) -> ModelResult<UserExerciseState> {
412 update_user_exercise_slide_state(
413 conn,
414 &user_exercise_slide_state,
415 user_points_update_strategy,
416 )
417 .await?;
418 let user_exercise_state = user_exercise_state_updater::update_user_exercise_state(
419 conn,
420 user_exercise_slide_state.user_exercise_state_id,
421 )
422 .await?;
423
424 Ok(user_exercise_state)
425}
426
427async fn update_user_exercise_slide_state(
428 conn: &mut PgConnection,
429 user_exercise_slide_state: &UserExerciseSlideState,
430 user_points_update_strategy: UserPointsUpdateStrategy,
431) -> ModelResult<()> {
432 let (points_from_tasks, grading_progress) =
433 user_exercise_task_states::get_grading_summary_by_user_exercise_slide_state_id(
434 conn,
435 user_exercise_slide_state.id,
436 )
437 .await?;
438 let new_score_given = user_exercise_task_states::figure_out_new_score_given(
439 user_exercise_slide_state.score_given,
440 points_from_tasks,
441 user_points_update_strategy,
442 );
443 let changes = user_exercise_slide_states::update(
444 conn,
445 user_exercise_slide_state.id,
446 new_score_given,
447 grading_progress,
448 )
449 .await?;
450 info!(
451 "Updating user exercise slide state {} affected {} rows.",
452 user_exercise_slide_state.id, changes
453 );
454 Ok(())
455}
456
457pub async fn propagate_user_exercise_state_update_from_exercise_task_grading_result(
460 conn: &mut PgConnection,
461 exercise: &Exercise,
462 exercise_task_grading: &ExerciseTaskGrading,
463 exercise_task_grading_result: &ExerciseTaskGradingResult,
464 user_exercise_slide_state: UserExerciseSlideState,
465 user_points_update_strategy: UserPointsUpdateStrategy,
466) -> ModelResult<UserExerciseState> {
467 let updated_exercise_task_grading = exercise_task_gradings::update_grading(
468 conn,
469 exercise_task_grading,
470 exercise_task_grading_result,
471 exercise,
472 )
473 .await?;
474 exercise_task_submissions::set_grading_id(
475 conn,
476 updated_exercise_task_grading.id,
477 updated_exercise_task_grading.exercise_task_submission_id,
478 )
479 .await?;
480 let user_exercise_task_state = user_exercise_task_states::upsert_with_grading(
481 conn,
482 user_exercise_slide_state.id,
483 &updated_exercise_task_grading,
484 )
485 .await?;
486 let user_exercise_slide_state = user_exercise_slide_states::get_by_id(
487 conn,
488 user_exercise_task_state.user_exercise_slide_state_id,
489 )
490 .await?;
491 let user_exercise_state = update_user_exercise_slide_state_and_user_exercise_state(
492 conn,
493 user_exercise_slide_state,
494 user_points_update_strategy,
495 )
496 .await?;
497 Ok(user_exercise_state)
498}
499
500#[derive(Debug, Serialize)]
501#[cfg_attr(feature = "ts_rs", derive(TS))]
502pub struct AnswersRequiringAttention {
503 pub exercise_max_points: i32,
504 pub data: Vec<AnswerRequiringAttentionWithTasks>,
505 pub total_pages: u32,
506}
507
508#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
509#[cfg_attr(feature = "ts_rs", derive(TS))]
510pub struct AnswerRequiringAttentionWithTasks {
511 pub id: Uuid,
512 pub user_id: Uuid,
513 pub created_at: DateTime<Utc>,
514 pub updated_at: DateTime<Utc>,
515 pub deleted_at: Option<DateTime<Utc>>,
516 pub data_json: Option<serde_json::Value>,
517 pub grading_progress: GradingProgress,
518 pub score_given: Option<f32>,
519 pub submission_id: Uuid,
520 pub exercise_id: Uuid,
521 pub tasks: Vec<CourseMaterialExerciseTask>,
522 pub given_peer_reviews: Vec<PeerReviewWithQuestionsAndAnswers>,
523 pub received_peer_or_self_reviews: Vec<PeerReviewWithQuestionsAndAnswers>,
524 pub received_peer_review_flagging_reports: Vec<FlaggedAnswer>,
525}
526
527pub async fn get_paginated_answers_requiring_attention_for_exercise(
529 conn: &mut PgConnection,
530 exercise_id: Uuid,
531 pagination: Pagination,
532 viewer_user_id: Uuid,
533 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
534) -> ModelResult<AnswersRequiringAttention> {
535 let exercise = exercises::get_exercise_by_id(conn, exercise_id).await?;
536 let answer_requiring_attention_count =
537 exercise_slide_submissions::answer_requiring_attention_count(conn, exercise_id).await?;
538 let data = exercise_slide_submissions::get_all_answers_requiring_attention(
539 conn,
540 exercise.id,
541 pagination,
542 )
543 .await?;
544 let mut answers = Vec::with_capacity(data.len());
545 for answer in &data {
546 let tasks = exercise_task_submissions::get_exercise_task_submission_info_by_exercise_slide_submission_id(
547 conn,
548 answer.submission_id,
549 viewer_user_id,
550 &fetch_service_info,
551 false,
552 )
553 .await?;
554 let given_peer_reviews = peer_or_self_review_question_submissions::get_given_peer_reviews(
555 conn,
556 answer.user_id,
557 answer.exercise_id,
558 )
559 .await?;
560
561 let received_peer_or_self_reviews =
562 peer_or_self_review_question_submissions::get_questions_and_answers_by_submission_id(
563 conn,
564 answer.submission_id,
565 )
566 .await?;
567 let received_peer_review_flagging_reports: Vec<FlaggedAnswer> =
568 flagged_answers::get_flagged_answers_by_submission_id(conn, answer.submission_id)
569 .await?;
570 let new_answer = AnswerRequiringAttentionWithTasks {
571 id: answer.id,
572 user_id: answer.user_id,
573 created_at: answer.created_at,
574 updated_at: answer.updated_at,
575 deleted_at: answer.deleted_at,
576 data_json: answer.data_json.to_owned(),
577 grading_progress: answer.grading_progress,
578 score_given: answer.score_given,
579 submission_id: answer.submission_id,
580 exercise_id: answer.exercise_id,
581 tasks,
582 given_peer_reviews,
583 received_peer_or_self_reviews,
584 received_peer_review_flagging_reports,
585 };
586 answers.push(new_answer);
587 }
588 Ok(AnswersRequiringAttention {
589 exercise_max_points: exercise.score_maximum,
590 data: answers,
591 total_pages: pagination.total_pages(answer_requiring_attention_count),
592 })
593}