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_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
312pub fn normalize_cms_peer_or_self_review_questions(
314 peer_or_self_review_questions: &mut [CmsPeerOrSelfReviewQuestion],
315) {
316 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}