blob: 37ac83da1af5894a11b600edf3472ec123d7da64 [file] [log] [blame]
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package frontend
import (
"net/http"
"net/url"
"strconv"
"golang.org/x/pkgsite/internal/log"
)
const maxLimit = 100
// pagination holds information related to paginated display. It is intended to
// be part of a view model struct.
//
// Given a sequence of results with offsets 0, 1, 2 ... (typically from a
// database query), we paginate it by dividing it into numbered pages
// 1, 2, 3, .... Each page except possibly the last has the same number of results.
type pagination struct {
baseURL *url.URL // URL common to all pages
Limit int // the number of results requested on a page
MaxLimit int // the maximum number of results allowed for any request
ResultCount int // number of results on this page
TotalCount int // total number of results
Approximate bool // whether or not the total count is approximate
Page int // number of the current page
PrevPage int // " " " previous page, usually Page-1 but zero if Page == 1
NextPage int // " " " next page, usually Page+1, but zero on the last page
Offset int // offset of the first item on the current page
Pages []int // consecutive page numbers to be displayed for navigation
Limits []int // limits to be displayed
}
// PageURL constructs a URL that displays the given page.
// It adds a "page" query parameter to the base URL.
func (p pagination) PageURL(page int) string {
newQuery := p.baseURL.Query()
newQuery.Set("page", strconv.Itoa(page))
p.baseURL.RawQuery = newQuery.Encode()
return p.baseURL.String()
}
// URL constructs a URL that adds limit and mode query parameters to the base
// URL. Passing a zero value omits the parameter.
func (p pagination) URL(limit int, mode, q string) string {
newQuery := p.baseURL.Query()
if limit != 0 {
newQuery.Set("limit", strconv.Itoa(limit))
}
if mode != "" {
newQuery.Set("m", mode)
}
if q != "" {
newQuery.Set("q", q)
}
p.baseURL.RawQuery = newQuery.Encode()
return p.baseURL.String()
}
// newPagination constructs a pagination. Call it after some results have been
// obtained.
// resultCount is the number of results in the current page.
// totalCount is the total number of results.
func newPagination(params paginationParams, resultCount, totalCount int) pagination {
return pagination{
baseURL: params.baseURL,
TotalCount: totalCount,
ResultCount: resultCount,
Offset: params.offset(),
Limit: params.limit,
MaxLimit: maxLimit,
Page: params.page,
PrevPage: prev(params.page),
NextPage: next(params.page, params.limit, totalCount),
Pages: pagesToLink(params.page, numPages(params.limit, totalCount), defaultNumPagesToLink),
Limits: []int{10, 30, maxSearchPageSize},
}
}
// paginationParams holds pagination parameters extracted from the request.
type paginationParams struct {
baseURL *url.URL
page int // the number of the page to display
limit int // the maximum number of results to display on the page
}
// offset returns the offset of the first result on the page.
func (p paginationParams) offset() int {
return offset(p.page, p.limit)
}
// newPaginationParams extracts pagination params from the request.
func newPaginationParams(r *http.Request, defaultLimit int) paginationParams {
positiveParam := func(key string, dflt int) (val int) {
var err error
if a := r.FormValue(key); a != "" {
val, err = strconv.Atoi(a)
if err != nil {
log.Errorf(r.Context(), "strconv.Atoi(%q) for page: %v", a, err)
}
}
if val < 1 {
val = dflt
}
return val
}
return paginationParams{
baseURL: r.URL,
page: positiveParam("page", 1),
limit: positiveParam("limit", defaultLimit),
}
}
const defaultNumPagesToLink = 5
// pagesToLink returns the page numbers that will be displayed. Given a
// page, it returns a slice containing numPagesToLink integers in ascending
// order and optimizes for page to be in the middle of that range. The max
// value of an integer in the return slice will be less than numPages.
func pagesToLink(page, numPages, numPagesToLink int) []int {
var pages []int
start := page - (numPagesToLink / 2)
if (numPages - start) < numPagesToLink {
start = numPages - numPagesToLink + 1
}
if start < 1 {
start = 1
}
for i := start; (i < start+numPagesToLink) && (i <= numPages); i++ {
pages = append(pages, i)
}
return pages
}
// numPages is the total number of pages needed to display all the results,
// given the specified maximum page size and the total number of results.
func numPages(pageSize, totalCount int) int {
return (totalCount + pageSize - 1) / pageSize
}
// offset returns the offset of the first result on page, assuming all previous
// pages were of size limit.
func offset(page, limit int) int {
if page <= 1 {
return 0
}
return (page - 1) * limit
}
// prev returns the number of the page before the given page, or zero if the
// given page is 1 or smaller.
func prev(page int) int {
if page <= 1 {
return 0
}
return page - 1
}
// next returns the number of the page after the given page, or zero if page is is the last page or larger.
// limit and totalCount are used to calculate the last page (see numPages).
func next(page, limit, totalCount int) int {
if page >= numPages(limit, totalCount) {
return 0
}
return page + 1
}