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