headless_lms_models/library/
grading.rs

1//! Collection of functions used for processing and evaluating user submissions for exercises.
2
3use 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/// Contains data sent by the student when they make a submission for an exercise slide.
33#[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/// If passed to to an exercise state update, it will update the peer review status with the given information
91#[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    /// Used to for calculating averages when acting on PeerReviewProcessingStrategy
98    pub received_peer_or_self_review_question_submissions: Vec<PeerOrSelfReviewQuestionSubmission>,
99}
100
101/// Inserts user submission to database. Tasks within submission are validated to make sure that
102/// they belong to the correct exercise slide.
103pub 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
174// Relocated regrading logic to condensate score update logic in a single place.
175// Needs better separation of concerns in the far future.
176pub 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    /// Grades exercise tasks by sending a request to their respective services.
222    Default,
223    /// Intended for test purposes only.
224    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
405/// Updates the user exercise state starting from a slide state, and propagates the update up to the
406/// whole user exercise state.
407async 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
457/// Updates the user exercise state starting from a single task, and propagates the update up to the
458/// whole user exercise state.
459pub 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
527/// Gets submissions that require input from the teacher to continue processing.
528pub 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}