Handle drafts and private/unlisted posts

pull/4/head
Noah 2017-11-24 12:53:13 -08:00
parent b127c61dd7
commit 34d444ab96
11 changed files with 185 additions and 39 deletions

View File

@ -22,9 +22,16 @@ func (b *Blog) Login(w http.ResponseWriter, r *http.Request, u *users.User) erro
// LoginHandler shows and handles the login page.
func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
vars := &Vars{
Form: forms.Setup{},
vars := NewVars()
vars.Form = forms.Setup{}
var nextURL string
if r.Method == http.MethodPost {
nextURL = r.FormValue("next")
} else {
nextURL = r.URL.Query().Get("next")
}
vars.Data["NextURL"] = nextURL
if r.Method == http.MethodPost {
form := &forms.Login{
@ -46,10 +53,9 @@ func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
b.Login(w, r, user)
// A next URL given? TODO: actually get to work
next := r.FormValue("next")
log.Info("Redirect after login to: %s", next)
if len(next) > 0 && next[0] == '/' {
b.Redirect(w, next)
log.Info("Redirect after login to: %s", nextURL)
if len(nextURL) > 0 && nextURL[0] == '/' {
b.Redirect(w, nextURL)
} else {
b.Redirect(w, "/")
}

View File

@ -28,11 +28,14 @@ type PostMeta struct {
func (b *Blog) BlogRoutes(r *mux.Router) {
// Public routes
r.HandleFunc("/blog", b.BlogIndex)
r.HandleFunc("/tagged/{tag}", b.Tagged)
// Login-required routers.
loginRouter := mux.NewRouter()
loginRouter.HandleFunc("/blog/edit", b.EditBlog)
loginRouter.HandleFunc("/blog/delete", b.DeletePost)
loginRouter.HandleFunc("/blog/drafts", b.Drafts)
loginRouter.HandleFunc("/blog/private", b.PrivatePosts)
r.PathPrefix("/blog").Handler(
negroni.New(
negroni.HandlerFunc(b.LoginRequired),
@ -52,6 +55,33 @@ func (b *Blog) BlogRoutes(r *mux.Router) {
// BlogIndex renders the main index page of the blog.
func (b *Blog) BlogIndex(w http.ResponseWriter, r *http.Request) {
b.PartialIndex(w, r, "", "")
}
// Tagged lets you browse blog posts by category.
func (b *Blog) Tagged(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
tag, ok := params["tag"]
if !ok {
b.BadRequest(w, r, "Missing category in URL")
}
b.PartialIndex(w, r, tag, "")
}
// Drafts renders an index view of only draft posts. Login required.
func (b *Blog) Drafts(w http.ResponseWriter, r *http.Request) {
b.PartialIndex(w, r, "", DRAFT)
}
// PrivatePosts renders an index view of only private posts. Login required.
func (b *Blog) PrivatePosts(w http.ResponseWriter, r *http.Request) {
b.PartialIndex(w, r, "", PRIVATE)
}
// PartialIndex handles common logic for blog index views.
func (b *Blog) PartialIndex(w http.ResponseWriter, r *http.Request,
tag, privacy string) {
v := NewVars(map[interface{}]interface{}{})
// Get the blog index.
@ -60,9 +90,52 @@ func (b *Blog) BlogIndex(w http.ResponseWriter, r *http.Request) {
// The set of blog posts to show.
var pool []posts.Post
for _, post := range idx.Posts {
// Limiting by a specific privacy setting? (drafts or private only)
if privacy != "" {
switch privacy {
case DRAFT:
if post.Privacy != DRAFT {
continue
}
case PRIVATE:
if post.Privacy != PRIVATE && post.Privacy != UNLISTED {
continue
}
}
} else {
// Exclude certain posts in generic index views.
if (post.Privacy == PRIVATE || post.Privacy == UNLISTED) && !b.LoggedIn(r) {
continue
} else if post.Privacy == DRAFT {
continue
}
}
// Limit by tag?
if tag != "" {
var tagMatch bool
if tag != "" {
for _, check := range post.Tags {
if check == tag {
tagMatch = true
break
}
}
}
if !tagMatch {
continue
}
}
pool = append(pool, post)
}
if len(pool) == 0 {
b.NotFound(w, r, "No blog posts were found.")
return
}
sort.Sort(sort.Reverse(posts.ByUpdated(pool)))
// Query parameters.
@ -106,7 +179,7 @@ func (b *Blog) BlogIndex(w http.ResponseWriter, r *http.Request) {
}
// Render the post.
if post.ContentType == "markdown" {
if post.ContentType == string(MARKDOWN) {
rendered = template.HTML(b.RenderTrustedMarkdown(post.Body))
} else {
rendered = template.HTML(post.Body)
@ -138,6 +211,14 @@ func (b *Blog) viewPost(w http.ResponseWriter, r *http.Request, fragment string)
return err
}
// Handle post privacy.
if post.Privacy == PRIVATE || post.Privacy == DRAFT {
if !b.LoggedIn(r) {
b.NotFound(w, r)
return nil
}
}
v := NewVars(map[interface{}]interface{}{
"Post": post,
})
@ -170,7 +251,7 @@ func (b *Blog) RenderPost(p *posts.Post, indexView bool) template.HTML {
// Render the post to HTML.
var rendered template.HTML
if p.ContentType == "markdown" {
if p.ContentType == string(MARKDOWN) {
rendered = template.HTML(b.RenderTrustedMarkdown(p.Body))
} else {
rendered = template.HTML(p.Body)
@ -234,7 +315,7 @@ func (b *Blog) EditBlog(w http.ResponseWriter, r *http.Request) {
// Previewing, or submitting?
switch r.FormValue("submit") {
case "preview":
if post.ContentType == "markdown" || post.ContentType == "markdown+html" {
if post.ContentType == string(MARKDOWN) {
v.Data["preview"] = template.HTML(b.RenderMarkdown(post.Body))
} else {
v.Data["preview"] = template.HTML(post.Body)

21
core/constants.go Normal file
View File

@ -0,0 +1,21 @@
package core
// PostPrivacy values.
type PostPrivacy string
// ContentType values
type ContentType string
// Post privacy constants.
const (
PUBLIC PostPrivacy = "public"
PRIVATE = "private"
UNLISTED = "unlisted"
DRAFT = "draft"
)
// Content types for blog posts.
const (
MARKDOWN ContentType = "markdown"
HTML ContentType = "html"
)

View File

@ -76,7 +76,18 @@ func (b *Blog) CurrentUser(r *http.Request) (*users.User, error) {
return u, err
}
return &users.User{}, errors.New("not authenticated")
return &users.User{
Admin: false,
}, errors.New("not authenticated")
}
// LoggedIn returns whether the current user is logged in to an account.
func (b *Blog) LoggedIn(r *http.Request) bool {
session := b.Session(r)
if loggedIn, ok := session.Values["logged-in"].(bool); ok && loggedIn {
return true
}
return false
}
// AuthMiddleware loads the user's authentication state.

View File

@ -82,7 +82,7 @@ func (p *Post) Validate() error {
p.ContentType != "html" {
return errors.New("invalid setting for ContentType")
}
if p.Privacy != "public" && p.Privacy != "draft" && p.Privacy != "private" {
if p.Privacy != "public" && p.Privacy != "draft" && p.Privacy != "private" && p.Privacy != "unlisted" {
return errors.New("invalid setting for Privacy")
}
return nil

View File

@ -60,6 +60,10 @@ func (v *Vars) LoadDefaults(b *Blog, w http.ResponseWriter, r *http.Request) {
v.Title = s.Site.Title
v.Path = r.URL.Path
user, err := b.CurrentUser(r)
v.CurrentUser = user
v.LoggedIn = err == nil
// Add any flashed messages from the endpoint controllers.
session := b.Session(r)
if flashes := session.Flashes(); len(flashes) > 0 {
@ -71,14 +75,6 @@ func (v *Vars) LoadDefaults(b *Blog, w http.ResponseWriter, r *http.Request) {
}
v.CSRF = b.GenerateCSRFToken(w, r, session)
ctx := r.Context()
if user, ok := ctx.Value(userKey).(*users.User); ok {
if user.ID > 0 {
v.LoggedIn = true
v.CurrentUser = user
}
}
}
// TemplateVars is an interface that describes the template variable struct.

View File

@ -82,20 +82,37 @@
<div class="card-body">
<h4 class="card-title">About</h4>
{{ if .LoggedIn }}
Hello, {{ .CurrentUser.Username }}.<br>
<a href="/logout">Log out</a><br>
{{ if .CurrentUser.Admin }}
<a href="/admin">Admin center</a>
{{ end }}
{{ else }}
<a href="/login">Log in</a>
{{ end }}
<p>Hello, world!</p>
</div>
</div>
{{ if .LoggedIn }}
<div class="card mb-4">
<div class="card-body">
<h4 class="cart-title">Control Center</h4>
<p>
Logged in as: <a href="/u/{{ .CurrentUser.Username }}">{{ .CurrentUser.Username }}</a>
</p>
<ul class="list-unstyled">
{{ if .CurrentUser.Admin }}
<li class="list-item"><a href="/admin">Admin Center</a></li>
{{ end }}
<li class="list-item"><a href="/logout">Log out</a></li>
</ul>
<h5>Manage Blog</h5>
<ul class="list-unstyled">
<li class="list-item"><a href="/blog/edit">Post Blog Entry</a></li>
<li class="list-item"><a href="/blog/drafts">View Drafts</a></li>
<li class="list-item"><a href="/blog/private">View Private</a></li>
</ul>
</div>
</div>
{{ end }}
<div class="card mb-4">
<div class="card-body">
<h4 class="card-title">Archives</h4>
@ -153,6 +170,15 @@
<li class="nav-item">
<a class="nav-link" href="#">Back to top</a>
</li>
{{ if .LoggedIn }}
<li class="nav-item">
<a class="nav-link" href="/logout">Log out</a>
</li>
{{ else }}
<li class="nav-item">
<a class="nav-link" href="/login">Log in</a>
</li>
{{ end }}
</ul>
</div>
<div class="col-4">

View File

@ -56,16 +56,6 @@
> Markdown
</label>
</div>
<div class="form-check form-check-inline">
<label class="form-check-label">
<input type="radio"
class="form-check-input"
name="content-type"
value="markdown+html"
{{ if eq .ContentType "markdown+html" }}checked{{ end }}
> Markdown + HTML
</label>
</div>
<div class="form-check form-check-inline">
<label class="form-check-label">
<input type="radio"
@ -99,6 +89,7 @@
<select name="privacy" class="form-control">
<option value="public"{{ if eq .Privacy "public" }} selected{{ end }}>Public: everybody can see this post</option>
<option value="private"{{ if eq .Privacy "private" }} selected{{ end }}>Private: only site admins can see this post</option>
<option value="unlisted"{{ if eq .Privacy "unlisted" }} selected{{ end }}>Unlisted: only those with the direct link can see it</option>
<option value="draft"{{ if eq .Privacy "draft" }} selected{{ end }}>Draft: don't show this post on the blog anywhere</option>
</select>
</div>

View File

@ -8,6 +8,13 @@
{{ end }}
<div class="blog-meta">
{{ if eq $p.Privacy "private" }}
<span class="blog-private">[private]</span>
{{ else if eq $p.Privacy "draft" }}
<span class="blog-draft">[draft]</span>
{{ else if eq $p.Privacy "unlisted" }}
<span class="blog-unlisted">[unlisted]</span>
{{ end }}
<span title="{{ $p.Created.Format "Jan 2 2006 @ 15:04:05 MST" }}">
{{ $p.Created.Format "January 2, 2006" }}
</span>

View File

@ -16,6 +16,12 @@ a.blog-title {
font-size: smaller;
margin-bottom: 2rem;
}
.blog-meta .blog-private, .blog-meta .blog-unlisted {
color: #F00;
}
.blog-meta .blog-draft {
color: #909;
}
/* Code blocks treated as <pre> tags */
.markdown code {

View File

@ -4,6 +4,7 @@
<form name="login" action="/login" method="POST">
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
<input type="hidden" name="next" value="{{ .Data.NextURL }}">
<div class="row">
<div class="col">
<input type="text" name="username" class="form-control" placeholder="Username">