Skip to main content

headless_lms_models/
peer_or_self_review_questions.rs

1use std::collections::HashMap;
2
3use sqlx::{Postgres, QueryBuilder, Row};
4use utoipa::ToSchema;
5
6use crate::prelude::*;
7
8#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type, ToSchema)]
9#[sqlx(type_name = "peer_review_question_type", rename_all = "snake_case")]
10pub enum PeerOrSelfReviewQuestionType {
11    Essay,
12    Scale,
13}
14
15#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
16
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, ToSchema)]
42
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 *
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 *
89FROM peer_or_self_review_questions
90WHERE id = $1
91  AND deleted_at IS NULL;
92        "#,
93        id,
94    )
95    .fetch_one(conn)
96    .await?;
97    Ok(res)
98}
99
100pub async fn get_by_ids(
101    conn: &mut PgConnection,
102    id: &[Uuid],
103) -> ModelResult<Vec<PeerOrSelfReviewQuestion>> {
104    let res = sqlx::query_as!(
105        PeerOrSelfReviewQuestion,
106        r#"
107SELECT *
108FROM peer_or_self_review_questions
109WHERE id IN (
110    SELECT UNNEST($1::uuid [])
111  )
112  AND deleted_at IS NULL;
113        "#,
114        id,
115    )
116    .fetch_all(conn)
117    .await?;
118    Ok(res)
119}
120
121pub async fn get_by_peer_or_self_review_configs_id(
122    conn: &mut PgConnection,
123    peer_review_id: Uuid,
124) -> ModelResult<Vec<PeerOrSelfReviewQuestion>> {
125    let res = sqlx::query_as!(
126        PeerOrSelfReviewQuestion,
127        r#"
128SELECT *
129FROM peer_or_self_review_questions
130WHERE peer_or_self_review_config_id = $1
131  AND deleted_at IS NULL;
132        "#,
133        peer_review_id,
134    )
135    .fetch_all(conn)
136    .await?;
137    Ok(res)
138}
139
140pub async fn get_all_by_peer_or_self_review_config_id(
141    conn: &mut PgConnection,
142    peer_or_self_review_config_id: Uuid,
143) -> ModelResult<Vec<PeerOrSelfReviewQuestion>> {
144    let res = sqlx::query_as!(
145        PeerOrSelfReviewQuestion,
146        r#"
147SELECT *
148FROM peer_or_self_review_questions
149WHERE peer_or_self_review_config_id = $1
150    AND deleted_at IS NULL;
151        "#,
152        peer_or_self_review_config_id
153    )
154    .fetch_all(conn)
155    .await?;
156    Ok(res)
157}
158
159pub async fn get_all_by_peer_or_self_review_config_id_as_map(
160    conn: &mut PgConnection,
161    peer_or_self_review_config_id: Uuid,
162) -> ModelResult<HashMap<Uuid, PeerOrSelfReviewQuestion>> {
163    let res = get_all_by_peer_or_self_review_config_id(conn, peer_or_self_review_config_id)
164        .await?
165        .into_iter()
166        .map(|x| (x.id, x))
167        .collect();
168    Ok(res)
169}
170
171/// The review questions of the undeleted exercises on the pages.
172pub async fn get_by_page_ids(
173    conn: &mut PgConnection,
174    page_ids: &[Uuid],
175) -> ModelResult<Vec<CmsPeerOrSelfReviewQuestion>> {
176    let res = sqlx::query_as!(
177        CmsPeerOrSelfReviewQuestion,
178        r#"
179SELECT prq.id as id,
180  prq.peer_or_self_review_config_id as peer_or_self_review_config_id,
181  prq.order_number as order_number,
182  prq.question as question,
183  prq.question_type,
184  prq.answer_required as answer_required,
185  prq.weight
186from pages p
187  join exercises e on p.id = e.page_id
188  join peer_or_self_review_configs pr on e.id = pr.exercise_id
189  join peer_or_self_review_questions prq on pr.id = prq.peer_or_self_review_config_id
190where p.id = ANY($1)
191  AND p.deleted_at IS NULL
192  AND e.deleted_at IS NULL
193  AND pr.deleted_at IS NULL
194  AND prq.deleted_at IS NULL;
195  "#,
196        page_ids
197    )
198    .fetch_all(conn)
199    .await?;
200
201    Ok(res)
202}
203
204pub async fn delete_peer_or_self_review_questions_by_peer_or_self_review_config_ids(
205    conn: &mut PgConnection,
206    peer_or_self_review_config_ids: &[Uuid],
207) -> ModelResult<Vec<Uuid>> {
208    let res = sqlx::query!(
209        "
210UPDATE peer_or_self_review_questions
211SET deleted_at = now()
212WHERE peer_or_self_review_config_id = ANY ($1)
213AND deleted_at IS NULL
214RETURNING *;
215    ",
216        peer_or_self_review_config_ids
217    )
218    .fetch_all(conn)
219    .await?
220    .into_iter()
221    .map(|x| x.id)
222    .collect();
223    Ok(res)
224}
225
226pub async fn get_course_default_cms_peer_or_self_review_questions(
227    conn: &mut PgConnection,
228    peer_or_self_review_config_id: Uuid,
229) -> ModelResult<Vec<CmsPeerOrSelfReviewQuestion>> {
230    let res = sqlx::query_as!(
231        CmsPeerOrSelfReviewQuestion,
232        r#"
233SELECT id,
234  peer_or_self_review_config_id,
235  order_number,
236  question_type,
237  question,
238  answer_required,
239  weight
240FROM peer_or_self_review_questions
241where peer_or_self_review_config_id = $1
242  AND deleted_at IS NULL;
243    "#,
244        peer_or_self_review_config_id
245    )
246    .fetch_all(conn)
247    .await?;
248
249    Ok(res)
250}
251
252pub async fn upsert_multiple_peer_or_self_review_questions(
253    conn: &mut PgConnection,
254    peer_or_self_review_questions: &[CmsPeerOrSelfReviewQuestion],
255) -> ModelResult<Vec<CmsPeerOrSelfReviewQuestion>> {
256    let mut sql: QueryBuilder<Postgres> = sqlx::QueryBuilder::new(
257        "INSERT INTO peer_or_self_review_questions (id, peer_or_self_review_config_id, order_number, question_type, question, answer_required) ",
258    );
259
260    sql.push_values(peer_or_self_review_questions, |mut x, prq| {
261        x.push_bind(prq.id)
262            .push_bind(prq.peer_or_self_review_config_id)
263            .push_bind(prq.order_number)
264            .push_bind(prq.question_type)
265            .push_bind(prq.question.as_str())
266            .push_bind(prq.answer_required);
267    });
268    sql.push(
269        r#" ON CONFLICT (id) DO
270UPDATE
271SET peer_or_self_review_config_id = excluded.peer_or_self_review_config_id,
272  order_number = excluded.order_number,
273  question_type = excluded.question_type,
274  question = excluded.question,
275  answer_required = excluded.answer_required,
276  deleted_at = NULL
277RETURNING id;
278"#,
279    );
280
281    let ids = sql
282        .build()
283        .fetch_all(&mut *conn)
284        .await?
285        .iter()
286        .map(|x| x.get(0))
287        .collect::<Vec<_>>();
288
289    let res = sqlx::query_as!(
290        CmsPeerOrSelfReviewQuestion,
291        r#"
292SELECT id,
293  peer_or_self_review_config_id,
294  order_number,
295  question,
296  question_type,
297  answer_required,
298  weight
299from peer_or_self_review_questions
300WHERE id IN (
301    SELECT UNNEST($1::uuid [])
302  )
303  AND deleted_at IS NULL;
304    "#,
305        &ids
306    )
307    .fetch_all(conn)
308    .await?;
309    Ok(res)
310}
311
312/** Modifies the questions in memory so that the weights sum to either 0 or 1. */
313pub fn normalize_cms_peer_or_self_review_questions(
314    peer_or_self_review_questions: &mut [CmsPeerOrSelfReviewQuestion],
315) {
316    // All scales have to be answered, skipping them does not make sense.
317    for question in peer_or_self_review_questions.iter_mut() {
318        if question.question_type == PeerOrSelfReviewQuestionType::Scale {
319            question.answer_required = true;
320        }
321    }
322    peer_or_self_review_questions.sort_by_key(|a| a.order_number);
323    info!(
324        "Peer review question weights before normalization: {:?}",
325        peer_or_self_review_questions
326            .iter()
327            .map(|x| x.weight)
328            .collect::<Vec<_>>()
329    );
330    let (mut allowed_to_have_weight, mut not_allowed_to_have_weight) =
331        peer_or_self_review_questions
332            .iter_mut()
333            .partition::<Vec<_>, _>(|q| q.question_type == PeerOrSelfReviewQuestionType::Scale);
334    let total_weight: f32 = allowed_to_have_weight.iter().map(|x| x.weight).sum();
335    if total_weight == 0.0 {
336        return;
337    }
338    for question in allowed_to_have_weight.iter_mut() {
339        question.weight /= total_weight;
340    }
341    for question in not_allowed_to_have_weight.iter_mut() {
342        question.weight = 0.0;
343    }
344    info!(
345        "Peer review question weights after normalization: {:?}",
346        peer_or_self_review_questions
347            .iter()
348            .map(|x| x.weight)
349            .collect::<Vec<_>>()
350    );
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn test_normalize_cms_peer_or_self_review_questions() {
359        let mut questions = vec![
360            CmsPeerOrSelfReviewQuestion {
361                id: Uuid::new_v4(),
362                peer_or_self_review_config_id: Uuid::new_v4(),
363                order_number: 1,
364                question: String::from("Question 1"),
365                question_type: PeerOrSelfReviewQuestionType::Scale,
366                answer_required: true,
367                weight: 2.0,
368            },
369            CmsPeerOrSelfReviewQuestion {
370                id: Uuid::new_v4(),
371                peer_or_self_review_config_id: Uuid::new_v4(),
372                order_number: 2,
373                question: String::from("Question 2"),
374                question_type: PeerOrSelfReviewQuestionType::Scale,
375                answer_required: true,
376                weight: 3.0,
377            },
378            CmsPeerOrSelfReviewQuestion {
379                id: Uuid::new_v4(),
380                peer_or_self_review_config_id: Uuid::new_v4(),
381                order_number: 3,
382                question: String::from("Question 3"),
383                question_type: PeerOrSelfReviewQuestionType::Essay,
384                answer_required: true,
385                weight: 1.0,
386            },
387        ];
388
389        normalize_cms_peer_or_self_review_questions(&mut questions);
390
391        assert_eq!(questions[0].weight, 2.0 / 5.0);
392        assert_eq!(questions[1].weight, 3.0 / 5.0);
393        assert_eq!(questions[2].weight, 0.0);
394    }
395}