1use 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
38pub 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
68pub 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}