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 id
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 id,
104 user_id,
105 course_id,
106 exam_id,
107 page_id,
108 feedback_given,
109 selected_text,
110 marked_as_read,
111 created_at,
112 updated_at,
113 deleted_at
114FROM feedback
115WHERE id = $1
116 AND deleted_at IS NULL
117 "#,
118 id
119 )
120 .fetch_one(conn)
121 .await?;
122 Ok(res)
123}
124
125pub async fn set_read_state_by_id_and_course_id(
126 conn: &mut PgConnection,
127 id: Uuid,
128 course_id: Uuid,
129 read: bool,
130) -> ModelResult<FeedbackRow> {
131 let res = sqlx::query_as!(
132 FeedbackRow,
133 r#"
134UPDATE feedback
135SET marked_as_read = $3
136WHERE id = $1
137 AND course_id = $2
138 AND deleted_at IS NULL
139RETURNING id,
140 user_id,
141 course_id,
142 exam_id,
143 page_id,
144 feedback_given,
145 selected_text,
146 marked_as_read,
147 created_at,
148 updated_at,
149 deleted_at
150 "#,
151 id,
152 course_id,
153 read
154 )
155 .fetch_one(conn)
156 .await?;
157 Ok(res)
158}
159
160#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, ToSchema)]
161
162pub struct Feedback {
163 pub id: Uuid,
164 pub user_id: Option<Uuid>,
165 pub course_id: Uuid,
166 pub page_id: Option<Uuid>,
167 pub feedback_given: String,
168 pub selected_text: Option<String>,
169 pub marked_as_read: bool,
170 pub created_at: DateTime<Utc>,
171 pub blocks: Vec<FeedbackBlock>,
172 pub page_title: String,
173 pub page_url_path: String,
174}
175
176pub async fn get_feedback_for_course(
177 conn: &mut PgConnection,
178 course_id: Uuid,
179 read: bool,
180 pagination: Pagination,
181) -> ModelResult<Vec<Feedback>> {
182 let res = sqlx::query!(
183 r#"
184SELECT fb.*,
185 pages.title as "page_title",
186 pages.url_path as "page_url_path"
187FROM (
188 SELECT feedback.id as "id!",
189 feedback.user_id,
190 feedback.course_id as "course_id!",
191 feedback.page_id,
192 feedback.feedback_given as "feedback_given!",
193 feedback.selected_text,
194 feedback.marked_as_read as "marked_as_read!",
195 feedback.created_at as "created_at!",
196 array_agg(block_feedback.block_id) filter (
197 where block_feedback.block_id IS NOT NULL
198 ) AS "block_ids: Vec<Uuid>",
199 array_agg(block_feedback.block_text) filter (
200 where block_feedback.block_id IS NOT NULL
201 ) AS "block_texts: Vec<Option<String>>",
202 array_agg(block_feedback.order_number) filter (
203 where block_feedback.block_id IS NOT NULL
204 ) AS "block_order_numbers: Vec<Option<i32>>"
205 FROM feedback
206 LEFT JOIN block_feedback ON block_feedback.feedback_id = feedback.id
207 WHERE course_id = $1
208 AND feedback.marked_as_read = $2
209 AND feedback.deleted_at IS NULL
210 AND block_feedback.deleted_at IS NULL
211 GROUP BY feedback.id,
212 feedback.user_id,
213 feedback.course_id,
214 feedback.feedback_given,
215 feedback.marked_as_read,
216 feedback.created_at
217 ORDER BY feedback.created_at DESC,
218 feedback.id
219 LIMIT $3 OFFSET $4
220 ) fb
221 JOIN pages on pages.id = fb.page_id
222"#,
223 course_id,
224 read,
225 pagination.limit(),
226 pagination.offset(),
227 )
228 .map(|r| Feedback {
229 id: r.id,
230 user_id: r.user_id,
231 course_id: r.course_id,
232 page_id: r.page_id,
233 feedback_given: r.feedback_given,
234 selected_text: r.selected_text,
235 marked_as_read: r.marked_as_read,
236 created_at: r.created_at,
237 blocks: r
238 .block_ids
239 .unwrap_or_default()
240 .into_iter()
241 .zip(r.block_texts.unwrap_or_default())
242 .zip(r.block_order_numbers.unwrap_or_default())
243 .map(|((id, text), order_number)| FeedbackBlock {
244 id,
245 text,
246 order_number,
247 })
248 .collect(),
249 page_title: r.page_title,
250 page_url_path: r.page_url_path,
251 })
252 .fetch_all(conn)
253 .await?;
254 Ok(res)
255}
256
257#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, ToSchema)]
258
259pub struct FeedbackCount {
260 pub read: u32,
261 pub unread: u32,
262}
263
264pub async fn get_feedback_count_for_course(
265 conn: &mut PgConnection,
266 course_id: Uuid,
267) -> ModelResult<FeedbackCount> {
268 let res = sqlx::query!(
269 "
270SELECT COUNT(*) filter (
271 where marked_as_read
272 ) AS read,
273 COUNT(*) filter (
274 where not(marked_as_read)
275 ) AS unread
276FROM feedback
277WHERE course_id = $1
278 AND feedback.deleted_at IS NULL
279",
280 course_id,
281 )
282 .fetch_one(conn)
283 .await?;
284 Ok(FeedbackCount {
285 read: res.read.unwrap_or_default().try_into()?,
286 unread: res.unread.unwrap_or_default().try_into()?,
287 })
288}