From 6ccb85a0dcdcd2103d3c564a9c4e3b7a8ff6748b Mon Sep 17 00:00:00 2001 From: konrad Date: Wed, 28 Jul 2021 19:06:40 +0000 Subject: [PATCH] Reorder tasks, lists and kanban buckets (#923) Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/api/pulls/923 Co-authored-by: konrad Co-committed-by: konrad --- pkg/db/fixtures/buckets.yml | 3 + pkg/db/fixtures/lists.yml | 23 +++++++ pkg/db/fixtures/tasks.yml | 2 + pkg/integrations/task_collection_test.go | 30 ++++----- pkg/migration/20210725153703.go | 71 +++++++++++++++++++++ pkg/migration/20210727204942.go | 68 ++++++++++++++++++++ pkg/migration/20210727211037.go | 68 ++++++++++++++++++++ pkg/models/kanban.go | 17 ++++- pkg/models/kanban_test.go | 25 ++++---- pkg/models/list.go | 16 ++++- pkg/models/list_test.go | 7 +- pkg/models/namespace.go | 1 + pkg/models/task_collection.go | 2 +- pkg/models/task_collection_sort.go | 39 +++++------ pkg/models/task_collection_test.go | 48 ++++++++++++++ pkg/models/tasks.go | 19 +++++- pkg/models/tasks_test.go | 12 ++-- pkg/modules/migration/trello/trello.go | 8 +-- pkg/modules/migration/trello/trello_test.go | 64 +++++++++---------- pkg/swagger/docs.go | 16 +++++ pkg/swagger/swagger.json | 16 +++++ pkg/swagger/swagger.yaml | 16 +++++ 22 files changed, 475 insertions(+), 96 deletions(-) create mode 100644 pkg/migration/20210725153703.go create mode 100644 pkg/migration/20210727204942.go create mode 100644 pkg/migration/20210727211037.go diff --git a/pkg/db/fixtures/buckets.yml b/pkg/db/fixtures/buckets.yml index c1ddcd94f6b..92c25cf7a94 100644 --- a/pkg/db/fixtures/buckets.yml +++ b/pkg/db/fixtures/buckets.yml @@ -3,6 +3,7 @@ list_id: 1 created_by_id: 1 limit: 9999999 # This bucket has a limit we will never exceed in the tests to make sure the logic allows for buckets with limits + position: 2 created: 2020-04-18 21:13:52 updated: 2020-04-18 21:13:52 - id: 2 @@ -10,6 +11,7 @@ list_id: 1 created_by_id: 1 limit: 3 + position: 1 created: 2020-04-18 21:13:52 updated: 2020-04-18 21:13:52 - id: 3 @@ -17,6 +19,7 @@ list_id: 1 created_by_id: 1 is_done_bucket: 1 + position: 3 created: 2020-04-18 21:13:52 updated: 2020-04-18 21:13:52 - id: 4 diff --git a/pkg/db/fixtures/lists.yml b/pkg/db/fixtures/lists.yml index 45336f951d4..e40f92d0dde 100644 --- a/pkg/db/fixtures/lists.yml +++ b/pkg/db/fixtures/lists.yml @@ -5,6 +5,7 @@ identifier: test1 owner_id: 1 namespace_id: 1 + position: 3 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -14,6 +15,7 @@ identifier: test2 owner_id: 3 namespace_id: 1 + position: 2 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -23,6 +25,7 @@ identifier: test3 owner_id: 3 namespace_id: 2 + position: 1 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -32,6 +35,7 @@ identifier: test4 owner_id: 3 namespace_id: 3 + position: 4 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -41,6 +45,7 @@ identifier: test5 owner_id: 5 namespace_id: 5 + position: 5 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -50,6 +55,7 @@ identifier: test6 owner_id: 6 namespace_id: 6 + position: 6 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -59,6 +65,7 @@ identifier: test7 owner_id: 6 namespace_id: 6 + position: 7 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -68,6 +75,7 @@ identifier: test8 owner_id: 6 namespace_id: 6 + position: 8 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -77,6 +85,7 @@ identifier: test9 owner_id: 6 namespace_id: 6 + position: 9 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -86,6 +95,7 @@ identifier: test10 owner_id: 6 namespace_id: 6 + position: 10 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -95,6 +105,7 @@ identifier: test11 owner_id: 6 namespace_id: 6 + position: 11 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -104,6 +115,7 @@ identifier: test12 owner_id: 6 namespace_id: 7 + position: 12 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -113,6 +125,7 @@ identifier: test13 owner_id: 6 namespace_id: 8 + position: 13 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -122,6 +135,7 @@ identifier: test14 owner_id: 6 namespace_id: 9 + position: 14 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -131,6 +145,7 @@ identifier: test15 owner_id: 6 namespace_id: 10 + position: 15 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -140,6 +155,7 @@ identifier: test16 owner_id: 6 namespace_id: 11 + position: 16 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -149,6 +165,7 @@ identifier: test17 owner_id: 6 namespace_id: 12 + position: 17 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 # This list is owned by user 7, and several other users have access to it via different methods. @@ -160,6 +177,7 @@ identifier: test18 owner_id: 7 namespace_id: 13 + position: 18 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -169,6 +187,7 @@ identifier: test19 owner_id: 7 namespace_id: 14 + position: 19 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 # User 1 does not have access to this list @@ -179,6 +198,7 @@ identifier: test20 owner_id: 13 namespace_id: 15 + position: 20 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -188,6 +208,7 @@ identifier: test21 owner_id: 1 namespace_id: 16 + position: 21 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -198,6 +219,7 @@ owner_id: 1 namespace_id: 1 is_archived: 1 + position: 22 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -207,5 +229,6 @@ identifier: test23 owner_id: 12 namespace_id: 17 + position: 23 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 diff --git a/pkg/db/fixtures/tasks.yml b/pkg/db/fixtures/tasks.yml index 4d944fd1a1b..ec0abf0bc83 100644 --- a/pkg/db/fixtures/tasks.yml +++ b/pkg/db/fixtures/tasks.yml @@ -8,6 +8,7 @@ created: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04 bucket_id: 1 + position: 2 - id: 2 title: 'task #2 done' done: true @@ -17,6 +18,7 @@ created: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04 bucket_id: 1 + position: 4 - id: 3 title: 'task #3 high prio' done: false diff --git a/pkg/integrations/task_collection_test.go b/pkg/integrations/task_collection_test.go index 7915af7de59..bbd64c25590 100644 --- a/pkg/integrations/task_collection_test.go +++ b/pkg/integrations/task_collection_test.go @@ -113,49 +113,49 @@ func TestTaskCollection(t *testing.T) { t.Run("by priority", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) t.Run("by priority desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`) + assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`) }) t.Run("by priority asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) // should equal duedate asc t.Run("by due_date", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) t.Run("by duedate desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`) + assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`) }) // Due date without unix suffix t.Run("by duedate asc without suffix", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) t.Run("by due_date without suffix", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) t.Run("by duedate desc without suffix", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`) + assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`) }) t.Run("by duedate asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) t.Run("invalid sort parameter", func(t *testing.T) { _, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams) @@ -341,33 +341,33 @@ func TestTaskCollection(t *testing.T) { t.Run("by priority", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) t.Run("by priority desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`) + assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`) }) t.Run("by priority asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) // should equal duedate asc t.Run("by due_date", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) t.Run("by duedate desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`) + assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`) }) t.Run("by duedate asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) t.Run("invalid parameter", func(t *testing.T) { // Invalid parameter should not sort at all diff --git a/pkg/migration/20210725153703.go b/pkg/migration/20210725153703.go new file mode 100644 index 00000000000..2eedb69d799 --- /dev/null +++ b/pkg/migration/20210725153703.go @@ -0,0 +1,71 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package migration + +import ( + "math" + + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type tasks20210725153703 struct { + ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"listtask"` + Position float64 `xorm:"double null" json:"position"` + KanbanPosition float64 `xorm:"double null" json:"kanban_position"` +} + +func (tasks20210725153703) TableName() string { + return "tasks" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20210725153703", + Description: "", + Migrate: func(tx *xorm.Engine) error { + err := tx.Sync2(tasks20210725153703{}) + if err != nil { + return err + } + + tasks := []*tasks20210725153703{} + err = tx.Where("position is not null").Find(&tasks) + if err != nil { + return err + } + + // Migrate all old kanban positions to the kanban positions property + for _, task := range tasks { + task.KanbanPosition = task.Position + task.Position = float64(task.ID) * math.Pow(2, 16) + _, err = tx. + Where("id = ?", task.ID). + Cols("kanban_position", "position"). + Update(task) + if err != nil { + return err + } + } + + return nil + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/migration/20210727204942.go b/pkg/migration/20210727204942.go new file mode 100644 index 00000000000..04fb82b0442 --- /dev/null +++ b/pkg/migration/20210727204942.go @@ -0,0 +1,68 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package migration + +import ( + "math" + + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type lists20210727204942 struct { + ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"list"` + Position float64 `xorm:"double null" json:"position"` +} + +func (lists20210727204942) TableName() string { + return "lists" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20210727204942", + Description: "Add list position parameter", + Migrate: func(tx *xorm.Engine) error { + err := tx.Sync2(lists20210727204942{}) + if err != nil { + return err + } + + lists := []*lists20210727204942{} + err = tx.Find(&lists) + if err != nil { + return err + } + + for _, list := range lists { + list.Position = float64(list.ID) * math.Pow(2, 16) + + _, err = tx. + Where("id = ?", list.ID). + Update(list) + if err != nil { + return err + } + } + + return nil + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/migration/20210727211037.go b/pkg/migration/20210727211037.go new file mode 100644 index 00000000000..53ce74a9482 --- /dev/null +++ b/pkg/migration/20210727211037.go @@ -0,0 +1,68 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package migration + +import ( + "math" + + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type buckets20210727211037 struct { + ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"list"` + Position float64 `xorm:"double null" json:"position"` +} + +func (buckets20210727211037) TableName() string { + return "buckets" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20210727211037", + Description: "Add bucket position property", + Migrate: func(tx *xorm.Engine) error { + err := tx.Sync2(buckets20210727211037{}) + if err != nil { + return err + } + + buckets := []*buckets20210727211037{} + err = tx.Find(&buckets) + if err != nil { + return err + } + + for _, bucket := range buckets { + bucket.Position = float64(bucket.ID) * math.Pow(2, 16) + + _, err = tx. + Where("id = ?", bucket.ID). + Update(bucket) + if err != nil { + return err + } + } + + return nil + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 6c00163779a..2407bc3180f 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -41,6 +41,9 @@ type Bucket struct { // If this bucket is the "done bucket". All tasks moved into this bucket will automatically marked as done. All tasks marked as done from elsewhere will be moved into this bucket. IsDoneBucket bool `xorm:"BOOL" json:"is_done_bucket"` + // The position this bucket has when querying all buckets. See the tasks.position property on how to use this. + Position float64 `xorm:"double null" json:"position"` + // A timestamp when this bucket was created. You cannot change this value. Created time.Time `xorm:"created not null" json:"created"` // A timestamp when this bucket was last updated. You cannot change this value. @@ -134,7 +137,10 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int // Get all buckets for this list buckets := []*Bucket{} - err = s.Where("list_id = ?", b.ListID).Find(&buckets) + err = s. + Where("list_id = ?", b.ListID). + OrderBy("position"). + Find(&buckets) if err != nil { return } @@ -167,7 +173,7 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int opts.sortby = []*sortParam{ { orderBy: orderAscending, - sortBy: taskPropertyPosition, + sortBy: taskPropertyKanbanPosition, }, } opts.page = page @@ -251,6 +257,12 @@ func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) { b.CreatedByID = b.CreatedBy.ID _, err = s.Insert(b) + if err != nil { + return + } + + b.Position = calculateDefaultPosition(b.ID, b.Position) + _, err = s.Where("id = ?", b.ID).Update(b) return } @@ -289,6 +301,7 @@ func (b *Bucket) Update(s *xorm.Session, a web.Auth) (err error) { "title", "limit", "is_done_bucket", + "position", ). Update(b) return diff --git a/pkg/models/kanban_test.go b/pkg/models/kanban_test.go index 1d41809a081..079da0f408c 100644 --- a/pkg/models/kanban_test.go +++ b/pkg/models/kanban_test.go @@ -49,21 +49,23 @@ func TestBucket_ReadAll(t *testing.T) { assert.Len(t, buckets, 3) // Assert all tasks are in the right bucket - assert.Len(t, buckets[0].Tasks, 12) - assert.Len(t, buckets[1].Tasks, 3) + assert.Len(t, buckets[0].Tasks, 3) + assert.Len(t, buckets[1].Tasks, 12) assert.Len(t, buckets[2].Tasks, 3) - // Assert we have bucket 0, 1, 2, 3 but not 4 (that belongs to a different list) - assert.Equal(t, int64(1), buckets[0].ID) - assert.Equal(t, int64(2), buckets[1].ID) + // Assert we have bucket 1, 2, 3 but not 4 (that belongs to a different list) and their position + assert.Equal(t, int64(2), buckets[0].ID) + assert.Equal(t, int64(1), buckets[1].ID) assert.Equal(t, int64(3), buckets[2].ID) // Kinda assert all tasks are in the right buckets - assert.Equal(t, int64(1), buckets[0].Tasks[0].BucketID) - assert.Equal(t, int64(1), buckets[0].Tasks[1].BucketID) - assert.Equal(t, int64(2), buckets[1].Tasks[0].BucketID) - assert.Equal(t, int64(2), buckets[1].Tasks[1].BucketID) - assert.Equal(t, int64(2), buckets[1].Tasks[2].BucketID) + assert.Equal(t, int64(1), buckets[1].Tasks[0].BucketID) + assert.Equal(t, int64(1), buckets[1].Tasks[1].BucketID) + + assert.Equal(t, int64(2), buckets[0].Tasks[0].BucketID) + assert.Equal(t, int64(2), buckets[0].Tasks[1].BucketID) + assert.Equal(t, int64(2), buckets[0].Tasks[2].BucketID) + assert.Equal(t, int64(3), buckets[2].Tasks[0].BucketID) assert.Equal(t, int64(3), buckets[2].Tasks[1].BucketID) assert.Equal(t, int64(3), buckets[2].Tasks[2].BucketID) @@ -87,7 +89,8 @@ func TestBucket_ReadAll(t *testing.T) { buckets := bucketsInterface.([]*Bucket) assert.Len(t, buckets, 3) - assert.Equal(t, int64(2), buckets[0].Tasks[0].ID) + assert.Equal(t, int64(2), buckets[1].Tasks[0].ID) + assert.Equal(t, int64(33), buckets[1].Tasks[1].ID) }) t.Run("accessed by link share", func(t *testing.T) { db.LoadAndAssertFixtures(t) diff --git a/pkg/models/list.go b/pkg/models/list.go index 61d4ebe8ba1..3c1ac7a0470 100644 --- a/pkg/models/list.go +++ b/pkg/models/list.go @@ -71,6 +71,9 @@ type List struct { // Will only returned when retreiving one list. Subscription *Subscription `xorm:"-" json:"subscription,omitempty"` + // The position this list has when querying all lists. See the tasks.position property on how to use this. + Position float64 `xorm:"double null" json:"position"` + // A timestamp when this list was created. You cannot change this value. Created time.Time `xorm:"created not null" json:"created"` // A timestamp when this list was last updated. You cannot change this value. @@ -284,7 +287,10 @@ func GetListSimpleByID(s *xorm.Session, listID int64) (list *List, err error) { return nil, ErrListDoesNotExist{ID: listID} } - exists, err := s.Where("id = ?", listID).Get(list) + exists, err := s. + Where("id = ?", listID). + OrderBy("position"). + Get(list) if err != nil { return } @@ -361,6 +367,7 @@ func getUserListsStatement(userID int64) *builder.Builder { builder.Eq{"un.user_id": userID}, builder.Eq{"l.owner_id": userID}, )). + OrderBy("position"). GroupBy("l.id") } @@ -559,6 +566,12 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error) if err != nil { return } + + list.Position = calculateDefaultPosition(list.ID, list.Position) + _, err = s.Where("id = ?", list.ID).Update(list) + if err != nil { + return + } if list.IsFavorite { if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil { return err @@ -572,6 +585,7 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error) "identifier", "hex_color", "background_file_id", + "position", } if list.Description != "" { colsToUpdate = append(colsToUpdate, "description") diff --git a/pkg/models/list_test.go b/pkg/models/list_test.go index c80355a5948..5a52b9016e1 100644 --- a/pkg/models/list_test.go +++ b/pkg/models/list_test.go @@ -200,8 +200,11 @@ func TestList_ReadAll(t *testing.T) { assert.NoError(t, err) assert.Equal(t, reflect.TypeOf(lists3).Kind(), reflect.Slice) - ls := reflect.ValueOf(lists3) - assert.Equal(t, 16, ls.Len()) + ls := lists3.([]*List) + assert.Equal(t, 16, len(ls)) + assert.Equal(t, int64(3), ls[0].ID) // List 3 has a position of 1 and should be sorted first + assert.Equal(t, int64(1), ls[1].ID) + assert.Equal(t, int64(4), ls[2].ID) _ = s.Close() }) t.Run("lists for nonexistant user", func(t *testing.T) { diff --git a/pkg/models/namespace.go b/pkg/models/namespace.go index 04f2bad6e7c..969bd0e2f56 100644 --- a/pkg/models/namespace.go +++ b/pkg/models/namespace.go @@ -315,6 +315,7 @@ func getNamespaceSubscriptions(s *xorm.Session, namespaceIDs []int64, userID int func getListsForNamespaces(s *xorm.Session, namespaceIDs []int64, archived bool) ([]*List, error) { lists := []*List{} listQuery := s. + OrderBy("position"). In("namespace_id", namespaceIDs) if !archived { diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index 3b749dbc757..ec2bf0a5ca4 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -73,11 +73,11 @@ func validateTaskField(fieldName string) error { taskPropertyCreated, taskPropertyUpdated, taskPropertyPosition, + taskPropertyKanbanPosition, taskPropertyBucketID: return nil } return ErrInvalidTaskField{TaskField: fieldName} - } func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskOptions, err error) { diff --git a/pkg/models/task_collection_sort.go b/pkg/models/task_collection_sort.go index 1cea9230969..7b72cc2090b 100644 --- a/pkg/models/task_collection_sort.go +++ b/pkg/models/task_collection_sort.go @@ -26,25 +26,26 @@ type ( ) const ( - taskPropertyID string = "id" - taskPropertyTitle string = "title" - taskPropertyDescription string = "description" - taskPropertyDone string = "done" - taskPropertyDoneAt string = "done_at" - taskPropertyDueDate string = "due_date" - taskPropertyCreatedByID string = "created_by_id" - taskPropertyListID string = "list_id" - taskPropertyRepeatAfter string = "repeat_after" - taskPropertyPriority string = "priority" - taskPropertyStartDate string = "start_date" - taskPropertyEndDate string = "end_date" - taskPropertyHexColor string = "hex_color" - taskPropertyPercentDone string = "percent_done" - taskPropertyUID string = "uid" - taskPropertyCreated string = "created" - taskPropertyUpdated string = "updated" - taskPropertyPosition string = "position" - taskPropertyBucketID string = "bucket_id" + taskPropertyID string = "id" + taskPropertyTitle string = "title" + taskPropertyDescription string = "description" + taskPropertyDone string = "done" + taskPropertyDoneAt string = "done_at" + taskPropertyDueDate string = "due_date" + taskPropertyCreatedByID string = "created_by_id" + taskPropertyListID string = "list_id" + taskPropertyRepeatAfter string = "repeat_after" + taskPropertyPriority string = "priority" + taskPropertyStartDate string = "start_date" + taskPropertyEndDate string = "end_date" + taskPropertyHexColor string = "hex_color" + taskPropertyPercentDone string = "percent_done" + taskPropertyUID string = "uid" + taskPropertyCreated string = "created" + taskPropertyUpdated string = "updated" + taskPropertyPosition string = "position" + taskPropertyKanbanPosition string = "kanban_position" + taskPropertyBucketID string = "bucket_id" ) const ( diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index 28b70dbd9a7..8452892695a 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -90,6 +90,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { ListID: 1, BucketID: 1, IsFavorite: true, + Position: 2, Labels: []*Label{ label4, }, @@ -160,6 +161,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { CreatedBy: user1, ListID: 1, BucketID: 1, + Position: 4, Labels: []*Label{ label4, }, @@ -517,6 +519,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { Created: time.Unix(1543626724, 0).In(loc), Updated: time.Unix(1543626724, 0).In(loc), BucketID: 1, + Position: 2, }, }, }, @@ -1033,6 +1036,51 @@ func TestTaskCollection_ReadAll(t *testing.T) { }, wantErr: false, }, + { + name: "order by position", + fields: fields{ + SortBy: []string{"position", "id"}, + OrderBy: []string{"asc", "asc"}, + }, + args: args{ + a: &user.User{ID: 1}, + }, + want: []*Task{ + // the other ones don't have a position set + 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, + // The only tasks with a position set + task1, + task2, + }, + }, } for _, tt := range tests { diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 9479c42f94e..8995733421b 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -118,6 +118,8 @@ type Task struct { // 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 position of tasks in the kanban board. See the docs for the `position` property on how to use this. + KanbanPosition float64 `xorm:"double null" json:"kanban_position"` // The user who initially created the task. CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"` @@ -836,6 +838,14 @@ func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucke return nil } +func calculateDefaultPosition(entityID int64, position float64) float64 { + if position == 0 { + return float64(entityID) * math.Pow(2, 16) + } + + return position +} + // Create is the implementation to create a list task // @Summary Create a task // @Description Inserts a task into a list. @@ -895,9 +905,8 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err t.Index = latestTask.Index + 1 // If no position was supplied, set a default one - if t.Position == 0 { - t.Position = float64(latestTask.ID+1) * math.Pow(2, 16) - } + t.Position = calculateDefaultPosition(latestTask.ID+1, t.Position) + t.KanbanPosition = calculateDefaultPosition(latestTask.ID+1, t.KanbanPosition) if _, err = s.Insert(t); err != nil { return err } @@ -1001,6 +1010,7 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) { "bucket_id", "position", "repeat_mode", + "kanban_position", } // When a repeating task is marked as done, we update all deadlines and reminders and set it as undone @@ -1107,6 +1117,9 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) { if t.Position == 0 { ot.Position = 0 } + if t.KanbanPosition == 0 { + ot.KanbanPosition = 0 + } // Repeat from current date if t.RepeatMode == TaskRepeatModeDefault { ot.RepeatMode = TaskRepeatModeDefault diff --git a/pkg/models/tasks_test.go b/pkg/models/tasks_test.go index 7a5dc69ffb1..49cb590c799 100644 --- a/pkg/models/tasks_test.go +++ b/pkg/models/tasks_test.go @@ -192,12 +192,12 @@ func TestTask_Update(t *testing.T) { defer s.Close() task := &Task{ - ID: 4, - Title: "test10000", - Description: "Lorem Ipsum Dolor", - Position: 10, - ListID: 1, - BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3 + ID: 4, + Title: "test10000", + Description: "Lorem Ipsum Dolor", + KanbanPosition: 10, + ListID: 1, + BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3 } err := task.Update(s, u) assert.NoError(t, err) diff --git a/pkg/modules/migration/trello/trello.go b/pkg/modules/migration/trello/trello.go index 316155b101a..e637218871f 100644 --- a/pkg/modules/migration/trello/trello.go +++ b/pkg/modules/migration/trello/trello.go @@ -196,10 +196,10 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board) (fullVikunjaHierachi // The usual stuff: Title, description, position, bucket id task := &models.Task{ - Title: card.Name, - Description: card.Desc, - Position: card.Pos, - BucketID: bucketID, + Title: card.Name, + Description: card.Desc, + KanbanPosition: card.Pos, + BucketID: bucketID, } if card.Due != nil { diff --git a/pkg/modules/migration/trello/trello_test.go b/pkg/modules/migration/trello/trello_test.go index 5a9d2485069..99e2170d1d4 100644 --- a/pkg/modules/migration/trello/trello_test.go +++ b/pkg/modules/migration/trello/trello_test.go @@ -209,11 +209,11 @@ func TestConvertTrelloToVikunja(t *testing.T) { }, Tasks: []*models.Task{ { - Title: "Test Card 1", - Description: "Card Description", - BucketID: 1, - Position: 123, - DueDate: time1, + Title: "Test Card 1", + Description: "Card Description", + BucketID: 1, + KanbanPosition: 123, + DueDate: time1, Labels: []*models.Label{ { Title: "Label 1", @@ -248,18 +248,18 @@ func TestConvertTrelloToVikunja(t *testing.T) { * [ ] Pending Task * [ ] Another Pending Task`, - BucketID: 1, - Position: 124, + BucketID: 1, + KanbanPosition: 124, }, { - Title: "Test Card 3", - BucketID: 1, - Position: 126, + Title: "Test Card 3", + BucketID: 1, + KanbanPosition: 126, }, { - Title: "Test Card 4", - BucketID: 1, - Position: 127, + Title: "Test Card 4", + BucketID: 1, + KanbanPosition: 127, Labels: []*models.Label{ { Title: "Label 2", @@ -268,9 +268,9 @@ func TestConvertTrelloToVikunja(t *testing.T) { }, }, { - Title: "Test Card 5", - BucketID: 2, - Position: 111, + Title: "Test Card 5", + BucketID: 2, + KanbanPosition: 111, Labels: []*models.Label{ { Title: "Label 3", @@ -279,20 +279,20 @@ func TestConvertTrelloToVikunja(t *testing.T) { }, }, { - Title: "Test Card 6", - BucketID: 2, - Position: 222, - DueDate: time1, + Title: "Test Card 6", + BucketID: 2, + KanbanPosition: 222, + DueDate: time1, }, { - Title: "Test Card 7", - BucketID: 2, - Position: 333, + Title: "Test Card 7", + BucketID: 2, + KanbanPosition: 333, }, { - Title: "Test Card 8", - BucketID: 2, - Position: 444, + Title: "Test Card 8", + BucketID: 2, + KanbanPosition: 444, }, }, }, @@ -306,9 +306,9 @@ func TestConvertTrelloToVikunja(t *testing.T) { }, Tasks: []*models.Task{ { - Title: "Test Card 634", - BucketID: 3, - Position: 123, + Title: "Test Card 634", + BucketID: 3, + KanbanPosition: 123, }, }, }, @@ -323,9 +323,9 @@ func TestConvertTrelloToVikunja(t *testing.T) { }, Tasks: []*models.Task{ { - Title: "Test Card 63423", - BucketID: 4, - Position: 123, + Title: "Test Card 63423", + BucketID: 4, + KanbanPosition: 123, }, }, }, diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 1743ccd2dde..e0c52082d01 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -7218,6 +7218,10 @@ var doc = `{ "type": "string" } }, + "position": { + "description": "The position this bucket has when querying all buckets. See the tasks.position property on how to use this.", + "type": "number" + }, "sort_by": { "description": "The query parameter to sort by. This is for ex. done, priority, etc.", "type": "array", @@ -7325,6 +7329,10 @@ var doc = `{ "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list. This value depends on the user making the call to the api.", "type": "boolean" }, + "kanban_position": { + "description": "The position of tasks in the kanban board. See the docs for the ` + "`" + `position` + "`" + ` property on how to use this.", + "type": "number" + }, "labels": { "description": "An array of labels which are associated with this task.", "type": "array", @@ -7573,6 +7581,10 @@ var doc = `{ "description": "The user who created this list.", "$ref": "#/definitions/user.User" }, + "position": { + "description": "The position this list has when querying all lists. See the tasks.position property on how to use this.", + "type": "number" + }, "subscription": { "description": "The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one list.", "$ref": "#/definitions/models.Subscription" @@ -7901,6 +7913,10 @@ var doc = `{ "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list. This value depends on the user making the call to the api.", "type": "boolean" }, + "kanban_position": { + "description": "The position of tasks in the kanban board. See the docs for the ` + "`" + `position` + "`" + ` property on how to use this.", + "type": "number" + }, "labels": { "description": "An array of labels which are associated with this task.", "type": "array", diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 6514dc4ba68..2bc4b1ef0d5 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -7201,6 +7201,10 @@ "type": "string" } }, + "position": { + "description": "The position this bucket has when querying all buckets. See the tasks.position property on how to use this.", + "type": "number" + }, "sort_by": { "description": "The query parameter to sort by. This is for ex. done, priority, etc.", "type": "array", @@ -7308,6 +7312,10 @@ "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list. This value depends on the user making the call to the api.", "type": "boolean" }, + "kanban_position": { + "description": "The position of tasks in the kanban board. See the docs for the `position` property on how to use this.", + "type": "number" + }, "labels": { "description": "An array of labels which are associated with this task.", "type": "array", @@ -7556,6 +7564,10 @@ "description": "The user who created this list.", "$ref": "#/definitions/user.User" }, + "position": { + "description": "The position this list has when querying all lists. See the tasks.position property on how to use this.", + "type": "number" + }, "subscription": { "description": "The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one list.", "$ref": "#/definitions/models.Subscription" @@ -7884,6 +7896,10 @@ "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list. This value depends on the user making the call to the api.", "type": "boolean" }, + "kanban_position": { + "description": "The position of tasks in the kanban board. See the docs for the `position` property on how to use this.", + "type": "number" + }, "labels": { "description": "An array of labels which are associated with this task.", "type": "array", diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index ec7e0c9abce..3f5aebca17f 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -102,6 +102,10 @@ definitions: items: type: string type: array + position: + description: The position this bucket has when querying all buckets. See the + tasks.position property on how to use this. + type: number sort_by: description: The query parameter to sort by. This is for ex. done, priority, etc. @@ -186,6 +190,10 @@ definitions: a separate "Important" list. This value depends on the user making the call to the api. type: boolean + kanban_position: + description: The position of tasks in the kanban board. See the docs for the + `position` property on how to use this. + type: number labels: description: An array of labels which are associated with this task. items: @@ -403,6 +411,10 @@ definitions: owner: $ref: '#/definitions/user.User' description: The user who created this list. + position: + description: The position this list has when querying all lists. See the tasks.position + property on how to use this. + type: number subscription: $ref: '#/definitions/models.Subscription' description: |- @@ -669,6 +681,10 @@ definitions: a separate "Important" list. This value depends on the user making the call to the api. type: boolean + kanban_position: + description: The position of tasks in the kanban board. See the docs for the + `position` property on how to use this. + type: number labels: description: An array of labels which are associated with this task. items: