Task Position (#412)
Fix misspell Fix sorting tasks with null values Fix sorting by priority for postgres Merge branch 'master' into feature/position Add community link Update golang.org/x/crypto commit hash to 44a6062 (#429) Update golang.org/x/crypto commit hash to 44a6062 Reviewed-on: https://kolaente.dev/vikunja/api/pulls/429 Update module lib/pq to v1.4.0 (#428) Update module lib/pq to v1.4.0 Reviewed-on: https://kolaente.dev/vikunja/api/pulls/428 Fix updating position Add ordering tasks in buckets by position Make task sort by string Merge branch 'master' into feature/position Update golang.org/x/crypto commit hash to 3c4aac8 (#419) Update golang.org/x/crypto commit hash to 3c4aac8 Reviewed-on: https://kolaente.dev/vikunja/api/pulls/419 Merge branch 'master' into feature/position Fix moving tasks back into the empty (ID: 0) bucket Add adding a default position when creating new tasks Update golang.org/x/crypto commit hash to a76a400 (#411) Update golang.org/x/crypto commit hash to a76a400 Reviewed-on: https://kolaente.dev/vikunja/api/pulls/411 Remove unused code Fix tests Add migration for position attribute Add position attribute Co-authored-by: kolaente <k@knt.li> Co-authored-by: renovate <renovatebot@kolaente.de> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/412
This commit is contained in:
parent
86bdd1e386
commit
e433289832
8 changed files with 163 additions and 978 deletions
|
@ -95,33 +95,33 @@ func TestTaskCollection(t *testing.T) {
|
||||||
t.Run("by priority", func(t *testing.T) {
|
t.Run("by priority", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||||
})
|
})
|
||||||
t.Run("by priority desc", func(t *testing.T) {
|
t.Run("by priority desc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`)
|
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`)
|
||||||
})
|
})
|
||||||
t.Run("by priority asc", func(t *testing.T) {
|
t.Run("by priority asc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||||
})
|
})
|
||||||
// should equal duedate asc
|
// should equal duedate asc
|
||||||
t.Run("by due_date", func(t *testing.T) {
|
t.Run("by due_date", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, urlParams)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||||
})
|
})
|
||||||
t.Run("by duedate desc", func(t *testing.T) {
|
t.Run("by duedate desc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, urlParams)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`)
|
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`)
|
||||||
})
|
})
|
||||||
t.Run("by duedate asc", func(t *testing.T) {
|
t.Run("by duedate asc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"asc"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"asc"}}, urlParams)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||||
})
|
})
|
||||||
t.Run("invalid sort parameter", func(t *testing.T) {
|
t.Run("invalid sort parameter", func(t *testing.T) {
|
||||||
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
|
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
|
||||||
|
@ -207,7 +207,7 @@ func TestTaskCollection(t *testing.T) {
|
||||||
// If no start date but an end date is specified, this should be null
|
// If no start date but an end date is specified, this should be null
|
||||||
// since we don't have any tasks in the fixtures with an end date >
|
// since we don't have any tasks in the fixtures with an end date >
|
||||||
// the current date.
|
// the current date.
|
||||||
assert.Equal(t, "null\n", rec.Body.String())
|
assert.Equal(t, "[]\n", rec.Body.String())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -270,33 +270,33 @@ func TestTaskCollection(t *testing.T) {
|
||||||
t.Run("by priority", func(t *testing.T) {
|
t.Run("by priority", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||||
})
|
})
|
||||||
t.Run("by priority desc", func(t *testing.T) {
|
t.Run("by priority desc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`)
|
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`)
|
||||||
})
|
})
|
||||||
t.Run("by priority asc", func(t *testing.T) {
|
t.Run("by priority asc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||||
})
|
})
|
||||||
// should equal duedate asc
|
// should equal duedate asc
|
||||||
t.Run("by due_date", func(t *testing.T) {
|
t.Run("by due_date", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, nil)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||||
})
|
})
|
||||||
t.Run("by duedate desc", func(t *testing.T) {
|
t.Run("by duedate desc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, nil)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`)
|
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`)
|
||||||
})
|
})
|
||||||
t.Run("by duedate asc", func(t *testing.T) {
|
t.Run("by duedate asc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"asc"}}, nil)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"asc"}}, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||||
})
|
})
|
||||||
t.Run("invalid parameter", func(t *testing.T) {
|
t.Run("invalid parameter", func(t *testing.T) {
|
||||||
// Invalid parameter should not sort at all
|
// Invalid parameter should not sort at all
|
||||||
|
@ -372,7 +372,7 @@ func TestTaskCollection(t *testing.T) {
|
||||||
// If no start date but an end date is specified, this should be null
|
// If no start date but an end date is specified, this should be null
|
||||||
// since we don't have any tasks in the fixtures with an end date >
|
// since we don't have any tasks in the fixtures with an end date >
|
||||||
// the current date.
|
// the current date.
|
||||||
assert.Equal(t, "null\n", rec.Body.String())
|
assert.Equal(t, "[]\n", rec.Body.String())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
65
pkg/migration/20200420215928.go
Normal file
65
pkg/migration/20200420215928.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-2020 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 migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.vikunja.io/api/pkg/models"
|
||||||
|
"math"
|
||||||
|
"src.techknowlogick.com/xormigrate"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type task20200420215928 struct {
|
||||||
|
Position float64 `xorm:"double null" json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s task20200420215928) TableName() string {
|
||||||
|
return "tasks"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
migrations = append(migrations, &xormigrate.Migration{
|
||||||
|
ID: "20200420215928",
|
||||||
|
Description: "Add position property to task",
|
||||||
|
Migrate: func(tx *xorm.Engine) error {
|
||||||
|
err := tx.Sync2(task20200420215928{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a position according to their id -> gives a starting position
|
||||||
|
tasks := []*models.Task{}
|
||||||
|
err = tx.Find(&tasks)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, task := range tasks {
|
||||||
|
task.Position = float64(task.ID) * math.Pow(2, 16)
|
||||||
|
_, err = tx.Where("id = ?", task.ID).Update(task)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Rollback: func(tx *xorm.Engine) error {
|
||||||
|
return tx.DropTables(task20200420215928{})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -573,7 +573,7 @@ func (err ErrTaskAttachmentIsTooLarge) HTTPError() web.HTTPError {
|
||||||
|
|
||||||
// ErrInvalidSortParam represents an error where the provided sort param is invalid
|
// ErrInvalidSortParam represents an error where the provided sort param is invalid
|
||||||
type ErrInvalidSortParam struct {
|
type ErrInvalidSortParam struct {
|
||||||
SortBy sortProperty
|
SortBy string
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrInvalidSortParam checks if an error is ErrInvalidSortParam.
|
// IsErrInvalidSortParam checks if an error is ErrInvalidSortParam.
|
||||||
|
|
|
@ -123,7 +123,15 @@ func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (r
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all tasks for this list
|
// Get all tasks for this list
|
||||||
tasks, _, _, err := getTasksForLists([]*List{{ID: b.ListID}}, &taskOptions{})
|
opts := &taskOptions{
|
||||||
|
sortby: []*sortParam{
|
||||||
|
{
|
||||||
|
sortBy: taskPropertyPosition,
|
||||||
|
orderBy: orderAscending,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tasks, _, _, err := getTasksForLists([]*List{{ID: b.ListID}}, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,8 @@ func validateTaskField(fieldName string) error {
|
||||||
taskPropertyPercentDone,
|
taskPropertyPercentDone,
|
||||||
taskPropertyUID,
|
taskPropertyUID,
|
||||||
taskPropertyCreated,
|
taskPropertyCreated,
|
||||||
taskPropertyUpdated:
|
taskPropertyUpdated,
|
||||||
|
taskPropertyPosition:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return ErrInvalidTaskField{TaskField: fieldName}
|
return ErrInvalidTaskField{TaskField: fieldName}
|
||||||
|
@ -106,7 +107,7 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i
|
||||||
var sort = make([]*sortParam, 0, len(tf.SortBy))
|
var sort = make([]*sortParam, 0, len(tf.SortBy))
|
||||||
for i, s := range tf.SortBy {
|
for i, s := range tf.SortBy {
|
||||||
param := &sortParam{
|
param := &sortParam{
|
||||||
sortBy: sortProperty(s),
|
sortBy: s,
|
||||||
orderBy: orderAscending,
|
orderBy: orderAscending,
|
||||||
}
|
}
|
||||||
// This checks if tf.OrderBy has an entry with the same index as the current entry from tf.SortBy
|
// This checks if tf.OrderBy has an entry with the same index as the current entry from tf.SortBy
|
||||||
|
|
|
@ -16,21 +16,12 @@
|
||||||
|
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
|
||||||
"code.vikunja.io/api/pkg/timeutil"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
sortParam struct {
|
sortParam struct {
|
||||||
sortBy sortProperty
|
sortBy string
|
||||||
orderBy sortOrder // asc or desc
|
orderBy sortOrder // asc or desc
|
||||||
}
|
}
|
||||||
|
|
||||||
sortProperty string
|
|
||||||
|
|
||||||
sortOrder string
|
sortOrder string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,12 +43,9 @@ const (
|
||||||
taskPropertyUID string = "uid"
|
taskPropertyUID string = "uid"
|
||||||
taskPropertyCreated string = "created"
|
taskPropertyCreated string = "created"
|
||||||
taskPropertyUpdated string = "updated"
|
taskPropertyUpdated string = "updated"
|
||||||
|
taskPropertyPosition string = "position"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p sortProperty) String() string {
|
|
||||||
return string(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
orderInvalid sortOrder = "invalid"
|
orderInvalid sortOrder = "invalid"
|
||||||
orderAscending sortOrder = "asc"
|
orderAscending sortOrder = "asc"
|
||||||
|
@ -82,139 +70,5 @@ func (sp *sortParam) validate() error {
|
||||||
if sp.orderBy != orderDescending && sp.orderBy != orderAscending {
|
if sp.orderBy != orderDescending && sp.orderBy != orderAscending {
|
||||||
return ErrInvalidSortOrder{OrderBy: sp.orderBy}
|
return ErrInvalidSortOrder{OrderBy: sp.orderBy}
|
||||||
}
|
}
|
||||||
return validateTaskField(string(sp.sortBy))
|
return validateTaskField(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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case for handling TimeStamp types
|
|
||||||
if field.Type.Name() == "TimeStamp" {
|
|
||||||
return func(lhs, rhs *Task) int64 {
|
|
||||||
return int64(extractProp(lhs).(timeutil.TimeStamp)) - int64(extractProp(rhs).(timeutil.TimeStamp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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[string]taskComparator{
|
|
||||||
taskPropertyID: mustMakeComparator("ID"),
|
|
||||||
taskPropertyText: mustMakeComparator("Text"),
|
|
||||||
taskPropertyDescription: mustMakeComparator("Description"),
|
|
||||||
taskPropertyDone: mustMakeComparator("Done"),
|
|
||||||
taskPropertyDoneAtUnix: mustMakeComparator("DoneAt"),
|
|
||||||
taskPropertyDueDateUnix: mustMakeComparator("DueDate"),
|
|
||||||
taskPropertyCreatedByID: mustMakeComparator("CreatedByID"),
|
|
||||||
taskPropertyListID: mustMakeComparator("ListID"),
|
|
||||||
taskPropertyRepeatAfter: mustMakeComparator("RepeatAfter"),
|
|
||||||
taskPropertyPriority: mustMakeComparator("Priority"),
|
|
||||||
taskPropertyStartDateUnix: mustMakeComparator("StartDate"),
|
|
||||||
taskPropertyEndDateUnix: mustMakeComparator("EndDate"),
|
|
||||||
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 != sortProperty(taskPropertyID)) { // Don't sort by ID last if the id parameter is already passed as the last parameter.
|
|
||||||
by = append(by, &sortParam{sortBy: sortProperty(taskPropertyID), orderBy: orderAscending})
|
|
||||||
}
|
|
||||||
|
|
||||||
comparators := make([]taskComparator, 0, len(by))
|
|
||||||
for _, param := range by {
|
|
||||||
comparator, ok := propertyComparators[string(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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,7 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/mohae/deepcopy"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"math/rand"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -62,11 +59,12 @@ func TestSortParamValidation(t *testing.T) {
|
||||||
taskPropertyUID,
|
taskPropertyUID,
|
||||||
taskPropertyCreated,
|
taskPropertyCreated,
|
||||||
taskPropertyUpdated,
|
taskPropertyUpdated,
|
||||||
|
taskPropertyPosition,
|
||||||
} {
|
} {
|
||||||
t.Run(test, func(t *testing.T) {
|
t.Run(test, func(t *testing.T) {
|
||||||
s := &sortParam{
|
s := &sortParam{
|
||||||
orderBy: orderAscending,
|
orderBy: orderAscending,
|
||||||
sortBy: sortProperty(test),
|
sortBy: test,
|
||||||
}
|
}
|
||||||
err := s.validate()
|
err := s.validate()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -92,774 +90,3 @@ func TestSortParamValidation(t *testing.T) {
|
||||||
assert.True(t, IsErrInvalidTaskField(err))
|
assert.True(t, IsErrInvalidTaskField(err))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
task1 = &Task{
|
|
||||||
ID: 1,
|
|
||||||
Text: "aaa",
|
|
||||||
Description: "Lorem Ipsum",
|
|
||||||
Done: true,
|
|
||||||
DoneAt: 1543626000,
|
|
||||||
ListID: 1,
|
|
||||||
UID: "JywtBPCESImlyKugvaZWrxmXAFAWXFISMeXYImEh",
|
|
||||||
Created: 1543626724,
|
|
||||||
Updated: 1543626724,
|
|
||||||
}
|
|
||||||
task2 = &Task{
|
|
||||||
ID: 2,
|
|
||||||
Text: "bbb",
|
|
||||||
Description: "Arem Ipsum",
|
|
||||||
Done: true,
|
|
||||||
DoneAt: 1543626724,
|
|
||||||
CreatedByID: 1,
|
|
||||||
ListID: 2,
|
|
||||||
PercentDone: 0.3,
|
|
||||||
StartDate: 1543626724,
|
|
||||||
Created: 1553626724,
|
|
||||||
Updated: 1553626724,
|
|
||||||
}
|
|
||||||
task3 = &Task{
|
|
||||||
ID: 3,
|
|
||||||
Text: "ccc",
|
|
||||||
DueDate: 1583626724,
|
|
||||||
Priority: 100,
|
|
||||||
ListID: 3,
|
|
||||||
HexColor: "000000",
|
|
||||||
PercentDone: 0.1,
|
|
||||||
Updated: 1555555555,
|
|
||||||
}
|
|
||||||
task4 = &Task{
|
|
||||||
ID: 4,
|
|
||||||
Text: "ddd",
|
|
||||||
Priority: 1,
|
|
||||||
StartDate: 1643626724,
|
|
||||||
ListID: 1,
|
|
||||||
}
|
|
||||||
task5 = &Task{
|
|
||||||
ID: 5,
|
|
||||||
Text: "eef",
|
|
||||||
Priority: 50,
|
|
||||||
UID: "shggzCHQWLhGNMNsOGOCOjcVkInOYjTAnORqTkdL",
|
|
||||||
DueDate: 1543636724,
|
|
||||||
Updated: 1565555555,
|
|
||||||
}
|
|
||||||
task6 = &Task{
|
|
||||||
ID: 6,
|
|
||||||
Text: "eef",
|
|
||||||
DueDate: 1543616724,
|
|
||||||
RepeatAfter: 6400,
|
|
||||||
CreatedByID: 2,
|
|
||||||
HexColor: "ffffff",
|
|
||||||
}
|
|
||||||
task7 = &Task{
|
|
||||||
ID: 7,
|
|
||||||
Text: "mmmn",
|
|
||||||
Description: "Zoremis",
|
|
||||||
StartDate: 1544600000,
|
|
||||||
EndDate: 1584600000,
|
|
||||||
UID: "tyzCZuLMSKhwclJOsDyDcUdyVAPBDOPHNTBOLTcW",
|
|
||||||
}
|
|
||||||
task8 = &Task{
|
|
||||||
ID: 8,
|
|
||||||
Text: "b123",
|
|
||||||
EndDate: 1544700000,
|
|
||||||
}
|
|
||||||
task9 = &Task{
|
|
||||||
ID: 9,
|
|
||||||
Done: true,
|
|
||||||
DoneAt: 1573626724,
|
|
||||||
Text: "a123",
|
|
||||||
RepeatAfter: 86000,
|
|
||||||
StartDate: 1544600000,
|
|
||||||
EndDate: 1544700000,
|
|
||||||
}
|
|
||||||
task10 = &Task{
|
|
||||||
ID: 10,
|
|
||||||
Text: "zzz",
|
|
||||||
Priority: 10,
|
|
||||||
PercentDone: 1,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type taskSortTestCase struct {
|
|
||||||
name string
|
|
||||||
wantAsc []*Task
|
|
||||||
wantDesc []*Task
|
|
||||||
sortProperty string
|
|
||||||
}
|
|
||||||
|
|
||||||
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 not
|
|
||||||
task3,
|
|
||||||
task4,
|
|
||||||
task5,
|
|
||||||
task6,
|
|
||||||
task7,
|
|
||||||
task8,
|
|
||||||
task10,
|
|
||||||
// These are done
|
|
||||||
task1,
|
|
||||||
task2,
|
|
||||||
task9,
|
|
||||||
},
|
|
||||||
wantDesc: []*Task{
|
|
||||||
// These are done
|
|
||||||
task1,
|
|
||||||
task2,
|
|
||||||
task9,
|
|
||||||
// These are not
|
|
||||||
task3,
|
|
||||||
task4,
|
|
||||||
task5,
|
|
||||||
task6,
|
|
||||||
task7,
|
|
||||||
task8,
|
|
||||||
task10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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: sortProperty(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: sortProperty(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: sortProperty(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 ID Descending", func(t *testing.T) {
|
|
||||||
want := []*Task{
|
|
||||||
// Not done
|
|
||||||
task10,
|
|
||||||
task8,
|
|
||||||
task7,
|
|
||||||
task6,
|
|
||||||
task5,
|
|
||||||
task4,
|
|
||||||
task3,
|
|
||||||
|
|
||||||
// Done
|
|
||||||
task9,
|
|
||||||
task2,
|
|
||||||
task1,
|
|
||||||
}
|
|
||||||
sortParams := []*sortParam{
|
|
||||||
{
|
|
||||||
sortBy: sortProperty(taskPropertyDone),
|
|
||||||
orderBy: orderAscending,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortBy: sortProperty(taskPropertyID),
|
|
||||||
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 Ascending and Text Descending", func(t *testing.T) {
|
|
||||||
want := []*Task{
|
|
||||||
// Not done
|
|
||||||
task10,
|
|
||||||
task7,
|
|
||||||
task5,
|
|
||||||
task6,
|
|
||||||
task4,
|
|
||||||
task3,
|
|
||||||
task8,
|
|
||||||
// Done
|
|
||||||
task2,
|
|
||||||
task1,
|
|
||||||
task9,
|
|
||||||
}
|
|
||||||
sortParams := []*sortParam{
|
|
||||||
{
|
|
||||||
sortBy: sortProperty(taskPropertyDone),
|
|
||||||
orderBy: orderAscending,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortBy: sortProperty(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{
|
|
||||||
// Done
|
|
||||||
task9,
|
|
||||||
task1,
|
|
||||||
task2,
|
|
||||||
// Not done
|
|
||||||
task8,
|
|
||||||
task3,
|
|
||||||
task4,
|
|
||||||
task5,
|
|
||||||
task6,
|
|
||||||
task7,
|
|
||||||
task10,
|
|
||||||
}
|
|
||||||
sortParams := []*sortParam{
|
|
||||||
{
|
|
||||||
sortBy: sortProperty(taskPropertyDone),
|
|
||||||
orderBy: orderDescending,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortBy: sortProperty(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)
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -24,10 +24,11 @@ import (
|
||||||
"code.vikunja.io/api/pkg/utils"
|
"code.vikunja.io/api/pkg/utils"
|
||||||
"code.vikunja.io/web"
|
"code.vikunja.io/web"
|
||||||
"github.com/imdario/mergo"
|
"github.com/imdario/mergo"
|
||||||
"sort"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
"xorm.io/xorm/schemas"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Task represents an task in a todolist
|
// Task represents an task in a todolist
|
||||||
|
@ -88,6 +89,14 @@ type Task struct {
|
||||||
// BucketID is the ID of the kanban bucket this task belongs to.
|
// BucketID is the ID of the kanban bucket this task belongs to.
|
||||||
BucketID int64 `xorm:"int(11) null" json:"bucket_id"`
|
BucketID int64 `xorm:"int(11) null" json:"bucket_id"`
|
||||||
|
|
||||||
|
// The position of the task - any task list can be sorted as usual by this parameter.
|
||||||
|
// When accessing tasks via kanban buckets, this is primarily used to sort them based on a range
|
||||||
|
// We're using a float64 here to make it possible to put any task within any two other tasks (by changing the number).
|
||||||
|
// You would calculate the new position between two tasks with something like task3.position = (task2.position - task1.position) / 2.
|
||||||
|
// A 64-Bit float leaves plenty of room to initially give tasks a position with 2^16 difference to the previous task
|
||||||
|
// which also leaves a lot of room for rearranging and sorting later.
|
||||||
|
Position float64 `xorm:"double null" json:"position"`
|
||||||
|
|
||||||
// The user who initially created the task.
|
// The user who initially created the task.
|
||||||
CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"`
|
CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"`
|
||||||
|
|
||||||
|
@ -143,7 +152,7 @@ func (t *Task) ReadAll(a web.Auth, search string, page int, perPage int) (result
|
||||||
return nil, 0, 0, nil
|
return nil, 0, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*Task, resultCount int, totalItems int64, err error) {
|
func getRawTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
||||||
|
|
||||||
// Get all list IDs and get the tasks
|
// Get all list IDs and get the tasks
|
||||||
var listIDs []int64
|
var listIDs []int64
|
||||||
|
@ -151,6 +160,15 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*T
|
||||||
listIDs = append(listIDs, l.ID)
|
listIDs = append(listIDs, l.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the id parameter as the last parameter to sorty by default, but only if it is not already passed as the last parameter.
|
||||||
|
if len(opts.sortby) == 0 ||
|
||||||
|
len(opts.sortby) > 0 && opts.sortby[len(opts.sortby)-1].sortBy != taskPropertyID {
|
||||||
|
opts.sortby = append(opts.sortby, &sortParam{
|
||||||
|
sortBy: taskPropertyID,
|
||||||
|
orderBy: orderAscending,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Since xorm does not use placeholders for order by, it is possible to expose this with sql injection if we're directly
|
// 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.
|
// 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.
|
// As a workaround to prevent this, we check for valid column names here prior to passing it to the db.
|
||||||
|
@ -160,7 +178,19 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*T
|
||||||
if err := param.validate(); err != nil {
|
if err := param.validate(); err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
orderby += param.sortBy.String() + " " + param.orderBy.String()
|
orderby += param.sortBy + " " + param.orderBy.String()
|
||||||
|
|
||||||
|
// Postgres sorts by default entries with null values after ones with values.
|
||||||
|
// To make that consistent with the sort order we have and other dbms, we're adding a separate clause here.
|
||||||
|
if x.Dialect().URI().DBType == schemas.POSTGRES {
|
||||||
|
if param.orderBy == orderAscending {
|
||||||
|
orderby += " NULLS FIRST"
|
||||||
|
}
|
||||||
|
if param.orderBy == orderDescending {
|
||||||
|
orderby += " NULLS LAST"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (i + 1) < len(opts.sortby) {
|
if (i + 1) < len(opts.sortby) {
|
||||||
orderby += ", "
|
orderby += ", "
|
||||||
}
|
}
|
||||||
|
@ -184,8 +214,6 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
taskMap = make(map[int64]*Task)
|
|
||||||
|
|
||||||
// Then return all tasks for that lists
|
// Then return all tasks for that lists
|
||||||
query := x.
|
query := x.
|
||||||
OrderBy(orderby)
|
OrderBy(orderby)
|
||||||
|
@ -208,7 +236,8 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*T
|
||||||
query = query.Limit(limit, start)
|
query = query.Limit(limit, start)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = query.Find(&taskMap)
|
tasks = []*Task{}
|
||||||
|
err = query.Find(&tasks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
|
@ -219,23 +248,25 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*T
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return taskMap, len(taskMap), totalItems, nil
|
return tasks, len(tasks), totalItems, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
func getTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
||||||
|
|
||||||
taskMap, resultCount, totalItems, err := getRawTasksForLists(lists, opts)
|
tasks, resultCount, totalItems, err = getRawTasksForLists(lists, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks, err = addMoreInfoToTasks(taskMap)
|
taskMap := make(map[int64]*Task, len(tasks))
|
||||||
|
for _, t := range tasks {
|
||||||
|
taskMap[t.ID] = t
|
||||||
|
}
|
||||||
|
|
||||||
|
err = addMoreInfoToTasks(taskMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
// 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
|
return tasks, resultCount, totalItems, err
|
||||||
}
|
}
|
||||||
|
@ -271,25 +302,28 @@ func (bt *BulkTask) GetTasksByIDs() (err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
taskMap := make(map[int64]*Task, len(bt.Tasks))
|
err = x.In("id", bt.IDs).Find(&bt.Tasks)
|
||||||
err = x.In("id", bt.IDs).Find(&taskMap)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bt.Tasks, err = addMoreInfoToTasks(taskMap)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTasksByUIDs gets all tasks from a bunch of uids
|
// GetTasksByUIDs gets all tasks from a bunch of uids
|
||||||
func GetTasksByUIDs(uids []string) (tasks []*Task, err error) {
|
func GetTasksByUIDs(uids []string) (tasks []*Task, err error) {
|
||||||
taskMap := make(map[int64]*Task)
|
tasks = []*Task{}
|
||||||
err = x.In("uid", uids).Find(&taskMap)
|
err = x.In("uid", uids).Find(&tasks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks, err = addMoreInfoToTasks(taskMap)
|
taskMap := make(map[int64]*Task, len(tasks))
|
||||||
|
for _, t := range tasks {
|
||||||
|
taskMap[t.ID] = t
|
||||||
|
}
|
||||||
|
|
||||||
|
err = addMoreInfoToTasks(taskMap)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,7 +335,7 @@ func getRemindersForTasks(taskIDs []int64) (reminders []*TaskReminder, err error
|
||||||
|
|
||||||
// This function takes a map with pointers and returns a slice with pointers to tasks
|
// This function takes a map with pointers and returns a slice with pointers to tasks
|
||||||
// It adds more stuff like assignees/labels/etc to a bunch of tasks
|
// It adds more stuff like assignees/labels/etc to a bunch of tasks
|
||||||
func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) {
|
func addMoreInfoToTasks(taskMap map[int64]*Task) (err error) {
|
||||||
|
|
||||||
// No need to iterate over users and stuff if the list doesn't has tasks
|
// No need to iterate over users and stuff if the list doesn't has tasks
|
||||||
if len(taskMap) == 0 {
|
if len(taskMap) == 0 {
|
||||||
|
@ -351,7 +385,7 @@ func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) {
|
||||||
In("task_id", taskIDs).
|
In("task_id", taskIDs).
|
||||||
Find(&attachments)
|
Find(&attachments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileIDs := []int64{}
|
fileIDs := []int64{}
|
||||||
|
@ -446,19 +480,6 @@ func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) {
|
||||||
taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], fullRelatedTasks[rt.OtherTaskID])
|
taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], fullRelatedTasks[rt.OtherTaskID])
|
||||||
}
|
}
|
||||||
|
|
||||||
// make a complete slice from the map
|
|
||||||
tasks = []*Task{}
|
|
||||||
for _, t := range taskMap {
|
|
||||||
tasks = append(tasks, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the output. In Go, contents on a map are put on that map in no particular order.
|
|
||||||
// Because of this, tasks are not sorted anymore in the output, this leads to confiusion.
|
|
||||||
// To avoid all this, we need to sort the slice afterwards
|
|
||||||
sort.Slice(tasks, func(i, j int) bool {
|
|
||||||
return tasks[i].ID < tasks[j].ID
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -533,6 +554,10 @@ func (t *Task) Create(a web.Auth) (err error) {
|
||||||
t.Index = latestTask.Index + 1
|
t.Index = latestTask.Index + 1
|
||||||
t.CreatedByID = u.ID
|
t.CreatedByID = u.ID
|
||||||
t.CreatedBy = u
|
t.CreatedBy = u
|
||||||
|
// If no position was supplied, set a default one
|
||||||
|
if t.Position == 0 {
|
||||||
|
t.Position = float64(latestTask.ID+1) * math.Pow(2, 16)
|
||||||
|
}
|
||||||
if _, err = x.Insert(t); err != nil {
|
if _, err = x.Insert(t); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -673,6 +698,10 @@ func (t *Task) Update() (err error) {
|
||||||
if t.BucketID == 0 {
|
if t.BucketID == 0 {
|
||||||
ot.BucketID = 0
|
ot.BucketID = 0
|
||||||
}
|
}
|
||||||
|
// Position
|
||||||
|
if t.Position == 0 {
|
||||||
|
ot.Position = 0
|
||||||
|
}
|
||||||
|
|
||||||
_, err = x.ID(t.ID).
|
_, err = x.ID(t.ID).
|
||||||
Cols("text",
|
Cols("text",
|
||||||
|
@ -688,6 +717,7 @@ func (t *Task) Update() (err error) {
|
||||||
"percent_done",
|
"percent_done",
|
||||||
"list_id",
|
"list_id",
|
||||||
"bucket_id",
|
"bucket_id",
|
||||||
|
"position",
|
||||||
).
|
).
|
||||||
Update(ot)
|
Update(ot)
|
||||||
*t = ot
|
*t = ot
|
||||||
|
@ -877,16 +907,16 @@ func (t *Task) ReadOne() (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks, err := addMoreInfoToTasks(taskMap)
|
err = addMoreInfoToTasks(taskMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tasks) == 0 {
|
if len(taskMap) == 0 {
|
||||||
return ErrTaskDoesNotExist{t.ID}
|
return ErrTaskDoesNotExist{t.ID}
|
||||||
}
|
}
|
||||||
|
|
||||||
*t = *tasks[0]
|
*t = *taskMap[t.ID]
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue