Pagingation for tasks in kanban buckets (#805)

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/805
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
konrad 2021-03-10 10:59:10 +00:00
parent 89d0fbcc7c
commit 466b2b676c
8 changed files with 155 additions and 53 deletions

View file

@ -89,6 +89,9 @@ func getDefaultBucket(s *xorm.Session, listID int64) (bucket *Bucket, err error)
// @Produce json // @Produce json
// @Security JWTKeyAuth // @Security JWTKeyAuth
// @Param id path int true "List Id" // @Param id path int true "List Id"
// @Param page query int false "The page number for tasks. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search tasks by task text."
// @Param filter_by query string false "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match." // @Param filter_by query string false "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match."
// @Param filter_value query string false "The value to filter for." // @Param filter_value query string false "The value to filter for."
// @Param filter_comparator query string false "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals`" // @Param filter_comparator query string false "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals`"
@ -99,9 +102,6 @@ func getDefaultBucket(s *xorm.Session, listID int64) (bucket *Bucket, err error)
// @Router /lists/{id}/buckets [get] // @Router /lists/{id}/buckets [get]
func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
// Note: I'm ignoring pagination for now since I've yet to figure out a way on how to make it work
// I'll probably just don't do it and instead make individual tasks archivable.
// Get all buckets for this list // Get all buckets for this list
buckets := []*Bucket{} buckets := []*Bucket{}
err = s.Where("list_id = ?", b.ListID).Find(&buckets) err = s.Where("list_id = ?", b.ListID).Find(&buckets)
@ -130,16 +130,62 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
bb.CreatedBy = users[bb.CreatedByID] bb.CreatedBy = users[bb.CreatedByID]
} }
// Get all tasks for this list tasks := []*Task{}
b.TaskCollection.ListID = b.ListID
b.TaskCollection.OrderBy = []string{string(orderAscending)} opts, err := getTaskFilterOptsFromCollection(&b.TaskCollection)
b.TaskCollection.SortBy = []string{taskPropertyPosition}
ts, _, _, err := b.TaskCollection.ReadAll(s, auth, "", -1, 0)
if err != nil { if err != nil {
return return nil, 0, 0, err
} }
tasks := ts.([]*Task) opts.sortby = []*sortParam{
{
orderBy: orderAscending,
sortBy: taskPropertyPosition,
},
}
opts.page = page
opts.perPage = perPage
opts.search = search
opts.filterConcat = filterConcatAnd
var bucketFilterIndex int
for i, filter := range opts.filters {
if filter.field == taskPropertyBucketID {
bucketFilterIndex = i
break
}
}
if bucketFilterIndex == 0 {
opts.filters = append(opts.filters, &taskFilter{
field: taskPropertyBucketID,
value: 0,
comparator: taskFilterComparatorEquals,
})
bucketFilterIndex = len(opts.filters) - 1
}
for id, bucket := range bucketMap {
opts.filters[bucketFilterIndex].value = id
ts, _, _, err := getRawTasksForLists(s, []*List{{ID: bucket.ListID}}, auth, opts)
if err != nil {
return nil, 0, 0, err
}
tasks = append(tasks, ts...)
}
taskMap := make(map[int64]*Task, len(tasks))
for _, t := range tasks {
taskMap[t.ID] = t
}
err = addMoreInfoToTasks(s, taskMap)
if err != nil {
return nil, 0, 0, err
}
// Put all tasks in their buckets // Put all tasks in their buckets
// All tasks which are not associated to any bucket will have bucket id 0 which is the nil value for int64 // All tasks which are not associated to any bucket will have bucket id 0 which is the nil value for int64

View file

@ -82,7 +82,7 @@ func TestBucket_ReadAll(t *testing.T) {
FilterValue: []string{"done"}, FilterValue: []string{"done"},
}, },
} }
bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", 0, 0) bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", -1, 0)
assert.NoError(t, err) assert.NoError(t, err)
buckets := bucketsInterface.([]*Bucket) buckets := bucketsInterface.([]*Bucket)

View file

@ -72,13 +72,52 @@ func validateTaskField(fieldName string) error {
taskPropertyUID, taskPropertyUID,
taskPropertyCreated, taskPropertyCreated,
taskPropertyUpdated, taskPropertyUpdated,
taskPropertyPosition: taskPropertyPosition,
taskPropertyBucketID:
return nil return nil
} }
return ErrInvalidTaskField{TaskField: fieldName} return ErrInvalidTaskField{TaskField: fieldName}
} }
func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskOptions, err error) {
if len(tf.SortByArr) > 0 {
tf.SortBy = append(tf.SortBy, tf.SortByArr...)
}
if len(tf.OrderByArr) > 0 {
tf.OrderBy = append(tf.OrderBy, tf.OrderByArr...)
}
var sort = make([]*sortParam, 0, len(tf.SortBy))
for i, s := range tf.SortBy {
param := &sortParam{
sortBy: s,
orderBy: orderAscending,
}
// This checks if tf.OrderBy has an entry with the same index as the current entry from tf.SortBy
// Taken from https://stackoverflow.com/a/27252199/10924593
if len(tf.OrderBy) > i {
param.orderBy = getSortOrderFromString(tf.OrderBy[i])
}
// Param validation
if err := param.validate(); err != nil {
return nil, err
}
sort = append(sort, param)
}
opts = &taskOptions{
sortby: sort,
filterConcat: taskFilterConcatinator(tf.FilterConcat),
filterIncludeNulls: tf.FilterIncludeNulls,
}
opts.filters, err = getTaskFiltersByCollections(tf)
return opts, err
}
// ReadAll gets all tasks for a collection // ReadAll gets all tasks for a collection
// @Summary Get tasks in a list // @Summary Get tasks in a list
// @Description Returns all tasks for the current list. // @Description Returns all tasks for the current list.
@ -113,47 +152,15 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
return sf.getTaskCollection().ReadAll(s, a, search, page, perPage) return sf.getTaskCollection().ReadAll(s, a, search, page, perPage)
} }
if len(tf.SortByArr) > 0 { taskopts, err := getTaskFilterOptsFromCollection(tf)
tf.SortBy = append(tf.SortBy, tf.SortByArr...)
}
if len(tf.OrderByArr) > 0 {
tf.OrderBy = append(tf.OrderBy, tf.OrderByArr...)
}
var sort = make([]*sortParam, 0, len(tf.SortBy))
for i, s := range tf.SortBy {
param := &sortParam{
sortBy: s,
orderBy: orderAscending,
}
// This checks if tf.OrderBy has an entry with the same index as the current entry from tf.SortBy
// Taken from https://stackoverflow.com/a/27252199/10924593
if len(tf.OrderBy) > i {
param.orderBy = getSortOrderFromString(tf.OrderBy[i])
}
// Param validation
if err := param.validate(); err != nil {
return nil, 0, 0, err
}
sort = append(sort, param)
}
taskopts := &taskOptions{
search: search,
page: page,
perPage: perPage,
sortby: sort,
filterConcat: taskFilterConcatinator(tf.FilterConcat),
filterIncludeNulls: tf.FilterIncludeNulls,
}
taskopts.filters, err = getTaskFiltersByCollections(tf)
if err != nil { if err != nil {
return return nil, 0, 0, err
} }
taskopts.search = search
taskopts.page = page
taskopts.perPage = perPage
shareAuth, is := a.(*LinkSharing) shareAuth, is := a.(*LinkSharing)
if is { if is {
list, err := GetListSimpleByID(s, shareAuth.ListID) list, err := GetListSimpleByID(s, shareAuth.ListID)

View file

@ -44,6 +44,7 @@ const (
taskPropertyCreated string = "created" taskPropertyCreated string = "created"
taskPropertyUpdated string = "updated" taskPropertyUpdated string = "updated"
taskPropertyPosition string = "position" taskPropertyPosition string = "position"
taskPropertyBucketID string = "bucket_id"
) )
const ( const (

View file

@ -306,7 +306,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
continue continue
} }
if f.field == "assignees" { if f.field == "assignees" || f.field == "user_id" {
f.field = "user_id" f.field = "user_id"
filter, err := getFilterCond(f, opts.filterIncludeNulls) filter, err := getFilterCond(f, opts.filterIncludeNulls)
if err != nil { if err != nil {
@ -316,7 +316,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
continue continue
} }
if f.field == "labels" { if f.field == "labels" || f.field == "label_id" {
f.field = "label_id" f.field = "label_id"
filter, err := getFilterCond(f, opts.filterIncludeNulls) filter, err := getFilterCond(f, opts.filterIncludeNulls)
if err != nil { if err != nil {
@ -326,7 +326,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
continue continue
} }
if f.field == "namespace" { if f.field == "namespace" || f.field == "namespace_id" {
f.field = "namespace_id" f.field = "namespace_id"
filter, err := getFilterCond(f, opts.filterIncludeNulls) filter, err := getFilterCond(f, opts.filterIncludeNulls)
if err != nil { if err != nil {

View file

@ -1223,6 +1223,24 @@ var doc = `{
"in": "path", "in": "path",
"required": true "required": true
}, },
{
"type": "integer",
"description": "The page number for tasks. Used for pagination. If not provided, the first page of results is returned.",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page.",
"name": "per_page",
"in": "query"
},
{
"type": "string",
"description": "Search tasks by task text.",
"name": "s",
"in": "query"
},
{ {
"type": "string", "type": "string",
"description": "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.", "description": "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.",

View file

@ -1206,6 +1206,24 @@
"in": "path", "in": "path",
"required": true "required": true
}, },
{
"type": "integer",
"description": "The page number for tasks. Used for pagination. If not provided, the first page of results is returned.",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page.",
"name": "per_page",
"in": "query"
},
{
"type": "string",
"description": "Search tasks by task text.",
"name": "s",
"in": "query"
},
{ {
"type": "string", "type": "string",
"description": "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.", "description": "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.",

View file

@ -1963,6 +1963,18 @@ paths:
name: id name: id
required: true required: true
type: integer type: integer
- description: The page number for tasks. Used for pagination. If not provided, the first page of results is returned.
in: query
name: page
type: integer
- description: The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page.
in: query
name: per_page
type: integer
- description: Search tasks by task text.
in: query
name: s
type: string
- description: The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match. - description: The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.
in: query in: query
name: filter_by name: filter_by