From e5559137dd471ff13d0bf243b32df20821be7946 Mon Sep 17 00:00:00 2001 From: konrad Date: Sat, 5 Sep 2020 20:16:02 +0000 Subject: [PATCH] Favorite tasks (#653) Fixed namespace tests Add test for favorite tasks Fix favorite tasks not being updated Fix integration tests Fix lint Return a pseudo namespace and list for favorites Make sure users can only see their favorites Add condition show tasks from the favorites list Regenerate swagger docs Add favorite field to task Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/api/pulls/653 --- pkg/db/fixtures/tasks.yml | 3 ++ pkg/integrations/task_collection_test.go | 30 ++++++++-------- pkg/migration/20200904101559.go | 2 +- pkg/migration/20200905151040.go | 43 ++++++++++++++++++++++ pkg/models/kanban.go | 2 +- pkg/models/label.go | 2 +- pkg/models/list.go | 16 +++++++++ pkg/models/list_duplicate.go | 2 +- pkg/models/list_rights.go | 27 ++++++++++++++ pkg/models/namespace.go | 38 +++++++++++++++++++- pkg/models/namespace_test.go | 10 +++--- pkg/models/task_collection.go | 4 +-- pkg/models/task_collection_test.go | 15 ++++++++ pkg/models/tasks.go | 45 +++++++++++++++++++++--- pkg/swagger/docs.go | 8 +++++ pkg/swagger/swagger.json | 8 +++++ pkg/swagger/swagger.yaml | 6 ++++ 17 files changed, 230 insertions(+), 31 deletions(-) create mode 100644 pkg/migration/20200905151040.go diff --git a/pkg/db/fixtures/tasks.yml b/pkg/db/fixtures/tasks.yml index b8644ef82b..0fb2a9864e 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 + is_favorite: true - id: 2 title: 'task #2 done' done: true @@ -140,6 +141,7 @@ list_id: 6 index: 1 bucket_id: 6 + is_favorite: true created: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04 - id: 16 @@ -315,6 +317,7 @@ list_id: 20 index: 20 bucket_id: 5 + is_favorite: true created: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04 - id: 35 diff --git a/pkg/integrations/task_collection_test.go b/pkg/integrations/task_collection_test.go index 9c3e558198..6c203901ee 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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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_from_current_date":false,"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,"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_from_current_date":false,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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) @@ -318,33 +318,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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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_from_current_date":false,"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,"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_from_current_date":false,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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/20200904101559.go b/pkg/migration/20200904101559.go index 6c6ab8f300..5be1a79c36 100644 --- a/pkg/migration/20200904101559.go +++ b/pkg/migration/20200904101559.go @@ -32,7 +32,7 @@ func (buckets20200904101559) TableName() string { func init() { migrations = append(migrations, &xormigrate.Migration{ ID: "20200904101559", - Description: "", + Description: "Add limit field to kanban", Migrate: func(tx *xorm.Engine) error { return tx.Sync2(buckets20200904101559{}) }, diff --git a/pkg/migration/20200905151040.go b/pkg/migration/20200905151040.go new file mode 100644 index 0000000000..5bc3412940 --- /dev/null +++ b/pkg/migration/20200905151040.go @@ -0,0 +1,43 @@ +// 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 . + +package migration + +import ( + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type tasks20200905151040 struct { + IsFavorite bool `xorm:"default false" json:"is_favorite"` +} + +func (tasks20200905151040) TableName() string { + return "tasks" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20200905151040", + Description: "Add favorite field to tasks", + Migrate: func(tx *xorm.Engine) error { + return tx.Sync2(tasks20200905151040{}) + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index c07fe769aa..1f15be630c 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -128,7 +128,7 @@ func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (r }, }, } - tasks, _, _, err := getTasksForLists([]*List{{ID: b.ListID}}, opts) + tasks, _, _, err := getTasksForLists([]*List{{ID: b.ListID}}, auth, opts) if err != nil { return } diff --git a/pkg/models/label.go b/pkg/models/label.go index 9a23f76882..dc0524cdf3 100644 --- a/pkg/models/label.go +++ b/pkg/models/label.go @@ -217,7 +217,7 @@ func getUserTaskIDs(u *user.User) (taskIDs []int64, err error) { return nil, err } - tasks, _, _, err := getRawTasksForLists(lists, &taskOptions{ + tasks, _, _, err := getRawTasksForLists(lists, u, &taskOptions{ page: -1, perPage: 0, }) diff --git a/pkg/models/list.go b/pkg/models/list.go index c06c9c0975..8499f308cb 100644 --- a/pkg/models/list.go +++ b/pkg/models/list.go @@ -74,6 +74,16 @@ type ListBackgroundType struct { // ListBackgroundUpload represents the list upload background type const ListBackgroundUpload string = "upload" +// FavoritesPseudoList holds all tasks marked as favorites +var FavoritesPseudoList = List{ + ID: -1, + Title: "Favorites", + Description: "This list has all tasks marked as favorites.", + NamespaceID: FavoritesPseudoNamespace.ID, + Created: time.Now(), + Updated: time.Now(), +} + // GetListsByNamespaceID gets all lists in a namespace func GetListsByNamespaceID(nID int64, doer *user.User) (lists []*List, err error) { if nID == -1 { @@ -165,6 +175,12 @@ func (l *List) ReadAll(a web.Auth, search string, page int, perPage int) (result // @Failure 500 {object} models.Message "Internal error" // @Router /lists/{id} [get] func (l *List) ReadOne() (err error) { + + if l.ID == FavoritesPseudoList.ID { + // Already "built" the list in CanRead + return nil + } + // Get list owner l.Owner, err = user.GetUserByID(l.OwnerID) if err != nil { diff --git a/pkg/models/list_duplicate.go b/pkg/models/list_duplicate.go index 56c7390528..916ad8795a 100644 --- a/pkg/models/list_duplicate.go +++ b/pkg/models/list_duplicate.go @@ -106,7 +106,7 @@ func (ld *ListDuplicate) Create(a web.Auth) (err error) { log.Debugf("Duplicated all buckets from list %d into %d", ld.ListID, ld.List.ID) // Get all tasks + all task details - tasks, _, _, err := getTasksForLists([]*List{{ID: ld.ListID}}, &taskOptions{}) + tasks, _, _, err := getTasksForLists([]*List{{ID: ld.ListID}}, a, &taskOptions{}) if err != nil { return err } diff --git a/pkg/models/list_rights.go b/pkg/models/list_rights.go index b8635f78c5..875409e8c7 100644 --- a/pkg/models/list_rights.go +++ b/pkg/models/list_rights.go @@ -25,6 +25,11 @@ import ( // CanWrite return whether the user can write on that list or not func (l *List) CanWrite(a web.Auth) (bool, error) { + // The favorite list can't be edited + if l.ID == FavoritesPseudoList.ID { + return false, nil + } + // Get the list and check the right originalList := &List{ID: l.ID} err := originalList.GetSimpleByID() @@ -63,6 +68,19 @@ func (l *List) CanWrite(a web.Auth) (bool, error) { // CanRead checks if a user has read access to a list func (l *List) CanRead(a web.Auth) (bool, int, error) { + + // The favorite list needs a special treatment + if l.ID == FavoritesPseudoList.ID { + owner, err := user.GetFromAuth(a) + if err != nil { + return false, 0, err + } + + *l = FavoritesPseudoList + l.Owner = owner + return true, int(RightRead), nil + } + // Check if the user is either owner or can read if err := l.GetSimpleByID(); err != nil { return false, 0, err @@ -83,6 +101,10 @@ func (l *List) CanRead(a web.Auth) (bool, int, error) { // CanUpdate checks if the user can update a list func (l *List) CanUpdate(a web.Auth) (canUpdate bool, err error) { + // The favorite list can't be edited + if l.ID == FavoritesPseudoList.ID { + return false, nil + } canUpdate, err = l.CanWrite(a) // If the list is archived and the user tries to un-archive it, let the request through if IsErrListIsArchived(err) && !l.IsArchived { @@ -105,6 +127,11 @@ func (l *List) CanCreate(a web.Auth) (bool, error) { // IsAdmin returns whether the user has admin rights on the list or not func (l *List) IsAdmin(a web.Auth) (bool, error) { + // The favorite list can't be edited + if l.ID == FavoritesPseudoList.ID { + return false, nil + } + originalList := &List{ID: l.ID} err := originalList.GetSimpleByID() if err != nil { diff --git a/pkg/models/namespace.go b/pkg/models/namespace.go index 1d3a2a74cf..83e030a422 100644 --- a/pkg/models/namespace.go +++ b/pkg/models/namespace.go @@ -62,6 +62,15 @@ var PseudoNamespace = Namespace{ Updated: time.Now(), } +// FavoritesPseudoNamespace is a pseudo namespace used to hold favorited lists and tasks +var FavoritesPseudoNamespace = Namespace{ + ID: -2, + Title: "Favorites", + Description: "Favorite lists and tasks.", + Created: time.Now(), + Updated: time.Now(), +} + // TableName makes beautiful table names func (Namespace) TableName() string { return "namespaces" @@ -79,6 +88,11 @@ func (n *Namespace) GetSimpleByID() (err error) { return } + if n.ID == FavoritesPseudoNamespace.ID { + *n = FavoritesPseudoNamespace + return + } + namespaceFromDB := &Namespace{} exists, err := x.Where("id = ?", n.ID).Get(namespaceFromDB) if err != nil { @@ -180,8 +194,17 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r ) } - // Create our pseudo-namespace to hold the shared lists + // Create our pseudo namespace with favorite lists // We want this one at the beginning, which is why we create it here + pseudoFavoriteNamespace := FavoritesPseudoNamespace + pseudoFavoriteNamespace.Owner = doer + all = append(all, &NamespaceWithLists{ + Namespace: pseudoFavoriteNamespace, + Lists: []*List{{}}, + }) + *all[0].Lists[0] = FavoritesPseudoList // Copying the list to be able to modify it later + + // Create our pseudo namespace to hold the shared lists pseudonamespace := PseudoNamespace pseudonamespace.Owner = doer all = append(all, &NamespaceWithLists{ @@ -266,6 +289,19 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r // Remove the pseudonamespace if we don't have any shared lists if len(individualLists) == 0 { + all = append(all[:1], all[2:]...) + } + + // Check if we have any favorites and remove the favorites namespace from the list if not + favoriteCount, err := x. + Join("INNER", "list", "tasks.list_id = list.id"). + Join("INNER", "namespaces", "list.namespace_id = namespaces.id"). + Where(builder.And(builder.Eq{"is_favorite": true}, builder.In("namespaces.id", namespaceids))). + Count(&Task{}) + if err != nil { + return nil, 0, 0, err + } + if favoriteCount == 0 { all = append(all[:0], all[1:]...) } diff --git a/pkg/models/namespace_test.go b/pkg/models/namespace_test.go index 5c99aa9eb5..1c23cf7296 100644 --- a/pkg/models/namespace_test.go +++ b/pkg/models/namespace_test.go @@ -143,8 +143,9 @@ func TestNamespace_ReadAll(t *testing.T) { namespaces := nn.([]*NamespaceWithLists) assert.NoError(t, err) assert.NotNil(t, namespaces) - assert.Len(t, namespaces, 9) // Total of 9 including shared - assert.Equal(t, int64(-1), namespaces[0].ID) // The first one should be the one with the shared namespaces + assert.Len(t, namespaces, 10) // Total of 10 including shared & favorites + assert.Equal(t, int64(-2), namespaces[0].ID) // The first one should be the one with favorites + assert.Equal(t, int64(-1), namespaces[1].ID) // The second one should be the one with the shared namespaces // Ensure every list and namespace are not archived for _, namespace := range namespaces { assert.False(t, namespace.IsArchived) @@ -161,7 +162,8 @@ func TestNamespace_ReadAll(t *testing.T) { namespaces := nn.([]*NamespaceWithLists) assert.NoError(t, err) assert.NotNil(t, namespaces) - assert.Len(t, namespaces, 10) // Total of 10 including shared, one is archived - assert.Equal(t, int64(-1), namespaces[0].ID) // The first one should be the one with the shared namespaces + assert.Len(t, namespaces, 11) // Total of 11 including shared & favorites, one is archived + assert.Equal(t, int64(-2), namespaces[0].ID) // The first one should be the one with favorites + assert.Equal(t, int64(-1), namespaces[1].ID) // The second one should be the one with the shared namespaces }) } diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index 082b2f3789..a7eb1d1849 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -150,7 +150,7 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i if err != nil { return nil, 0, 0, err } - return getTasksForLists([]*List{list}, taskopts) + return getTasksForLists([]*List{list}, a, taskopts) } // If the list ID is not set, we get all tasks for the user. @@ -176,5 +176,5 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i tf.Lists = []*List{{ID: tf.ListID}} } - return getTasksForLists(tf.Lists, taskopts) + return getTasksForLists(tf.Lists, a, taskopts) } diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index 31f2dc029f..ddd3ddd79b 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -66,6 +66,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { CreatedBy: user1, ListID: 1, BucketID: 1, + IsFavorite: true, Labels: []*Label{ { ID: 4, @@ -288,6 +289,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { CreatedByID: 6, CreatedBy: user6, ListID: 6, + IsFavorite: true, RelatedTasks: map[RelationKind][]*Task{}, BucketID: 6, Created: time.Unix(1543626724, 0).In(loc), @@ -484,6 +486,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { Index: 1, CreatedByID: 1, ListID: 1, + IsFavorite: true, Created: time.Unix(1543626724, 0).In(loc), Updated: time.Unix(1543626724, 0).In(loc), BucketID: 1, @@ -836,6 +839,18 @@ func TestTaskCollection_ReadAll(t *testing.T) { }, wantErr: false, }, + { + name: "favorited tasks", + args: defaultArgs, + fields: fields{ + ListID: FavoritesPseudoList.ID, + }, + want: []*Task{ + task1, + task15, + // Task 34 is also a favorite, but on a list user 1 has no access to. + }, + }, } for _, tt := range tests { diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 3a0ff05380..af86d83ff3 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -84,6 +84,9 @@ type Task struct { // All attachments this task has Attachments []*TaskAttachment `xorm:"-" json:"attachments"` + // True if a task is a favorite task. Favorite tasks show up in a separate "Important" list + IsFavorite bool `xorm:"default false" json:"is_favorite"` + // A timestamp when this task was created. You cannot change this value. Created time.Time `xorm:"created not null" json:"created"` // A timestamp when this task was last updated. You cannot change this value. @@ -165,7 +168,7 @@ func (t *Task) ReadAll(a web.Auth, search string, page int, perPage int) (result return nil, 0, 0, nil } -func getRawTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) { +func getRawTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) { // If the user does not have any lists, don't try to get any tasks if len(lists) == 0 { @@ -179,7 +182,11 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resul // Get all list IDs and get the tasks var listIDs []int64 + var hasFavoriteLists bool for _, l := range lists { + if l.ID == FavoritesPseudoList.ID { + hasFavoriteLists = true + } listIDs = append(listIDs, l.ID) } @@ -274,11 +281,34 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resul } } + var listIDCond builder.Cond + var listCond builder.Cond if len(listIDs) > 0 { - query = query.In("list_id", listIDs) - queryCount = queryCount.In("list_id", listIDs) + listIDCond = builder.In("list_id", listIDs) + listCond = listIDCond } + if hasFavoriteLists { + // Make sure users can only see their favorites + userLists, _, _, err := getRawListsForUser(&listOptions{ + user: &user.User{ID: a.GetID()}, + page: -1, + }) + if err != nil { + return nil, 0, 0, err + } + + userListIDs := make([]int64, len(userLists)) + for _, l := range userLists { + userListIDs = append(userListIDs, l.ID) + } + + listCond = builder.Or(listIDCond, builder.And(builder.Eq{"is_favorite": true}, builder.In("list_id", userListIDs))) + } + + query = query.Where(listCond) + queryCount = queryCount.Where(listCond) + if len(filters) > 0 { if opts.filterConcat == filterConcatOr { query = query.Where(builder.Or(filters...)) @@ -311,9 +341,9 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resul return tasks, len(tasks), totalItems, nil } -func getTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) { +func getTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) { - tasks, resultCount, totalItems, err = getRawTasksForLists(lists, opts) + tasks, resultCount, totalItems, err = getRawTasksForLists(lists, a, opts) if err != nil { return nil, 0, 0, err } @@ -760,6 +790,7 @@ func (t *Task) Update() (err error) { "bucket_id", "position", "repeat_from_current_date", + "is_favorite", } // Make sure we have a bucket @@ -868,6 +899,10 @@ func (t *Task) Update() (err error) { if !t.RepeatFromCurrentDate { ot.RepeatFromCurrentDate = false } + // Is Favorite + if !t.IsFavorite { + ot.IsFavorite = false + } _, err = s.ID(t.ID). Cols(colsToUpdate...). diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index ed155cbc4e..65b1a11a0f 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -6341,6 +6341,10 @@ var doc = `{ "description": "The task index, calculated per list", "type": "integer" }, + "is_favorite": { + "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list", + "type": "boolean" + }, "labels": { "description": "An array of labels which are associated with this task.", "type": "array", @@ -6802,6 +6806,10 @@ var doc = `{ "description": "The task index, calculated per list", "type": "integer" }, + "is_favorite": { + "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list", + "type": "boolean" + }, "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 1b88b06798..71101f8e74 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -6324,6 +6324,10 @@ "description": "The task index, calculated per list", "type": "integer" }, + "is_favorite": { + "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list", + "type": "boolean" + }, "labels": { "description": "An array of labels which are associated with this task.", "type": "array", @@ -6785,6 +6789,10 @@ "description": "The task index, calculated per list", "type": "integer" }, + "is_favorite": { + "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list", + "type": "boolean" + }, "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 df96a673f1..c3104072d8 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -130,6 +130,9 @@ definitions: index: description: The task index, calculated per list type: integer + is_favorite: + description: True if a task is a favorite task. Favorite tasks show up in a separate "Important" list + type: boolean labels: description: An array of labels which are associated with this task. items: @@ -478,6 +481,9 @@ definitions: index: description: The task index, calculated per list type: integer + is_favorite: + description: True if a task is a favorite task. Favorite tasks show up in a separate "Important" list + type: boolean labels: description: An array of labels which are associated with this task. items: