1use crate::prelude::*;
2
3#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
4#[cfg_attr(feature = "ts_rs", derive(TS))]
5pub struct NewFeedback {
6 pub feedback_given: String,
7 pub selected_text: Option<String>,
8 pub related_blocks: Vec<FeedbackBlock>,
9 pub page_id: Uuid,
10}
11
12#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq)]
13#[cfg_attr(feature = "ts_rs", derive(TS))]
14pub struct FeedbackBlock {
15 pub id: Uuid,
16 pub text: Option<String>,
17 pub order_number: Option<i32>,
18}
19
20pub async fn insert(
21 conn: &mut PgConnection,
22 pkey_policy: PKeyPolicy<Uuid>,
23 user_id: Option<Uuid>,
24 course_id: Uuid,
25 new_feedback: NewFeedback,
26) -> ModelResult<Uuid> {
27 let mut tx = conn.begin().await?;
28 let res = sqlx::query!(
29 "
30INSERT INTO feedback(
31 id,
32 user_id,
33 course_id,
34 feedback_given,
35 selected_text,
36 page_id
37 )
38VALUES ($1, $2, $3, $4, $5, $6)
39RETURNING id
40 ",
41 pkey_policy.into_uuid(),
42 user_id,
43 course_id,
44 new_feedback.feedback_given,
45 new_feedback.selected_text,
46 new_feedback.page_id
47 )
48 .fetch_one(&mut *tx)
49 .await?;
50 for (n, block) in new_feedback.related_blocks.iter().enumerate() {
51 sqlx::query!(
52 "
53INSERT INTO block_feedback(feedback_id, block_id, block_text, order_number)
54VALUES ($1, $2, $3, $4)
55",
56 res.id,
57 block.id,
58 block.text,
59 n as i32
60 )
61 .execute(&mut *tx)
62 .await?;
63 }
64 tx.commit().await?;
65 Ok(res.id)
66}
67
68pub async fn mark_as_read(conn: &mut PgConnection, id: Uuid, read: bool) -> ModelResult<()> {
69 sqlx::query!(
70 "
71UPDATE feedback
72SET marked_as_read = $1
73WHERE id = $2
74",
75 read,
76 id
77 )
78 .execute(conn)
79 .await?;
80 Ok(())
81}
82
83#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq)]
84#[cfg_attr(feature = "ts_rs", derive(TS))]
85pub struct Feedback {
86 pub id: Uuid,
87 pub user_id: Option<Uuid>,
88 pub course_id: Uuid,
89 pub page_id: Option<Uuid>,
90 pub feedback_given: String,
91 pub selected_text: Option<String>,
92 pub marked_as_read: bool,
93 pub created_at: DateTime<Utc>,
94 pub blocks: Vec<FeedbackBlock>,
95 pub page_title: String,
96 pub page_url_path: String,
97}
98
99pub async fn get_feedback_for_course(
100 conn: &mut PgConnection,
101 course_id: Uuid,
102 read: bool,
103 pagination: Pagination,
104) -> ModelResult<Vec<Feedback>> {
105 let res = sqlx::query!(
106 r#"
107SELECT fb.*,
108 pages.title as "page_title",
109 pages.url_path as "page_url_path"
110FROM (
111 SELECT feedback.id as "id!",
112 feedback.user_id,
113 feedback.course_id as "course_id!",
114 feedback.page_id,
115 feedback.feedback_given as "feedback_given!",
116 feedback.selected_text,
117 feedback.marked_as_read as "marked_as_read!",
118 feedback.created_at as "created_at!",
119 array_agg(block_feedback.block_id) filter (
120 where block_feedback.block_id IS NOT NULL
121 ) AS "block_ids: Vec<Uuid>",
122 array_agg(block_feedback.block_text) filter (
123 where block_feedback.block_id IS NOT NULL
124 ) AS "block_texts: Vec<Option<String>>",
125 array_agg(block_feedback.order_number) filter (
126 where block_feedback.block_id IS NOT NULL
127 ) AS "block_order_numbers: Vec<Option<i32>>"
128 FROM feedback
129 LEFT JOIN block_feedback ON block_feedback.feedback_id = feedback.id
130 WHERE course_id = $1
131 AND feedback.marked_as_read = $2
132 AND feedback.deleted_at IS NULL
133 AND block_feedback.deleted_at IS NULL
134 GROUP BY feedback.id,
135 feedback.user_id,
136 feedback.course_id,
137 feedback.feedback_given,
138 feedback.marked_as_read,
139 feedback.created_at
140 ORDER BY feedback.created_at DESC,
141 feedback.id
142 LIMIT $3 OFFSET $4
143 ) fb
144 JOIN pages on pages.id = fb.page_id
145"#,
146 course_id,
147 read,
148 pagination.limit(),
149 pagination.offset(),
150 )
151 .map(|r| Feedback {
152 id: r.id,
153 user_id: r.user_id,
154 course_id: r.course_id,
155 page_id: r.page_id,
156 feedback_given: r.feedback_given,
157 selected_text: r.selected_text,
158 marked_as_read: r.marked_as_read,
159 created_at: r.created_at,
160 blocks: r
161 .block_ids
162 .unwrap_or_default()
163 .into_iter()
164 .zip(r.block_texts.unwrap_or_default())
165 .zip(r.block_order_numbers.unwrap_or_default())
166 .map(|((id, text), order_number)| FeedbackBlock {
167 id,
168 text,
169 order_number,
170 })
171 .collect(),
172 page_title: r.page_title,
173 page_url_path: r.page_url_path,
174 })
175 .fetch_all(conn)
176 .await?;
177 Ok(res)
178}
179
180#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq)]
181#[cfg_attr(feature = "ts_rs", derive(TS))]
182pub struct FeedbackCount {
183 pub read: u32,
184 pub unread: u32,
185}
186
187pub async fn get_feedback_count_for_course(
188 conn: &mut PgConnection,
189 course_id: Uuid,
190) -> ModelResult<FeedbackCount> {
191 let res = sqlx::query!(
192 "
193SELECT COUNT(*) filter (
194 where marked_as_read
195 ) AS read,
196 COUNT(*) filter (
197 where not(marked_as_read)
198 ) AS unread
199FROM feedback
200WHERE course_id = $1
201 AND feedback.deleted_at IS NULL
202",
203 course_id,
204 )
205 .fetch_one(conn)
206 .await?;
207 Ok(FeedbackCount {
208 read: res.read.unwrap_or_default().try_into()?,
209 unread: res.unread.unwrap_or_default().try_into()?,
210 })
211}