Sort Order for tasks (#110)
This commit is contained in:
@ -788,6 +788,60 @@ func (err ErrTaskAttachmentIsTooLarge) HTTPError() web.HTTPError {
|
||||
}
|
||||
}
|
||||
|
||||
// ErrInvalidSortParam represents an error where the provided sort param is invalid
|
||||
type ErrInvalidSortParam struct {
|
||||
SortBy sortProperty
|
||||
}
|
||||
|
||||
// IsErrInvalidSortParam checks if an error is ErrInvalidSortParam.
|
||||
func IsErrInvalidSortParam(err error) bool {
|
||||
_, ok := err.(ErrInvalidSortParam)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidSortParam) Error() string {
|
||||
return fmt.Sprintf("Sort param is invalid [SortBy: %s]", err.SortBy)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidSortParam holds the unique world-error code of this error
|
||||
const ErrCodeInvalidSortParam = 4013
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidSortParam) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
Code: ErrCodeInvalidSortParam,
|
||||
Message: fmt.Sprintf("The task sort param '%s' is invalid.", err.SortBy),
|
||||
}
|
||||
}
|
||||
|
||||
// ErrInvalidSortOrder represents an error where the provided sort order is invalid
|
||||
type ErrInvalidSortOrder struct {
|
||||
OrderBy sortOrder
|
||||
}
|
||||
|
||||
// IsErrInvalidSortOrder checks if an error is ErrInvalidSortOrder.
|
||||
func IsErrInvalidSortOrder(err error) bool {
|
||||
_, ok := err.(ErrInvalidSortOrder)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidSortOrder) Error() string {
|
||||
return fmt.Sprintf("Sort order is invalid [OrderBy: %s]", err.OrderBy)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidSortOrder holds the unique world-error code of this error
|
||||
const ErrCodeInvalidSortOrder = 4014
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidSortOrder) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
Code: ErrCodeInvalidSortOrder,
|
||||
Message: fmt.Sprintf("The task sort order '%s' is invalid. Allowed is either asc or desc.", err.OrderBy),
|
||||
}
|
||||
}
|
||||
|
||||
// =================
|
||||
// Namespace errors
|
||||
// =================
|
||||
|
@ -209,7 +209,6 @@ func getUserTaskIDs(u *User) (taskIDs []int64, err error) {
|
||||
tasks, _, _, err := getRawTasksForLists(lists, &taskOptions{
|
||||
startDate: time.Unix(0, 0),
|
||||
endDate: time.Unix(0, 0),
|
||||
sortby: SortTasksByUnsorted,
|
||||
page: -1,
|
||||
perPage: 0,
|
||||
})
|
||||
|
@ -24,18 +24,22 @@ import (
|
||||
|
||||
// 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"`
|
||||
Sorting string `query:"sort"` // Parameter to sort by
|
||||
StartDateSortUnix int64 `query:"startdate"`
|
||||
EndDateSortUnix int64 `query:"enddate"`
|
||||
ListID int64 `param:"list"`
|
||||
StartDateSortUnix int64 `query:"startdate"`
|
||||
EndDateSortUnix int64 `query:"enddate"`
|
||||
Lists []*List
|
||||
|
||||
// The query parameter to sort by. This is for ex. done, priority, etc.
|
||||
SortBy []string `query:"sort_by"`
|
||||
// The query parameter to order the items by. This can be either asc or desc, with asc being the default.
|
||||
OrderBy []string `query:"order_by"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// ReadAll gets all tasks for a collection
|
||||
// @Summary Get tasks on a list
|
||||
// @Summary Get tasks in a list
|
||||
// @Description Returns all tasks for the current list.
|
||||
// @tags task
|
||||
// @Accept json
|
||||
@ -44,7 +48,8 @@ type TaskCollection 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 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 unix 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 unix timestamp. If no start date, but an end date is specified, the start date is set to the current time."
|
||||
// @Security JWTKeyAuth
|
||||
@ -52,31 +57,32 @@ type TaskCollection struct {
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/tasks [get]
|
||||
func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
|
||||
var sortby SortBy
|
||||
switch tf.Sorting {
|
||||
case "priority":
|
||||
sortby = SortTasksByPriorityDesc
|
||||
case "prioritydesc":
|
||||
sortby = SortTasksByPriorityDesc
|
||||
case "priorityasc":
|
||||
sortby = SortTasksByPriorityAsc
|
||||
case "duedate":
|
||||
sortby = SortTasksByDueDateDesc
|
||||
case "duedatedesc":
|
||||
sortby = SortTasksByDueDateDesc
|
||||
case "duedateasc":
|
||||
sortby = SortTasksByDueDateAsc
|
||||
default:
|
||||
sortby = SortTasksByUnsorted
|
||||
|
||||
var sort = make([]*sortParam, 0, len(tf.SortBy))
|
||||
for i, s := range tf.SortBy {
|
||||
param := &sortParam{
|
||||
sortBy: sortProperty(s),
|
||||
orderBy: orderAscending,
|
||||
}
|
||||
// This checks if tf.OrderBy has an entry with the same index as the current entry from tf.SortBy
|
||||
// Taken from https://stackoverflow.com/a/27252199/10924593
|
||||
if len(tf.OrderBy) > i {
|
||||
param.orderBy = getSortOrderFromString(tf.OrderBy[i])
|
||||
}
|
||||
// Param validation
|
||||
if err := param.validate(); err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
sort = append(sort, param)
|
||||
}
|
||||
|
||||
taskopts := &taskOptions{
|
||||
search: search,
|
||||
sortby: sortby,
|
||||
startDate: time.Unix(tf.StartDateSortUnix, 0),
|
||||
endDate: time.Unix(tf.EndDateSortUnix, 0),
|
||||
page: page,
|
||||
perPage: perPage,
|
||||
sortby: sort,
|
||||
}
|
||||
|
||||
shareAuth, is := a.(*LinkSharing)
|
||||
|
233
pkg/models/task_collection_sort.go
Normal file
233
pkg/models/task_collection_sort.go
Normal file
@ -0,0 +1,233 @@
|
||||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2019 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type (
|
||||
sortParam struct {
|
||||
sortBy sortProperty
|
||||
orderBy sortOrder // asc or desc
|
||||
}
|
||||
|
||||
sortProperty string
|
||||
|
||||
sortOrder string
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func (p sortProperty) String() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
const (
|
||||
orderInvalid sortOrder = "invalid"
|
||||
orderAscending sortOrder = "asc"
|
||||
orderDescending sortOrder = "desc"
|
||||
)
|
||||
|
||||
func (o sortOrder) String() string {
|
||||
return string(o)
|
||||
}
|
||||
|
||||
func getSortOrderFromString(s string) sortOrder {
|
||||
if s == "asc" {
|
||||
return orderAscending
|
||||
}
|
||||
if s == "desc" {
|
||||
return orderDescending
|
||||
}
|
||||
return orderInvalid
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
type taskComparator func(lhs, rhs *Task) int64
|
||||
|
||||
func mustMakeComparator(fieldName string) taskComparator {
|
||||
field, ok := reflect.TypeOf(&Task{}).Elem().FieldByName(fieldName)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Field '%s' has not been found on Task", fieldName))
|
||||
}
|
||||
|
||||
extractProp := func(task *Task) interface{} {
|
||||
return reflect.ValueOf(task).Elem().FieldByIndex(field.Index).Interface()
|
||||
}
|
||||
|
||||
switch field.Type.Kind() {
|
||||
case reflect.Int64:
|
||||
return func(lhs, rhs *Task) int64 {
|
||||
return extractProp(lhs).(int64) - extractProp(rhs).(int64)
|
||||
}
|
||||
case reflect.Float64:
|
||||
return func(lhs, rhs *Task) int64 {
|
||||
floatLHS, floatRHS := extractProp(lhs).(float64), extractProp(rhs).(float64)
|
||||
if floatLHS > floatRHS {
|
||||
return 1
|
||||
} else if floatLHS < floatRHS {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
case reflect.String:
|
||||
return func(lhs, rhs *Task) int64 {
|
||||
strLHS, strRHS := extractProp(lhs).(string), extractProp(rhs).(string)
|
||||
if strLHS > strRHS {
|
||||
return 1
|
||||
} else if strLHS < strRHS {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
case reflect.Bool:
|
||||
return func(lhs, rhs *Task) int64 {
|
||||
boolLHS, boolRHS := extractProp(lhs).(bool), extractProp(rhs).(bool)
|
||||
if !boolLHS && boolRHS {
|
||||
return 1
|
||||
} else if boolLHS && !boolRHS {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("Unsupported type for sorting: %s", field.Type.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
// 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{
|
||||
taskPropertyID: mustMakeComparator("ID"),
|
||||
taskPropertyText: mustMakeComparator("Text"),
|
||||
taskPropertyDescription: mustMakeComparator("Description"),
|
||||
taskPropertyDone: mustMakeComparator("Done"),
|
||||
taskPropertyDoneAtUnix: mustMakeComparator("DoneAtUnix"),
|
||||
taskPropertyDueDateUnix: mustMakeComparator("DueDateUnix"),
|
||||
taskPropertyCreatedByID: mustMakeComparator("CreatedByID"),
|
||||
taskPropertyListID: mustMakeComparator("ListID"),
|
||||
taskPropertyRepeatAfter: mustMakeComparator("RepeatAfter"),
|
||||
taskPropertyPriority: mustMakeComparator("Priority"),
|
||||
taskPropertyStartDateUnix: mustMakeComparator("StartDateUnix"),
|
||||
taskPropertyEndDateUnix: mustMakeComparator("EndDateUnix"),
|
||||
taskPropertyHexColor: mustMakeComparator("HexColor"),
|
||||
taskPropertyPercentDone: mustMakeComparator("PercentDone"),
|
||||
taskPropertyUID: mustMakeComparator("UID"),
|
||||
taskPropertyCreated: mustMakeComparator("Created"),
|
||||
taskPropertyUpdated: mustMakeComparator("Updated"),
|
||||
}
|
||||
|
||||
// Creates a taskComparator that sorts by the first comparator and falls back to
|
||||
// the second one (and so on...) if the properties were equal.
|
||||
func combineComparators(comparators ...taskComparator) taskComparator {
|
||||
return func(lhs, rhs *Task) int64 {
|
||||
for _, compare := range comparators {
|
||||
res := compare(lhs, rhs)
|
||||
if res != 0 {
|
||||
return res
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func sortTasks(tasks []*Task, by []*sortParam) {
|
||||
|
||||
// Always sort at least by id asc so we have a consistent order of items every time
|
||||
// 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})
|
||||
}
|
||||
|
||||
comparators := make([]taskComparator, 0, len(by))
|
||||
for _, param := range by {
|
||||
comparator, ok := propertyComparators[param.sortBy]
|
||||
if !ok {
|
||||
panic("No suitable comparator for sortBy found! Param was " + param.sortBy)
|
||||
}
|
||||
|
||||
// This is a descending sort, so we need to negate the comparator (i.e. switch the inputs).
|
||||
if param.orderBy == orderDescending {
|
||||
oldComparator := comparator
|
||||
comparator = func(lhs, rhs *Task) int64 {
|
||||
return oldComparator(lhs, rhs) * -1
|
||||
}
|
||||
}
|
||||
|
||||
comparators = append(comparators, comparator)
|
||||
}
|
||||
|
||||
combinedComparator := combineComparators(comparators...)
|
||||
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
lhs, rhs := tasks[i], tasks[j]
|
||||
|
||||
res := combinedComparator(lhs, rhs)
|
||||
return res <= 0
|
||||
})
|
||||
}
|
829
pkg/models/task_collection_sort_test.go
Normal file
829
pkg/models/task_collection_sort_test.go
Normal file
@ -0,0 +1,829 @@
|
||||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2019 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/mohae/deepcopy"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSortParamValidation(t *testing.T) {
|
||||
t.Run("Test valid order by", func(t *testing.T) {
|
||||
t.Run(orderAscending.String(), func(t *testing.T) {
|
||||
s := &sortParam{
|
||||
orderBy: orderAscending,
|
||||
sortBy: "id",
|
||||
}
|
||||
err := s.validate()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run(orderDescending.String(), func(t *testing.T) {
|
||||
s := &sortParam{
|
||||
orderBy: orderDescending,
|
||||
sortBy: "id",
|
||||
}
|
||||
err := s.validate()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
})
|
||||
t.Run("Test valid sort by", func(t *testing.T) {
|
||||
for _, test := range []sortProperty{
|
||||
taskPropertyID,
|
||||
taskPropertyText,
|
||||
taskPropertyDescription,
|
||||
taskPropertyDone,
|
||||
taskPropertyDoneAtUnix,
|
||||
taskPropertyDueDateUnix,
|
||||
taskPropertyCreatedByID,
|
||||
taskPropertyListID,
|
||||
taskPropertyRepeatAfter,
|
||||
taskPropertyPriority,
|
||||
taskPropertyStartDateUnix,
|
||||
taskPropertyEndDateUnix,
|
||||
taskPropertyHexColor,
|
||||
taskPropertyPercentDone,
|
||||
taskPropertyUID,
|
||||
taskPropertyCreated,
|
||||
taskPropertyUpdated,
|
||||
} {
|
||||
t.Run(test.String(), func(t *testing.T) {
|
||||
s := &sortParam{
|
||||
orderBy: orderAscending,
|
||||
sortBy: test,
|
||||
}
|
||||
err := s.validate()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("Test invalid order by", func(t *testing.T) {
|
||||
s := &sortParam{
|
||||
orderBy: "somethingInvalid",
|
||||
sortBy: "id",
|
||||
}
|
||||
err := s.validate()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidSortOrder(err))
|
||||
})
|
||||
t.Run("Test invalid sort by", func(t *testing.T) {
|
||||
s := &sortParam{
|
||||
orderBy: orderAscending,
|
||||
sortBy: "somethingInvalid",
|
||||
}
|
||||
err := s.validate()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidSortParam(err))
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
task1 = &Task{
|
||||
ID: 1,
|
||||
Text: "aaa",
|
||||
Description: "Lorem Ipsum",
|
||||
Done: true,
|
||||
DoneAtUnix: 1543626000,
|
||||
ListID: 1,
|
||||
UID: "JywtBPCESImlyKugvaZWrxmXAFAWXFISMeXYImEh",
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task2 = &Task{
|
||||
ID: 2,
|
||||
Text: "bbb",
|
||||
Description: "Arem Ipsum",
|
||||
Done: true,
|
||||
DoneAtUnix: 1543626724,
|
||||
CreatedByID: 1,
|
||||
ListID: 2,
|
||||
PercentDone: 0.3,
|
||||
StartDateUnix: 1543626724,
|
||||
Created: 1553626724,
|
||||
Updated: 1553626724,
|
||||
}
|
||||
task3 = &Task{
|
||||
ID: 3,
|
||||
Text: "ccc",
|
||||
DueDateUnix: 1583626724,
|
||||
Priority: 100,
|
||||
ListID: 3,
|
||||
HexColor: "000000",
|
||||
PercentDone: 0.1,
|
||||
Updated: 1555555555,
|
||||
}
|
||||
task4 = &Task{
|
||||
ID: 4,
|
||||
Text: "ddd",
|
||||
Priority: 1,
|
||||
StartDateUnix: 1643626724,
|
||||
ListID: 1,
|
||||
}
|
||||
task5 = &Task{
|
||||
ID: 5,
|
||||
Text: "eef",
|
||||
Priority: 50,
|
||||
UID: "shggzCHQWLhGNMNsOGOCOjcVkInOYjTAnORqTkdL",
|
||||
DueDateUnix: 1543636724,
|
||||
Updated: 1565555555,
|
||||
}
|
||||
task6 = &Task{
|
||||
ID: 6,
|
||||
Text: "eef",
|
||||
DueDateUnix: 1543616724,
|
||||
RepeatAfter: 6400,
|
||||
CreatedByID: 2,
|
||||
HexColor: "ffffff",
|
||||
}
|
||||
task7 = &Task{
|
||||
ID: 7,
|
||||
Text: "mmmn",
|
||||
Description: "Zoremis",
|
||||
StartDateUnix: 1544600000,
|
||||
EndDateUnix: 1584600000,
|
||||
UID: "tyzCZuLMSKhwclJOsDyDcUdyVAPBDOPHNTBOLTcW",
|
||||
}
|
||||
task8 = &Task{
|
||||
ID: 8,
|
||||
Text: "b123",
|
||||
EndDateUnix: 1544700000,
|
||||
}
|
||||
task9 = &Task{
|
||||
ID: 9,
|
||||
Done: true,
|
||||
DoneAtUnix: 1573626724,
|
||||
Text: "a123",
|
||||
RepeatAfter: 86000,
|
||||
StartDateUnix: 1544600000,
|
||||
EndDateUnix: 1544700000,
|
||||
}
|
||||
task10 = &Task{
|
||||
ID: 10,
|
||||
Text: "zzz",
|
||||
Priority: 10,
|
||||
PercentDone: 1,
|
||||
}
|
||||
)
|
||||
|
||||
type taskSortTestCase struct {
|
||||
name string
|
||||
wantAsc []*Task
|
||||
wantDesc []*Task
|
||||
sortProperty sortProperty
|
||||
}
|
||||
|
||||
var taskSortTestCases = []taskSortTestCase{
|
||||
{
|
||||
name: "id",
|
||||
sortProperty: taskPropertyID,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task10,
|
||||
task9,
|
||||
task8,
|
||||
task7,
|
||||
task6,
|
||||
task5,
|
||||
task4,
|
||||
task3,
|
||||
task2,
|
||||
task1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "text",
|
||||
sortProperty: taskPropertyText,
|
||||
wantAsc: []*Task{
|
||||
task9,
|
||||
task1,
|
||||
task8,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task10,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task10,
|
||||
task7,
|
||||
task5,
|
||||
task6,
|
||||
task4,
|
||||
task3,
|
||||
task2,
|
||||
task8,
|
||||
task1,
|
||||
task9,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
sortProperty: taskPropertyDescription,
|
||||
wantAsc: []*Task{
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task2,
|
||||
task1,
|
||||
task7,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task7,
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "done",
|
||||
sortProperty: taskPropertyDone,
|
||||
wantAsc: []*Task{
|
||||
// These are done
|
||||
task1,
|
||||
task2,
|
||||
task9,
|
||||
// These are not
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task10,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
// These are not
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task10,
|
||||
// These are done
|
||||
task1,
|
||||
task2,
|
||||
task9,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "done at",
|
||||
sortProperty: taskPropertyDoneAtUnix,
|
||||
wantAsc: []*Task{
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task10,
|
||||
task1,
|
||||
task2,
|
||||
task9,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task9,
|
||||
task2,
|
||||
task1,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "due date",
|
||||
sortProperty: taskPropertyDueDateUnix,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
task4,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task6,
|
||||
task5,
|
||||
task3,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task3,
|
||||
task5,
|
||||
task6,
|
||||
task1,
|
||||
task2,
|
||||
task4,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "created by id",
|
||||
sortProperty: taskPropertyCreatedByID,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task2,
|
||||
task6,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task6,
|
||||
task2,
|
||||
task1,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list id",
|
||||
sortProperty: taskPropertyListID,
|
||||
wantAsc: []*Task{
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task1,
|
||||
task4,
|
||||
task2,
|
||||
task3,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task3,
|
||||
task2,
|
||||
task1,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "repeat after",
|
||||
sortProperty: taskPropertyRepeatAfter,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task7,
|
||||
task8,
|
||||
task10,
|
||||
task6,
|
||||
task9,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task9,
|
||||
task6,
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task7,
|
||||
task8,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "priority",
|
||||
sortProperty: taskPropertyPriority,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task4,
|
||||
task10,
|
||||
task5,
|
||||
task3,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task3,
|
||||
task5,
|
||||
task10,
|
||||
task4,
|
||||
task1,
|
||||
task2,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "start date",
|
||||
sortProperty: taskPropertyStartDateUnix,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task3,
|
||||
task5,
|
||||
task6,
|
||||
task8,
|
||||
task10,
|
||||
task2,
|
||||
task7,
|
||||
task9,
|
||||
task4,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task4,
|
||||
task7,
|
||||
task9,
|
||||
task2,
|
||||
task1,
|
||||
task3,
|
||||
task5,
|
||||
task6,
|
||||
task8,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "end date",
|
||||
sortProperty: taskPropertyEndDateUnix,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task10,
|
||||
task8,
|
||||
task9,
|
||||
task7,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hex color",
|
||||
sortProperty: taskPropertyHexColor,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
task4,
|
||||
task5,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task3,
|
||||
task6,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task6,
|
||||
task3,
|
||||
task1,
|
||||
task2,
|
||||
task4,
|
||||
task5,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "percent done",
|
||||
sortProperty: taskPropertyPercentDone,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task3,
|
||||
task2,
|
||||
task10,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task10,
|
||||
task2,
|
||||
task3,
|
||||
task1,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uid",
|
||||
sortProperty: taskPropertyUID,
|
||||
wantAsc: []*Task{
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task6,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task1,
|
||||
task5,
|
||||
task7,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task7,
|
||||
task5,
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task6,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "created",
|
||||
sortProperty: taskPropertyCreated,
|
||||
wantAsc: []*Task{
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task1,
|
||||
task2,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task2,
|
||||
task1,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "updated",
|
||||
sortProperty: taskPropertyUpdated,
|
||||
wantAsc: []*Task{
|
||||
task4,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task5,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task5,
|
||||
task3,
|
||||
task2,
|
||||
task1,
|
||||
task4,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestTaskSort(t *testing.T) {
|
||||
|
||||
assertTestSliceMatch := func(t *testing.T, got, want []*Task) {
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Error("Slices do not match in order")
|
||||
t.Error("Got\t| Want")
|
||||
for in, task := range got {
|
||||
fail := ""
|
||||
if task.ID != want[in].ID {
|
||||
fail = "wrong"
|
||||
}
|
||||
t.Errorf("\t%d\t| %d \t%s", task.ID, want[in].ID, fail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, testCase := range taskSortTestCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Run("asc default", func(t *testing.T) {
|
||||
by := []*sortParam{
|
||||
{
|
||||
sortBy: testCase.sortProperty,
|
||||
},
|
||||
}
|
||||
|
||||
got := deepcopy.Copy(testCase.wantAsc).([]*Task)
|
||||
|
||||
// Destroy wanted order to obtain some slice we can sort
|
||||
rand.Shuffle(len(got), func(i, j int) {
|
||||
got[i], got[j] = got[j], got[i]
|
||||
})
|
||||
|
||||
sortTasks(got, by)
|
||||
|
||||
assertTestSliceMatch(t, got, testCase.wantAsc)
|
||||
})
|
||||
t.Run("asc", func(t *testing.T) {
|
||||
by := []*sortParam{
|
||||
{
|
||||
sortBy: testCase.sortProperty,
|
||||
orderBy: orderAscending,
|
||||
},
|
||||
}
|
||||
|
||||
got := deepcopy.Copy(testCase.wantAsc).([]*Task)
|
||||
|
||||
// Destroy wanted order to obtain some slice we can sort
|
||||
rand.Shuffle(len(got), func(i, j int) {
|
||||
got[i], got[j] = got[j], got[i]
|
||||
})
|
||||
|
||||
sortTasks(got, by)
|
||||
|
||||
assertTestSliceMatch(t, got, testCase.wantAsc)
|
||||
})
|
||||
t.Run("desc", func(t *testing.T) {
|
||||
by := []*sortParam{
|
||||
{
|
||||
sortBy: testCase.sortProperty,
|
||||
orderBy: orderDescending,
|
||||
},
|
||||
}
|
||||
|
||||
got := deepcopy.Copy(testCase.wantDesc).([]*Task)
|
||||
|
||||
// Destroy wanted order to obtain some slice we can sort
|
||||
rand.Shuffle(len(got), func(i, j int) {
|
||||
got[i], got[j] = got[j], got[i]
|
||||
})
|
||||
|
||||
sortTasks(got, by)
|
||||
|
||||
assertTestSliceMatch(t, got, testCase.wantDesc)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Other cases
|
||||
t.Run("Order by Done Ascending and Text Descending", func(t *testing.T) {
|
||||
want := []*Task{
|
||||
// Done
|
||||
task2,
|
||||
task1,
|
||||
task9,
|
||||
|
||||
// Not done
|
||||
task10,
|
||||
task7,
|
||||
task5,
|
||||
task6,
|
||||
task4,
|
||||
task3,
|
||||
task8,
|
||||
}
|
||||
sortParams := []*sortParam{
|
||||
{
|
||||
sortBy: taskPropertyDone,
|
||||
orderBy: orderAscending,
|
||||
},
|
||||
{
|
||||
sortBy: taskPropertyText,
|
||||
orderBy: orderDescending,
|
||||
},
|
||||
}
|
||||
|
||||
got := deepcopy.Copy(want).([]*Task)
|
||||
|
||||
// Destroy wanted order to obtain some slice we can sort
|
||||
rand.Shuffle(len(got), func(i, j int) {
|
||||
got[i], got[j] = got[j], got[i]
|
||||
})
|
||||
|
||||
sortTasks(got, sortParams)
|
||||
|
||||
assertTestSliceMatch(t, got, want)
|
||||
})
|
||||
t.Run("Order by Done Descending and Text Ascending", func(t *testing.T) {
|
||||
want := []*Task{
|
||||
// Not done
|
||||
task8,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task10,
|
||||
|
||||
// Done
|
||||
task9,
|
||||
task1,
|
||||
task2,
|
||||
}
|
||||
sortParams := []*sortParam{
|
||||
{
|
||||
sortBy: taskPropertyDone,
|
||||
orderBy: orderDescending,
|
||||
},
|
||||
{
|
||||
sortBy: taskPropertyText,
|
||||
orderBy: orderAscending,
|
||||
},
|
||||
}
|
||||
|
||||
got := deepcopy.Copy(want).([]*Task)
|
||||
|
||||
// Destroy wanted order to obtain some slice we can sort
|
||||
rand.Shuffle(len(got), func(i, j int) {
|
||||
got[i], got[j] = got[j], got[i]
|
||||
})
|
||||
|
||||
sortTasks(got, sortParams)
|
||||
|
||||
assertTestSliceMatch(t, got, want)
|
||||
|
||||
})
|
||||
}
|
635
pkg/models/task_collection_test.go
Normal file
635
pkg/models/task_collection_test.go
Normal file
@ -0,0 +1,635 @@
|
||||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2019 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/web"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/d4l3k/messagediff.v1"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
assert.NoError(t, db.LoadFixtures())
|
||||
|
||||
// Dummy users
|
||||
user1 := &User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
AvatarURL: "111d68d06e2d317b5a59c2c6c5bad808", // hash for ""
|
||||
}
|
||||
user2 := &User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
AvatarURL: "ab53a2911ddf9b4817ac01ddcd3d975f", // hash for ""
|
||||
}
|
||||
user6 := &User{
|
||||
ID: 6,
|
||||
Username: "user6",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
AvatarURL: "3efbe51f864c6666bc27caf4c6ff90ed", // hash for ""
|
||||
}
|
||||
|
||||
// We use individual variables for the tasks here to be able to rearrange or remove ones more easily
|
||||
task1 := &Task{
|
||||
ID: 1,
|
||||
Text: "task #1",
|
||||
Description: "Lorem Ipsum",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
Labels: []*Label{
|
||||
{
|
||||
ID: 4,
|
||||
Title: "Label #4 - visible via other task",
|
||||
CreatedByID: 2,
|
||||
CreatedBy: user2,
|
||||
Updated: 0,
|
||||
Created: 0,
|
||||
},
|
||||
},
|
||||
RelatedTasks: map[RelationKind][]*Task{
|
||||
RelationKindSubtask: {
|
||||
{
|
||||
ID: 29,
|
||||
Text: "task #29 with parent task (1)",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
},
|
||||
},
|
||||
Attachments: []*TaskAttachment{
|
||||
{
|
||||
ID: 1,
|
||||
TaskID: 1,
|
||||
FileID: 1,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
File: &files.File{
|
||||
ID: 1,
|
||||
Name: "test",
|
||||
Size: 100,
|
||||
CreatedUnix: 1570998791,
|
||||
CreatedByID: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
TaskID: 1,
|
||||
FileID: 9999,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
},
|
||||
},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task2 := &Task{
|
||||
ID: 2,
|
||||
Text: "task #2 done",
|
||||
Done: true,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
Labels: []*Label{
|
||||
{
|
||||
ID: 4,
|
||||
Title: "Label #4 - visible via other task",
|
||||
CreatedByID: 2,
|
||||
CreatedBy: user2,
|
||||
Updated: 0,
|
||||
Created: 0,
|
||||
},
|
||||
},
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task3 := &Task{
|
||||
ID: 3,
|
||||
Text: "task #3 high prio",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
Priority: 100,
|
||||
}
|
||||
task4 := &Task{
|
||||
ID: 4,
|
||||
Text: "task #4 low prio",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
Priority: 1,
|
||||
}
|
||||
task5 := &Task{
|
||||
ID: 5,
|
||||
Text: "task #5 higher due date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
DueDateUnix: 1543636724,
|
||||
}
|
||||
task6 := &Task{
|
||||
ID: 6,
|
||||
Text: "task #6 lower due date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
DueDateUnix: 1543616724,
|
||||
}
|
||||
task7 := &Task{
|
||||
ID: 7,
|
||||
Text: "task #7 with start date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
StartDateUnix: 1544600000,
|
||||
}
|
||||
task8 := &Task{
|
||||
ID: 8,
|
||||
Text: "task #8 with end date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
EndDateUnix: 1544700000,
|
||||
}
|
||||
task9 := &Task{
|
||||
ID: 9,
|
||||
Text: "task #9 with start and end date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
StartDateUnix: 1544600000,
|
||||
EndDateUnix: 1544700000,
|
||||
}
|
||||
task10 := &Task{
|
||||
ID: 10,
|
||||
Text: "task #10 basic",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task11 := &Task{
|
||||
ID: 11,
|
||||
Text: "task #11 basic",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task12 := &Task{
|
||||
ID: 12,
|
||||
Text: "task #12 basic",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task15 := &Task{
|
||||
ID: 15,
|
||||
Text: "task #15",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 6,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task16 := &Task{
|
||||
ID: 16,
|
||||
Text: "task #16",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 7,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task17 := &Task{
|
||||
ID: 17,
|
||||
Text: "task #17",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 8,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task18 := &Task{
|
||||
ID: 18,
|
||||
Text: "task #18",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 9,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task19 := &Task{
|
||||
ID: 19,
|
||||
Text: "task #19",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 10,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task20 := &Task{
|
||||
ID: 20,
|
||||
Text: "task #20",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 11,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task21 := &Task{
|
||||
ID: 21,
|
||||
Text: "task #21",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 12,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task22 := &Task{
|
||||
ID: 22,
|
||||
Text: "task #22",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 13,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task23 := &Task{
|
||||
ID: 23,
|
||||
Text: "task #23",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 14,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task24 := &Task{
|
||||
ID: 24,
|
||||
Text: "task #24",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 15,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task25 := &Task{
|
||||
ID: 25,
|
||||
Text: "task #25",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 16,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task26 := &Task{
|
||||
ID: 26,
|
||||
Text: "task #26",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 17,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task27 := &Task{
|
||||
ID: 27,
|
||||
Text: "task #27 with reminders",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
RemindersUnix: []int64{1543626724, 1543626824},
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task28 := &Task{
|
||||
ID: 28,
|
||||
Text: "task #28 with repeat after",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
RepeatAfter: 3600,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task29 := &Task{
|
||||
ID: 29,
|
||||
Text: "task #29 with parent task (1)",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{
|
||||
RelationKindParenttask: {
|
||||
{
|
||||
ID: 1,
|
||||
Text: "task #1",
|
||||
Description: "Lorem Ipsum",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
},
|
||||
},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task30 := &Task{
|
||||
ID: 30,
|
||||
Text: "task #30 with assignees",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
Assignees: []*User{
|
||||
user1,
|
||||
user2,
|
||||
},
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task31 := &Task{
|
||||
ID: 31,
|
||||
Text: "task #31 with color",
|
||||
HexColor: "f0f0f0",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task32 := &Task{
|
||||
ID: 32,
|
||||
Text: "task #32",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 3,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task33 := &Task{
|
||||
ID: 33,
|
||||
Text: "task #33 with percent done",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
PercentDone: 0.5,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
type args struct {
|
||||
search string
|
||||
a web.Auth
|
||||
page int
|
||||
}
|
||||
type testcase struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want interface{}
|
||||
wantErr bool
|
||||
}
|
||||
tests := []testcase{
|
||||
{
|
||||
name: "ReadAll Tasks normally",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: []*Task{
|
||||
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,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
// For more sorting tests see task_collection_sort_test.go
|
||||
name: "ReadAll Tasks sorted by done asc and id desc",
|
||||
fields: fields{
|
||||
SortBy: []string{"done", "id"},
|
||||
OrderBy: []string{"asc", "desc"},
|
||||
},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: []*Task{
|
||||
task2,
|
||||
task33,
|
||||
task32,
|
||||
task31,
|
||||
task30,
|
||||
task29,
|
||||
task28,
|
||||
task27,
|
||||
task26,
|
||||
task25,
|
||||
task24,
|
||||
task23,
|
||||
task22,
|
||||
task21,
|
||||
task20,
|
||||
task19,
|
||||
task18,
|
||||
task17,
|
||||
task16,
|
||||
task15,
|
||||
task12,
|
||||
task11,
|
||||
task10,
|
||||
task9,
|
||||
task8,
|
||||
task7,
|
||||
task6,
|
||||
task5,
|
||||
task4,
|
||||
task3,
|
||||
task1,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks with range",
|
||||
fields: fields{
|
||||
StartDateSortUnix: 1544500000,
|
||||
EndDateSortUnix: 1544600000,
|
||||
},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: []*Task{
|
||||
task7,
|
||||
task9,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks with range",
|
||||
fields: fields{
|
||||
StartDateSortUnix: 1544700000,
|
||||
EndDateSortUnix: 1545000000,
|
||||
},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: []*Task{
|
||||
task8,
|
||||
task9,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks with range without end date",
|
||||
fields: fields{
|
||||
StartDateSortUnix: 1544700000,
|
||||
},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: []*Task{
|
||||
task8,
|
||||
task9,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.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,
|
||||
}
|
||||
got, _, _, err := lt.ReadAll(tt.args.a, tt.args.search, tt.args.page, 50)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Test %s, Task.ReadAll() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,728 +0,0 @@
|
||||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2018-2019 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/d4l3k/messagediff.v1"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
)
|
||||
|
||||
func sortTasksForTesting(by SortBy) (tasks []*Task) {
|
||||
user1 := &User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
AvatarURL: "111d68d06e2d317b5a59c2c6c5bad808", // hash for ""
|
||||
}
|
||||
user2 := &User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
AvatarURL: "ab53a2911ddf9b4817ac01ddcd3d975f", // hash for ""
|
||||
}
|
||||
user6 := &User{
|
||||
ID: 6,
|
||||
Username: "user6",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
AvatarURL: "3efbe51f864c6666bc27caf4c6ff90ed", // hash for ""
|
||||
}
|
||||
|
||||
tasks = []*Task{
|
||||
{
|
||||
ID: 1,
|
||||
Text: "task #1",
|
||||
Description: "Lorem Ipsum",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
Labels: []*Label{
|
||||
{
|
||||
ID: 4,
|
||||
Title: "Label #4 - visible via other task",
|
||||
CreatedByID: 2,
|
||||
CreatedBy: user2,
|
||||
Updated: 0,
|
||||
Created: 0,
|
||||
},
|
||||
},
|
||||
RelatedTasks: map[RelationKind][]*Task{
|
||||
RelationKindSubtask: {
|
||||
{
|
||||
ID: 29,
|
||||
Text: "task #29 with parent task (1)",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
},
|
||||
},
|
||||
Attachments: []*TaskAttachment{
|
||||
{
|
||||
ID: 1,
|
||||
TaskID: 1,
|
||||
FileID: 1,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
File: &files.File{
|
||||
ID: 1,
|
||||
Name: "test",
|
||||
Size: 100,
|
||||
CreatedUnix: 1570998791,
|
||||
CreatedByID: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
TaskID: 1,
|
||||
FileID: 9999,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
},
|
||||
},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Text: "task #2 done",
|
||||
Done: true,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
Labels: []*Label{
|
||||
{
|
||||
ID: 4,
|
||||
Title: "Label #4 - visible via other task",
|
||||
CreatedByID: 2,
|
||||
CreatedBy: user2,
|
||||
Updated: 0,
|
||||
Created: 0,
|
||||
},
|
||||
},
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Text: "task #3 high prio",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
Priority: 100,
|
||||
},
|
||||
{
|
||||
ID: 4,
|
||||
Text: "task #4 low prio",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
Priority: 1,
|
||||
},
|
||||
{
|
||||
ID: 5,
|
||||
Text: "task #5 higher due date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
DueDateUnix: 1543636724,
|
||||
},
|
||||
{
|
||||
ID: 6,
|
||||
Text: "task #6 lower due date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
DueDateUnix: 1543616724,
|
||||
},
|
||||
{
|
||||
ID: 7,
|
||||
Text: "task #7 with start date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
StartDateUnix: 1544600000,
|
||||
},
|
||||
{
|
||||
ID: 8,
|
||||
Text: "task #8 with end date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
EndDateUnix: 1544700000,
|
||||
},
|
||||
{
|
||||
ID: 9,
|
||||
Text: "task #9 with start and end date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
StartDateUnix: 1544600000,
|
||||
EndDateUnix: 1544700000,
|
||||
},
|
||||
{
|
||||
ID: 10,
|
||||
Text: "task #10 basic",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 11,
|
||||
Text: "task #11 basic",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 12,
|
||||
Text: "task #12 basic",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 15,
|
||||
Text: "task #15",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 6,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 16,
|
||||
Text: "task #16",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 7,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 17,
|
||||
Text: "task #17",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 8,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 18,
|
||||
Text: "task #18",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 9,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 19,
|
||||
Text: "task #19",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 10,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 20,
|
||||
Text: "task #20",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 11,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 21,
|
||||
Text: "task #21",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 12,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 22,
|
||||
Text: "task #22",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 13,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 23,
|
||||
Text: "task #23",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 14,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 24,
|
||||
Text: "task #24",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 15,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 25,
|
||||
Text: "task #25",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 16,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 26,
|
||||
Text: "task #26",
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 17,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 27,
|
||||
Text: "task #27 with reminders",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
RemindersUnix: []int64{1543626724, 1543626824},
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 28,
|
||||
Text: "task #28 with repeat after",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
RepeatAfter: 3600,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 29,
|
||||
Text: "task #29 with parent task (1)",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{
|
||||
RelationKindParenttask: {
|
||||
{
|
||||
ID: 1,
|
||||
Text: "task #1",
|
||||
Description: "Lorem Ipsum",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
},
|
||||
},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 30,
|
||||
Text: "task #30 with assignees",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
Assignees: []*User{
|
||||
user1,
|
||||
user2,
|
||||
},
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 31,
|
||||
Text: "task #31 with color",
|
||||
HexColor: "f0f0f0",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 32,
|
||||
Text: "task #32",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 3,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 33,
|
||||
Text: "task #33 with percent done",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
PercentDone: 0.5,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
}
|
||||
|
||||
switch by {
|
||||
case SortTasksByPriorityDesc:
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
return tasks[i].Priority > tasks[j].Priority
|
||||
})
|
||||
case SortTasksByPriorityAsc:
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
return tasks[i].Priority < tasks[j].Priority
|
||||
})
|
||||
case SortTasksByDueDateDesc:
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
return tasks[i].DueDateUnix > tasks[j].DueDateUnix
|
||||
})
|
||||
case SortTasksByDueDateAsc:
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
return tasks[i].DueDateUnix < tasks[j].DueDateUnix
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestTask_ReadAll(t *testing.T) {
|
||||
assert.NoError(t, db.LoadFixtures())
|
||||
|
||||
// Dummy users
|
||||
user1 := &User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
AvatarURL: "111d68d06e2d317b5a59c2c6c5bad808", // hash for ""
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
ListID int64
|
||||
Sorting string
|
||||
StartDateSortUnix int64
|
||||
EndDateSortUnix int64
|
||||
Lists []*List
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
type args struct {
|
||||
search string
|
||||
a web.Auth
|
||||
page int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want interface{}
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ReadAll Tasks normally",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: sortTasksForTesting(SortTasksByUnsorted),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks sorted by priority (desc)",
|
||||
fields: fields{
|
||||
Sorting: "priority",
|
||||
},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: sortTasksForTesting(SortTasksByPriorityDesc),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks sorted by priority asc",
|
||||
fields: fields{
|
||||
Sorting: "priorityasc",
|
||||
},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: sortTasksForTesting(SortTasksByPriorityAsc),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks sorted by priority desc",
|
||||
fields: fields{
|
||||
Sorting: "prioritydesc",
|
||||
},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: sortTasksForTesting(SortTasksByPriorityDesc),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks sorted by due date default desc",
|
||||
fields: fields{
|
||||
Sorting: "duedate",
|
||||
},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: sortTasksForTesting(SortTasksByDueDateDesc),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks sorted by due date asc",
|
||||
fields: fields{
|
||||
Sorting: "duedateasc",
|
||||
},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: sortTasksForTesting(SortTasksByDueDateAsc),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks sorted by due date desc",
|
||||
fields: fields{
|
||||
Sorting: "duedatedesc",
|
||||
},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
|
||||
want: sortTasksForTesting(SortTasksByDueDateDesc),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks with range",
|
||||
fields: fields{
|
||||
StartDateSortUnix: 1544500000,
|
||||
EndDateSortUnix: 1544600000,
|
||||
},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: []*Task{
|
||||
{
|
||||
ID: 7,
|
||||
Text: "task #7 with start date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
StartDateUnix: 1544600000,
|
||||
},
|
||||
{
|
||||
ID: 9,
|
||||
Text: "task #9 with start and end date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
StartDateUnix: 1544600000,
|
||||
EndDateUnix: 1544700000,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks with range",
|
||||
fields: fields{
|
||||
StartDateSortUnix: 1544700000,
|
||||
EndDateSortUnix: 1545000000,
|
||||
},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: []*Task{
|
||||
{
|
||||
ID: 8,
|
||||
Text: "task #8 with end date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
EndDateUnix: 1544700000,
|
||||
},
|
||||
{
|
||||
ID: 9,
|
||||
Text: "task #9 with start and end date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
StartDateUnix: 1544600000,
|
||||
EndDateUnix: 1544700000,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks with range without end date",
|
||||
fields: fields{
|
||||
StartDateSortUnix: 1544700000,
|
||||
},
|
||||
args: args{
|
||||
search: "",
|
||||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: []*Task{
|
||||
{
|
||||
ID: 8,
|
||||
Text: "task #8 with end date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
EndDateUnix: 1544700000,
|
||||
},
|
||||
{
|
||||
ID: 9,
|
||||
Text: "task #9 with start and end date",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
StartDateUnix: 1544600000,
|
||||
EndDateUnix: 1544700000,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
lt := &TaskCollection{
|
||||
ListID: tt.fields.ListID,
|
||||
Sorting: tt.fields.Sorting,
|
||||
StartDateSortUnix: tt.fields.StartDateSortUnix,
|
||||
EndDateSortUnix: tt.fields.EndDateSortUnix,
|
||||
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 {
|
||||
t.Errorf("Test %s, Task.ReadAll() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -105,25 +105,13 @@ func (TaskReminder) TableName() string {
|
||||
return "task_reminders"
|
||||
}
|
||||
|
||||
// SortBy declares constants to sort
|
||||
type SortBy int
|
||||
|
||||
// These are possible sort options
|
||||
const (
|
||||
SortTasksByUnsorted SortBy = -1
|
||||
SortTasksByDueDateAsc = iota
|
||||
SortTasksByDueDateDesc
|
||||
SortTasksByPriorityAsc
|
||||
SortTasksByPriorityDesc
|
||||
)
|
||||
|
||||
type taskOptions struct {
|
||||
search string
|
||||
sortby SortBy
|
||||
startDate time.Time
|
||||
endDate time.Time
|
||||
page int
|
||||
perPage int
|
||||
sortby []*sortParam
|
||||
}
|
||||
|
||||
// ReadAll is a dummy function to still have that endpoint documented
|
||||
@ -154,16 +142,19 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*T
|
||||
listIDs = append(listIDs, l.ID)
|
||||
}
|
||||
|
||||
// Since xorm does not use placeholders for order by, it is possible to expose this with sql injection if we're directly
|
||||
// passing user input to the db.
|
||||
// As a workaround to prevent this, we check for valid column names here prior to passing it to the db.
|
||||
var orderby string
|
||||
switch opts.sortby {
|
||||
case SortTasksByPriorityDesc:
|
||||
orderby = "priority desc"
|
||||
case SortTasksByPriorityAsc:
|
||||
orderby = "priority asc"
|
||||
case SortTasksByDueDateDesc:
|
||||
orderby = "due_date_unix desc"
|
||||
case SortTasksByDueDateAsc:
|
||||
orderby = "due_date_unix asc"
|
||||
for i, param := range opts.sortby {
|
||||
// Validate the params
|
||||
if err := param.validate(); err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
orderby += param.sortBy.String() + " " + param.orderBy.String()
|
||||
if (i + 1) < len(opts.sortby) {
|
||||
orderby += ", "
|
||||
}
|
||||
}
|
||||
|
||||
taskMap = make(map[int64]*Task)
|
||||
@ -232,34 +223,13 @@ func getTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resultCo
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
// Because the list is sorted by id which we don't want (since we're dealing with maps)
|
||||
// Because the list is fully unsorted (since we're dealing with maps)
|
||||
// we have to manually sort the tasks again here.
|
||||
sortTasks(tasks, opts.sortby)
|
||||
|
||||
return tasks, resultCount, totalItems, err
|
||||
}
|
||||
|
||||
func sortTasks(tasks []*Task, by SortBy) {
|
||||
switch by {
|
||||
case SortTasksByPriorityDesc:
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
return tasks[i].Priority > tasks[j].Priority
|
||||
})
|
||||
case SortTasksByPriorityAsc:
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
return tasks[i].Priority < tasks[j].Priority
|
||||
})
|
||||
case SortTasksByDueDateDesc:
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
return tasks[i].DueDateUnix > tasks[j].DueDateUnix
|
||||
})
|
||||
case SortTasksByDueDateAsc:
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
return tasks[i].DueDateUnix < tasks[j].DueDateUnix
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetTasksByListID gets all todotasks for a list
|
||||
func GetTasksByListID(listID int64) (tasks []*Task, err error) {
|
||||
// make a map so we can put in a lot of other stuff more easily
|
||||
|
Reference in New Issue
Block a user