Skip to main content

headless_lms_models/
suspected_cheaters.rs

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}