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
171pub async fn get_by_page_id(
172    conn: &mut PgConnection,
173    page_id: Uuid,
174) -> ModelResult<Vec<CmsPeerOrSelfReviewQuestion>> {
175    let res = sqlx::query_as!(
176        CmsPeerOrSelfReviewQuestion,
177        r#"
178SELECT prq.id as id,
179  prq.peer_or_self_review_config_id as peer_or_self_review_config_id,
180  prq.order_number as order_number,
181  prq.question as question,
182  prq.question_type,
183  prq.answer_required as answer_required,
184  prq.weight
185from pages p
186  join exercises e on p.id = e.page_id
187  join peer_or_self_review_configs pr on e.id = pr.exercise_id
188  join peer_or_self_review_questions prq on pr.id = prq.peer_or_self_review_config_id
189where p.id = $1
190  AND p.deleted_at IS NULL
191  AND e.deleted_at IS NULL
192  AND pr.deleted_at IS NULL
193  AND prq.deleted_at IS NULL;
194  "#,
195        page_id
196    )
197    .fetch_all(conn)
198    .await?;
199
200    Ok(res)
201}
202
203pub async fn delete_peer_or_self_review_questions_by_peer_or_self_review_config_ids(
204    conn: &mut PgConnection,
205    peer_or_self_review_config_ids: &[Uuid],
206) -> ModelResult<Vec<Uuid>> {
207    let res = sqlx::query!(
208        "
209UPDATE peer_or_self_review_questions
210SET deleted_at = now()
211WHERE peer_or_self_review_config_id = ANY ($1)
212AND deleted_at IS NULL
213RETURNING *;
214    ",
215        peer_or_self_review_config_ids
216    )
217    .fetch_all(conn)
218    .await?
219    .into_iter()
220    .map(|x| x.id)
221    .collect();
222    Ok(res)
223}
224
225pub async fn get_course_default_cms_peer_or_self_review_questions(
226    conn: &mut PgConnection,
227    peer_or_self_review_config_id: Uuid,
228) -> ModelResult<Vec<CmsPeerOrSelfReviewQuestion>> {
229    let res = sqlx::query_as!(
230        CmsPeerOrSelfReviewQuestion,
231        r#"
232SELECT id,
233  peer_or_self_review_config_id,
234  order_number,
235  question_type,
236  question,
237  answer_required,
238  weight
239FROM peer_or_self_review_questions
240where peer_or_self_review_config_id = $1
241  AND deleted_at IS NULL;
242    "#,
243        peer_or_self_review_config_id
244    )
245    .fetch_all(conn)
246    .await?;
247
248    Ok(res)
249}
250
251pub async fn upsert_multiple_peer_or_self_review_questions(
252    conn: &mut PgConnection,
253    peer_or_self_review_questions: &[CmsPeerOrSelfReviewQuestion],
254) -> ModelResult<Vec<CmsPeerOrSelfReviewQuestion>> {
255    let mut sql: QueryBuilder<Postgres> = sqlx::QueryBuilder::new(
256        "INSERT INTO peer_or_self_review_questions (id, peer_or_self_review_config_id, order_number, question_type, question, answer_required) ",
257    );
258
259    sql.push_values(peer_or_self_review_questions, |mut x, prq| {
260        x.push_bind(prq.id)
261            .push_bind(prq.peer_or_self_review_config_id)
262            .push_bind(prq.order_number)
263            .push_bind(prq.question_type)
264            .push_bind(prq.question.as_str())
265            .push_bind(prq.answer_required);
266    });
267    sql.push(
268        r#" ON CONFLICT (id) DO
269UPDATE
270SET peer_or_self_review_config_id = excluded.peer_or_self_review_config_id,
271  order_number = excluded.order_number,
272  question_type = excluded.question_type,
273  question = excluded.question,
274  answer_required = excluded.answer_required,
275  deleted_at = NULL
276RETURNING id;
277"#,
278    );
279
280    let ids = sql
281        .build()
282        .fetch_all(&mut *conn)
283        .await?
284        .iter()
285        .map(|x| x.get(0))
286        .collect::<Vec<_>>();
287
288    let res = sqlx::query_as!(
289        CmsPeerOrSelfReviewQuestion,
290        r#"
291SELECT id,
292  peer_or_self_review_config_id,
293  order_number,
294  question,
295  question_type,
296  answer_required,
297  weight
298from peer_or_self_review_questions
299WHERE id IN (
300    SELECT UNNEST($1::uuid [])
301  )
302  AND deleted_at IS NULL;
303    "#,
304        &ids
305    )
306    .fetch_all(conn)
307    .await?;
308    Ok(res)
309}
310
311/** Modifies the questions in memory so that the weights sum to either 0 or 1. */
312pub fn normalize_cms_peer_or_self_review_questions(
313    peer_or_self_review_questions: &mut [CmsPeerOrSelfReviewQuestion],
314) {
315    // All scales have to be answered, skipping them does not make sense.
316    for question in peer_or_self_review_questions.iter_mut() {
317        if question.question_type == PeerOrSelfReviewQuestionType::Scale {
318            question.answer_required = true;
319        }
320    }
321    peer_or_self_review_questions.sort_by_key(|a| a.order_number);
322    info!(
323        "Peer review question weights before normalization: {:?}",
324        peer_or_self_review_questions
325            .iter()
326            .map(|x| x.weight)
327            .collect::<Vec<_>>()
328    );
329    let (mut allowed_to_have_weight, mut not_allowed_to_have_weight) =
330        peer_or_self_review_questions
331            .iter_mut()
332            .partition::<Vec<_>, _>(|q| q.question_type == PeerOrSelfReviewQuestionType::Scale);
333    let total_weight: f32 = allowed_to_have_weight.iter().map(|x| x.weight).sum();
334    if total_weight == 0.0 {
335        return;
336    }
337    for question in allowed_to_have_weight.iter_mut() {
338        question.weight /= total_weight;
339    }
340    for question in not_allowed_to_have_weight.iter_mut() {
341        question.weight = 0.0;
342    }
343    info!(
344        "Peer review question weights after normalization: {:?}",
345        peer_or_self_review_questions
346            .iter()
347            .map(|x| x.weight)
348            .collect::<Vec<_>>()
349    );
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn test_normalize_cms_peer_or_self_review_questions() {
358        let mut questions = vec![
359            CmsPeerOrSelfReviewQuestion {
360                id: Uuid::new_v4(),
361                peer_or_self_review_config_id: Uuid::new_v4(),
362                order_number: 1,
363                question: String::from("Question 1"),
364                question_type: PeerOrSelfReviewQuestionType::Scale,
365                answer_required: true,
366                weight: 2.0,
367            },
368            CmsPeerOrSelfReviewQuestion {
369                id: Uuid::new_v4(),
370                peer_or_self_review_config_id: Uuid::new_v4(),
371                order_number: 2,
372                question: String::from("Question 2"),
373                question_type: PeerOrSelfReviewQuestionType::Scale,
374                answer_required: true,
375                weight: 3.0,
376            },
377            CmsPeerOrSelfReviewQuestion {
378                id: Uuid::new_v4(),
379                peer_or_self_review_config_id: Uuid::new_v4(),
380                order_number: 3,
381                question: String::from("Question 3"),
382                question_type: PeerOrSelfReviewQuestionType::Essay,
383                answer_required: true,
384                weight: 1.0,
385            },
386        ];
387
388        normalize_cms_peer_or_self_review_questions(&mut questions);
389
390        assert_eq!(questions[0].weight, 2.0 / 5.0);
391        assert_eq!(questions[1].weight, 3.0 / 5.0);
392        assert_eq!(questions[2].weight, 0.0);
393    }
394}