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 =
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}