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