1use std::{
2 collections::{HashMap, HashSet},
3 convert::TryFrom,
4 future::Future,
5 pin::Pin,
6};
7
8use futures::{
9 future::{BoxFuture, FutureExt},
10 stream::{FuturesUnordered, StreamExt},
11};
12use itertools::Itertools;
13use sqlx::PgConnection;
14use url::Url;
15
16use crate::{
17 self as models, ModelResult,
18 exercise_service_info::ExerciseServiceInfo,
19 exercise_services::{ExerciseService, get_internal_grade_url},
20 exercise_task_gradings::{ExerciseTaskGrading, ExerciseTaskGradingResult},
21 exercise_task_regrading_submissions::ExerciseTaskRegradingSubmission,
22 exercise_task_submissions::ExerciseTaskSubmission,
23 exercise_tasks::ExerciseTask,
24 exercises::{Exercise, GradingProgress},
25 prelude::*,
26};
27
28type GradingFutures =
29 HashMap<String, Vec<Pin<Box<dyn Future<Output = GradingData> + Send + 'static>>>>;
30
31pub async fn regrade(
32 conn: &mut PgConnection,
33 exercise_services_by_type: &HashMap<String, (ExerciseService, ExerciseServiceInfo)>,
34 send_grading_request: impl Fn(
35 Url,
36 &ExerciseTask,
37 &ExerciseTaskSubmission,
38 ) -> BoxFuture<'static, ModelResult<ExerciseTaskGradingResult>>,
39) -> ModelResult<()> {
40 let mut grading_futures = GradingFutures::new();
42 let mut incomplete_regradings = HashSet::new();
44
45 tracing::debug!("fetching uncompleted regradings");
46 let regrading_ids =
47 models::regradings::get_uncompleted_regradings_and_mark_as_started(&mut *conn).await?;
48 for regrading_id in regrading_ids.iter().copied() {
49 models::regradings::set_total_grading_progress(
51 &mut *conn,
52 regrading_id,
53 GradingProgress::Pending,
54 )
55 .await?;
56 match do_single_regrading(
57 conn,
58 exercise_services_by_type,
59 regrading_id,
60 &mut grading_futures,
61 &send_grading_request,
62 )
63 .await
64 {
65 Ok(regrading_status) => {
66 if !regrading_status.missing_exercise_services.is_empty() {
67 let msg = format!(
68 "Regrading {} failed: no exercise service found for exercise types [{}]",
69 regrading_id,
70 regrading_status.missing_exercise_services.iter().join(", ")
71 );
72 tracing::error!("{}", msg);
73 models::regradings::set_error_message(conn, regrading_id, &msg).await?;
74 models::regradings::set_total_grading_progress(
75 conn,
76 regrading_id,
77 GradingProgress::Failed,
78 )
79 .await?;
80 incomplete_regradings.insert(regrading_id);
81 } else if regrading_status.exercise_services_full {
82 incomplete_regradings.insert(regrading_id);
83 }
84 }
85 Err(err) => {
86 tracing::error!("Regrading {} failed: {}", regrading_id, err);
87 let backtrace: Option<&backtrace::Backtrace> = err.backtrace();
88 if let Some(bt) = backtrace {
89 tracing::error!("Backtrace:\n{:?}", bt);
90 }
91
92 let error_message = if let Some(bt) = backtrace {
93 format!("{}\n\nBacktrace:\n{:?}", err, bt)
94 } else {
95 err.to_string()
96 };
97 models::regradings::set_error_message(conn, regrading_id, &error_message).await?;
98 models::regradings::set_total_grading_progress(
99 conn,
100 regrading_id,
101 GradingProgress::Failed,
102 )
103 .await?;
104 incomplete_regradings.insert(regrading_id);
105 }
106 }
107 }
108
109 let mut grading_futures = grading_futures
111 .into_iter()
112 .flat_map(|v| v.1)
113 .collect::<FuturesUnordered<_>>();
114 while let Some(GradingData {
115 exercise_service_name,
116 regrading_submission,
117 grading,
118 exercise,
119 exercise_service_result,
120 }) = grading_futures.next().await
121 {
122 let grading_result = match exercise_service_result {
123 Ok(grading_result) => grading_result,
124 Err(err) => {
125 tracing::error!(
126 "Failed to get grading from exercise service {}: {}",
127 exercise_service_name,
128 err
129 );
130 models::exercise_task_gradings::set_grading_progress(
131 &mut *conn,
132 grading.id,
133 GradingProgress::Failed,
134 )
135 .await?;
136 continue;
137 }
138 };
139 models::library::grading::update_grading_with_single_regrading_result(
140 conn,
141 &exercise,
142 ®rading_submission,
143 &grading,
144 &grading_result,
145 )
146 .await?;
147 }
148 for regrading_id in regrading_ids {
150 if !incomplete_regradings.contains(®rading_id) {
151 models::regradings::complete_regrading(conn, regrading_id).await?;
152 }
153 }
154 Ok(())
155}
156
157struct RegradingStatus {
158 exercise_services_full: bool,
159 missing_exercise_services: HashSet<String>,
160}
161
162async fn do_single_regrading(
163 conn: &mut PgConnection,
164 exercise_services_by_type: &HashMap<String, (ExerciseService, ExerciseServiceInfo)>,
165 regrading_id: Uuid,
166 grading_futures: &mut GradingFutures,
167 send_grading_request: impl Fn(
168 Url,
169 &ExerciseTask,
170 &ExerciseTaskSubmission,
171 ) -> BoxFuture<'static, ModelResult<ExerciseTaskGradingResult>>,
172) -> ModelResult<RegradingStatus> {
173 let mut regrading_status = RegradingStatus {
174 exercise_services_full: false,
175 missing_exercise_services: HashSet::new(),
176 };
177
178 let regrading_submissions =
180 models::exercise_task_regrading_submissions::get_regrading_submissions(
181 &mut *conn,
182 regrading_id,
183 )
184 .await?;
185 tracing::info!(
186 "found {} submissions for regrading {}",
187 regrading_submissions.len(),
188 regrading_id
189 );
190 for regrading_submission in regrading_submissions {
191 if let Some(grading_id) = regrading_submission.grading_after_regrading {
194 let grading = models::exercise_task_gradings::get_by_id(&mut *conn, grading_id).await?;
196 if grading.grading_progress == GradingProgress::FullyGraded {
197 continue;
199 }
200 }
202
203 let submission = models::exercise_task_submissions::get_submission(
205 &mut *conn,
206 regrading_submission.exercise_task_submission_id,
207 )
208 .await?;
209 let exercise_slide =
210 models::exercise_slides::get_exercise_slide(&mut *conn, submission.exercise_slide_id)
211 .await?;
212 let exercise = models::exercises::get_by_id(&mut *conn, exercise_slide.exercise_id).await?;
213 if exercise.exam_id.is_some() {
214 info!(
215 "Submission being regraded is from an exam, making sure we only give points from the last submission."
216 );
217 let exercise_slide_submission = models::exercise_slide_submissions::get_by_id(
218 &mut *conn,
219 submission.exercise_slide_submission_id,
220 )
221 .await?;
222 let latest_submission =
223 models::exercise_slide_submissions::get_users_latest_exercise_slide_submission(
224 &mut *conn,
225 submission.exercise_slide_id,
226 exercise_slide_submission.user_id,
227 )
228 .await?;
229 if exercise_slide_submission.id != latest_submission.id {
230 info!(
231 "Exam submission being regraded is not the latest submission, refusing to grade it."
232 );
233 models::exercise_task_gradings::set_grading_progress(
234 &mut *conn,
235 regrading_submission.id,
236 GradingProgress::Failed,
237 )
238 .await?;
239 continue;
240 }
241 }
242 let not_ready_grading =
243 models::exercise_task_gradings::new_grading(&mut *conn, &exercise, &submission).await?;
244 models::exercise_task_regrading_submissions::set_grading_after_regrading(
245 conn,
246 regrading_submission.id,
247 not_ready_grading.id,
248 )
249 .await?;
250 let exercise_task = models::exercise_tasks::get_exercise_task_by_id(
252 &mut *conn,
253 submission.exercise_task_id,
254 )
255 .await?;
256 if let Some((exercise_service, exercise_service_info)) =
257 exercise_services_by_type.get(&exercise_task.exercise_type)
258 {
259 models::exercise_task_gradings::set_grading_progress(
261 &mut *conn,
262 not_ready_grading.id,
263 GradingProgress::Pending,
264 )
265 .await?;
266
267 let entry = grading_futures
268 .entry(exercise_task.exercise_type.clone())
269 .or_default();
270
271 let limit = usize::try_from(exercise_service.max_reprocessing_submissions_at_once)
273 .unwrap_or_else(|_e| {
274 tracing::error!(
275 "{}: invalid max_reprocessing_submissions_at_once {}",
276 exercise_service.name,
277 exercise_service.max_reprocessing_submissions_at_once
278 );
279 usize::MAX
280 });
281 if entry.len() < limit {
282 let exercise =
283 models::exercises::get_by_id(&mut *conn, exercise_slide.exercise_id).await?;
284 let grade_url =
285 get_internal_grade_url(exercise_service, exercise_service_info).await?;
286
287 let exercise_service_name = exercise_service.name.clone();
288 let grading_future = send_grading_request(grade_url, &exercise_task, &submission)
289 .map(move |exercise_service_result| GradingData {
290 exercise_service_name,
291 regrading_submission,
292 grading: not_ready_grading,
293 exercise,
294 exercise_service_result,
295 });
296 entry.push(Box::pin(grading_future));
297 } else {
298 regrading_status.exercise_services_full = true;
300 }
301 } else {
302 let msg = format!(
303 "No exercise services found for type {}",
304 exercise_task.exercise_type,
305 );
306 tracing::error!("{}", msg);
307 models::exercise_task_gradings::set_grading_progress(
308 &mut *conn,
309 not_ready_grading.id,
310 GradingProgress::Failed,
311 )
312 .await?;
313 regrading_status
314 .missing_exercise_services
315 .insert(exercise_task.exercise_type);
316 }
317 }
318 Ok(regrading_status)
319}
320
321struct GradingData {
322 exercise_service_name: String,
323 regrading_submission: ExerciseTaskRegradingSubmission,
324 grading: ExerciseTaskGrading,
325 exercise: Exercise,
326 exercise_service_result: ModelResult<ExerciseTaskGradingResult>,
327}
328
329#[cfg(test)]
330mod test {
331 use headless_lms_utils::numbers::f32_approx_eq;
332 use mockito::{Matcher, ServerGuard};
333 use models::{
334 exercise_services,
335 exercise_task_gradings::{ExerciseTaskGradingResult, UserPointsUpdateStrategy},
336 exercise_tasks::NewExerciseTask,
337 exercises::{self, GradingProgress},
338 library::grading::{
339 GradingPolicy, StudentExerciseSlideSubmission, StudentExerciseSlideSubmissionResult,
340 StudentExerciseTaskSubmission,
341 },
342 user_exercise_states::{self, ExerciseWithUserState},
343 };
344 use serde_json::Value;
345
346 use super::*;
347 use crate::test_helper::*;
348
349 #[tokio::test]
350 async fn regrades_submission() {
351 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter, :page, :exercise, :slide);
352 let exercise = exercises::get_by_id(tx.as_mut(), exercise).await.unwrap();
353 let task = models::exercise_tasks::insert(
354 tx.as_mut(),
355 PKeyPolicy::Generate,
356 NewExerciseTask {
357 exercise_slide_id: slide,
358 exercise_type: "test-exercise".to_string(),
359 assignment: vec![],
360 public_spec: Some(Value::Null),
361 private_spec: Some(Value::Null),
362 model_solution_spec: Some(Value::Null),
363 order_number: 0,
364 },
365 )
366 .await
367 .unwrap();
368 let grading_result = ExerciseTaskGradingResult {
369 grading_progress: models::exercises::GradingProgress::FullyGraded,
370 score_given: 0.0,
371 score_maximum: 100,
372 feedback_text: None,
373 feedback_json: None,
374 set_user_variables: Some(HashMap::new()),
375 };
376 let original_grading = create_initial_submission(
377 tx.as_mut(),
378 user,
379 &exercise,
380 course,
381 slide,
382 StudentExerciseSlideSubmission {
383 exercise_slide_id: slide,
384 exercise_task_submissions: vec![StudentExerciseTaskSubmission {
385 exercise_task_id: task,
386 data_json: Value::Null,
387 }],
388 },
389 HashMap::from([(task, grading_result.clone())]),
390 )
391 .await
392 .unwrap();
393
394 let regrading = models::regradings::insert(
395 tx.as_mut(),
396 UserPointsUpdateStrategy::CanAddPointsButCannotRemovePoints,
397 )
398 .await
399 .unwrap();
400 let exercise_task_submission_result = original_grading
401 .exercise_task_submission_results
402 .first()
403 .unwrap();
404 let regrading_submission_id = models::exercise_task_regrading_submissions::insert(
405 tx.as_mut(),
406 PKeyPolicy::Generate,
407 regrading,
408 exercise_task_submission_result.submission.id,
409 exercise_task_submission_result.grading.as_ref().unwrap().id,
410 )
411 .await
412 .unwrap();
413 let mut server = mockito::Server::new_async().await;
414 let _m = server
415 .mock("POST", Matcher::Any)
416 .with_body(serde_json::to_string(&grading_result).unwrap())
417 .create();
418 let service = create_mock_service(tx.as_mut(), "test-exercise".to_string(), 1, &server)
419 .await
420 .unwrap();
421 let services = HashMap::from([("test-exercise".to_string(), service)]);
422
423 let regrading_submission =
424 models::exercise_task_regrading_submissions::get_regrading_submission(
425 tx.as_mut(),
426 regrading_submission_id,
427 )
428 .await
429 .unwrap();
430 assert!(regrading_submission.grading_after_regrading.is_none());
431
432 regrade(tx.as_mut(), &services, |_, _, _| {
433 async {
434 Ok(ExerciseTaskGradingResult {
435 grading_progress: GradingProgress::FullyGraded,
436 score_given: 0.0,
437 score_maximum: 1,
438 feedback_text: None,
439 feedback_json: None,
440 set_user_variables: None,
441 })
442 }
443 .boxed()
444 })
445 .await
446 .unwrap();
447
448 let regrading_submission =
449 models::exercise_task_regrading_submissions::get_regrading_submission(
450 tx.as_mut(),
451 regrading_submission_id,
452 )
453 .await
454 .unwrap();
455 let new_grading = regrading_submission.grading_after_regrading.unwrap();
456 let grading = models::exercise_task_gradings::get_by_id(tx.as_mut(), new_grading)
457 .await
458 .unwrap();
459 assert_eq!(grading.score_given, Some(0.0))
460 }
461
462 #[tokio::test]
463 async fn regrades_complete() {
464 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter, :page, :exercise, :slide);
465 let exercise = exercises::get_by_id(tx.as_mut(), exercise).await.unwrap();
466 let task = models::exercise_tasks::insert(
467 tx.as_mut(),
468 PKeyPolicy::Generate,
469 NewExerciseTask {
470 exercise_slide_id: slide,
471 exercise_type: "test-exercise".to_string(),
472 assignment: vec![],
473 public_spec: Some(Value::Null),
474 private_spec: Some(Value::Null),
475 model_solution_spec: Some(Value::Null),
476 order_number: 0,
477 },
478 )
479 .await
480 .unwrap();
481 let grading_result = ExerciseTaskGradingResult {
482 grading_progress: models::exercises::GradingProgress::FullyGraded,
483 score_given: 0.0,
484 score_maximum: 100,
485 feedback_text: None,
486 feedback_json: None,
487 set_user_variables: Some(HashMap::new()),
488 };
489 let original_grading = create_initial_submission(
490 tx.as_mut(),
491 user,
492 &exercise,
493 course,
494 slide,
495 StudentExerciseSlideSubmission {
496 exercise_slide_id: slide,
497 exercise_task_submissions: vec![StudentExerciseTaskSubmission {
498 exercise_task_id: task,
499 data_json: Value::Null,
500 }],
501 },
502 HashMap::from([(task, grading_result.clone())]),
503 )
504 .await
505 .unwrap();
506 let mut server = mockito::Server::new_async().await;
507 let _m = server
508 .mock("POST", Matcher::Any)
509 .with_body(serde_json::to_string(&grading_result).unwrap())
510 .create();
511 let service = create_mock_service(tx.as_mut(), "test-exercise".to_string(), 1, &server)
512 .await
513 .unwrap();
514 let services = HashMap::from([("test-exercise".to_string(), service)]);
515
516 let regrading = models::regradings::insert(
517 tx.as_mut(),
518 UserPointsUpdateStrategy::CanAddPointsButCannotRemovePoints,
519 )
520 .await
521 .unwrap();
522 let exercise_task_submission_result = original_grading
523 .exercise_task_submission_results
524 .first()
525 .unwrap();
526 let _regrading_submission_id = models::exercise_task_regrading_submissions::insert(
527 tx.as_mut(),
528 PKeyPolicy::Generate,
529 regrading,
530 exercise_task_submission_result.submission.id,
531 exercise_task_submission_result.grading.as_ref().unwrap().id,
532 )
533 .await
534 .unwrap();
535
536 let regrading = models::regradings::get_by_id(tx.as_mut(), regrading)
537 .await
538 .unwrap();
539 assert_eq!(regrading.total_grading_progress, GradingProgress::NotReady);
540 assert!(regrading.regrading_started_at.is_none());
541 assert!(regrading.regrading_completed_at.is_none());
542
543 regrade(tx.as_mut(), &services, |_, _, _| {
544 async {
545 Ok(ExerciseTaskGradingResult {
546 grading_progress: GradingProgress::FullyGraded,
547 score_given: 1.0,
548 score_maximum: 1,
549 feedback_text: None,
550 feedback_json: None,
551 set_user_variables: None,
552 })
553 }
554 .boxed()
555 })
556 .await
557 .unwrap();
558
559 let regrading_1 = models::regradings::get_by_id(tx.as_mut(), regrading.id)
560 .await
561 .unwrap();
562 assert_eq!(
563 regrading_1.total_grading_progress,
564 GradingProgress::FullyGraded
565 );
566 assert!(regrading_1.regrading_started_at.is_some());
567 assert!(regrading_1.regrading_completed_at.is_some());
568 }
569
570 #[tokio::test]
571 async fn regrades_partial() {
572 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter, :page, :exercise, slide: slide_1);
573 let exercise = exercises::get_by_id(tx.as_mut(), exercise).await.unwrap();
574 let grading_result = ExerciseTaskGradingResult {
575 grading_progress: models::exercises::GradingProgress::FullyGraded,
576 score_given: 0.0,
577 score_maximum: 100,
578 feedback_text: None,
579 feedback_json: None,
580 set_user_variables: Some(HashMap::new()),
581 };
582
583 let task_1 = models::exercise_tasks::insert(
584 tx.as_mut(),
585 PKeyPolicy::Generate,
586 NewExerciseTask {
587 exercise_slide_id: slide_1,
588 exercise_type: "test-exercise-1".to_string(),
589 assignment: vec![],
590 public_spec: Some(Value::Null),
591 private_spec: Some(Value::Null),
592 model_solution_spec: Some(Value::Null),
593 order_number: 0,
594 },
595 )
596 .await
597 .unwrap();
598 let original_grading_1 = create_initial_submission(
599 tx.as_mut(),
600 user,
601 &exercise,
602 course,
603 slide_1,
604 StudentExerciseSlideSubmission {
605 exercise_slide_id: slide_1,
606 exercise_task_submissions: vec![StudentExerciseTaskSubmission {
607 exercise_task_id: task_1,
608 data_json: Value::Null,
609 }],
610 },
611 HashMap::from([(task_1, grading_result.clone())]),
612 )
613 .await
614 .unwrap();
615 let task_submission_result_1 = original_grading_1
616 .exercise_task_submission_results
617 .first()
618 .unwrap();
619
620 let slide_2 =
621 models::exercise_slides::insert(tx.as_mut(), PKeyPolicy::Generate, exercise.id, 1)
622 .await
623 .unwrap();
624 let task_2 = models::exercise_tasks::insert(
625 tx.as_mut(),
626 PKeyPolicy::Generate,
627 NewExerciseTask {
628 exercise_slide_id: slide_2,
629 exercise_type: "test-exercise-2".to_string(),
630 assignment: vec![],
631 public_spec: Some(Value::Null),
632 private_spec: Some(Value::Null),
633 model_solution_spec: Some(Value::Null),
634 order_number: 0,
635 },
636 )
637 .await
638 .unwrap();
639 let original_grading_2 = create_initial_submission(
640 tx.as_mut(),
641 user,
642 &exercise,
643 course,
644 slide_2,
645 StudentExerciseSlideSubmission {
646 exercise_slide_id: slide_2,
647 exercise_task_submissions: vec![StudentExerciseTaskSubmission {
648 exercise_task_id: task_2,
649 data_json: Value::Null,
650 }],
651 },
652 HashMap::from([(task_2, grading_result.clone())]),
653 )
654 .await
655 .unwrap();
656 user_exercise_states::upsert_selected_exercise_slide_id(
657 tx.as_mut(),
658 user,
659 exercise.id,
660 Some(course),
661 None,
662 Some(slide_2),
663 )
664 .await
665 .unwrap();
666 let task_submission_result_2 = original_grading_2
667 .exercise_task_submission_results
668 .first()
669 .unwrap();
670 let mut server = mockito::Server::new_async().await;
671 let _m = server
672 .mock("POST", Matcher::Any)
673 .with_body(serde_json::to_string(&grading_result).unwrap())
674 .create();
675 let service_1 = create_mock_service(tx.as_mut(), "test-exercise-1".to_string(), 1, &server)
676 .await
677 .unwrap();
678 let service_2 = create_mock_service(tx.as_mut(), "test-exercise-2".to_string(), 0, &server)
679 .await
680 .unwrap();
681 let services = HashMap::from([
682 ("test-exercise-1".to_string(), service_1),
683 ("test-exercise-2".to_string(), service_2),
684 ]);
685
686 let regrading = models::regradings::insert(
687 tx.as_mut(),
688 UserPointsUpdateStrategy::CanAddPointsButCannotRemovePoints,
689 )
690 .await
691 .unwrap();
692 let _regrading_submission_1 = models::exercise_task_regrading_submissions::insert(
693 tx.as_mut(),
694 PKeyPolicy::Generate,
695 regrading,
696 task_submission_result_1.submission.id,
697 task_submission_result_1.grading.as_ref().unwrap().id,
698 )
699 .await
700 .unwrap();
701 let _regrading_submission_2 = models::exercise_task_regrading_submissions::insert(
702 tx.as_mut(),
703 PKeyPolicy::Generate,
704 regrading,
705 task_submission_result_2.submission.id,
706 task_submission_result_2.grading.as_ref().unwrap().id,
707 )
708 .await
709 .unwrap();
710
711 let regrading_2 = models::regradings::get_by_id(tx.as_mut(), regrading)
712 .await
713 .unwrap();
714 assert_eq!(
715 regrading_2.total_grading_progress,
716 GradingProgress::NotReady
717 );
718 assert!(regrading_2.regrading_started_at.is_none());
719
720 regrade(tx.as_mut(), &services, |_, _, _| {
721 async {
722 Ok(ExerciseTaskGradingResult {
723 grading_progress: GradingProgress::Pending,
724 score_given: 0.0,
725 score_maximum: 1,
726 feedback_text: None,
727 feedback_json: None,
728 set_user_variables: None,
729 })
730 }
731 .boxed()
732 })
733 .await
734 .unwrap();
735
736 let regrading_2 = models::regradings::get_by_id(tx.as_mut(), regrading)
737 .await
738 .unwrap();
739 assert_eq!(regrading_2.total_grading_progress, GradingProgress::Pending);
740 assert!(regrading_2.regrading_started_at.is_some());
741 assert!(regrading_2.regrading_completed_at.is_none());
742 }
743
744 #[tokio::test]
745 async fn updates_exercise_state() {
746 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter, :page, :exercise, :slide);
747 let exercise = exercises::get_by_id(tx.as_mut(), exercise).await.unwrap();
748 let task = models::exercise_tasks::insert(
749 tx.as_mut(),
750 PKeyPolicy::Generate,
751 NewExerciseTask {
752 exercise_slide_id: slide,
753 exercise_type: "test-exercise".to_string(),
754 assignment: vec![],
755 public_spec: Some(Value::Null),
756 private_spec: Some(Value::Null),
757 model_solution_spec: Some(Value::Null),
758 order_number: 0,
759 },
760 )
761 .await
762 .unwrap();
763 let original_grading = create_initial_submission(
764 tx.as_mut(),
765 user,
766 &exercise,
767 course,
768 slide,
769 StudentExerciseSlideSubmission {
770 exercise_slide_id: slide,
771 exercise_task_submissions: vec![StudentExerciseTaskSubmission {
772 exercise_task_id: task,
773 data_json: Value::Null,
774 }],
775 },
776 HashMap::from([(
777 task,
778 ExerciseTaskGradingResult {
779 grading_progress: models::exercises::GradingProgress::FullyGraded,
780 score_given: 0.0,
781 score_maximum: 100,
782 feedback_text: None,
783 feedback_json: None,
784 set_user_variables: Some(HashMap::new()),
785 },
786 )]),
787 )
788 .await
789 .unwrap();
790 let mut server = mockito::Server::new_async().await;
791 let _m = server
792 .mock("POST", Matcher::Any)
793 .with_body(
794 serde_json::to_string(&ExerciseTaskGradingResult {
795 grading_progress: models::exercises::GradingProgress::FullyGraded,
796 score_given: 100.0,
797 score_maximum: 100,
798 feedback_text: None,
799 feedback_json: None,
800 set_user_variables: Some(HashMap::new()),
801 })
802 .unwrap(),
803 )
804 .create();
805 let service = create_mock_service(tx.as_mut(), "test-exercise".to_string(), 1, &server)
806 .await
807 .unwrap();
808 let services = HashMap::from([("test-exercise".to_string(), service)]);
809
810 let regrading = models::regradings::insert(
811 tx.as_mut(),
812 UserPointsUpdateStrategy::CanAddPointsButCannotRemovePoints,
813 )
814 .await
815 .unwrap();
816 let exercise_task_submission_result = original_grading
817 .exercise_task_submission_results
818 .first()
819 .unwrap();
820 let _regrading_submission_id = models::exercise_task_regrading_submissions::insert(
821 tx.as_mut(),
822 PKeyPolicy::Generate,
823 regrading,
824 exercise_task_submission_result.submission.id,
825 exercise_task_submission_result.grading.as_ref().unwrap().id,
826 )
827 .await
828 .unwrap();
829
830 let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
831 tx.as_mut(),
832 user,
833 exercise.id,
834 Some(course),
835 None,
836 )
837 .await
838 .unwrap();
839 assert!(
840 f32_approx_eq(user_exercise_state.score_given.unwrap(), 0.0),
841 "{} != {}",
842 user_exercise_state.score_given.unwrap(),
843 0.0
844 );
845
846 regrade(tx.as_mut(), &services, |_, _, _| {
847 async {
848 Ok(ExerciseTaskGradingResult {
849 grading_progress: GradingProgress::FullyGraded,
850 score_given: 1.0,
851 score_maximum: 1,
852 feedback_text: None,
853 feedback_json: None,
854 set_user_variables: None,
855 })
856 }
857 .boxed()
858 })
859 .await
860 .unwrap();
861
862 let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
863 tx.as_mut(),
864 user,
865 exercise.id,
866 Some(course),
867 None,
868 )
869 .await
870 .unwrap();
871 assert!(
872 f32_approx_eq(user_exercise_state.score_given.unwrap(), 1.0),
873 "{} != {}",
874 user_exercise_state.score_given.unwrap(),
875 1.0
876 );
877 }
878
879 #[tokio::test]
880 async fn fail_on_missing_service() {
881 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter, :page, :exercise, :slide, :task);
882 let exercise = exercises::get_by_id(tx.as_mut(), exercise).await.unwrap();
883 let grading_result = ExerciseTaskGradingResult {
884 grading_progress: models::exercises::GradingProgress::FullyGraded,
885 score_given: 0.0,
886 score_maximum: 100,
887 feedback_text: None,
888 feedback_json: None,
889 set_user_variables: Some(HashMap::new()),
890 };
891 let original_grading = create_initial_submission(
892 tx.as_mut(),
893 user,
894 &exercise,
895 course,
896 slide,
897 StudentExerciseSlideSubmission {
898 exercise_slide_id: slide,
899 exercise_task_submissions: vec![StudentExerciseTaskSubmission {
900 exercise_task_id: task,
901 data_json: Value::Null,
902 }],
903 },
904 HashMap::from([(task, grading_result.clone())]),
905 )
906 .await
907 .unwrap();
908 let exercise_task_submission_result = original_grading
909 .exercise_task_submission_results
910 .first()
911 .unwrap();
912
913 let regrading = models::regradings::insert(
914 tx.as_mut(),
915 UserPointsUpdateStrategy::CanAddPointsButCannotRemovePoints,
916 )
917 .await
918 .unwrap();
919 let _regrading_submission_id = models::exercise_task_regrading_submissions::insert(
920 tx.as_mut(),
921 PKeyPolicy::Generate,
922 regrading,
923 exercise_task_submission_result.submission.id,
924 exercise_task_submission_result.grading.as_ref().unwrap().id,
925 )
926 .await
927 .unwrap();
928
929 let services = HashMap::new();
930 regrade(tx.as_mut(), &services, |_, _, _| unimplemented!())
931 .await
932 .unwrap();
933
934 let regrading = models::regradings::get_by_id(tx.as_mut(), regrading)
935 .await
936 .unwrap();
937 assert_eq!(regrading.total_grading_progress, GradingProgress::Failed);
938 }
939
940 async fn create_initial_submission(
941 conn: &mut PgConnection,
942 user_id: Uuid,
943 exercise: &Exercise,
944 course_id: Uuid,
945 exercise_slide_id: Uuid,
946 submission: StudentExerciseSlideSubmission,
947 mock_results: HashMap<Uuid, ExerciseTaskGradingResult>,
948 ) -> ModelResult<StudentExerciseSlideSubmissionResult> {
949 user_exercise_states::upsert_selected_exercise_slide_id(
950 conn,
951 user_id,
952 exercise.id,
953 Some(course_id),
954 None,
955 Some(exercise_slide_id),
956 )
957 .await?;
958 let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
959 conn,
960 user_id,
961 exercise.id,
962 Some(course_id),
963 None,
964 )
965 .await?;
966 let mut exercise_with_user_state =
967 ExerciseWithUserState::new(exercise.clone(), user_exercise_state).unwrap();
968 let grading = crate::library::grading::grade_user_submission(
969 conn,
970 &mut exercise_with_user_state,
971 &submission,
972 GradingPolicy::Fixed(mock_results),
973 |_| unimplemented!(),
974 |_, _, _| unimplemented!(),
975 )
976 .await
977 .unwrap();
978 Ok(grading)
979 }
980
981 async fn create_mock_service(
982 conn: &mut PgConnection,
983 service_slug: String,
984 max_reprocessing_submissions_at_once: i32,
985 server: &ServerGuard,
986 ) -> ModelResult<(ExerciseService, ExerciseServiceInfo)> {
987 let exercise_service = models::exercise_services::insert_exercise_service(
988 conn,
989 &exercise_services::ExerciseServiceNewOrUpdate {
990 name: "".to_string(),
991 slug: service_slug,
992 public_url: "".to_string(),
993 internal_url: Some(server.url()),
994 max_reprocessing_submissions_at_once,
995 },
996 )
997 .await?;
998 let info = models::exercise_service_info::insert(
999 conn,
1000 &models::exercise_service_info::PathInfo {
1001 exercise_service_id: exercise_service.id,
1002 user_interface_iframe_path: "/iframe".to_string(),
1003 grade_endpoint_path: "/grade".to_string(),
1004 public_spec_endpoint_path: "/public-spec".to_string(),
1005 model_solution_spec_endpoint_path: "/model-solution".to_string(),
1006 has_custom_view: false,
1007 },
1008 )
1009 .await?;
1010
1011 Ok((exercise_service, info))
1012 }
1013}