1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-18 01:57:02 +00:00

Added partial templates

mailbox/list now renders
This commit is contained in:
James Hillyerd
2012-10-21 12:42:54 -07:00
parent 9c94bb2ab1
commit 71bb52a64a
17 changed files with 238 additions and 147 deletions

View File

@@ -34,6 +34,9 @@ theme=integral
# Path to the selected themes template files # Path to the selected themes template files
template.dir=%(install.dir)s/themes/%(theme)s/templates template.dir=%(install.dir)s/themes/%(theme)s/templates
# Should we cache parsed templates (set to false during theme dev)
template.cache=false
# Path to the selected themes public (static) files # Path to the selected themes public (static) files
public.dir=%(install.dir)s/themes/%(theme)s/public public.dir=%(install.dir)s/themes/%(theme)s/public

View File

@@ -17,10 +17,11 @@ type SmtpConfig struct {
} }
type WebConfig struct { type WebConfig struct {
Ip4address net.IP Ip4address net.IP
Ip4port int Ip4port int
TemplateDir string TemplateDir string
PublicDir string TemplateCache bool
PublicDir string
} }
var smtpConfig *SmtpConfig var smtpConfig *SmtpConfig
@@ -69,6 +70,7 @@ func LoadConfig(filename string) error {
requireOption(messages, "web", "ip4.address") requireOption(messages, "web", "ip4.address")
requireOption(messages, "web", "ip4.port") requireOption(messages, "web", "ip4.port")
requireOption(messages, "web", "template.dir") requireOption(messages, "web", "template.dir")
requireOption(messages, "web", "template.cache")
requireOption(messages, "web", "public.dir") requireOption(messages, "web", "public.dir")
requireOption(messages, "datastore", "path") requireOption(messages, "datastore", "path")
if messages.Len() > 0 { if messages.Len() > 0 {
@@ -158,6 +160,13 @@ func parseWebConfig() error {
} }
webConfig.TemplateDir = str webConfig.TemplateDir = str
option = "[web]template.cache"
flag, err := Config.Bool("web", "template.cache")
if err != nil {
return fmt.Errorf("Failed to parse %v: %v", option, err)
}
webConfig.TemplateCache = flag
option = "[web]public.dir" option = "[web]public.dir"
str, err = Config.String("web", "public.dir") str, err = Config.String("web", "public.dir")
if err != nil { if err != nil {

View File

@@ -141,6 +141,7 @@ a:hover {
#content { #content {
width: 928px; width: 928px;
min-height: 300px;
margin: 0 auto; margin: 0 auto;
} }
@@ -266,7 +267,6 @@ a:hover {
#emailContent { #emailContent {
padding-bottom: 20px; padding-bottom: 20px;
min-height: 300px;
} }
#emailHeader { #emailHeader {

View File

@@ -4,42 +4,50 @@ Design by Free CSS Templates
http://www.freecsstemplates.org http://www.freecsstemplates.org
Released for free under a Creative Commons Attribution 2.5 License Released for free under a Creative Commons Attribution 2.5 License
--> -->
{{define "col1menu"}}{{/* Used inside #content div */}}
<div id="colOne">
<div id="logo">
<h1><a href="#">inbucket</a></h1>
<h2>email testing service</h2>
</div>
</div>
{{end}}
<html xmlns="http://www.w3.org/1999/xhtml"> <html xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{{template "title" .}}</title> <title>{{template "title" .}}</title>
<link href="/public/main.css" rel="stylesheet" type="text/css" /> <link href="/public/main.css" rel="stylesheet" type="text/css" />
<link rel="shortcut icon" type="image/png" href="/public/images/favicon.png"> <link rel="shortcut icon" type="image/png" href="/public/images/favicon.png">
<script src="/public/jquery-1.5.2.min.js" type="text/javascript" charset="utf-8"></script> <script src="/public/jquery-1.5.2.min.js" type="text/javascript" charset="utf-8"></script>
</head> {{template "script" .}}
<body> </head>
<div id="header"> <body>
<ul id="menu"> <div id="header">
<li><a href="/" accesskey="1" title="">Home</a></li> <ul id="menu">
<li><a href="/" accesskey="2" title="">About</a></li> <li><a href="/" accesskey="1" title="">Home</a></li>
</ul> <li><a href="/" accesskey="2" title="">About</a></li>
<form id="search" action="/mailbox" method="GET"> </ul>
<fieldset> <form id="search" action="{{reverse "MailboxIndex"}}" method="GET">
<input name="name" type="text" id="input1" /> <fieldset>
<input name="submit" type="submit" id="input2" value="Go" /> <input name="name" type="text" id="input1" />
</fieldset> <input name="submit" type="submit" id="input2" value="Go" />
</form> </fieldset>
</div> </form>
<div id="content"> </div>
{{template "content" .}} <div id="content">
</div> <div id="colOne">
<div id="footer"> {{template "menu" .}}
<p>Inbucket is an open source project hosted at </div>
<a href="http://github.com/jhillyerd/inbucket/">github</a>. <div id="colTwo">
Design by <a href="http://www.freecsstemplates.org/">FCT</a>.</p> {{with .ctx.Session.Flashes "errors"}}
</div> <div class="errors">
</body> <p>Please fix the following errors and resubmit:<p>
<ul>
{{range .}}
<li>{{.}}</li>
{{end}}
</ul>
</div>
{{end}}
{{template "content" .}}
</div>
</div>
<div id="footer">
<p>Inbucket is an open source project hosted at
<a href="http://github.com/jhillyerd/inbucket/">github</a>.
Design by <a href="http://www.freecsstemplates.org/">FCT</a>.</p>
</div>
</body>
</html> </html>

View File

@@ -1,7 +1,7 @@
{{$title := printf "Inbucket for %v" .name}} {{define "title"}}{{printf "Inbucket for %v" .name}}{{end}}
{{set "title" $title .}}
{{template "header.html" .}}
{{$name := .name}} {{$name := .name}}
{{define "script"}}
<script> <script>
function listLoaded() { function listLoaded() {
$('.listEntry').hover( $('.listEntry').hover(
@@ -15,14 +15,14 @@
function() { function() {
$('.listEntry').removeClass("listEntrySelected") $('.listEntry').removeClass("listEntrySelected")
$(this).addClass("listEntrySelected") $(this).addClass("listEntrySelected")
$('#emailContent').load('/mailbox/show/{{$name}}/' + this.id) $('#emailContent').load('/mailbox/show/{{.name}}/' + this.id)
} }
) )
$("#messageList").slideDown() $("#messageList").slideDown()
} }
function loadList() { function loadList() {
$('#messageList').load("/mailbox/list/{{$name}}", listLoaded) $('#messageList').load("/mailbox/list/{{.name}}", listLoaded)
} }
function reloadList() { function reloadList() {
@@ -39,44 +39,41 @@
$('#emailContent').empty() $('#emailContent').empty()
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '/mailbox/delete/{{$name}}/' + id, url: '/mailbox/delete/{{.name}}/' + id,
success: reloadList success: reloadList
}) })
} }
function htmlView(id) { function htmlView(id) {
window.open('/mailbox/html/{{$name}}/' + id, '_blank', window.open('/mailbox/html/{{.name}}/' + id, '_blank',
'width=800,height=600,' + 'width=800,height=600,' +
'menubar=yes,resizable=yes,scrollbars=yes,status=yes,toolbar=yes') 'menubar=yes,resizable=yes,scrollbars=yes,status=yes,toolbar=yes')
} }
function messageSource(id) { function messageSource(id) {
window.open('/mailbox/source/{{$name}}/' + id, '_blank', window.open('/mailbox/source/{{.name}}/' + id, '_blank',
'width=800,height=600,' + 'width=800,height=600,' +
'menubar=no,resizable=yes,scrollbars=yes,status=no,toolbar=no') 'menubar=no,resizable=yes,scrollbars=yes,status=no,toolbar=no')
} }
$(document).ready(listInit) $(document).ready(listInit)
</script> </script>
{{end}}
<div id="colOne"> {{define "menu"}}
<div id="logo"> <div id="logo">
<h1><a href="#">inbucket</a></h1> <h1><a href="#">inbucket</a></h1>
<h2>mail for {{.name}}</h2> <h2>mail for {{.name}}</h2>
</div>
<div class="box" style="text-align:center; padding-bottom:10px;">
<a href="javascript:reloadList()">Refresh List</a>
</div>
<div id="messageList"></div>
</div> </div>
<div class="box" style="text-align:center; padding-bottom:10px;">
<div id="colTwo"> <a href="javascript:reloadList()">Refresh List</a>
{{template "errors.html" .}}
<div id="emailContent">
<p>Select a message at left, or enter a different username into the box on upper right.</p>
</div>
</div> </div>
<div id="messageList"></div>
{{end}}
{{template "footer.html" .}} {{define "content"}}
<div id="emailContent">
<p>Select a message at left, or enter a different username into the box on upper right.</p>
</div>
{{end}}

View File

@@ -1,6 +1,4 @@
<div id="colOne"> <div id="logo">
<div id="logo"> <h1><a href="/">inbucket</a></h1>
<h1><a href="#">inbucket</a></h1> <h2>email testing service</h2>
<h2>email testing service</h2> </div>
</div>
</div>

View File

@@ -1,11 +0,0 @@
{{define "title"}}Inbucket{{end}}
{{define "content"}}
{{template "col1menu" .}}
<div id="colTwo">
<p>Inbucket is an email testing service; it will accept email for any email
address and make it available to view without a password.</p>
<p>To view email for a particular address, enter the username portion
of the address into the box on the upper right and click <em>go</em>.</p>
</div>
{{end}}

View File

@@ -0,0 +1,19 @@
{{define "title"}}Inbucket{{end}}
{{define "script"}}{{end}}
{{define "menu"}}
<div id="logo">
<h1><a href="/">inbucket</a></h1>
<h2>email testing service</h2>
</div>
{{end}}
{{define "content"}}
<p>Inbucket is an email testing service; it will accept email for any email
address and make it available to view without a password.</p>
<p>To view email for a particular address, enter the username portion
of the address into the box on the upper right and click <em>go</em>.</p>
{{end}}

View File

@@ -1,12 +1,16 @@
package web package web
import ( import (
"github.com/gorilla/mux"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/jhillyerd/inbucket"
"net/http" "net/http"
) )
type Context struct { type Context struct {
Session *sessions.Session Vars map[string]string
Session *sessions.Session
DataStore *inbucket.DataStore
} }
func (c *Context) Close() { func (c *Context) Close() {
@@ -14,9 +18,13 @@ func (c *Context) Close() {
} }
func NewContext(req *http.Request) (*Context, error) { func NewContext(req *http.Request) (*Context, error) {
vars := mux.Vars(req)
sess, err := sessionStore.Get(req, "inbucket") sess, err := sessionStore.Get(req, "inbucket")
ds := inbucket.NewDataStore()
ctx := &Context{ ctx := &Context{
Session: sess, Vars: vars,
Session: sess,
DataStore: ds,
} }
if err != nil { if err != nil {
return ctx, err return ctx, err

View File

@@ -8,21 +8,8 @@ import (
) )
var TemplateFuncs = template.FuncMap{ var TemplateFuncs = template.FuncMap{
// Reversable routing function // Reversable routing function (shared with templates)
"reverse": func(name string, things ...interface{}) string { "reverse": reverse,
// Convert the things to strings
strs := make([]string, len(things))
for i, th := range things {
strs[i] = fmt.Sprint(th)
}
// Grab the route
u, err := Router.Get(name).URL(strs...)
if err != nil {
inbucket.Error("Failed to reverse route: %v", err)
return "/ROUTE-ERROR"
}
return u.Path
},
// Friendly date & time rendering // Friendly date & time rendering
"friendlyTime": func(t time.Time) template.HTML { "friendlyTime": func(t time.Time) template.HTML {
ty, tm, td := t.Date() ty, tm, td := t.Date()
@@ -33,3 +20,18 @@ var TemplateFuncs = template.FuncMap{
return template.HTML(t.Format("Mon Jan 2, 2006")) return template.HTML(t.Format("Mon Jan 2, 2006"))
}, },
} }
func reverse(name string, things ...interface{}) string {
// Convert the things to strings
strs := make([]string, len(things))
for i, th := range things {
strs[i] = fmt.Sprint(th)
}
// Grab the route
u, err := Router.Get(name).URL(strs...)
if err != nil {
inbucket.Error("Failed to reverse route: %v", err)
return "/ROUTE-ERROR"
}
return u.Path
}

View File

@@ -1,52 +1,52 @@
package web package web
import ( import (
"github.com/jhillyerd/inbucket/app/inbucket" "github.com/jhillyerd/inbucket"
"github.com/robfig/revel" "net/http"
"html/template"
) )
type Mailbox struct { func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
*rev.Controller name := req.FormValue("name")
} if len(name) == 0 {
ctx.Session.AddFlash("Account name is required", "errors")
func (c Mailbox) Index(name string) rev.Result { http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
c.Validation.Required(name).Message("Account name is required") return nil
if c.Validation.HasErrors() {
c.Validation.Keep()
c.FlashParams()
return c.Redirect(Application.Index)
} }
return c.Render(name) return RenderTemplate("mailbox/index.html", w, map[string]interface{}{
"ctx": ctx,
"name": name,
})
} }
func (c Mailbox) List(name string) rev.Result { func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
c.Validation.Required(name).Message("Account name is required") name := ctx.Vars["name"]
if len(name) == 0 {
if c.Validation.HasErrors() { ctx.Session.AddFlash("Account name is required", "errors")
c.Validation.Keep() http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
c.FlashParams() return nil
return c.Redirect(Application.Index)
} }
ds := inbucket.NewDataStore() mb, err := ctx.DataStore.MailboxFor(name)
mb, err := ds.MailboxFor(name)
if err != nil { if err != nil {
return c.RenderError(err) return err
} }
messages, err := mb.GetMessages() messages, err := mb.GetMessages()
if err != nil { if err != nil {
return c.RenderError(err) return err
} }
rev.INFO.Printf("Got %v messsages", len(messages)) inbucket.Trace("Got %v messsages", len(messages))
c.Response.Out.Header().Set("Expires", "-1") return RenderPartial("mailbox/_list.html", w, map[string]interface{}{
return c.Render(name, messages) "ctx": ctx,
"name": name,
"messages": messages,
})
} }
/*
func (c Mailbox) Show(name string, id string) rev.Result { func (c Mailbox) Show(name string, id string) rev.Result {
func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
c.Validation.Required(name).Message("Account name is required") c.Validation.Required(name).Message("Account name is required")
c.Validation.Required(id).Message("Message ID is required") c.Validation.Required(id).Message("Message ID is required")
@@ -77,6 +77,7 @@ func (c Mailbox) Show(name string, id string) rev.Result {
} }
func (c Mailbox) Delete(name string, id string) rev.Result { func (c Mailbox) Delete(name string, id string) rev.Result {
func MailboxDelete(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
c.Validation.Required(name).Message("Account name is required") c.Validation.Required(name).Message("Account name is required")
c.Validation.Required(id).Message("Message ID is required") c.Validation.Required(id).Message("Message ID is required")
@@ -104,6 +105,7 @@ func (c Mailbox) Delete(name string, id string) rev.Result {
} }
func (c Mailbox) Html(name string, id string) rev.Result { func (c Mailbox) Html(name string, id string) rev.Result {
func MailboxHtml(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
c.Validation.Required(name).Message("Account name is required") c.Validation.Required(name).Message("Account name is required")
c.Validation.Required(id).Message("Message ID is required") c.Validation.Required(id).Message("Message ID is required")
@@ -135,6 +137,7 @@ func (c Mailbox) Html(name string, id string) rev.Result {
} }
func (c Mailbox) Source(name string, id string) rev.Result { func (c Mailbox) Source(name string, id string) rev.Result {
func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
c.Validation.Required(name).Message("Account name is required") c.Validation.Required(name).Message("Account name is required")
c.Validation.Required(id).Message("Message ID is required") c.Validation.Required(id).Message("Message ID is required")
@@ -161,3 +164,4 @@ func (c Mailbox) Source(name string, id string) rev.Result {
c.Response.Out.Header().Set("Expires", "-1") c.Response.Out.Header().Set("Expires", "-1")
return c.RenderText(*raw) return c.RenderText(*raw)
} }
*/

View File

@@ -5,5 +5,7 @@ import (
) )
func RootIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { func RootIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
return T("root-index.html").Execute(w, nil) return RenderTemplate("root/index.html", w, map[string]interface{}{
"ctx": ctx,
})
} }

View File

@@ -31,17 +31,19 @@ var Router *mux.Router
var sessionStore sessions.Store var sessionStore sessions.Store
func setupRoutes(cfg inbucket.WebConfig) { func setupRoutes(cfg inbucket.WebConfig) {
r := mux.NewRouter() Router = mux.NewRouter()
Router = r
inbucket.Info("Theme templates mapped to '%v'", cfg.TemplateDir) inbucket.Info("Theme templates mapped to '%v'", cfg.TemplateDir)
inbucket.Info("Theme static content mapped to '%v'", cfg.PublicDir) inbucket.Info("Theme static content mapped to '%v'", cfg.PublicDir)
r := Router
// Static content // Static content
r.PathPrefix("/public/").Handler(http.StripPrefix("/public/", r.PathPrefix("/public/").Handler(http.StripPrefix("/public/",
http.FileServer(http.Dir(cfg.PublicDir)))) http.FileServer(http.Dir(cfg.PublicDir))))
// Root // Root
r.Path("/").Handler(handler(RootIndex)) r.Path("/").Handler(handler(RootIndex)).Name("RootIndex").Methods("GET")
r.Path("/mailbox").Handler(handler(MailboxIndex)).Name("MailboxIndex").Methods("GET")
r.Path("/mailbox/list/{name}").Handler(handler(MailboxList)).Name("MailboxList").Methods("GET")
} }
// Start() the web server // Start() the web server

View File

@@ -3,31 +3,81 @@ package web
import ( import (
"github.com/jhillyerd/inbucket" "github.com/jhillyerd/inbucket"
"html/template" "html/template"
"net/http"
"path"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
) )
var cachedTemplates = map[string]*template.Template{}
var cachedMutex sync.Mutex var cachedMutex sync.Mutex
var cachedTemplates = map[string]*template.Template{}
var cachedPartials = map[string]*template.Template{}
func T(name string) *template.Template { // RenderTemplate fetches the named template and renders it to the provided
// ResponseWriter.
func RenderTemplate(name string, w http.ResponseWriter, data interface{}) error {
t, err := ParseTemplate(name, false)
if err != nil {
inbucket.Error("Error in template '%v': %v", name, err)
return err
}
w.Header().Set("Expires", "-1")
return t.Execute(w, data)
}
// RenderPartial fetches the named template and renders it to the provided
// ResponseWriter.
func RenderPartial(name string, w http.ResponseWriter, data interface{}) error {
t, err := ParseTemplate(name, true)
if err != nil {
inbucket.Error("Error in template '%v': %v", name, err)
return err
}
w.Header().Set("Expires", "-1")
return t.Execute(w, data)
}
// ParseTemplate loads the requested template along with _base.html, caching
// the result (if configured to do so)
func ParseTemplate(name string, partial bool) (*template.Template, error) {
cachedMutex.Lock() cachedMutex.Lock()
defer cachedMutex.Unlock() defer cachedMutex.Unlock()
if t, ok := cachedTemplates[name]; ok { if t, ok := cachedTemplates[name]; ok {
return t return t, nil
} }
templateDir := inbucket.GetWebConfig().TemplateDir cfg := inbucket.GetWebConfig()
templateFile := filepath.Join(templateDir, name) tempPath := strings.Replace(name, "/", string(filepath.Separator), -1)
inbucket.Trace("Parsing template %v", templateFile) tempFile := filepath.Join(cfg.TemplateDir, tempPath)
inbucket.Trace("Parsing template %v", tempFile)
t := template.New("_base.html").Funcs(TemplateFuncs) var err error
t = template.Must(t.ParseFiles( var t *template.Template
filepath.Join(templateDir, "_base.html"), if partial {
templateFile, // Need to get basename of file to make it root template w/ funcs
)) base := path.Base(name)
cachedTemplates[name] = t t = template.New(base).Funcs(TemplateFuncs)
t, err = t.ParseFiles(tempFile)
} else {
t = template.New("_base.html").Funcs(TemplateFuncs)
t, err = t.ParseFiles(filepath.Join(cfg.TemplateDir, "_base.html"), tempFile)
}
if err != nil {
return nil, err
}
return t // Allows us to disable caching for theme development
if cfg.TemplateCache {
if partial {
inbucket.Trace("Caching partial %v", name)
cachedTemplates[name] = t
} else {
inbucket.Trace("Caching template %v", name)
cachedTemplates[name] = t
}
}
return t, nil
} }