Browse Source

Improve pagination (#105)

release/0.13
konrad 2 years ago
parent
commit
8948a5f219
  1. 4
      config.yml.sample
  2. 2
      docs/content/doc/practical-instructions/feature.md
  3. 4
      docs/content/doc/setup/config.md
  4. 14
      go.mod
  5. 41
      go.sum
  6. 4
      pkg/config/config.go
  7. 17
      pkg/models/label.go
  8. 32
      pkg/models/label_task.go
  9. 2
      pkg/models/label_task_test.go
  10. 2
      pkg/models/label_test.go
  11. 25
      pkg/models/link_sharing.go
  12. 45
      pkg/models/list.go
  13. 5
      pkg/models/list_read_test.go
  14. 26
      pkg/models/list_team.go
  15. 8
      pkg/models/list_team_test.go
  16. 22
      pkg/models/list_users.go
  17. 2
      pkg/models/list_users_test.go
  18. 8
      pkg/models/models.go
  19. 42
      pkg/models/namespace.go
  20. 22
      pkg/models/namespace_team.go
  21. 6
      pkg/models/namespace_team_test.go
  22. 2
      pkg/models/namespace_test.go
  23. 22
      pkg/models/namespace_users.go
  24. 2
      pkg/models/namespace_users_test.go
  25. 24
      pkg/models/task_assignees.go
  26. 19
      pkg/models/task_attachment.go
  27. 2
      pkg/models/task_attachment_test.go
  28. 2
      pkg/models/task_readall_test.go
  29. 65
      pkg/models/tasks.go
  30. 22
      pkg/models/teams.go
  31. 2
      pkg/models/teams_test.go
  32. 2
      pkg/routes/caldav/listStorageProvider.go
  33. 6
      pkg/routes/routes.go
  34. 114
      pkg/swagger/docs.go
  35. 112
      pkg/swagger/swagger.json
  36. 101
      pkg/swagger/swagger.yaml
  37. 36
      vendor/code.vikunja.io/web/Readme.md
  38. 9
      vendor/code.vikunja.io/web/handler/config.go
  39. 47
      vendor/code.vikunja.io/web/handler/read_all.go
  40. 2
      vendor/code.vikunja.io/web/web.go
  41. 21
      vendor/github.com/labstack/echo/v4/bind.go
  42. 7
      vendor/github.com/labstack/echo/v4/context.go
  43. 43
      vendor/github.com/labstack/echo/v4/echo.go
  44. 10
      vendor/github.com/labstack/echo/v4/go.mod
  45. 47
      vendor/github.com/labstack/echo/v4/go.sum
  46. 6
      vendor/github.com/labstack/echo/v4/middleware/logger.go
  47. 13
      vendor/github.com/labstack/echo/v4/middleware/proxy.go
  48. 3
      vendor/github.com/labstack/echo/v4/middleware/proxy_1_11.go
  49. 8
      vendor/github.com/labstack/echo/v4/middleware/secure.go
  50. 16
      vendor/github.com/labstack/echo/v4/router.go
  51. 3
      vendor/github.com/labstack/gommon/bytes/bytes.go
  52. 6
      vendor/github.com/mattn/go-colorable/colorable_appengine.go
  53. 6
      vendor/github.com/mattn/go-colorable/colorable_others.go
  54. 43
      vendor/github.com/mattn/go-colorable/colorable_windows.go
  55. 2
      vendor/github.com/mattn/go-colorable/go.sum
  56. 6
      vendor/github.com/mattn/go-colorable/noncolorable.go
  57. 2
      vendor/github.com/valyala/fasttemplate/README.md
  58. 16
      vendor/golang.org/x/sys/unix/sockcmsg_dragonfly.go
  59. 2
      vendor/golang.org/x/sys/unix/sockcmsg_linux.go
  60. 36
      vendor/golang.org/x/sys/unix/sockcmsg_unix.go
  61. 38
      vendor/golang.org/x/sys/unix/sockcmsg_unix_other.go
  62. 2
      vendor/golang.org/x/sys/unix/syscall_bsd.go
  63. 2
      vendor/golang.org/x/sys/unix/syscall_darwin.go
  64. 1
      vendor/golang.org/x/sys/unix/syscall_darwin_386.go
  65. 1
      vendor/golang.org/x/sys/unix/syscall_darwin_amd64.go
  66. 4
      vendor/golang.org/x/sys/unix/syscall_darwin_arm.go
  67. 4
      vendor/golang.org/x/sys/unix/syscall_darwin_arm64.go
  68. 24
      vendor/golang.org/x/sys/unix/syscall_dragonfly.go
  69. 4
      vendor/golang.org/x/sys/unix/syscall_freebsd.go
  70. 2
      vendor/golang.org/x/sys/unix/syscall_linux.go
  71. 4
      vendor/golang.org/x/sys/unix/syscall_netbsd.go
  72. 4
      vendor/golang.org/x/sys/unix/syscall_openbsd.go
  73. 2
      vendor/golang.org/x/sys/unix/syscall_solaris.go
  74. 32
      vendor/golang.org/x/sys/unix/zsyscall_darwin_386.1_11.go
  75. 42
      vendor/golang.org/x/sys/unix/zsyscall_darwin_386.go
  76. 6
      vendor/golang.org/x/sys/unix/zsyscall_darwin_386.s
  77. 32
      vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.1_11.go
  78. 42
      vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.go
  79. 4
      vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.s
  80. 16
      vendor/golang.org/x/sys/unix/zsyscall_darwin_arm.1_11.go
  81. 21
      vendor/golang.org/x/sys/unix/zsyscall_darwin_arm.go
  82. 16
      vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.1_11.go
  83. 21
      vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.go
  84. 4
      vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.s
  85. 25
      vendor/golang.org/x/tools/go/internal/gcimporter/iexport.go
  86. 28
      vendor/golang.org/x/tools/go/internal/gcimporter/iimport.go
  87. 186
      vendor/golang.org/x/tools/go/packages/golist.go
  88. 21
      vendor/golang.org/x/tools/go/packages/golist_overlay.go
  89. 6
      vendor/golang.org/x/tools/internal/gopathwalk/walk.go
  90. 100
      vendor/golang.org/x/tools/internal/span/parse.go
  91. 285
      vendor/golang.org/x/tools/internal/span/span.go
  92. 151
      vendor/golang.org/x/tools/internal/span/token.go
  93. 39
      vendor/golang.org/x/tools/internal/span/token111.go
  94. 16
      vendor/golang.org/x/tools/internal/span/token112.go
  95. 152
      vendor/golang.org/x/tools/internal/span/uri.go
  96. 94
      vendor/golang.org/x/tools/internal/span/utf16.go
  97. 17
      vendor/modules.txt

4
config.yml.sample

@ -11,8 +11,8 @@ service:
# Vikunja will also look in this path for a config file, so you could provide only this variable to point to a folder
# with a config file which will then be used.
rootpath: <rootpath>
# The number of items which gets returned per page
pagecount: 50
# The max number of items which can be returned per page
maxitemsperpage: 50
# If set to true, enables a /metrics endpoint for prometheus to collect metrics about the system
# You'll need to use redis for this in order to enable common metrics over multiple nodes
enablemetrics: false

2
docs/content/doc/practical-instructions/feature.md

@ -27,7 +27,7 @@ You can feed this function directly into xorm's `Limit`-Function like so:
{{< highlight golang >}}
lists := []List{}
err := x.Limit(getLimitFromPageIndex(pageIndex)).Find(&lists)
err := x.Limit(getLimitFromPageIndex(pageIndex, itemsPerPage)).Find(&lists)
{{< /highlight >}}
// TODO: Add a full example from start to finish, like a tutorial on how to create a new endpoint?

4
docs/content/doc/setup/config.md

@ -54,8 +54,8 @@ service:
# Vikunja will also look in this path for a config file, so you could provide only this variable to point to a folder
# with a config file which will then be used.
rootpath: <the path of the executable>
# The number of items which gets returned per page
pagecount: 50
# The max number of items which can be returned per page
maxitemsperpage: 50
# If set to true, enables a /metrics endpoint for prometheus to collect metrics about the system
# You'll need to use redis for this in order to enable common metrics over multiple nodes
enablemetrics: false

14
go.mod

@ -18,7 +18,7 @@ module code.vikunja.io/api
require (
cloud.google.com/go v0.34.0 // indirect
code.vikunja.io/web v0.0.0-20190628075253-b457b5a1a332
code.vikunja.io/web v0.0.0-20191023202526-f337750c3573
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
github.com/beevik/etree v1.1.0 // indirect
@ -46,10 +46,11 @@ require (
github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb
github.com/json-iterator/go v1.1.7 // indirect
github.com/kr/pty v1.1.8 // indirect
github.com/labstack/echo/v4 v4.1.7-0.20190627175217-8fb7b5be270f
github.com/labstack/gommon v0.2.9
github.com/labstack/echo/v4 v4.1.11
github.com/labstack/gommon v0.3.0
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef
github.com/mailru/easyjson v0.7.0 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/mattn/go-oci8 v0.0.0-20181130072307-052f5d97b9b6 // indirect
github.com/mattn/go-runewidth v0.0.4 // indirect
@ -73,12 +74,13 @@ require (
github.com/ugorji/go v1.1.7 // indirect
github.com/ulule/limiter/v3 v3.3.0
github.com/urfave/cli v1.22.1 // indirect
github.com/valyala/fasttemplate v1.1.0 // indirect
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
golang.org/x/lint v0.0.0-20190409202823-959b441ac422
golang.org/x/net v0.0.0-20191011234655-491137f69257 // indirect
golang.org/x/net v0.0.0-20191021144547-ec77196f6094 // indirect
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a // indirect
golang.org/x/sys v0.0.0-20191023151326-f89234f9a2c2 // indirect
golang.org/x/tools v0.0.0-20191023202404-2b779830f9d3 // indirect
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect
google.golang.org/appengine v1.5.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect

41
go.sum

@ -7,6 +7,20 @@ code.vikunja.io/web v0.0.0-20190628071027-b5c16e24b0a7 h1:P9ncMaJE7RbYqBXF9lwT0h
code.vikunja.io/web v0.0.0-20190628071027-b5c16e24b0a7/go.mod h1:cuP1/ieGWAZzgQGw+QPt6Y5F0fVb/8Ol5NV4QSezGdo=
code.vikunja.io/web v0.0.0-20190628075253-b457b5a1a332 h1:gXxyLkjhgN+vqrLvPyqyScyG5fbu44FJp61TvntWM24=
code.vikunja.io/web v0.0.0-20190628075253-b457b5a1a332/go.mod h1:cuP1/ieGWAZzgQGw+QPt6Y5F0fVb/8Ol5NV4QSezGdo=
code.vikunja.io/web v0.0.0-20191021211916-f7834b02a174 h1:hBY+r6bzGEfHxolaXbiVoz2LBNNnyHZK7d7Ga4Jowu8=
code.vikunja.io/web v0.0.0-20191021211916-f7834b02a174/go.mod h1:cuP1/ieGWAZzgQGw+QPt6Y5F0fVb/8Ol5NV4QSezGdo=
code.vikunja.io/web v0.0.0-20191022193355-23a3d145177a h1:exDC9eZ+SK0GT3zB/5f3OBahWzbTZlvX9OfZWgqlbeI=
code.vikunja.io/web v0.0.0-20191022193355-23a3d145177a/go.mod h1:cuP1/ieGWAZzgQGw+QPt6Y5F0fVb/8Ol5NV4QSezGdo=
code.vikunja.io/web v0.0.0-20191022195605-8edfc5d33c79 h1:U2px27G/b082nUu8vO21wFNKF9BM+5YQJj4XRZiyn2I=
code.vikunja.io/web v0.0.0-20191022195605-8edfc5d33c79/go.mod h1:cuP1/ieGWAZzgQGw+QPt6Y5F0fVb/8Ol5NV4QSezGdo=
code.vikunja.io/web v0.0.0-20191023144416-3ee093147b6d h1:zhNidbAwqJSnkql03i7aHDUMyQo1vM8yR1Ks495FKvc=
code.vikunja.io/web v0.0.0-20191023144416-3ee093147b6d/go.mod h1:cuP1/ieGWAZzgQGw+QPt6Y5F0fVb/8Ol5NV4QSezGdo=
code.vikunja.io/web v0.0.0-20191023145656-bce8b505205d h1:Fw5eiTr4p82l4PLaML1ARgx3fjyebxVNvPsCz727brk=
code.vikunja.io/web v0.0.0-20191023145656-bce8b505205d/go.mod h1:cuP1/ieGWAZzgQGw+QPt6Y5F0fVb/8Ol5NV4QSezGdo=
code.vikunja.io/web v0.0.0-20191023190415-502bbbbd9dfa h1:rtYKpdT/6wGgxGNFUzl9Q/AHgS778+rSC20AcBPNu/I=
code.vikunja.io/web v0.0.0-20191023190415-502bbbbd9dfa/go.mod h1:cuP1/ieGWAZzgQGw+QPt6Y5F0fVb/8Ol5NV4QSezGdo=
code.vikunja.io/web v0.0.0-20191023202526-f337750c3573 h1:q+nf3ao4vLpoAaksuk6lkRAMAcD2grOPNj/HwjejLl4=
code.vikunja.io/web v0.0.0-20191023202526-f337750c3573/go.mod h1:cuP1/ieGWAZzgQGw+QPt6Y5F0fVb/8Ol5NV4QSezGdo=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
@ -170,10 +184,14 @@ github.com/labstack/echo/v4 v4.1.6 h1:WOvLa4T1KzWCRpANwz0HGgWDelXSSGwIKtKBbFdHTv
github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE=
github.com/labstack/echo/v4 v4.1.7-0.20190627175217-8fb7b5be270f h1:fNJtR+TNyxTdYCZU40fc8Or8RyBqMOKYNv+Zay5gjvk=
github.com/labstack/echo/v4 v4.1.7-0.20190627175217-8fb7b5be270f/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE=
github.com/labstack/echo/v4 v4.1.11 h1:z0BZoArY4FqdpUEl+wlHp4hnr/oSR6MTmQmv8OHSoww=
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU=
github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4=
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef h1:RZnRnSID1skF35j/15KJ6hKZkdIC/teQClJK5wP5LU4=
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef/go.mod h1:4LATl0uhhtytR6p9n1AlktDyIz4u2iUnWEdI3L/hXiw=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
@ -193,6 +211,8 @@ github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0X
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
@ -200,6 +220,7 @@ github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-oci8 v0.0.0-20181115070430-6eefff3c767c/go.mod h1:/M9VLO+lUPmxvoOK2PfWRZ8mTtB4q1Hy9lEGijv9Nr8=
@ -312,6 +333,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
@ -326,6 +349,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI=
@ -352,6 +376,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191011234655-491137f69257 h1:ry8e2D+cwaV6hk7lb3aRTjjZo24shrbK0e11QEOkTIg=
golang.org/x/net v0.0.0-20191011234655-491137f69257/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191021144547-ec77196f6094 h1:5O4U9trLjNpuhpynaDsqwCk+Tw6seqJz1EbqbnzHrc8=
golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -375,9 +401,16 @@ golang.org/x/sys v0.0.0-20190621203818-d432491b9138 h1:t8BZD9RDjkm9/h7yYN6kE8oae
golang.org/x/sys v0.0.0-20190621203818-d432491b9138/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7 h1:HmbHVPwrPEKPGLAcHSrMe6+hqSUlvZU0rab6x5EXfGU=
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191023145028-b69606af412f h1:HNixo/W24k2W4EliZfUFl5ApIz/dMDShw52wmWfJ8/s=
golang.org/x/sys v0.0.0-20191023145028-b69606af412f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191023151326-f89234f9a2c2 h1:I7efaDQAsIQmkTF+WSdcydwVWzK07Yuz8IFF8rNkDe0=
golang.org/x/sys v0.0.0-20191023151326-f89234f9a2c2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
@ -396,6 +429,14 @@ golang.org/x/tools v0.0.0-20190628034336-212fb13d595e h1:ZlQjfVdpDxeqxRfmO30CdqW
golang.org/x/tools v0.0.0-20190628034336-212fb13d595e/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a h1:TwMENskLwU2NnWBzrJGEWHqSiGUkO/B4rfyhwqDxDYQ=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191022174149-ab6dbf99d100 h1:OT2Y8iVtXGHPODZd6iwpndJmAYRiZc75IYxlufvlkLg=
golang.org/x/tools v0.0.0-20191022174149-ab6dbf99d100/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191023143423-ff611c50cd12 h1:s9/f9YHBWfC3jIKMbJElk5+EwgC58Khn6t1EdLnQ9+k=
golang.org/x/tools v0.0.0-20191023143423-ff611c50cd12/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191023163450-98e333b8b3a3 h1:4haCIJia9wHJUU7z9f7PTC8Nf599Ok93njSCHb5gJas=
golang.org/x/tools v0.0.0-20191023163450-98e333b8b3a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191023202404-2b779830f9d3 h1:0vQisIa3mUFShxg7Xyq8WFt/ArQ1soDk5A5uF62IJCc=
golang.org/x/tools v0.0.0-20191023202404-2b779830f9d3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk=

4
pkg/config/config.go

@ -38,7 +38,7 @@ const (
ServiceFrontendurl Key = `service.frontendurl`
ServiceEnableCaldav Key = `service.enablecaldav`
ServiceRootpath Key = `service.rootpath`
ServicePageCount Key = `service.pagecount`
ServiceMaxItemsPerPage Key = `service.maxitemsperpage`
ServiceEnableMetrics Key = `service.enablemetrics`
ServiceMotd Key = `service.motd`
ServiceEnableLinkSharing Key = `service.enablelinksharing`
@ -146,7 +146,7 @@ func InitDefaultConfig() {
}
exPath := filepath.Dir(ex)
ServiceRootpath.setDefault(exPath)
ServicePageCount.setDefault(50)
ServiceMaxItemsPerPage.setDefault(50)
ServiceEnableMetrics.setDefault(false)
ServiceMotd.setDefault("")
ServiceEnableLinkSharing.setDefault(true)

17
pkg/models/label.go

@ -124,15 +124,16 @@ func (l *Label) Delete() (err error) {
// @tags labels
// @Accept json
// @Produce json
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search labels by label text."
// @Security JWTKeyAuth
// @Success 200 {array} models.Label "The labels"
// @Failure 500 {object} models.Message "Internal error"
// @Router /labels [get]
func (l *Label) ReadAll(search string, a web.Auth, page int) (ls interface{}, err error) {
func (l *Label) ReadAll(a web.Auth, search string, page int, perPage int) (ls interface{}, resultCount int, numberOfEntries int64, err error) {
if _, is := a.(*LinkSharing); is {
return nil, ErrGenericForbidden{}
return nil, 0, 0, ErrGenericForbidden{}
}
u := &User{ID: a.GetID()}
@ -140,13 +141,15 @@ func (l *Label) ReadAll(search string, a web.Auth, page int) (ls interface{}, er
// Get all tasks
taskIDs, err := getUserTaskIDs(u)
if err != nil {
return nil, err
return nil, 0, 0, err
}
return getLabelsByTaskIDs(&LabelByTaskIDsOptions{
Search: search,
User: u,
TaskIDs: taskIDs,
Page: page,
PerPage: perPage,
GetUnusedLabels: true,
GroupByLabelIDsOnly: true,
})
@ -198,15 +201,17 @@ func getLabelByIDSimple(labelID int64) (*Label, error) {
func getUserTaskIDs(u *User) (taskIDs []int64, err error) {
// Get all lists
lists, err := getRawListsForUser("", u, -1)
lists, _, _, err := getRawListsForUser("", u, -1, 0)
if err != nil {
return nil, err
}
tasks, err := getRawTasksForLists(lists, &taskOptions{
tasks, _, _, err := getRawTasksForLists(lists, &taskOptions{
startDate: time.Unix(0, 0),
endDate: time.Unix(0, 0),
sortby: SortTasksByUnsorted,
page: -1,
perPage: 0,
})
if err != nil {
return nil, err

32
pkg/models/label_task.go

@ -101,21 +101,22 @@ func (lt *LabelTask) Create(a web.Auth) (err error) {
// @Accept json
// @Produce json
// @Param task path int true "Task ID"
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search labels by label text."
// @Security JWTKeyAuth
// @Success 200 {array} models.Label "The labels"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{task}/labels [get]
func (lt *LabelTask) ReadAll(search string, a web.Auth, page int) (labels interface{}, err error) {
func (lt *LabelTask) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
// Check if the user has the right to see the task
task := Task{ID: lt.TaskID}
canRead, err := task.CanRead(a)
if err != nil {
return nil, err
return nil, 0, 0, err
}
if !canRead {
return nil, ErrNoRightToSeeTask{lt.TaskID, a.GetID()}
return nil, 0, 0, ErrNoRightToSeeTask{lt.TaskID, a.GetID()}
}
return getLabelsByTaskIDs(&LabelByTaskIDsOptions{
@ -137,6 +138,7 @@ type LabelByTaskIDsOptions struct {
User *User
Search string
Page int
PerPage int
TaskIDs []int64
GetUnusedLabels bool
GroupByLabelIDsOnly bool
@ -144,7 +146,7 @@ type LabelByTaskIDsOptions struct {
// Helper function to get all labels for a set of tasks
// Used when getting all labels for one task as well when getting all lables
func getLabelsByTaskIDs(opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, err error) {
func getLabelsByTaskIDs(opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, resultCount int, totalEntries int64, err error) {
// Include unused labels. Needed to be able to show a list of all unused labels a user
// has access to.
var uidOrNil interface{}
@ -172,10 +174,10 @@ func getLabelsByTaskIDs(opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, err
Or(builder.In("label_task.task_id", opts.TaskIDs)).
And("labels.title LIKE ?", "%"+opts.Search+"%").
GroupBy(groupBy).
Limit(getLimitFromPageIndex(opts.Page)).
Limit(getLimitFromPageIndex(opts.Page, opts.PerPage)).
Find(&labels)
if err != nil {
return nil, err
return nil, 0, 0, err
}
// Get all created by users
@ -186,7 +188,7 @@ func getLabelsByTaskIDs(opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, err
users := make(map[int64]*User)
err = x.In("id", userids).Find(&users)
if err != nil {
return nil, err
return nil, 0, 0, err
}
// Obfuscate all user emails
@ -199,7 +201,19 @@ func getLabelsByTaskIDs(opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, err
labels[in].CreatedBy = users[l.CreatedByID]
}
return labels, err
// Get the total number of entries
totalEntries, err = x.Table("labels").
Join("LEFT", "label_task", "label_task.label_id = labels.id").
Where(requestOrNil, uidOrNil).
Or(builder.In("label_task.task_id", opts.TaskIDs)).
And("labels.title LIKE ?", "%"+opts.Search+"%").
GroupBy(groupBy).
Count(&Label{})
if err != nil {
return nil, 0, 0, err
}
return labels, len(labels), totalEntries, err
}
// Create or update a bunch of task labels

2
pkg/models/label_task_test.go

@ -89,7 +89,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
gotLabels, err := l.ReadAll(tt.args.search, tt.args.a, tt.args.page)
gotLabels, _, _, err := l.ReadAll(tt.args.a, tt.args.search, tt.args.page, 0)
if (err != nil) != tt.wantErr {
t.Errorf("LabelTask.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
return

2
pkg/models/label_test.go

@ -117,7 +117,7 @@ func TestLabel_ReadAll(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
gotLs, err := l.ReadAll(tt.args.search, tt.args.a, tt.args.page)
gotLs, _, _, err := l.ReadAll(tt.args.a, tt.args.search, tt.args.page, 0)
if (err != nil) != tt.wantErr {
t.Errorf("Label.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
return

25
pkg/models/link_sharing.go

@ -136,29 +136,30 @@ func (share *LinkSharing) ReadOne() (err error) {
// @Accept json
// @Produce json
// @Param list path int true "List ID"
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search shares by hash."
// @Security JWTKeyAuth
// @Success 200 {array} models.LinkSharing "The share links"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{list}/shares [get]
func (share *LinkSharing) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
func (share *LinkSharing) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
list := &List{ID: share.ListID}
can, err := list.CanRead(a)
if err != nil {
return nil, err
return nil, 0, 0, err
}
if !can {
return nil, ErrGenericForbidden{}
return nil, 0, 0, ErrGenericForbidden{}
}
var shares []*LinkSharing
err = x.
Where("list_id = ? AND hash LIKE ?", share.ListID, "%"+search+"%").
Limit(getLimitFromPageIndex(page)).
Limit(getLimitFromPageIndex(page, perPage)).
Find(&shares)
if err != nil {
return nil, err
return nil, 0, 0, err
}
// Find all users and add them
@ -170,14 +171,22 @@ func (share *LinkSharing) ReadAll(search string, a web.Auth, page int) (interfac
users := make(map[int64]*User)
err = x.In("id", userIDs).Find(&users)
if err != nil {
return nil, err
return nil, 0, 0, err
}
for _, s := range shares {
s.SharedBy = users[s.SharedByID]
}
return shares, err
// Total count
totalItems, err = x.
Where("list_id = ? AND hash LIKE ?", share.ListID, "%"+search+"%").
Count(&LinkSharing{})
if err != nil {
return nil, 0, 0, err
}
return shares, len(shares), totalItems, err
}
// Delete removes a link share

45
pkg/models/list.go

@ -77,35 +77,36 @@ func GetListsByNamespaceID(nID int64, doer *User) (lists []*List, err error) {
// @tags list
// @Accept json
// @Produce json
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search lists by title."
// @Security JWTKeyAuth
// @Success 200 {array} models.List "The lists"
// @Failure 403 {object} code.vikunja.io/web.HTTPError "The user does not have access to the list"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists [get]
func (l *List) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
func (l *List) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
// Check if we're dealing with a share auth
shareAuth, ok := a.(*LinkSharing)
if ok {
list := &List{ID: shareAuth.ListID}
err := list.GetSimpleByID()
if err != nil {
return nil, err
return nil, 0, 0, err
}
lists := []*List{list}
err = AddListDetails(lists)
return lists, err
return lists, 0, 0, err
}
lists, err := getRawListsForUser(search, &User{ID: a.GetID()}, page)
lists, resultCount, totalItems, err := getRawListsForUser(search, &User{ID: a.GetID()}, page, perPage)
if err != nil {
return nil, err
return nil, 0, 0, err
}
// Add more list details
err = AddListDetails(lists)
return lists, err
return lists, resultCount, totalItems, err
}
// ReadOne gets one list by its ID
@ -177,10 +178,10 @@ func GetListSimplByTaskID(taskID int64) (l *List, err error) {
}
// Gets the lists only, without any tasks or so
func getRawListsForUser(search string, u *User, page int) (lists []*List, err error) {
func getRawListsForUser(search string, u *User, page int, perPage int) (lists []*List, resultCount int, totalItems int64, err error) {
fullUser, err := GetUserByID(u.ID)
if err != nil {
return lists, err
return nil, 0, 0, err
}
// Gets all Lists where the user is either owner or in a team which has access to the list
@ -201,11 +202,33 @@ func getRawListsForUser(search string, u *User, page int) (lists []*List, err er
Or("ul.user_id = ?", fullUser.ID).
Or("un.user_id = ?", fullUser.ID).
GroupBy("l.id").
Limit(getLimitFromPageIndex(page)).
Limit(getLimitFromPageIndex(page, perPage)).
Where("l.title LIKE ?", "%"+search+"%").
Find(&lists)
if err != nil {
return nil, 0, 0, err
}
return lists, err
totalItems, err = x.
Table("list").
Alias("l").
Join("INNER", []string{"namespaces", "n"}, "l.namespace_id = n.id").
Join("LEFT", []string{"team_namespaces", "tn"}, "tn.namespace_id = n.id").
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
Join("LEFT", []string{"users_namespace", "un"}, "un.namespace_id = l.namespace_id").
Where("tm.user_id = ?", fullUser.ID).
Or("tm2.user_id = ?", fullUser.ID).
Or("l.owner_id = ?", fullUser.ID).
Or("ul.user_id = ?", fullUser.ID).
Or("un.user_id = ?", fullUser.ID).
GroupBy("l.id").
Limit(getLimitFromPageIndex(page, perPage)).
Where("l.title LIKE ?", "%"+search+"%").
Count(&List{})
return lists, len(lists), totalItems, err
}
// AddListDetails adds owner user objects and list tasks to all lists in the slice

5
pkg/models/list_read_test.go

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

26
pkg/models/list_team.go

@ -154,22 +154,23 @@ func (tl *TeamList) Delete() (err error) {
// @Accept json
// @Produce json
// @Param id path int true "List ID"
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search teams by its name."
// @Security JWTKeyAuth
// @Success 200 {array} models.TeamWithRight "The teams with their right."
// @Failure 403 {object} code.vikunja.io/web.HTTPError "No right to see the list."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id}/teams [get]
func (tl *TeamList) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
func (tl *TeamList) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
// Check if the user can read the namespace
l := &List{ID: tl.ListID}
canRead, err := l.CanRead(a)
if err != nil {
return nil, err
return nil, 0, 0, err
}
if !canRead {
return nil, ErrNeedToHaveListReadAccess{ListID: tl.ListID, UserID: a.GetID()}
return nil, 0, 0, ErrNeedToHaveListReadAccess{ListID: tl.ListID, UserID: a.GetID()}
}
// Get the teams
@ -178,11 +179,24 @@ func (tl *TeamList) ReadAll(search string, a web.Auth, page int) (interface{}, e
Table("teams").
Join("INNER", "team_list", "team_id = teams.id").
Where("team_list.list_id = ?", tl.ListID).
Limit(getLimitFromPageIndex(page)).
Limit(getLimitFromPageIndex(page, perPage)).
Where("teams.name LIKE ?", "%"+search+"%").
Find(&all)
if err != nil {
return nil, 0, 0, err
}
totalItems, err = x.
Table("teams").
Join("INNER", "team_list", "team_id = teams.id").
Where("team_list.list_id = ?", tl.ListID).
Where("teams.name LIKE ?", "%"+search+"%").
Count(&TeamWithRight{})
if err != nil {
return nil, 0, 0, err
}
return all, err
return all, len(all), totalItems, err
}
// Update updates a team <-> list relation

8
pkg/models/list_team_test.go

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

22
pkg/models/list_users.go

@ -159,22 +159,23 @@ func (lu *ListUser) Delete() (err error) {
// @Accept json
// @Produce json
// @Param id path int true "List ID"
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search users by its name."
// @Security JWTKeyAuth
// @Success 200 {array} models.UserWithRight "The users with the right they have."
// @Failure 403 {object} code.vikunja.io/web.HTTPError "No right to see the list."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id}/users [get]
func (lu *ListUser) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
func (lu *ListUser) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
// Check if the user has access to the list
l := &List{ID: lu.ListID}
canRead, err := l.CanRead(a)
if err != nil {
return nil, err
return nil, 0, 0, err
}
if !canRead {
return nil, ErrNeedToHaveListReadAccess{UserID: a.GetID(), ListID: lu.ListID}
return nil, 0, 0, ErrNeedToHaveListReadAccess{UserID: a.GetID(), ListID: lu.ListID}
}
// Get all users
@ -182,16 +183,25 @@ func (lu *ListUser) ReadAll(search string, a web.Auth, page int) (interface{}, e
err = x.
Join("INNER", "users_list", "user_id = users.id").
Where("users_list.list_id = ?", lu.ListID).
Limit(getLimitFromPageIndex(page)).
Limit(getLimitFromPageIndex(page, perPage)).
Where("users.username LIKE ?", "%"+search+"%").
Find(&all)
if err != nil {
return nil, 0, 0, err
}
// Obfuscate all user emails
for _, u := range all {
u.Email = ""
}
return all, err
numberOfTotalItems, err = x.
Join("INNER", "users_list", "user_id = users.id").
Where("users_list.list_id = ?", lu.ListID).
Where("users.username LIKE ?", "%"+search+"%").
Count(&UserWithRight{})
return all, len(all), numberOfTotalItems, err
}
// Update updates a user <-> list relation

2
pkg/models/list_users_test.go

@ -203,7 +203,7 @@ func TestListUser_ReadAll(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
got, err := ul.ReadAll(tt.args.search, tt.args.a, tt.args.page)
got, _, _, err := ul.ReadAll(tt.args.a, tt.args.search, tt.args.page, 50)
if (err != nil) != tt.wantErr {
t.Errorf("ListUser.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
}

8
pkg/models/models.go

@ -69,14 +69,18 @@ func SetEngine() (err error) {
return nil
}
func getLimitFromPageIndex(page int) (limit, start int) {
func getLimitFromPageIndex(page int, perPage int) (limit, start int) {
// Get everything when page index is -1
if page < 0 {
return 0, 0
}
limit = config.ServicePageCount.GetInt()
limit = config.ServiceMaxItemsPerPage.GetInt()
if perPage > 0 {
limit = perPage
}
start = limit * (page - 1)
return
}

42
pkg/models/namespace.go

@ -131,20 +131,21 @@ type NamespaceWithLists struct {
// @tags namespace
// @Accept json
// @Produce json
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search namespaces by name."
// @Security JWTKeyAuth
// @Success 200 {array} models.NamespaceWithLists "The Namespaces."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces [get]
func (n *Namespace) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
if _, is := a.(*LinkSharing); is {
return nil, ErrGenericForbidden{}
return nil, 0, 0, ErrGenericForbidden{}
}
doer, err := getUserWithError(a)
if err != nil {
return nil, err
return nil, 0, 0, err
}
all := []*NamespaceWithLists{}
@ -167,11 +168,11 @@ func (n *Namespace) ReadAll(search string, a web.Auth, page int) (interface{}, e
Or("namespaces.owner_id = ?", doer.ID).
Or("users_namespace.user_id = ?", doer.ID).
GroupBy("namespaces.id").
Limit(getLimitFromPageIndex(page)).
Limit(getLimitFromPageIndex(page, perPage)).
Where("namespaces.name LIKE ?", "%"+search+"%").
Find(&all)
if err != nil {
return all, err
return all, 0, 0, err
}
// Get all users
@ -187,7 +188,7 @@ func (n *Namespace) ReadAll(search string, a web.Auth, page int) (interface{}, e
Find(&users)
if err != nil {
return all, err
return all, 0, 0, err
}
// Make a list of namespace ids
@ -202,7 +203,7 @@ func (n *Namespace) ReadAll(search string, a web.Auth, page int) (interface{}, e
In("namespace_id", namespaceids).
Find(&lists)
if err != nil {
return all, err
return all, 0, 0, err
}
// Get all lists individually shared with our user (not via a namespace)
@ -218,7 +219,7 @@ func (n *Namespace) ReadAll(search string, a web.Auth, page int) (interface{}, e
GroupBy("l.id").
Find(&individualLists)
if err != nil {
return nil, err
return nil, 0, 0, err
}
// Make the namespace -1 so we now later which one it was
@ -234,9 +235,13 @@ func (n *Namespace) ReadAll(search string, a web.Auth, page int) (interface{}, e
}
// More details for the lists
AddListDetails(lists)
err = AddListDetails(lists)
if err != nil {
return nil, 0, 0, err
}
// Put objects in our namespace list
// TODO: Refactor this to use maps for better efficiency
for i, n := range all {
// Users
@ -255,7 +260,22 @@ func (n *Namespace) ReadAll(search string, a web.Auth, page int) (interface{}, e
}
}
return all, nil
numberOfTotalItems, err = x.
Table("namespaces").
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
Join("LEFT", "users_namespace", "users_namespace.namespace_id = namespaces.id").
Where("team_members.user_id = ?", doer.ID).
Or("namespaces.owner_id = ?", doer.ID).
Or("users_namespace.user_id = ?", doer.ID).
GroupBy("namespaces.id").
Where("namespaces.name LIKE ?", "%"+search+"%").
Count(&NamespaceWithLists{})
if err != nil {
return all, 0, 0, err
}
return all, len(all), numberOfTotalItems, nil
}
// Create implements the creation method via the interface

22
pkg/models/namespace_team.go

@ -139,22 +139,23 @@ func (tn *TeamNamespace) Delete() (err error) {
// @Accept json
// @Produce json
// @Param id path int true "Namespace ID"
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search teams by its name."
// @Security JWTKeyAuth
// @Success 200 {array} models.TeamWithRight "The teams with the right they have."
// @Failure 403 {object} code.vikunja.io/web.HTTPError "No right to see the namespace."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{id}/teams [get]
func (tn *TeamNamespace) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
func (tn *TeamNamespace) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
// Check if the user can read the namespace
n := Namespace{ID: tn.NamespaceID}
canRead, err := n.CanRead(a)
if err != nil {
return nil, err
return nil, 0, 0, err
}
if !canRead {
return nil, ErrNeedToHaveNamespaceReadAccess{NamespaceID: tn.NamespaceID, UserID: a.GetID()}
return nil, 0, 0, ErrNeedToHaveNamespaceReadAccess{NamespaceID: tn.NamespaceID, UserID: a.GetID()}
}
// Get the teams
@ -163,11 +164,20 @@ func (tn *TeamNamespace) ReadAll(search string, a web.Auth, page int) (interface
err = x.Table("teams").
Join("INNER", "team_namespaces", "team_id = teams.id").
Where("team_namespaces.namespace_id = ?", tn.NamespaceID).
Limit(getLimitFromPageIndex(page)).
Limit(getLimitFromPageIndex(page, perPage)).
Where("teams.name LIKE ?", "%"+search+"%").
Find(&all)
if err != nil {
return nil, 0, 0, err
}
numberOfTotalItems, err = x.Table("teams").
Join("INNER", "team_namespaces", "team_id = teams.id").
Where("team_namespaces.namespace_id = ?", tn.NamespaceID).
Where("teams.name LIKE ?", "%"+search+"%").
Count(&TeamWithRight{})
return all, err
return all, len(all), numberOfTotalItems, err
}
// Update updates a team <-> namespace relation

6
pkg/models/namespace_team_test.go

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

2
pkg/models/namespace_test.go

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

22
pkg/models/namespace_users.go

@ -145,22 +145,23 @@ func (nu *NamespaceUser) Delete() (err error) {
// @Accept json
// @Produce json
// @Param id path int true "Namespace ID"
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search users by its name."
// @Security JWTKeyAuth
// @Success 200 {array} models.UserWithRight "The users with the right they have."
// @Failure 403 {object} code.vikunja.io/web.HTTPError "No right to see the namespace."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{id}/users [get]
func (nu *NamespaceUser) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
func (nu *NamespaceUser) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
// Check if the user has access to the namespace
l := Namespace{ID: nu.NamespaceID}
canRead, err := l.CanRead(a)
if err != nil {
return nil, err
return nil, 0, 0, err
}
if !canRead {
return nil, ErrNeedToHaveNamespaceReadAccess{}
return nil, 0, 0, ErrNeedToHaveNamespaceReadAccess{}
}
// Get all users
@ -168,16 +169,25 @@ func (nu *NamespaceUser) ReadAll(search string, a web.Auth, page int) (interface
err = x.
Join("INNER", "users_namespace", "user_id = users.id").
Where("users_namespace.namespace_id = ?", nu.NamespaceID).
Limit(getLimitFromPageIndex(page)).
Limit(getLimitFromPageIndex(page, perPage)).
Where("users.username LIKE ?", "%"+search+"%").
Find(&all)
if err != nil {
return nil, 0, 0, err
}
// Obfuscate all user emails
for _, u := range all {
u.Email = ""
}
return all, err
numberOfTotalItems, err = x.
Join("INNER", "users_namespace", "user_id = users.id").
Where("users_namespace.namespace_id = ?", nu.NamespaceID).
Where("users.username LIKE ?", "%"+search+"%").
Count(&UserWithRight{})
return all, len(all), numberOfTotalItems, err
}
// Update updates a user <-> namespace relation

2
pkg/models/namespace_users_test.go

@ -202,7 +202,7 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
got, err := un.ReadAll(tt.args.search, tt.args.a, tt.args.page)
got, _, _, err := un.ReadAll(tt.args.a, tt.args.search, tt.args.page, 50)
if (err != nil) != tt.wantErr {
t.Errorf("NamespaceUser.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
return

24
pkg/models/task_assignees.go

@ -219,25 +219,26 @@ func (t *Task) addNewAssigneeByID(newAssigneeID int64, list *List) (err error) {
// @tags assignees
// @Accept json
// @Produce json
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search assignees by their username."
// @Param taskID path int true "Task ID"
// @Security JWTKeyAuth
// @Success 200 {array} models.User "The assignees"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/assignees [get]
func (la *TaskAssginee) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
func (la *TaskAssginee) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
task, err := GetListSimplByTaskID(la.TaskID)
if err != nil {
return nil, err
return nil, 0, 0, err
}
can, err := task.CanRead(a)
if err != nil {
return nil, err
return nil, 0, 0, err
}
if !can {
return nil, ErrGenericForbidden{}
return nil, 0, 0, ErrGenericForbidden{}
}
var taskAssignees []*User
@ -245,9 +246,18 @@ func (la *TaskAssginee) ReadAll(search string, a web.Auth, page int) (interface{
Select("users.*").
Join("INNER", "users", "task_assignees.user_id = users.id").
Where("task_id = ? AND users.username LIKE ?", la.TaskID, "%"+search+"%").
Limit(getLimitFromPageIndex(page)).
Limit(getLimitFromPageIndex(page, perPage)).
Find(&taskAssignees)
return taskAssignees, err
if err != nil {
return nil, 0, 0, err
}
numberOfTotalItems, err = x.Table("task_assignees").
Select("users.*").
Join("INNER", "users", "task_assignees.user_id = users.id").
Where("task_id = ? AND users.username LIKE ?", la.TaskID, "%"+search+"%").
Count(&User{})
return taskAssignees, len(taskAssignees), numberOfTotalItems, err
}
// BulkAssignees is a helper struct used to update multiple assignees at once.

19
pkg/models/task_attachment.go

@ -100,21 +100,23 @@ func (ta *TaskAttachment) ReadOne() (err error) {
// @Accept json
// @Produce json
// @Param id path int true "Task ID"
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Security JWTKeyAuth
// @Success 200 {array} models.TaskAttachment "All attachments for this task"
// @Failure 403 {object} models.Message "No access to this task."
// @Failure 404 {object} models.Message "The task does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{id}/attachments [get]
func (ta *TaskAttachment) ReadAll(s string, a web.Auth, page int) (interface{}, error) {
func (ta *TaskAttachment) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
attachments := []*TaskAttachment{}
err := x.
Limit(getLimitFromPageIndex(page)).
err = x.
Limit(getLimitFromPageIndex(page, perPage)).
Where("task_id = ?", ta.TaskID).
Find(&attachments)
if err != nil {
return nil, err
return nil, 0, 0, err
}
fileIDs := make([]int64, 0, len(attachments))
@ -127,13 +129,13 @@ func (ta *TaskAttachment) ReadAll(s string, a web.Auth, page int) (interface{},
fs := make(map[int64]*files.File)
err = x.In("id", fileIDs).Find(&fs)
if err != nil {
return nil, err
return nil, 0, 0, err
}
us := make(map[int64]*User)
err = x.In("id", userIDs).Find(&us)
if err != nil {
return nil, err
return nil, 0, 0, err
}
for _, r := range attachments {
@ -146,7 +148,10 @@ func (ta *TaskAttachment) ReadAll(s string, a web.Auth, page int) (interface{},
r.CreatedBy = us[r.CreatedByID]
}
return attachments, err
numberOfTotalItems, err = x.
Where("task_id = ?", ta.TaskID).
Count(&TaskAttachment{})
return attachments, len(attachments), numberOfTotalItems, err
}
// Delete removes an attachment

2
pkg/models/task_attachment_test.go

@ -119,7 +119,7 @@ func TestTaskAttachment_NewAttachment(t *testing.T) {
func TestTaskAttachment_ReadAll(t *testing.T) {
files.InitTestFileFixtures(t)
ta := &TaskAttachment{TaskID: 1}
as, err := ta.ReadAll("", &User{ID: 1}, 0)
as, _, _, err := ta.ReadAll(&User{ID: 1}, "", 0, 50)
attachments, _ := as.([]*TaskAttachment)
assert.NoError(t, err)
assert.Len(t, attachments, 3)

2
pkg/models/task_readall_test.go

@ -728,7 +728,7 @@ func TestTask_ReadAll(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
got, err := lt.ReadAll(tt.args.search, tt.args.a, tt.args.page)
got, _, _, err := lt.ReadAll(tt.args.a, tt.args.search, tt.args.page, 50)
if (err != nil) != tt.wantErr {
t.Errorf("Test %s, Task.ReadAll() error = %v, wantErr %v", tt.name, err, tt.wantErr)
return

65
pkg/models/tasks.go

@ -123,7 +123,8 @@ const (
// @tags task
// @Accept json
// @Produce json
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search tasks by task text."
// @Param sort query string false "The sorting parameter. Possible values to sort by are priority, prioritydesc, priorityasc, duedate, duedatedesc, duedateasc."
// @Param startdate query int false "The start date parameter to filter by. Expects a unix timestamp. If no end date, but a start date is specified, the end date is set to the current time."
@ -132,7 +133,7 @@ const (
// @Success 200 {array} models.Task "The tasks"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/all [get]
func (t *Task) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
func (t *Task) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
var sortby SortBy
switch t.Sorting {
case "priority":
@ -156,6 +157,8 @@ func (t *Task) ReadAll(search string, a web.Auth, page int) (interface{}, error)
sortby: sortby,
startDate: time.Unix(t.StartDateSortUnix, 0),
endDate: time.Unix(t.EndDateSortUnix, 0),
page: page,
perPage: perPage,
}
shareAuth, is := a.(*LinkSharing)
@ -163,15 +166,15 @@ func (t *Task) ReadAll(search string, a web.Auth, page int) (interface{}, error)
list := &List{ID: shareAuth.ListID}
err := list.GetSimpleByID()
if err != nil {
return nil, err
return nil, 0, 0, err
}
return getTasksForLists([]*List{list}, taskopts)
}
// Get all lists for the user
lists, err := getRawListsForUser("", &User{ID: a.GetID()}, page)
lists, _, _, err := getRawListsForUser("", &User{ID: a.GetID()}, -1, 0)
if err != nil {
return nil, err
return nil, 0, 0, err
}
return getTasksForLists(lists, taskopts)
@ -182,9 +185,11 @@ type taskOptions struct {
sortby SortBy
startDate time.Time
endDate time.Time
page int
perPage int
}
func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*Task, err error) {
func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*Task, resultCount int, totalItems int64, err error) {
// Get all list IDs and get the tasks
var listIDs []int64
@ -219,42 +224,62 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*T
endDateUnix = opts.endDate.Unix()
}
if err := x.In("list_id", listIDs).
err := x.In("list_id", listIDs).
Where("text LIKE ?", "%"+opts.search+"%").
And("((due_date_unix BETWEEN ? AND ?) OR "+
"(start_date_unix BETWEEN ? and ?) OR "+
"(end_date_unix BETWEEN ? and ?))", startDateUnix, endDateUnix, startDateUnix, endDateUnix, startDateUnix, endDateUnix).
OrderBy(orderby).
Find(&taskMap); err != nil {
return nil, err
Limit(getLimitFromPageIndex(opts.page, opts.perPage)).
Find(&taskMap)
if err != nil {
return nil, 0, 0, err
}
totalItems, err = x.In("list_id", listIDs).
Where("text LIKE ?", "%"+opts.search+"%").
And("((due_date_unix BETWEEN ? AND ?) OR "+
"(start_date_unix BETWEEN ? and ?) OR "+
"(end_date_unix BETWEEN ? and ?))", startDateUnix, endDateUnix, startDateUnix, endDateUnix, startDateUnix, endDateUnix).
Count(&Task{})
if err != nil {
return nil, 0, 0, err
}
} else {
if err := x.In("list_id", listIDs).
err := x.In("list_id", listIDs).
Where("text LIKE ?", "%"+opts.search+"%").
OrderBy(orderby).
Find(&taskMap); err != nil {
return nil, err
Limit(getLimitFromPageIndex(opts.page, opts.perPage)).
Find(&taskMap)
if err != nil {
return nil, 0, 0, err
}
totalItems, err = x.In("list_id", listIDs).
Where("text LIKE ?", "%"+opts.search+"%").
Count(&Task{})
if err != nil {
return nil, 0, 0, err
}
}
return
return taskMap, len(taskMap),