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