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
347pub fn normalize_cms_peer_or_self_review_questions(
349 peer_or_self_review_questions: &mut [CmsPeerOrSelfReviewQuestion],
350) {
351 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}