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 <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/748 Co-Authored-By: konrad <konrad@kola-entertainments.de> Co-Committed-By: konrad <konrad@kola-entertainments.de>
This commit is contained in:
parent
111efd5fae
commit
2d4e2e452c
12 changed files with 288 additions and 51 deletions
|
@ -17,8 +17,12 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/log"
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/user"
|
"code.vikunja.io/api/pkg/user"
|
||||||
"code.vikunja.io/web"
|
"code.vikunja.io/web"
|
||||||
"xorm.io/builder"
|
"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})
|
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)
|
limit, start := getLimitFromPageIndex(opts.Page, opts.PerPage)
|
||||||
|
|
||||||
query := x.Table("labels").
|
query := x.Table("labels").
|
||||||
Select(selectStmt).
|
Select(selectStmt).
|
||||||
Join("LEFT", "label_task", "label_task.label_id = labels.id").
|
Join("LEFT", "label_task", "label_task.label_id = labels.id").
|
||||||
Where(cond).
|
Where(cond).
|
||||||
And("labels.title LIKE ?", "%"+opts.Search+"%").
|
|
||||||
GroupBy(groupBy).
|
GroupBy(groupBy).
|
||||||
OrderBy("labels.id ASC")
|
OrderBy("labels.id ASC")
|
||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
|
|
|
@ -17,8 +17,12 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/log"
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/files"
|
"code.vikunja.io/api/pkg/files"
|
||||||
"code.vikunja.io/api/pkg/metrics"
|
"code.vikunja.io/api/pkg/metrics"
|
||||||
"code.vikunja.io/api/pkg/user"
|
"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)
|
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
|
// 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
|
// Or in a team which has namespace read access
|
||||||
query := x.Select("l.*").
|
query := x.Select("l.*").
|
||||||
|
@ -340,7 +362,7 @@ func getRawListsForUser(opts *listOptions) (lists []*List, resultCount int, tota
|
||||||
builder.Eq{"l.owner_id": fullUser.ID},
|
builder.Eq{"l.owner_id": fullUser.ID},
|
||||||
)).
|
)).
|
||||||
GroupBy("l.id").
|
GroupBy("l.id").
|
||||||
Where("l.title LIKE ?", "%"+opts.search+"%").
|
Where(filterCond).
|
||||||
Where(isArchivedCond)
|
Where(isArchivedCond)
|
||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
query = query.Limit(limit, start)
|
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},
|
builder.Eq{"l.owner_id": fullUser.ID},
|
||||||
)).
|
)).
|
||||||
GroupBy("l.id").
|
GroupBy("l.id").
|
||||||
Where("l.title LIKE ?", "%"+opts.search+"%").
|
Where(filterCond).
|
||||||
Where(isArchivedCond).
|
Where(isArchivedCond).
|
||||||
Count(&List{})
|
Count(&List{})
|
||||||
return lists, len(lists), totalItems, err
|
return lists, len(lists), totalItems, err
|
||||||
|
|
|
@ -18,8 +18,12 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/log"
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/metrics"
|
"code.vikunja.io/api/pkg/metrics"
|
||||||
"code.vikunja.io/api/pkg/user"
|
"code.vikunja.io/api/pkg/user"
|
||||||
"code.vikunja.io/web"
|
"code.vikunja.io/web"
|
||||||
|
@ -51,6 +55,9 @@ type Namespace struct {
|
||||||
// A timestamp when this namespace was last updated. You cannot change this value.
|
// A timestamp when this namespace was last updated. You cannot change this value.
|
||||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
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.CRUDable `xorm:"-" json:"-"`
|
||||||
web.Rights `xorm:"-" json:"-"`
|
web.Rights `xorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
@ -171,6 +178,19 @@ type NamespaceWithLists struct {
|
||||||
Lists []*List `xorm:"-" json:"lists"`
|
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
|
// ReadAll gets all namespaces a user has access to
|
||||||
// @Summary Get 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.
|
// @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 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 s query string false "Search namespaces by name."
|
||||||
// @Param is_archived query bool false "If true, also returns all archived namespaces."
|
// @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
|
// @Security JWTKeyAuth
|
||||||
// @Success 200 {array} models.NamespaceWithLists "The Namespaces."
|
// @Success 200 {array} models.NamespaceWithLists "The Namespaces."
|
||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
// @Router /namespaces [get]
|
// @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) {
|
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 {
|
if _, is := a.(*LinkSharing); is {
|
||||||
return nil, 0, 0, ErrGenericForbidden{}
|
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)
|
limit, start := getLimitFromPageIndex(page, perPage)
|
||||||
query := x.Select("namespaces.*").
|
query := x.Select("namespaces.*").
|
||||||
Table("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("namespaces.owner_id = ?", doer.ID).
|
||||||
Or("users_namespace.user_id = ?", doer.ID).
|
Or("users_namespace.user_id = ?", doer.ID).
|
||||||
GroupBy("namespaces.id").
|
GroupBy("namespaces.id").
|
||||||
Where("namespaces.title LIKE ?", "%"+search+"%").
|
Where(filterCond).
|
||||||
Where(isArchivedCond)
|
Where(isArchivedCond)
|
||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
query = query.Limit(limit, start)
|
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
|
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
|
// Make a list of namespace ids
|
||||||
var namespaceids []int64
|
var namespaceids []int64
|
||||||
var userIDs []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
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if n.NamespacesOnly {
|
||||||
|
all := makeNamespaceSliceFromMap(namespaces, userMap)
|
||||||
|
return all, len(all), numberOfTotalItems, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Get all lists
|
// Get all lists
|
||||||
lists := []*List{}
|
lists := []*List{}
|
||||||
listQuery := x.
|
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
|
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
|
// 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)
|
// Put it all together (and sort it)
|
||||||
all := make([]*NamespaceWithLists, 0, len(namespaces))
|
all := makeNamespaceSliceFromMap(namespaces, userMap)
|
||||||
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, len(all), numberOfTotalItems, nil
|
return all, len(all), numberOfTotalItems, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -148,6 +148,7 @@ func TestNamespace_Delete(t *testing.T) {
|
||||||
|
|
||||||
func TestNamespace_ReadAll(t *testing.T) {
|
func TestNamespace_ReadAll(t *testing.T) {
|
||||||
user1 := &user.User{ID: 1}
|
user1 := &user.User{ID: 1}
|
||||||
|
user7 := &user.User{ID: 7}
|
||||||
user11 := &user.User{ID: 11}
|
user11 := &user.User{ID: 11}
|
||||||
user12 := &user.User{ID: 12}
|
user12 := &user.User{ID: 12}
|
||||||
|
|
||||||
|
@ -157,7 +158,7 @@ func TestNamespace_ReadAll(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
namespaces := nn.([]*NamespaceWithLists)
|
namespaces := nn.([]*NamespaceWithLists)
|
||||||
assert.NotNil(t, namespaces)
|
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(-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(-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
|
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) {
|
t.Run("archived", func(t *testing.T) {
|
||||||
n := &Namespace{
|
n := &Namespace{
|
||||||
IsArchived: true,
|
IsArchived: true,
|
||||||
|
|
|
@ -91,7 +91,7 @@ func validateTaskField(fieldName string) error {
|
||||||
// @Param s query string false "Search tasks by task text."
|
// @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 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 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_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`"
|
||||||
// @Param filter_concat query string false "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`."
|
// @Param filter_concat query string false "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`."
|
||||||
|
|
|
@ -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) {
|
func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string) (nativeValue interface{}, err error) {
|
||||||
|
|
||||||
var realFieldName = strcase.ToCamel(fieldName)
|
realFieldName := strings.ReplaceAll(strcase.ToCamel(fieldName), "Id", "ID")
|
||||||
if strings.ToLower(fieldName) == "id" {
|
|
||||||
realFieldName = "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)
|
field, ok := reflect.TypeOf(&Task{}).Elem().FieldByName(realFieldName)
|
||||||
|
|
|
@ -941,6 +941,46 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||||
},
|
},
|
||||||
wantErr: false,
|
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 {
|
for _, tt := range tests {
|
||||||
|
|
|
@ -144,7 +144,7 @@ type taskOptions struct {
|
||||||
// @Param s query string false "Search tasks by task text."
|
// @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 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 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_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`"
|
||||||
// @Param filter_concat query string false "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`."
|
// @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{}
|
reminderFilters := []builder.Cond{}
|
||||||
assigneeFilters := []builder.Cond{}
|
assigneeFilters := []builder.Cond{}
|
||||||
labelFilters := []builder.Cond{}
|
labelFilters := []builder.Cond{}
|
||||||
|
namespaceFilters := []builder.Cond{}
|
||||||
|
|
||||||
var filters = make([]builder.Cond, 0, len(opts.filters))
|
var filters = make([]builder.Cond, 0, len(opts.filters))
|
||||||
// To still find tasks with nil values, we exclude 0s when comparing with >/< values.
|
// 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
|
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)
|
filter, err := getFilterCond(f, opts.filterIncludeNulls)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
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))
|
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)
|
query = query.Where(listCond)
|
||||||
queryCount = queryCount.Where(listCond)
|
queryCount = queryCount.Where(listCond)
|
||||||
|
|
||||||
|
|
|
@ -1892,7 +1892,7 @@ var doc = `{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"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",
|
"name": "filter_by",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
|
@ -3036,6 +3036,12 @@ var doc = `{
|
||||||
"description": "If true, also returns all archived namespaces.",
|
"description": "If true, also returns all archived namespaces.",
|
||||||
"name": "is_archived",
|
"name": "is_archived",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If true, also returns only namespaces without their lists.",
|
||||||
|
"name": "namespaces_only",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
@ -4006,7 +4012,7 @@ var doc = `{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"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",
|
"name": "filter_by",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1875,7 +1875,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"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",
|
"name": "filter_by",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
|
@ -3019,6 +3019,12 @@
|
||||||
"description": "If true, also returns all archived namespaces.",
|
"description": "If true, also returns all archived namespaces.",
|
||||||
"name": "is_archived",
|
"name": "is_archived",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If true, also returns only namespaces without their lists.",
|
||||||
|
"name": "namespaces_only",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
@ -3989,7 +3995,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"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",
|
"name": "filter_by",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
|
|
|
@ -2443,7 +2443,7 @@ paths:
|
||||||
in: query
|
in: query
|
||||||
name: order_by
|
name: order_by
|
||||||
type: string
|
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
|
in: query
|
||||||
name: filter_by
|
name: filter_by
|
||||||
type: string
|
type: string
|
||||||
|
@ -3008,6 +3008,10 @@ paths:
|
||||||
in: query
|
in: query
|
||||||
name: is_archived
|
name: is_archived
|
||||||
type: boolean
|
type: boolean
|
||||||
|
- description: If true, also returns only namespaces without their lists.
|
||||||
|
in: query
|
||||||
|
name: namespaces_only
|
||||||
|
type: boolean
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
@ -4467,7 +4471,7 @@ paths:
|
||||||
in: query
|
in: query
|
||||||
name: order_by
|
name: order_by
|
||||||
type: string
|
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
|
in: query
|
||||||
name: filter_by
|
name: filter_by
|
||||||
type: string
|
type: string
|
||||||
|
|
|
@ -17,20 +17,41 @@
|
||||||
|
|
||||||
package user
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
// ListUsers returns a list with all users, filtered by an optional searchstring
|
// 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 == "" {
|
if searchterm == "" {
|
||||||
err = x.Find(&users)
|
err = x.Find(&users)
|
||||||
} else {
|
return
|
||||||
err = x.
|
|
||||||
Where("username LIKE ?", "%"+searchterm+"%").
|
|
||||||
Find(&users)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
err = x.
|
||||||
return []User{}, err
|
Where("username LIKE ?", "%"+searchterm+"%").
|
||||||
}
|
Find(&users)
|
||||||
|
return
|
||||||
return users, nil
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue