Skip to main content

headless_lms_models/
completion_registration_credit_justifications.rs

1//! Why a student chose credits over a certificate, on the old completion registration page.
2//!
3//! Advisory throughout: nothing reads these rows to decide anything. They exist so we can tell how
4//! often a student without Finnish e-identification still needs the credits.
5
6use crate::prelude::*;
7
8#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
9pub struct CompletionRegistrationCreditJustification {
10    pub id: Uuid,
11    pub created_at: DateTime<Utc>,
12    pub updated_at: DateTime<Utc>,
13    pub deleted_at: Option<DateTime<Utc>>,
14    pub course_module_completion_id: Uuid,
15    pub user_id: Uuid,
16    pub justification: String,
17}
18
19pub async fn get_by_completion_id(
20    conn: &mut PgConnection,
21    course_module_completion_id: Uuid,
22) -> ModelResult<Option<CompletionRegistrationCreditJustification>> {
23    let res = sqlx::query_as!(
24        CompletionRegistrationCreditJustification,
25        "
26SELECT *
27FROM completion_registration_credit_justifications
28WHERE course_module_completion_id = $1
29  AND deleted_at IS NULL
30        ",
31        course_module_completion_id,
32    )
33    .fetch_optional(conn)
34    .await?;
35    Ok(res)
36}
37
38/// Records what the student wrote, replacing anything they wrote before.
39pub async fn upsert(
40    conn: &mut PgConnection,
41    course_module_completion_id: Uuid,
42    user_id: Uuid,
43    justification: &str,
44) -> ModelResult<CompletionRegistrationCreditJustification> {
45    let res = sqlx::query_as!(
46        CompletionRegistrationCreditJustification,
47        "
48INSERT INTO completion_registration_credit_justifications (
49    course_module_completion_id,
50    user_id,
51    justification
52  )
53VALUES ($1, $2, $3)
54ON CONFLICT (course_module_completion_id, deleted_at) DO
55UPDATE
56SET justification = EXCLUDED.justification
57RETURNING *
58        ",
59        course_module_completion_id,
60        user_id,
61        justification,
62    )
63    .fetch_one(conn)
64    .await?;
65    Ok(res)
66}
67
68/// Erases everything the user wrote here. Called from account deletion, which is why this is a
69/// hard delete: the text is the student's own prose about their own circumstances.
70pub async fn delete_all_by_user_id(conn: &mut PgConnection, user_id: Uuid) -> ModelResult<()> {
71    sqlx::query!(
72        "
73DELETE FROM completion_registration_credit_justifications
74WHERE user_id = $1
75        ",
76        user_id,
77    )
78    .execute(conn)
79    .await?;
80    Ok(())
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::{
87        course_module_completions::{
88            self, CourseModuleCompletionGranter, NewCourseModuleCompletion,
89        },
90        test_helper::*,
91        users,
92    };
93
94    async fn complete(
95        conn: &mut PgConnection,
96        course_id: Uuid,
97        course_module_id: Uuid,
98        user_id: Uuid,
99    ) -> Uuid {
100        course_module_completions::insert(
101            conn,
102            PKeyPolicy::Generate,
103            &NewCourseModuleCompletion {
104                course_id,
105                course_module_id,
106                user_id,
107                completion_date: Utc::now(),
108                completion_registration_attempt_date: None,
109                completion_language: "en-US".to_string(),
110                eligible_for_ects: true,
111                email: "student@example.com".to_string(),
112                grade: None,
113                passed: true,
114            },
115            CourseModuleCompletionGranter::Automatic,
116        )
117        .await
118        .unwrap()
119        .id
120    }
121
122    #[tokio::test]
123    async fn replaces_the_earlier_answer_instead_of_keeping_both() {
124        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
125        let completion = complete(tx.as_mut(), course, course_module.id, user).await;
126
127        upsert(tx.as_mut(), completion, user, "First answer.")
128            .await
129            .unwrap();
130        upsert(tx.as_mut(), completion, user, "Second answer.")
131            .await
132            .unwrap();
133
134        let stored = get_by_completion_id(tx.as_mut(), completion)
135            .await
136            .unwrap()
137            .unwrap();
138        assert_eq!(stored.justification, "Second answer.");
139    }
140
141    #[tokio::test]
142    async fn deleting_the_account_takes_the_answer_with_it() {
143        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
144        let completion = complete(tx.as_mut(), course, course_module.id, user).await;
145        upsert(tx.as_mut(), completion, user, "My employer needs them.")
146            .await
147            .unwrap();
148
149        users::delete_user(tx.as_mut(), user).await.unwrap();
150
151        assert!(
152            get_by_completion_id(tx.as_mut(), completion)
153                .await
154                .unwrap()
155                .is_none()
156        );
157    }
158}