Move controller to the template renderer (#68)
This commit is contained in:
parent
6a7070a729
commit
5531e0bec2
26 changed files with 654 additions and 679 deletions
162
pkg/page/page.go
Normal file
162
pkg/page/page.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mikestefanello/pagoda/ent"
|
||||
"github.com/mikestefanello/pagoda/pkg/context"
|
||||
"github.com/mikestefanello/pagoda/pkg/htmx"
|
||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||
"github.com/mikestefanello/pagoda/templates"
|
||||
|
||||
echomw "github.com/labstack/echo/v4/middleware"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// Page consists of all data that will be used to render a page response for a given route.
|
||||
// While it's not required for a handler to render a Page on a route, this is the common data
|
||||
// object that will be passed to the templates, making it easy for all handlers to share
|
||||
// functionality both on the back and frontend. The Page can be expanded to include anything else
|
||||
// your app wants to support.
|
||||
// Methods on this page also then become available in the templates, which can be more useful than
|
||||
// the funcmap if your methods require data stored in the page, such as the context.
|
||||
type Page struct {
|
||||
// AppName stores the name of the application.
|
||||
// If omitted, the configuration value will be used.
|
||||
AppName string
|
||||
|
||||
// Title stores the title of the page
|
||||
Title string
|
||||
|
||||
// Context stores the request context
|
||||
Context echo.Context
|
||||
|
||||
// Path stores the path of the current request
|
||||
Path string
|
||||
|
||||
// URL stores the URL of the current request
|
||||
URL string
|
||||
|
||||
// Data stores whatever additional data that needs to be passed to the templates.
|
||||
// This is what the handler uses to pass the content of the page.
|
||||
Data any
|
||||
|
||||
// Form stores a struct that represents a form on the page.
|
||||
// This should be a struct with fields for each form field, using both "form" and "validate" tags
|
||||
// It should also contain form.FormSubmission if you wish to have validation
|
||||
// messages and markup presented to the user
|
||||
Form any
|
||||
|
||||
// Layout stores the name of the layout base template file which will be used when the page is rendered.
|
||||
// This should match a template file located within the layouts directory inside the templates directory.
|
||||
// The template extension should not be included in this value.
|
||||
Layout templates.Layout
|
||||
|
||||
// Name stores the name of the page as well as the name of the template file which will be used to render
|
||||
// the content portion of the layout template.
|
||||
// This should match a template file located within the pages directory inside the templates directory.
|
||||
// The template extension should not be included in this value.
|
||||
Name templates.Page
|
||||
|
||||
// IsHome stores whether the requested page is the home page or not
|
||||
IsHome bool
|
||||
|
||||
// IsAuth stores whether the user is authenticated
|
||||
IsAuth bool
|
||||
|
||||
// AuthUser stores the authenticated user
|
||||
AuthUser *ent.User
|
||||
|
||||
// StatusCode stores the HTTP status code that will be returned
|
||||
StatusCode int
|
||||
|
||||
// Metatags stores metatag values
|
||||
Metatags struct {
|
||||
// Description stores the description metatag value
|
||||
Description string
|
||||
|
||||
// Keywords stores the keywords metatag values
|
||||
Keywords []string
|
||||
}
|
||||
|
||||
// Pager stores a pager which can be used to page lists of results
|
||||
Pager Pager
|
||||
|
||||
// CSRF stores the CSRF token for the given request.
|
||||
// This will only be populated if the CSRF middleware is in effect for the given request.
|
||||
// If this is populated, all forms must include this value otherwise the requests will be rejected.
|
||||
CSRF string
|
||||
|
||||
// Headers stores a list of HTTP headers and values to be set on the response
|
||||
Headers map[string]string
|
||||
|
||||
// RequestID stores the ID of the given request.
|
||||
// This will only be populated if the request ID middleware is in effect for the given request.
|
||||
RequestID string
|
||||
|
||||
// HTMX provides the ability to interact with the HTMX library
|
||||
HTMX struct {
|
||||
// Request contains the information provided by HTMX about the current request
|
||||
Request htmx.Request
|
||||
|
||||
// Response contains values to pass back to HTMX
|
||||
Response *htmx.Response
|
||||
}
|
||||
|
||||
// Cache stores values for caching the response of this page
|
||||
Cache struct {
|
||||
// Enabled dictates if the response of this page should be cached.
|
||||
// Cached responses are served via middleware.
|
||||
Enabled bool
|
||||
|
||||
// Expiration stores the amount of time that the cache entry should live for before expiring.
|
||||
// If omitted, the configuration value will be used.
|
||||
Expiration time.Duration
|
||||
|
||||
// Tags stores a list of tags to apply to the cache entry.
|
||||
// These are useful when invalidating cache for dynamic events such as entity operations.
|
||||
Tags []string
|
||||
}
|
||||
}
|
||||
|
||||
// New creates and initiatizes a new Page for a given request context
|
||||
func New(ctx echo.Context) Page {
|
||||
p := Page{
|
||||
Context: ctx,
|
||||
Path: ctx.Request().URL.Path,
|
||||
URL: ctx.Request().URL.String(),
|
||||
StatusCode: http.StatusOK,
|
||||
Pager: NewPager(ctx, DefaultItemsPerPage),
|
||||
Headers: make(map[string]string),
|
||||
RequestID: ctx.Response().Header().Get(echo.HeaderXRequestID),
|
||||
}
|
||||
|
||||
p.IsHome = p.Path == "/"
|
||||
|
||||
if csrf := ctx.Get(echomw.DefaultCSRFConfig.ContextKey); csrf != nil {
|
||||
p.CSRF = csrf.(string)
|
||||
}
|
||||
|
||||
if u := ctx.Get(context.AuthenticatedUserKey); u != nil {
|
||||
p.IsAuth = true
|
||||
p.AuthUser = u.(*ent.User)
|
||||
}
|
||||
|
||||
p.HTMX.Request = htmx.GetRequest(ctx)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// GetMessages gets all flash messages for a given type.
|
||||
// This allows for easy access to flash messages from the templates.
|
||||
func (p Page) GetMessages(typ msg.Type) []template.HTML {
|
||||
strs := msg.Get(p.Context, typ)
|
||||
ret := make([]template.HTML, len(strs))
|
||||
for k, v := range strs {
|
||||
ret[k] = template.HTML(v)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
77
pkg/page/page_test.go
Normal file
77
pkg/page/page_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mikestefanello/pagoda/ent"
|
||||
"github.com/mikestefanello/pagoda/pkg/context"
|
||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||
"github.com/mikestefanello/pagoda/pkg/tests"
|
||||
|
||||
echomw "github.com/labstack/echo/v4/middleware"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
e := echo.New()
|
||||
ctx, _ := tests.NewContext(e, "/")
|
||||
p := New(ctx)
|
||||
assert.Same(t, ctx, p.Context)
|
||||
assert.Equal(t, "/", p.Path)
|
||||
assert.Equal(t, "/", p.URL)
|
||||
assert.Equal(t, http.StatusOK, p.StatusCode)
|
||||
assert.Equal(t, NewPager(ctx, DefaultItemsPerPage), p.Pager)
|
||||
assert.Empty(t, p.Headers)
|
||||
assert.True(t, p.IsHome)
|
||||
assert.False(t, p.IsAuth)
|
||||
assert.Empty(t, p.CSRF)
|
||||
assert.Empty(t, p.RequestID)
|
||||
assert.False(t, p.Cache.Enabled)
|
||||
|
||||
ctx, _ = tests.NewContext(e, "/abc?def=123")
|
||||
usr := &ent.User{
|
||||
ID: 1,
|
||||
}
|
||||
ctx.Set(context.AuthenticatedUserKey, usr)
|
||||
ctx.Set(echomw.DefaultCSRFConfig.ContextKey, "csrf")
|
||||
p = New(ctx)
|
||||
assert.Equal(t, "/abc", p.Path)
|
||||
assert.Equal(t, "/abc?def=123", p.URL)
|
||||
assert.False(t, p.IsHome)
|
||||
assert.True(t, p.IsAuth)
|
||||
assert.Equal(t, usr, p.AuthUser)
|
||||
assert.Equal(t, "csrf", p.CSRF)
|
||||
}
|
||||
|
||||
func TestPage_GetMessages(t *testing.T) {
|
||||
ctx, _ := tests.NewContext(echo.New(), "/")
|
||||
tests.InitSession(ctx)
|
||||
p := New(ctx)
|
||||
|
||||
// Set messages
|
||||
msgTests := make(map[msg.Type][]string)
|
||||
msgTests[msg.TypeWarning] = []string{
|
||||
"abc",
|
||||
"def",
|
||||
}
|
||||
msgTests[msg.TypeInfo] = []string{
|
||||
"123",
|
||||
"456",
|
||||
}
|
||||
for typ, values := range msgTests {
|
||||
for _, value := range values {
|
||||
msg.Set(ctx, typ, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the messages
|
||||
for typ, values := range msgTests {
|
||||
msgs := p.GetMessages(typ)
|
||||
|
||||
for i, message := range msgs {
|
||||
assert.Equal(t, values[i], string(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
80
pkg/page/pager.go
Normal file
80
pkg/page/pager.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultItemsPerPage stores the default amount of items per page
|
||||
DefaultItemsPerPage = 20
|
||||
|
||||
// PageQueryKey stores the query key used to indicate the current page
|
||||
PageQueryKey = "page"
|
||||
)
|
||||
|
||||
// Pager provides a mechanism to allow a user to page results via a query parameter
|
||||
type Pager struct {
|
||||
// Items stores the total amount of items in the result set
|
||||
Items int
|
||||
|
||||
// Page stores the current page number
|
||||
Page int
|
||||
|
||||
// ItemsPerPage stores the amount of items to display per page
|
||||
ItemsPerPage int
|
||||
|
||||
// Pages stores the total amount of pages in the result set
|
||||
Pages int
|
||||
}
|
||||
|
||||
// NewPager creates a new Pager
|
||||
func NewPager(ctx echo.Context, itemsPerPage int) Pager {
|
||||
p := Pager{
|
||||
ItemsPerPage: itemsPerPage,
|
||||
Page: 1,
|
||||
}
|
||||
|
||||
if page := ctx.QueryParam(PageQueryKey); page != "" {
|
||||
if pageInt, err := strconv.Atoi(page); err == nil {
|
||||
if pageInt > 0 {
|
||||
p.Page = pageInt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// SetItems sets the amount of items in total for the pager and calculate the amount
|
||||
// of total pages based off on the item per page.
|
||||
// This should be used rather than setting either items or pages directly.
|
||||
func (p *Pager) SetItems(items int) {
|
||||
p.Items = items
|
||||
p.Pages = int(math.Ceil(float64(items) / float64(p.ItemsPerPage)))
|
||||
|
||||
if p.Page > p.Pages {
|
||||
p.Page = p.Pages
|
||||
}
|
||||
}
|
||||
|
||||
// IsBeginning determines if the pager is at the beginning of the pages
|
||||
func (p Pager) IsBeginning() bool {
|
||||
return p.Page == 1
|
||||
}
|
||||
|
||||
// IsEnd determines if the pager is at the end of the pages
|
||||
func (p Pager) IsEnd() bool {
|
||||
return p.Page >= p.Pages
|
||||
}
|
||||
|
||||
// GetOffset determines the offset of the results in order to get the items for
|
||||
// the current page
|
||||
func (p Pager) GetOffset() int {
|
||||
if p.Page == 0 {
|
||||
p.Page = 1
|
||||
}
|
||||
return (p.Page - 1) * p.ItemsPerPage
|
||||
}
|
||||
69
pkg/page/pager_test.go
Normal file
69
pkg/page/pager_test.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mikestefanello/pagoda/pkg/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewPager(t *testing.T) {
|
||||
e := echo.New()
|
||||
ctx, _ := tests.NewContext(e, "/")
|
||||
pgr := NewPager(ctx, 10)
|
||||
assert.Equal(t, 10, pgr.ItemsPerPage)
|
||||
assert.Equal(t, 1, pgr.Page)
|
||||
assert.Equal(t, 0, pgr.Items)
|
||||
assert.Equal(t, 0, pgr.Pages)
|
||||
|
||||
ctx, _ = tests.NewContext(e, fmt.Sprintf("/abc?%s=%d", PageQueryKey, 2))
|
||||
pgr = NewPager(ctx, 10)
|
||||
assert.Equal(t, 2, pgr.Page)
|
||||
|
||||
ctx, _ = tests.NewContext(e, fmt.Sprintf("/abc?%s=%d", PageQueryKey, -2))
|
||||
pgr = NewPager(ctx, 10)
|
||||
assert.Equal(t, 1, pgr.Page)
|
||||
}
|
||||
|
||||
func TestPager_SetItems(t *testing.T) {
|
||||
ctx, _ := tests.NewContext(echo.New(), "/")
|
||||
pgr := NewPager(ctx, 20)
|
||||
pgr.SetItems(100)
|
||||
assert.Equal(t, 100, pgr.Items)
|
||||
assert.Equal(t, 5, pgr.Pages)
|
||||
}
|
||||
|
||||
func TestPager_IsBeginning(t *testing.T) {
|
||||
ctx, _ := tests.NewContext(echo.New(), "/")
|
||||
pgr := NewPager(ctx, 20)
|
||||
pgr.Pages = 10
|
||||
assert.True(t, pgr.IsBeginning())
|
||||
pgr.Page = 2
|
||||
assert.False(t, pgr.IsBeginning())
|
||||
pgr.Page = 1
|
||||
assert.True(t, pgr.IsBeginning())
|
||||
}
|
||||
|
||||
func TestPager_IsEnd(t *testing.T) {
|
||||
ctx, _ := tests.NewContext(echo.New(), "/")
|
||||
pgr := NewPager(ctx, 20)
|
||||
pgr.Pages = 10
|
||||
assert.False(t, pgr.IsEnd())
|
||||
pgr.Page = 10
|
||||
assert.True(t, pgr.IsEnd())
|
||||
pgr.Page = 1
|
||||
assert.False(t, pgr.IsEnd())
|
||||
}
|
||||
|
||||
func TestPager_GetOffset(t *testing.T) {
|
||||
ctx, _ := tests.NewContext(echo.New(), "/")
|
||||
pgr := NewPager(ctx, 20)
|
||||
assert.Equal(t, 0, pgr.GetOffset())
|
||||
pgr.Page = 2
|
||||
assert.Equal(t, 20, pgr.GetOffset())
|
||||
pgr.Page = 3
|
||||
assert.Equal(t, 40, pgr.GetOffset())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue