feat(filters): query-based filter logic (#2177)
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2177
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
- id: 1
|
||||
filters: '{"sort_by":null,"order_by":null,"filter_by":["start_date","end_date","due_date"],"filter_value":["2018-12-11T03:46:40+00:00","2018-12-13T11:20:01+00:00","2018-11-29T14:00:00+00:00"],"filter_comparator":["greater","less","greater"],"filter_concat":"","filter_include_nulls":false}'
|
||||
filters: '{"sort_by":null,"order_by":null,"filter":"start_date > \u00272018-12-11T03:46:40+00:00\u0027 || end_date < \u00272018-12-13T11:20:01+00:00\u0027 || due_date > \u00272018-11-29T14:00:00+00:00\u0027","filter_include_nulls":false}'
|
||||
title: testfilter1
|
||||
owner_id: 1
|
||||
updated: 2020-09-08 15:13:12
|
||||
|
@ -184,9 +184,7 @@ func TestTaskCollection(t *testing.T) {
|
||||
t.Run("start and end date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date", "end_date", "due_date"},
|
||||
"filter_value": []string{"2018-12-11T03:46:40+00:00", "2018-12-13T11:20:01+00:00", "2018-11-29T14:00:00+00:00"},
|
||||
"filter_comparator": []string{"greater", "less", "greater"},
|
||||
"filter": []string{"start_date > '2018-12-11T03:46:40+00:00' || end_date < '2018-12-13T11:20:01+00:00' || due_date > '2018-11-29T14:00:00+00:00'"},
|
||||
},
|
||||
urlParams,
|
||||
)
|
||||
@ -209,9 +207,7 @@ func TestTaskCollection(t *testing.T) {
|
||||
t.Run("start date only", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date"},
|
||||
"filter_value": []string{"2018-10-20T01:46:40+00:00"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
"filter": []string{"start_date > '2018-10-20T01:46:40+00:00'"},
|
||||
},
|
||||
urlParams,
|
||||
)
|
||||
@ -234,9 +230,7 @@ func TestTaskCollection(t *testing.T) {
|
||||
t.Run("end date only", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"end_date"},
|
||||
"filter_value": []string{"2018-12-13T11:20:01+00:00"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
"filter": []string{"end_date > '2018-12-13T11:20:01+00:00'"},
|
||||
},
|
||||
urlParams,
|
||||
)
|
||||
@ -249,9 +243,7 @@ func TestTaskCollection(t *testing.T) {
|
||||
t.Run("unix timestamps", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date", "end_date", "due_date"},
|
||||
"filter_value": []string{"1544500000", "1513164001", "1543500000"},
|
||||
"filter_comparator": []string{"greater", "less", "greater"},
|
||||
"filter": []string{"start_date > 1544500000 || end_date < 1513164001 || due_date > 1543500000"},
|
||||
},
|
||||
urlParams,
|
||||
)
|
||||
@ -275,9 +267,7 @@ func TestTaskCollection(t *testing.T) {
|
||||
t.Run("invalid date", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"due_date"},
|
||||
"filter_value": []string{"invalid"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
"filter": []string{"due_date > invalid"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
@ -411,9 +401,7 @@ func TestTaskCollection(t *testing.T) {
|
||||
t.Run("start and end date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date", "end_date", "due_date"},
|
||||
"filter_value": []string{"2018-12-11T03:46:40+00:00", "2018-12-13T11:20:01+00:00", "2018-11-29T14:00:00+00:00"},
|
||||
"filter_comparator": []string{"greater", "less", "greater"},
|
||||
"filter": []string{"start_date > '2018-12-11T03:46:40+00:00' || end_date < '2018-12-13T11:20:01+00:00' || due_date > '2018-11-29T14:00:00+00:00'"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
@ -436,9 +424,7 @@ func TestTaskCollection(t *testing.T) {
|
||||
t.Run("start date only", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date"},
|
||||
"filter_value": []string{"2018-10-20T01:46:40+00:00"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
"filter": []string{"start_date > '2018-10-20T01:46:40+00:00'"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
@ -461,9 +447,7 @@ func TestTaskCollection(t *testing.T) {
|
||||
t.Run("end date only", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"end_date"},
|
||||
"filter_value": []string{"2018-12-13T11:20:01+00:00"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
"filter": []string{"end_date > '2018-12-13T11:20:01+00:00'"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
@ -477,9 +461,7 @@ func TestTaskCollection(t *testing.T) {
|
||||
t.Run("invalid date", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"due_date"},
|
||||
"filter_value": []string{"invalid"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
"filter": []string{"due_date > invalid"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
107
pkg/migration/20231121191822.go
Normal file
107
pkg/migration/20231121191822.go
Normal file
@ -0,0 +1,107 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type taskCollectionFilter20231121191822 struct {
|
||||
SortBy []string `query:"sort_by" json:"sort_by"`
|
||||
OrderBy []string `query:"order_by" json:"order_by"`
|
||||
|
||||
FilterBy []string `query:"filter_by" json:"filter_by,omitempty"`
|
||||
FilterValue []string `query:"filter_value" json:"filter_value,omitempty"`
|
||||
FilterComparator []string `query:"filter_comparator" json:"filter_comparator,omitempty"`
|
||||
FilterConcat string `query:"filter_concat" json:"filter_concat,omitempty"`
|
||||
|
||||
Filter string `query:"filter" json:"filter"`
|
||||
FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"`
|
||||
}
|
||||
|
||||
type savedFilter20231121191822 struct {
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"filter"`
|
||||
Filters *taskCollectionFilter20231121191822 `xorm:"JSON not null" json:"filters" valid:"required"`
|
||||
}
|
||||
|
||||
func (savedFilter20231121191822) TableName() string {
|
||||
return "saved_filters"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20231121191822",
|
||||
Description: "Migrate saved filter structure",
|
||||
Migrate: func(tx *xorm.Engine) (err error) {
|
||||
allFilters := []*savedFilter20231121191822{}
|
||||
err = tx.Find(&allFilters)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, filter := range allFilters {
|
||||
var filterStrings []string
|
||||
for i, f := range filter.Filters.FilterBy {
|
||||
var comparator string
|
||||
switch filter.Filters.FilterComparator[i] {
|
||||
case "equals":
|
||||
comparator = "="
|
||||
case "greater":
|
||||
comparator = ">"
|
||||
case "greater_equals":
|
||||
comparator = ">="
|
||||
case "less":
|
||||
comparator = "<"
|
||||
case "less_equals":
|
||||
comparator = "<="
|
||||
case "not_equals":
|
||||
comparator = "!="
|
||||
case "like":
|
||||
comparator = "~"
|
||||
case "in":
|
||||
comparator = "?="
|
||||
}
|
||||
filterStrings = append(filterStrings, f+" "+comparator+" "+filter.Filters.FilterValue[i])
|
||||
}
|
||||
|
||||
filter.Filters.FilterConcat = " || "
|
||||
if filter.Filters.FilterConcat == "and" {
|
||||
filter.Filters.FilterConcat = " && "
|
||||
}
|
||||
filter.Filters.Filter = strings.Join(filterStrings, filter.Filters.FilterConcat)
|
||||
|
||||
filter.Filters.FilterBy = nil
|
||||
filter.Filters.FilterComparator = nil
|
||||
filter.Filters.FilterValue = nil
|
||||
filter.Filters.FilterConcat = ""
|
||||
|
||||
_, err = tx.Where("id = ?", filter.ID).Update(filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
@ -1021,7 +1021,7 @@ func (err ErrTaskRelationCycle) Error() string {
|
||||
}
|
||||
|
||||
// ErrCodeTaskRelationCycle holds the unique world-error code of this error
|
||||
const ErrCodeTaskRelationCycle = 4022
|
||||
const ErrCodeTaskRelationCycle = 4023
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTaskRelationCycle) HTTPError() web.HTTPError {
|
||||
@ -1032,6 +1032,34 @@ func (err ErrTaskRelationCycle) HTTPError() web.HTTPError {
|
||||
}
|
||||
}
|
||||
|
||||
// ErrInvalidFilterExpression represents an error where the task filter expression was invalid
|
||||
type ErrInvalidFilterExpression struct {
|
||||
Expression string
|
||||
ExpressionError error
|
||||
}
|
||||
|
||||
// IsErrInvalidFilterExpression checks if an error is ErrInvalidFilterExpression.
|
||||
func IsErrInvalidFilterExpression(err error) bool {
|
||||
_, ok := err.(ErrInvalidFilterExpression)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidFilterExpression) Error() string {
|
||||
return fmt.Sprintf("Task filter expression '%s' is invalid [ExpressionError: %v]", err.Expression, err.ExpressionError)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidFilterExpression holds the unique world-error code of this error
|
||||
const ErrCodeInvalidFilterExpression = 4024
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidFilterExpression) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
Code: ErrCodeInvalidFilterExpression,
|
||||
Message: fmt.Sprintf("The filter expression '%s' is invalid: %v", err.Expression, err.ExpressionError),
|
||||
}
|
||||
}
|
||||
|
||||
// ============
|
||||
// Team errors
|
||||
// ============
|
||||
|
@ -17,6 +17,8 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
@ -106,10 +108,7 @@ func getDefaultBucketID(s *xorm.Session, project *Project) (bucketID int64, err
|
||||
// @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`"
|
||||
// @Param filter_concat query string false "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`."
|
||||
// @Param filter query string false "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature."
|
||||
// @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`."
|
||||
// @Success 200 {array} models.Bucket "The buckets with their tasks"
|
||||
// @Failure 500 {object} models.Message "Internal server error"
|
||||
@ -173,28 +172,36 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
||||
opts.page = page
|
||||
opts.perPage = perPage
|
||||
opts.search = search
|
||||
opts.filterConcat = filterConcatAnd
|
||||
|
||||
var bucketFilterIndex int
|
||||
for i, filter := range opts.filters {
|
||||
for _, filter := range opts.parsedFilters {
|
||||
if filter.field == taskPropertyBucketID {
|
||||
bucketFilterIndex = i
|
||||
|
||||
// Limiting the map to the one filter we're looking for is the easiest way to ensure we only
|
||||
// get tasks in this bucket
|
||||
bucketID := filter.value.(int64)
|
||||
bucket := bucketMap[bucketID]
|
||||
|
||||
bucketMap = make(map[int64]*Bucket, 1)
|
||||
bucketMap[bucketID] = bucket
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if bucketFilterIndex == 0 {
|
||||
opts.filters = append(opts.filters, &taskFilter{
|
||||
field: taskPropertyBucketID,
|
||||
value: 0,
|
||||
comparator: taskFilterComparatorEquals,
|
||||
})
|
||||
bucketFilterIndex = len(opts.filters) - 1
|
||||
}
|
||||
|
||||
originalFilter := opts.filter
|
||||
for id, bucket := range bucketMap {
|
||||
|
||||
opts.filters[bucketFilterIndex].value = id
|
||||
if !strings.Contains(originalFilter, "bucket_id") {
|
||||
var filterString string
|
||||
if originalFilter == "" {
|
||||
filterString = "bucket_id = " + strconv.FormatInt(id, 10)
|
||||
} else {
|
||||
filterString = "(" + originalFilter + ") && bucket_id = " + strconv.FormatInt(id, 10)
|
||||
}
|
||||
opts.parsedFilters, err = getTaskFiltersFromFilterString(filterString)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ts, _, total, err := getRawTasksForProjects(s, []*Project{{ID: bucket.ProjectID}}, auth, opts)
|
||||
if err != nil {
|
||||
|
@ -81,9 +81,7 @@ func TestBucket_ReadAll(t *testing.T) {
|
||||
b := &Bucket{
|
||||
ProjectID: 1,
|
||||
TaskCollection: TaskCollection{
|
||||
FilterBy: []string{"title"},
|
||||
FilterComparator: []string{"like"},
|
||||
FilterValue: []string{"done"},
|
||||
Filter: "title ~ 'done'",
|
||||
},
|
||||
}
|
||||
bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", -1, 0)
|
||||
@ -94,6 +92,30 @@ func TestBucket_ReadAll(t *testing.T) {
|
||||
assert.Equal(t, int64(2), buckets[0].Tasks[0].ID)
|
||||
assert.Equal(t, int64(33), buckets[0].Tasks[1].ID)
|
||||
})
|
||||
t.Run("filtered by bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
testuser := &user.User{ID: 1}
|
||||
b := &Bucket{
|
||||
ProjectID: 1,
|
||||
TaskCollection: TaskCollection{
|
||||
Filter: "title ~ 'task' && bucket_id = 2",
|
||||
},
|
||||
}
|
||||
bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", -1, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
buckets := bucketsInterface.([]*Bucket)
|
||||
assert.Len(t, buckets, 3)
|
||||
assert.Empty(t, buckets[0].Tasks, 0)
|
||||
assert.Len(t, buckets[1].Tasks, 3)
|
||||
assert.Empty(t, buckets[2].Tasks, 0)
|
||||
assert.Equal(t, int64(3), buckets[1].Tasks[0].ID)
|
||||
assert.Equal(t, int64(4), buckets[1].Tasks[1].ID)
|
||||
assert.Equal(t, int64(5), buckets[1].Tasks[2].ID)
|
||||
})
|
||||
t.Run("accessed by link share", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
@ -65,7 +65,7 @@ func TestSavedFilter_Create(t *testing.T) {
|
||||
vals := map[string]interface{}{
|
||||
"title": "'test'",
|
||||
"description": "'Lorem Ipsum dolor sit amet'",
|
||||
"filters": "'{\"sort_by\":null,\"order_by\":null,\"filter_by\":null,\"filter_value\":null,\"filter_comparator\":null,\"filter_concat\":\"\",\"filter_include_nulls\":false}'",
|
||||
"filters": "'{\"sort_by\":null,\"order_by\":null,\"filter\":\"\",\"filter_include_nulls\":false}'",
|
||||
"owner_id": 1,
|
||||
}
|
||||
// Postgres can't compare json values directly, see https://dba.stackexchange.com/a/106290/210721
|
||||
|
@ -33,17 +33,8 @@ type TaskCollection struct {
|
||||
OrderBy []string `query:"order_by" json:"order_by"`
|
||||
OrderByArr []string `query:"order_by[]" json:"-"`
|
||||
|
||||
// The field name of the field to filter by
|
||||
FilterBy []string `query:"filter_by" json:"filter_by"`
|
||||
FilterByArr []string `query:"filter_by[]" json:"-"`
|
||||
// The value of the field name to filter by
|
||||
FilterValue []string `query:"filter_value" json:"filter_value"`
|
||||
FilterValueArr []string `query:"filter_value[]" json:"-"`
|
||||
// The comparator for field and value
|
||||
FilterComparator []string `query:"filter_comparator" json:"filter_comparator"`
|
||||
FilterComparatorArr []string `query:"filter_comparator[]" json:"-"`
|
||||
// The way all filter conditions are concatenated together, can be either "and" or "or".,
|
||||
FilterConcat string `query:"filter_concat" json:"filter_concat"`
|
||||
Filter string `query:"filter" json:"filter"`
|
||||
|
||||
// If set to true, the result will also include null values
|
||||
FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"`
|
||||
|
||||
@ -110,11 +101,11 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption
|
||||
|
||||
opts = &taskSearchOptions{
|
||||
sortby: sort,
|
||||
filterConcat: taskFilterConcatinator(tf.FilterConcat),
|
||||
filterIncludeNulls: tf.FilterIncludeNulls,
|
||||
filter: tf.Filter,
|
||||
}
|
||||
|
||||
opts.filters, err = getTaskFiltersByCollections(tf)
|
||||
opts.parsedFilters, err = getTaskFiltersFromFilterString(tf.Filter)
|
||||
return opts, err
|
||||
}
|
||||
|
||||
@ -130,10 +121,7 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption
|
||||
// @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`, `project_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. 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. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)- or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style relative dates for all date fields like `due_date`, `start_date`, `end_date`, etc."
|
||||
// @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 query string false "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature."
|
||||
// @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.Task "The tasks"
|
||||
|
@ -23,6 +23,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ganigeorgiev/fexpr"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
@ -54,6 +56,7 @@ type taskFilter struct {
|
||||
value interface{} // Needs to be an interface to be able to hold the field's native value
|
||||
comparator taskFilterComparator
|
||||
isNumeric bool
|
||||
join taskFilterConcatinator
|
||||
}
|
||||
|
||||
func parseTimeFromUserInput(timeString string) (value time.Time, err error) {
|
||||
@ -88,61 +91,83 @@ func parseTimeFromUserInput(timeString string) (value time.Time, err error) {
|
||||
return value.In(config.GetTimeZone()), err
|
||||
}
|
||||
|
||||
func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err error) {
|
||||
|
||||
if len(c.FilterByArr) > 0 {
|
||||
c.FilterBy = append(c.FilterBy, c.FilterByArr...)
|
||||
func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error) {
|
||||
filter = &taskFilter{
|
||||
join: filterConcatAnd,
|
||||
}
|
||||
if f.Join == fexpr.JoinOr {
|
||||
filter.join = filterConcatOr
|
||||
}
|
||||
|
||||
if len(c.FilterValueArr) > 0 {
|
||||
c.FilterValue = append(c.FilterValue, c.FilterValueArr...)
|
||||
}
|
||||
|
||||
if len(c.FilterComparatorArr) > 0 {
|
||||
c.FilterComparator = append(c.FilterComparator, c.FilterComparatorArr...)
|
||||
}
|
||||
|
||||
if c.FilterConcat != "" && c.FilterConcat != filterConcatAnd && c.FilterConcat != filterConcatOr {
|
||||
return nil, ErrInvalidTaskFilterConcatinator{
|
||||
Concatinator: taskFilterConcatinator(c.FilterConcat),
|
||||
}
|
||||
}
|
||||
|
||||
filters = make([]*taskFilter, 0, len(c.FilterBy))
|
||||
for i, f := range c.FilterBy {
|
||||
filter := &taskFilter{
|
||||
field: f,
|
||||
comparator: taskFilterComparatorEquals,
|
||||
}
|
||||
|
||||
if len(c.FilterComparator) > i {
|
||||
filter.comparator, err = getFilterComparatorFromString(c.FilterComparator[i])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = validateTaskFieldComparator(filter.comparator)
|
||||
var value string
|
||||
switch v := f.Item.(type) {
|
||||
case fexpr.Expr:
|
||||
filter.field = v.Left.Literal
|
||||
value = v.Right.Literal
|
||||
filter.comparator, err = getFilterComparatorFromOp(v.Op)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Cast the field value to its native type
|
||||
var reflectValue *reflect.StructField
|
||||
if len(c.FilterValue) > i {
|
||||
reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, c.FilterValue[i])
|
||||
case []fexpr.ExprGroup:
|
||||
values := make([]*taskFilter, 0, len(v))
|
||||
for _, expression := range v {
|
||||
subfilter, err := parseFilterFromExpression(expression)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidTaskFilterValue{
|
||||
Value: filter.field,
|
||||
Field: c.FilterValue[i],
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
values = append(values, subfilter)
|
||||
}
|
||||
if reflectValue != nil {
|
||||
filter.isNumeric = reflectValue.Type.Kind() == reflect.Int64
|
||||
}
|
||||
filter.value = values
|
||||
return
|
||||
}
|
||||
|
||||
filters = append(filters, filter)
|
||||
err = validateTaskFieldComparator(filter.comparator)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Cast the field value to its native type
|
||||
var reflectValue *reflect.StructField
|
||||
if filter.field == "project" {
|
||||
filter.field = "project_id"
|
||||
}
|
||||
reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, value)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidTaskFilterValue{
|
||||
Value: filter.field,
|
||||
Field: value,
|
||||
}
|
||||
}
|
||||
if reflectValue != nil {
|
||||
filter.isNumeric = reflectValue.Type.Kind() == reflect.Int64
|
||||
}
|
||||
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func getTaskFiltersFromFilterString(filter string) (filters []*taskFilter, err error) {
|
||||
|
||||
if filter == "" {
|
||||
return
|
||||
}
|
||||
|
||||
filter = strings.ReplaceAll(filter, " in ", " ?= ")
|
||||
|
||||
parsedFilter, err := fexpr.Parse(filter)
|
||||
if err != nil {
|
||||
return nil, &ErrInvalidFilterExpression{
|
||||
Expression: filter,
|
||||
ExpressionError: err,
|
||||
}
|
||||
}
|
||||
|
||||
filters = make([]*taskFilter, 0, len(parsedFilter))
|
||||
for _, f := range parsedFilter {
|
||||
parsedFilter, err := parseFilterFromExpression(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filters = append(filters, parsedFilter)
|
||||
}
|
||||
|
||||
return
|
||||
@ -167,26 +192,28 @@ func validateTaskFieldComparator(comparator taskFilterComparator) error {
|
||||
}
|
||||
}
|
||||
|
||||
func getFilterComparatorFromString(comparator string) (taskFilterComparator, error) {
|
||||
switch comparator {
|
||||
case "equals":
|
||||
func getFilterComparatorFromOp(op fexpr.SignOp) (taskFilterComparator, error) {
|
||||
switch op {
|
||||
case fexpr.SignEq:
|
||||
return taskFilterComparatorEquals, nil
|
||||
case "greater":
|
||||
case fexpr.SignGt:
|
||||
return taskFilterComparatorGreater, nil
|
||||
case "greater_equals":
|
||||
case fexpr.SignGte:
|
||||
return taskFilterComparatorGreateEquals, nil
|
||||
case "less":
|
||||
case fexpr.SignLt:
|
||||
return taskFilterComparatorLess, nil
|
||||
case "less_equals":
|
||||
case fexpr.SignLte:
|
||||
return taskFilterComparatorLessEquals, nil
|
||||
case "not_equals":
|
||||
case fexpr.SignNeq:
|
||||
return taskFilterComparatorNotEquals, nil
|
||||
case "like":
|
||||
case fexpr.SignLike:
|
||||
return taskFilterComparatorLike, nil
|
||||
case fexpr.SignAnyEq:
|
||||
fallthrough
|
||||
case "in":
|
||||
return taskFilterComparatorIn, nil
|
||||
default:
|
||||
return taskFilterComparatorInvalid, ErrInvalidTaskFilterComparator{Comparator: taskFilterComparator(comparator)}
|
||||
return taskFilterComparatorInvalid, ErrInvalidTaskFilterComparator{Comparator: taskFilterComparator(op)}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,8 @@ import (
|
||||
"gopkg.in/d4l3k/messagediff.v1"
|
||||
)
|
||||
|
||||
// To only run a selected tests: ^\QTestTaskCollection_ReadAll\E$/^\QReadAll_Tasks_with_range\E$
|
||||
|
||||
func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
// Dummy users
|
||||
user1 := &user.User{
|
||||
@ -675,10 +677,8 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
SortBy []string // Is a string, since this is the place where a query string comes from the user
|
||||
OrderBy []string
|
||||
|
||||
FilterBy []string
|
||||
FilterValue []string
|
||||
FilterComparator []string
|
||||
FilterIncludeNulls bool
|
||||
Filter string
|
||||
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
@ -792,9 +792,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "ReadAll Tasks with range",
|
||||
fields: fields{
|
||||
FilterBy: []string{"start_date", "end_date"},
|
||||
FilterValue: []string{"2018-12-11T03:46:40+00:00", "2018-12-13T11:20:01+00:00"},
|
||||
FilterComparator: []string{"greater", "less"},
|
||||
Filter: "start_date > '2018-12-11T03:46:40+00:00' || end_date < '2018-12-13T11:20:01+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -807,9 +805,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "ReadAll Tasks with different range",
|
||||
fields: fields{
|
||||
FilterBy: []string{"start_date", "end_date"},
|
||||
FilterValue: []string{"2018-12-13T11:20:00+00:00", "2018-12-16T22:40:00+00:00"},
|
||||
FilterComparator: []string{"greater", "less"},
|
||||
Filter: "start_date > '2018-12-13T11:20:00+00:00' || end_date < '2018-12-16T22:40:00+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -821,20 +817,16 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "ReadAll Tasks with range with start date only",
|
||||
fields: fields{
|
||||
FilterBy: []string{"start_date"},
|
||||
FilterValue: []string{"2018-12-12T07:33:20+00:00"},
|
||||
FilterComparator: []string{"greater"},
|
||||
Filter: "start_date > '2018-12-12T07:33:20+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks with range with start date only and greater equals",
|
||||
name: "ReadAll Tasks with range with start date only between",
|
||||
fields: fields{
|
||||
FilterBy: []string{"start_date"},
|
||||
FilterValue: []string{"2018-12-12T07:33:20+00:00"},
|
||||
FilterComparator: []string{"greater_equals"},
|
||||
Filter: "start_date > '2018-12-12T00:00:00+00:00' && start_date < '2018-12-13T00:00:00+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -843,12 +835,35 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks with range with start date only and greater equals",
|
||||
fields: fields{
|
||||
Filter: "start_date >= '2018-12-12T07:33:20+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task7,
|
||||
task9,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "range and nesting",
|
||||
fields: fields{
|
||||
Filter: "(start_date > '2018-12-12T00:00:00+00:00' && start_date < '2018-12-13T00:00:00+00:00') || end_date > '2018-12-13T00:00:00+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "undone tasks only",
|
||||
fields: fields{
|
||||
FilterBy: []string{"done"},
|
||||
FilterValue: []string{"false"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "done = false",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -892,9 +907,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "done tasks only",
|
||||
fields: fields{
|
||||
FilterBy: []string{"done"},
|
||||
FilterValue: []string{"true"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "done = true",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -905,9 +918,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "done tasks only - not equals done",
|
||||
fields: fields{
|
||||
FilterBy: []string{"done"},
|
||||
FilterValue: []string{"false"},
|
||||
FilterComparator: []string{"not_equals"},
|
||||
Filter: "done != false",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -918,10 +929,8 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "range with nulls",
|
||||
fields: fields{
|
||||
FilterBy: []string{"start_date", "end_date"},
|
||||
FilterValue: []string{"2018-12-11T03:46:40+00:00", "2018-12-13T11:20:01+00:00"},
|
||||
FilterComparator: []string{"greater", "less"},
|
||||
FilterIncludeNulls: true,
|
||||
Filter: "start_date > '2018-12-11T03:46:40+00:00' || end_date < '2018-12-13T11:20:01+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -976,9 +985,26 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "filtered with like",
|
||||
fields: fields{
|
||||
FilterBy: []string{"title"},
|
||||
FilterValue: []string{"with"},
|
||||
FilterComparator: []string{"like"},
|
||||
Filter: "title ~ with",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task27,
|
||||
task28,
|
||||
task29,
|
||||
task30,
|
||||
task31,
|
||||
task33,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filtered with like and '",
|
||||
fields: fields{
|
||||
Filter: "title ~ 'with'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -997,9 +1023,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "filtered reminder dates",
|
||||
fields: fields{
|
||||
FilterBy: []string{"reminders", "reminders"},
|
||||
FilterValue: []string{"2018-10-01T00:00:00+00:00", "2018-12-10T00:00:00+00:00"},
|
||||
FilterComparator: []string{"greater", "less"},
|
||||
Filter: "reminders > '2018-10-01T00:00:00+00:00' && reminders < '2018-12-10T00:00:00+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -1008,12 +1032,22 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter in keyword",
|
||||
fields: fields{
|
||||
Filter: "id in '1,2,34'", // user does not have permission to access task 34
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter in",
|
||||
fields: fields{
|
||||
FilterBy: []string{"id"},
|
||||
FilterValue: []string{"1,2,34"}, // Task 34 is forbidden for user 1
|
||||
FilterComparator: []string{"in"},
|
||||
Filter: "id ?= '1,2,34'", // user does not have permission to access task 34
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -1025,9 +1059,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "filter assignees by username",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"user1"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "assignees = 'user1'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -1038,9 +1070,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "filter assignees by username with users field name",
|
||||
fields: fields{
|
||||
FilterBy: []string{"users"},
|
||||
FilterValue: []string{"user1"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "users = 'user1'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: nil,
|
||||
@ -1049,9 +1079,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "filter assignees by username with user_id field name",
|
||||
fields: fields{
|
||||
FilterBy: []string{"user_id"},
|
||||
FilterValue: []string{"user1"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "user_id = 'user1'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: nil,
|
||||
@ -1060,9 +1088,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "filter assignees by multiple username",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees", "assignees"},
|
||||
FilterValue: []string{"user1", "user2"},
|
||||
FilterComparator: []string{"equals", "equals"},
|
||||
Filter: "assignees = 'user1' || assignees = 'user2'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -1074,9 +1100,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "filter assignees by numbers",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"1"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "assignees = 1",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
@ -1085,20 +1109,51 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "filter assignees by name with like",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"user"},
|
||||
FilterComparator: []string{"like"},
|
||||
Filter: "assignees ~ 'user'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
// Same as without any filter since the filter is ignored
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task11,
|
||||
task12,
|
||||
task15,
|
||||
task16,
|
||||
task17,
|
||||
task18,
|
||||
task19,
|
||||
task20,
|
||||
task21,
|
||||
task22,
|
||||
task23,
|
||||
task24,
|
||||
task25,
|
||||
task26,
|
||||
task27,
|
||||
task28,
|
||||
task29,
|
||||
task30,
|
||||
task31,
|
||||
task32,
|
||||
task33,
|
||||
task35,
|
||||
task39,
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter assignees in by id",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"1,2"},
|
||||
FilterComparator: []string{"in"},
|
||||
Filter: "assignees ?= '1,2'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
@ -1107,9 +1162,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "filter assignees in by username",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"user1,user2"},
|
||||
FilterComparator: []string{"in"},
|
||||
Filter: "assignees ?= 'user1,user2'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -1121,9 +1174,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
{
|
||||
name: "filter labels",
|
||||
fields: fields{
|
||||
FilterBy: []string{"labels"},
|
||||
FilterValue: []string{"4"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "labels = 4",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -1134,11 +1185,9 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter project",
|
||||
name: "filter project_id",
|
||||
fields: fields{
|
||||
FilterBy: []string{"project_id"},
|
||||
FilterValue: []string{"6"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "project_id = 6",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -1146,13 +1195,31 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter project",
|
||||
fields: fields{
|
||||
Filter: "project = 6",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task15,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter project forbidden",
|
||||
fields: fields{
|
||||
Filter: "project_id = 20", // user1 has no access to project 20
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
wantErr: false,
|
||||
},
|
||||
// TODO filter parent project?
|
||||
{
|
||||
name: "filter by index",
|
||||
fields: fields{
|
||||
FilterBy: []string{"index"},
|
||||
FilterValue: []string{"5"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "index = 5",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
@ -1321,6 +1388,8 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
task9,
|
||||
},
|
||||
},
|
||||
// TODO unix dates
|
||||
// TODO date magic
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@ -1334,11 +1403,10 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
SortBy: tt.fields.SortBy,
|
||||
OrderBy: tt.fields.OrderBy,
|
||||
|
||||
FilterBy: tt.fields.FilterBy,
|
||||
FilterValue: tt.fields.FilterValue,
|
||||
FilterComparator: tt.fields.FilterComparator,
|
||||
FilterIncludeNulls: tt.fields.FilterIncludeNulls,
|
||||
|
||||
Filter: tt.fields.Filter,
|
||||
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
|
@ -76,23 +76,21 @@ func getOrderByDBStatement(opts *taskSearchOptions) (orderby string, err error)
|
||||
return
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
||||
func convertFiltersToDBFilterCond(rawFilters []*taskFilter, includeNulls bool) (filterCond builder.Cond, err error) {
|
||||
|
||||
orderby, err := getOrderByDBStatement(opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Some filters need a special treatment since they are in a separate table
|
||||
reminderFilters := []builder.Cond{}
|
||||
assigneeFilters := []builder.Cond{}
|
||||
labelFilters := []builder.Cond{}
|
||||
projectFilters := []builder.Cond{}
|
||||
|
||||
var filters = make([]builder.Cond, 0, len(opts.filters))
|
||||
var dbFilters = make([]builder.Cond, 0, len(rawFilters))
|
||||
// To still find tasks with nil values, we exclude 0s when comparing with >/< values.
|
||||
for _, f := range opts.filters {
|
||||
for _, f := range rawFilters {
|
||||
|
||||
if nested, is := f.value.([]*taskFilter); is {
|
||||
nestedDBFilters, err := convertFiltersToDBFilterCond(nested, includeNulls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbFilters = append(dbFilters, nestedDBFilters)
|
||||
continue
|
||||
}
|
||||
|
||||
if f.field == "reminders" {
|
||||
filter, err := getFilterCond(&taskFilter{
|
||||
// recreating the struct here to avoid modifying it when reusing the opts struct
|
||||
@ -100,17 +98,17 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
||||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
}, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
reminderFilters = append(reminderFilters, filter)
|
||||
dbFilters = append(dbFilters, getFilterCondForSeparateTable("task_reminders", filter))
|
||||
continue
|
||||
}
|
||||
|
||||
if f.field == "assignees" {
|
||||
if f.comparator == taskFilterComparatorLike {
|
||||
return nil, totalCount, err
|
||||
return
|
||||
}
|
||||
filter, err := getFilterCond(&taskFilter{
|
||||
// recreating the struct here to avoid modifying it when reusing the opts struct
|
||||
@ -118,11 +116,17 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
||||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
}, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
assigneeFilters = append(assigneeFilters, filter)
|
||||
|
||||
assigneeFilter := builder.In("user_id",
|
||||
builder.Select("id").
|
||||
From("users").
|
||||
Where(filter),
|
||||
)
|
||||
dbFilters = append(dbFilters, getFilterCondForSeparateTable("task_assignees", assigneeFilter))
|
||||
continue
|
||||
}
|
||||
|
||||
@ -133,11 +137,12 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
||||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
}, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
labelFilters = append(labelFilters, filter)
|
||||
|
||||
dbFilters = append(dbFilters, getFilterCondForSeparateTable("label_tasks", filter))
|
||||
continue
|
||||
}
|
||||
|
||||
@ -148,19 +153,60 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
||||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
}, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
projectFilters = append(projectFilters, filter)
|
||||
|
||||
cond := builder.In(
|
||||
"project_id",
|
||||
builder.
|
||||
Select("id").
|
||||
From("projects").
|
||||
Where(filter),
|
||||
)
|
||||
dbFilters = append(dbFilters, cond)
|
||||
continue
|
||||
}
|
||||
|
||||
filter, err := getFilterCond(f, opts.filterIncludeNulls)
|
||||
filter, err := getFilterCond(f, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
filters = append(filters, filter)
|
||||
dbFilters = append(dbFilters, filter)
|
||||
}
|
||||
|
||||
if len(dbFilters) > 0 {
|
||||
if len(dbFilters) == 1 {
|
||||
filterCond = dbFilters[0]
|
||||
} else {
|
||||
for i, f := range dbFilters {
|
||||
if len(dbFilters) > i+1 {
|
||||
switch rawFilters[i+1].join {
|
||||
case filterConcatOr:
|
||||
filterCond = builder.Or(filterCond, f, dbFilters[i+1])
|
||||
case filterConcatAnd:
|
||||
filterCond = builder.And(filterCond, f, dbFilters[i+1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filterCond, nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
||||
|
||||
orderby, err := getOrderByDBStatement(opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
filterCond, err := convertFiltersToDBFilterCond(opts.parsedFilters, opts.filterIncludeNulls)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Then return all tasks for that projects
|
||||
@ -199,53 +245,6 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
||||
favoritesCond = builder.In("id", favCond)
|
||||
}
|
||||
|
||||
if len(reminderFilters) > 0 {
|
||||
filters = append(filters, getFilterCondForSeparateTable("task_reminders", opts.filterConcat, reminderFilters))
|
||||
}
|
||||
|
||||
if len(assigneeFilters) > 0 {
|
||||
assigneeFilter := []builder.Cond{
|
||||
builder.In("user_id",
|
||||
builder.Select("id").
|
||||
From("users").
|
||||
Where(builder.Or(assigneeFilters...)),
|
||||
)}
|
||||
filters = append(filters, getFilterCondForSeparateTable("task_assignees", opts.filterConcat, assigneeFilter))
|
||||
}
|
||||
|
||||
if len(labelFilters) > 0 {
|
||||
filters = append(filters, getFilterCondForSeparateTable("label_tasks", opts.filterConcat, labelFilters))
|
||||
}
|
||||
|
||||
if len(projectFilters) > 0 {
|
||||
var filtercond builder.Cond
|
||||
if opts.filterConcat == filterConcatOr {
|
||||
filtercond = builder.Or(projectFilters...)
|
||||
}
|
||||
if opts.filterConcat == filterConcatAnd {
|
||||
filtercond = builder.And(projectFilters...)
|
||||
}
|
||||
|
||||
cond := builder.In(
|
||||
"project_id",
|
||||
builder.
|
||||
Select("id").
|
||||
From("projects").
|
||||
Where(filtercond),
|
||||
)
|
||||
filters = append(filters, cond)
|
||||
}
|
||||
|
||||
var filterCond builder.Cond
|
||||
if len(filters) > 0 {
|
||||
if opts.filterConcat == filterConcatOr {
|
||||
filterCond = builder.Or(filters...)
|
||||
}
|
||||
if opts.filterConcat == filterConcatAnd {
|
||||
filterCond = builder.And(filters...)
|
||||
}
|
||||
}
|
||||
|
||||
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
||||
cond := builder.And(builder.Or(projectIDCond, favoritesCond), where, filterCond)
|
||||
|
||||
@ -316,41 +315,23 @@ func convertFilterValues(value interface{}) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
||||
// Parsing and rebuilding the filter for Typesense has the advantage that we have more control over
|
||||
// what Typesense finally gets to see.
|
||||
func convertParsedFilterToTypesense(rawFilters []*taskFilter) (filterBy string, err error) {
|
||||
|
||||
var sortbyFields []string
|
||||
for i, param := range opts.sortby {
|
||||
// Validate the params
|
||||
if err := param.validate(); err != nil {
|
||||
return nil, totalCount, err
|
||||
filters := []string{}
|
||||
|
||||
for _, f := range rawFilters {
|
||||
|
||||
if nested, is := f.value.([]*taskFilter); is {
|
||||
nestedDBFilters, err := convertParsedFilterToTypesense(nested)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
filters = append(filters, "("+nestedDBFilters+")")
|
||||
continue
|
||||
}
|
||||
|
||||
// Typesense does not allow sorting by ID, so we sort by created timestamp instead
|
||||
if param.sortBy == "id" {
|
||||
param.sortBy = "created"
|
||||
}
|
||||
|
||||
sortbyFields = append(sortbyFields, param.sortBy+"(missing_values:last):"+param.orderBy.String())
|
||||
|
||||
if i == 2 {
|
||||
// Typesense supports up to 3 sorting parameters
|
||||
// https://typesense.org/docs/0.25.0/api/search.html#ranking-and-sorting-parameters
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sortby := strings.Join(sortbyFields, ",")
|
||||
|
||||
projectIDStrings := []string{}
|
||||
for _, id := range opts.projectIDs {
|
||||
projectIDStrings = append(projectIDStrings, strconv.FormatInt(id, 10))
|
||||
}
|
||||
filterBy := []string{
|
||||
"project_id: [" + strings.Join(projectIDStrings, ", ") + "]",
|
||||
}
|
||||
|
||||
for _, f := range opts.filters {
|
||||
|
||||
if f.field == "reminders" {
|
||||
f.field = "reminders.reminder"
|
||||
}
|
||||
@ -363,6 +344,10 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
|
||||
f.field = "labels.id"
|
||||
}
|
||||
|
||||
if f.field == "project" {
|
||||
f.field = "project_id"
|
||||
}
|
||||
|
||||
filter := f.field
|
||||
|
||||
switch f.comparator {
|
||||
@ -394,7 +379,67 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
|
||||
filter += "]"
|
||||
}
|
||||
|
||||
filterBy = append(filterBy, filter)
|
||||
filters = append(filters, filter)
|
||||
}
|
||||
|
||||
if len(filters) > 0 {
|
||||
if len(filters) == 1 {
|
||||
filterBy = filters[0]
|
||||
} else {
|
||||
for i, f := range filters {
|
||||
if len(filters) > i+1 {
|
||||
switch rawFilters[i+1].join {
|
||||
case filterConcatOr:
|
||||
filterBy = f + " || " + filters[i+1]
|
||||
case filterConcatAnd:
|
||||
filterBy = f + " && " + filters[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
||||
|
||||
var sortbyFields []string
|
||||
for i, param := range opts.sortby {
|
||||
// Validate the params
|
||||
if err := param.validate(); err != nil {
|
||||
return nil, totalCount, err
|
||||
}
|
||||
|
||||
// Typesense does not allow sorting by ID, so we sort by created timestamp instead
|
||||
if param.sortBy == "id" {
|
||||
param.sortBy = "created"
|
||||
}
|
||||
|
||||
sortbyFields = append(sortbyFields, param.sortBy+"(missing_values:last):"+param.orderBy.String())
|
||||
|
||||
if i == 2 {
|
||||
// Typesense supports up to 3 sorting parameters
|
||||
// https://typesense.org/docs/0.25.0/api/search.html#ranking-and-sorting-parameters
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sortby := strings.Join(sortbyFields, ",")
|
||||
|
||||
projectIDStrings := []string{}
|
||||
for _, id := range opts.projectIDs {
|
||||
projectIDStrings = append(projectIDStrings, strconv.FormatInt(id, 10))
|
||||
}
|
||||
|
||||
filter, err := convertParsedFilterToTypesense(opts.parsedFilters)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
filterBy := []string{
|
||||
"project_id: [" + strings.Join(projectIDStrings, ", ") + "]",
|
||||
"(" + filter + ")",
|
||||
}
|
||||
|
||||
////////////////
|
||||
|
@ -162,8 +162,8 @@ func (t *Task) GetFrontendURL() string {
|
||||
type taskFilterConcatinator string
|
||||
|
||||
const (
|
||||
filterConcatAnd = "and"
|
||||
filterConcatOr = "or"
|
||||
filterConcatAnd taskFilterConcatinator = "and"
|
||||
filterConcatOr taskFilterConcatinator = "or"
|
||||
)
|
||||
|
||||
type taskSearchOptions struct {
|
||||
@ -171,9 +171,9 @@ type taskSearchOptions struct {
|
||||
page int
|
||||
perPage int
|
||||
sortby []*sortParam
|
||||
filters []*taskFilter
|
||||
filterConcat taskFilterConcatinator
|
||||
parsedFilters []*taskFilter
|
||||
filterIncludeNulls bool
|
||||
filter string
|
||||
projectIDs []int64
|
||||
}
|
||||
|
||||
@ -238,21 +238,13 @@ func getFilterCond(f *taskFilter, includeNulls bool) (cond builder.Cond, err err
|
||||
return
|
||||
}
|
||||
|
||||
func getFilterCondForSeparateTable(table string, concat taskFilterConcatinator, conds []builder.Cond) builder.Cond {
|
||||
var filtercond builder.Cond
|
||||
if concat == filterConcatOr {
|
||||
filtercond = builder.Or(conds...)
|
||||
}
|
||||
if concat == filterConcatAnd {
|
||||
filtercond = builder.And(conds...)
|
||||
}
|
||||
|
||||
func getFilterCondForSeparateTable(table string, cond builder.Cond) builder.Cond {
|
||||
return builder.In(
|
||||
"id",
|
||||
builder.
|
||||
Select("task_id").
|
||||
From(table).
|
||||
Where(filtercond),
|
||||
Where(cond),
|
||||
)
|
||||
}
|
||||
|
||||
@ -273,11 +265,6 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
|
||||
return nil, 0, 0, nil
|
||||
}
|
||||
|
||||
// Set the default concatinator of filter variables to or if none was provided
|
||||
if opts.filterConcat == "" {
|
||||
opts.filterConcat = filterConcatOr
|
||||
}
|
||||
|
||||
// Get all project IDs and get the tasks
|
||||
opts.projectIDs = []int64{}
|
||||
var hasFavoritesProject bool
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Package swagger Code generated by swaggo/swag. DO NOT EDIT
|
||||
// Code generated by swaggo/swag. DO NOT EDIT.
|
||||
|
||||
package swagger
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
@ -1937,26 +1938,8 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"name": "filter_by",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The value to filter for.",
|
||||
"name": "filter_value",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "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` + "`" + `",
|
||||
"name": "filter_comparator",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The concatinator to use for filters. Available values are ` + "`" + `and` + "`" + ` or ` + "`" + `or` + "`" + `. Defaults to ` + "`" + `or` + "`" + `.",
|
||||
"name": "filter_concat",
|
||||
"description": "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature.",
|
||||
"name": "filter",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@ -2169,26 +2152,8 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"name": "filter_by",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)- or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style relative dates for all date fields like ` + "`" + `due_date` + "`" + `, ` + "`" + `start_date` + "`" + `, ` + "`" + `end_date` + "`" + `, etc.",
|
||||
"name": "filter_value",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "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` + "`" + `",
|
||||
"name": "filter_comparator",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The concatinator to use for filters. Available values are ` + "`" + `and` + "`" + ` or ` + "`" + `or` + "`" + `. Defaults to ` + "`" + `or` + "`" + `.",
|
||||
"name": "filter_concat",
|
||||
"description": "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature.",
|
||||
"name": "filter",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@ -8132,35 +8097,13 @@ const docTemplate = `{
|
||||
"models.TaskCollection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter_by": {
|
||||
"description": "The field name of the field to filter by",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"filter_comparator": {
|
||||
"description": "The comparator for field and value",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"filter_concat": {
|
||||
"description": "The way all filter conditions are concatenated together, can be either \"and\" or \"or\".,",
|
||||
"filter": {
|
||||
"type": "string"
|
||||
},
|
||||
"filter_include_nulls": {
|
||||
"description": "If set to true, the result will also include null values",
|
||||
"type": "boolean"
|
||||
},
|
||||
"filter_value": {
|
||||
"description": "The value of the field name to filter by",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"order_by": {
|
||||
"description": "The query parameter to order the items by. This can be either asc or desc, with asc being the default.",
|
||||
"type": "array",
|
||||
@ -9001,8 +8944,6 @@ var SwaggerInfo = &swag.Spec{
|
||||
Description: "# Pagination\nEvery endpoint capable of pagination will return two headers:\n* `x-pagination-total-pages`: The total number of available pages for this request\n* `x-pagination-result-count`: The number of items returned for this request.\n# Rights\nAll endpoints which return a single item (project, task, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.\nThis can be used to show or hide ui elements based on the rights the user has.\n# Errors\nAll errors have an error code and a human-readable error message in addition to the http status code. You should always check for the status code in the response, not only the http status code.\nDue to limitations in the swagger library we're using for this document, only one error per http status code is documented here. Make sure to check the [error docs](https://vikunja.io/docs/errors/) in Vikunja's documentation for a full list of available error codes.\n# Authorization\n**JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer <jwt-token>`-header to authenticate successfully.\n\n**API Token:** You can create scoped API tokens for your user and use the token to make authenticated requests in the context of that user. The token must be provided via an `Authorization: Bearer <token>` header, similar to jwt auth. See the documentation for the `api` group to manage token creation and revocation.\n\n**BasicAuth:** Only used when requesting tasks via CalDAV.\n<!-- ReDoc-Inject: <security-definitions> -->",
|
||||
InfoInstanceName: "swagger",
|
||||
SwaggerTemplate: docTemplate,
|
||||
LeftDelim: "{{",
|
||||
RightDelim: "}}",
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -1929,26 +1929,8 @@
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"name": "filter_by",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The value to filter for.",
|
||||
"name": "filter_value",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "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`",
|
||||
"name": "filter_comparator",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`.",
|
||||
"name": "filter_concat",
|
||||
"description": "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature.",
|
||||
"name": "filter",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@ -2161,26 +2143,8 @@
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"name": "filter_by",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)- or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style relative dates for all date fields like `due_date`, `start_date`, `end_date`, etc.",
|
||||
"name": "filter_value",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "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`",
|
||||
"name": "filter_comparator",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`.",
|
||||
"name": "filter_concat",
|
||||
"description": "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature.",
|
||||
"name": "filter",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@ -8124,35 +8088,13 @@
|
||||
"models.TaskCollection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter_by": {
|
||||
"description": "The field name of the field to filter by",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"filter_comparator": {
|
||||
"description": "The comparator for field and value",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"filter_concat": {
|
||||
"description": "The way all filter conditions are concatenated together, can be either \"and\" or \"or\".,",
|
||||
"filter": {
|
||||
"type": "string"
|
||||
},
|
||||
"filter_include_nulls": {
|
||||
"description": "If set to true, the result will also include null values",
|
||||
"type": "boolean"
|
||||
},
|
||||
"filter_value": {
|
||||
"description": "The value of the field name to filter by",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"order_by": {
|
||||
"description": "The query parameter to order the items by. This can be either asc or desc, with asc being the default.",
|
||||
"type": "array",
|
||||
|
@ -783,28 +783,11 @@ definitions:
|
||||
type: object
|
||||
models.TaskCollection:
|
||||
properties:
|
||||
filter_by:
|
||||
description: The field name of the field to filter by
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
filter_comparator:
|
||||
description: The comparator for field and value
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
filter_concat:
|
||||
description: The way all filter conditions are concatenated together, can
|
||||
be either "and" or "or".,
|
||||
filter:
|
||||
type: string
|
||||
filter_include_nulls:
|
||||
description: If set to true, the result will also include null values
|
||||
type: boolean
|
||||
filter_value:
|
||||
description: The value of the field name to filter by
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
order_by:
|
||||
description: The query parameter to order the items by. This can be either
|
||||
asc or desc, with asc being the default.
|
||||
@ -2718,27 +2701,10 @@ paths:
|
||||
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 filter query to match tasks by. Check out https://vikunja.io/docs/filters
|
||||
for a full explanation of the feature.
|
||||
in: query
|
||||
name: filter_by
|
||||
type: string
|
||||
- description: The value to filter for.
|
||||
in: query
|
||||
name: filter_value
|
||||
type: string
|
||||
- description: 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`
|
||||
in: query
|
||||
name: filter_comparator
|
||||
type: string
|
||||
- description: The concatinator to use for filters. Available values are `and`
|
||||
or `or`. Defaults to `or`.
|
||||
in: query
|
||||
name: filter_concat
|
||||
name: filter
|
||||
type: string
|
||||
- description: If set to true the result will include filtered fields whose
|
||||
value is set to `null`. Available values are `true` or `false`. Defaults
|
||||
@ -2885,30 +2851,10 @@ paths:
|
||||
in: query
|
||||
name: order_by
|
||||
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 filter query to match tasks by. Check out https://vikunja.io/docs/filters
|
||||
for a full explanation of the feature.
|
||||
in: query
|
||||
name: filter_by
|
||||
type: string
|
||||
- description: The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)-
|
||||
or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style
|
||||
relative dates for all date fields like `due_date`, `start_date`, `end_date`,
|
||||
etc.
|
||||
in: query
|
||||
name: filter_value
|
||||
type: string
|
||||
- description: 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`
|
||||
in: query
|
||||
name: filter_comparator
|
||||
type: string
|
||||
- description: The concatinator to use for filters. Available values are `and`
|
||||
or `or`. Defaults to `or`.
|
||||
in: query
|
||||
name: filter_concat
|
||||
name: filter
|
||||
type: string
|
||||
- description: If set to true the result will include filtered fields whose
|
||||
value is set to `null`. Available values are `true` or `false`. Defaults
|
||||
|
Reference in New Issue
Block a user