Skip to main content

headless_lms_models/
regradings.rs

1use crate::{
2    exercise_task_gradings::{self, ExerciseTaskGrading, UserPointsUpdateStrategy},
3    exercise_task_regrading_submissions, exercise_task_submissions,
4    exercises::GradingProgress,
5    prelude::*,
6};
7use utoipa::ToSchema;
8
9#[derive(Debug, Deserialize, Serialize, ToSchema)]
10
11pub struct Regrading {
12    pub id: Uuid,
13    pub created_at: DateTime<Utc>,
14    pub updated_at: DateTime<Utc>,
15    pub regrading_started_at: Option<DateTime<Utc>>,
16    pub regrading_completed_at: Option<DateTime<Utc>>,
17    pub total_grading_progress: GradingProgress,
18    pub user_points_update_strategy: UserPointsUpdateStrategy,
19    pub user_id: Option<Uuid>,
20}
21
22#[derive(Debug, Deserialize, Serialize, ToSchema)]
23
24pub struct NewRegrading {
25    user_points_update_strategy: UserPointsUpdateStrategy,
26    ids: Vec<Uuid>,
27    id_type: NewRegradingIdType,
28}
29
30#[derive(Debug, Deserialize, Serialize, ToSchema)]
31
32pub enum NewRegradingIdType {
33    ExerciseTaskSubmissionId,
34    ExerciseId,
35}
36
37#[derive(Debug, Deserialize, Serialize, ToSchema)]
38
39pub struct RegradingInfo {
40    pub regrading: Regrading,
41    pub submission_infos: Vec<RegradingSubmissionInfo>,
42}
43
44#[derive(Debug, Deserialize, Serialize, ToSchema)]
45
46pub struct RegradingSubmissionInfo {
47    pub exercise_task_submission_id: Uuid,
48    pub grading_before_regrading: ExerciseTaskGrading,
49    pub grading_after_regrading: Option<ExerciseTaskGrading>,
50}
51
52pub async fn insert(
53    conn: &mut PgConnection,
54    user_points_update_strategy: UserPointsUpdateStrategy,
55) -> ModelResult<Uuid> {
56    let res = sqlx::query!(
57        "
58INSERT INTO regradings (user_points_update_strategy)
59VALUES ($1)
60RETURNING id
61        ",
62        user_points_update_strategy as UserPointsUpdateStrategy
63    )
64    .fetch_one(conn)
65    .await?;
66    Ok(res.id)
67}
68
69/// Creates a new regrading for the exercise task submission ids supplied as arguments.
70pub async fn insert_and_create_regradings(
71    conn: &mut PgConnection,
72    new_regrading: NewRegrading,
73    user_id: Uuid,
74) -> ModelResult<Uuid> {
75    let mut tx = conn.begin().await?;
76    info!("Creating a new regrading.");
77    let res = sqlx::query!(
78        "
79INSERT INTO regradings (user_points_update_strategy, user_id)
80VALUES ($1, $2)
81RETURNING id
82        ",
83        new_regrading.user_points_update_strategy as UserPointsUpdateStrategy,
84        user_id
85    )
86    .fetch_one(&mut *tx)
87    .await?;
88
89    let exercise_task_submission_ids = match new_regrading.id_type {
90        NewRegradingIdType::ExerciseTaskSubmissionId => new_regrading.ids,
91        NewRegradingIdType::ExerciseId => {
92            let mut ids = Vec::new();
93            for id in new_regrading.ids {
94                let exercise = crate::exercises::get_by_id(&mut tx, id).await?;
95                let submission_ids = if exercise.exam_id.is_some() {
96                    // On exams only the last submission is considered.
97                    // That's why we will only regrade those.
98                    exercise_task_submissions::get_latest_submission_ids_by_exercise_id(
99                        &mut tx,
100                        exercise.id,
101                    )
102                    .await?
103                } else {
104                    exercise_task_submissions::get_ids_by_exercise_id(&mut tx, exercise.id).await?
105                };
106                ids.extend(submission_ids);
107            }
108            ids
109        }
110    };
111
112    info!(
113        "Adding {:?} exercise task submissions to the regrading.",
114        exercise_task_submission_ids.len()
115    );
116    for id in &exercise_task_submission_ids {
117        let exercise_task_submission = exercise_task_submissions::get_by_id(&mut tx, *id).await?;
118        if exercise_task_submission.deleted_at.is_some() {
119            warn!(
120                "Skipping regrading of deleted exercise task submission {:?}",
121                id
122            );
123            continue;
124        }
125        let grading_before_regrading_id = exercise_task_submission
126            .exercise_task_grading_id
127            .ok_or_else(|| {
128                ModelError::new(
129                    ModelErrorType::PreconditionFailed,
130                    "One of the submissions to be regraded has not been graded yet.".to_string(),
131                    None,
132                )
133            })?;
134        let _etrs = exercise_task_regrading_submissions::insert(
135            &mut tx,
136            PKeyPolicy::Generate,
137            res.id,
138            *id,
139            grading_before_regrading_id,
140        )
141        .await?;
142    }
143    tx.commit().await?;
144    Ok(res.id)
145}
146
147pub async fn get_regrading_info_by_id(
148    conn: &mut PgConnection,
149    regrading_id: Uuid,
150) -> ModelResult<RegradingInfo> {
151    let regrading = get_by_id(&mut *conn, regrading_id).await?;
152    let etrs =
153        exercise_task_regrading_submissions::get_regrading_submissions(&mut *conn, regrading_id)
154            .await?;
155    let mut grading_id_to_grading =
156        exercise_task_gradings::get_new_and_old_exercise_task_gradings_by_regrading_id(
157            &mut *conn,
158            regrading_id,
159        )
160        .await?;
161    let submission_infos = etrs
162        .iter()
163        .map(|e| -> ModelResult<_> {
164            Ok(RegradingSubmissionInfo {
165                exercise_task_submission_id: e.exercise_task_submission_id,
166                grading_before_regrading: grading_id_to_grading
167                    .remove(&e.grading_before_regrading)
168                    .ok_or_else(|| {
169                        ModelError::new(
170                            ModelErrorType::Generic,
171                            "Grading before regrading not found".to_string(),
172                            None,
173                        )
174                    })?,
175                grading_after_regrading: e
176                    .grading_after_regrading
177                    .and_then(|gar| grading_id_to_grading.remove(&gar)),
178            })
179        })
180        .collect::<ModelResult<Vec<_>>>()?;
181    Ok(RegradingInfo {
182        regrading,
183        submission_infos,
184    })
185}
186
187pub async fn get_all_paginated(
188    conn: &mut PgConnection,
189    pagination: Pagination,
190) -> ModelResult<Vec<Regrading>> {
191    let res = sqlx::query_as!(
192        Regrading,
193        r#"
194SELECT id,
195  created_at,
196  updated_at,
197  regrading_started_at,
198  regrading_completed_at,
199  total_grading_progress AS "total_grading_progress: _",
200  user_points_update_strategy AS "user_points_update_strategy: _",
201  user_id
202FROM regradings
203WHERE deleted_at IS NULL
204ORDER BY regradings.created_at
205LIMIT $1 OFFSET $2;
206"#,
207        pagination.limit(),
208        pagination.offset()
209    )
210    .fetch_all(conn)
211    .await?;
212    Ok(res)
213}
214
215pub async fn get_all_count(conn: &mut PgConnection) -> ModelResult<i64> {
216    let res = sqlx::query!(
217        "
218SELECT COUNT(*) as count
219from regradings
220WHERE deleted_at IS NULL;
221"
222    )
223    .fetch_one(conn)
224    .await?;
225    Ok(res.count.unwrap_or(0))
226}
227
228pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<Regrading> {
229    let res = sqlx::query_as!(
230        Regrading,
231        r#"
232SELECT id,
233  regrading_started_at,
234  regrading_completed_at,
235  created_at,
236  updated_at,
237  total_grading_progress AS "total_grading_progress: _",
238  user_points_update_strategy AS "user_points_update_strategy: _",
239  user_id
240FROM regradings
241WHERE id = $1
242"#,
243        id
244    )
245    .fetch_one(conn)
246    .await?;
247    Ok(res)
248}
249
250pub async fn get_uncompleted_regradings_and_mark_as_started(
251    conn: &mut PgConnection,
252) -> ModelResult<Vec<Uuid>> {
253    let res = sqlx::query!(
254        r#"
255UPDATE regradings
256SET regrading_started_at = CASE
257    WHEN regrading_started_at IS NULL THEN now()
258    ELSE regrading_started_at
259  END
260WHERE regrading_completed_at IS NULL
261  AND deleted_at IS NULL
262RETURNING id
263"#
264    )
265    .fetch_all(&mut *conn)
266    .await?
267    .into_iter()
268    .map(|r| r.id)
269    .collect();
270
271    Ok(res)
272}
273
274pub async fn set_total_grading_progress(
275    conn: &mut PgConnection,
276    regrading_id: Uuid,
277    progress: GradingProgress,
278) -> ModelResult<()> {
279    sqlx::query!(
280        "
281UPDATE regradings
282SET total_grading_progress = $1
283WHERE id = $2
284",
285        progress as GradingProgress,
286        regrading_id
287    )
288    .execute(conn)
289    .await?;
290    Ok(())
291}
292
293pub async fn complete_regrading(conn: &mut PgConnection, regrading_id: Uuid) -> ModelResult<()> {
294    sqlx::query!(
295        "
296UPDATE regradings
297SET regrading_completed_at = now(),
298  total_grading_progress = 'fully-graded'
299WHERE id = $1
300",
301        regrading_id
302    )
303    .execute(conn)
304    .await?;
305    Ok(())
306}
307
308pub async fn set_error_message(
309    conn: &mut PgConnection,
310    regrading_id: Uuid,
311    error_message: &str,
312) -> ModelResult<()> {
313    sqlx::query!(
314        "
315UPDATE regradings
316SET error_message = $1
317WHERE id = $2
318",
319        error_message,
320        regrading_id
321    )
322    .execute(conn)
323    .await?;
324    Ok(())
325}