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