1
0

Sort Order for tasks (#110)

This commit is contained in:
konrad
2019-12-07 14:30:51 +00:00
parent e890001ee1
commit d8399e374c
22 changed files with 2080 additions and 994 deletions

View File

@ -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
// =================

View File

@ -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,
})

View File

@ -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)

View 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
})
}

View 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)
})
}

View 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)
}
})
}
}

View File

@ -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)
}
})
}
}

View File

@ -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