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