From 466b2b676c9b8a9bef6019be7e3a528f9b8a8863 Mon Sep 17 00:00:00 2001 From: konrad Date: Wed, 10 Mar 2021 10:59:10 +0000 Subject: [PATCH] Pagingation for tasks in kanban buckets (#805) Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/api/pulls/805 Co-authored-by: konrad Co-committed-by: konrad --- pkg/models/kanban.go | 66 +++++++++++++++++++---- pkg/models/kanban_test.go | 2 +- pkg/models/task_collection.go | 85 ++++++++++++++++-------------- pkg/models/task_collection_sort.go | 1 + pkg/models/tasks.go | 6 +-- pkg/swagger/docs.go | 18 +++++++ pkg/swagger/swagger.json | 18 +++++++ pkg/swagger/swagger.yaml | 12 +++++ 8 files changed, 155 insertions(+), 53 deletions(-) diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 264114fd1d9..1c19e76d675 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -89,6 +89,9 @@ func getDefaultBucket(s *xorm.Session, listID int64) (bucket *Bucket, err error) // @Produce json // @Security JWTKeyAuth // @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_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`" @@ -99,9 +102,6 @@ func getDefaultBucket(s *xorm.Session, listID int64) (bucket *Bucket, err error) // @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) { - // 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 buckets := []*Bucket{} 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] } - // Get all tasks for this list - b.TaskCollection.ListID = b.ListID - b.TaskCollection.OrderBy = []string{string(orderAscending)} - b.TaskCollection.SortBy = []string{taskPropertyPosition} - ts, _, _, err := b.TaskCollection.ReadAll(s, auth, "", -1, 0) + tasks := []*Task{} + + opts, err := getTaskFilterOptsFromCollection(&b.TaskCollection) 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 // All tasks which are not associated to any bucket will have bucket id 0 which is the nil value for int64 diff --git a/pkg/models/kanban_test.go b/pkg/models/kanban_test.go index a3add2c4f8b..450afbe3c45 100644 --- a/pkg/models/kanban_test.go +++ b/pkg/models/kanban_test.go @@ -82,7 +82,7 @@ func TestBucket_ReadAll(t *testing.T) { FilterValue: []string{"done"}, }, } - bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", 0, 0) + bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", -1, 0) assert.NoError(t, err) buckets := bucketsInterface.([]*Bucket) diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index a5403d8169a..3b749dbc757 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -72,13 +72,52 @@ func validateTaskField(fieldName string) error { taskPropertyUID, taskPropertyCreated, taskPropertyUpdated, - taskPropertyPosition: + taskPropertyPosition, + taskPropertyBucketID: return nil } 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 // @Summary Get tasks in a 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) } - 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, 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) + taskopts, err := getTaskFilterOptsFromCollection(tf) if err != nil { - return + return nil, 0, 0, err } + taskopts.search = search + taskopts.page = page + taskopts.perPage = perPage + shareAuth, is := a.(*LinkSharing) if is { list, err := GetListSimpleByID(s, shareAuth.ListID) diff --git a/pkg/models/task_collection_sort.go b/pkg/models/task_collection_sort.go index 1deaf4da626..1cea9230969 100644 --- a/pkg/models/task_collection_sort.go +++ b/pkg/models/task_collection_sort.go @@ -44,6 +44,7 @@ const ( taskPropertyCreated string = "created" taskPropertyUpdated string = "updated" taskPropertyPosition string = "position" + taskPropertyBucketID string = "bucket_id" ) const ( diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 8732dcebc11..b7045317cec 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -306,7 +306,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO continue } - if f.field == "assignees" { + if f.field == "assignees" || f.field == "user_id" { f.field = "user_id" filter, err := getFilterCond(f, opts.filterIncludeNulls) if err != nil { @@ -316,7 +316,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO continue } - if f.field == "labels" { + if f.field == "labels" || f.field == "label_id" { f.field = "label_id" filter, err := getFilterCond(f, opts.filterIncludeNulls) if err != nil { @@ -326,7 +326,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO continue } - if f.field == "namespace" { + if f.field == "namespace" || f.field == "namespace_id" { f.field = "namespace_id" filter, err := getFilterCond(f, opts.filterIncludeNulls) if err != nil { diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 29f71362fbf..d2900bbb9ea 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1223,6 +1223,24 @@ var doc = `{ "in": "path", "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", "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.", diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 3137dac4d1e..79e53cb4d90 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -1206,6 +1206,24 @@ "in": "path", "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", "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.", diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 0df8e9df2f0..c638800941a 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -1963,6 +1963,18 @@ paths: name: id required: true 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. in: query name: filter_by