Added pagination (#16)

This commit is contained in:
konrad 2018-11-09 10:30:17 +00:00 committed by Gitea
parent 0e7e1b7e38
commit d232836423
22 changed files with 71 additions and 31 deletions

View file

@ -222,7 +222,7 @@ Teams sind global, d.h. Ein Team kann mehrere Namespaces verwalten.
* [ ] Wir brauchen noch ne gute idee, wie man die listen kriegt, auf die man nur so Zugriff hat (ohne namespace) * [ ] Wir brauchen noch ne gute idee, wie man die listen kriegt, auf die man nur so Zugriff hat (ohne namespace)
* Dazu am Besten nen pseudonamespace anlegen (id -1 oder so), der hat das dann alles * Dazu am Besten nen pseudonamespace anlegen (id -1 oder so), der hat das dann alles
* [ ] Validation der ankommenden structs, am besten mit https://github.com/go-validator/validator oder mit dem Ding von echo * [ ] Validation der ankommenden structs, am besten mit https://github.com/go-validator/validator oder mit dem Ding von echo
* [ ] Pagination * [x] Pagination
* Sollte in der Config definierbar sein, wie viel pro Seite angezeigt werden soll, die CRUD-Methoden übergeben dann ein "gibt mir die Seite sowieso" an die CRUDable-Funktionenen, die müssen das dann Auswerten. Geht leider nicht anders, wenn man erst 2342352 Einträge hohlt und die dann nachträglich auf 200 begrenzt ist das ne massive Ressourcenverschwendung. * Sollte in der Config definierbar sein, wie viel pro Seite angezeigt werden soll, die CRUD-Methoden übergeben dann ein "gibt mir die Seite sowieso" an die CRUDable-Funktionenen, die müssen das dann Auswerten. Geht leider nicht anders, wenn man erst 2342352 Einträge hohlt und die dann nachträglich auf 200 begrenzt ist das ne massive Ressourcenverschwendung.
* [ ] Testing mit locust: https://locust.io/ * [ ] Testing mit locust: https://locust.io/
* [ ] Methode einbauen, um mit einem gültigen token ein neues gültiges zu kriegen * [ ] Methode einbauen, um mit einem gültigen token ein neues gültiges zu kriegen

View file

@ -4,7 +4,7 @@ Content-Type: application/json
{ {
"username": "user", "username": "user",
"password": "12345" "password": "1234"
} }
> {% client.global.set("auth_token", response.body.token); %} > {% client.global.set("auth_token", response.body.token); %}

View file

@ -1,5 +1,5 @@
# Get all lists # Get all lists
GET http://localhost:8080/api/v1/lists GET http://localhost:8080/api/v1/lists?page=0
Authorization: Bearer {{auth_token}} Authorization: Bearer {{auth_token}}
### ###

View file

@ -7,6 +7,8 @@ service:
interface: ":3456" interface: ":3456"
# The URL of the frontend, used to send password reset emails. # The URL of the frontend, used to send password reset emails.
frontendurl: "" frontendurl: ""
# The number of items which gets returned per page
pagecount: 50
database: database:
# Database type to use. Supported types are mysql and sqlite. # Database type to use. Supported types are mysql and sqlite.
@ -48,4 +50,4 @@ mailer:
# Wether to skip verification of the tls certificate on the server # Wether to skip verification of the tls certificate on the server
skiptlsverify: false skiptlsverify: false
# The default from address when sending emails # The default from address when sending emails
fromemail: 'mail@vikunja' fromemail: 'mail@vikunja'

View file

@ -33,6 +33,8 @@ service:
frontendurl: "" frontendurl: ""
# The base path on the file system where the binary and assets are # The base path on the file system where the binary and assets are
rootpath: <the path of the executable> rootpath: <the path of the executable>
# The number of items which gets returned per page
pagecount: 50
database: database:
# Database type to use. Supported types are mysql and sqlite. # Database type to use. Supported types are mysql and sqlite.

View file

@ -29,6 +29,7 @@ func InitConfig() (err error) {
} }
exPath := filepath.Dir(ex) exPath := filepath.Dir(ex)
viper.SetDefault("service.rootpath", exPath) viper.SetDefault("service.rootpath", exPath)
viper.SetDefault("service.pagecount", 50)
// Database // Database
viper.SetDefault("database.type", "sqlite") viper.SetDefault("database.type", "sqlite")
viper.SetDefault("database.host", "localhost") viper.SetDefault("database.host", "localhost")

View file

@ -4,7 +4,7 @@ package models
type CRUDable interface { type CRUDable interface {
Create(*User) error Create(*User) error
ReadOne() error ReadOne() error
ReadAll(*User) (interface{}, error) ReadAll(*User, int) (interface{}, error)
Update() error Update() error
Delete() error Delete() error
} }

View file

@ -27,8 +27,8 @@ func GetListsByNamespaceID(nID int64) (lists []*List, err error) {
} }
// ReadAll gets all lists a user has access to // ReadAll gets all lists a user has access to
func (l *List) ReadAll(u *User) (interface{}, error) { func (l *List) ReadAll(u *User, page int) (interface{}, error) {
lists, err := getRawListsForUser(u) lists, err := getRawListsForUser(u, page)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -80,7 +80,7 @@ func (l *List) GetSimpleByID() (err error) {
} }
// Gets the lists only, without any tasks or so // Gets the lists only, without any tasks or so
func getRawListsForUser(u *User) (lists []*List, err error) { func getRawListsForUser(u *User, page int) (lists []*List, err error) {
fullUser, err := GetUserByID(u.ID) fullUser, err := GetUserByID(u.ID)
if err != nil { if err != nil {
return lists, err return lists, err
@ -104,6 +104,7 @@ func getRawListsForUser(u *User) (lists []*List, err error) {
Or("ul.user_id = ?", fullUser.ID). Or("ul.user_id = ?", fullUser.ID).
Or("un.user_id = ?", fullUser.ID). Or("un.user_id = ?", fullUser.ID).
GroupBy("l.id"). GroupBy("l.id").
Limit(getLimitFromPageIndex(page)).
Find(&lists) Find(&lists)
return lists, err return lists, err
@ -160,14 +161,14 @@ type ListTasksDummy struct {
} }
// ReadAll gets all tasks for a user // ReadAll gets all tasks for a user
func (lt *ListTasksDummy) ReadAll(u *User) (interface{}, error) { func (lt *ListTasksDummy) ReadAll(u *User, page int) (interface{}, error) {
return GetTasksByUser(u) return GetTasksByUser(u, page)
} }
//GetTasksByUser returns all tasks for a user //GetTasksByUser returns all tasks for a user
func GetTasksByUser(u *User) (tasks []*ListTask, err error) { func GetTasksByUser(u *User, page int) (tasks []*ListTask, err error) {
// Get all lists // Get all lists
lists, err := getRawListsForUser(u) lists, err := getRawListsForUser(u, page)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -20,14 +20,14 @@ func TestList_ReadAll(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
lists2 := List{} lists2 := List{}
lists3, err := lists2.ReadAll(&u) lists3, err := lists2.ReadAll(&u, 1)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(lists3).Kind(), reflect.Slice) assert.Equal(t, reflect.TypeOf(lists3).Kind(), reflect.Slice)
s := reflect.ValueOf(lists3) s := reflect.ValueOf(lists3)
assert.Equal(t, s.Len(), 1) assert.Equal(t, s.Len(), 1)
// Try getting lists for a nonexistant user // Try getting lists for a nonexistant user
_, err = lists2.ReadAll(&User{ID: 984234}) _, err = lists2.ReadAll(&User{ID: 984234}, 1)
assert.Error(t, err) assert.Error(t, err)
assert.True(t, IsErrUserDoesNotExist(err)) assert.True(t, IsErrUserDoesNotExist(err))
} }

View file

@ -1,7 +1,7 @@
package models package models
// ReadAll gets all users who have access to a list // ReadAll gets all users who have access to a list
func (ul *ListUser) ReadAll(u *User) (interface{}, error) { func (ul *ListUser) ReadAll(u *User, page int) (interface{}, error) {
// Check if the user has access to the list // Check if the user has access to the list
l := &List{ID: ul.ListID} l := &List{ID: ul.ListID}
if err := l.GetSimpleByID(); err != nil { if err := l.GetSimpleByID(); err != nil {
@ -16,6 +16,7 @@ func (ul *ListUser) ReadAll(u *User) (interface{}, error) {
err := x. err := x.
Join("INNER", "users_list", "user_id = users.id"). Join("INNER", "users_list", "user_id = users.id").
Where("users_list.list_id = ?", ul.ListID). Where("users_list.list_id = ?", ul.ListID).
Limit(getLimitFromPageIndex(page)).
Find(&all) Find(&all)
return all, err return all, err

View file

@ -90,3 +90,15 @@ func SetEngine() (err error) {
return nil return nil
} }
func getLimitFromPageIndex(page int) (limit, start int) {
// Get everything when page index is -1
if page < 0 {
return 0, 0
}
limit = viper.GetInt("service.pagecount")
start = limit * (page - 1)
return
}

View file

@ -53,7 +53,7 @@ func (n *Namespace) ReadOne() (err error) {
} }
// ReadAll gets all namespaces a user has access to // ReadAll gets all namespaces a user has access to
func (n *Namespace) ReadAll(doer *User) (interface{}, error) { func (n *Namespace) ReadAll(doer *User, page int) (interface{}, error) {
type namespaceWithLists struct { type namespaceWithLists struct {
Namespace `xorm:"extends"` Namespace `xorm:"extends"`
@ -71,6 +71,7 @@ func (n *Namespace) ReadAll(doer *User) (interface{}, error) {
Or("namespaces.owner_id = ?", doer.ID). Or("namespaces.owner_id = ?", doer.ID).
Or("users_namespace.user_id = ?", doer.ID). Or("users_namespace.user_id = ?", doer.ID).
GroupBy("namespaces.id"). GroupBy("namespaces.id").
Limit(getLimitFromPageIndex(page)).
Find(&all) Find(&all)
if err != nil { if err != nil {

View file

@ -85,7 +85,7 @@ func TestNamespace_Create(t *testing.T) {
assert.True(t, IsErrNamespaceDoesNotExist(err)) assert.True(t, IsErrNamespaceDoesNotExist(err))
// Get all namespaces of a user // Get all namespaces of a user
nsps, err := n.ReadAll(&doer) nsps, err := n.ReadAll(&doer, 1)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(nsps).Kind(), reflect.Slice) assert.Equal(t, reflect.TypeOf(nsps).Kind(), reflect.Slice)
s := reflect.ValueOf(nsps) s := reflect.ValueOf(nsps)

View file

@ -1,7 +1,7 @@
package models package models
// ReadAll gets all users who have access to a namespace // ReadAll gets all users who have access to a namespace
func (un *NamespaceUser) ReadAll(u *User) (interface{}, error) { func (un *NamespaceUser) ReadAll(u *User, page int) (interface{}, error) {
// Check if the user has access to the namespace // Check if the user has access to the namespace
l, err := GetNamespaceByID(un.NamespaceID) l, err := GetNamespaceByID(un.NamespaceID)
if err != nil { if err != nil {
@ -16,6 +16,7 @@ func (un *NamespaceUser) ReadAll(u *User) (interface{}, error) {
err = x. err = x.
Join("INNER", "users_namespace", "user_id = users.id"). Join("INNER", "users_namespace", "user_id = users.id").
Where("users_namespace.namespace_id = ?", un.NamespaceID). Where("users_namespace.namespace_id = ?", un.NamespaceID).
Limit(getLimitFromPageIndex(page)).
Find(&all) Find(&all)
return all, err return all, err

View file

@ -1,7 +1,7 @@
package models package models
// ReadAll implements the method to read all teams of a list // ReadAll implements the method to read all teams of a list
func (tl *TeamList) ReadAll(u *User) (interface{}, error) { func (tl *TeamList) ReadAll(u *User, page int) (interface{}, error) {
// Check if the user can read the namespace // Check if the user can read the namespace
l := &List{ID: tl.ListID} l := &List{ID: tl.ListID}
if err := l.GetSimpleByID(); err != nil { if err := l.GetSimpleByID(); err != nil {
@ -17,6 +17,7 @@ func (tl *TeamList) ReadAll(u *User) (interface{}, error) {
Table("teams"). Table("teams").
Join("INNER", "team_list", "team_id = teams.id"). Join("INNER", "team_list", "team_id = teams.id").
Where("team_list.list_id = ?", tl.ListID). Where("team_list.list_id = ?", tl.ListID).
Limit(getLimitFromPageIndex(page)).
Find(&all) Find(&all)
return all, err return all, err

View file

@ -50,27 +50,27 @@ func TestTeamList(t *testing.T) {
assert.True(t, IsErrListDoesNotExist(err)) assert.True(t, IsErrListDoesNotExist(err))
// Test Read all // Test Read all
teams, err := tl.ReadAll(&u) teams, err := tl.ReadAll(&u, 1)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice) assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
s := reflect.ValueOf(teams) s := reflect.ValueOf(teams)
assert.Equal(t, s.Len(), 1) assert.Equal(t, s.Len(), 1)
// Test Read all for nonexistant list // Test Read all for nonexistant list
_, err = tl4.ReadAll(&u) _, err = tl4.ReadAll(&u, 1)
assert.Error(t, err) assert.Error(t, err)
assert.True(t, IsErrListDoesNotExist(err)) assert.True(t, IsErrListDoesNotExist(err))
// Test Read all for a list where the user is owner of the namespace this list belongs to // Test Read all for a list where the user is owner of the namespace this list belongs to
tl5 := tl tl5 := tl
tl5.ListID = 2 tl5.ListID = 2
_, err = tl5.ReadAll(&u) _, err = tl5.ReadAll(&u, 1)
assert.NoError(t, err) assert.NoError(t, err)
// Test read all for a list where the user not has access // Test read all for a list where the user not has access
tl6 := tl tl6 := tl
tl6.ListID = 3 tl6.ListID = 3
_, err = tl6.ReadAll(&u) _, err = tl6.ReadAll(&u, 1)
assert.Error(t, err) assert.Error(t, err)
assert.True(t, IsErrNeedToHaveListReadAccess(err)) assert.True(t, IsErrNeedToHaveListReadAccess(err))

View file

@ -1,7 +1,7 @@
package models package models
// ReadAll implements the method to read all teams of a namespace // ReadAll implements the method to read all teams of a namespace
func (tn *TeamNamespace) ReadAll(user *User) (interface{}, error) { func (tn *TeamNamespace) ReadAll(user *User, page int) (interface{}, error) {
// Check if the user can read the namespace // Check if the user can read the namespace
n, err := GetNamespaceByID(tn.NamespaceID) n, err := GetNamespaceByID(tn.NamespaceID)
if err != nil { if err != nil {
@ -17,6 +17,7 @@ func (tn *TeamNamespace) ReadAll(user *User) (interface{}, error) {
err = x.Table("teams"). err = x.Table("teams").
Join("INNER", "team_namespaces", "team_id = teams.id"). Join("INNER", "team_namespaces", "team_id = teams.id").
Where("team_namespaces.namespace_id = ?", tn.NamespaceID). Where("team_namespaces.namespace_id = ?", tn.NamespaceID).
Limit(getLimitFromPageIndex(page)).
Find(&all) Find(&all)
return all, err return all, err

View file

@ -49,20 +49,20 @@ func TestTeamNamespace(t *testing.T) {
assert.True(t, IsErrNamespaceDoesNotExist(err)) assert.True(t, IsErrNamespaceDoesNotExist(err))
// Check readall // Check readall
teams, err := tn.ReadAll(&dummyuser) teams, err := tn.ReadAll(&dummyuser, 1)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice) assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
s := reflect.ValueOf(teams) s := reflect.ValueOf(teams)
assert.Equal(t, s.Len(), 1) assert.Equal(t, s.Len(), 1)
// Check readall for a nonexistant namespace // Check readall for a nonexistant namespace
_, err = tn4.ReadAll(&dummyuser) _, err = tn4.ReadAll(&dummyuser, 1)
assert.Error(t, err) assert.Error(t, err)
assert.True(t, IsErrNamespaceDoesNotExist(err)) assert.True(t, IsErrNamespaceDoesNotExist(err))
// Check with no right to read the namespace // Check with no right to read the namespace
nouser := &User{ID: 393} nouser := &User{ID: 393}
_, err = tn.ReadAll(nouser) _, err = tn.ReadAll(nouser, 1)
assert.Error(t, err) assert.Error(t, err)
assert.True(t, IsErrNeedToHaveNamespaceReadAccess(err)) assert.True(t, IsErrNeedToHaveNamespaceReadAccess(err))

View file

@ -84,12 +84,13 @@ func (t *Team) ReadOne() (err error) {
} }
// ReadAll gets all teams the user is part of // ReadAll gets all teams the user is part of
func (t *Team) ReadAll(user *User) (teams interface{}, err error) { func (t *Team) ReadAll(user *User, page int) (teams interface{}, err error) {
all := []*Team{} all := []*Team{}
err = x.Select("teams.*"). err = x.Select("teams.*").
Table("teams"). Table("teams").
Join("INNER", "team_members", "team_members.team_id = teams.id"). Join("INNER", "team_members", "team_members.team_id = teams.id").
Where("team_members.user_id = ?", user.ID). Where("team_members.user_id = ?", user.ID).
Limit(getLimitFromPageIndex(page)).
Find(&all) Find(&all)
return all, err return all, err

View file

@ -32,7 +32,7 @@ func TestTeam_Create(t *testing.T) {
assert.True(t, dummyteam.CanRead(&doer)) assert.True(t, dummyteam.CanRead(&doer))
// Get all teams the user is part of // Get all teams the user is part of
ts, err := tm.ReadAll(&doer) ts, err := tm.ReadAll(&doer, 1)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(ts).Kind(), reflect.Slice) assert.Equal(t, reflect.TypeOf(ts).Kind(), reflect.Slice)
s := reflect.ValueOf(ts) s := reflect.ValueOf(ts)

View file

@ -39,7 +39,7 @@ func Caldav(c echo.Context) error {
} }
// Get all tasks for that user // Get all tasks for that user
tasks, err := models.GetTasksByUser(&u) tasks, err := models.GetTasksByUser(&u, -1)
if err != nil { if err != nil {
return crud.HandleHTTPError(err) return crud.HandleHTTPError(err)
} }

View file

@ -1,9 +1,11 @@
package crud package crud
import ( import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"github.com/labstack/echo" "github.com/labstack/echo"
"net/http" "net/http"
"strconv"
) )
// ReadAllWeb is the webhandler to get all objects of a type // ReadAllWeb is the webhandler to get all objects of a type
@ -21,7 +23,21 @@ func (c *WebHandler) ReadAllWeb(ctx echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided.") return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided.")
} }
lists, err := currentStruct.ReadAll(&currentUser) // Pagination
page := ctx.QueryParam("page")
if page == "" {
page = "1"
}
pageNumber, err := strconv.Atoi(page)
if err != nil {
log.Log.Error(err.Error())
return echo.NewHTTPError(http.StatusBadRequest, "Bad page requested.")
}
if pageNumber < 0 {
return echo.NewHTTPError(http.StatusBadRequest, "Bad page requested.")
}
lists, err := currentStruct.ReadAll(&currentUser, pageNumber)
if err != nil { if err != nil {
return HandleHTTPError(err) return HandleHTTPError(err)
} }