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