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_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/// Contains data sent by the student when they make a submission for an exercise slide.
35#[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/// If passed to to an exercise state update, it will update the peer review status with the given information
94#[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    /// Used to for calculating averages when acting on PeerReviewProcessingStrategy
101    pub received_peer_or_self_review_question_submissions: Vec<PeerOrSelfReviewQuestionSubmission>,
102}
103
104/// Inserts user submission to database. Tasks within submission are validated to make sure that
105/// they belong to the correct exercise slide.
106pub 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
180// Relocated regrading logic to condensate score update logic in a single place.
181// Needs better separation of concerns in the far future.
182pub 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    /// Grades exercise tasks by sending a request to their respective services.
228    Default,
229    /// Intended for test purposes only.
230    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
411/// Updates the user exercise state starting from a slide state, and propagates the update up to the
412/// whole user exercise state.
413async 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
463/// Updates the user exercise state starting from a single task, and propagates the update up to the
464/// whole user exercise state.
465pub 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
533/// Gets submissions that require input from the teacher to continue processing.
534pub 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}