feat: add date math for filters (#1086)
This adds support for relative dates in filters, similar to the ones from [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). In short, it allows you to filter for due dates by passing in dates like "now - 7d" to get a date from 7 days ago. This is a very powerful addition for saved filters as they will allow you to create filters for all kinds of stuff where you previously only could use fixed dates. Now you can for example create a saved filter for "all tasks this week". Frontend PR: https://kolaente.dev/vikunja/frontend/pulls/1342 Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/1086
This commit is contained in:
parent
d08dcc4e44
commit
0a1d8c9404
8 changed files with 46 additions and 8 deletions
1
go.mod
1
go.mod
|
@ -59,6 +59,7 @@ require (
|
||||||
github.com/swaggo/swag v1.8.0
|
github.com/swaggo/swag v1.8.0
|
||||||
github.com/tkuchiki/go-timezone v0.2.2
|
github.com/tkuchiki/go-timezone v0.2.2
|
||||||
github.com/ulule/limiter/v3 v3.10.0
|
github.com/ulule/limiter/v3 v3.10.0
|
||||||
|
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93
|
||||||
github.com/yuin/goldmark v1.4.8
|
github.com/yuin/goldmark v1.4.8
|
||||||
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70
|
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70
|
||||||
golang.org/x/image v0.0.0-20220302094943-723b81ca9867
|
golang.org/x/image v0.0.0-20220302094943-723b81ca9867
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -769,6 +769,8 @@ github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52
|
||||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93 h1:bT0ZMfsMi2Xh8dopgxhFT+OJH88QITHpdppdkG1rXJQ=
|
||||||
|
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93/go.mod h1:PnwzbSst7KD3vpBzzlntZU5gjVa455Uqa5QPiKSYJzQ=
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
|
|
|
@ -244,12 +244,37 @@ func TestTaskCollection(t *testing.T) {
|
||||||
// the current date.
|
// the current date.
|
||||||
assert.Equal(t, "[]\n", rec.Body.String())
|
assert.Equal(t, "[]\n", rec.Body.String())
|
||||||
})
|
})
|
||||||
|
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"},
|
||||||
|
},
|
||||||
|
urlParams,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||||
|
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||||
|
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||||
|
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||||
|
assert.Contains(t, rec.Body.String(), `task #5`)
|
||||||
|
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||||
|
assert.Contains(t, rec.Body.String(), `task #7`)
|
||||||
|
assert.NotContains(t, rec.Body.String(), `task #8`)
|
||||||
|
assert.Contains(t, rec.Body.String(), `task #9`)
|
||||||
|
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||||
|
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||||
|
assert.NotContains(t, rec.Body.String(), `task #12`)
|
||||||
|
assert.NotContains(t, rec.Body.String(), `task #13`)
|
||||||
|
assert.NotContains(t, rec.Body.String(), `task #14`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
t.Run("invalid date", func(t *testing.T) {
|
t.Run("invalid date", func(t *testing.T) {
|
||||||
_, err := testHandler.testReadAllWithUser(
|
_, err := testHandler.testReadAllWithUser(
|
||||||
url.Values{
|
url.Values{
|
||||||
"filter_by": []string{"due_date"},
|
"filter_by": []string{"due_date"},
|
||||||
"filter_value": []string{"1540000000"},
|
"filter_value": []string{"invalid"},
|
||||||
"filter_comparator": []string{"greater"},
|
"filter_comparator": []string{"greater"},
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
|
@ -451,7 +476,7 @@ func TestTaskCollection(t *testing.T) {
|
||||||
_, err := testHandler.testReadAllWithUser(
|
_, err := testHandler.testReadAllWithUser(
|
||||||
url.Values{
|
url.Values{
|
||||||
"filter_by": []string{"due_date"},
|
"filter_by": []string{"due_date"},
|
||||||
"filter_value": []string{"1540000000"},
|
"filter_value": []string{"invalid"},
|
||||||
"filter_comparator": []string{"greater"},
|
"filter_comparator": []string{"greater"},
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
|
|
|
@ -131,7 +131,7 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskOptions, err
|
||||||
// @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. 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. 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_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`."
|
||||||
// @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`."
|
// @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`."
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/config"
|
"code.vikunja.io/api/pkg/config"
|
||||||
"github.com/iancoleman/strcase"
|
"github.com/iancoleman/strcase"
|
||||||
|
"github.com/vectordotdev/go-datemath"
|
||||||
"xorm.io/xorm/schemas"
|
"xorm.io/xorm/schemas"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -159,9 +160,15 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa
|
||||||
value, err = strconv.ParseBool(rawValue)
|
value, err = strconv.ParseBool(rawValue)
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
if field.Type == schemas.TimeType {
|
if field.Type == schemas.TimeType {
|
||||||
|
var t datemath.Expression
|
||||||
|
t, err = datemath.Parse(rawValue)
|
||||||
|
if err == nil {
|
||||||
|
value = t.Time(datemath.WithLocation(config.GetTimeZone()))
|
||||||
|
} else {
|
||||||
value, err = time.Parse(time.RFC3339, rawValue)
|
value, err = time.Parse(time.RFC3339, rawValue)
|
||||||
value = value.(time.Time).In(config.GetTimeZone())
|
value = value.(time.Time).In(config.GetTimeZone())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
// If this is a slice of pointers we're dealing with some property which is a relation
|
// If this is a slice of pointers we're dealing with some property which is a relation
|
||||||
// In that case we don't really care about what the actual type is, we just cast the value to an
|
// In that case we don't really care about what the actual type is, we just cast the value to an
|
||||||
|
|
|
@ -1995,7 +1995,7 @@ var doc = `{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The value to filter for.",
|
"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",
|
"name": "filter_value",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1979,7 +1979,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The value to filter for.",
|
"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",
|
"name": "filter_value",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
|
|
|
@ -2898,7 +2898,10 @@ paths:
|
||||||
in: query
|
in: query
|
||||||
name: filter_by
|
name: filter_by
|
||||||
type: string
|
type: string
|
||||||
- description: The value to filter for.
|
- 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
|
in: query
|
||||||
name: filter_value
|
name: filter_value
|
||||||
type: string
|
type: string
|
||||||
|
|
Loading…
Reference in a new issue