headless_lms_models/
peer_or_self_review_questions.rs

1use std::collections::HashMap;
2
3use sqlx::{Postgres, QueryBuilder, Row};
4
5use crate::prelude::*;
6
7#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type)]
8#[sqlx(type_name = "peer_review_question_type", rename_all = "snake_case")]
9#[cfg_attr(feature = "ts_rs", derive(TS))]
10pub enum PeerOrSelfReviewQuestionType {
11    Essay,
12    Scale,
13}
14
15#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
16#[cfg_attr(feature = "ts_rs", derive(TS))]
17pub struct CmsPeerOrSelfReviewQuestion {
18    pub id: Uuid,
19    pub peer_or_self_review_config_id: Uuid,
20    pub order_number: i32,
21    pub question: String,
22    pub question_type: PeerOrSelfReviewQuestionType,
23    pub answer_required: bool,
24    pub weight: f32,
25}
26
27impl From<PeerOrSelfReviewQuestion> for CmsPeerOrSelfReviewQuestion {
28    fn from(prq: PeerOrSelfReviewQuestion) -> Self {
29        Self {
30            id: prq.id,
31            peer_or_self_review_config_id: prq.peer_or_self_review_config_id,
32            order_number: prq.order_number,
33            question: prq.question,
34            question_type: prq.question_type,
35            answer_required: prq.answer_required,
36            weight: prq.weight,
37        }
38    }
39}
40
41#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
42#[cfg_attr(feature = "ts_rs", derive(TS))]
43pub struct PeerOrSelfReviewQuestion {
44    pub id: Uuid,
45    pub created_at: DateTime<Utc>,
46    pub updated_at: DateTime<Utc>,
47    pub deleted_at: Option<DateTime<Utc>>,
48    pub peer_or_self_review_config_id: Uuid,
49    pub order_number: i32,
50    pub question: String,
51    pub question_type: PeerOrSelfReviewQuestionType,
52    pub answer_required: bool,
53    pub weight: f32,
54}
55
56pub async fn insert(
57    conn: &mut PgConnection,
58    pkey_policy: PKeyPolicy<Uuid>,
59    new_peer_review_question: &CmsPeerOrSelfReviewQuestion,
60) -> ModelResult<Uuid> {
61    let res = sqlx::query!(
62        "
63INSERT INTO peer_or_self_review_questions (
64    id,
65    peer_or_self_review_config_id,
66    order_number,
67    question,
68    question_type
69  )
70VALUES ($1, $2, $3, $4, $5)
71RETURNING id
72        ",
73        pkey_policy.into_uuid(),
74        new_peer_review_question.peer_or_self_review_config_id,
75        new_peer_review_question.order_number,
76        new_peer_review_question.question,
77        new_peer_review_question.question_type as PeerOrSelfReviewQuestionType,
78    )
79    .fetch_one(conn)
80    .await?;
81    Ok(res.id)
82}
83
84pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<PeerOrSelfReviewQuestion> {
85    let res = sqlx::query_as!(
86        PeerOrSelfReviewQuestion,
87        r#"
88SELECT id,
89  created_at,
90  updated_at,
91  deleted_at,
92  peer_or_self_review_config_id,
93  order_number,
94  question,
95  question_type AS "question_type: _",
96  answer_required,
97  weight
98FROM peer_or_self_review_questions
99WHERE id = $1
100  AND deleted_at IS NULL;
101        "#,
102        id,
103    )
104    .fetch_one(conn)
105    .await?;
106    Ok(res)
107}
108
109pub async fn get_by_ids(
110    conn: &mut PgConnection,
111    id: &[Uuid],
112) -> ModelResult<Vec<PeerOrSelfReviewQuestion>> {
113    let res = sqlx::query_as!(
114        PeerOrSelfReviewQuestion,
115        r#"
116SELECT id,
117  created_at,
118  updated_at,
119  deleted_at,
120  peer_or_self_review_config_id,
121  order_number,
122  question,
123  question_type AS "question_type: _",
124  answer_required,
125  weight
126FROM peer_or_self_review_questions
127WHERE id IN (
128    SELECT UNNEST($1::uuid [])
129  )
130  AND deleted_at IS NULL;
131        "#,
132        id,
133    )
134    .fetch_all(conn)
135    .await?;
136    Ok(res)
137}
138
139pub async fn get_by_peer_or_self_review_configs_id(
140    conn: &mut PgConnection,
141    peer_review_id: Uuid,
142) -> ModelResult<Vec<PeerOrSelfReviewQuestion>> {
143    let res = sqlx::query_as!(
144        PeerOrSelfReviewQuestion,
145        r#"
146SELECT id,
147    created_at,
148    updated_at,
149    deleted_at,
150    peer_or_self_review_config_id,
151    order_number,
152    question,
153    question_type AS "question_type: _",
154    answer_required,
155    weight
156FROM peer_or_self_review_questions
157WHERE peer_or_self_review_config_id = $1
158  AND deleted_at IS NULL;
159        "#,
160        peer_review_id,
161    )
162    .fetch_all(conn)
163    .await?;
164    Ok(res)
165}
166
167pub async fn get_all_by_peer_or_self_review_config_id(
168    conn: &mut PgConnection,
169    peer_or_self_review_config_id: Uuid,
170) -> ModelResult<Vec<PeerOrSelfReviewQuestion>> {
171    let res = sqlx::query_as!(
172        PeerOrSelfReviewQuestion,
173        r#"
174SELECT id,
175    created_at,
176    updated_at,
177    deleted_at,
178    peer_or_self_review_config_id,
179    order_number,
180    question,
181    question_type AS "question_type: _",
182    answer_required,
183    weight
184FROM peer_or_self_review_questions
185WHERE peer_or_self_review_config_id = $1
186    AND deleted_at IS NULL;
187        "#,
188        peer_or_self_review_config_id
189    )
190    .fetch_all(conn)
191    .await?;
192    Ok(res)
193}
194
195pub async fn get_all_by_peer_or_self_review_config_id_as_map(
196    conn: &mut PgConnection,
197    peer_or_self_review_config_id: Uuid,
198) -> ModelResult<HashMap<Uuid, PeerOrSelfReviewQuestion>> {
199    let res = get_all_by_peer_or_self_review_config_id(conn, peer_or_self_review_config_id)
200        .await?
201        .into_iter()
202        .map(|x| (x.id, x))
203        .collect();
204    Ok(res)
205}
206
207pub async fn get_by_page_id(
208    conn: &mut PgConnection,
209    page_id: Uuid,
210) -> ModelResult<Vec<CmsPeerOrSelfReviewQuestion>> {
211    let res = sqlx::query_as!(
212        CmsPeerOrSelfReviewQuestion,
213        r#"
214SELECT prq.id as id,
215  prq.peer_or_self_review_config_id as peer_or_self_review_config_id,
216  prq.order_number as order_number,
217  prq.question as question,
218  prq.question_type AS "question_type: _",
219  prq.answer_required as answer_required,
220  prq.weight
221from pages p
222  join exercises e on p.id = e.page_id
223  join peer_or_self_review_configs pr on e.id = pr.exercise_id
224  join peer_or_self_review_questions prq on pr.id = prq.peer_or_self_review_config_id
225where p.id = $1
226  AND p.deleted_at IS NULL
227  AND e.deleted_at IS NULL
228  AND pr.deleted_at IS NULL
229  AND prq.deleted_at IS NULL;
230  "#,
231        page_id
232    )
233    .fetch_all(conn)
234    .await?;
235
236    Ok(res)
237}
238
239pub async fn delete_peer_or_self_review_questions_by_peer_or_self_review_config_ids(
240    conn: &mut PgConnection,
241    peer_or_self_review_config_ids: &[Uuid],
242) -> ModelResult<Vec<Uuid>> {
243    let res = sqlx::query!(
244        "
245UPDATE peer_or_self_review_questions
246SET deleted_at = now()
247WHERE peer_or_self_review_config_id = ANY ($1)
248AND deleted_at IS NULL
249RETURNING id;
250    ",
251        peer_or_self_review_config_ids
252    )
253    .fetch_all(conn)
254    .await?
255    .into_iter()
256    .map(|x| x.id)
257    .collect();
258    Ok(res)
259}
260
261pub async fn get_course_default_cms_peer_or_self_review_questions(
262    conn: &mut PgConnection,
263    peer_or_self_review_config_id: Uuid,
264) -> ModelResult<Vec<CmsPeerOrSelfReviewQuestion>> {
265    let res = sqlx::query_as!(
266        CmsPeerOrSelfReviewQuestion,
267        r#"
268SELECT id,
269  peer_or_self_review_config_id,
270  order_number,
271  question_type AS "question_type: _",
272  question,
273  answer_required,
274  weight
275FROM peer_or_self_review_questions
276where peer_or_self_review_config_id = $1
277  AND deleted_at IS NULL;
278    "#,
279        peer_or_self_review_config_id
280    )
281    .fetch_all(conn)
282    .await?;
283
284    Ok(res)
285}
286
287pub async fn upsert_multiple_peer_or_self_review_questions(
288    conn: &mut PgConnection,
289    peer_or_self_review_questions: &[CmsPeerOrSelfReviewQuestion],
290) -> ModelResult<Vec<CmsPeerOrSelfReviewQuestion>> {
291    let mut sql: QueryBuilder<Postgres> = sqlx::QueryBuilder::new(
292        "INSERT INTO peer_or_self_review_questions (id, peer_or_self_review_config_id, order_number, question_type, question, answer_required) ",
293    );
294
295    sql.push_values(peer_or_self_review_questions, |mut x, prq| {
296        x.push_bind(prq.id)
297            .push_bind(prq.peer_or_self_review_config_id)
298            .push_bind(prq.order_number)
299            .push_bind(prq.question_type)
300            .push_bind(prq.question.as_str())
301            .push_bind(prq.answer_required);
302    });
303    sql.push(
304        r#" ON CONFLICT (id) DO
305UPDATE
306SET peer_or_self_review_config_id = excluded.peer_or_self_review_config_id,
307  order_number = excluded.order_number,
308  question_type = excluded.question_type,
309  question = excluded.question,
310  answer_required = excluded.answer_required,
311  deleted_at = NULL
312RETURNING id;
313"#,
314    );
315
316    let ids = sql
317        .build()
318        .fetch_all(&mut *conn)
319        .await?
320        .iter()
321        .map(|x| x.get(0))
322        .collect::<Vec<_>>();
323
324    let res = sqlx::query_as!(
325        CmsPeerOrSelfReviewQuestion,
326        r#"
327SELECT id,
328  peer_or_self_review_config_id,
329  order_number,
330  question,
331  question_type AS "question_type: _",
332  answer_required,
333  weight
334from peer_or_self_review_questions
335WHERE id IN (
336    SELECT UNNEST($1::uuid [])
337  )
338  AND deleted_at IS NULL;
339    "#,
340        &ids
341    )
342    .fetch_all(conn)
343    .await?;
344    Ok(res)
345}
346
347/** Modifies the questions in memory so that the weights sum to either 0 or 1. */
348pub fn normalize_cms_peer_or_self_review_questions(
349    peer_or_self_review_questions: &mut [CmsPeerOrSelfReviewQuestion],
350) {
351    // All scales have to be answered, skipping them does not make sense.
352    for question in peer_or_self_review_questions.iter_mut() {
353        if question.question_type == PeerOrSelfReviewQuestionType::Scale {
354            question.answer_required = true;
355        }
356    }
357    peer_or_self_review_questions.sort_by(|a, b| a.order_number.cmp(&b.order_number));
358    info!(
359        "Peer review question weights before normalization: {:?}",
360        peer_or_self_review_questions
361            .iter()
362            .map(|x| x.weight)
363            .collect::<Vec<_>>()
364    );
365    let (mut allowed_to_have_weight, mut not_allowed_to_have_weight) =
366        peer_or_self_review_questions
367            .iter_mut()
368            .partition::<Vec<_>, _>(|q| q.question_type == PeerOrSelfReviewQuestionType::Scale);
369    let total_weight: f32 = allowed_to_have_weight.iter().map(|x| x.weight).sum();
370    if total_weight == 0.0 {
371        return;
372    }
373    for question in allowed_to_have_weight.iter_mut() {
374        question.weight /= total_weight;
375    }
376    for question in not_allowed_to_have_weight.iter_mut() {
377        question.weight = 0.0;
378    }
379    info!(
380        "Peer review question weights after normalization: {:?}",
381        peer_or_self_review_questions
382            .iter()
383            .map(|x| x.weight)
384            .collect::<Vec<_>>()
385    );
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn test_normalize_cms_peer_or_self_review_questions() {
394        let mut questions = vec![
395            CmsPeerOrSelfReviewQuestion {
396                id: Uuid::new_v4(),
397                peer_or_self_review_config_id: Uuid::new_v4(),
398                order_number: 1,
399                question: String::from("Question 1"),
400                question_type: PeerOrSelfReviewQuestionType::Scale,
401                answer_required: true,
402                weight: 2.0,
403            },
404            CmsPeerOrSelfReviewQuestion {
405                id: Uuid::new_v4(),
406                peer_or_self_review_config_id: Uuid::new_v4(),
407                order_number: 2,
408                question: String::from("Question 2"),
409                question_type: PeerOrSelfReviewQuestionType::Scale,
410                answer_required: true,
411                weight: 3.0,
412            },
413            CmsPeerOrSelfReviewQuestion {
414                id: Uuid::new_v4(),
415                peer_or_self_review_config_id: Uuid::new_v4(),
416                order_number: 3,
417                question: String::from("Question 3"),
418                question_type: PeerOrSelfReviewQuestionType::Essay,
419                answer_required: true,
420                weight: 1.0,
421            },
422        ];
423
424        normalize_cms_peer_or_self_review_questions(&mut questions);
425
426        assert_eq!(questions[0].weight, 2.0 / 5.0);
427        assert_eq!(questions[1].weight, 3.0 / 5.0);
428        assert_eq!(questions[2].weight, 0.0);
429    }
430}