Skip to main content

headless_lms_models/
feedback.rs

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}