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}
30
31#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
32
33pub struct NewTeacherGradingDecision {
34 pub user_exercise_state_id: Uuid,
35 pub exercise_id: Uuid,
36 pub action: TeacherDecisionType,
37 pub manual_points: Option<f32>,
38 pub justification: Option<String>,
39 pub hidden: bool,
40}
41
42pub async fn add_teacher_grading_decision(
43 conn: &mut PgConnection,
44 user_exercise_state_id: Uuid,
45 action: TeacherDecisionType,
46 score_given: f32,
47 decision_maker_user_id: Option<Uuid>,
48 justification: Option<String>,
49 hidden: bool,
50) -> ModelResult<TeacherGradingDecision> {
51 let res = sqlx::query_as!(
52 TeacherGradingDecision,
53 r#"
54INSERT INTO teacher_grading_decisions (
55 user_exercise_state_id,
56 teacher_decision,
57 score_given,
58 user_id,
59 justification,
60 hidden
61 )
62VALUES ($1, $2, $3, $4, $5, $6)
63RETURNING *;
64 "#,
65 user_exercise_state_id,
66 action as TeacherDecisionType,
67 score_given,
68 decision_maker_user_id,
69 justification,
70 hidden
71 )
72 .fetch_one(conn)
73 .await?;
74 Ok(res)
75}
76
77#[allow(clippy::too_many_arguments)]
78pub async fn upsert_by_state_id_and_exercise_id(
79 conn: &mut PgConnection,
80 user_exercise_state_id: Uuid,
81 exercise_id: Uuid,
82 action: TeacherDecisionType,
83 score_given: f32,
84 decision_maker_user_id: Option<Uuid>,
85 justification: Option<String>,
86 hidden: bool,
87) -> ModelResult<TeacherGradingDecision> {
88 let res = sqlx::query_as!(
89 TeacherGradingDecision,
90 r#"
91INSERT INTO teacher_grading_decisions (
92 user_exercise_state_id,
93 teacher_decision,
94 score_given,
95 user_id,
96 justification,
97 hidden
98)
99SELECT ues.id, $3, $4, $5, $6, $7
100FROM user_exercise_states ues
101WHERE ues.id = $1
102 AND ues.exercise_id = $2
103 AND ues.deleted_at IS NULL
104RETURNING id,
105 user_exercise_state_id,
106 user_id,
107 created_at,
108 updated_at,
109 deleted_at,
110 score_given,
111 teacher_decision,
112 justification,
113 hidden;
114 "#,
115 user_exercise_state_id,
116 exercise_id,
117 action as TeacherDecisionType,
118 score_given,
119 decision_maker_user_id,
120 justification,
121 hidden
122 )
123 .fetch_one(conn)
124 .await?;
125 Ok(res)
126}
127
128pub async fn try_to_get_latest_grading_decision_by_user_exercise_state_id(
129 conn: &mut PgConnection,
130 user_exercise_state_id: Uuid,
131) -> ModelResult<Option<TeacherGradingDecision>> {
132 let res = sqlx::query_as!(
133 TeacherGradingDecision,
134 r#"
135SELECT *
136FROM teacher_grading_decisions
137WHERE user_exercise_state_id = $1
138 AND deleted_at IS NULL
139ORDER BY created_at DESC
140LIMIT 1
141 "#,
142 user_exercise_state_id,
143 )
144 .fetch_optional(conn)
145 .await?;
146 Ok(res)
147}
148
149pub async fn try_to_get_latest_grading_decision_by_user_exercise_state_id_for_users(
150 conn: &mut PgConnection,
151 user_exercise_state_ids: &[Uuid],
152) -> ModelResult<HashMap<Uuid, TeacherGradingDecision>> {
153 let decisions = sqlx::query_as!(
154 TeacherGradingDecision,
155 r#"
156SELECT DISTINCT ON (user_exercise_state_id) id,
157 user_exercise_state_id,
158 user_id,
159 created_at,
160 updated_at,
161 deleted_at,
162 score_given,
163 teacher_decision,
164 justification,
165 hidden
166FROM teacher_grading_decisions
167WHERE user_exercise_state_id = ANY($1)
168 AND deleted_at IS NULL
169ORDER BY user_exercise_state_id,
170 created_at DESC
171 "#,
172 user_exercise_state_ids,
173 )
174 .fetch_all(conn)
175 .await?;
176
177 let mut res: HashMap<Uuid, TeacherGradingDecision> = HashMap::new();
178 for item in decisions.into_iter() {
179 res.insert(item.user_exercise_state_id, item);
180 }
181
182 Ok(res)
183}
184
185pub async fn get_all_latest_grading_decisions_by_user_id_and_course_id(
186 conn: &mut PgConnection,
187 user_id: Uuid,
188 course_id: Uuid,
189) -> ModelResult<Vec<TeacherGradingDecision>> {
190 let res = sqlx::query_as!(
191 TeacherGradingDecision,
192 r#"
193SELECT DISTINCT ON (user_exercise_state_id)
194 id,
195 user_exercise_state_id,
196 user_id,
197 created_at,
198 updated_at,
199 deleted_at,
200 score_given,
201 teacher_decision,
202 justification,
203 hidden
204FROM teacher_grading_decisions
205WHERE user_exercise_state_id IN (
206 SELECT user_exercise_states.id
207 FROM user_exercise_states
208 WHERE user_exercise_states.user_id = $1
209 AND user_exercise_states.course_id = $2
210 AND user_exercise_states.deleted_at IS NULL
211 )
212 AND deleted_at IS NULL
213 ORDER BY user_exercise_state_id, created_at DESC
214 "#,
215 user_id,
216 course_id,
217 )
218 .fetch_all(conn)
219 .await?;
220 Ok(res)
221}
222
223pub async fn get_all_latest_grading_decisions_by_user_id_and_exam_id(
224 conn: &mut PgConnection,
225 user_id: Uuid,
226 exam_id: Uuid,
227) -> ModelResult<Vec<TeacherGradingDecision>> {
228 let res = sqlx::query_as!(
229 TeacherGradingDecision,
230 r#"
231SELECT DISTINCT ON (user_exercise_state_id)
232 id,
233 user_exercise_state_id,
234 user_id,
235 created_at,
236 updated_at,
237 deleted_at,
238 score_given,
239 teacher_decision,
240 justification,
241 hidden
242FROM teacher_grading_decisions
243WHERE user_exercise_state_id IN (
244 SELECT user_exercise_states.id
245 FROM user_exercise_states
246 WHERE user_exercise_states.user_id = $1
247 AND user_exercise_states.exam_id = $2
248 AND user_exercise_states.deleted_at IS NULL
249 )
250 AND deleted_at IS NULL
251 ORDER BY user_exercise_state_id, created_at DESC
252 "#,
253 user_id,
254 exam_id,
255 )
256 .fetch_all(conn)
257 .await?;
258 Ok(res)
259}
260
261pub async fn update_teacher_grading_decision_hidden_field(
262 conn: &mut PgConnection,
263 teacher_grading_decision_id: Uuid,
264 hidden: bool,
265) -> ModelResult<TeacherGradingDecision> {
266 let res = sqlx::query_as!(
267 TeacherGradingDecision,
268 r#"
269 UPDATE teacher_grading_decisions
270 SET hidden = $1
271 WHERE id = $2
272 RETURNING *;
273 "#,
274 hidden,
275 teacher_grading_decision_id
276 )
277 .fetch_one(conn)
278 .await?;
279 Ok(res)
280}
281
282pub async fn get_by_ids(
283 conn: &mut PgConnection,
284 ids: &[Uuid],
285) -> ModelResult<Vec<TeacherGradingDecision>> {
286 let res = sqlx::query_as!(
287 TeacherGradingDecision,
288 r#"
289SELECT *
290FROM teacher_grading_decisions
291WHERE id = ANY($1)
292 AND deleted_at IS NULL
293 "#,
294 ids
295 )
296 .fetch_all(conn)
297 .await?;
298 Ok(res)
299}