From eab7dae75b839a21c06bf6724a44562ccd0f3200 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 10 Feb 2018 13:16:20 -0800 Subject: [PATCH] Further simplify template rendering --- core/admin.go | 2 +- core/auth.go | 1 - core/blog.go | 26 ++--- core/comments.go | 12 +- core/internal/controllers/setup/setup.go | 1 + core/internal/middleware/auth/auth.go | 3 - core/internal/render/functions.go | 14 +++ core/internal/render/templates.go | 125 ++++++++++----------- core/pages.go | 2 +- core/templates.go | 135 ++--------------------- root/.layout.gohtml | 2 +- root/blog/entry.gohtml | 2 +- 12 files changed, 105 insertions(+), 220 deletions(-) create mode 100644 core/internal/controllers/setup/setup.go create mode 100644 core/internal/render/functions.go diff --git a/core/admin.go b/core/admin.go index 8f2a748..b910951 100644 --- a/core/admin.go +++ b/core/admin.go @@ -33,7 +33,7 @@ func (b *Blog) AdminRoutes(r *mux.Router) { // AdminHandler is the admin landing page. func (b *Blog) AdminHandler(w http.ResponseWriter, r *http.Request) { - b.RenderTemplate(w, r, "admin/index", render.Vars{}) + render.Template(w, r, "admin/index", NewVars()) } // FileTree holds information about files in the document roots. diff --git a/core/auth.go b/core/auth.go index fd8cf92..29efd4b 100644 --- a/core/auth.go +++ b/core/auth.go @@ -23,7 +23,6 @@ func (b *Blog) AuthRoutes(r *mux.Router) { // MustLogin handles errors from the LoginRequired middleware by redirecting // the user to the login page. func (b *Blog) MustLogin(w http.ResponseWriter, r *http.Request) { - log.Info("MustLogin for %s", r.URL.Path) responses.Redirect(w, "/login?next="+r.URL.Path) } diff --git a/core/blog.go b/core/blog.go index 4708d99..de2e5d3 100644 --- a/core/blog.go +++ b/core/blog.go @@ -44,6 +44,10 @@ type Archive struct { // BlogRoutes attaches the blog routes to the app. func (b *Blog) BlogRoutes(r *mux.Router) { + render.Funcs["RenderIndex"] = b.RenderIndex + render.Funcs["RenderPost"] = b.RenderPost + render.Funcs["RenderTags"] = b.RenderTags + // Public routes r.HandleFunc("/blog", b.IndexHandler) r.HandleFunc("/blog.rss", b.RSSHandler) @@ -302,19 +306,14 @@ func (b *Blog) RenderIndex(r *http.Request, tag, privacy string) template.HTML { // Render the blog index partial. var output bytes.Buffer v := render.Vars{ + NoLayout: true, Data: map[interface{}]interface{}{ "PreviousPage": previousPage, "NextPage": nextPage, "View": view, }, } - v = b.LoadDefaults(v, r) - render.PartialTemplate(&output, "blog/index.partial", render.Config{ - Request: r, - Vars: &v, - WithLayout: false, - Functions: b.TemplateFuncs(nil, r, nil), - }) + b.RenderTemplate(&output, r, "blog/index.partial", v) return template.HTML(output.String()) } @@ -333,19 +332,13 @@ func (b *Blog) RenderTags(r *http.Request, indexView bool) template.HTML { var output bytes.Buffer v := render.Vars{ + NoLayout: true, Data: map[interface{}]interface{}{ "IndexView": indexView, "Tags": tags, }, } - v = b.LoadDefaults(v, r) - render.PartialTemplate(&output, "blog/tags.partial", render.Config{ - Request: r, - Vars: &v, - WithLayout: false, - Functions: b.TemplateFuncs(nil, nil, nil), - }) - // b.RenderPartialTemplate(&output, r, "blog/tags.partial", v, false, nil) + b.RenderTemplate(&output, r, "blog/tags.partial", v) return template.HTML(output.String()) } @@ -455,6 +448,7 @@ func (b *Blog) RenderPost(r *http.Request, p *posts.Post, indexView bool, numCom } meta := render.Vars{ + NoLayout: true, Data: map[interface{}]interface{}{ "Post": p, "Rendered": rendered, @@ -465,7 +459,7 @@ func (b *Blog) RenderPost(r *http.Request, p *posts.Post, indexView bool, numCom }, } output := bytes.Buffer{} - err = b.RenderPartialTemplate(&output, r, "blog/entry.partial", meta, false, nil) + err = b.RenderTemplate(&output, r, "blog/entry.partial", meta) if err != nil { return template.HTML(fmt.Sprintf("[template error in blog/entry.partial: %s]", err.Error())) } diff --git a/core/comments.go b/core/comments.go index 8af2624..9d10114 100644 --- a/core/comments.go +++ b/core/comments.go @@ -10,7 +10,6 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" - gorilla "github.com/gorilla/sessions" "github.com/kirsle/blog/core/internal/log" "github.com/kirsle/blog/core/internal/markdown" "github.com/kirsle/blog/core/internal/middleware/auth" @@ -23,6 +22,8 @@ import ( // CommentRoutes attaches the comment routes to the app. func (b *Blog) CommentRoutes(r *mux.Router) { + render.Funcs["RenderComments"] = b.RenderComments + r.HandleFunc("/comments", b.CommentHandler) r.HandleFunc("/comments/subscription", b.SubscriptionHandler) r.HandleFunc("/comments/quick-delete", b.QuickDeleteHandler) @@ -40,13 +41,16 @@ type CommentMeta struct { } // RenderComments renders a comment form partial and returns the HTML. -func (b *Blog) RenderComments(session *gorilla.Session, csrfToken, url, subject string, ids ...string) template.HTML { +func (b *Blog) RenderComments(r *http.Request, subject string, ids ...string) template.HTML { id := strings.Join(ids, "-") + session := sessions.Get(r) + url := r.URL.Path // Load their cached name and email if they posted a comment before. name, _ := session.Values["c.name"].(string) email, _ := session.Values["c.email"].(string) editToken, _ := session.Values["c.token"].(string) + csrf, _ := session.Values["csrf"].(string) // Check if the user is a logged-in admin, to make all comments editable. var isAdmin bool @@ -71,7 +75,7 @@ func (b *Blog) RenderComments(session *gorilla.Session, csrfToken, url, subject c.HTML = template.HTML(markdown.RenderMarkdown(c.Body)) c.ThreadID = thread.ID c.OriginURL = url - c.CSRF = csrfToken + c.CSRF = csrf // Look up the author username. if c.UserID > 0 { @@ -120,7 +124,7 @@ func (b *Blog) RenderComments(session *gorilla.Session, csrfToken, url, subject ID: thread.ID, OriginURL: url, Subject: subject, - CSRF: csrfToken, + CSRF: csrf, Thread: &thread, NewComment: comments.Comment{ Name: name, diff --git a/core/internal/controllers/setup/setup.go b/core/internal/controllers/setup/setup.go new file mode 100644 index 0000000..9bca696 --- /dev/null +++ b/core/internal/controllers/setup/setup.go @@ -0,0 +1 @@ +package setup diff --git a/core/internal/middleware/auth/auth.go b/core/internal/middleware/auth/auth.go index be3d87f..dad1fd8 100644 --- a/core/internal/middleware/auth/auth.go +++ b/core/internal/middleware/auth/auth.go @@ -5,7 +5,6 @@ import ( "errors" "net/http" - "github.com/kirsle/blog/core/internal/log" "github.com/kirsle/blog/core/internal/models/users" "github.com/kirsle/blog/core/internal/sessions" "github.com/kirsle/blog/core/internal/types" @@ -46,8 +45,6 @@ func LoginRequired(onError http.HandlerFunc) negroni.HandlerFunc { return } } - - log.Info("Redirect away!") onError(w, r) } diff --git a/core/internal/render/functions.go b/core/internal/render/functions.go new file mode 100644 index 0000000..9e18b3c --- /dev/null +++ b/core/internal/render/functions.go @@ -0,0 +1,14 @@ +package render + +import ( + "html/template" + "strings" + "time" +) + +// Funcs is a global funcmap that the blog can hook its internal +// methods onto. +var Funcs = template.FuncMap{ + "StringsJoin": strings.Join, + "Now": time.Now, +} diff --git a/core/internal/render/templates.go b/core/internal/render/templates.go index 3367a92..0b80619 100644 --- a/core/internal/render/templates.go +++ b/core/internal/render/templates.go @@ -9,23 +9,14 @@ import ( "github.com/kirsle/blog/core/internal/forms" "github.com/kirsle/blog/core/internal/log" + "github.com/kirsle/blog/core/internal/middleware" + "github.com/kirsle/blog/core/internal/middleware/auth" + "github.com/kirsle/blog/core/internal/models/settings" "github.com/kirsle/blog/core/internal/models/users" + "github.com/kirsle/blog/core/internal/sessions" + "github.com/kirsle/blog/core/internal/types" ) -// Config provides the settings and injectables for rendering templates. -type Config struct { - // Refined and raw variables for the templates. - Vars *Vars // Normal RenderTemplate's - - // Wrap the template with the `.layout.gohtml` - WithLayout bool - - // Inject your own functions for the Go templates. - Functions map[string]interface{} - - Request *http.Request -} - // Vars is an interface to implement by the templates to pass their own custom // variables in. It auto-loads global template variables (site name, etc.) // when the template is rendered. @@ -34,6 +25,7 @@ type Vars struct { SetupNeeded bool Title string Path string + TemplatePath string LoggedIn bool CurrentUser *users.User CSRF string @@ -53,18 +45,58 @@ type Vars struct { Form forms.Form } -// PartialTemplate handles rendering a Go template to a writer, without -// doing anything extra to the vars or dealing with net/http. This is ideal for -// rendering partials, such as comment partials. -// -// This will wrap the template in `.layout.gohtml` by default. To render just -// a bare template on its own, i.e. for partial templates, create a Vars struct -// with `Vars{NoIndex: true}` -func PartialTemplate(w io.Writer, path string, C Config) error { - if C.Request == nil { - panic("render.RenderPartialTemplate(): The *http.Request is nil!?") +// loadDefaults combines template variables with default, globally available vars. +func (v *Vars) loadDefaults(r *http.Request) { + // Get the site settings. + s, err := settings.Load() + if err != nil { + s = settings.Defaults() } + if s.Initialized == false && !strings.HasPrefix(r.URL.Path, "/initial-setup") { + v.SetupNeeded = true + } + v.Request = r + v.RequestTime = r.Context().Value(types.StartTimeKey).(time.Time) + v.Title = s.Site.Title + v.Path = r.URL.Path + + user, err := auth.CurrentUser(r) + v.CurrentUser = user + v.LoggedIn = err == nil +} + +// Template responds with an HTML template. +// +// The vars will be massaged a bit to load the global defaults (such as the +// website title and user login status), the user's session may be updated with +// new CSRF token, and other such things. If you just want to render a template +// without all that nonsense, use RenderPartialTemplate. +func Template(w io.Writer, r *http.Request, path string, v Vars) error { + // Inject globally available variables. + v.loadDefaults(r) + + // If this is the HTTP response, handle session-related things. + if rw, ok := w.(http.ResponseWriter); ok { + rw.Header().Set("Content-Type", "text/html; encoding=UTF-8") + session := sessions.Get(r) + + // Flashed messages. + if flashes := session.Flashes(); len(flashes) > 0 { + for _, flash := range flashes { + _ = flash + v.Flashes = append(v.Flashes, flash.(string)) + } + session.Save(r, rw) + } + + // CSRF token for forms. + v.CSRF = middleware.GenerateCSRFToken(rw, r, session) + } + + v.RequestDuration = time.Now().Sub(v.RequestTime) + v.Editable = !strings.HasPrefix(path, "admin/") + // v interface{}, withLayout bool, functions map[string]interface{}) error { var ( layout Filepath @@ -80,7 +112,7 @@ func PartialTemplate(w io.Writer, path string, C Config) error { } // Get the layout template. - if C.WithLayout { + if !v.NoLayout { templateName = "layout" layout, err = ResolvePath(".layout") if err != nil { @@ -98,27 +130,12 @@ func PartialTemplate(w io.Writer, path string, C Config) error { return err } - // Template functions. - funcmap := template.FuncMap{ - "StringsJoin": strings.Join, - "Now": time.Now, - "TemplateName": func() string { - return filepath.URI - }, - } - if C.Functions != nil { - for name, fn := range C.Functions { - funcmap[name] = fn - } - } - - // Useful template functions. - t := template.New(filepath.Absolute).Funcs(funcmap) + t := template.New(filepath.Absolute).Funcs(Funcs) // Parse the template files. The layout comes first because it's the wrapper // and allows the filepath template to set the page title. var templates []string - if C.WithLayout { + if !v.NoLayout { templates = append(templates, layout.Absolute) } t, err = t.ParseFiles(append(templates, commentEntry.Absolute, filepath.Absolute)...) @@ -127,7 +144,7 @@ func PartialTemplate(w io.Writer, path string, C Config) error { return err } - err = t.ExecuteTemplate(w, templateName, C.Vars) + err = t.ExecuteTemplate(w, templateName, v) if err != nil { log.Error("Template parsing error: %s", err) return err @@ -135,25 +152,3 @@ func PartialTemplate(w io.Writer, path string, C Config) error { return nil } - -// Template responds with an HTML template. -// -// The vars will be massaged a bit to load the global defaults (such as the -// website title and user login status), the user's session may be updated with -// new CSRF token, and other such things. If you just want to render a template -// without all that nonsense, use RenderPartialTemplate. -func Template(w http.ResponseWriter, path string, C Config) error { - if C.Request == nil { - panic("render.RenderTemplate(): The *http.Request is nil!?") - } - - w.Header().Set("Content-Type", "text/html; encoding=UTF-8") - PartialTemplate(w, path, Config{ - Request: C.Request, - Vars: C.Vars, - WithLayout: true, - Functions: C.Functions, - }) - - return nil -} diff --git a/core/pages.go b/core/pages.go index c2ce736..7bea655 100644 --- a/core/pages.go +++ b/core/pages.go @@ -41,7 +41,7 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) { // Is it a template file? if strings.HasSuffix(filepath.URI, ".gohtml") { - b.RenderTemplate(w, r, filepath.URI, render.Vars{}) + b.RenderTemplate(w, r, filepath.URI, NewVars()) return } diff --git a/core/templates.go b/core/templates.go index b8759ec..07d45bf 100644 --- a/core/templates.go +++ b/core/templates.go @@ -1,49 +1,12 @@ package core import ( - "html/template" "io" "net/http" - "strings" - "time" - "github.com/kirsle/blog/core/internal/forms" - "github.com/kirsle/blog/core/internal/middleware" - "github.com/kirsle/blog/core/internal/middleware/auth" - "github.com/kirsle/blog/core/internal/models/settings" - "github.com/kirsle/blog/core/internal/models/users" "github.com/kirsle/blog/core/internal/render" - "github.com/kirsle/blog/core/internal/sessions" - "github.com/kirsle/blog/core/internal/types" ) -// Vars is an interface to implement by the templates to pass their own custom -// variables in. It auto-loads global template variables (site name, etc.) -// when the template is rendered. -type Vars struct { - // Global, "constant" template variables. - SetupNeeded bool - Title string - Path string - LoggedIn bool - CurrentUser *users.User - CSRF string - Editable bool // page is editable - Request *http.Request - RequestTime time.Time - RequestDuration time.Duration - - // Configuration variables - NoLayout bool // don't wrap in .layout.html, just render the template - - // Common template variables. - Message string - Flashes []string - Error error - Data map[interface{}]interface{} - Form forms.Form -} - // NewVars initializes a Vars struct with the custom Data map initialized. // You may pass in an initial value for this map if you want. func NewVars(data ...map[interface{}]interface{}) render.Vars { @@ -58,102 +21,20 @@ func NewVars(data ...map[interface{}]interface{}) render.Vars { } } -// LoadDefaults combines template variables with default, globally available vars. -func (b *Blog) LoadDefaults(v render.Vars, r *http.Request) render.Vars { - // Get the site settings. - s, err := settings.Load() - if err != nil { - s = settings.Defaults() - } - - if s.Initialized == false && !strings.HasPrefix(r.URL.Path, "/initial-setup") { - v.SetupNeeded = true - } - v.Request = r - v.RequestTime = r.Context().Value(types.StartTimeKey).(time.Time) - v.Title = s.Site.Title - v.Path = r.URL.Path - - user, err := auth.CurrentUser(r) - v.CurrentUser = user - v.LoggedIn = err == nil - - return v -} - -// RenderPartialTemplate handles rendering a Go template to a writer, without -// doing anything extra to the vars or dealing with net/http. This is ideal for -// rendering partials, such as comment partials. -// -// This will wrap the template in `.layout.gohtml` by default. To render just -// a bare template on its own, i.e. for partial templates, create a Vars struct -// with `Vars{NoIndex: true}` -func (b *Blog) RenderPartialTemplate(w io.Writer, r *http.Request, path string, v render.Vars, withLayout bool, functions map[string]interface{}) error { - v = b.LoadDefaults(v, r) - return render.PartialTemplate(w, path, render.Config{ - Request: r, - Vars: &v, - WithLayout: withLayout, - Functions: b.TemplateFuncs(nil, nil, functions), - }) -} - // RenderTemplate responds with an HTML template. // // The vars will be massaged a bit to load the global defaults (such as the // website title and user login status), the user's session may be updated with -// new CSRF token, and other such things. If you just want to render a template -// without all that nonsense, use RenderPartialTemplate. -func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars render.Vars) error { +// new CSRF token, and other such things. +// +// For server-rendered templates given directly to the user (i.e., in controllers), +// give it the http.ResponseWriter; for partial templates you can give it a +// bytes.Buffer to write to instead. The subtle difference is whether or not the +// template will have access to the request's session. +func (b *Blog) RenderTemplate(w io.Writer, r *http.Request, path string, vars render.Vars) error { if r == nil { panic("core.RenderTemplate(): the *http.Request is nil!?") } - // Inject globally available variables. - vars = b.LoadDefaults(vars, r) - - // Add any flashed messages from the endpoint controllers. - session := sessions.Get(r) - if flashes := session.Flashes(); len(flashes) > 0 { - for _, flash := range flashes { - _ = flash - vars.Flashes = append(vars.Flashes, flash.(string)) - } - session.Save(r, w) - } - - vars.RequestDuration = time.Now().Sub(vars.RequestTime) - vars.CSRF = middleware.GenerateCSRFToken(w, r, session) - vars.Editable = !strings.HasPrefix(path, "admin/") - - return render.Template(w, path, render.Config{ - Request: r, - Vars: &vars, - Functions: b.TemplateFuncs(w, r, nil), - }) -} - -// TemplateFuncs returns the common template function map. -func (b *Blog) TemplateFuncs(w http.ResponseWriter, r *http.Request, inject map[string]interface{}) map[string]interface{} { - fn := map[string]interface{}{ - "RenderIndex": b.RenderIndex, - "RenderPost": b.RenderPost, - "RenderTags": b.RenderTags, - "RenderComments": func(subject string, ids ...string) template.HTML { - if w == nil || r == nil { - return template.HTML("[RenderComments Error: need both http.ResponseWriter and http.Request]") - } - - session := sessions.Get(r) - csrf := middleware.GenerateCSRFToken(w, r, session) - return b.RenderComments(session, csrf, r.URL.Path, subject, ids...) - }, - } - - if inject != nil { - for k, v := range inject { - fn[k] = v - } - } - return fn + return render.Template(w, r, path, vars) } diff --git a/root/.layout.gohtml b/root/.layout.gohtml index fa8d95c..84d1eee 100644 --- a/root/.layout.gohtml +++ b/root/.layout.gohtml @@ -81,7 +81,7 @@ {{ if and .CurrentUser.Admin .Editable }}

- Admin: [edit this page] + Admin: [edit this page]

{{ end }} diff --git a/root/blog/entry.gohtml b/root/blog/entry.gohtml index 35364ed..410ad65 100644 --- a/root/blog/entry.gohtml +++ b/root/blog/entry.gohtml @@ -18,7 +18,7 @@

Comments

{{ $idStr := printf "%d" $p.ID}} - {{ RenderComments $p.Title "post" $idStr }} + {{ RenderComments .Request $p.Title "post" $idStr }} {{ else }}
Comments are disabled on this post.