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 =
120            exercise_task_submissions::get_grading_ref(&mut tx, *id).await?;
121        if exercise_task_submission.deleted_at.is_some() {
122            warn!(
123                "Skipping regrading of deleted exercise task submission {:?}",
124                id
125            );
126            continue;
127        }
128        let grading_before_regrading_id = exercise_task_submission
129            .exercise_task_grading_id
130            .ok_or_else(|| {
131                ModelError::new(
132                    ModelErrorType::PreconditionFailed,
133                    "One of the submissions to be regraded has not been graded yet.".to_string(),
134                    None,
135                )
136            })?;
137        let _etrs = exercise_task_regrading_submissions::insert(
138            &mut tx,
139            PKeyPolicy::Generate,
140            res.id,
141            *id,
142            grading_before_regrading_id,
143        )
144        .await?;
145    }
146    tx.commit().await?;
147    Ok(res.id)
148}
149
150pub async fn get_regrading_info_by_id(
151    conn: &mut PgConnection,
152    regrading_id: Uuid,
153) -> ModelResult<RegradingInfo> {
154    let regrading = get_by_id(&mut *conn, regrading_id).await?;
155    let etrs =
156        exercise_task_regrading_submissions::get_regrading_submissions(&mut *conn, regrading_id)
157            .await?;
158    let mut grading_id_to_grading =
159        exercise_task_gradings::get_new_and_old_exercise_task_gradings_by_regrading_id(
160            &mut *conn,
161            regrading_id,
162        )
163        .await?;
164    let submission_infos = etrs
165        .iter()
166        .map(|e| -> ModelResult<_> {
167            Ok(RegradingSubmissionInfo {
168                exercise_task_submission_id: e.exercise_task_submission_id,
169                grading_before_regrading: grading_id_to_grading
170                    .remove(&e.grading_before_regrading)
171                    .ok_or_else(|| {
172                        ModelError::new(
173                            ModelErrorType::Generic,
174                            "Grading before regrading not found".to_string(),
175                            None,
176                        )
177                    })?,
178                grading_after_regrading: e
179                    .grading_after_regrading
180                    .and_then(|gar| grading_id_to_grading.remove(&gar)),
181            })
182        })
183        .collect::<ModelResult<Vec<_>>>()?;
184    Ok(RegradingInfo {
185        regrading,
186        submission_infos,
187    })
188}
189
190pub async fn get_all_paginated(
191    conn: &mut PgConnection,
192    pagination: Pagination,
193) -> ModelResult<Vec<Regrading>> {
194    let res = sqlx::query_as!(
195        Regrading,
196        r#"
197SELECT *
198FROM regradings
199WHERE deleted_at IS NULL
200ORDER BY regradings.created_at
201LIMIT $1 OFFSET $2;
202"#,
203        pagination.limit(),
204        pagination.offset()
205    )
206    .fetch_all(conn)
207    .await?;
208    Ok(res)
209}
210
211pub async fn get_all_count(conn: &mut PgConnection) -> ModelResult<i64> {
212    let res = sqlx::query!(
213        "
214SELECT COUNT(*) as count
215from regradings
216WHERE deleted_at IS NULL;
217"
218    )
219    .fetch_one(conn)
220    .await?;
221    Ok(res.count.unwrap_or(0))
222}
223
224pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<Regrading> {
225    let res = sqlx::query_as!(
226        Regrading,
227        r#"
228SELECT *
229FROM regradings
230WHERE id = $1
231"#,
232        id
233    )
234    .fetch_one(conn)
235    .await?;
236    Ok(res)
237}
238
239pub async fn get_uncompleted_regradings_and_mark_as_started(
240    conn: &mut PgConnection,
241) -> ModelResult<Vec<Uuid>> {
242    let res = sqlx::query!(
243        r#"
244UPDATE regradings
245SET regrading_started_at = CASE
246    WHEN regrading_started_at IS NULL THEN now()
247    ELSE regrading_started_at
248  END
249WHERE regrading_completed_at IS NULL
250  AND deleted_at IS NULL
251RETURNING *
252"#
253    )
254    .fetch_all(&mut *conn)
255    .await?
256    .into_iter()
257    .map(|r| r.id)
258    .collect();
259
260    Ok(res)
261}
262
263pub async fn set_total_grading_progress(
264    conn: &mut PgConnection,
265    regrading_id: Uuid,
266    progress: GradingProgress,
267) -> ModelResult<()> {
268    sqlx::query!(
269        "
270UPDATE regradings
271SET total_grading_progress = $1
272WHERE id = $2
273",
274        progress as GradingProgress,
275        regrading_id
276    )
277    .execute(conn)
278    .await?;
279    Ok(())
280}
281
282pub async fn complete_regrading(conn: &mut PgConnection, regrading_id: Uuid) -> ModelResult<()> {
283    sqlx::query!(
284        "
285UPDATE regradings
286SET regrading_completed_at = now(),
287  total_grading_progress = 'fully-graded'
288WHERE id = $1
289",
290        regrading_id
291    )
292    .execute(conn)
293    .await?;
294    Ok(())
295}
296
297pub async fn set_error_message(
298    conn: &mut PgConnection,
299    regrading_id: Uuid,
300    error_message: &str,
301) -> ModelResult<()> {
302    sqlx::query!(
303        "
304UPDATE regradings
305SET error_message = $1
306WHERE id = $2
307",
308        error_message,
309        regrading_id
310    )
311    .execute(conn)
312    .await?;
313    Ok(())
314}