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