1use crate::prelude::*;
2use crate::{course_module_completions, course_modules};
3use utoipa::ToSchema;
4
5#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
6
7pub struct SuspectedCheaters {
8 pub id: Uuid,
9 pub user_id: Uuid,
10 pub course_id: Uuid,
11 pub created_at: DateTime<Utc>,
12 pub deleted_at: Option<DateTime<Utc>>,
13 pub updated_at: Option<DateTime<Utc>>,
14 pub total_duration_seconds: Option<i32>,
15 pub total_points: i32,
16 pub is_archived: Option<bool>,
17}
18
19#[derive(Debug, Serialize, Deserialize, ToSchema)]
20
21pub struct ThresholdData {
22 pub duration_seconds: i32,
23}
24
25#[derive(Debug, Serialize, Deserialize)]
26
27pub struct DeletedSuspectedCheater {
28 pub id: i32,
29 pub count: i32,
30}
31
32#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
33
34pub struct Threshold {
35 pub id: Uuid,
36 pub course_module_id: Uuid,
37 pub created_at: DateTime<Utc>,
38 pub updated_at: DateTime<Utc>,
39 pub deleted_at: Option<DateTime<Utc>>,
40 pub duration_seconds: i32,
41}
42
43pub async fn insert(
44 conn: &mut PgConnection,
45 user_id: Uuid,
46 course_id: Uuid,
47 total_duration_seconds: Option<i32>,
48 total_points: i32,
49) -> ModelResult<bool> {
50 let res = sqlx::query!(
51 "
52 INSERT INTO suspected_cheaters (
53 user_id,
54 total_duration_seconds,
55 total_points,
56 course_id
57 )
58 VALUES ($1, $2, $3, $4)
59 ON CONFLICT (user_id, course_id) WHERE deleted_at IS NULL
60 DO UPDATE SET
61 total_duration_seconds = EXCLUDED.total_duration_seconds,
62 total_points = EXCLUDED.total_points
63 RETURNING is_archived
64 ",
65 user_id,
66 total_duration_seconds,
67 total_points,
68 course_id
69 )
70 .fetch_one(&mut *conn)
71 .await?;
72 Ok(!res.is_archived)
73}
74
75pub async fn insert_thresholds(
76 conn: &mut PgConnection,
77 course_id: Uuid,
78 duration_seconds: i32,
79) -> ModelResult<Threshold> {
80 let default_module = course_modules::get_default_by_course_id(conn, course_id).await?;
81
82 let threshold = sqlx::query_as!(
83 Threshold,
84 "
85 INSERT INTO cheater_thresholds (
86 course_module_id,
87 duration_seconds
88 )
89 VALUES ($1, $2)
90 ON CONFLICT (course_module_id)
91 DO UPDATE SET
92 duration_seconds = EXCLUDED.duration_seconds,
93 deleted_at = NULL
94 RETURNING *
95 ",
96 default_module.id,
97 duration_seconds,
98 )
99 .fetch_one(conn)
100 .await?;
101
102 Ok(threshold)
103}
104
105pub async fn get_thresholds_by_id(
106 conn: &mut PgConnection,
107 course_id: Uuid,
108) -> ModelResult<Threshold> {
109 let default_module = course_modules::get_default_by_course_id(conn, course_id).await?;
110
111 let thresholds = sqlx::query_as!(
112 Threshold,
113 "
114 SELECT id,
115 course_module_id,
116 duration_seconds,
117 created_at,
118 updated_at,
119 deleted_at
120 FROM cheater_thresholds
121 WHERE course_module_id = $1
122 AND deleted_at IS NULL;
123 ",
124 default_module.id
125 )
126 .fetch_one(conn)
127 .await?;
128 Ok(thresholds)
129}
130
131pub async fn archive_suspected_cheater(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
132 sqlx::query!(
133 "
134 UPDATE suspected_cheaters
135 SET is_archived = TRUE
136 WHERE user_id = $1
137 ",
138 id
139 )
140 .execute(conn)
141 .await?;
142 Ok(())
143}
144
145pub async fn approve_suspected_cheater(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
146 sqlx::query!(
147 "
148 UPDATE suspected_cheaters
149 SET is_archived = FALSE
150 WHERE user_id = $1
151 ",
152 id
153 )
154 .execute(conn)
155 .await?;
156 Ok(())
157}
158
159pub async fn get_by_user_id_and_course_id(
160 conn: &mut PgConnection,
161 user_id: Uuid,
162 course_id: Uuid,
163) -> ModelResult<SuspectedCheaters> {
164 let cheater = sqlx::query_as!(
165 SuspectedCheaters,
166 "
167SELECT *
168FROM suspected_cheaters
169WHERE user_id = $1
170 AND course_id = $2
171 AND deleted_at IS NULL;
172 ",
173 user_id,
174 course_id
175 )
176 .fetch_one(conn)
177 .await?;
178 Ok(cheater)
179}
180
181pub async fn archive_by_user_id_and_course_id(
182 conn: &mut PgConnection,
183 user_id: Uuid,
184 course_id: Uuid,
185) -> ModelResult<SuspectedCheaters> {
186 let cheater = sqlx::query_as!(
187 SuspectedCheaters,
188 "
189UPDATE suspected_cheaters
190SET is_archived = TRUE
191WHERE user_id = $1
192 AND course_id = $2
193 AND deleted_at IS NULL
194RETURNING *
195 ",
196 user_id,
197 course_id
198 )
199 .fetch_one(&mut *conn)
200 .await?;
201 course_module_completions::update_needs_to_be_reviewed_by_course_and_user_ids(
202 conn, course_id, user_id, false,
203 )
204 .await?;
205 Ok(cheater)
206}
207
208pub async fn approve_by_user_id_and_course_id(
209 conn: &mut PgConnection,
210 user_id: Uuid,
211 course_id: Uuid,
212) -> ModelResult<SuspectedCheaters> {
213 let cheater = sqlx::query_as!(
214 SuspectedCheaters,
215 "
216UPDATE suspected_cheaters
217SET is_archived = FALSE
218WHERE user_id = $1
219 AND course_id = $2
220 AND deleted_at IS NULL
221RETURNING *
222 ",
223 user_id,
224 course_id
225 )
226 .fetch_one(conn)
227 .await?;
228 Ok(cheater)
229}
230
231pub async fn get_suspected_cheaters_by_id(
232 conn: &mut PgConnection,
233 id: Uuid,
234) -> ModelResult<SuspectedCheaters> {
235 let cheaters = sqlx::query_as!(
236 SuspectedCheaters,
237 "
238 SELECT *
239 FROM suspected_cheaters
240 WHERE user_id = $1
241 AND deleted_at IS NULL;
242 ",
243 id
244 )
245 .fetch_one(conn)
246 .await?;
247 Ok(cheaters)
248}
249
250pub async fn get_all_suspected_cheaters_in_course(
251 conn: &mut PgConnection,
252 course_id: Uuid,
253 archive: bool,
254) -> ModelResult<Vec<SuspectedCheaters>> {
255 let cheaters = sqlx::query_as!(
256 SuspectedCheaters,
257 "
258SELECT *
259FROM suspected_cheaters
260WHERE course_id = $1
261 AND is_archived = $2
262 AND deleted_at IS NULL;
263 ",
264 course_id,
265 archive
266 )
267 .fetch_all(conn)
268 .await?;
269 Ok(cheaters)
270}
271
272pub async fn insert_thresholds_by_module_id(
273 conn: &mut PgConnection,
274 course_module_id: Uuid,
275 duration_seconds: i32,
276) -> ModelResult<Threshold> {
277 let threshold = sqlx::query_as!(
278 Threshold,
279 "
280 INSERT INTO cheater_thresholds (
281 course_module_id,
282 duration_seconds
283 )
284 VALUES ($1, $2)
285 ON CONFLICT (course_module_id)
286 DO UPDATE SET
287 duration_seconds = EXCLUDED.duration_seconds,
288 deleted_at = NULL
289 RETURNING *
290 ",
291 course_module_id,
292 duration_seconds,
293 )
294 .fetch_one(conn)
295 .await?;
296
297 Ok(threshold)
298}
299
300pub async fn get_thresholds_by_module_id(
301 conn: &mut PgConnection,
302 course_module_id: Uuid,
303) -> ModelResult<Option<Threshold>> {
304 let threshold = sqlx::query_as!(
305 Threshold,
306 "
307 SELECT id,
308 course_module_id,
309 duration_seconds,
310 created_at,
311 updated_at,
312 deleted_at
313 FROM cheater_thresholds
314 WHERE course_module_id = $1
315 AND deleted_at IS NULL;
316 ",
317 course_module_id
318 )
319 .fetch_optional(conn)
320 .await?;
321 Ok(threshold)
322}
323
324pub async fn get_all_thresholds_for_course(
325 conn: &mut PgConnection,
326 course_id: Uuid,
327) -> ModelResult<Vec<Threshold>> {
328 let thresholds = sqlx::query_as!(
329 Threshold,
330 "
331 SELECT ct.id,
332 ct.course_module_id,
333 ct.duration_seconds,
334 ct.created_at,
335 ct.updated_at,
336 ct.deleted_at
337 FROM cheater_thresholds ct
338 JOIN course_modules cm ON ct.course_module_id = cm.id
339 WHERE cm.course_id = $1
340 AND ct.deleted_at IS NULL
341 AND cm.deleted_at IS NULL;
342 ",
343 course_id
344 )
345 .fetch_all(conn)
346 .await?;
347 Ok(thresholds)
348}
349
350pub async fn delete_threshold_for_module(
351 conn: &mut PgConnection,
352 course_module_id: Uuid,
353) -> ModelResult<()> {
354 sqlx::query!(
355 "
356 UPDATE cheater_thresholds
357 SET deleted_at = NOW()
358 WHERE course_module_id = $1
359 AND deleted_at IS NULL
360 ",
361 course_module_id
362 )
363 .execute(conn)
364 .await?;
365 Ok(())
366}