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:
@ -126,7 +126,7 @@ func TestTaskCollection(t *testing.T) {
|
||||
t.Run("invalid sort parameter", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeInvalidSortParam)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeInvalidTaskField)
|
||||
})
|
||||
t.Run("invalid sort order", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"id"}, "order_by": []string{"loremipsum"}}, urlParams)
|
||||
@ -145,7 +145,14 @@ func TestTaskCollection(t *testing.T) {
|
||||
})
|
||||
t.Run("Date range", func(t *testing.T) {
|
||||
t.Run("start and end date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"startdate": []string{"1540000000"}, "enddate": []string{"1544700001"}}, urlParams)
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date", "end_date", "due_date"},
|
||||
"filter_value": []string{"1544500000", "1544700001", "1543500000"},
|
||||
"filter_comparator": []string{"greater", "less", "greater"},
|
||||
},
|
||||
urlParams,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
@ -163,16 +170,23 @@ func TestTaskCollection(t *testing.T) {
|
||||
assert.NotContains(t, rec.Body.String(), `task #14`)
|
||||
})
|
||||
t.Run("start date only", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"startdate": []string{"1540000000"}}, urlParams)
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date"},
|
||||
"filter_value": []string{"1540000000"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
},
|
||||
urlParams,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.Contains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #6`)
|
||||
assert.Contains(t, rec.Body.String(), `task #7`)
|
||||
assert.Contains(t, rec.Body.String(), `task #8`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8`)
|
||||
assert.Contains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
@ -181,7 +195,14 @@ func TestTaskCollection(t *testing.T) {
|
||||
assert.NotContains(t, rec.Body.String(), `task #14`)
|
||||
})
|
||||
t.Run("end date only", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"enddate": []string{"1544700001"}}, urlParams)
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"end_date"},
|
||||
"filter_value": []string{"1544700001"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
},
|
||||
urlParams,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
// If no start date but an end date is specified, this should be null
|
||||
// since we don't have any tasks in the fixtures with an end date >
|
||||
@ -289,7 +310,14 @@ func TestTaskCollection(t *testing.T) {
|
||||
})
|
||||
t.Run("Date range", func(t *testing.T) {
|
||||
t.Run("start and end date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"startdate": []string{"1540000000"}, "enddate": []string{"1544700001"}}, nil)
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date", "end_date", "due_date"},
|
||||
"filter_value": []string{"1544500000", "1544700001", "1543500000"},
|
||||
"filter_comparator": []string{"greater", "less", "greater"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
@ -307,16 +335,23 @@ func TestTaskCollection(t *testing.T) {
|
||||
assert.NotContains(t, rec.Body.String(), `task #14`)
|
||||
})
|
||||
t.Run("start date only", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"startdate": []string{"1540000000"}}, nil)
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date"},
|
||||
"filter_value": []string{"1540000000"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.Contains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #6`)
|
||||
assert.Contains(t, rec.Body.String(), `task #7`)
|
||||
assert.Contains(t, rec.Body.String(), `task #8`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8`)
|
||||
assert.Contains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
@ -325,7 +360,14 @@ func TestTaskCollection(t *testing.T) {
|
||||
assert.NotContains(t, rec.Body.String(), `task #14`)
|
||||
})
|
||||
t.Run("end date only", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"enddate": []string{"1544700001"}}, nil)
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"end_date"},
|
||||
"filter_value": []string{"1544700001"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
// If no start date but an end date is specified, this should be null
|
||||
// since we don't have any tasks in the fixtures with an end date >
|
||||
|
@ -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
|
||||
// =================
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
154
pkg/models/task_collection_filter.go
Normal file
154
pkg/models/task_collection_filter.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user