27540534ad
Allow regular (non-admin) users access to the Manage Forums page so they can create and manage their own forums. Things that were already working: * The admin forum page was already anticipating regular LoginRequired credential * Users only see their owned forums, while admins can see and manage ALL forums Improvements made to the Forum Admin page: * Change the title color from admin-red to user-blue. * Add ability to search (filter) and sort the forums. Other changes: * Turn the Forum tab bar into a reusable component.
202 lines
5.3 KiB
Go
202 lines
5.3 KiB
Go
package templates
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"code.nonshy.com/nonshy/website/pkg/config"
|
|
"code.nonshy.com/nonshy/website/pkg/log"
|
|
"code.nonshy.com/nonshy/website/pkg/session"
|
|
)
|
|
|
|
// Template is a logical HTML template for the app with ability to wrap around an html/template
|
|
// and provide middlewares, hooks or live reloading capability in debug mode.
|
|
type Template struct {
|
|
filename string // Filename on disk (index.html)
|
|
filepath string // Full path on disk (./web/templates/index.html)
|
|
modified time.Time // Modification date of the file at init time
|
|
tmpl *template.Template
|
|
}
|
|
|
|
// LoadTemplate processes and returns a template. Filename is relative
|
|
// to the template directory, e.g. "index.html". Call this at the initialization
|
|
// of your endpoint controller; in debug mode the template HTML from disk may be
|
|
// reloaded if modified after initial load.
|
|
func LoadTemplate(filename string) (*Template, error) {
|
|
filepath := config.TemplatePath + "/" + filename
|
|
stat, err := os.Stat(filepath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("LoadTemplate(%s): %s", filename, err)
|
|
}
|
|
|
|
files := templates(config.TemplatePath + "/" + filename)
|
|
tmpl := template.New("page")
|
|
tmpl.Funcs(TemplateFuncs(nil))
|
|
tmpl.ParseFiles(files...)
|
|
|
|
return &Template{
|
|
filename: filename,
|
|
filepath: filepath,
|
|
modified: stat.ModTime(),
|
|
tmpl: tmpl,
|
|
}, nil
|
|
}
|
|
|
|
// LoadCustom loads a bare template without the site theme and partial templates attached.
|
|
//
|
|
// The custom TempleFuncs and vars are still available (PrettyTitle, .CurrentUser, etc.)
|
|
func LoadCustom(filename string) (*Template, error) {
|
|
filepath := config.TemplatePath + "/" + filename
|
|
stat, err := os.Stat(filepath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("LoadTemplate(%s): %s", filename, err)
|
|
}
|
|
|
|
tmpl := template.New("page")
|
|
tmpl.Funcs(TemplateFuncs(nil))
|
|
tmpl.ParseFiles(filepath)
|
|
|
|
return &Template{
|
|
filename: filename,
|
|
filepath: filepath,
|
|
modified: stat.ModTime(),
|
|
tmpl: tmpl,
|
|
}, nil
|
|
}
|
|
|
|
// Must LoadTemplate or panic.
|
|
func Must(filename string) *Template {
|
|
tmpl, err := LoadTemplate(filename)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return tmpl
|
|
}
|
|
|
|
// Must LoadCustom or panic.
|
|
func MustLoadCustom(filename string) *Template {
|
|
tmpl, err := LoadCustom(filename)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return tmpl
|
|
}
|
|
|
|
// Execute a loaded template. In debug mode, the template file may be reloaded
|
|
// from disk if the file on disk has been modified.
|
|
func (t *Template) Execute(w http.ResponseWriter, r *http.Request, vars map[string]interface{}) error {
|
|
if vars == nil {
|
|
vars = map[string]interface{}{}
|
|
}
|
|
|
|
// Merge in global variables.
|
|
MergeVars(r, vars)
|
|
MergeUserVars(r, vars)
|
|
|
|
// Merge the flashed messsage variables in.
|
|
if r != nil {
|
|
sess := session.Get(r)
|
|
flashes, errors := sess.ReadFlashes(w)
|
|
vars["Flashes"] = flashes
|
|
vars["Errors"] = errors
|
|
}
|
|
|
|
// Reload the template from disk?
|
|
if t.IsModifiedLocally() {
|
|
if err := t.Reload(); err != nil {
|
|
log.Error("Reloading error: %s", err)
|
|
}
|
|
}
|
|
|
|
// Install the function map.
|
|
tmpl := t.tmpl
|
|
if r != nil {
|
|
tmpl = t.tmpl.Funcs(TemplateFuncs(r))
|
|
}
|
|
|
|
if err := tmpl.ExecuteTemplate(w, "base", vars); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsModifiedLocally checks if any of the template partials of your Template have
|
|
// had their files locally on disk modified, so to know to reload them.
|
|
func (t *Template) IsModifiedLocally() bool {
|
|
// Check all the template files from base.html, to partials, to our filepath.
|
|
var files = templates(t.filepath)
|
|
for _, filename := range files {
|
|
if stat, err := os.Stat(filename); err == nil {
|
|
if stat.ModTime().After(t.modified) {
|
|
log.Info("Template(%s).Execute: file %s updated on disk, reloading", t.filename, filename)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Reload the template from disk.
|
|
func (t *Template) Reload() error {
|
|
stat, err := os.Stat(t.filepath)
|
|
if err != nil {
|
|
return fmt.Errorf("Reload(%s): %s", t.filename, err)
|
|
}
|
|
|
|
files := templates(t.filepath)
|
|
tmpl := template.New("page")
|
|
tmpl.Funcs(TemplateFuncs(nil))
|
|
tmpl.ParseFiles(files...)
|
|
|
|
t.tmpl = tmpl
|
|
t.modified = stat.ModTime()
|
|
return nil
|
|
}
|
|
|
|
// Base template layout.
|
|
var baseTemplates = []string{
|
|
config.TemplatePath + "/base.html",
|
|
config.TemplatePath + "/partials/user_avatar.html",
|
|
config.TemplatePath + "/partials/like_modal.html",
|
|
config.TemplatePath + "/partials/right_click.html",
|
|
config.TemplatePath + "/partials/mark_explicit.html",
|
|
config.TemplatePath + "/partials/themes.html",
|
|
config.TemplatePath + "/partials/forum_tabs.html",
|
|
}
|
|
|
|
// templates returns a template chain with the base templates preceding yours.
|
|
// Files given are expected to be full paths (config.TemplatePath + file)
|
|
func templates(files ...string) []string {
|
|
return append(baseTemplates, files...)
|
|
}
|
|
|
|
// RenderTemplate executes a template. Filename is relative to the templates
|
|
// root, e.g. "index.html"
|
|
func RenderTemplate(w io.Writer, r *http.Request, filename string, vars map[string]interface{}) error {
|
|
if vars == nil {
|
|
vars = map[string]interface{}{}
|
|
}
|
|
|
|
// Merge in user vars.
|
|
MergeVars(r, vars)
|
|
MergeUserVars(r, vars)
|
|
|
|
files := templates(config.TemplatePath + "/" + filename)
|
|
tmpl := template.Must(
|
|
template.New("index").ParseFiles(files...),
|
|
)
|
|
|
|
err := tmpl.ExecuteTemplate(w, "base", vars)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|