Skip to main content

headless_lms_models/
research_forms.rs

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 NewResearchFormQuestion {
39    pub question_id: Uuid,
40    pub course_id: Uuid,
41    pub research_consent_form_id: Uuid,
42    pub question: String,
43}
44
45#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
46
47pub struct NewResearchFormQuestionAnswer {
48    pub user_id: Uuid,
49    pub research_form_question_id: Uuid,
50    pub research_consent: bool,
51}
52
53#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
54
55pub struct ResearchFormQuestionAnswer {
56    pub id: Uuid,
57    pub user_id: Uuid,
58    pub course_id: Uuid,
59    pub research_form_question_id: Uuid,
60    pub research_consent: bool,
61    pub created_at: DateTime<Utc>,
62    pub updated_at: DateTime<Utc>,
63    pub deleted_at: Option<DateTime<Utc>>,
64}
65
66impl NewResearchForm {
67    /// Creates `NewResearchForm` with provided values that is public by default.
68    pub fn new(course_id: Uuid) -> Self {
69        Self {
70            course_id,
71            content: Default::default(),
72        }
73    }
74
75    /// Sets the content of this research form.
76    pub fn set_content(mut self, content: serde_json::Value) -> Self {
77        self.content = content;
78        self
79    }
80}
81
82pub async fn upsert_research_form(
83    conn: &mut PgConnection,
84    pkey_policy: PKeyPolicy<Uuid>,
85    new_research_form: &NewResearchForm,
86) -> ModelResult<ResearchForm> {
87    let form_res = sqlx::query_as!(
88        ResearchForm,
89        "
90INSERT INTO course_specific_research_consent_forms (
91    id,
92    course_id,
93    content
94  )
95VALUES ($1, $2, $3) ON CONFLICT (course_id, deleted_at)
96DO UPDATE SET content = $3
97RETURNING *
98",
99        pkey_policy.into_uuid(),
100        new_research_form.course_id,
101        serde_json::to_value(new_research_form.content.clone())?,
102    )
103    .fetch_one(conn)
104    .await?;
105    Ok(form_res)
106}
107
108pub async fn get_research_form_with_course_id(
109    conn: &mut PgConnection,
110    course_id: Uuid,
111) -> ModelResult<ResearchForm> {
112    let form_res = sqlx::query_as!(
113        ResearchForm,
114        "
115SELECT * FROM course_specific_research_consent_forms
116WHERE course_id = $1
117AND deleted_at IS NULL
118",
119        course_id,
120    )
121    .fetch_one(conn)
122    .await?;
123    Ok(form_res)
124}
125
126pub async fn upsert_research_form_questions(
127    conn: &mut PgConnection,
128    questions: &[NewResearchFormQuestion],
129) -> ModelResult<Vec<ResearchFormQuestion>> {
130    let mut tx = conn.begin().await?;
131
132    let mut inserted_questions = Vec::new();
133
134    for question in questions {
135        // Blocks a save from hijacking a question id that belongs to another course; the caller
136        // must still send a validated course_id/form_id pair for a normal save to succeed.
137        let form_res = sqlx::query_as!(
138            ResearchFormQuestion,
139            "
140INSERT INTO course_specific_consent_form_questions (
141    id,
142    course_id,
143    research_consent_form_id,
144    question
145  )
146VALUES ($1, $2, $3, $4) ON CONFLICT (id)
147DO UPDATE SET question = $4,
148research_consent_form_id = $3,
149deleted_at = NULL
150WHERE course_specific_consent_form_questions.course_id = excluded.course_id
151RETURNING *
152",
153            question.question_id,
154            question.course_id,
155            question.research_consent_form_id,
156            question.question
157        )
158        .fetch_optional(&mut *tx)
159        .await?
160        .ok_or_else(|| {
161            model_err!(
162                InvalidRequest,
163                "Question id belongs to a different course".to_string()
164            )
165        })?;
166
167        inserted_questions.push(form_res);
168    }
169
170    tx.commit().await?;
171
172    Ok(inserted_questions)
173}
174
175pub async fn get_research_form_questions_with_course_id(
176    conn: &mut PgConnection,
177    course_id: Uuid,
178) -> ModelResult<Vec<ResearchFormQuestion>> {
179    let form_res = sqlx::query_as!(
180        ResearchFormQuestion,
181        "
182SELECT * FROM course_specific_consent_form_questions
183WHERE course_id = $1
184AND deleted_at IS NULL
185",
186        course_id,
187    )
188    .fetch_all(conn)
189    .await?;
190    Ok(form_res)
191}
192
193pub async fn get_question_by_id(
194    conn: &mut PgConnection,
195    question_id: Uuid,
196) -> ModelResult<ResearchFormQuestion> {
197    let res = sqlx::query_as!(
198        ResearchFormQuestion,
199        "
200SELECT *
201FROM course_specific_consent_form_questions
202WHERE id = $1
203  AND deleted_at IS NULL
204        ",
205        question_id
206    )
207    .fetch_one(conn)
208    .await?;
209    Ok(res)
210}
211
212pub struct ExportedCourseResearchFormQustionAnswer {
213    pub course_id: Uuid,
214    pub research_consent_form_id: Uuid,
215    pub research_form_question_id: Uuid,
216    pub question: String,
217    pub user_id: Uuid,
218    pub research_consent: bool,
219    pub created_at: DateTime<Utc>,
220    pub updated_at: DateTime<Utc>,
221}
222
223pub fn stream_course_research_form_user_answers(
224    conn: &mut PgConnection,
225    course_id: Uuid,
226) -> impl Stream<Item = sqlx::Result<ExportedCourseResearchFormQustionAnswer>> + '_ {
227    sqlx::query_as!(
228        ExportedCourseResearchFormQustionAnswer,
229        r#"
230    SELECT DISTINCT ON (a.research_form_question_id, a.user_id)
231        q.course_id,
232        q.research_consent_form_id,
233        a.research_form_question_id,
234        q.question,
235        a.user_id,
236        a.research_consent,
237        a.created_at,
238        a.updated_at
239        FROM course_specific_consent_form_answers a
240    LEFT JOIN course_specific_consent_form_questions q ON a.research_form_question_id = q.id
241    WHERE a.course_id = $1
242    AND a.deleted_at IS NULL
243    AND q.deleted_at IS NULL
244    ORDER BY a.user_id, a.research_form_question_id, a.updated_at DESC
245    "#,
246        course_id
247    )
248    .fetch(conn)
249}
250
251pub async fn upsert_research_form_anwser(
252    conn: &mut PgConnection,
253    course_id: Uuid,
254    answer: &NewResearchFormQuestionAnswer,
255) -> ModelResult<Uuid> {
256    let form_res = sqlx::query!(
257        "
258INSERT INTO course_specific_consent_form_answers (
259    user_id,
260    course_id,
261    research_form_question_id,
262    research_consent
263  )
264VALUES ($1, $2, $3, $4) ON CONFLICT (user_id, research_form_question_id)
265DO UPDATE SET research_consent = $4
266RETURNING *
267",
268        answer.user_id,
269        course_id,
270        answer.research_form_question_id,
271        answer.research_consent
272    )
273    .fetch_one(conn)
274    .await?;
275    Ok(form_res.id)
276}
277
278pub async fn upsert_answer_for_user_id_and_question_id(
279    conn: &mut PgConnection,
280    user_id: Uuid,
281    course_id: Uuid,
282    question_id: Uuid,
283    research_consent: bool,
284) -> ModelResult<Uuid> {
285    let res = sqlx::query!(
286        "
287INSERT INTO course_specific_consent_form_answers (
288    user_id,
289    course_id,
290    research_form_question_id,
291    research_consent
292)
293SELECT $1, q.course_id, q.id, $4
294FROM course_specific_consent_form_questions q
295WHERE q.id = $2
296  AND q.course_id = $3
297  AND q.deleted_at IS NULL
298ON CONFLICT (user_id, research_form_question_id)
299DO UPDATE SET
300  course_id = excluded.course_id,
301  research_consent = excluded.research_consent,
302  deleted_at = NULL
303RETURNING id
304        ",
305        user_id,
306        question_id,
307        course_id,
308        research_consent
309    )
310    .fetch_one(conn)
311    .await?;
312    Ok(res.id)
313}
314
315pub async fn get_research_form_answers_with_user_id(
316    conn: &mut PgConnection,
317    course_id: Uuid,
318    user_id: Uuid,
319) -> ModelResult<Vec<ResearchFormQuestionAnswer>> {
320    let form_res = sqlx::query_as!(
321        ResearchFormQuestionAnswer,
322        "
323SELECT * FROM course_specific_consent_form_answers
324WHERE course_id = $1 AND user_id = $2
325AND deleted_at IS NULL
326",
327        course_id,
328        user_id
329    )
330    .fetch_all(conn)
331    .await?;
332    Ok(form_res)
333}
334
335pub async fn get_all_research_form_answers_with_user_id(
336    conn: &mut PgConnection,
337    user_id: Uuid,
338) -> ModelResult<Vec<ResearchFormQuestionAnswer>> {
339    let form_res = sqlx::query_as!(
340        ResearchFormQuestionAnswer,
341        "
342SELECT * FROM course_specific_consent_form_answers
343WHERE user_id = $1
344AND deleted_at IS NULL
345",
346        user_id
347    )
348    .fetch_all(conn)
349    .await?;
350    Ok(form_res)
351}
352
353pub async fn get_all_research_form_answers_with_user_course_and_question_id(
354    conn: &mut PgConnection,
355    user_id: Uuid,
356    course_id: Uuid,
357    course_specific_consent_form_question_id: Uuid,
358) -> ModelResult<Vec<ResearchFormQuestionAnswer>> {
359    let form_res = sqlx::query_as!(
360        ResearchFormQuestionAnswer,
361        "
362SELECT * FROM course_specific_consent_form_answers
363WHERE user_id = $1
364AND course_id = $2
365AND research_form_question_id = $3
366AND deleted_at IS NULL
367",
368        user_id,
369        course_id,
370        course_specific_consent_form_question_id
371    )
372    .fetch_all(conn)
373    .await?;
374    Ok(form_res)
375}