Skip to content

Commit

Permalink
Merge pull request #7 from brokenhandsio/drafts
Browse files Browse the repository at this point in the history
Drafts
  • Loading branch information
0xTim authored Mar 24, 2017
2 parents bc80bcf + 27ad958 commit fef6876
Show file tree
Hide file tree
Showing 14 changed files with 183 additions and 89 deletions.
37 changes: 18 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ There is an example of how it can work in a site (and what it requires in terms
* Multiple user accounts
* Tags on blog posts
* Snippet for posts
* Draft Posts
* Works with any Fluent driver
* Protected Admin route for creating blog posts
* Pagination on the main blog page
Expand Down Expand Up @@ -91,7 +92,7 @@ SteamPress currently supports using [Disqus](https://disqus.com) for the comment
}
```

This will pass it through to the Leaf templates for the Blog index (`blog.leaf`), blog posts (`blogpost.leaf`), author page (`profile.leaf`) and tag page (`tag.leaf`) so you can include it if needs be. If you want to manually set up comments you can do this yourself and just include the necessary files for your provider. This is mainly to provide easily configuration for the [Platform site](https://github.com/brokenhandsio/SteamPressExample).
This will pass it through to the Leaf templates for the Blog index (`blog.leaf`), blog posts (`blogpost.leaf`), author page (`profile.leaf`) and tag page (`tag.leaf`) so you can include it if needs be. If you want to manually set up comments you can do this yourself and just include the necessary files for your provider. This is mainly to provide easy configuration for the [Platform site](https://github.com/brokenhandsio/SteamPressExample).

## Open Graph Twitter Card Support

Expand All @@ -108,18 +109,18 @@ SteamPress expects there to be a number of Leaf template files in the correct lo
The basic structure of your `Resources/View` directory should be:

* `blog`
* `blog.leaf` - the main index page
* `blogpost.leaf` - the page for a single blog post
* `tag.leaf` - the page for a tag
* `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
* `admin`
* `createPost.leaf` - the page for creating and editing a blog post
* `createUser.leaf` - the page for creating and editing a user
* `index.leaf` - the index page for the Admin site
* `login.leaf` - the login page for the Admin site
* `resetPassword.leaf` - the page for resetting your password
* `blog.leaf` - the main index page
* `blogpost.leaf` - the page for a single blog post
* `tag.leaf` - the page for a tag
* `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
* `admin`
* `createPost.leaf` - the page for creating and editing a blog post
* `createUser.leaf` - the page for creating and editing a user
* `index.leaf` - the index page for the Admin site
* `login.leaf` - the login page for the Admin site
* `resetPassword.leaf` - the page for resetting your password

## Main Blog Site

Expand Down Expand Up @@ -268,7 +269,7 @@ The blog post has a number of `Context`s you can pass to the `makeNode()` functi

* `.shortSnippet` - this will return the post with an `id`, `title`, `author_name`, `author_username`, `slug_url`, `created_date` (Human readable) and `short_snippet`
* `.longSnippet` - this will return the post with an `id`, `title`, `author_name`, `author_username`, `slug_url`, `created_date` (Human readable) and `long_snippet`. It will also include all of the tags in a `tags` object if there are any associated with that post
* `.all` - this returns the post with all information, including both snippet lengths, including author names and human readable dates, as well as both dates in ISO 8601 format under the parameter names `created_date_iso8601` and `last_edited_date_iso8601`
* `.all` - this returns the post with all information, including both snippet lengths, including author names and human readable dates, as well as both dates in ISO 8601 format under the parameter names `created_date_iso8601` and `last_edited_date_iso8601`.

If no `Context` is supplied to the `makeNode()` call you will get:

Expand All @@ -278,6 +279,7 @@ If no `Context` is supplied to the `makeNode()` call you will get:
* `bloguser_id` - The ID of the Author of the post
* `created` - The time the post was created as a `Double`
* `slug_url`
* `published` - Whether the post has been published or not

## Blog User

Expand Down Expand Up @@ -322,17 +324,14 @@ SteamPress also contains an API for accessing certain things that may be useful.
# Known issues

* When the admin user is created when first accessing the login screen, sometimes two are created so you need to use the first password displayed. You can then delete the second Admin user in the Admin pane.
* Despite me being a big believer in TDD and it saving me on many occasions, I neglected to actually write any tests for this. So despite the fact that I have been tripped up due to no tests, I haven't written the unit tests yet, mainly because this started out as a Spike to see how easy it would be. They will definitely be coming soon!

# Roadmap

I anticipate SteamPress staying on a version 0 for some time, whilst some of the biggest features are implemented. Semantic versioning makes life a little difficult for this as any new endpoints will require a breaking change for you to add Leaf templates! However, I will aim to stabilise this as quickly as possible, and any breaking changes will be announced in the [releases](https://github.com/brokenhandsio/SteamPress/releases) page.

On the roadmap we have:

* Image uploading - you can link to images easily but can't upload any without redeploying the site - I may implement some functionality for this depending on whether people want images going to the same site as the code or something like an S3 bucket (I'm leaning towards the S3 option so answers on a postcard!)
* Blog drafts - it would be nice not to publish posts until you want to
* Sitemap/RSS feed - again for SEO
* AMP endpoints for posts
* Sitemap/RSS feed for SEO
* 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!
39 changes: 33 additions & 6 deletions Sources/SteamPress/Controllers/BlogAdminController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,20 @@ struct BlogAdminController {

// MARK: - Blog Posts handlers
func createPostHandler(_ request: Request) throws -> ResponseRepresentable {
return try viewFactory.createBlogPostView(uri: request.uri, errors: nil, title: nil, contents: nil, slugUrl: nil, tags: nil, isEditing: false, postToEdit: nil)
return try viewFactory.createBlogPostView(uri: request.uri, errors: nil, title: nil, contents: nil, slugUrl: nil, tags: nil, isEditing: false, postToEdit: nil, draft: true)
}

func createPostPostHandler(_ request: Request) throws -> ResponseRepresentable {
let rawTitle = request.data["inputTitle"]?.string
let rawContents = request.data["inputPostContents"]?.string
let rawTags = request.data["inputTags"]?.array
let rawSlugUrl = request.data["inputSlugUrl"]?.string
let draft = request.data["save-draft"]?.string
let publish = request.data["publish"]?.string

if draft == nil && publish == nil {
throw Abort.badRequest
}

// I must be able to inline all of this
var tagsArray: [Node] = []
Expand All @@ -69,7 +75,7 @@ struct BlogAdminController {
}

if let createPostErrors = validatePostCreation(title: rawTitle, contents: rawContents, slugUrl: rawSlugUrl) {
return try viewFactory.createBlogPostView(uri: request.uri, errors: createPostErrors, title: rawTitle, contents: rawContents, slugUrl: rawSlugUrl, tags: tagsArray, isEditing: false, postToEdit: nil)
return try viewFactory.createBlogPostView(uri: request.uri, errors: createPostErrors, title: rawTitle, contents: rawContents, slugUrl: rawSlugUrl, tags: tagsArray, isEditing: false, postToEdit: nil, draft: true)
}

guard let user = try request.auth.user() as? BlogUser, let title = rawTitle, let contents = rawContents, var slugUrl = rawSlugUrl else {
Expand All @@ -80,8 +86,14 @@ struct BlogAdminController {

// Make sure slugUrl is unique
slugUrl = BlogPost.generateUniqueSlugUrl(from: slugUrl)

var published = false

if let _ = publish {
published = true
}

var newPost = BlogPost(title: title, contents: contents, author: user, creationDate: creationDate, slugUrl: slugUrl)
var newPost = BlogPost(title: title, contents: contents, author: user, creationDate: creationDate, slugUrl: slugUrl, published: published)
try newPost.save()

// Save the tags
Expand Down Expand Up @@ -116,14 +128,20 @@ struct BlogAdminController {
func editPostHandler(request: Request, post: BlogPost) throws -> ResponseRepresentable {
let tags = try post.tags()
let tagsArray: [Node] = tags.map { $0.name.makeNode() }
return try viewFactory.createBlogPostView(uri: request.uri, errors: nil, title: post.title, contents: post.contents, slugUrl: post.slugUrl, tags: tagsArray, isEditing: true, postToEdit: post)
return try viewFactory.createBlogPostView(uri: request.uri, errors: nil, title: post.title, contents: post.contents, slugUrl: post.slugUrl, tags: tagsArray, isEditing: true, postToEdit: post, draft: !post.published)
}

func editPostPostHandler(request: Request, post: BlogPost) throws -> ResponseRepresentable {
let rawTitle = request.data["inputTitle"]?.string
let rawContents = request.data["inputPostContents"]?.string
let rawTags = request.data["inputTags"]?.array
let rawSlugUrl = request.data["inputSlugUrl"]?.string
let draft = request.data["save-draft"]?.string
let publish = request.data["publish"]?.string

if draft == nil && publish == nil {
throw Abort.badRequest
}

var tagsArray: [Node] = []

Expand All @@ -135,7 +153,7 @@ struct BlogAdminController {
}

if let errors = validatePostCreation(title: rawTitle, contents: rawContents, slugUrl: rawSlugUrl) {
return try viewFactory.createBlogPostView(uri: request.uri, errors: errors, title: rawTitle, contents: rawContents, slugUrl: rawSlugUrl, tags: tagsArray, isEditing: true, postToEdit: post)
return try viewFactory.createBlogPostView(uri: request.uri, errors: errors, title: rawTitle, contents: rawContents, slugUrl: rawSlugUrl, tags: tagsArray, isEditing: true, postToEdit: post, draft: false)
}

guard let title = rawTitle, let contents = rawContents, let slugUrl = rawSlugUrl else {
Expand All @@ -145,7 +163,6 @@ struct BlogAdminController {
var post = post
post.title = title
post.contents = contents
post.lastEdited = Date()
if (post.slugUrl != slugUrl) {
post.slugUrl = BlogPost.generateUniqueSlugUrl(from: slugUrl)
}
Expand Down Expand Up @@ -175,6 +192,16 @@ struct BlogAdminController {
for newTagString in tagsToAdd {
try BlogTag.addTag(newTagString, to: post)
}

if post.published {
post.lastEdited = Date()
}
else {
post.created = Date()
if let _ = publish {
post.published = true
}
}

try post.save()

Expand Down
12 changes: 7 additions & 5 deletions Sources/SteamPress/Controllers/BlogController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ struct BlogController {
fileprivate let pathCreator: BlogPathCreator
fileprivate let viewFactory: ViewFactory
fileprivate let postsPerPage: Int
fileprivate let config: Config

// MARK: - Initialiser
init(drop: Droplet, pathCreator: BlogPathCreator, viewFactory: ViewFactory, postsPerPage: Int) {
init(drop: Droplet, pathCreator: BlogPathCreator, viewFactory: ViewFactory, postsPerPage: Int, config: Config) {
self.drop = drop
self.pathCreator = pathCreator
self.viewFactory = viewFactory
self.postsPerPage = postsPerPage
self.config = config
}

// MARK: - Add routes
Expand All @@ -42,7 +44,7 @@ struct BlogController {
func indexHandler(request: Request) throws -> ResponseRepresentable {
let tags = try BlogTag.all()
let authors = try BlogUser.all()
let paginatedBlogPosts = try BlogPost.query().sort("created", .descending).paginator(postsPerPage, request: request)
let paginatedBlogPosts = try BlogPost.query().filter("published", true).sort("created", .descending).paginator(postsPerPage, request: request)

return try viewFactory.blogIndexView(uri: request.uri, paginatedPosts: paginatedBlogPosts, tags: tags, authors: authors, loggedInUser: getLoggedInUser(in: request), disqusName: getDisqusName(), siteTwitterHandle: getSiteTwitterHandle())
}
Expand Down Expand Up @@ -72,7 +74,7 @@ struct BlogController {
throw Abort.notFound
}

let paginatedBlogPosts = try tag.blogPosts().sorted { $0.created > $1.created }.paginator(postsPerPage, request: request)
let paginatedBlogPosts = try tag.blogPosts().paginator(postsPerPage, request: request)

return try viewFactory.tagView(uri: request.uri, tag: tag, paginatedPosts: paginatedBlogPosts, user: getLoggedInUser(in: request), disqusName: getDisqusName(), siteTwitterHandle: getSiteTwitterHandle())
}
Expand Down Expand Up @@ -113,11 +115,11 @@ struct BlogController {
}

private func getDisqusName() -> String? {
return drop.config["disqus", "disqusName"]?.string
return config["disqus", "disqusName"]?.string
}

private func getSiteTwitterHandle() -> String? {
return drop.config["twitter", "siteHandle"]?.string
return config["twitter", "siteHandle"]?.string
}

}
60 changes: 32 additions & 28 deletions Sources/SteamPress/Models/BlogPost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@ public class BlogPost: Model {
public var created: Date
public var lastEdited: Date?
public var slugUrl: String
public var published: Bool

init(title: String, contents: String, author: BlogUser, creationDate: Date, slugUrl: String) {
init(title: String, contents: String, author: BlogUser, creationDate: Date, slugUrl: String, published: Bool) {
self.id = nil
self.title = title
self.contents = contents
self.author = author.id
self.created = creationDate
self.slugUrl = BlogPost.generateUniqueSlugUrl(from: slugUrl)
self.lastEdited = nil
self.published = published
}

required public init(node: Node, in context: Context) throws {
Expand All @@ -31,6 +33,7 @@ public class BlogPost: Model {
contents = try node.extract("contents")
author = try node.extract("bloguser_id")
slugUrl = try node.extract("slug_url")
published = try node.extract("published")
let createdTime: Double = try node.extract("created")
let lastEditedTime: Double? = try? node.extract("last_edited")

Expand All @@ -46,14 +49,14 @@ extension BlogPost: NodeRepresentable {
public func makeNode(context: Context) throws -> Node {
let createdTime = created.timeIntervalSince1970

var node = try Node(node: [
"id": id,
"title": title,
"contents": contents,
"bloguser_id": author,
"created": createdTime,
"slug_url": slugUrl
])
var node: [String: Node] = [:]
node["id"] = id
node["title"] = title.makeNode()
node["contents"] = contents.makeNode()
node["bloguser_id"] = author?.makeNode()
node["created"] = createdTime.makeNode()
node["slug_url"] = slugUrl.makeNode()
node["published"] = published.makeNode()

if let lastEdited = lastEdited {
node["last_edited"] = lastEdited.timeIntervalSince1970.makeNode()
Expand All @@ -67,25 +70,25 @@ extension BlogPost: NodeRepresentable {

switch context {
case BlogPostContext.shortSnippet:
node = try Node(node: [
"id": id,
"title": title,
"author_name": try getAuthor()?.name.makeNode(),
"author_username": try getAuthor()?.username.makeNode(),
"short_snippet": shortSnippet().makeNode(),
"created_date": createdDate.makeNode(),
"slug_url": slugUrl
])
node = [:]
node["id"] = id
node["title"] = title.makeNode()
node["author_name"] = try getAuthor()?.name.makeNode()
node["author_username"] = try getAuthor()?.username.makeNode()
node["short_snippet"] = shortSnippet().makeNode()
node["created_date"] = createdDate.makeNode()
node["slug_url"] = slugUrl.makeNode()
node["published"] = published.makeNode()
case BlogPostContext.longSnippet:
node = try Node(node: [
"id": id,
"title": title,
"author_name": try getAuthor()?.name.makeNode(),
"author_username": try getAuthor()?.username.makeNode(),
"long_snippet": longSnippet().makeNode(),
"created_date": createdDate.makeNode(),
"slug_url": slugUrl
])
node = [:]
node["id"] = id
node["title"] = title.makeNode()
node["author_name"] = try getAuthor()?.name.makeNode()
node["author_username"] = try getAuthor()?.username.makeNode()
node["long_snippet"] = longSnippet().makeNode()
node["created_date"] = createdDate.makeNode()
node["slug_url"] = slugUrl.makeNode()
node["published"] = published.makeNode()

let allTags = try tags()

Expand Down Expand Up @@ -123,7 +126,7 @@ extension BlogPost: NodeRepresentable {
default: break
}

return node
return try node.makeNode()
}
}

Expand All @@ -138,6 +141,7 @@ extension BlogPost {
posts.double("created")
posts.double("last_edited", optional: true)
posts.string("slug_url", unique: true)
posts.bool("published")
}
}

Expand Down
13 changes: 13 additions & 0 deletions Sources/SteamPress/Models/BlogPostDraft.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Fluent

struct BlogPostDraft: Preparation {
static func prepare(_ database: Database) throws {
try database.modify(BlogPost.entity, closure: { blogPost in
blogPost.bool("published", optional: false, default: true)
})
}

static func revert(_ database: Database) throws {

}
}
2 changes: 1 addition & 1 deletion Sources/SteamPress/Models/BlogTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ extension BlogTag {

extension BlogTag {
func blogPosts() throws -> [BlogPost] {
return try siblings().all()
return try siblings().filter("published", true).sort("created", .descending).all()
}

func deletePivot(for post: BlogPost) throws {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SteamPress/Models/BlogUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ extension BlogUser: Auth.User {

extension BlogUser {
func posts() throws -> [BlogPost] {
return try children("bloguser_id", BlogPost.self).all()
return try children("bloguser_id", BlogPost.self).sort("created", .descending).filter("published", true).all()
}
}

Expand Down
Loading

0 comments on commit fef6876

Please sign in to comment.