about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/handlers.rs16
-rw-r--r--src/main.rs1
-rw-r--r--src/models.rs2
-rw-r--r--src/render.rs21
-rw-r--r--templates/index.html6
-rw-r--r--templates/search.html48
6 files changed, 90 insertions, 4 deletions
diff --git a/src/handlers.rs b/src/handlers.rs
index 33f33deccb87..02ff99394edf 100644
--- a/src/handlers.rs
+++ b/src/handlers.rs
@@ -176,6 +176,22 @@ pub fn reply_thread(state: State<AppState>,
         .responder()
 }
 
+/// This handler executes a full-text search on the forum database and
+/// displays the results to the user.
+pub fn search_forum(state: State<AppState>,
+                    query: Form<SearchPosts>) -> ConverseResponse {
+    let query_string = query.0.query.clone();
+    state.db.send(query.0)
+        .flatten()
+        .and_then(move |results| state.renderer.send(SearchResultPage {
+            results,
+            query: query_string,
+        }).from_err())
+        .flatten()
+        .map(|res| HttpResponse::Ok().content_type(HTML).body(res))
+        .responder()
+}
+
 /// This handler initiates an OIDC login.
 pub fn login(state: State<AppState>) -> ConverseResponse {
     state.oidc.send(GetLoginUrl)
diff --git a/src/main.rs b/src/main.rs
index 9f4eea03fa31..92eeca48df64 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -153,6 +153,7 @@ fn main() {
             .resource("/thread/submit", |r| r.method(Method::POST).with3(submit_thread))
             .resource("/thread/reply", |r| r.method(Method::POST).with3(reply_thread))
             .resource("/thread/{id}", |r| r.method(Method::GET).with2(forum_thread))
+            .resource("/search", |r| r.method(Method::POST).with2(search_forum))
             .resource("/oidc/login", |r| r.method(Method::GET).with(login))
             .resource("/oidc/callback", |r| r.method(Method::POST).with3(callback));
 
diff --git a/src/models.rs b/src/models.rs
index 927a78513669..dfadd53fd34f 100644
--- a/src/models.rs
+++ b/src/models.rs
@@ -74,7 +74,7 @@ pub struct NewPost {
 /// This struct models the response of a full-text search query. It
 /// does not use a table/schema definition struct like the other
 /// tables, as no table of this type actually exists.
-#[derive(QueryableByName, Debug)]
+#[derive(QueryableByName, Debug, Serialize)]
 pub struct SearchResult {
     #[sql_type = "Integer"]
     pub post_id: i32,
diff --git a/src/render.rs b/src/render.rs
index 186b96d247c0..537cab59daef 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -175,3 +175,24 @@ impl Handler<NewThreadPage> for Renderer {
         Ok(self.tera.render("new-thread.html", &ctx)?)
     }
 }
+
+/// Message used to render search results
+pub struct SearchResultPage {
+    pub query: String,
+    pub results: Vec<SearchResult>,
+}
+
+impl Message for SearchResultPage {
+    type Result = Result<String>;
+}
+
+impl Handler<SearchResultPage> for Renderer {
+    type Result = Result<String>;
+
+    fn handle(&mut self, msg: SearchResultPage, _: &mut Self::Context) -> Self::Result {
+        let mut ctx = Context::new();
+        ctx.add("query", &msg.query);
+        ctx.add("results", &msg.results);
+        Ok(self.tera.render("search.html", &ctx)?)
+    }
+}
diff --git a/templates/index.html b/templates/index.html
index 35cfeba05023..263c8828a038 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -14,9 +14,9 @@
         <a class="navbar-brand" href="/">
           <h2>Converse</h2>
         </a>
-        <form class="form-inline">
-          <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" disabled>
-          <button class="btn btn-outline-success my-2 my-sm-0 mr-1" type="submit" disabled>Search</button>
+        <form class="form-inline" method="post" action="/search">
+          <input class="form-control mr-sm-2" type="search" placeholder="Search" name="query" aria-label="Search">
+          <button class="btn btn-outline-success my-2 my-sm-0 mr-1" type="submit">Search</button>
           <a class="btn btn-outline-secondary my-2" href="/thread/new">New thread</a>
         </form>
       </nav>
diff --git a/templates/search.html b/templates/search.html
new file mode 100644
index 000000000000..95384b1caf58
--- /dev/null
+++ b/templates/search.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta http-equiv="Content-Security-Policy" content="script-src 'self';">
+    <!-- Bootstrap CSS -->
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
+    <title>Converse Index</title>
+  </head>
+  <body>
+    <header>
+      <nav class="navbar navbar-light bg-light justify-content-between mb-3">
+        <a class="navbar-brand" href="/">
+          <h2>Converse</h2>
+        </a>
+        <form class="form-inline" method="post" action="/search">
+          <input class="form-control mr-sm-2" type="search" placeholder="Search" name="query" aria-label="Search">
+          <button class="btn btn-outline-success my-2 my-sm-0 mr-1" type="submit">Search</button>
+          <a class="btn btn-outline-secondary my-2" href="/thread/new">New thread</a>
+          <a class="btn btn-outline-secondary my-2" href="/">Back to index</a>
+        </form>
+      </nav>
+    </header>
+    <div class="container">
+      <div class="row">
+        <div class="col-4">
+          <h2>Search results for '{{ query }}':</h2>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col-12">
+          <div class="list-group">
+            {% for result in results -%}
+            <a href="/thread/{{ result.thread_id }}#post-{{ result.post_id }}" class="list-group-item flex-column list-group-item-action align-items-start">
+              <div class="d-flex w-100 justify-content-between">
+                <h5 class="mb-1">In thread '{{ result.title }}':</h5>
+                <div>{{ result.headline }}</div>
+                <div>(Posted by <i>{{ result.author }})</i></div>
+              </div>
+            </a>
+            {%- endfor %}
+          </div>
+        </div>
+      </div>
+    </div>
+  </body>
+</html>