diff --git a/go.mod b/go.mod index dedf2a2a..01adc737 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( github.com/swaggo/swag v1.8.0 github.com/tkuchiki/go-timezone v0.2.2 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 golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 golang.org/x/image v0.0.0-20220302094943-723b81ca9867 diff --git a/go.sum b/go.sum index a2d33181..0d53dc01 100644 --- a/go.sum +++ b/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/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/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/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= diff --git a/pkg/integrations/task_collection_test.go b/pkg/integrations/task_collection_test.go index bbd64c25..28eaef00 100644 --- a/pkg/integrations/task_collection_test.go +++ b/pkg/integrations/task_collection_test.go @@ -244,12 +244,37 @@ func TestTaskCollection(t *testing.T) { // the current date. 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) { _, err := testHandler.testReadAllWithUser( url.Values{ "filter_by": []string{"due_date"}, - "filter_value": []string{"1540000000"}, + "filter_value": []string{"invalid"}, "filter_comparator": []string{"greater"}, }, nil, @@ -451,7 +476,7 @@ func TestTaskCollection(t *testing.T) { _, err := testHandler.testReadAllWithUser( url.Values{ "filter_by": []string{"due_date"}, - "filter_value": []string{"1540000000"}, + "filter_value": []string{"invalid"}, "filter_comparator": []string{"greater"}, }, nil, diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index 70be8d74..f6a22a12 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -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 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." +// @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_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`." diff --git a/pkg/models/task_collection_filter.go b/pkg/models/task_collection_filter.go index 4b8ed142..0e86a683 100644 --- a/pkg/models/task_collection_filter.go +++ b/pkg/models/task_collection_filter.go @@ -25,6 +25,7 @@ import ( "code.vikunja.io/api/pkg/config" "github.com/iancoleman/strcase" + "github.com/vectordotdev/go-datemath" "xorm.io/xorm/schemas" ) @@ -159,8 +160,14 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa value, err = strconv.ParseBool(rawValue) case reflect.Struct: if field.Type == schemas.TimeType { - value, err = time.Parse(time.RFC3339, rawValue) - value = value.(time.Time).In(config.GetTimeZone()) + 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 = value.(time.Time).In(config.GetTimeZone()) + } } case reflect.Slice: // If this is a slice of pointers we're dealing with some property which is a relation diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index ca64c34a..ab411d3f 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1995,7 +1995,7 @@ var doc = `{ }, { "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", "in": "query" }, diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 2e5d604a..01a96a8a 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -1979,7 +1979,7 @@ }, { "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", "in": "query" }, diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 5f72c4ce..b3e1849a 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -2898,7 +2898,10 @@ paths: in: query name: filter_by 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 name: filter_value type: string