1use crate::{
2 feedback_categories::{self, NewFeedbackCategory},
3 prelude::*,
4};
5use utoipa::ToSchema;
6
7#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
8
9pub struct NewFeedback {
10 pub feedback_given: String,
11 pub selected_text: Option<String>,
12 pub related_blocks: Vec<FeedbackBlock>,
13 pub page_id: Uuid,
14 pub category: Option<NewFeedbackCategory>,
15}
16
17#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, ToSchema)]
18
19pub struct FeedbackBlock {
20 pub id: Uuid,
21 pub text: Option<String>,
22 pub order_number: Option<i32>,
23}
24
25#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, ToSchema)]
26pub struct FeedbackRow {
27 pub id: Uuid,
28 pub created_at: DateTime<Utc>,
29 pub updated_at: DateTime<Utc>,
30 pub deleted_at: Option<DateTime<Utc>>,
31 pub user_id: Option<Uuid>,
32 pub course_id: Option<Uuid>,
33 pub exam_id: Option<Uuid>,
34 pub page_id: Option<Uuid>,
35 pub feedback_given: String,
36 pub selected_text: Option<String>,
37 pub marked_as_read: bool,
38 pub category_id: Option<Uuid>,
39}
40
41pub async fn insert(
42 conn: &mut PgConnection,
43 pkey_policy: PKeyPolicy<Uuid>,
44 user_id: Option<Uuid>,
45 course_id: Uuid,
46 new_feedback: NewFeedback,
47) -> ModelResult<Uuid> {
48 let category_id = if let Some(category) = new_feedback.category {
49 feedback_categories::insert_or_get_by_name(&mut *conn, category)
52 .await
53 .inspect_err(|e| error!("Error while inserting new feedback cateogry: {e}"))
54 .ok()
55 } else {
56 None
57 };
58 let mut tx = conn.begin().await?;
59
60 let res = sqlx::query!(
61 "
62INSERT INTO feedback(
63 id,
64 user_id,
65 course_id,
66 feedback_given,
67 selected_text,
68 page_id,
69 category_id
70 )
71VALUES ($1, $2, $3, $4, $5, $6, $7)
72RETURNING *
73 ",
74 pkey_policy.into_uuid(),
75 user_id,
76 course_id,
77 new_feedback.feedback_given,
78 new_feedback.selected_text,
79 new_feedback.page_id,
80 category_id
81 )
82 .fetch_one(&mut *tx)
83 .await?;
84 for (n, block) in new_feedback.related_blocks.iter().enumerate() {
85 sqlx::query!(
86 "
87INSERT INTO block_feedback(feedback_id, block_id, block_text, order_number)
88VALUES ($1, $2, $3, $4)
89",
90 res.id,
91 block.id,
92 block.text,
93 n as i32
94 )
95 .execute(&mut *tx)
96 .await?;
97 }
98 tx.commit().await?;
99 Ok(res.id)
100}
101
102pub async fn mark_as_read(conn: &mut PgConnection, id: Uuid, read: bool) -> ModelResult<()> {
103 sqlx::query!(
104 "
105UPDATE feedback
106SET marked_as_read = $1
107WHERE id = $2
108",
109 read,
110 id
111 )
112 .execute(conn)
113 .await?;
114 Ok(())
115}
116
117pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<FeedbackRow> {
118 let res = sqlx::query_as!(
119 FeedbackRow,
120 r#"
121SELECT *
122FROM feedback
123WHERE id = $1
124 AND deleted_at IS NULL
125 "#,
126 id
127 )
128 .fetch_one(conn)
129 .await?;
130 Ok(res)
131}
132
133pub async fn set_read_state_by_id_and_course_id(
134 conn: &mut PgConnection,
135 id: Uuid,
136 course_id: Uuid,
137 read: bool,
138) -> ModelResult<FeedbackRow> {
139 let res = sqlx::query_as!(
140 FeedbackRow,
141 r#"
142UPDATE feedback
143SET marked_as_read = $3
144WHERE id = $1
145 AND course_id = $2
146 AND deleted_at IS NULL
147RETURNING *
148 "#,
149 id,
150 course_id,
151 read
152 )
153 .fetch_one(conn)
154 .await?;
155 Ok(res)
156}
157
158#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, ToSchema)]
159
160pub struct Feedback {
161 pub id: Uuid,
162 pub user_id: Option<Uuid>,
163 pub course_id: Uuid,
164 pub page_id: Option<Uuid>,
165 pub feedback_given: String,
166 pub selected_text: Option<String>,
167 pub marked_as_read: bool,
168 pub created_at: DateTime<Utc>,
169 pub blocks: Vec<FeedbackBlock>,
170 pub page_title: String,
171 pub page_url_path: String,
172 pub feedback_category_name: Option<String>,
173 pub feedback_category_id: Option<Uuid>,
174}
175
176pub async fn get_feedback_for_course(
177 conn: &mut PgConnection,
178 course_id: Uuid,
179 category_filter: Option<Uuid>,
180 read: bool,
181 pagination: Pagination,
182) -> ModelResult<Vec<Feedback>> {
183 let res = sqlx::query!(
184 r#"
185SELECT fb.*,
186 pages.title AS "page_title",
187 pages.url_path AS "page_url_path"
188FROM (
189 SELECT feedback.id AS "id!",
190 feedback.user_id,
191 feedback.course_id AS "course_id!",
192 feedback.page_id,
193 feedback.feedback_given AS "feedback_given!",
194 feedback.selected_text,
195 feedback.marked_as_read AS "marked_as_read!",
196 feedback.created_at AS "created_at!",
197 feedback.category_id,
198 feedback_categories.name AS "feedback_category_name: Option<String>",
199 array_agg(block_feedback.block_id) filter (
200 WHERE block_feedback.block_id IS NOT NULL
201 ) AS "block_ids: Vec<Uuid>",
202 array_agg(block_feedback.block_text) filter (
203 WHERE block_feedback.block_id IS NOT NULL
204 ) AS "block_texts: Vec<Option<String>>",
205 array_agg(block_feedback.order_number) filter (
206 WHERE block_feedback.block_id IS NOT NULL
207 ) AS "block_order_numbers: Vec<Option<i32>>"
208 FROM feedback
209 LEFT JOIN block_feedback ON block_feedback.feedback_id = feedback.id
210 LEFT JOIN feedback_categories ON feedback_categories.id = feedback.category_id
211 AND feedback_categories.deleted_at IS NULL
212 WHERE course_id = $1
213 AND feedback.marked_as_read = $2
214 AND feedback.deleted_at IS NULL
215 AND block_feedback.deleted_at IS NULL
216 AND (
217 CASE
218 WHEN $5::uuid IS NOT NULL
219 THEN feedback_categories.id = $5::uuid
220 ELSE TRUE
221 END
222 )
223 GROUP BY feedback.id,
224 feedback.user_id,
225 feedback.course_id,
226 feedback.feedback_given,
227 feedback.marked_as_read,
228 feedback.created_at,
229 feedback_categories.name
230 ORDER BY feedback.created_at DESC,
231 feedback.id
232 LIMIT $3 OFFSET $4
233 ) fb
234 JOIN pages ON pages.id = fb.page_id
235ORDER BY fb."created_at!" DESC,
236 fb."id!"
237 "#,
238 course_id,
239 read,
240 pagination.limit(),
241 pagination.offset(),
242 category_filter as Option<Uuid>
243 )
244 .map(|r| Feedback {
245 id: r.id,
246 user_id: r.user_id,
247 course_id: r.course_id,
248 page_id: r.page_id,
249 feedback_given: r.feedback_given,
250 selected_text: r.selected_text,
251 marked_as_read: r.marked_as_read,
252 created_at: r.created_at,
253 blocks: r
254 .block_ids
255 .unwrap_or_default()
256 .into_iter()
257 .zip(r.block_texts.unwrap_or_default())
258 .zip(r.block_order_numbers.unwrap_or_default())
259 .map(|((id, text), order_number)| FeedbackBlock {
260 id,
261 text,
262 order_number,
263 })
264 .collect(),
265 page_title: r.page_title,
266 page_url_path: r.page_url_path,
267 feedback_category_name: r.feedback_category_name,
268 feedback_category_id: r.category_id,
269 })
270 .fetch_all(conn)
271 .await?;
272 Ok(res)
273}
274
275#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, ToSchema)]
276
277pub struct FeedbackCount {
278 pub read: u32,
279 pub unread: u32,
280 pub categories: Vec<CategoryFeedbackCount>,
281}
282
283#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, ToSchema)]
284pub struct CategoryFeedbackCount {
285 pub category_id: Uuid,
286 pub read_feedback: u32,
287 pub unread_feedback: u32,
288}
289
290pub async fn get_feedback_count_for_course(
291 conn: &mut PgConnection,
292 course_id: Uuid,
293) -> ModelResult<FeedbackCount> {
294 let res_categories = sqlx::query!(
295 r#"
296SELECT COUNT(*) filter (
297 WHERE marked_as_read
298 ) AS READ,
299 COUNT(*) filter (
300 WHERE NOT(marked_as_read)
301 ) AS unread,
302 category_id
303FROM feedback
304WHERE course_id = $1
305 AND deleted_at IS NULL
306GROUP BY category_id
307 "#,
308 course_id
309 )
310 .fetch_all(&mut *conn)
311 .await?;
312
313 let mut all_read = 0;
314 let mut all_unread = 0;
315
316 let c: Vec<CategoryFeedbackCount> = res_categories
317 .iter()
318 .filter_map(|x| {
319 let read = x.read.unwrap_or_default().try_into().ok()?;
320 let unread = x.unread.unwrap_or_default().try_into().ok()?;
321 all_read += read;
322 all_unread += unread;
323 Some(CategoryFeedbackCount {
324 category_id: x.category_id?,
325 read_feedback: read,
326 unread_feedback: unread,
327 })
328 })
329 .collect();
330
331 Ok(FeedbackCount {
332 read: all_read,
333 unread: all_unread,
334 categories: c,
335 })
336}
337
338pub async fn get_block_ids_by_page_ids(
340 conn: &mut PgConnection,
341 course_id: Uuid,
342 page_ids: &[Uuid],
343) -> ModelResult<Vec<crate::pages::PageBlockReferences>> {
344 let res = sqlx::query_as!(
345 crate::pages::PageBlockReferences,
346 r#"
347SELECT feedback.id,
348 feedback.page_id AS "page_id!",
349 array_agg(
350 block_feedback.block_id
351 ORDER BY block_feedback.order_number
352 ) AS "block_ids!"
353FROM feedback
354 JOIN block_feedback ON block_feedback.feedback_id = feedback.id
355 AND block_feedback.deleted_at IS NULL
356WHERE feedback.page_id = ANY($1)
357 AND feedback.course_id = $2
358 AND feedback.deleted_at IS NULL
359GROUP BY feedback.id
360 "#,
361 page_ids,
362 course_id
363 )
364 .fetch_all(conn)
365 .await?;
366 Ok(res)
367}
368
369pub async fn set_page_ids(
371 conn: &mut PgConnection,
372 feedback_ids: &[Uuid],
373 page_ids: &[Uuid],
374) -> ModelResult<()> {
375 sqlx::query!(
376 "
377UPDATE feedback
378SET page_id = moved.page_id
379FROM UNNEST($1::uuid [], $2::uuid []) AS moved(id, page_id)
380WHERE feedback.id = moved.id
381 ",
382 feedback_ids,
383 page_ids
384 )
385 .execute(conn)
386 .await?;
387 Ok(())
388}