website/pkg/templates/templates.go
Noah Petherbridge 27540534ad Forum Admin Page for Regular Users
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.
2024-08-20 19:31:56 -07:00

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
}