1use futures::Stream;
2use utoipa::ToSchema;
3
4use crate::prelude::*;
5
6#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
7
8pub struct ResearchForm {
9 pub id: Uuid,
10 pub course_id: Uuid,
11 pub content: serde_json::Value,
12 pub created_at: DateTime<Utc>,
13 pub updated_at: DateTime<Utc>,
14 pub deleted_at: Option<DateTime<Utc>>,
15}
16
17#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
18
19pub struct NewResearchForm {
20 pub course_id: Uuid,
21 pub content: serde_json::Value,
22}
23
24#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
25
26pub struct ResearchFormQuestion {
27 pub id: Uuid,
28 pub course_id: Uuid,
29 pub research_consent_form_id: Uuid,
30 pub question: String,
31 pub created_at: DateTime<Utc>,
32 pub updated_at: DateTime<Utc>,
33 pub deleted_at: Option<DateTime<Utc>>,
34}
35
36#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
37
38pub struct NewResearchFormQuestionAnswer {
39 pub user_id: Uuid,
40 pub research_form_question_id: Uuid,
41 pub research_consent: bool,
42}
43
44#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
45
46pub struct ResearchFormQuestionAnswer {
47 pub id: Uuid,
48 pub user_id: Uuid,
49 pub course_id: Uuid,
50 pub research_form_question_id: Uuid,
51 pub research_consent: bool,
52 pub created_at: DateTime<Utc>,
53 pub updated_at: DateTime<Utc>,
54 pub deleted_at: Option<DateTime<Utc>>,
55}
56
57impl NewResearchForm {
58 pub fn new(course_id: Uuid) -> Self {
60 Self {
61 course_id,
62 content: Default::default(),
63 }
64 }
65
66 pub fn set_content(mut self, content: serde_json::Value) -> Self {
68 self.content = content;
69 self
70 }
71}
72
73pub async fn upsert_research_form(
74 conn: &mut PgConnection,
75 pkey_policy: PKeyPolicy<Uuid>,
76 new_research_form: &NewResearchForm,
77) -> ModelResult<ResearchForm> {
78 let form_res = sqlx::query_as!(
79 ResearchForm,
80 "
81INSERT INTO course_specific_research_consent_forms (
82 id,
83 course_id,
84 content
85 )
86VALUES ($1, $2, $3) ON CONFLICT (course_id, deleted_at)
87DO UPDATE SET content = $3
88RETURNING *
89",
90 pkey_policy.into_uuid(),
91 new_research_form.course_id,
92 serde_json::to_value(new_research_form.content.clone())?,
93 )
94 .fetch_one(conn)
95 .await?;
96 Ok(form_res)
97}
98
99pub async fn get_research_form_with_course_id(
100 conn: &mut PgConnection,
101 course_id: Uuid,
102) -> ModelResult<ResearchForm> {
103 let form_res = sqlx::query_as!(
104 ResearchForm,
105 "
106SELECT * FROM course_specific_research_consent_forms
107WHERE course_id = $1
108AND deleted_at IS NULL
109",
110 course_id,
111 )
112 .fetch_one(conn)
113 .await?;
114 Ok(form_res)
115}
116
117pub async fn save_research_form(
123 conn: &mut PgConnection,
124 course_id: Uuid,
125 content: serde_json::Value,
126) -> ModelResult<ResearchForm> {
127 let questions = questions_in_content(&content)?;
128 if questions.is_empty() {
129 return Err(model_err!(
130 InvalidRequest,
131 "A research form needs at least one question".to_string()
132 ));
133 }
134
135 let mut tx = conn.begin().await?;
136 let form = upsert_research_form(
137 &mut tx,
138 PKeyPolicy::Generate,
139 &NewResearchForm { course_id, content },
140 )
141 .await?;
142
143 for (question_id, question) in &questions {
144 sqlx::query!(
146 "
147INSERT INTO course_specific_consent_form_questions (
148 id,
149 course_id,
150 research_consent_form_id,
151 question
152 )
153VALUES ($1, $2, $3, $4) ON CONFLICT (id)
154DO UPDATE SET question = $4,
155research_consent_form_id = $3,
156deleted_at = NULL
157WHERE course_specific_consent_form_questions.course_id = excluded.course_id
158RETURNING id
159",
160 question_id,
161 course_id,
162 form.id,
163 question
164 )
165 .fetch_optional(&mut *tx)
166 .await?
167 .ok_or_else(|| {
168 model_err!(
169 InvalidRequest,
170 "Question id belongs to a different course".to_string()
171 )
172 })?;
173 }
174
175 let question_ids: Vec<Uuid> = questions.iter().map(|(id, _)| *id).collect();
176 sqlx::query!(
177 "
178UPDATE course_specific_consent_form_questions
179SET deleted_at = NOW()
180WHERE research_consent_form_id = $1
181 AND deleted_at IS NULL
182 AND id <> ALL($2::uuid [])
183",
184 form.id,
185 &question_ids
186 )
187 .execute(&mut *tx)
188 .await?;
189
190 tx.commit().await?;
191 Ok(form)
192}
193
194fn questions_in_content(content: &serde_json::Value) -> ModelResult<Vec<(Uuid, String)>> {
196 let blocks = content.as_array().map(Vec::as_slice).unwrap_or_default();
197 blocks
198 .iter()
199 .filter(|block| block["name"] == "moocfi/research-consent-question")
200 .map(|block| {
201 let id = block["clientId"]
202 .as_str()
203 .and_then(|id| Uuid::parse_str(id).ok())
204 .ok_or_else(|| {
205 model_err!(InvalidRequest, "Question has no valid id".to_string())
206 })?;
207 let question = block["attributes"]["content"]
208 .as_str()
209 .unwrap_or_default()
210 .trim()
211 .to_string();
212 if question.is_empty() {
213 return Err(model_err!(
214 InvalidRequest,
215 "Research form questions cannot be empty".to_string()
216 ));
217 }
218 Ok((id, question))
219 })
220 .collect()
221}
222
223pub async fn get_research_form_questions_with_course_id(
224 conn: &mut PgConnection,
225 course_id: Uuid,
226) -> ModelResult<Vec<ResearchFormQuestion>> {
227 let form_res = sqlx::query_as!(
228 ResearchFormQuestion,
229 "
230SELECT * FROM course_specific_consent_form_questions
231WHERE course_id = $1
232AND deleted_at IS NULL
233",
234 course_id,
235 )
236 .fetch_all(conn)
237 .await?;
238 Ok(form_res)
239}
240
241pub async fn get_question_by_id(
242 conn: &mut PgConnection,
243 question_id: Uuid,
244) -> ModelResult<ResearchFormQuestion> {
245 let res = sqlx::query_as!(
246 ResearchFormQuestion,
247 "
248SELECT *
249FROM course_specific_consent_form_questions
250WHERE id = $1
251 AND deleted_at IS NULL
252 ",
253 question_id
254 )
255 .fetch_one(conn)
256 .await?;
257 Ok(res)
258}
259
260pub struct ExportedCourseResearchFormQustionAnswer {
261 pub course_id: Uuid,
262 pub research_consent_form_id: Uuid,
263 pub research_form_question_id: Uuid,
264 pub question: String,
265 pub user_id: Uuid,
266 pub research_consent: bool,
267 pub created_at: DateTime<Utc>,
268 pub updated_at: DateTime<Utc>,
269}
270
271pub fn stream_course_research_form_user_answers(
272 conn: &mut PgConnection,
273 course_id: Uuid,
274) -> impl Stream<Item = sqlx::Result<ExportedCourseResearchFormQustionAnswer>> + '_ {
275 sqlx::query_as!(
276 ExportedCourseResearchFormQustionAnswer,
277 r#"
278 SELECT DISTINCT ON (a.research_form_question_id, a.user_id)
279 q.course_id,
280 q.research_consent_form_id,
281 a.research_form_question_id,
282 q.question,
283 a.user_id,
284 a.research_consent,
285 a.created_at,
286 a.updated_at
287 FROM course_specific_consent_form_answers a
288 LEFT JOIN course_specific_consent_form_questions q ON a.research_form_question_id = q.id
289 WHERE a.course_id = $1
290 AND a.deleted_at IS NULL
291 AND q.deleted_at IS NULL
292 ORDER BY a.user_id, a.research_form_question_id, a.updated_at DESC
293 "#,
294 course_id
295 )
296 .fetch(conn)
297}
298
299pub async fn upsert_research_form_anwser(
300 conn: &mut PgConnection,
301 course_id: Uuid,
302 answer: &NewResearchFormQuestionAnswer,
303) -> ModelResult<Uuid> {
304 let form_res = sqlx::query!(
305 "
306INSERT INTO course_specific_consent_form_answers (
307 user_id,
308 course_id,
309 research_form_question_id,
310 research_consent
311 )
312VALUES ($1, $2, $3, $4) ON CONFLICT (user_id, research_form_question_id)
313DO UPDATE SET research_consent = $4
314RETURNING *
315",
316 answer.user_id,
317 course_id,
318 answer.research_form_question_id,
319 answer.research_consent
320 )
321 .fetch_one(conn)
322 .await?;
323 Ok(form_res.id)
324}
325
326pub async fn upsert_answer_for_user_id_and_question_id(
327 conn: &mut PgConnection,
328 user_id: Uuid,
329 course_id: Uuid,
330 question_id: Uuid,
331 research_consent: bool,
332) -> ModelResult<Uuid> {
333 let res = sqlx::query!(
334 "
335INSERT INTO course_specific_consent_form_answers (
336 user_id,
337 course_id,
338 research_form_question_id,
339 research_consent
340)
341SELECT $1, q.course_id, q.id, $4
342FROM course_specific_consent_form_questions q
343WHERE q.id = $2
344 AND q.course_id = $3
345 AND q.deleted_at IS NULL
346ON CONFLICT (user_id, research_form_question_id)
347DO UPDATE SET
348 course_id = excluded.course_id,
349 research_consent = excluded.research_consent,
350 deleted_at = NULL
351RETURNING id
352 ",
353 user_id,
354 question_id,
355 course_id,
356 research_consent
357 )
358 .fetch_one(conn)
359 .await?;
360 Ok(res.id)
361}
362
363pub async fn get_research_form_answers_with_user_id(
364 conn: &mut PgConnection,
365 course_id: Uuid,
366 user_id: Uuid,
367) -> ModelResult<Vec<ResearchFormQuestionAnswer>> {
368 let form_res = sqlx::query_as!(
369 ResearchFormQuestionAnswer,
370 "
371SELECT * FROM course_specific_consent_form_answers
372WHERE course_id = $1 AND user_id = $2
373AND deleted_at IS NULL
374",
375 course_id,
376 user_id
377 )
378 .fetch_all(conn)
379 .await?;
380 Ok(form_res)
381}
382
383pub async fn get_all_research_form_answers_with_user_id(
384 conn: &mut PgConnection,
385 user_id: Uuid,
386) -> ModelResult<Vec<ResearchFormQuestionAnswer>> {
387 let form_res = sqlx::query_as!(
388 ResearchFormQuestionAnswer,
389 "
390SELECT * FROM course_specific_consent_form_answers
391WHERE user_id = $1
392AND deleted_at IS NULL
393",
394 user_id
395 )
396 .fetch_all(conn)
397 .await?;
398 Ok(form_res)
399}
400
401pub async fn get_all_research_form_answers_with_user_course_and_question_id(
402 conn: &mut PgConnection,
403 user_id: Uuid,
404 course_id: Uuid,
405 course_specific_consent_form_question_id: Uuid,
406) -> ModelResult<Vec<ResearchFormQuestionAnswer>> {
407 let form_res = sqlx::query_as!(
408 ResearchFormQuestionAnswer,
409 "
410SELECT * FROM course_specific_consent_form_answers
411WHERE user_id = $1
412AND course_id = $2
413AND research_form_question_id = $3
414AND deleted_at IS NULL
415",
416 user_id,
417 course_id,
418 course_specific_consent_form_question_id
419 )
420 .fetch_all(conn)
421 .await?;
422 Ok(form_res)
423}