Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions kibble/api/bios.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func (p pageV1) mapToModel(serviceConfig models.ServiceConfig, itemIndex models.
},
PageCollections: make([]models.PageCollection, 0),
CustomFields: p.CustomFields,
PublishedDate: utils.ParseTimeFromString(p.PublishedDate),
}

page.Seo = models.Seo{
Expand Down Expand Up @@ -132,6 +133,7 @@ type pageV1 struct {
Title string `json:"title"`
URL string `json:"url"`
CustomFields map[string]interface{} `json:"custom"`
PublishedDate string `json:"published_date"`
}

type filmSummary struct {
Expand Down
12 changes: 10 additions & 2 deletions kibble/datastore/page_datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,21 @@ func (ds *PageDataSource) Iterator(ctx models.RenderContext, renderer models.Ren
// don't render external pages
if p.PageType != "external" {
templatePath := strings.Replace(ctx.Route.TemplatePath, ":type", p.PageType, 1)
errCount += renderer.Render(templatePath, ds.GetRouteForEntity(ctx, &p), data)
if renderer.HasTemplate(templatePath) || !strings.Contains(ctx.Route.TemplatePath, ":type") {
errCount += renderer.Render(templatePath, ds.GetRouteForEntity(ctx, &p), data)
} else {
log.Warning("Template not found for page type '%s': %s", p.PageType, templatePath)
}
}

// now partial end points
if ctx.Route.HasPartial() {
templatePath := strings.Replace(ctx.Route.PartialTemplatePath, ":type", p.PageType, 1)
errCount += renderer.Render(templatePath, ds.GetPartialRouteForEntity(ctx, &p), data)
if renderer.HasTemplate(templatePath) || !strings.Contains(ctx.Route.PartialTemplatePath, ":type") {
errCount += renderer.Render(templatePath, ds.GetPartialRouteForEntity(ctx, &p), data)
} else {
log.Warning("Partial Template not found for page type '%s': %s", p.PageType, templatePath)
}
}

}
Expand Down
155 changes: 95 additions & 60 deletions kibble/datastore/page_index_datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
package datastore

import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"sort"
"strings"

"kibble/models"
Expand All @@ -38,6 +39,21 @@ var indexArgs = []models.RouteArgument{
// PageIndexDataSource - a list of all Pages
type PageIndexDataSource struct{}

type pageIndexDataSourceOptions struct {
PageTypes []string `json:"pageTypes"`
SortBy []string `json:"sortBy"`
}

func (opts pageIndexDataSourceOptions) IsAllowedPageType(pageType string) bool {
for _, pt := range opts.PageTypes {
if pt == pageType {
return true
}
}

return len(opts.PageTypes) == 0
}

// GetName - returns the name of the datasource
func (ds *PageIndexDataSource) GetName() string {
return "PageIndex"
Expand All @@ -56,76 +72,41 @@ func (ds *PageIndexDataSource) GetEntityType() reflect.Type {
// Iterator - return a list of all Pages, iteration of 1
func (ds *PageIndexDataSource) Iterator(ctx models.RenderContext, renderer models.Renderer) (errCount int) {

// rule for page 1
if ctx.Route.PageSize > 0 {

if !strings.Contains(ctx.Route.URLPath, ":index") {
panic(fmt.Errorf("Page route is missing an :index. Either add and index placeholder or remove the pageSize"))
var options pageIndexDataSourceOptions
if len(ctx.Route.Options) > 0 {
err := json.Unmarshal(ctx.Route.Options, &options)
if err != nil {
panic(fmt.Errorf("unable to parse datasource options: %w", err))
}
}

ctx.Route.Pagination = models.Pagination{
Index: 1,
Total: (len(ctx.Site.Pages) / ctx.Route.PageSize) + 1,
Size: ctx.Route.PageSize,
pages := make(models.Pages, 0, len(ctx.Site.Pages))
for _, page := range ctx.Site.Pages {
if !options.IsAllowedPageType(page.PageType) {
continue
}
pages = append(pages, page)
}

// page count
for pi := 0; pi < ctx.Route.Pagination.Total; pi++ {

ctx.Route.Pagination.Index = pi + 1
ctx.Route.Pagination.PreviousURL = ""
ctx.Route.Pagination.NextURL = ""

path := strings.Replace(ctx.Route.URLPath, ":index",
strconv.Itoa(ctx.Route.Pagination.Index), 1)

if pi > 0 {
ctx.Route.Pagination.PreviousURL =
strings.Replace(ctx.Route.URLPath, ":index",
strconv.Itoa(ctx.Route.Pagination.Index-1), 1)
}

if pi < ctx.Route.Pagination.Total-1 {
ctx.Route.Pagination.NextURL =
strings.Replace(ctx.Route.URLPath, ":index",
strconv.Itoa(ctx.Route.Pagination.Index+1), 1)
}

startIndex := pi * ctx.Route.PageSize
endIndex := ((pi * ctx.Route.PageSize) + ctx.Route.PageSize) - 1
if endIndex >= len(ctx.Site.Pages) {
endIndex = len(ctx.Site.Pages) - 1
}

clonedPages := make([]*models.Page, endIndex-startIndex+1)
for i := startIndex; i <= endIndex; i++ {
clonedPages[i-startIndex] = transformPage(ctx.Site.Pages[i])
}

vars := make(jet.VarMap)
vars.Set("pages", clonedPages)
vars.Set("pagination", ctx.Route.Pagination)
vars.Set("site", ctx.Site)
errCount += renderer.Render(ctx.Route.TemplatePath, ctx.RoutePrefix+path, vars)
}
} else {
if len(options.SortBy) > 0 {
sortPages(pages, ParseSortKeys(options.SortBy))
}

ctx.Route.Pagination = models.Pagination{
Index: 1,
Total: len(ctx.Site.Pages),
Size: len(ctx.Site.Pages),
}
if ctx.Route.PageSize > 0 && !strings.Contains(ctx.Route.URLPath, ":index") {
panic(fmt.Errorf("page route is missing an :index. Either add and index placeholder or remove the pageSize"))
}

clonedPages := make([]*models.Page, len(ctx.Site.Pages))
for i, f := range ctx.Site.Pages {
clonedPages[i] = transformPage(f)
for _, pagination := range ctx.Paginate(len(pages)) {
clonedPages := make([]*models.Page, pagination.ItemCount)
for i := 0; i < pagination.ItemCount; i++ {
clonedPages[i] = transformPage(pages[pagination.ItemSliceStart+i])
}

vars := make(jet.VarMap)
vars.Set("pages", clonedPages)
vars.Set("pagination", ctx.Route.Pagination)
vars.Set("pagination", pagination)
vars.Set("site", ctx.Site)
errCount += renderer.Render(ctx.Route.TemplatePath, ctx.RoutePrefix+ctx.Route.URLPath, vars)
errCount += renderer.Render(ctx.Route.TemplatePath, pagination.CurrentURL, vars)
}

return
Expand Down Expand Up @@ -155,3 +136,57 @@ func transformPage(f models.Page) *models.Page {

return &f
}

type sortKey struct {
Field string
Desc bool
}

func ParseSortKeys(raw []string) []sortKey {
keys := make([]sortKey, 0, len(raw))
for _, s := range raw {
parts := strings.SplitN(s, ":", 2)
key := sortKey{Field: parts[0]}
if len(parts) == 2 && strings.EqualFold(parts[1], "desc") {
key.Desc = true
}
keys = append(keys, key)
}
return keys
}

func sortPages(pages models.Pages, keys []sortKey) {
// Apply from last to first so earlier keys win (like SQL ORDER BY)
for i := len(keys) - 1; i >= 0; i-- {
key := keys[i]

sort.SliceStable(pages, func(i, j int) bool {
pi, pj := pages[i], pages[j]

switch key.Field {
case "published_date":
// handle equal first so we don't randomly flip
if pi.PublishedDate.Equal(pj.PublishedDate) {
return false
}
if key.Desc {
return pi.PublishedDate.After(pj.PublishedDate)
}
return pi.PublishedDate.Before(pj.PublishedDate)

case "title":
if pi.Title == pj.Title {
return false
}
if key.Desc {
return pi.Title > pj.Title
}
return pi.Title < pj.Title

default:
// unknown field → no-op for this pass
return false
}
})
}
}
3 changes: 3 additions & 0 deletions kibble/models/pages.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

package models

import "time"

// PageCollection - part of a page
type PageCollection struct {
ID int
Expand Down Expand Up @@ -42,6 +44,7 @@ type Page struct {
URL string
CustomFields CustomFields
Plans []Plan
PublishedDate time.Time
}

// Pages -
Expand Down
110 changes: 110 additions & 0 deletions kibble/models/paginator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package models

import (
"strconv"
"strings"
)

type Paginator struct {
Route *Route
RoutePrefix string
TotalItems int
}

func (ctx *RenderContext) Paginate(totalItems int) []Pagination {
return Paginator{
RoutePrefix: ctx.RoutePrefix,
Route: ctx.Route,
TotalItems: totalItems,
}.GetAll()
}

// Pagination describes a single page of results
type Pagination struct {
Index int // current page, 1-based index
Size int // nominal page size. Actual number of results may differ
Total int // Total pages
PreviousURL string // Prev page href, or blank
CurrentURL string // Current page href
NextURL string // Next page href, or blank
ItemSliceStart int // First item index (inclusive) for use with [:] slice operator
ItemSliceEnd int // Last item index (exclusive) for use with [:] slice operator
ItemCount int // Number of items on the page
}

func (p Paginator) GetAll() []Pagination {
firstPage := p.GetPagination(1)

results := make([]Pagination, firstPage.Total)
results[0] = firstPage
for i := 1; i < firstPage.Total; i++ {
results[i] = p.GetPagination(i + 1)
}

return results
}

func (p Paginator) GetPagination(currentPage int) Pagination {
totalItems := p.TotalItems
pageSize := p.Route.PageSize
if pageSize <= 0 {
// slightly weird edge case here: if there are no items the page size is 0
pageSize = totalItems
}

var totalPages int
if pageSize <= 0 || totalItems <= 0 {
// ensure that empty collections always render at least one page
totalPages = 1
} else {
totalPages = (totalItems + pageSize - 1) / pageSize
}

if currentPage < 1 {
currentPage = 1
} else if currentPage > totalPages {
currentPage = totalPages
}

pagination := Pagination{
Index: currentPage,
Total: totalPages,
Size: pageSize,
CurrentURL: p.GetPaginationURLPath(currentPage),
PreviousURL: "",
NextURL: "",
ItemSliceStart: 0,
ItemSliceEnd: 0,
ItemCount: 0,
}

// NOTE: follows Go slice semantics [start:end)
pagination.ItemSliceStart = (pagination.Index - 1) * pageSize
pagination.ItemSliceEnd = pagination.ItemSliceStart + pageSize
if pagination.ItemSliceEnd > totalItems {
pagination.ItemSliceEnd = totalItems
}

pagination.ItemCount = pagination.ItemSliceEnd - pagination.ItemSliceStart

if pagination.Index > 1 {
pagination.PreviousURL = p.GetPaginationURLPath(pagination.Index - 1)
}

if pagination.Index < pagination.Total {
pagination.NextURL = p.GetPaginationURLPath(pagination.Index + 1)
}

return pagination
}

func (p Paginator) GetPaginationURLPath(pageIndex int) string {
var urlPath string
if len(p.Route.FirstPageURLPath) > 0 && pageIndex <= 1 {
urlPath = p.Route.FirstPageURLPath
} else {
urlPath = p.Route.URLPath
}

return p.RoutePrefix + strings.Replace(urlPath, ":index", strconv.Itoa(pageIndex), 1)
}
1 change: 1 addition & 0 deletions kibble/models/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ type RenderContext struct {
type Renderer interface {
Initialise()
Render(templatePath string, filePath string, data jet.VarMap) (errorCount int)
HasTemplate(templatePath string) bool
}
Loading
Loading