Skip to content

Commit

Permalink
Merge pull request #25 from brokenhandsio/search
Browse files Browse the repository at this point in the history
Add basic search functionality to SteamPress
  • Loading branch information
0xTim committed Sep 21, 2017
2 parents 4ff4637 + 9cbb5e7 commit 0a346a2
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 16 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ There is an example of how it can work in a site (and what it requires in terms
* Support for comments via Disqus
* Open Graph and Twitter Card support
* RSS/Atom Feed support
* Blog Search

# How to Use

Expand Down Expand Up @@ -202,6 +203,10 @@ SteamPress automatically provides endpoints for registering RSS readers, either
* `copyright` - an optional copyright message to add to the feeds
* `imageURL` - an optional image/logo to add to the feeds. Note that for Atom this should a 2:1 landscape scaled image

## Search Support

SteamPress has a built in blog search. It was register a route, `/search` under your blog path, which you can send a query through to, with a key of `term` to search the blog.


# Expected Leaf Templates

Expand All @@ -218,6 +223,7 @@ The basic structure of your `Resources/View` directory should be:
* `profile.leaf` - the page for a user profile
* `tags.leaf` - the page for displaying all of the tags
* `authors.leaf` - the page for displaying all of the authors
* `search.leaf` - the page to display search results
* `admin`
* `createPost.leaf` - the page for creating and editing a blog post
* `createUser.leaf` - the page for creating and editing a user
Expand Down Expand Up @@ -300,6 +306,15 @@ This is the page for viewing all of the authors on the blog. It provides a usefu
* `uri` - the URI of the page - useful for Open Graph
* `google_analytics_identifier` - your Google Analytics identifier if configured

### `search.leaf`

This is the page that will display search results. It has a number of parameters on it on top of the standard parameters:

* `emptySearch` - returned if no search term of an empty search term was received
* `searchTerm` - the search term if provided
* `searchCount` - the number of results returned from the search
* `posts` - the posts found from the search, paginated, in `longSnippet` form

## Admin Site

### `index.leaf`
Expand Down Expand Up @@ -432,5 +447,4 @@ I anticipate SteamPress staying on a version 0 for some time, whilst some of the
On the roadmap we have:

* AMP/Facebook instant articles endpoints for posts
* Searching through the blog
* Saving state when logging in - if you go to a page (e.g. edit post) but need to be logged in, it would be great if you could head back to that page once logged in. Also, if you have edited a post and your session expires before you post it, wouldn't it be great if it remembered everything!
16 changes: 16 additions & 0 deletions Sources/SteamPress/Controllers/BlogController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ struct BlogController {
fileprivate let tagsPath = "tags"
fileprivate let authorsPath = "authors"
fileprivate let apiPath = "api"
fileprivate let searchPath = "search"
fileprivate let drop: Droplet
fileprivate let pathCreator: BlogPathCreator
fileprivate let viewFactory: ViewFactory
Expand All @@ -32,6 +33,7 @@ struct BlogController {
index.get(blogPostsPath, String.parameter, handler: blogPostHandler)
index.get(apiPath, tagsPath, handler: tagApiHandler)
index.get(blogPostsPath, handler: blogPostIndexRedirectHandler)
index.get(searchPath, handler: searchHandler)

if enableAuthorsPages {
index.get(authorsPath, String.parameter, handler: authorViewHandler)
Expand Down Expand Up @@ -111,6 +113,20 @@ struct BlogController {
func tagApiHandler(request: Request) throws -> ResponseRepresentable {
return try JSON(node: BlogTag.all().makeNode(in: nil))
}

func searchHandler(request: Request) throws -> ResponseRepresentable {
guard let searchTerm = request.query?["term"]?.string, searchTerm != "" else {
return try viewFactory.searchView(uri: request.getURIWithHTTPSIfReverseProxy(), searchTerm: nil, foundPosts: nil, emptySearch: true, user: getLoggedInUser(in: request))
}

let posts = try BlogPost.makeQuery().filter(BlogPost.Properties.published, true).or { orGroup in
try orGroup.filter(BlogPost.Properties.title, .contains, searchTerm)
try orGroup.filter(BlogPost.Properties.contents, .contains, searchTerm)
}
.sort(BlogPost.Properties.created, .descending).paginate(for: request)

return try viewFactory.searchView(uri: request.uri, searchTerm: searchTerm, foundPosts: posts, emptySearch: false, user: getLoggedInUser(in: request))
}

private func getLoggedInUser(in request: Request) -> BlogUser? {
var loggedInUser: BlogUser? = nil
Expand Down
2 changes: 2 additions & 0 deletions Sources/SteamPress/Feed Generators/RSSFeedGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ struct RSSFeedGenerator {
xmlFeed += "<pubDate>\(rfc822DateFormatter.string(from: postDate))</pubDate>\n"
}

xmlFeed += "<textinput>\n<description>Search \(title)</description>\n<title>Search</title>\n<link>\(getRootPath(for: request))/search?</link>\n<name>term</name>\n</textinput>\n"

for post in posts {
xmlFeed += try post.getPostRSSFeed(rootPath: getRootPath(for: request), dateFormatter: rfc822DateFormatter)
}
Expand Down
22 changes: 22 additions & 0 deletions Sources/SteamPress/Views/LeafViewFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,27 @@ struct LeafViewFactory: ViewFactory {

return try createPublicView(template: "blog/profile", uri: uri, parameters: parameters, user: loggedInUser)
}

func searchView(uri: URI, searchTerm: String?, foundPosts: Page<BlogPost>?, emptySearch: Bool, user: BlogUser?) throws -> View {
var parameters: [String: Vapor.Node] = [:]

let searchCount = foundPosts?.total ?? 0
if searchCount > 0 {
parameters["posts"] = try foundPosts?.makeNode(for: uri, in: BlogPostContext.longSnippet)
}

parameters["searchCount"] = searchCount.makeNode(in: nil)

if emptySearch {
parameters["emptySearch"] = true
}

if let searchTerm = searchTerm {
parameters["searchTerm"] = searchTerm.makeNode(in: nil)
}

return try createPublicView(template: "blog/search", uri: uri, parameters: parameters, user: user)
}

private func createPublicView(template: String, uri: URI, parameters: [String: NodeRepresentable], user: BlogUser? = nil) throws -> View {
var viewParameters = parameters
Expand All @@ -321,6 +342,7 @@ struct LeafViewFactory: ViewFactory {

return try viewRenderer.make(template, viewParameters.makeNode(in: nil))
}

}

extension URI {
Expand Down
1 change: 1 addition & 0 deletions Sources/SteamPress/Views/ViewFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ protocol ViewFactory {
func allAuthorsView(uri: URI, allAuthors: [BlogUser], user: BlogUser?) throws -> View
func allTagsView(uri: URI, allTags: [BlogTag], user: BlogUser?) throws -> View
func profileView(uri: URI, author: BlogUser, paginatedPosts: Page<BlogPost>, loggedInUser: BlogUser?) throws -> View
func searchView(uri: URI, searchTerm: String?, foundPosts: Page<BlogPost>?, emptySearch: Bool, user: BlogUser?) throws -> View
}
67 changes: 67 additions & 0 deletions Tests/SteamPressTests/BlogControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class BlogControllerTests: XCTestCase {
("testDisabledBlogAuthorsPath", testDisabledBlogAuthorsPath),
("testDisabledBlogTagsPath", testDisabledBlogTagsPath),
("testTagAPIEndpointReportsArrayOfTagsAsJson", testTagAPIEndpointReportsArrayOfTagsAsJson),
("testBlogPassedToSearchPageCorrectly", testBlogPassedToSearchPageCorrectly),
("testThatFlagSetIfEmptySearch", testThatFlagSetIfEmptySearch),
("testThatFlagSetIfNoSearchTerm", testThatFlagSetIfNoSearchTerm),
]

private var drop: Droplet!
Expand Down Expand Up @@ -410,4 +413,68 @@ class BlogControllerTests: XCTestCase {
XCTAssertEqual(nodeArray[0]["name"]?.string, "The first tag")
XCTAssertEqual(nodeArray[1]["name"]?.string, "The second tag")
}

func testBlogPassedToSearchPageCorrectly() throws {
try setupDrop()
let searchRequest = Request(method: .get, uri: "/search?term=Test")
let searchResponse = try drop.respond(to: searchRequest)

XCTAssertEqual(searchResponse.status, .ok)
XCTAssertEqual(viewFactory.searchTerm, "Test")
XCTAssertEqual(viewFactory.searchPosts?.data[0].title, post.title)
XCTAssertFalse(viewFactory.emptySearch ?? true)
}

func testThatFlagSetIfEmptySearch() throws {
try setupDrop()
let searchRequest = Request(method: .get, uri: "/search?term=")
let searchResponse = try drop.respond(to: searchRequest)

XCTAssertEqual(searchResponse.status, .ok)
XCTAssertNil(viewFactory.searchPosts)
XCTAssertTrue(viewFactory.emptySearch ?? false)
}

func testThatFlagSetIfNoSearchTerm() throws {
try setupDrop()
let searchRequest = Request(method: .get, uri: "/search")
let searchResponse = try drop.respond(to: searchRequest)

XCTAssertEqual(searchResponse.status, .ok)
XCTAssertNil(viewFactory.searchPosts)
XCTAssertTrue(viewFactory.emptySearch ?? false)
}
}

































10 changes: 10 additions & 0 deletions Tests/SteamPressTests/Fakes/CapturingViewFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,14 @@ class CapturingViewFactory: ViewFactory {
self.allTagsPageTags = allTags
return createDummyView()
}

private(set) var searchPosts: Page<BlogPost>?
private(set) var emptySearch: Bool?
private(set) var searchTerm: String?
func searchView(uri: URI, searchTerm: String?, foundPosts: Page<BlogPost>?, emptySearch: Bool, user: BlogUser?) throws -> View {
self.searchPosts = foundPosts
self.emptySearch = emptySearch
self.searchTerm = searchTerm
return createDummyView()
}
}
Loading

0 comments on commit 0a346a2

Please sign in to comment.