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
71pub 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 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}