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 <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/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 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 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. 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_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_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`." // @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, err = time.Parse(time.RFC3339, value)
nativeValue = nativeValue.(time.Time).In(config.GetTimeZone()) 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: default:
panic(fmt.Errorf("unrecognized filter type %s for field %s, value %s", field.Type.String(), fieldName, value)) 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, 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 { 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 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 //nolint:gocyclo
func getRawTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) { 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)) var filters = make([]builder.Cond, 0, len(opts.filters))
// To still find tasks with nil values, we exclude 0s when comparing with >/< values. // To still find tasks with nil values, we exclude 0s when comparing with >/< values.
for _, f := range opts.filters { for _, f := range opts.filters {
switch f.comparator { if f.field == "reminders" {
case taskFilterComparatorEquals: f.field = "reminder" // This is the name in the db
filters = append(filters, &builder.Eq{f.field: f.value}) filter, err := getFilterCond(f, opts.filterIncludeNulls)
case taskFilterComparatorNotEquals: if err != nil {
filters = append(filters, &builder.Neq{f.field: f.value}) return nil, 0, 0, err
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})
} }
case taskFilterComparatorGreateEquals: reminderFilters = append(reminderFilters, filter)
if opts.filterIncludeNulls { continue
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 { filter, err := getFilterCond(f, opts.filterIncludeNulls)
filters = append(filters, builder.Or(&builder.Lt{f.field: f.value}, &builder.IsNull{f.field})) if err != nil {
} else { return nil, 0, 0, err
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
} }
filters = append(filters, filter)
} }
// Then return all tasks for that lists // 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))) 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) query = query.Where(listCond)
queryCount = queryCount.Where(listCond) queryCount = queryCount.Where(listCond)

View file

@ -1892,7 +1892,7 @@ var doc = `{
}, },
{ {
"type": "string", "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", "name": "filter_by",
"in": "query" "in": "query"
}, },

View file

@ -1875,7 +1875,7 @@
}, },
{ {
"type": "string", "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", "name": "filter_by",
"in": "query" "in": "query"
}, },

View file

@ -2443,7 +2443,7 @@ paths:
in: query in: query
name: order_by name: order_by
type: string 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 in: query
name: filter_by name: filter_by
type: string type: string