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