1
0

Task filters (#243)

Fix not returning errors

Fix integration tests

Add more tests

Make task filtering actually work

Change tests

Fix using filter conditions

Fix test

Remove unused fields

Fix static check

Remove start and end date fields on task collection

Fix misspell

add filter logic when getting tasks

Add parsing filter query parameters into task filters

Start adding support for filters

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/243
This commit is contained in:
konrad
2020-04-11 14:20:33 +00:00
parent 0e2449482f
commit 0ba121fdfb
50 changed files with 7799 additions and 212 deletions

View File

@ -653,6 +653,60 @@ func (err ErrTaskCommentDoesNotExist) HTTPError() web.HTTPError {
}
}
// ErrInvalidTaskField represents an error where the provided task field is invalid
type ErrInvalidTaskField struct {
TaskField string
}
// IsErrInvalidTaskField checks if an error is ErrInvalidTaskField.
func IsErrInvalidTaskField(err error) bool {
_, ok := err.(ErrInvalidTaskField)
return ok
}
func (err ErrInvalidTaskField) Error() string {
return fmt.Sprintf("Task Field is invalid [TaskField: %s]", err.TaskField)
}
// ErrCodeInvalidTaskField holds the unique world-error code of this error
const ErrCodeInvalidTaskField = 4016
// HTTPError holds the http error description
func (err ErrInvalidTaskField) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusBadRequest,
Code: ErrCodeInvalidTaskField,
Message: fmt.Sprintf("The task field '%s' is invalid.", err.TaskField),
}
}
// ErrInvalidTaskFilterComparator represents an error where the provided task field is invalid
type ErrInvalidTaskFilterComparator struct {
Comparator taskFilterComparator
}
// IsErrInvalidTaskFilterComparator checks if an error is ErrInvalidTaskFilterComparator.
func IsErrInvalidTaskFilterComparator(err error) bool {
_, ok := err.(ErrInvalidTaskFilterComparator)
return ok
}
func (err ErrInvalidTaskFilterComparator) Error() string {
return fmt.Sprintf("Task filter comparator is invalid [Comparator: %s]", err.Comparator)
}
// ErrCodeInvalidTaskFilterComparator holds the unique world-error code of this error
const ErrCodeInvalidTaskFilterComparator = 4017
// HTTPError holds the http error description
func (err ErrInvalidTaskFilterComparator) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusBadRequest,
Code: ErrCodeInvalidTaskFilterComparator,
Message: fmt.Sprintf("The task filter comparator '%s' is invalid.", err.Comparator),
}
}
// =================
// Namespace errors
// =================

View File

@ -20,7 +20,6 @@ import (
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"time"
)
// Label represents a label
@ -212,10 +211,8 @@ func getUserTaskIDs(u *user.User) (taskIDs []int64, err error) {
}
tasks, _, _, err := getRawTasksForLists(lists, &taskOptions{
startDate: time.Unix(0, 0),
endDate: time.Unix(0, 0),
page: -1,
perPage: 0,
page: -1,
perPage: 0,
})
if err != nil {
return nil, err

View File

@ -20,15 +20,12 @@ package models
import (
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"time"
)
// TaskCollection is a struct used to hold filter details and not clutter the Task struct with information not related to actual tasks.
type TaskCollection struct {
ListID int64 `param:"list"`
StartDateSortUnix int64 `query:"startdate"`
EndDateSortUnix int64 `query:"enddate"`
Lists []*List
ListID int64 `param:"list"`
Lists []*List
// The query parameter to sort by. This is for ex. done, priority, etc.
SortBy []string `query:"sort_by"`
@ -37,10 +34,46 @@ type TaskCollection struct {
OrderBy []string `query:"order_by"`
OrderByArr []string `query:"order_by[]"`
// The field name of the field to filter by
FilterBy []string `query:"filter_by"`
FilterByArr []string `query:"filter_by[]"`
// The value of the field name to filter by
FilterValue []string `query:"filter_value"`
FilterValueArr []string `query:"filter_value[]"`
// The comparator for field and value
FilterComparator []string `query:"filter_comparator"`
FilterComparatorArr []string `query:"filter_comparator[]"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
}
func validateTaskField(fieldName string) error {
switch fieldName {
case
taskPropertyID,
taskPropertyText,
taskPropertyDescription,
taskPropertyDone,
taskPropertyDoneAtUnix,
taskPropertyDueDateUnix,
taskPropertyCreatedByID,
taskPropertyListID,
taskPropertyRepeatAfter,
taskPropertyPriority,
taskPropertyStartDateUnix,
taskPropertyEndDateUnix,
taskPropertyHexColor,
taskPropertyPercentDone,
taskPropertyUID,
taskPropertyCreated,
taskPropertyUpdated:
return nil
}
return ErrInvalidTaskField{TaskField: fieldName}
}
// ReadAll gets all tasks for a collection
// @Summary Get tasks in a list
// @Description Returns all tasks for the current list.
@ -53,8 +86,9 @@ type TaskCollection struct {
// @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`, `text`, `description`, `done`, `done_at_unix`, `due_date_unix`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date_unix`, `end_date_unix`, `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 startdate query int false "The start date parameter to filter by. Expects a timestamp. If no end date, but a start date is specified, the end date is set to the current time."
// @Param enddate query int false "The end date parameter to filter by. Expects a timestamp. If no start date, but an end date is specified, the start date is set to the current time."
// @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_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` and `less_equals`. Defaults to `equals`"
// @Security JWTKeyAuth
// @Success 200 {array} models.Task "The tasks"
// @Failure 500 {object} models.Message "Internal error"
@ -88,12 +122,15 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i
}
taskopts := &taskOptions{
search: search,
startDate: time.Unix(tf.StartDateSortUnix, 0),
endDate: time.Unix(tf.EndDateSortUnix, 0),
page: page,
perPage: perPage,
sortby: sort,
search: search,
page: page,
perPage: perPage,
sortby: sort,
}
taskopts.filters, err = getTaskFiltersByCollections(tf)
if err != nil {
return
}
shareAuth, is := a.(*LinkSharing)

View File

@ -0,0 +1,154 @@
// Copyright 2020 Vikunja and contriubtors. All rights reserved.
//
// This file is part of Vikunja.
//
// Vikunja is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Vikunja is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"github.com/iancoleman/strcase"
"reflect"
"strconv"
)
type taskFilterComparator string
const (
taskFilterComparatorInvalid taskFilterComparator = "invalid"
taskFilterComparatorEquals taskFilterComparator = "="
taskFilterComparatorGreater taskFilterComparator = ">"
taskFilterComparatorGreateEquals taskFilterComparator = ">="
taskFilterComparatorLess taskFilterComparator = "<"
taskFilterComparatorLessEquals taskFilterComparator = "<="
taskFilterComparatorNotEquals taskFilterComparator = "!="
)
type taskFilter struct {
field string
value interface{} // Needs to be an interface to be able to hold the field's native value
comparator taskFilterComparator
}
func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err error) {
if len(c.FilterByArr) > 0 {
c.FilterBy = append(c.FilterBy, c.FilterByArr...)
}
if len(c.FilterValueArr) > 0 {
c.FilterValue = append(c.FilterValue, c.FilterValueArr...)
}
if len(c.FilterComparatorArr) > 0 {
c.FilterValue = append(c.FilterValue, c.FilterComparatorArr...)
}
filters = make([]*taskFilter, 0, len(c.FilterBy))
for i, f := range c.FilterBy {
filter := &taskFilter{
field: f,
comparator: taskFilterComparatorEquals,
}
if len(c.FilterComparator) > i {
filter.comparator, err = getFilterComparatorFromString(c.FilterComparator[i])
if err != nil {
return
}
}
err = validateTaskFieldComparator(filter.comparator)
if err != nil {
return
}
// Cast the field value to its native type
if len(c.FilterValue) > i {
filter.value, err = getNativeValueForTaskField(filter.field, c.FilterValue[i])
if err != nil {
return
}
}
// Special case for pseudo date fields
// FIXME: This is really dirty, to fix this the db fields should be renamed
if filter.field+"_unix" == taskPropertyDoneAtUnix ||
filter.field+"_unix" == taskPropertyDueDateUnix ||
filter.field+"_unix" == taskPropertyStartDateUnix ||
filter.field+"_unix" == taskPropertyEndDateUnix {
filter.field += "_unix"
}
filters = append(filters, filter)
}
return
}
func validateTaskFieldComparator(comparator taskFilterComparator) error {
switch comparator {
case
taskFilterComparatorEquals,
taskFilterComparatorGreater,
taskFilterComparatorGreateEquals,
taskFilterComparatorLess,
taskFilterComparatorLessEquals,
taskFilterComparatorNotEquals:
return nil
default:
return ErrInvalidTaskFilterComparator{Comparator: comparator}
}
}
func getFilterComparatorFromString(comparator string) (taskFilterComparator, error) {
switch comparator {
case "equals":
return taskFilterComparatorEquals, nil
case "greater":
return taskFilterComparatorGreater, nil
case "greater_equals":
return taskFilterComparatorGreateEquals, nil
case "less":
return taskFilterComparatorLess, nil
case "less_equals":
return taskFilterComparatorLessEquals, nil
case "not_equals":
return taskFilterComparatorNotEquals, nil
default:
return taskFilterComparatorInvalid, ErrInvalidTaskFilterComparator{Comparator: taskFilterComparator(comparator)}
}
}
func getNativeValueForTaskField(fieldName, value string) (nativeValue interface{}, err error) {
field, ok := reflect.TypeOf(&Task{}).Elem().FieldByName(strcase.ToCamel(fieldName))
if !ok {
return nil, ErrInvalidTaskField{TaskField: fieldName}
}
switch field.Type.Kind() {
case reflect.Int64:
nativeValue, err = strconv.ParseInt(value, 10, 64)
case reflect.Float64:
nativeValue, err = strconv.ParseFloat(value, 64)
case reflect.String:
nativeValue = value
case reflect.Bool:
nativeValue, err = strconv.ParseBool(value)
default:
}
return
}

View File

@ -35,23 +35,23 @@ type (
)
const (
taskPropertyID sortProperty = "id"
taskPropertyText sortProperty = "text"
taskPropertyDescription sortProperty = "description"
taskPropertyDone sortProperty = "done"
taskPropertyDoneAtUnix sortProperty = "done_at_unix"
taskPropertyDueDateUnix sortProperty = "due_date_unix"
taskPropertyCreatedByID sortProperty = "created_by_id"
taskPropertyListID sortProperty = "list_id"
taskPropertyRepeatAfter sortProperty = "repeat_after"
taskPropertyPriority sortProperty = "priority"
taskPropertyStartDateUnix sortProperty = "start_date_unix"
taskPropertyEndDateUnix sortProperty = "end_date_unix"
taskPropertyHexColor sortProperty = "hex_color"
taskPropertyPercentDone sortProperty = "percent_done"
taskPropertyUID sortProperty = "uid"
taskPropertyCreated sortProperty = "created"
taskPropertyUpdated sortProperty = "updated"
taskPropertyID string = "id"
taskPropertyText string = "text"
taskPropertyDescription string = "description"
taskPropertyDone string = "done"
taskPropertyDoneAtUnix string = "done_at_unix"
taskPropertyDueDateUnix string = "due_date_unix"
taskPropertyCreatedByID string = "created_by_id"
taskPropertyListID string = "list_id"
taskPropertyRepeatAfter string = "repeat_after"
taskPropertyPriority string = "priority"
taskPropertyStartDateUnix string = "start_date_unix"
taskPropertyEndDateUnix string = "end_date_unix"
taskPropertyHexColor string = "hex_color"
taskPropertyPercentDone string = "percent_done"
taskPropertyUID string = "uid"
taskPropertyCreated string = "created"
taskPropertyUpdated string = "updated"
)
func (p sortProperty) String() string {
@ -82,28 +82,7 @@ func (sp *sortParam) validate() error {
if sp.orderBy != orderDescending && sp.orderBy != orderAscending {
return ErrInvalidSortOrder{OrderBy: sp.orderBy}
}
switch sp.sortBy {
case
taskPropertyID,
taskPropertyText,
taskPropertyDescription,
taskPropertyDone,
taskPropertyDoneAtUnix,
taskPropertyDueDateUnix,
taskPropertyCreatedByID,
taskPropertyListID,
taskPropertyRepeatAfter,
taskPropertyPriority,
taskPropertyStartDateUnix,
taskPropertyEndDateUnix,
taskPropertyHexColor,
taskPropertyPercentDone,
taskPropertyUID,
taskPropertyCreated,
taskPropertyUpdated:
return nil
}
return ErrInvalidSortParam{SortBy: sp.sortBy}
return validateTaskField(string(sp.sortBy))
}
type taskComparator func(lhs, rhs *Task) int64
@ -168,7 +147,7 @@ func mustMakeComparator(fieldName string) taskComparator {
// This is a map of properties that can be sorted by
// and their appropriate comparator function.
// The comparator function sorts in ascending mode.
var propertyComparators = map[sortProperty]taskComparator{
var propertyComparators = map[string]taskComparator{
taskPropertyID: mustMakeComparator("ID"),
taskPropertyText: mustMakeComparator("Text"),
taskPropertyDescription: mustMakeComparator("Description"),
@ -208,13 +187,13 @@ func sortTasks(tasks []*Task, by []*sortParam) {
// If we would not do this, we would get a different order for items with the same content every time
// the slice is sorted. To circumvent this, we always order at least by ID.
if len(by) == 0 ||
(len(by) > 0 && by[len(by)-1].sortBy != taskPropertyID) { // Don't sort by ID last if the id parameter is already passed as the last parameter.
by = append(by, &sortParam{sortBy: taskPropertyID, orderBy: orderAscending})
(len(by) > 0 && by[len(by)-1].sortBy != sortProperty(taskPropertyID)) { // Don't sort by ID last if the id parameter is already passed as the last parameter.
by = append(by, &sortParam{sortBy: sortProperty(taskPropertyID), orderBy: orderAscending})
}
comparators := make([]taskComparator, 0, len(by))
for _, param := range by {
comparator, ok := propertyComparators[param.sortBy]
comparator, ok := propertyComparators[string(param.sortBy)]
if !ok {
panic("No suitable comparator for sortBy found! Param was " + param.sortBy)
}

View File

@ -44,7 +44,7 @@ func TestSortParamValidation(t *testing.T) {
})
})
t.Run("Test valid sort by", func(t *testing.T) {
for _, test := range []sortProperty{
for _, test := range []string{
taskPropertyID,
taskPropertyText,
taskPropertyDescription,
@ -63,10 +63,10 @@ func TestSortParamValidation(t *testing.T) {
taskPropertyCreated,
taskPropertyUpdated,
} {
t.Run(test.String(), func(t *testing.T) {
t.Run(test, func(t *testing.T) {
s := &sortParam{
orderBy: orderAscending,
sortBy: test,
sortBy: sortProperty(test),
}
err := s.validate()
assert.NoError(t, err)
@ -89,7 +89,7 @@ func TestSortParamValidation(t *testing.T) {
}
err := s.validate()
assert.Error(t, err)
assert.True(t, IsErrInvalidSortParam(err))
assert.True(t, IsErrInvalidTaskField(err))
})
}
@ -185,7 +185,7 @@ type taskSortTestCase struct {
name string
wantAsc []*Task
wantDesc []*Task
sortProperty sortProperty
sortProperty string
}
var taskSortTestCases = []taskSortTestCase{
@ -692,7 +692,7 @@ func TestTaskSort(t *testing.T) {
t.Run("asc default", func(t *testing.T) {
by := []*sortParam{
{
sortBy: testCase.sortProperty,
sortBy: sortProperty(testCase.sortProperty),
},
}
@ -710,7 +710,7 @@ func TestTaskSort(t *testing.T) {
t.Run("asc", func(t *testing.T) {
by := []*sortParam{
{
sortBy: testCase.sortProperty,
sortBy: sortProperty(testCase.sortProperty),
orderBy: orderAscending,
},
}
@ -729,7 +729,7 @@ func TestTaskSort(t *testing.T) {
t.Run("desc", func(t *testing.T) {
by := []*sortParam{
{
sortBy: testCase.sortProperty,
sortBy: sortProperty(testCase.sortProperty),
orderBy: orderDescending,
},
}
@ -767,11 +767,11 @@ func TestTaskSort(t *testing.T) {
}
sortParams := []*sortParam{
{
sortBy: taskPropertyDone,
sortBy: sortProperty(taskPropertyDone),
orderBy: orderAscending,
},
{
sortBy: taskPropertyID,
sortBy: sortProperty(taskPropertyID),
orderBy: orderDescending,
},
}
@ -804,11 +804,11 @@ func TestTaskSort(t *testing.T) {
}
sortParams := []*sortParam{
{
sortBy: taskPropertyDone,
sortBy: sortProperty(taskPropertyDone),
orderBy: orderAscending,
},
{
sortBy: taskPropertyText,
sortBy: sortProperty(taskPropertyText),
orderBy: orderDescending,
},
}
@ -841,11 +841,11 @@ func TestTaskSort(t *testing.T) {
}
sortParams := []*sortParam{
{
sortBy: taskPropertyDone,
sortBy: sortProperty(taskPropertyDone),
orderBy: orderDescending,
},
{
sortBy: taskPropertyText,
sortBy: sortProperty(taskPropertyText),
orderBy: orderAscending,
},
}

View File

@ -507,14 +507,17 @@ func TestTaskCollection_ReadAll(t *testing.T) {
}
type fields struct {
ListID int64
StartDateSortUnix int64
EndDateSortUnix int64
Lists []*List
SortBy []string // Is a string, since this is the place where a query string comes from the user
OrderBy []string
CRUDable web.CRUDable
Rights web.Rights
ListID int64
Lists []*List
SortBy []string // Is a string, since this is the place where a query string comes from the user
OrderBy []string
FilterBy []string
FilterValue []string
FilterComparator []string
CRUDable web.CRUDable
Rights web.Rights
}
type args struct {
search string
@ -528,15 +531,18 @@ func TestTaskCollection_ReadAll(t *testing.T) {
want interface{}
wantErr bool
}
defaultArgs := args{
search: "",
a: &user.User{ID: 1},
page: 0,
}
tests := []testcase{
{
name: "ReadAll Tasks normally",
fields: fields{},
args: args{
search: "",
a: &user.User{ID: 1},
page: 0,
},
args: defaultArgs,
want: []*Task{
task1,
task2,
@ -579,11 +585,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
SortBy: []string{"done", "id"},
OrderBy: []string{"asc", "desc"},
},
args: args{
search: "",
a: &user.User{ID: 1},
page: 0,
},
args: defaultArgs,
want: []*Task{
task33,
task32,
@ -622,14 +624,51 @@ func TestTaskCollection_ReadAll(t *testing.T) {
{
name: "ReadAll Tasks with range",
fields: fields{
StartDateSortUnix: 1544500000,
EndDateSortUnix: 1544600000,
FilterBy: []string{"start_date", "end_date"},
FilterValue: []string{"1544500000", "1544700001"},
FilterComparator: []string{"greater", "less"},
},
args: args{
search: "",
a: &user.User{ID: 1},
page: 0,
args: defaultArgs,
want: []*Task{
task7,
task8,
task9,
},
wantErr: false,
},
{
name: "ReadAll Tasks with different range",
fields: fields{
FilterBy: []string{"start_date", "end_date"},
FilterValue: []string{"1544700000", "1545000000"},
FilterComparator: []string{"greater", "less"},
},
args: defaultArgs,
want: []*Task{
task8,
task9,
},
wantErr: false,
},
{
name: "ReadAll Tasks with range with start date only",
fields: fields{
FilterBy: []string{"start_date"},
FilterValue: []string{"1544600000"},
FilterComparator: []string{"greater"},
},
args: defaultArgs,
want: []*Task{},
wantErr: false,
},
{
name: "ReadAll Tasks with range with start date only and greater equals",
fields: fields{
FilterBy: []string{"start_date"},
FilterValue: []string{"1544600000"},
FilterComparator: []string{"greater_equals"},
},
args: defaultArgs,
want: []*Task{
task7,
task9,
@ -637,35 +676,71 @@ func TestTaskCollection_ReadAll(t *testing.T) {
wantErr: false,
},
{
name: "ReadAll Tasks with range",
name: "undone tasks only",
fields: fields{
StartDateSortUnix: 1544700000,
EndDateSortUnix: 1545000000,
},
args: args{
search: "",
a: &user.User{ID: 1},
page: 0,
FilterBy: []string{"done"},
FilterValue: []string{"false"},
FilterComparator: []string{"equals"},
},
args: defaultArgs,
want: []*Task{
task1,
// Task 2 is done
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,
},
wantErr: false,
},
{
name: "ReadAll Tasks with range without end date",
name: "done tasks only",
fields: fields{
StartDateSortUnix: 1544700000,
},
args: args{
search: "",
a: &user.User{ID: 1},
page: 0,
FilterBy: []string{"done"},
FilterValue: []string{"true"},
FilterComparator: []string{"equals"},
},
args: defaultArgs,
want: []*Task{
task8,
task9,
task2,
},
wantErr: false,
},
{
name: "done tasks only - not equals done",
fields: fields{
FilterBy: []string{"done"},
FilterValue: []string{"false"},
FilterComparator: []string{"not_equals"},
},
args: defaultArgs,
want: []*Task{
task2,
},
wantErr: false,
},
@ -676,13 +751,16 @@ func TestTaskCollection_ReadAll(t *testing.T) {
db.LoadAndAssertFixtures(t)
lt := &TaskCollection{
ListID: tt.fields.ListID,
StartDateSortUnix: tt.fields.StartDateSortUnix,
EndDateSortUnix: tt.fields.EndDateSortUnix,
SortBy: tt.fields.SortBy,
OrderBy: tt.fields.OrderBy,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
ListID: tt.fields.ListID,
SortBy: tt.fields.SortBy,
OrderBy: tt.fields.OrderBy,
FilterBy: tt.fields.FilterBy,
FilterValue: tt.fields.FilterValue,
FilterComparator: tt.fields.FilterComparator,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
got, _, _, err := lt.ReadAll(tt.args.a, tt.args.search, tt.args.page, 50)
if (err != nil) != tt.wantErr {
@ -690,7 +768,11 @@ func TestTaskCollection_ReadAll(t *testing.T) {
return
}
if diff, equal := messagediff.PrettyDiff(got, tt.want); !equal {
t.Errorf("Test %s, LabelTask.ReadAll() = %v, want %v, \ndiff: %v", tt.name, got, tt.want, diff)
if len(got.([]*Task)) == 0 && len(tt.want.([]*Task)) == 0 {
return
}
t.Errorf("Test %s, Task.ReadAll() = %v, want %v, \ndiff: %v", tt.name, got, tt.want, diff)
}
})
}

View File

@ -27,6 +27,7 @@ import (
"sort"
"strconv"
"time"
"xorm.io/builder"
)
// Task represents an task in a todolist
@ -38,7 +39,7 @@ type Task struct {
// The task description.
Description string `xorm:"longtext null" json:"description"`
// Whether a task is done or not.
Done bool `xorm:"INDEX null" json:"done"`
Done bool `xorm:"INDEX null default false" json:"done"`
// The time when a task was marked as done.
DoneAt timeutil.TimeStamp `xorm:"INDEX null 'done_at_unix'" json:"doneAt"`
// The time when the task is due.
@ -110,12 +111,11 @@ func (TaskReminder) TableName() string {
}
type taskOptions struct {
search string
startDate time.Time
endDate time.Time
page int
perPage int
sortby []*sortParam
search string
page int
perPage int
sortby []*sortParam
filters []*taskFilter
}
// ReadAll is a dummy function to still have that endpoint documented
@ -127,9 +127,11 @@ type taskOptions struct {
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search tasks by task text."
// @Param sort query string false "The sorting parameter. Possible values to sort by are priority, prioritydesc, priorityasc, duedate, duedatedesc, duedateasc."
// @Param startdate query int false "The start date parameter to filter by. Expects a timestamp. If no end date, but a start date is specified, the end date is set to the current time."
// @Param enddate query int false "The end date parameter to filter by. Expects a timestamp. If no start date, but an end date is specified, the start date is set to the current time."
// @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`, `text`, `description`, `done`, `done_at_unix`, `due_date_unix`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date_unix`, `end_date_unix`, `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_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` and `less_equals`. Defaults to `equals`"
// @Security JWTKeyAuth
// @Success 200 {array} models.Task "The tasks"
// @Failure 500 {object} models.Message "Internal error"
@ -161,26 +163,32 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*T
}
}
var filters = make([]builder.Cond, 0, len(opts.filters))
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:
filters = append(filters, &builder.Gt{f.field: f.value})
case taskFilterComparatorGreateEquals:
filters = append(filters, &builder.Gte{f.field: f.value})
case taskFilterComparatorLess:
filters = append(filters, &builder.Lt{f.field: f.value})
case taskFilterComparatorLessEquals:
filters = append(filters, &builder.Lte{f.field: f.value})
}
}
taskMap = make(map[int64]*Task)
// Then return all tasks for that lists
if opts.startDate.Unix() != 0 || opts.endDate.Unix() != 0 {
startDateUnix := time.Now().Unix()
if opts.startDate.Unix() != 0 {
startDateUnix = opts.startDate.Unix()
}
endDateUnix := time.Now().Unix()
if opts.endDate.Unix() != 0 {
endDateUnix = opts.endDate.Unix()
}
if len(filters) > 0 {
err := x.In("list_id", listIDs).
Where("text LIKE ?", "%"+opts.search+"%").
And("((due_date_unix BETWEEN ? AND ?) OR "+
"(start_date_unix BETWEEN ? and ?) OR "+
"(end_date_unix BETWEEN ? and ?))", startDateUnix, endDateUnix, startDateUnix, endDateUnix, startDateUnix, endDateUnix).
Where(builder.Or(filters...)).
OrderBy(orderby).
Limit(getLimitFromPageIndex(opts.page, opts.perPage)).
Find(&taskMap)
@ -190,9 +198,7 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*T
totalItems, err = x.In("list_id", listIDs).
Where("text LIKE ?", "%"+opts.search+"%").
And("((due_date_unix BETWEEN ? AND ?) OR "+
"(start_date_unix BETWEEN ? and ?) OR "+
"(end_date_unix BETWEEN ? and ?))", startDateUnix, endDateUnix, startDateUnix, endDateUnix, startDateUnix, endDateUnix).
Where(builder.Or(filters...)).
Count(&Task{})
if err != nil {
return nil, 0, 0, err