1
/*
2
 * This file is part of mailpot
3
 *
4
 * Copyright 2020 - Manos Pitsidianakis
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Affero General Public License as
8
 * published by the Free Software Foundation, either version 3 of the
9
 * License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU Affero General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Affero General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 */
19

            
20
use super::*;
21

            
22
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
23
pub struct SearchTerm {
24
    query: Option<String>,
25
}
26

            
27
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
28
pub struct SearchResult {
29
    pk: i64,
30
    id: String,
31
    description: Option<String>,
32
    topics: Vec<String>,
33
}
34

            
35
impl std::fmt::Display for SearchResult {
36
    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
37
        write!(fmt, "{:?}", self)
38
    }
39
}
40

            
41
impl Object for SearchResult {
42
    fn kind(&self) -> minijinja::value::ObjectKind {
43
        minijinja::value::ObjectKind::Struct(self)
44
    }
45

            
46
    fn call_method(
47
        &self,
48
        _state: &minijinja::State,
49
        name: &str,
50
        _args: &[Value],
51
    ) -> std::result::Result<Value, Error> {
52
        match name {
53
            "topics_html" => crate::minijinja_utils::topics_common(&self.topics),
54
            _ => Err(Error::new(
55
                minijinja::ErrorKind::UnknownMethod,
56
                format!("object has no method named {name}"),
57
            )),
58
        }
59
    }
60
}
61

            
62
impl minijinja::value::StructObject for SearchResult {
63
    fn get_field(&self, name: &str) -> Option<Value> {
64
        match name {
65
            "pk" => Some(Value::from_serializable(&self.pk)),
66
            "id" => Some(Value::from_serializable(&self.id)),
67
            "description" => Some(
68
                self.description
69
                    .clone()
70
                    .map(Value::from_safe_string)
71
                    .unwrap_or_else(|| Value::from_serializable(&self.description)),
72
            ),
73
            "topics" => Some(Value::from_serializable(&self.topics)),
74
            _ => None,
75
        }
76
    }
77

            
78
    fn static_fields(&self) -> Option<&'static [&'static str]> {
79
        Some(&["pk", "id", "description", "topics"][..])
80
    }
81
}
82
pub async fn list_topics(
83
    _: TopicsPath,
84
    mut session: WritableSession,
85
    Query(SearchTerm { query: term }): Query<SearchTerm>,
86
    auth: AuthContext,
87
    State(state): State<Arc<AppState>>,
88
) -> Result<Html<String>, ResponseError> {
89
    let db = Connection::open_db(state.conf.clone())?.trusted();
90

            
91
    let results: Vec<Value> = {
92
        if let Some(term) = term.as_ref() {
93
            let mut stmt = db.connection.prepare(
94
                "SELECT DISTINCT list.pk, list.id, list.description, list.topics FROM list, \
95
                 json_each(list.topics) WHERE json_each.value IS ?;",
96
            )?;
97
            let iter = stmt.query_map([&term], |row| {
98
                let pk = row.get(0)?;
99
                let id = row.get(1)?;
100
                let description = row.get(2)?;
101
                let topics = mailpot::models::MailingList::topics_from_json_value(row.get(3)?)?;
102
                Ok(Value::from_object(SearchResult {
103
                    pk,
104
                    id,
105
                    description,
106
                    topics,
107
                }))
108
            })?;
109
            let mut ret = vec![];
110
            for el in iter {
111
                let el = el?;
112
                ret.push(el);
113
            }
114
            ret
115
        } else {
116
            db.lists()?
117
                .into_iter()
118
                .map(DbVal::into_inner)
119
                .map(|l| SearchResult {
120
                    pk: l.pk,
121
                    id: l.id,
122
                    description: l.description,
123
                    topics: l.topics,
124
                })
125
                .map(Value::from_object)
126
                .collect()
127
        }
128
    };
129

            
130
    let crumbs = vec![
131
        Crumb {
132
            label: "Home".into(),
133
            url: "/".into(),
134
        },
135
        Crumb {
136
            label: "Search for topics".into(),
137
            url: TopicsPath.to_crumb(),
138
        },
139
    ];
140
    let context = minijinja::context! {
141
        canonical_url => TopicsPath.to_crumb(),
142
        term,
143
        results,
144
        page_title => "Topic Search Results",
145
        description => "",
146
        current_user => auth.current_user,
147
        messages => session.drain_messages(),
148
        crumbs,
149
    };
150
    Ok(Html(
151
        TEMPLATES.get_template("topics.html")?.render(context)?,
152
    ))
153
}