From 92bcce3f7cc1eeba50233db0399fa88ce468cf33 Mon Sep 17 00:00:00 2001 From: konrad Date: Sat, 19 Dec 2020 15:14:20 +0000 Subject: [PATCH] Add task filter for reminders (#745) Update swagger docs about reminders Fix filter concat for reminders Add task filter for reminders Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/api/pulls/745 Co-Authored-By: konrad Co-Committed-By: konrad --- pkg/models/task_collection.go | 2 +- pkg/models/task_collection_filter.go | 8 ++ pkg/models/task_collection_test.go | 13 ++++ pkg/models/tasks.go | 107 +++++++++++++++++---------- pkg/swagger/docs.go | 2 +- pkg/swagger/swagger.json | 2 +- pkg/swagger/swagger.yaml | 2 +- 7 files changed, 91 insertions(+), 45 deletions(-) diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index 46d4408690b..3b3985b53af 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -91,7 +91,7 @@ func validateTaskField(fieldName string) error { // @Param s query string false "Search tasks by task text." // @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`." // @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`." -// @Param filter_by query string false "The name of the field to filter by. 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 except `labels`, `assignees`, `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_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` and `like`. Defaults to `equals`" // @Param filter_concat query string false "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`." diff --git a/pkg/models/task_collection_filter.go b/pkg/models/task_collection_filter.go index 5cb738f288c..d0022fbfd00 100644 --- a/pkg/models/task_collection_filter.go +++ b/pkg/models/task_collection_filter.go @@ -163,6 +163,14 @@ func getNativeValueForTaskField(fieldName, value string) (nativeValue interface{ nativeValue, err = time.Parse(time.RFC3339, value) nativeValue = nativeValue.(time.Time).In(config.GetTimeZone()) } + case reflect.Slice: + t := reflect.SliceOf(schemas.TimeType) + if t != nil { + nativeValue, err = time.Parse(time.RFC3339, value) + nativeValue = nativeValue.(time.Time).In(config.GetTimeZone()) + return + } + fallthrough default: panic(fmt.Errorf("unrecognized filter type %s for field %s, value %s", field.Type.String(), fieldName, value)) } diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index 071640d7820..c68f2d67d3f 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -879,6 +879,19 @@ func TestTaskCollection_ReadAll(t *testing.T) { }, wantErr: false, }, + { + name: "filtered reminders", + 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"}, + }, + args: defaultArgs, + want: []*Task{ + task27, + }, + wantErr: false, + }, } for _, tt := range tests { diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index fb3bdfd5ef8..03489b12c89 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -156,6 +156,37 @@ func (t *Task) ReadAll(a web.Auth, search string, page int, perPage int) (result return nil, 0, 0, nil } +func getFilterCond(f *taskFilter, includeNulls bool) (cond builder.Cond, err error) { + switch f.comparator { + case taskFilterComparatorEquals: + cond = &builder.Eq{f.field: f.value} + case taskFilterComparatorNotEquals: + cond = &builder.Neq{f.field: f.value} + case taskFilterComparatorGreater: + cond = &builder.Gt{f.field: f.value} + case taskFilterComparatorGreateEquals: + cond = &builder.Gte{f.field: f.value} + case taskFilterComparatorLess: + cond = &builder.Lt{f.field: f.value} + case taskFilterComparatorLessEquals: + cond = &builder.Lte{f.field: f.value} + case taskFilterComparatorLike: + val, is := f.value.(string) + if !is { + return nil, ErrInvalidTaskFilterValue{Field: f.field, Value: f.value} + } + cond = &builder.Like{f.field, "%" + val + "%"} + case taskFilterComparatorInvalid: + // Nothing to do + } + + if includeNulls { + cond = builder.Or(cond, &builder.IsNull{f.field}) + } + + return +} + //nolint:gocyclo func getRawTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) { @@ -215,52 +246,27 @@ func getRawTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks [] } } + // Reminder filters need a special treatment since they are in a separate database + reminderFilters := []builder.Cond{} + var filters = make([]builder.Cond, 0, len(opts.filters)) // To still find tasks with nil values, we exclude 0s when comparing with >/< values. for _, f := range opts.filters { - switch f.comparator { - case taskFilterComparatorEquals: - filters = append(filters, &builder.Eq{f.field: f.value}) - case taskFilterComparatorNotEquals: - filters = append(filters, &builder.Neq{f.field: f.value}) - case taskFilterComparatorGreater: - if opts.filterIncludeNulls { - filters = append(filters, builder.Or(&builder.Gt{f.field: f.value}, &builder.IsNull{f.field})) - } else { - filters = append(filters, &builder.Gt{f.field: f.value}) + if f.field == "reminders" { + f.field = "reminder" // This is the name in the db + filter, err := getFilterCond(f, opts.filterIncludeNulls) + if err != nil { + return nil, 0, 0, err } - case taskFilterComparatorGreateEquals: - if opts.filterIncludeNulls { - filters = append(filters, builder.Or(&builder.Gte{f.field: f.value}, &builder.IsNull{f.field})) - } else { - filters = append(filters, &builder.Gte{f.field: f.value}) - } - case taskFilterComparatorLess: - if opts.filterIncludeNulls { - filters = append(filters, builder.Or(&builder.Lt{f.field: f.value}, &builder.IsNull{f.field})) - } else { - filters = append(filters, &builder.Lt{f.field: f.value}) - } - case taskFilterComparatorLessEquals: - if opts.filterIncludeNulls { - filters = append(filters, builder.Or(&builder.Lte{f.field: f.value}, &builder.IsNull{f.field})) - } else { - filters = append(filters, &builder.Lte{f.field: f.value}) - } - case taskFilterComparatorLike: - val, is := f.value.(string) - if !is { - return nil, 0, 0, ErrInvalidTaskFilterValue{Field: f.field, Value: f.value} - } - c := &builder.Like{f.field, "%" + val + "%"} - if opts.filterIncludeNulls { - filters = append(filters, builder.Or(c, &builder.IsNull{f.field})) - } else { - filters = append(filters, c) - } - case taskFilterComparatorInvalid: - // Nothing to do + reminderFilters = append(reminderFilters, filter) + continue } + + filter, err := getFilterCond(f, opts.filterIncludeNulls) + if err != nil { + return nil, 0, 0, err + } + filters = append(filters, filter) } // Then return all tasks for that lists @@ -308,6 +314,25 @@ func getRawTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks [] listCond = builder.Or(listIDCond, builder.And(builder.Eq{"is_favorite": true}, builder.In("list_id", userListIDs))) } + if len(reminderFilters) > 0 { + var filtercond builder.Cond + if opts.filterConcat == filterConcatOr { + filtercond = builder.Or(reminderFilters...) + } + if opts.filterConcat == filterConcatAnd { + filtercond = builder.And(reminderFilters...) + } + reminderFilter := builder.In( + "id", + builder. + Select("task_id"). + From("task_reminders"). + Where(filtercond), + ) + + filters = append(filters, reminderFilter) + } + query = query.Where(listCond) queryCount = queryCount.Where(listCond) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 0c5c0ddd1ff..d76499811f8 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1892,7 +1892,7 @@ var doc = `{ }, { "type": "string", - "description": "The name of the field to filter by. 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 except ` + "`" + `labels` + "`" + `, ` + "`" + `assignees` + "`" + `, ` + "`" + `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.", "name": "filter_by", "in": "query" }, diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 45a6af5836b..f9fb78832d7 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -1875,7 +1875,7 @@ }, { "type": "string", - "description": "The name of the field to filter by. 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 except `labels`, `assignees`, `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.", "name": "filter_by", "in": "query" }, diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index a206f963dc5..87300dd6615 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -2443,7 +2443,7 @@ paths: in: query name: order_by type: string - - description: The name of the field to filter by. 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 except `labels`, `assignees`, `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. in: query name: filter_by type: string