1use std::collections::HashMap;
2
3use crate::prelude::*;
4use utoipa::ToSchema;
5
6#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
7
8pub struct TeacherGradingDecision {
9 pub id: Uuid,
10 pub user_exercise_state_id: Uuid,
11 pub user_id: Option<Uuid>,
12 pub created_at: DateTime<Utc>,
13 pub updated_at: DateTime<Utc>,
14 pub deleted_at: Option<DateTime<Utc>>,
15 pub score_given: f32,
16 pub teacher_decision: TeacherDecisionType,
17 pub justification: Option<String>,
18 pub hidden: Option<bool>,
19}
20
21#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, sqlx::Type, ToSchema)]
22#[sqlx(type_name = "teacher_decision_type", rename_all = "kebab-case")]
23pub enum TeacherDecisionType {
24 FullPoints,
25 ZeroPoints,
26 CustomPoints,
27 SuspectedPlagiarism,
28 RejectAndReset,
29 UnauthorizedAiUse,
30 BadAnswer,
31 Other,
32}
33
34#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
35
36pub struct NewTeacherGradingDecision {
37 pub user_exercise_state_id: Uuid,
38 pub exercise_id: Uuid,
39 pub action: TeacherDecisionType,
40 pub manual_points: Option<f32>,
41 pub justification: Option<String>,
42 pub hidden: bool,
43 pub reset_exercise: bool,
47}
48
49pub async fn add_teacher_grading_decision(
50 conn: &mut PgConnection,
51 user_exercise_state_id: Uuid,
52 action: TeacherDecisionType,
53 score_given: f32,
54 decision_maker_user_id: Option<Uuid>,
55 justification: Option<String>,
56 hidden: bool,
57) -> ModelResult<TeacherGradingDecision> {
58 let res = sqlx::query_as!(
59 TeacherGradingDecision,
60 r#"
61INSERT INTO teacher_grading_decisions (
62 user_exercise_state_id,
63 teacher_decision,
64 score_given,
65 user_id,
66 justification,
67 hidden
68 )
69VALUES ($1, $2, $3, $4, $5, $6)
70RETURNING *;
71 "#,
72 user_exercise_state_id,
73 action as TeacherDecisionType,
74 score_given,
75 decision_maker_user_id,
76 justification,
77 hidden
78 )
79 .fetch_one(conn)
80 .await?;
81 Ok(res)
82}
83
84#[allow(clippy::too_many_arguments)]
85pub async fn upsert_by_state_id_and_exercise_id(
86 conn: &mut PgConnection,
87 user_exercise_state_id: Uuid,
88 exercise_id: Uuid,
89 action: TeacherDecisionType,
90 score_given: f32,
91 decision_maker_user_id: Option<Uuid>,
92 justification: Option<String>,
93 hidden: bool,
94) -> ModelResult<TeacherGradingDecision> {
95 let res = sqlx::query_as!(
96 TeacherGradingDecision,
97 r#"
98INSERT INTO teacher_grading_decisions (
99 user_exercise_state_id,
100 teacher_decision,
101 score_given,
102 user_id,
103 justification,
104 hidden
105)
106SELECT ues.id, $3, $4, $5, $6, $7
107FROM user_exercise_states ues
108WHERE ues.id = $1
109 AND ues.exercise_id = $2
110 AND ues.deleted_at IS NULL
111RETURNING id,
112 user_exercise_state_id,
113 user_id,
114 created_at,
115 updated_at,
116 deleted_at,
117 score_given,
118 teacher_decision,
119 justification,
120 hidden;
121 "#,
122 user_exercise_state_id,
123 exercise_id,
124 action as TeacherDecisionType,
125 score_given,
126 decision_maker_user_id,
127 justification,
128 hidden
129 )
130 .fetch_one(conn)
131 .await?;
132 Ok(res)
133}
134
135pub async fn try_to_get_latest_grading_decision_by_user_exercise_state_id(
136 conn: &mut PgConnection,
137 user_exercise_state_id: Uuid,
138) -> ModelResult<Option<TeacherGradingDecision>> {
139 let res = sqlx::query_as!(
140 TeacherGradingDecision,
141 r#"
142SELECT *
143FROM teacher_grading_decisions
144WHERE user_exercise_state_id = $1
145 AND deleted_at IS NULL
146ORDER BY created_at DESC
147LIMIT 1
148 "#,
149 user_exercise_state_id,
150 )
151 .fetch_optional(conn)
152 .await?;
153 Ok(res)
154}
155
156pub async fn try_to_get_latest_grading_decision_still_addressed_to_student(
164 conn: &mut PgConnection,
165 user_id: Uuid,
166 exercise_id: Uuid,
167) -> ModelResult<Option<TeacherGradingDecision>> {
168 let res = sqlx::query_as!(
169 TeacherGradingDecision,
170 r#"
171SELECT tgd.id,
172 tgd.user_exercise_state_id,
173 tgd.user_id,
174 tgd.created_at,
175 tgd.updated_at,
176 tgd.deleted_at,
177 tgd.score_given,
178 tgd.teacher_decision AS "teacher_decision: _",
179 tgd.justification,
180 tgd.hidden
181FROM teacher_grading_decisions tgd
182 JOIN user_exercise_states ues ON ues.id = tgd.user_exercise_state_id
183WHERE ues.user_id = $1
184 AND ues.exercise_id = $2
185 AND tgd.deleted_at IS NULL
186 AND NOT EXISTS (
187 SELECT 1
188 FROM exercise_slide_submissions ess
189 WHERE ess.user_id = $1
190 AND ess.exercise_id = $2
191 AND ess.deleted_at IS NULL
192 AND ess.created_at > tgd.created_at
193 )
194 AND NOT EXISTS (
195 SELECT 1
196 FROM exercise_reset_logs erl
197 WHERE erl.reset_for = $1
198 AND erl.exercise_id = $2
199 AND erl.deleted_at IS NULL
200 AND erl.reset_at > tgd.created_at
201 )
202ORDER BY tgd.created_at DESC
203LIMIT 1
204 "#,
205 user_id,
206 exercise_id,
207 )
208 .fetch_optional(conn)
209 .await?;
210 Ok(res)
211}
212
213pub async fn try_to_get_latest_grading_decision_by_user_exercise_state_id_for_users(
214 conn: &mut PgConnection,
215 user_exercise_state_ids: &[Uuid],
216) -> ModelResult<HashMap<Uuid, TeacherGradingDecision>> {
217 let decisions = sqlx::query_as!(
218 TeacherGradingDecision,
219 r#"
220SELECT DISTINCT ON (user_exercise_state_id) id,
221 user_exercise_state_id,
222 user_id,
223 created_at,
224 updated_at,
225 deleted_at,
226 score_given,
227 teacher_decision,
228 justification,
229 hidden
230FROM teacher_grading_decisions
231WHERE user_exercise_state_id = ANY($1)
232 AND deleted_at IS NULL
233ORDER BY user_exercise_state_id,
234 created_at DESC
235 "#,
236 user_exercise_state_ids,
237 )
238 .fetch_all(conn)
239 .await?;
240
241 let mut res: HashMap<Uuid, TeacherGradingDecision> = HashMap::new();
242 for item in decisions.into_iter() {
243 res.insert(item.user_exercise_state_id, item);
244 }
245
246 Ok(res)
247}
248
249pub async fn get_all_latest_grading_decisions_by_user_id_and_course_id(
250 conn: &mut PgConnection,
251 user_id: Uuid,
252 course_id: Uuid,
253) -> ModelResult<Vec<TeacherGradingDecision>> {
254 let res = sqlx::query_as!(
255 TeacherGradingDecision,
256 r#"
257SELECT DISTINCT ON (user_exercise_state_id)
258 id,
259 user_exercise_state_id,
260 user_id,
261 created_at,
262 updated_at,
263 deleted_at,
264 score_given,
265 teacher_decision,
266 justification,
267 hidden
268FROM teacher_grading_decisions
269WHERE user_exercise_state_id IN (
270 SELECT user_exercise_states.id
271 FROM user_exercise_states
272 WHERE user_exercise_states.user_id = $1
273 AND user_exercise_states.course_id = $2
274 AND user_exercise_states.deleted_at IS NULL
275 )
276 AND deleted_at IS NULL
277 ORDER BY user_exercise_state_id, created_at DESC
278 "#,
279 user_id,
280 course_id,
281 )
282 .fetch_all(conn)
283 .await?;
284 Ok(res)
285}
286
287pub async fn get_all_latest_grading_decisions_by_user_id_and_exam_id(
288 conn: &mut PgConnection,
289 user_id: Uuid,
290 exam_id: Uuid,
291) -> ModelResult<Vec<TeacherGradingDecision>> {
292 let res = sqlx::query_as!(
293 TeacherGradingDecision,
294 r#"
295SELECT DISTINCT ON (user_exercise_state_id)
296 id,
297 user_exercise_state_id,
298 user_id,
299 created_at,
300 updated_at,
301 deleted_at,
302 score_given,
303 teacher_decision,
304 justification,
305 hidden
306FROM teacher_grading_decisions
307WHERE user_exercise_state_id IN (
308 SELECT user_exercise_states.id
309 FROM user_exercise_states
310 WHERE user_exercise_states.user_id = $1
311 AND user_exercise_states.exam_id = $2
312 AND user_exercise_states.deleted_at IS NULL
313 )
314 AND deleted_at IS NULL
315 ORDER BY user_exercise_state_id, created_at DESC
316 "#,
317 user_id,
318 exam_id,
319 )
320 .fetch_all(conn)
321 .await?;
322 Ok(res)
323}
324
325pub async fn update_teacher_grading_decision_hidden_field(
326 conn: &mut PgConnection,
327 teacher_grading_decision_id: Uuid,
328 hidden: bool,
329) -> ModelResult<TeacherGradingDecision> {
330 let res = sqlx::query_as!(
331 TeacherGradingDecision,
332 r#"
333 UPDATE teacher_grading_decisions
334 SET hidden = $1
335 WHERE id = $2
336 RETURNING *;
337 "#,
338 hidden,
339 teacher_grading_decision_id
340 )
341 .fetch_one(conn)
342 .await?;
343 Ok(res)
344}
345
346pub async fn get_by_ids(
347 conn: &mut PgConnection,
348 ids: &[Uuid],
349) -> ModelResult<Vec<TeacherGradingDecision>> {
350 let res = sqlx::query_as!(
351 TeacherGradingDecision,
352 r#"
353SELECT *
354FROM teacher_grading_decisions
355WHERE id = ANY($1)
356 AND deleted_at IS NULL
357 "#,
358 ids
359 )
360 .fetch_all(conn)
361 .await?;
362 Ok(res)
363}