headless_lms_models/
exercise_reset_logs.rs

1use crate::prelude::*;
2use sqlx::PgConnection;
3
4#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
5#[cfg_attr(feature = "ts_rs", derive(TS))]
6pub struct ExerciseResetLog {
7    pub id: Uuid,
8    pub reset_by: Option<Uuid>,
9    pub reset_by_first_name: Option<String>,
10    pub reset_by_last_name: Option<String>,
11    pub reset_for: Uuid,
12    pub exercise_id: Uuid,
13    pub exercise_name: String,
14    pub reason: Option<String>,
15    pub course_id: Uuid,
16    pub reset_at: DateTime<Utc>,
17    pub created_at: DateTime<Utc>,
18    pub updated_at: DateTime<Utc>,
19    pub deleted_at: Option<DateTime<Utc>>,
20}
21
22/// Adds a log entry for reset exercises for a user
23pub async fn log_exercise_reset(
24    tx: &mut PgConnection,
25    reset_by: Option<Uuid>,
26    user_id: Uuid,
27    exercise_ids: &[Uuid],
28    course_id: Uuid,
29    reason: Option<String>,
30) -> Result<(), sqlx::Error> {
31    sqlx::query!(
32        r#"
33INSERT INTO exercise_reset_logs (
34    reset_by,
35    reset_for,
36    exercise_id,
37    course_id,
38    reason,
39    reset_at
40  )
41SELECT $1,
42  $2,
43  unnest($3::uuid []),
44  $4,
45  $5,
46  NOW()
47      "#,
48        reset_by,
49        user_id,
50        &exercise_ids,
51        course_id,
52        reason
53    )
54    .execute(&mut *tx)
55    .await?;
56
57    Ok(())
58}
59
60pub async fn get_exercise_reset_logs_for_user(
61    conn: &mut PgConnection,
62    user_id: Uuid,
63) -> ModelResult<Vec<ExerciseResetLog>> {
64    let result = sqlx::query_as!(
65        ExerciseResetLog,
66        r#"
67SELECT erl.id,
68  erl.reset_by,
69  ud.first_name AS reset_by_first_name,
70  ud.last_name AS reset_by_last_name,
71  erl.reset_for,
72  erl.exercise_id,
73  e.name AS exercise_name,
74  erl.reason,
75  erl.course_id,
76  erl.reset_at,
77  erl.created_at,
78  erl.updated_at,
79  erl.deleted_at
80FROM exercise_reset_logs erl
81  JOIN exercises e ON erl.exercise_id = e.id
82  LEFT JOIN user_details ud ON erl.reset_by = ud.user_id
83WHERE erl.reset_for = $1
84ORDER BY erl.reset_at DESC"#,
85        user_id
86    )
87    .fetch_all(&mut *conn)
88    .await?;
89
90    Ok(result)
91}
92
93/// Check if the user's exercise has been reset and no new submissions have been made since.
94pub async fn user_should_see_reset_message_for_exercise(
95    conn: &mut PgConnection,
96    user_id: Uuid,
97    exercise_id: Uuid,
98) -> ModelResult<Option<String>> {
99    let row = sqlx::query!(
100        r#"
101SELECT erl.reset_by, erl.reason
102FROM exercise_reset_logs erl
103LEFT JOIN exercise_slide_submissions es
104  ON es.user_id = erl.reset_for
105  AND es.exercise_id = erl.exercise_id
106  AND es.created_at > erl.reset_at
107WHERE erl.reset_for = $1
108  AND erl.exercise_id = $2
109  AND es.id IS NULL
110ORDER BY erl.reset_at DESC
111LIMIT 1
112"#,
113        user_id,
114        exercise_id
115    )
116    .fetch_optional(conn)
117    .await?;
118
119    let message = row.and_then(|r| {
120        if let Some(reason) = r.reason {
121            Some(reason)
122        } else if r.reset_by.is_some() {
123            Some("reset-by-staff".to_string())
124        } else {
125            None
126        }
127    });
128
129    Ok(message)
130}