footer: Add a semi-flexible template system

This commit is contained in:
Tom Wiesing 2023-01-06 22:52:47 +01:00
parent bda763725e
commit 021fc3cc7e
No known key found for this signature in database
26 changed files with 239 additions and 208 deletions

View file

@ -1,3 +1,20 @@
{{ template "_form.html" . }}
{{ define "form/title" }}Login Required{{ end }}
{{ define "form/button" }}Login{{ end }}
{{ define "form/inside" }}
<div class="pure-form-group">
<p>
The site you are attempting to access requires you to login with your account.
Simply fill out the form below and click <em>Login</em>.
</p>
<p>
<small>
This functionality requires you to have cookies enabled.
A cookie to prevent fraudulent login attempts has already been set on your machine.
When clicking the <em>Login</em> button, a further cookie will be set on your computer to keep track of your login session.
If you do not wish to have futher cookie(s) set, do not click Login and do not attempt to access protected sites.
See also <a href="/legal/#cookies">Legal Notices</a>.
</small>
</p>
</div>
{{ end }}

View file

@ -79,7 +79,7 @@ func (panel *UserPanel) UserFormContext(ctx httpx.FormContext, r *http.Request)
user, err := panel.Dependencies.Auth.UserOf(r)
uctx := userFormContext{FormContext: ctx}
panel.Dependencies.Custom.Update(&uctx)
panel.Dependencies.Custom.Update(&uctx, r)
if err == nil {
uctx.User = &user.User
}

View file

@ -98,7 +98,7 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
FormContext: context,
},
}
panel.Dependencies.Custom.Update(&ctx.userFormContext)
panel.Dependencies.Custom.Update(&ctx.userFormContext, r)
if err == nil && user != nil {
ctx.userFormContext.User = &user.User

View file

@ -28,9 +28,9 @@ func (panel *UserPanel) routeUser(ctx context.Context) http.Handler {
userTemplate := panel.Dependencies.Custom.Template(userTemplate)
return &httpx.HTMLHandler[routeUserContext]{
Handler: func(r *http.Request) (ruc routeUserContext, err error) {
panel.Dependencies.Custom.Update(&ruc)
panel.Dependencies.Custom.Update(&ruc, r)
ruc.AuthUser, err = panel.Dependencies.Auth.UserOf(r)
return routeUserContext{}, err
return ruc, err
},
Template: userTemplate,
}

View file

@ -125,7 +125,7 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler {
if context.Err != nil {
context.Err = errLoginFailed
}
httpx.WriteHTML(auth.Dependencies.Custom.NewForm(context), nil, loginTemplate, "", w, r)
httpx.WriteHTML(auth.Dependencies.Custom.NewForm(context, r), nil, loginTemplate, "", w, r)
},
Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) {

View file

@ -28,7 +28,7 @@ type componentContext struct {
}
func (admin *Admin) components(r *http.Request) (cp componentContext, err error) {
admin.Dependencies.Custom.Update(&cp)
admin.Dependencies.Custom.Update(&cp, r)
cp.Analytics = *admin.Analytics
return
@ -49,7 +49,7 @@ type ingredientsContext struct {
}
func (admin *Admin) ingredients(r *http.Request) (cp ingredientsContext, err error) {
admin.Dependencies.Custom.Update(&cp)
admin.Dependencies.Custom.Update(&cp, r)
// find the instance itself!
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), mux.Vars(r)["slug"])

View file

@ -87,7 +87,7 @@ type indexContext struct {
}
func (admin *Admin) index(r *http.Request) (idx indexContext, err error) {
admin.Dependencies.Custom.Update(&idx)
admin.Dependencies.Custom.Update(&idx, r)
idx.Distillery, idx.Instances, err = admin.Status(r.Context(), true)
return
}

View file

@ -2,7 +2,6 @@ package admin
import (
_ "embed"
"html/template"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
@ -25,13 +24,12 @@ var instanceTemplate = static.AssetsAdmin.MustParseShared(
type instanceContext struct {
custom.BaseContext
CSRF template.HTML
Instance models.Instance
Info status.WissKI
}
func (admin *Admin) instance(r *http.Request) (is instanceContext, err error) {
admin.Dependencies.Custom.Update(&is)
admin.Dependencies.Custom.Update(&is, r)
is.CSRF = csrf.TemplateField(r)

View file

@ -3,7 +3,6 @@ package admin
import (
"context"
"errors"
"html/template"
"net/http"
_ "embed"
@ -27,12 +26,11 @@ type userContext struct {
custom.BaseContext
httpx.FormContext
CSRF template.HTML
Users []*auth.AuthUser
}
func (admin *Admin) users(r *http.Request) (uc userContext, err error) {
admin.Dependencies.Custom.Update(&uc)
admin.Dependencies.Custom.Update(&uc, r)
uc.CSRF = csrf.TemplateField(r)
uc.Users, err = admin.Dependencies.Auth.Users(r.Context())

View file

@ -1,9 +1,6 @@
{{ template "_base.html" . }}
{{ define "title" }}WissKI Distillery{{ end }}
{{ define "header/time" }}
<!-- no header/time -->
{{ end }}
{{ define "header"}}
<!-- no header -->
{{ end }}

View file

@ -33,7 +33,7 @@ var homeTemplate = static.AssetsHome.MustParseShared("home.html", homeHTMLStr)
func (home *Home) homeRender(ctx context.Context) ([]byte, error) {
var context homeContext
home.Dependencies.Custom.Update(&context)
home.Dependencies.Custom.Update(&context, nil)
// setup a couple of static things
context.Time = time.Now().UTC()
@ -60,10 +60,10 @@ func (home *Home) homeRender(ctx context.Context) ([]byte, error) {
// render the template
var buffer bytes.Buffer
home.homeTemplate.Get(func() *template.Template {
err = home.homeTemplate.Get(func() *template.Template {
return home.Dependencies.Custom.Template(homeTemplate)
}).Execute(&buffer, context)
return buffer.Bytes(), nil
return buffer.Bytes(), err
}
type homeContext struct {

View file

@ -56,13 +56,13 @@ type legalContext struct {
AssetsDisclaimer string
}
func (legal *Legal) context(r *http.Request) (legalContext, error) {
return legalContext{
BaseContext: legal.Dependencies.Custom.New(),
LegalNotices: cli.LegalNotices,
func (legal *Legal) context(r *http.Request) (lc legalContext, err error) {
legal.Dependencies.Custom.Update(&lc, r)
CSRFCookie: control.CSRFCookie,
SessionCookie: control.SessionCookie,
AssetsDisclaimer: static.AssetsDisclaimer,
}, nil
lc.LegalNotices = cli.LegalNotices
lc.CSRFCookie = control.CSRFCookie
lc.SessionCookie = control.SessionCookie
lc.AssetsDisclaimer = static.AssetsDisclaimer
return
}

View file

@ -1,9 +1,6 @@
{{ template "_base.html" . }}
{{ define "title" }}Legal{{ end }}
{{ define "header/time" }}
<!-- no header/time -->
{{ end }}
{{ define "header"}}
<!-- no header -->
{{ end }}

View file

@ -10,23 +10,23 @@ var AssetsDisclaimer string
// AssetsHome contains assets for the 'Home' entrypoint.
var AssetsHome = Assets{
Scripts: `<script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Home.9f00501f.css"><link rel="stylesheet" href="/static/Home.2353e048.css">`,
Styles: `<link rel="stylesheet" href="/static/Home.4b303448.css"><link rel="stylesheet" href="/static/Home.2353e048.css">`,
}
// AssetsUser contains assets for the 'User' entrypoint.
var AssetsUser = Assets{
Scripts: `<script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/User.4197014b.js"></script><script src="/static/User.30d54198.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Home.9f00501f.css"><link rel="stylesheet" href="/static/User.38d394c2.css">`,
Styles: `<link rel="stylesheet" href="/static/Home.4b303448.css"><link rel="stylesheet" href="/static/User.38d394c2.css">`,
}
// AssetsAdmin contains assets for the 'Admin' entrypoint.
var AssetsAdmin = Assets{
Scripts: `<script nomodule="" defer src="/static/User.30d54198.js"></script><script type="module" src="/static/User.4197014b.js"></script><script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Admin.4ca3cb6f.js"></script><script src="/static/Admin.9750ba9c.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Home.9f00501f.css"><link rel="stylesheet" href="/static/Admin.6d59e220.css"><link rel="stylesheet" href="/static/User.38d394c2.css"><link rel="stylesheet" href="/static/Admin.6d2ae968.css">`,
Styles: `<link rel="stylesheet" href="/static/Home.4b303448.css"><link rel="stylesheet" href="/static/Admin.6d59e220.css"><link rel="stylesheet" href="/static/User.38d394c2.css"><link rel="stylesheet" href="/static/Admin.6d2ae968.css">`,
}
// AssetsLegal contains assets for the 'Legal' entrypoint.
var AssetsLegal = Assets{
Scripts: `<script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Legal.38d394c2.js"></script><script src="/static/Legal.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Home.9f00501f.css"><link rel="stylesheet" href="/static/Legal.d1531eba.css"><link rel="stylesheet" href="/static/Legal.20259812.css">`,
Styles: `<link rel="stylesheet" href="/static/Home.4b303448.css"><link rel="stylesheet" href="/static/Legal.d1531eba.css"><link rel="stylesheet" href="/static/Legal.20259812.css">`,
}

View file

@ -0,0 +1,90 @@
package custom
import (
"html/template"
"net/http"
"reflect"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/gorilla/csrf"
"github.com/tkw1536/goprogram/lib/reflectx"
)
// baseContextName is the name of the [BaseContext] type
var baseContextName = reflectx.TypeOf[BaseContext]().Name()
// BaseContext represents a context shared by all the templates.
//
// This context should always be initialized using the [custom.Update], [custom.New] or [custom.NewForm] functions.
// Other invocations might cause an error at runtime.
type BaseContext struct {
inited bool // has this context been inited
GeneratedAt time.Time // time this page was generated at
CSRF template.HTML // CSRF Field
}
// constants that are used in various parts of the template to render stuff
const (
errorPrefix template.HTML = `<div style="z-index:10000;position:fixed;top:0;left:0;width:100vh;height:100vw;background:red;text-align:center;padding:10vh 10vw;font-size:xx-large;font-weight:bold">`
errorSuffix template.HTML = "</div>"
csrfError template.HTML = errorPrefix + "CSRF used but not provided" + errorSuffix
initError template.HTML = errorPrefix + "BaseContext not initialized" + errorSuffix
)
// Use updates this context to use the values from the given base.
// For convenience the passed context is also returned.
func (tc *BaseContext) use(base component.Base, r *http.Request) *BaseContext {
tc.inited = true
tc.GeneratedAt = time.Now().UTC()
// setup the CSRF field
tc.CSRF = csrfError
if r != nil {
tc.CSRF = csrf.TemplateField(r)
}
return tc
}
func (bc BaseContext) DoInitCheck() template.HTML {
if !bc.inited {
return initError
}
return ""
}
// NewForm is like New, but returns a new BaseFormContext
func (custom *Custom) NewForm(context httpx.FormContext, r *http.Request) (ctx BaseFormContext) {
ctx.FormContext = context
ctx.use(custom.Base, r)
return
}
// RenderContext is exactly like NewForm, but returns any to be used as httpx.Form.RenderTemplateContext
func (custom *Custom) RenderContext(ctx httpx.FormContext, r *http.Request) any {
return custom.NewForm(ctx, r)
}
// Update updates an embedded BaseContext field in context.
//
// Assumes that context is a pointer to a struct type.
// If this is not the case, might call panic().
func (custom *Custom) Update(context any, r *http.Request) *BaseContext {
ctx := reflect.ValueOf(context).
Elem().FieldByName(baseContextName).Addr().
Interface().(*BaseContext)
ctx.use(custom.Base, r)
return ctx
}
// BaseFormContext combines BaseContext and FormContext
type BaseFormContext struct {
BaseContext
httpx.FormContext
}

View file

@ -5,4 +5,7 @@ import "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
// Custom implements theme and page customization.
type Custom struct {
component.Base
Dependencies struct {
// nothing yet
}
}

View file

@ -0,0 +1,3 @@
<p>
Generated <time format="{{ .GeneratedAt.Format "2006-01-02T15:04:05Z" }}">{{ .GeneratedAt.Format "2006-01-02T15:04:05Z07:00" }}</time> by <a href="https://github.com/FAU-CDI/wisski-distillery" target="_blank" rel="noopener noreferer">WissKI Distillery</a>. This site might use cookies for essential purposes, see also <a href="/legal/">Legal Notices</a>.
</p>

View file

@ -1,20 +1,15 @@
package custom
import (
_ "embed"
"html/template"
"net/http"
"reflect"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/tkw1536/goprogram/lib/reflectx"
)
const footerName = "footer"
// defaultTemplate is the default footer template
var defaultTemplate = template.Must(template.New("footer.html").Parse(`<p>Powered By WissKI Distillery</p>`))
//go:embed "footer.html"
var defaultTemplateStr string
var defaultTemplate = template.Must(template.New("footer.html").Parse(defaultTemplateStr))
// Template creates a copy of template with shared template parts updated accordingly.
// Any template using this should use one of the template contexts in this package.
@ -25,58 +20,3 @@ func (custom *Custom) Template(tpl *template.Template) *template.Template {
template.Must(clone.AddParseTree(footerName, tree)) // add the parse tree to it
return clone // and return the tree
}
// NewContext returns a new BaseContext
func (custom *Custom) New() (ctx BaseContext) {
ctx.Use(custom.Base)
return
}
// NewForm is like New, but returns a new BaseFormContext
func (custom *Custom) NewForm(context httpx.FormContext) (ctx BaseFormContext) {
ctx.FormContext = context
ctx.Use(custom.Base)
return
}
// RenderContext can be used as httpx.Form.RenderTemplateContext.
// It returns a new [BaseFormContext].
func (custom *Custom) RenderContext(ctx httpx.FormContext, r *http.Request) any {
return BaseFormContext{
FormContext: ctx,
BaseContext: custom.New(),
}
}
// Update updates an embedded BaseContext field in context.
//
// Assumes that context is a pointer to a struct type.
// If this is not the case, might call panic().
func (custom *Custom) Update(context any) *BaseContext {
ctx := reflect.ValueOf(context).
Elem().FieldByName(contextName).Addr().
Interface().(*BaseContext)
ctx.Use(custom.Base)
return ctx
}
// contextName is the name of the [BaseContext] field.
var contextName = reflectx.TypeOf[BaseContext]().Name()
// BaseContext is a context struct shared by all contexts
type BaseContext struct {
Time time.Time // time this page was generated at
}
// Use updates this context to use the values from the given base.
// For convenience the passed context is also returned.
func (tc *BaseContext) Use(base component.Base) *BaseContext {
tc.Time = time.Now().UTC()
return tc
}
// BaseFormContext combines BaseContext and FormContext
type BaseFormContext struct {
BaseContext
httpx.FormContext
}

View file

@ -8,6 +8,20 @@ footer {
margin: 2em;
}
footer {
font-size: small;
border-top: 1px solid black;
padding-top: 1em;
}
footer a {
color: blue !important;
}
time {
font-style: italic;
}
.padding {
padding: 1em;
}

View file

@ -9,14 +9,10 @@
</head>
<body>
{{ .BaseContext.DoInitCheck }}
<header>
<h1 id="top">{{ template "title" . }}</h1>
{{ block "header/time" . }}
{{ if .Time }}
<small>Generated at <code class="date">{{ .Time.Format "2006-01-02T15:04:05Z07:00" }}</code></small>
{{ end }}
{{ end }}
{{ block "header" . }}header{{ end }}
{{ block "header" . }}<!-- no header by default -->{{ end }}
</header>
<main>
<div class="pure-g">
@ -25,11 +21,8 @@
</main>
<footer>
{{ if .Time }}
Generated at <code>{{ .Time }}</code>
{{ end }}
{{ block "footer" . }}
<!-- no footer by default -->
{{ block "footer" .BaseContext }}
<!-- no footer -->
{{ end }}
</footer>

View file

@ -2,7 +2,6 @@
{{ define "title" }}{{ block "form/title" . }}Form{{ end }}{{ end }}
{{ define "header" }}<!-- no header -->{{ end }}
{{ define "header/time" }}<!-- no time -->{{ end }}
{{ define "content" }}
<div class="pure-u-1">
{{ block "form/extra" . }}<!-- no extra -->{{ end }}
@ -24,7 +23,7 @@
{{ block "form/inside" . }}<!-- no inside -->{{ end }}
{{ .Form }}
<input type="submit" value="{{ block " form/button" .}}Submit{{ end }}" class="pure-button">
<input type="submit" value="{{ block "form/button" .}}Submit{{ end }}" class="pure-button">
</fieldset>
</form>
</div>