Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/paginated list #46

Merged
merged 13 commits into from
Aug 27, 2021
Merged
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: 1 addition & 1 deletion scripts/start.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#!/bin/sh
go run ./cmd/main.go --web-log-mode=debug
go run ./cmd/main.go --web-log-mode=debug --db-type=postgres --db-connection="host=localhost user=iliaf password=iliaf dbname=iliaf port=5432 sslmode=disable"
56 changes: 41 additions & 15 deletions src/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
package service

import (
"errors"
"fmt"
"math/rand"
"strconv"
Expand All @@ -24,17 +23,23 @@ type Service struct {
store store.Interface
}

type Error string

func (e Error) Error() string {
return string(e)
}

// ErrPasteNotFound and other common errors.
var (
ErrPasteNotFound = errors.New("paste not found")
ErrUserNotFound = errors.New("user not found")
ErrPasteIsPrivate = errors.New("paste is private")
ErrPasteHasPassword = errors.New("paste has password")
ErrWrongPassword = errors.New("paste password is incorrect")
ErrStoreFailure = errors.New("store opertation failed")
ErrEmptyBody = errors.New("body is empty")
ErrWrongPrivacy = errors.New("privacy is wrong")
ErrWrongDuration = errors.New("wrong duration format")
const (
ErrPasteNotFound = Error("paste not found")
ErrUserNotFound = Error("user not found")
ErrPasteIsPrivate = Error("paste is private")
ErrPasteHasPassword = Error("paste has password")
ErrWrongPassword = Error("paste password is incorrect")
ErrStoreFailure = Error("store opertation failed")
ErrEmptyBody = Error("body is empty")
ErrWrongPrivacy = Error("privacy is wrong")
ErrWrongDuration = Error("wrong duration format")
)

// PasteRequest is an input to Create method, normally comes from a web form.
Expand Down Expand Up @@ -224,7 +229,7 @@ func (s Service) GetOrUpdateUser(usr store.User) (store.User, error) {
return usr, nil
}

// UserPastes returns a list of the last 10 paste for a user.
// UserPastes returns a list of the last 10 paste for a user ordered by creation date.
func (s Service) UserPastes(uid string) ([]store.Paste, error) {
pastes, err := s.store.Find(store.FindRequest{
UserID: uid,
Expand All @@ -239,7 +244,28 @@ func (s Service) UserPastes(uid string) ([]store.Paste, error) {
return pastes, nil
}

// GetCount returns total count of pastes and users.
func (s Service) GetCount() (pastes, users int64) {
return s.store.Count()
// Get a list of pastes for a particular user.
//
func (s Service) GetPastes(uid string, sort string, limit int, skip int) ([]store.Paste, error) {
pastes, err := s.store.Find(store.FindRequest{
UserID: uid,
Sort: sort,
Since: time.Time{},
Limit: limit,
Skip: skip,
})
if err != nil {
return nil, fmt.Errorf("Service.GetPastes: %w: (%v)", ErrStoreFailure, err)
}
return pastes, nil
}

// PastesCount return a number of pastes for a user.
func (s Service) PastesCount(uid string) int64 {
return s.store.Count(uid)
}

// GetTotals returns total count of pastes and users.
func (s Service) GetTotals() (pastes, users int64) {
return s.store.Totals()
}
15 changes: 14 additions & 1 deletion src/store/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func NewMemDB() *MemDB {
}

// Count returns total count of pastes and users.
func (m *MemDB) Count() (pastes, users int64) {
func (m *MemDB) Totals() (pastes, users int64) {
m.RLock()
defer m.RUnlock()

Expand Down Expand Up @@ -107,6 +107,19 @@ func (m *MemDB) Find(req FindRequest) (pastes []Paste, err error) {
return pastes[skip:end], nil
}

func (m *MemDB) Count(uid string) int64 {
m.RLock()
defer m.RUnlock()
// Count all the pastes for a user
var cnt int64
for _, p := range m.pastes {
if p.User.ID == uid {
cnt++
}
}
return cnt
}

// Get returns a paste by ID.
func (m *MemDB) Get(id int64) (Paste, error) {
m.RLock()
Expand Down
28 changes: 25 additions & 3 deletions src/store/memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ var findTestCases = []testCaseForFind{
},
}

// TestCount tests that we can count pastes and users correctly.
func TestCount(t *testing.T) {
// TestTotals tests that we can count pastes and users correctly.
func TestTotals(t *testing.T) {
t.Parallel()

// We need a dedicated store because other test running in parallel
Expand Down Expand Up @@ -127,7 +127,7 @@ func TestCount(t *testing.T) {
// Check the counts
wantUsers := uCnt
wantPastes := uCnt * pCnt
gotPastes, gotUsers := m.Count()
gotPastes, gotUsers := m.Totals()

if wantUsers != gotUsers {
t.Errorf("users count is incorrect, want %d, got %d", wantUsers, gotUsers)
Expand All @@ -137,6 +137,28 @@ func TestCount(t *testing.T) {
}
}

func TestCount(t *testing.T) {
t.Parallel()

usr := randomUser()
_, err := mdb.SaveUser(usr)
if err != nil {
t.Fatalf("failed to save user: %v", err)
}
pCnt := rand.Int63n(20)
for i := int64(0); i < pCnt; i++ {
paste := randomPaste(usr)
_, err = mdb.Create(paste)
if err != nil {
t.Fatalf("failed to create paste: %v", err)
}
}
got := mdb.Count(usr.ID)
if got != pCnt {
t.Errorf("pastes count for user %s is incorrect, want %d got %d", usr.ID, pCnt, got)
}
}

// TestDelete tests that we can delete a paste.
func TestDelete(t *testing.T) {
t.Parallel()
Expand Down
7 changes: 6 additions & 1 deletion src/store/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func NewPostgresDB(conn string, autoMigrate bool) (*PostgresDB, error) {
}

// Count returns total count of pastes and users.
func (pg *PostgresDB) Count() (pastes, users int64) {
func (pg *PostgresDB) Totals() (pastes, users int64) {
pg.db.Model(&Paste{}).Count(&pastes)
pg.db.Model(&User{}).Count(&users)
return
Expand Down Expand Up @@ -119,6 +119,11 @@ func (pg *PostgresDB) Find(req FindRequest) (pastes []Paste, err error) {
return pastes, nil
}

func (pg *PostgresDB) Count(uid string) (pastes int64) {
pg.db.Model(&Paste{}).Where("user_id = ?", uid).Count(&pastes)
return
}

// Get returns a paste by ID.
func (pg *PostgresDB) Get(id int64) (Paste, error) {
var paste Paste
Expand Down
21 changes: 21 additions & 0 deletions src/store/postgres_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
package store

import (
"math/rand"
"sort"
"testing"
"time"
)

func TestCountPDB(t *testing.T) {
usr := randomUser()
_, err := pdb.SaveUser(usr)
if err != nil {
t.Fatalf("failed to save user: %v", err)
}
pCnt := rand.Int63n(20)
for i := int64(0); i < pCnt; i++ {
paste := randomPaste(usr)
_, err = pdb.Create(paste)
if err != nil {
t.Fatalf("failed to create paste: %v", err)
}
}
got := pdb.Count(usr.ID)
if got != pCnt {
t.Errorf("pastes count for user %s is incorrect, want %d got %d", usr.ID, pCnt, got)
}
}

// TestDelete tests that we can delete a paste.
func TestDeletePDB(t *testing.T) {
t.Parallel()
Expand Down
21 changes: 11 additions & 10 deletions src/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,26 @@ import (
// Interface defines methods that an implementation of a concrete storage
// must provide.
type Interface interface {
Count() (pastes, users int64) // return total counts for pastes and users
Totals() (pastes, users int64) // return total counts for pastes and users
Create(paste Paste) (id int64, err error) // create new paste and return its id
Delete(id int64) error // delete paste by id
Find(req FindRequest) ([]Paste, error) // find pastes
Count(uid string) int64 // return pastes count for a user
Get(id int64) (Paste, error) // get paste by id
Update(paste Paste) (Paste, error) // update paste information and return updated paste
SaveUser(usr User) (id string, err error) // creates or updates a user
User(id string) (User, error) // get user by id
}

// FindRequest is an input to the Find method
type FindRequest struct {
UserID string
Sort string
Since time.Time
Limit int
Skip int
}

// User represents a single user.
type User struct {
ID string `json:"id" gorm:"primaryKey"`
Expand Down Expand Up @@ -120,12 +130,3 @@ func (p Paste) Expiration() string {

return p.Expires.Sub(p.CreatedAt).String()
}

// FindRequest is an input to the Find method
type FindRequest struct {
UserID string
Sort string
Since time.Time
Limit int
Skip int
}
40 changes: 37 additions & 3 deletions src/web/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,24 @@ package web
import (
"bytes"
"errors"
"math"
"net/http"
"strconv"

"github.com/go-pkgz/auth/token"
"github.com/gorilla/mux"
"github.com/iliafrenkel/go-pb/src/service"
"github.com/iliafrenkel/go-pb/src/store"
)

// Paginator struct used to build paginators on list pages.
type Paginator struct {
Number int
Offset int
Size int
IsCurrent bool
}

// PageData contains the data that any page template may need.
type PageData struct {
Title string
Expand All @@ -26,6 +36,7 @@ type PageData struct {
User token.User
Pastes []store.Paste
Paste store.Paste
Pages []Paginator
Server string
Version string
ErrorCode int
Expand All @@ -38,7 +49,7 @@ type PageData struct {
// Generate HTML from a template with PageData.
func (h *Server) generateHTML(tpl string, p PageData) []byte {
var html bytes.Buffer
pcnt, ucnt := h.service.GetCount()
pcnt, ucnt := h.service.GetTotals()
var pd = PageData{
Title: h.options.BrandName + " - " + p.Title,
Brand: h.options.BrandName,
Expand All @@ -49,6 +60,7 @@ func (h *Server) generateHTML(tpl string, p PageData) []byte {
User: p.User,
Pastes: p.Pastes,
Paste: p.Paste,
Pages: p.Pages,
Server: h.options.Proto + "://" + h.options.Addr,
Version: h.options.Version,
ErrorCode: p.ErrorCode,
Expand Down Expand Up @@ -308,14 +320,36 @@ func (h *Server) handleGetPastePage(w http.ResponseWriter, r *http.Request) {
// handleGetPastesList generates a page to view a list of pastes.
func (h *Server) handleGetPastesList(w http.ResponseWriter, r *http.Request) {
usr, _ := token.GetUserInfo(r)
limit := 10 //TODO: make it configurable as PageSize
skip, err := strconv.Atoi(r.FormValue("skip"))
if err != nil {
skip = 0
}

pastes, err := h.service.UserPastes(usr.ID)
pastes, err := h.service.GetPastes(usr.ID, "-created", limit, skip)
if err != nil {
h.showInternalError(w, err)
return
}
count := h.service.PastesCount(usr.ID)
pageCount := int(math.Ceil(float64(count) / float64(limit)))

_, e := w.Write(h.generateHTML("list.html", PageData{Title: "Pastes", Pastes: pastes, User: usr}))
pages := make([]Paginator, pageCount)
for i := 1; i <= pageCount; i++ {
pages[i-1] = Paginator{
Number: i,
Offset: (i - 1) * limit,
Size: limit,
IsCurrent: skip/limit == i-1,
}
}

_, e := w.Write(h.generateHTML("list.html", PageData{
Title: "Pastes",
Pastes: pastes,
Pages: pages,
User: usr,
}))
if e != nil {
h.log.Logf("ERROR handleGetPastesList: failed to write: %v", e)
}
Expand Down
9 changes: 9 additions & 0 deletions templates/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ <h5 class="card-title text-center">My Pastes</h5>
{{template "paste.html" .}}
{{end}}
</div>
{{if and .Pages (gt (len .Pages) 1)}}
<nav class="mt-3" aria-label="...">
<ul class="pagination justify-content-center">
{{range .Pages}}
<li class="page-item{{if .IsCurrent}} active{{end}}"><a class="page-link" href="/l/?skip={{.Offset}}">{{.Number}}</a></li>
{{end}}
</ul>
</nav>
{{end}}
{{else}}
<h1 class="display-6 text-center">Nothing to see here yet.</h1>
{{end}}
Expand Down