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 *
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}