headless_lms_models/
feedback.rs

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}