diff --git a/pkg/models/task_collection_filter.go b/pkg/models/task_collection_filter.go index d285c60e2..a5017edd9 100644 --- a/pkg/models/task_collection_filter.go +++ b/pkg/models/task_collection_filter.go @@ -90,6 +90,60 @@ func parseTimeFromUserInput(timeString string) (value time.Time, err error) { return value.In(config.GetTimeZone()), err } +func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error) { + filter = &taskFilter{ + join: filterConcatAnd, + } + if f.Join == fexpr.JoinOr { + filter.join = filterConcatOr + } + + var value string + switch v := f.Item.(type) { + case fexpr.Expr: + filter.field = v.Left.Literal + value = v.Right.Literal + filter.comparator, err = getFilterComparatorFromOp(v.Op) + if err != nil { + return + } + case []fexpr.ExprGroup: + values := make([]*taskFilter, 0, len(v)) + for _, expression := range v { + subfilter, err := parseFilterFromExpression(expression) + if err != nil { + return nil, err + } + values = append(values, subfilter) + } + filter.value = values + return + } + + err = validateTaskFieldComparator(filter.comparator) + if err != nil { + return + } + + // Cast the field value to its native type + var reflectValue *reflect.StructField + if filter.field == "project" { + filter.field = "project_id" + } + reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, value) + if err != nil { + return nil, ErrInvalidTaskFilterValue{ + Value: filter.field, + Field: value, + } + } + if reflectValue != nil { + filter.isNumeric = reflectValue.Type.Kind() == reflect.Int64 + } + + return filter, nil +} + func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err error) { if c.Filter == "" { @@ -121,46 +175,10 @@ func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err filters = make([]*taskFilter, 0, len(parsedFilter)) for _, f := range parsedFilter { - - filter := &taskFilter{ - join: filterConcatAnd, - } - if f.Join == fexpr.JoinOr { - filter.join = filterConcatOr - } - - var value string - switch v := f.Item.(type) { - case fexpr.Expr: - filter.field = v.Left.Literal - value = v.Right.Literal // TODO: nesting - filter.comparator, err = getFilterComparatorFromOp(v.Op) - if err != nil { - return - } - } - - err = validateTaskFieldComparator(filter.comparator) + filter, err := parseFilterFromExpression(f) if err != nil { - return + return nil, err } - - // Cast the field value to its native type - var reflectValue *reflect.StructField - if filter.field == "project" { - filter.field = "project_id" - } - reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, value) - if err != nil { - return nil, ErrInvalidTaskFilterValue{ - Value: filter.field, - Field: value, - } - } - if reflectValue != nil { - filter.isNumeric = reflectValue.Type.Kind() == reflect.Int64 - } - filters = append(filters, filter) } diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index ad77dba7a..494135fc8 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -851,6 +851,19 @@ func TestTaskCollection_ReadAll(t *testing.T) { }, wantErr: false, }, + { + name: "range and nesting", + fields: fields{ + Filter: "(start_date > '2018-12-12T00:00:00+00:00' && start_date < '2018-12-13T00:00:00+00:00') || end_date > '2018-12-13T00:00:00+00:00'", + }, + args: defaultArgs, + want: []*Task{ + task7, + task8, + task9, + }, + wantErr: false, + }, { name: "undone tasks only", fields: fields{ @@ -1090,8 +1103,42 @@ func TestTaskCollection_ReadAll(t *testing.T) { fields: fields{ Filter: "assignees ~ 'user'", }, - args: defaultArgs, - want: []*Task{}, + args: defaultArgs, + want: []*Task{ + // Same as without any filter since the filter is ignored + task1, + task2, + task3, + task4, + task5, + task6, + task7, + task8, + task9, + task10, + task11, + task12, + task15, + task16, + task17, + task18, + task19, + task20, + task21, + task22, + task23, + task24, + task25, + task26, + task27, + task28, + task29, + task30, + task31, + task32, + task33, + task35, + }, wantErr: false, }, { diff --git a/pkg/models/task_search.go b/pkg/models/task_search.go index f936a86cf..aa2fae24c 100644 --- a/pkg/models/task_search.go +++ b/pkg/models/task_search.go @@ -76,17 +76,21 @@ func getOrderByDBStatement(opts *taskSearchOptions) (orderby string, err error) return } -//nolint:gocyclo -func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) { +func convertFiltersToDBFilterCond(rawFilters []*taskFilter, includeNulls bool) (filterCond builder.Cond, err error) { - orderby, err := getOrderByDBStatement(opts) - if err != nil { - return nil, 0, err - } - - var filters = make([]builder.Cond, 0, len(opts.filters)) + var dbFilters = make([]builder.Cond, 0, len(rawFilters)) // To still find tasks with nil values, we exclude 0s when comparing with >/< values. - for _, f := range opts.filters { + for _, f := range rawFilters { + + if nested, is := f.value.([]*taskFilter); is { + nestedDBFilters, err := convertFiltersToDBFilterCond(nested, includeNulls) + if err != nil { + return nil, err + } + dbFilters = append(dbFilters, nestedDBFilters) + continue + } + if f.field == "reminders" { filter, err := getFilterCond(&taskFilter{ // recreating the struct here to avoid modifying it when reusing the opts struct @@ -94,17 +98,17 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo value: f.value, comparator: f.comparator, isNumeric: f.isNumeric, - }, opts.filterIncludeNulls) + }, includeNulls) if err != nil { - return nil, totalCount, err + return nil, err } - filters = append(filters, getFilterCondForSeparateTable("task_reminders", filter)) + dbFilters = append(dbFilters, getFilterCondForSeparateTable("task_reminders", filter)) continue } if f.field == "assignees" { if f.comparator == taskFilterComparatorLike { - return nil, totalCount, err + return } filter, err := getFilterCond(&taskFilter{ // recreating the struct here to avoid modifying it when reusing the opts struct @@ -112,9 +116,9 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo value: f.value, comparator: f.comparator, isNumeric: f.isNumeric, - }, opts.filterIncludeNulls) + }, includeNulls) if err != nil { - return nil, totalCount, err + return nil, err } assigneeFilter := builder.In("user_id", @@ -122,7 +126,7 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo From("users"). Where(filter), ) - filters = append(filters, getFilterCondForSeparateTable("task_assignees", assigneeFilter)) + dbFilters = append(dbFilters, getFilterCondForSeparateTable("task_assignees", assigneeFilter)) continue } @@ -133,12 +137,12 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo value: f.value, comparator: f.comparator, isNumeric: f.isNumeric, - }, opts.filterIncludeNulls) + }, includeNulls) if err != nil { - return nil, totalCount, err + return nil, err } - filters = append(filters, getFilterCondForSeparateTable("label_tasks", filter)) + dbFilters = append(dbFilters, getFilterCondForSeparateTable("label_tasks", filter)) continue } @@ -149,9 +153,9 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo value: f.value, comparator: f.comparator, isNumeric: f.isNumeric, - }, opts.filterIncludeNulls) + }, includeNulls) if err != nil { - return nil, totalCount, err + return nil, err } cond := builder.In( @@ -161,15 +165,48 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo From("projects"). Where(filter), ) - filters = append(filters, cond) + dbFilters = append(dbFilters, cond) continue } - filter, err := getFilterCond(f, opts.filterIncludeNulls) + filter, err := getFilterCond(f, includeNulls) if err != nil { - return nil, totalCount, err + return nil, err } - filters = append(filters, filter) + dbFilters = append(dbFilters, filter) + } + + if len(dbFilters) > 0 { + if len(dbFilters) == 1 { + filterCond = dbFilters[0] + } else { + for i, f := range dbFilters { + if len(dbFilters) > i+1 { + switch rawFilters[i+1].join { + case filterConcatOr: + filterCond = builder.Or(filterCond, f, dbFilters[i+1]) + case filterConcatAnd: + filterCond = builder.And(filterCond, f, dbFilters[i+1]) + } + } + } + } + } + + return filterCond, nil +} + +//nolint:gocyclo +func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) { + + orderby, err := getOrderByDBStatement(opts) + if err != nil { + return nil, 0, err + } + + filterCond, err := convertFiltersToDBFilterCond(opts.filters, opts.filterIncludeNulls) + if err != nil { + return nil, 0, err } // Then return all tasks for that projects @@ -208,24 +245,6 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo favoritesCond = builder.In("id", favCond) } - var filterCond builder.Cond - if len(filters) > 0 { - if len(filters) == 1 { - filterCond = filters[0] - } else { - for i, f := range filters { - if len(filters) > i+1 { - switch opts.filters[i+1].join { - case filterConcatOr: - filterCond = builder.Or(filterCond, f, filters[i+1]) - case filterConcatAnd: - filterCond = builder.And(filterCond, f, filters[i+1]) - } - } - } - } - } - limit, start := getLimitFromPageIndex(opts.page, opts.perPage) cond := builder.And(builder.Or(projectIDCond, favoritesCond), where, filterCond)