Skip to main content

headless_lms_models/
feedback_categories.rs

1use crate::prelude::*;
2use utoipa::ToSchema;
3
4#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
5pub struct FeedbackCategory {
6    pub id: Uuid,
7    pub created_at: DateTime<Utc>,
8    pub updated_at: DateTime<Utc>,
9    pub deleted_at: Option<DateTime<Utc>>,
10    pub name: String,
11}
12
13#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
14pub struct NewFeedbackCategory {
15    pub name: String,
16}
17
18/// Trim and normalize category names:
19///     - lowercase
20///     - remove trailing and preceding non-alphanumeric chars
21///     - remove '*' and ';', these shouldn't be needed
22fn normalize(input: &str) -> String {
23    input
24        .to_lowercase()
25        .replace(";", "")
26        .replace("*", "")
27        .trim_end_matches(|x: char| !x.is_alphanumeric())
28        .trim_start_matches(|x: char| !x.is_alphanumeric())
29        .to_string()
30}
31
32pub async fn insert_or_get_by_name(
33    conn: &mut PgConnection,
34    category: NewFeedbackCategory,
35) -> ModelResult<Uuid> {
36    let normalized_name = normalize(&category.name);
37    if normalized_name.is_empty() {
38        return Err(model_err!(
39            InvalidRequest,
40            format!(
41                "Cannot insert invalid or empty feedback_category name. Name: {} Name after normalization: {}",
42                &category.name, normalized_name
43            )
44        ));
45    }
46
47    // use normalized name for getting
48    if let Some(c) = get_by_name(conn, &normalized_name).await? {
49        return Ok(c.id);
50    };
51
52    let res: Option<FeedbackCategory> = sqlx::query_as!(
53        FeedbackCategory,
54        r#"
55INSERT INTO feedback_categories (name)
56VALUES ($1) ON CONFLICT DO NOTHING
57RETURNING *
58        "#,
59        &normalized_name,
60    )
61    .fetch_optional(&mut *conn)
62    .await?;
63
64    if let Some(c) = res {
65        Ok(c.id)
66    } else {
67        get_by_name(conn, &normalized_name)
68            .await?
69            .map(|x| x.id)
70            .ok_or(model_err!(RecordNotFound, "Category n"))
71    }
72}
73
74pub async fn get_by_name(
75    conn: &mut PgConnection,
76    // Should be normalized before calling
77    name: &String,
78) -> ModelResult<Option<FeedbackCategory>> {
79    let res = sqlx::query_as!(
80        FeedbackCategory,
81        "
82SELECT *
83FROM feedback_categories
84WHERE name = $1
85  AND deleted_at IS NULL
86        ",
87        name
88    )
89    .fetch_optional(conn)
90    .await?;
91
92    Ok(res)
93}
94
95pub async fn get_all(
96    conn: &mut PgConnection,
97    course_id: Uuid,
98) -> ModelResult<Vec<FeedbackCategory>> {
99    let res = sqlx::query_as!(
100        FeedbackCategory,
101        "
102SELECT fc.*
103FROM feedback_categories AS fc
104WHERE EXISTS (
105    SELECT id
106    FROM feedback AS f
107    WHERE fc.id = f.category_id
108      AND f.deleted_at IS NULL
109      AND f.course_id = $1
110  )
111  AND fc.deleted_at IS NULL
112        ",
113        course_id
114    )
115    .fetch_all(conn)
116    .await?;
117
118    Ok(res)
119}
120
121pub async fn get_all_by_read_status(
122    conn: &mut PgConnection,
123    course_id: Uuid,
124    read: bool,
125) -> ModelResult<Vec<FeedbackCategory>> {
126    let res = sqlx::query_as!(
127        FeedbackCategory,
128        "
129SELECT fc.*
130FROM feedback_categories AS fc
131WHERE EXISTS (
132    SELECT id
133    FROM feedback AS f
134    WHERE fc.id = f.category_id
135      AND f.deleted_at IS NULL
136      AND f.course_id = $1
137      AND f.marked_as_read = $2
138  )
139  AND fc.deleted_at IS NULL
140        ",
141        course_id,
142        read
143    )
144    .fetch_all(conn)
145    .await?;
146
147    Ok(res)
148}