Add task filter for reminders (#745)
continuous-integration/drone/push Build is passing Details

Update swagger docs about reminders

Fix filter concat for reminders

Add task filter for reminders

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#745
Co-Authored-By: konrad <konrad@kola-entertainments.de>
Co-Committed-By: konrad <konrad@kola-entertainments.de>
This commit is contained in:
konrad 2020-12-19 15:14:20 +00:00
parent 9508d0faee
commit 92bcce3f7c
7 changed files with 91 additions and 45 deletions

View File

@ -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`."

View File

@ -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))
}

View File

@ -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 {

View File

@ -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)

View File

@ -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"
},

View File

@ -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"
},

View File

@ -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