From 2d4e2e452c6da0d3edb3a4f986d02dd3fbef0b64 Mon Sep 17 00:00:00 2001 From: konrad Date: Mon, 21 Dec 2020 23:13:15 +0000 Subject: [PATCH] Add task filter for lists and namespaces (#748) Add more tests for getting namespaces Fix namespaces not found Fix namespaces not found Make like the default Update docs & fix docs Enable searching namespaces by their ids Enable searching lists by their ids Enable searching labels by their ids Enable searching by user ids Update docs Add namespace filter Add task filter for lists Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/api/pulls/748 Co-Authored-By: konrad Co-Committed-By: konrad --- pkg/models/label_task.go | 22 ++++++- pkg/models/list.go | 26 ++++++++- pkg/models/namespace.go | 87 +++++++++++++++++++--------- pkg/models/namespace_test.go | 40 ++++++++++++- pkg/models/task_collection.go | 2 +- pkg/models/task_collection_filter.go | 21 ++++++- pkg/models/task_collection_test.go | 40 +++++++++++++ pkg/models/tasks.go | 32 +++++++++- pkg/swagger/docs.go | 10 +++- pkg/swagger/swagger.json | 10 +++- pkg/swagger/swagger.yaml | 8 ++- pkg/user/users_list.go | 41 +++++++++---- 12 files changed, 288 insertions(+), 51 deletions(-) diff --git a/pkg/models/label_task.go b/pkg/models/label_task.go index 57fae608c9f..b631ed83ec3 100644 --- a/pkg/models/label_task.go +++ b/pkg/models/label_task.go @@ -17,8 +17,12 @@ package models import ( + "strconv" + "strings" "time" + "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/user" "code.vikunja.io/web" "xorm.io/builder" @@ -171,13 +175,29 @@ func getLabelsByTaskIDs(opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, res cond = builder.Or(cond, builder.Eq{"labels.created_by_id": opts.User.ID}) } + vals := strings.Split(opts.Search, ",") + ids := []int64{} + for _, val := range vals { + v, err := strconv.ParseInt(val, 10, 64) + if err != nil { + log.Debugf("Label search string part '%s' is not a number: %s", val, err) + continue + } + ids = append(ids, v) + } + + if len(ids) > 0 { + cond = builder.And(cond, builder.In("labels.id", ids)) + } else { + cond = builder.And(cond, &builder.Like{"labels.title", "%" + opts.Search + "%"}) + } + limit, start := getLimitFromPageIndex(opts.Page, opts.PerPage) query := x.Table("labels"). Select(selectStmt). Join("LEFT", "label_task", "label_task.label_id = labels.id"). Where(cond). - And("labels.title LIKE ?", "%"+opts.Search+"%"). GroupBy(groupBy). OrderBy("labels.id ASC") if limit > 0 { diff --git a/pkg/models/list.go b/pkg/models/list.go index fa5cbe98e47..21ffa9f0899 100644 --- a/pkg/models/list.go +++ b/pkg/models/list.go @@ -17,8 +17,12 @@ package models import ( + "strconv" + "strings" "time" + "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/files" "code.vikunja.io/api/pkg/metrics" "code.vikunja.io/api/pkg/user" @@ -320,6 +324,24 @@ func getRawListsForUser(opts *listOptions) (lists []*List, resultCount int, tota limit, start := getLimitFromPageIndex(opts.page, opts.perPage) + var filterCond builder.Cond + vals := strings.Split(opts.search, ",") + ids := []int64{} + for _, val := range vals { + v, err := strconv.ParseInt(val, 10, 64) + if err != nil { + log.Debugf("List search string part '%s' is not a number: %s", val, err) + continue + } + ids = append(ids, v) + } + + if len(ids) > 0 { + filterCond = builder.In("l.id", ids) + } else { + filterCond = &builder.Like{"l.title", "%" + opts.search + "%"} + } + // Gets all Lists where the user is either owner or in a team which has access to the list // Or in a team which has namespace read access query := x.Select("l.*"). @@ -340,7 +362,7 @@ func getRawListsForUser(opts *listOptions) (lists []*List, resultCount int, tota builder.Eq{"l.owner_id": fullUser.ID}, )). GroupBy("l.id"). - Where("l.title LIKE ?", "%"+opts.search+"%"). + Where(filterCond). Where(isArchivedCond) if limit > 0 { query = query.Limit(limit, start) @@ -368,7 +390,7 @@ func getRawListsForUser(opts *listOptions) (lists []*List, resultCount int, tota builder.Eq{"l.owner_id": fullUser.ID}, )). GroupBy("l.id"). - Where("l.title LIKE ?", "%"+opts.search+"%"). + Where(filterCond). Where(isArchivedCond). Count(&List{}) return lists, len(lists), totalItems, err diff --git a/pkg/models/namespace.go b/pkg/models/namespace.go index 5c767b816af..4d4fb472afd 100644 --- a/pkg/models/namespace.go +++ b/pkg/models/namespace.go @@ -18,8 +18,12 @@ package models import ( "sort" + "strconv" + "strings" "time" + "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/metrics" "code.vikunja.io/api/pkg/user" "code.vikunja.io/web" @@ -51,6 +55,9 @@ type Namespace struct { // A timestamp when this namespace was last updated. You cannot change this value. Updated time.Time `xorm:"updated not null" json:"updated"` + // If set to true, will only return the namespaces, not their lists. + NamespacesOnly bool `xorm:"-" json:"-" query:"namespaces_only"` + web.CRUDable `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"` } @@ -171,6 +178,19 @@ type NamespaceWithLists struct { Lists []*List `xorm:"-" json:"lists"` } +func makeNamespaceSliceFromMap(namespaces map[int64]*NamespaceWithLists, userMap map[int64]*user.User) []*NamespaceWithLists { + all := make([]*NamespaceWithLists, 0, len(namespaces)) + for _, n := range namespaces { + n.Owner = userMap[n.OwnerID] + all = append(all, n) + } + sort.Slice(all, func(i, j int) bool { + return all[i].ID < all[j].ID + }) + + return all +} + // ReadAll gets all namespaces a user has access to // @Summary Get all namespaces a user has access to // @Description Returns all namespaces a user has access to. @@ -181,10 +201,12 @@ type NamespaceWithLists struct { // @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." // @Param is_archived query bool false "If true, also returns all archived namespaces." +// @Param namespaces_only query bool false "If true, also returns only namespaces without their lists." // @Security JWTKeyAuth // @Success 200 {array} models.NamespaceWithLists "The Namespaces." // @Failure 500 {object} models.Message "Internal error" // @Router /namespaces [get] +//nolint:gocyclo 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, 0, 0, ErrGenericForbidden{} @@ -210,6 +232,22 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r ) } + var filterCond builder.Cond = &builder.Like{"namespaces.title", "%" + search + "%"} + vals := strings.Split(search, ",") + ids := []int64{} + for _, val := range vals { + v, err := strconv.ParseInt(val, 10, 64) + if err != nil { + log.Debugf("Namespace search string part '%s' is not a number: %s", val, err) + continue + } + ids = append(ids, v) + } + + if len(ids) > 0 { + filterCond = builder.In("namespaces.id", ids) + } + limit, start := getLimitFromPageIndex(page, perPage) query := x.Select("namespaces.*"). Table("namespaces"). @@ -220,7 +258,7 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r Or("namespaces.owner_id = ?", doer.ID). Or("users_namespace.user_id = ?", doer.ID). GroupBy("namespaces.id"). - Where("namespaces.title LIKE ?", "%"+search+"%"). + Where(filterCond). Where(isArchivedCond) if limit > 0 { query = query.Limit(limit, start) @@ -230,6 +268,22 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r return nil, 0, 0, err } + 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). + And("namespaces.is_archived = false"). + GroupBy("namespaces.id"). + Where(filterCond). + Count(&NamespaceWithLists{}) + if err != nil { + return nil, 0, 0, err + } + // Make a list of namespace ids var namespaceids []int64 var userIDs []int64 @@ -245,6 +299,11 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r return nil, 0, 0, err } + if n.NamespacesOnly { + all := makeNamespaceSliceFromMap(namespaces, userMap) + return all, len(all), numberOfTotalItems, nil + } + // Get all lists lists := []*List{} listQuery := x. @@ -258,22 +317,6 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r return nil, 0, 0, err } - 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). - And("namespaces.is_archived = false"). - GroupBy("namespaces.id"). - Where("namespaces.title LIKE ?", "%"+search+"%"). - Count(&NamespaceWithLists{}) - if err != nil { - return nil, 0, 0, err - } - /////////////// // Shared Lists @@ -397,15 +440,7 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r ////////////////////// // Put it all together (and sort it) - all := make([]*NamespaceWithLists, 0, len(namespaces)) - for _, n := range namespaces { - n.Owner = userMap[n.OwnerID] - all = append(all, n) - } - sort.Slice(all, func(i, j int) bool { - return all[i].ID < all[j].ID - }) - + all := makeNamespaceSliceFromMap(namespaces, userMap) return all, len(all), numberOfTotalItems, nil } diff --git a/pkg/models/namespace_test.go b/pkg/models/namespace_test.go index bc1f4502d3c..b7a93d8346c 100644 --- a/pkg/models/namespace_test.go +++ b/pkg/models/namespace_test.go @@ -148,6 +148,7 @@ func TestNamespace_Delete(t *testing.T) { func TestNamespace_ReadAll(t *testing.T) { user1 := &user.User{ID: 1} + user7 := &user.User{ID: 7} user11 := &user.User{ID: 11} user12 := &user.User{ID: 12} @@ -157,7 +158,7 @@ func TestNamespace_ReadAll(t *testing.T) { assert.NoError(t, err) namespaces := nn.([]*NamespaceWithLists) assert.NotNil(t, namespaces) - assert.Len(t, namespaces, 11) // Total of 10 including shared, favorites and saved filters + assert.Len(t, namespaces, 11) // Total of 11 including shared, favorites and saved filters assert.Equal(t, int64(-3), namespaces[0].ID) // The first one should be the one with shared filters assert.Equal(t, int64(-2), namespaces[1].ID) // The second one should be the one with favorites assert.Equal(t, int64(-1), namespaces[2].ID) // The third one should be the one with the shared namespaces @@ -169,6 +170,43 @@ func TestNamespace_ReadAll(t *testing.T) { } } }) + t.Run("namespaces only", func(t *testing.T) { + n := &Namespace{ + NamespacesOnly: true, + } + nn, _, _, err := n.ReadAll(user1, "", 1, -1) + assert.NoError(t, err) + namespaces := nn.([]*NamespaceWithLists) + assert.NotNil(t, namespaces) + assert.Len(t, namespaces, 8) // Total of 8 - excluding shared, favorites and saved filters (normally 11) + // Ensure every namespace does not contain lists + for _, namespace := range namespaces { + assert.Nil(t, namespace.Lists) + } + }) + t.Run("ids only", func(t *testing.T) { + n := &Namespace{ + NamespacesOnly: true, + } + nn, _, _, err := n.ReadAll(user7, "13,14", 1, -1) + assert.NoError(t, err) + namespaces := nn.([]*NamespaceWithLists) + assert.NotNil(t, namespaces) + assert.Len(t, namespaces, 2) + assert.Equal(t, int64(13), namespaces[0].ID) + assert.Equal(t, int64(14), namespaces[1].ID) + }) + t.Run("ids only but ids with other people's namespace", func(t *testing.T) { + n := &Namespace{ + NamespacesOnly: true, + } + nn, _, _, err := n.ReadAll(user1, "1,w", 1, -1) + assert.NoError(t, err) + namespaces := nn.([]*NamespaceWithLists) + assert.NotNil(t, namespaces) + assert.Len(t, namespaces, 1) + assert.Equal(t, int64(1), namespaces[0].ID) + }) t.Run("archived", func(t *testing.T) { n := &Namespace{ IsArchived: true, diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index 13b42e681bf..a0c56a15f19 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -91,7 +91,7 @@ func validateTaskField(fieldName string) error { // @Param s query string false "Search tasks by task text." // @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`." // @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`." -// @Param filter_by query string false "The name of the field to filter by. Allowed values are all task properties except `list` and `namespace`. 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_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_concat query string false "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`." diff --git a/pkg/models/task_collection_filter.go b/pkg/models/task_collection_filter.go index 09b73556ff6..aed3b1c3048 100644 --- a/pkg/models/task_collection_filter.go +++ b/pkg/models/task_collection_filter.go @@ -188,9 +188,24 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string) (nativeValue interface{}, err error) { - var realFieldName = strcase.ToCamel(fieldName) - if strings.ToLower(fieldName) == "id" { - realFieldName = "ID" + realFieldName := strings.ReplaceAll(strcase.ToCamel(fieldName), "Id", "ID") + + if realFieldName == "Namespace" { + if comparator == taskFilterComparatorIn { + vals := strings.Split(value, ",") + valueSlice := []interface{}{} + for _, val := range vals { + v, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return nil, err + } + valueSlice = append(valueSlice, v) + } + return valueSlice, nil + } + + nativeValue, err = strconv.ParseInt(value, 10, 64) + return } field, ok := reflect.TypeOf(&Task{}).Elem().FieldByName(realFieldName) diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index 664ab6386f6..b5925ab9bf4 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -941,6 +941,46 @@ func TestTaskCollection_ReadAll(t *testing.T) { }, wantErr: false, }, + { + name: "filter list", + fields: fields{ + FilterBy: []string{"list_id"}, + FilterValue: []string{"6"}, + FilterComparator: []string{"equals"}, + }, + args: defaultArgs, + want: []*Task{ + task15, + }, + wantErr: false, + }, + { + name: "filter namespace", + fields: fields{ + FilterBy: []string{"namespace"}, + FilterValue: []string{"7"}, + FilterComparator: []string{"equals"}, + }, + args: defaultArgs, + want: []*Task{ + task21, + }, + wantErr: false, + }, + { + name: "filter namespace in", + fields: fields{ + FilterBy: []string{"namespace"}, + FilterValue: []string{"7,8"}, + FilterComparator: []string{"in"}, + }, + args: defaultArgs, + want: []*Task{ + task21, + task22, + }, + wantErr: false, + }, } for _, tt := range tests { diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 6e4f1ddae23..dab48ac8a77 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -144,7 +144,7 @@ type taskOptions struct { // @Param s query string false "Search tasks by task text." // @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`." // @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`." -// @Param filter_by query string false "The name of the field to filter by. Allowed values are all task properties except `list` and `namespace`. 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_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_concat query string false "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`." @@ -271,6 +271,7 @@ func getRawTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks [] reminderFilters := []builder.Cond{} assigneeFilters := []builder.Cond{} labelFilters := []builder.Cond{} + namespaceFilters := []builder.Cond{} var filters = make([]builder.Cond, 0, len(opts.filters)) // To still find tasks with nil values, we exclude 0s when comparing with >/< values. @@ -305,6 +306,16 @@ func getRawTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks [] continue } + if f.field == "namespace" { + f.field = "namespace_id" + filter, err := getFilterCond(f, opts.filterIncludeNulls) + if err != nil { + return nil, 0, 0, err + } + namespaceFilters = append(namespaceFilters, filter) + continue + } + filter, err := getFilterCond(f, opts.filterIncludeNulls) if err != nil { return nil, 0, 0, err @@ -369,6 +380,25 @@ func getRawTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks [] filters = append(filters, getFilterCondForSeparateTable("label_task", opts.filterConcat, labelFilters)) } + if len(namespaceFilters) > 0 { + var filtercond builder.Cond + if opts.filterConcat == filterConcatOr { + filtercond = builder.Or(namespaceFilters...) + } + if opts.filterConcat == filterConcatAnd { + filtercond = builder.And(namespaceFilters...) + } + + cond := builder.In( + "list_id", + builder. + Select("id"). + From("list"). + Where(filtercond), + ) + filters = append(filters, cond) + } + query = query.Where(listCond) queryCount = queryCount.Where(listCond) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 22ef34cad39..0bacbcc830c 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1892,7 +1892,7 @@ var doc = `{ }, { "type": "string", - "description": "The name of the field to filter by. Allowed values are all task properties except ` + "`" + `list` + "`" + ` and ` + "`" + `namespace` + "`" + `. 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.", "name": "filter_by", "in": "query" }, @@ -3036,6 +3036,12 @@ var doc = `{ "description": "If true, also returns all archived namespaces.", "name": "is_archived", "in": "query" + }, + { + "type": "boolean", + "description": "If true, also returns only namespaces without their lists.", + "name": "namespaces_only", + "in": "query" } ], "responses": { @@ -4006,7 +4012,7 @@ var doc = `{ }, { "type": "string", - "description": "The name of the field to filter by. Allowed values are all task properties except ` + "`" + `list` + "`" + ` and ` + "`" + `namespace` + "`" + `. 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.", "name": "filter_by", "in": "query" }, diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index e500da9e6fd..c9c62efa456 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -1875,7 +1875,7 @@ }, { "type": "string", - "description": "The name of the field to filter by. Allowed values are all task properties except `list` and `namespace`. 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.", "name": "filter_by", "in": "query" }, @@ -3019,6 +3019,12 @@ "description": "If true, also returns all archived namespaces.", "name": "is_archived", "in": "query" + }, + { + "type": "boolean", + "description": "If true, also returns only namespaces without their lists.", + "name": "namespaces_only", + "in": "query" } ], "responses": { @@ -3989,7 +3995,7 @@ }, { "type": "string", - "description": "The name of the field to filter by. Allowed values are all task properties except `list` and `namespace`. 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.", "name": "filter_by", "in": "query" }, diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index e520a2ead3e..4ab41e8af1c 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -2443,7 +2443,7 @@ paths: in: query name: order_by type: string - - description: The name of the field to filter by. Allowed values are all task properties except `list` and `namespace`. 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 name: filter_by type: string @@ -3008,6 +3008,10 @@ paths: in: query name: is_archived type: boolean + - description: If true, also returns only namespaces without their lists. + in: query + name: namespaces_only + type: boolean produces: - application/json responses: @@ -4467,7 +4471,7 @@ paths: in: query name: order_by type: string - - description: The name of the field to filter by. Allowed values are all task properties except `list` and `namespace`. 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 name: filter_by type: string diff --git a/pkg/user/users_list.go b/pkg/user/users_list.go index dd838aab3be..750d17e17ea 100644 --- a/pkg/user/users_list.go +++ b/pkg/user/users_list.go @@ -17,20 +17,41 @@ package user +import ( + "strconv" + "strings" + + "code.vikunja.io/api/pkg/log" +) + // ListUsers returns a list with all users, filtered by an optional searchstring -func ListUsers(searchterm string) (users []User, err error) { +func ListUsers(searchterm string) (users []*User, err error) { + + vals := strings.Split(searchterm, ",") + ids := []int64{} + for _, val := range vals { + v, err := strconv.ParseInt(val, 10, 64) + if err != nil { + log.Debugf("User search string part '%s' is not a number: %s", val, err) + continue + } + ids = append(ids, v) + } + + if len(ids) > 0 { + err = x. + In("id", ids). + Find(&users) + return + } if searchterm == "" { err = x.Find(&users) - } else { - err = x. - Where("username LIKE ?", "%"+searchterm+"%"). - Find(&users) + return } - if err != nil { - return []User{}, err - } - - return users, nil + err = x. + Where("username LIKE ?", "%"+searchterm+"%"). + Find(&users) + return }