diff --git a/Makefile b/Makefile index e05555d7..7728f76c 100644 --- a/Makefile +++ b/Makefile @@ -214,7 +214,7 @@ gocyclo-check: go get -u github.com/fzipp/gocyclo; \ go install $(GOFLAGS) github.com/fzipp/gocyclo; \ fi - for S in $(GOFILES); do gocyclo -over 23 $$S || exit 1; done; + for S in $(GOFILES); do gocyclo -over 24 $$S || exit 1; done; .PHONY: static-check static-check: diff --git a/docs/content/doc/usage/errors.md b/docs/content/doc/usage/errors.md index cc89e1b4..a312284f 100644 --- a/docs/content/doc/usage/errors.md +++ b/docs/content/doc/usage/errors.md @@ -33,6 +33,7 @@ This document describes the different errors Vikunja can return. | 3004 | 403 | The user needs to have read permissions on that list to perform that action. | | 3005 | 400 | The list title cannot be empty. | | 3006 | 404 | The list share does not exist. | +| 3007 | 400 | A list with this identifier already exists. | | 4001 | 400 | The list task text cannot be empty. | | 4002 | 404 | The list task does not exist. | | 4003 | 403 | All bulk editing tasks must belong to the same list. | diff --git a/pkg/integrations/task_collection_test.go b/pkg/integrations/task_collection_test.go index e9f3e497..0acfc73e 100644 --- a/pkg/integrations/task_collection_test.go +++ b/pkg/integrations/task_collection_test.go @@ -95,33 +95,33 @@ 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,"text":"task #33 with percent done","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) }) 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,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,`) + assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":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,"text":"task #33 with percent done","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) }) // should equal duedate asc t.Run("by duedate", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) }) t.Run("by duedate desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`) + assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"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_unix"}, "order_by": []string{"asc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) }) t.Run("invalid sort parameter", func(t *testing.T) { _, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams) @@ -249,33 +249,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,"text":"task #33 with percent done","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) }) 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,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,`) + assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":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,"text":"task #33 with percent done","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) }) // should equal duedate asc t.Run("by duedate", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) }) t.Run("by duedate desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`) + assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"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_unix"}, "order_by": []string{"asc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) }) t.Run("invalid parameter", func(t *testing.T) { // Invalid parameter should not sort at all diff --git a/pkg/migration/20191207204427.go b/pkg/migration/20191207204427.go new file mode 100644 index 00000000..7fbad831 --- /dev/null +++ b/pkg/migration/20191207204427.go @@ -0,0 +1,44 @@ +// Copyright 2019 Vikunja and contriubtors. All rights reserved. +// +// This file is part of Vikunja. +// +// Vikunja 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. +// +// Vikunja 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 Vikunja. If not, see . + +package migration + +import ( + "github.com/go-xorm/xorm" + "src.techknowlogick.com/xormigrate" +) + +type list20191207204427 struct { + Identifier string `xorm:"varchar(10) null" json:"identifier"` +} + +func (list20191207204427) TableName() string { + return "list" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20191207204427", + Description: "Add task identifier to list", + Migrate: func(tx *xorm.Engine) error { + return tx.Sync2(list20191207204427{}) + }, + Rollback: func(tx *xorm.Engine) error { + return dropTableColum(tx, "list", "indentifier") + }, + }) +} diff --git a/pkg/migration/20191207220736.go b/pkg/migration/20191207220736.go new file mode 100644 index 00000000..88e7be45 --- /dev/null +++ b/pkg/migration/20191207220736.go @@ -0,0 +1,77 @@ +// Copyright 2019 Vikunja and contriubtors. All rights reserved. +// +// This file is part of Vikunja. +// +// Vikunja 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. +// +// Vikunja 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 Vikunja. If not, see . + +package migration + +import ( + "github.com/go-xorm/xorm" + "src.techknowlogick.com/xormigrate" +) + +type task20191207220736 struct { + ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"listtask"` + Index int64 `xorm:"int(11) not null default 0" json:"index"` + ListID int64 `xorm:"int(11) INDEX not null" json:"listID" param:"list"` +} + +func (task20191207220736) TableName() string { + return "tasks" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20191207220736", + Description: "Add task index to tasks", + Migrate: func(tx *xorm.Engine) error { + err := tx.Sync2(task20191207220736{}) + if err != nil { + return err + } + + // Get all tasks, ordered by list and id + tasks := []*task20191207220736{} + err = tx. + OrderBy("list_id asc, id asc"). + Find(&tasks) + if err != nil { + return err + } + + var currentIndex int64 = 1 + for i, task := range tasks { + // Reset the current counter if we're encountering a new list + // We can do this because the list is sorted by list id + if i > 0 && tasks[i-1].ListID != task.ListID { + currentIndex = 1 + } + + task.Index = currentIndex + _, err = tx.Where("id = ?", task.ID).Update(task) + if err != nil { + return err + } + + currentIndex++ + } + + return nil + }, + Rollback: func(tx *xorm.Engine) error { + return dropTableColum(tx, "tasks", "index") + }, + }) +} diff --git a/pkg/models/error.go b/pkg/models/error.go index 0af2b978..9824fe2b 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -471,6 +471,33 @@ func (err ErrListShareDoesNotExist) HTTPError() web.HTTPError { return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeListShareDoesNotExist, Message: "The list share does not exist."} } +// ErrListIdentifierIsNotUnique represents a "ErrListIdentifierIsNotUnique" kind of error. Used if the provided list identifier is not unique. +type ErrListIdentifierIsNotUnique struct { + Identifier string +} + +// IsErrListIdentifierIsNotUnique checks if an error is a ErrListIdentifierIsNotUnique. +func IsErrListIdentifierIsNotUnique(err error) bool { + _, ok := err.(ErrListIdentifierIsNotUnique) + return ok +} + +func (err ErrListIdentifierIsNotUnique) Error() string { + return fmt.Sprintf("List identifier is not unique.") +} + +// ErrCodeListIdentifierIsNotUnique holds the unique world-error code of this error +const ErrCodeListIdentifierIsNotUnique = 3007 + +// HTTPError holds the http error description +func (err ErrListIdentifierIsNotUnique) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusBadRequest, + Code: ErrCodeListIdentifierIsNotUnique, + Message: "A list with this identifier already exists.", + } +} + // ================ // List task errors // ================ diff --git a/pkg/models/fixtures/list.yml b/pkg/models/fixtures/list.yml index a86aa608..fa8edcb9 100644 --- a/pkg/models/fixtures/list.yml +++ b/pkg/models/fixtures/list.yml @@ -2,6 +2,7 @@ id: 1 title: Test1 description: Lorem Ipsum + identifier: test1 owner_id: 1 namespace_id: 1 updated: 0 @@ -10,6 +11,7 @@ id: 2 title: Test2 description: Lorem Ipsum + identifier: test2 owner_id: 3 namespace_id: 1 updated: 0 @@ -18,6 +20,7 @@ id: 3 title: Test3 description: Lorem Ipsum + identifier: test3 owner_id: 3 namespace_id: 2 updated: 0 @@ -26,6 +29,7 @@ id: 4 title: Test4 description: Lorem Ipsum + identifier: test4 owner_id: 3 namespace_id: 3 updated: 0 @@ -34,6 +38,7 @@ id: 5 title: Test5 description: Lorem Ipsum + identifier: test5 owner_id: 5 namespace_id: 5 updated: 0 @@ -42,6 +47,7 @@ id: 6 title: Test6 description: Lorem Ipsum + identifier: test6 owner_id: 6 namespace_id: 6 updated: 0 @@ -50,6 +56,7 @@ id: 7 title: Test7 description: Lorem Ipsum + identifier: test7 owner_id: 6 namespace_id: 6 updated: 0 @@ -58,6 +65,7 @@ id: 8 title: Test8 description: Lorem Ipsum + identifier: test8 owner_id: 6 namespace_id: 6 updated: 0 @@ -66,6 +74,7 @@ id: 9 title: Test9 description: Lorem Ipsum + identifier: test9 owner_id: 6 namespace_id: 6 updated: 0 @@ -74,6 +83,7 @@ id: 10 title: Test10 description: Lorem Ipsum + identifier: test10 owner_id: 6 namespace_id: 6 updated: 0 @@ -82,6 +92,7 @@ id: 11 title: Test11 description: Lorem Ipsum + identifier: test11 owner_id: 6 namespace_id: 6 updated: 0 @@ -90,6 +101,7 @@ id: 12 title: Test12 description: Lorem Ipsum + identifier: test12 owner_id: 6 namespace_id: 7 updated: 0 @@ -98,6 +110,7 @@ id: 13 title: Test13 description: Lorem Ipsum + identifier: test13 owner_id: 6 namespace_id: 8 updated: 0 @@ -106,6 +119,7 @@ id: 14 title: Test14 description: Lorem Ipsum + identifier: test14 owner_id: 6 namespace_id: 9 updated: 0 @@ -114,6 +128,7 @@ id: 15 title: Test15 description: Lorem Ipsum + identifier: test15 owner_id: 6 namespace_id: 10 updated: 0 @@ -122,6 +137,7 @@ id: 16 title: Test16 description: Lorem Ipsum + identifier: test16 owner_id: 6 namespace_id: 11 updated: 0 @@ -130,6 +146,7 @@ id: 17 title: Test17 description: Lorem Ipsum + identifier: test17 owner_id: 6 namespace_id: 12 updated: 0 @@ -140,6 +157,7 @@ id: 18 title: Test18 description: Lorem Ipsum + identifier: test18 owner_id: 7 namespace_id: 13 updated: 0 @@ -148,6 +166,7 @@ id: 19 title: Test19 description: Lorem Ipsum + identifier: test19 owner_id: 7 namespace_id: 14 updated: 0 @@ -156,6 +175,7 @@ id: 20 title: Test20 description: Lorem Ipsum + identifier: test20 owner_id: 13 namespace_id: 15 updated: 0 diff --git a/pkg/models/fixtures/tasks.yml b/pkg/models/fixtures/tasks.yml index 246ba478..38818119 100644 --- a/pkg/models/fixtures/tasks.yml +++ b/pkg/models/fixtures/tasks.yml @@ -3,6 +3,7 @@ description: 'Lorem Ipsum' created_by_id: 1 list_id: 1 + index: 1 created: 1543626724 updated: 1543626724 - id: 2 @@ -10,12 +11,14 @@ done: true created_by_id: 1 list_id: 1 + index: 2 created: 1543626724 updated: 1543626724 - id: 3 text: 'task #3 high prio' created_by_id: 1 list_id: 1 + index: 3 created: 1543626724 updated: 1543626724 priority: 100 @@ -23,6 +26,7 @@ text: 'task #4 low prio' created_by_id: 1 list_id: 1 + index: 4 created: 1543626724 updated: 1543626724 priority: 1 @@ -30,6 +34,7 @@ text: 'task #5 higher due date' created_by_id: 1 list_id: 1 + index: 5 created: 1543626724 updated: 1543626724 due_date_unix: 1543636724 @@ -37,6 +42,7 @@ text: 'task #6 lower due date' created_by_id: 1 list_id: 1 + index: 6 created: 1543626724 updated: 1543626724 due_date_unix: 1543616724 @@ -44,6 +50,7 @@ text: 'task #7 with start date' created_by_id: 1 list_id: 1 + index: 7 created: 1543626724 updated: 1543626724 start_date_unix: 1544600000 @@ -51,6 +58,7 @@ text: 'task #8 with end date' created_by_id: 1 list_id: 1 + index: 8 created: 1543626724 updated: 1543626724 end_date_unix: 1544700000 @@ -58,6 +66,7 @@ text: 'task #9 with start and end date' created_by_id: 1 list_id: 1 + index: 9 created: 1543626724 updated: 1543626724 start_date_unix: 1544600000 @@ -66,108 +75,126 @@ text: 'task #10 basic' created_by_id: 1 list_id: 1 + index: 10 created: 1543626724 updated: 1543626724 - id: 11 text: 'task #11 basic' created_by_id: 1 list_id: 1 + index: 11 created: 1543626724 updated: 1543626724 - id: 12 text: 'task #12 basic' created_by_id: 1 list_id: 1 + index: 12 created: 1543626724 updated: 1543626724 - id: 13 text: 'task #13 basic other list' created_by_id: 1 list_id: 2 + index: 1 created: 1543626724 updated: 1543626724 - id: 14 text: 'task #14 basic other list' created_by_id: 5 list_id: 5 + index: 1 created: 1543626724 updated: 1543626724 - id: 15 text: 'task #15' created_by_id: 6 list_id: 6 + index: 1 created: 1543626724 updated: 1543626724 - id: 16 text: 'task #16' created_by_id: 6 list_id: 7 + index: 1 created: 1543626724 updated: 1543626724 - id: 17 text: 'task #17' created_by_id: 6 list_id: 8 + index: 1 created: 1543626724 updated: 1543626724 - id: 18 text: 'task #18' created_by_id: 6 list_id: 9 + index: 1 created: 1543626724 updated: 1543626724 - id: 19 text: 'task #19' created_by_id: 6 list_id: 10 + index: 1 created: 1543626724 updated: 1543626724 - id: 20 text: 'task #20' created_by_id: 6 list_id: 11 + index: 1 created: 1543626724 updated: 1543626724 - id: 21 text: 'task #21' created_by_id: 6 list_id: 12 + index: 1 created: 1543626724 updated: 1543626724 - id: 22 text: 'task #22' created_by_id: 6 list_id: 13 + index: 1 created: 1543626724 updated: 1543626724 - id: 23 text: 'task #23' created_by_id: 6 list_id: 14 + index: 1 created: 1543626724 updated: 1543626724 - id: 24 text: 'task #24' created_by_id: 6 list_id: 15 + index: 1 created: 1543626724 updated: 1543626724 - id: 25 text: 'task #25' created_by_id: 6 list_id: 16 + index: 1 created: 1543626724 updated: 1543626724 - id: 26 text: 'task #26' created_by_id: 6 list_id: 17 + index: 1 created: 1543626724 updated: 1543626724 - id: 27 text: 'task #27 with reminders' created_by_id: 1 list_id: 1 + index: 12 created: 1543626724 updated: 1543626724 - id: 28 @@ -176,24 +203,28 @@ created_by_id: 1 repeat_after: 3600 list_id: 1 + index: 13 created: 1543626724 updated: 1543626724 - id: 29 text: 'task #29 with parent task (1)' created_by_id: 1 list_id: 1 + index: 14 created: 1543626724 updated: 1543626724 - id: 30 text: 'task #30 with assignees' created_by_id: 1 list_id: 1 + index: 15 created: 1543626724 updated: 1543626724 - id: 31 text: 'task #31 with color' created_by_id: 1 list_id: 1 + index: 16 hex_color: f0f0f0 created: 1543626724 updated: 1543626724 @@ -201,12 +232,14 @@ text: 'task #32' created_by_id: 1 list_id: 3 + index: 1 created: 1543626724 updated: 1543626724 - id: 33 text: 'task #33 with percent done' created_by_id: 1 list_id: 1 + index: 17 percent_done: 0.5 created: 1543626724 updated: 1543626724 diff --git a/pkg/models/list.go b/pkg/models/list.go index 7f52a6dc..53bde652 100644 --- a/pkg/models/list.go +++ b/pkg/models/list.go @@ -29,8 +29,11 @@ type List struct { Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(3|250)" minLength:"3" maxLength:"250"` // The description of the list. Description string `xorm:"longtext null" json:"description"` - OwnerID int64 `xorm:"int(11) INDEX not null" json:"-"` - NamespaceID int64 `xorm:"int(11) INDEX not null" json:"-" param:"namespace"` + // The unique list short identifier. Used to build task identifiers. + Identifier string `xorm:"varchar(10) null" json:"identifier" valid:"runelength(0|10)" minLength:"0" maxLength:"10"` + + OwnerID int64 `xorm:"int(11) INDEX not null" json:"-"` + NamespaceID int64 `xorm:"int(11) INDEX not null" json:"-" param:"namespace"` // The user who created this list. Owner *User `xorm:"-" json:"owner" valid:"-"` @@ -265,6 +268,17 @@ func CreateOrUpdateList(list *List) (err error) { } } + // Check if the identifier is unique and not empty + if list.Identifier != "" { + exists, err := x.Where("identifier = ?", list.Identifier).Exist(&List{}) + if err != nil { + return err + } + if exists { + return ErrListIdentifierIsNotUnique{Identifier: list.Identifier} + } + } + if list.ID == 0 { _, err = x.Insert(list) metrics.UpdateCount(1, metrics.ListCountKey) diff --git a/pkg/models/list_create_test.go b/pkg/models/list_create_test.go deleted file mode 100644 index 23026591..00000000 --- a/pkg/models/list_create_test.go +++ /dev/null @@ -1,97 +0,0 @@ -// Vikunja is a todo-list application to facilitate your life. -// Copyright 2018-2019 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package models - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestList_Create(t *testing.T) { - // Create test database - //assert.NoError(t, LoadFixtures()) - - // Get our doer - doer, err := GetUserByID(1) - assert.NoError(t, err) - - // Dummy list for testing - dummylist := List{ - Title: "test", - Description: "Lorem Ipsum", - NamespaceID: 1, - } - - // Check if the user can create - allowed, _ := dummylist.CanCreate(doer) - assert.True(t, allowed) - - // Create it - err = dummylist.Create(doer) - assert.NoError(t, err) - - // Get the list - newdummy := List{ID: dummylist.ID} - canRead, err := newdummy.CanRead(doer) - assert.NoError(t, err) - assert.True(t, canRead) - err = newdummy.ReadOne() - assert.NoError(t, err) - assert.Equal(t, dummylist.Title, newdummy.Title) - assert.Equal(t, dummylist.Description, newdummy.Description) - assert.Equal(t, dummylist.OwnerID, doer.ID) - - // Check if the user can see it - allowed, _ = dummylist.CanRead(doer) - assert.True(t, allowed) - - // Try updating a list - allowed, _ = dummylist.CanUpdate(doer) - assert.True(t, allowed) - dummylist.Description = "Lorem Ipsum dolor sit amet." - err = dummylist.Update() - assert.NoError(t, err) - - // Delete it - allowed, _ = dummylist.CanDelete(doer) - assert.True(t, allowed) - - err = dummylist.Delete() - assert.NoError(t, err) - - // Try updating a nonexistant list - err = dummylist.Update() - assert.Error(t, err) - assert.True(t, IsErrListDoesNotExist(err)) - - // Check creation with a nonexistant namespace - list3 := List{ - Title: "test", - Description: "Lorem Ipsum", - NamespaceID: 876694, - } - - err = list3.Create(doer) - assert.Error(t, err) - assert.True(t, IsErrNamespaceDoesNotExist(err)) - - // Try creating with a nonexistant owner - nUser := &User{ID: 9482385} - err = dummylist.Create(nUser) - assert.Error(t, err) - assert.True(t, IsErrUserDoesNotExist(err)) -} diff --git a/pkg/models/list_read_test.go b/pkg/models/list_read_test.go deleted file mode 100644 index 359a5268..00000000 --- a/pkg/models/list_read_test.go +++ /dev/null @@ -1,50 +0,0 @@ -// Vikunja is a todo-list application to facilitate your life. -// Copyright 2018-2019 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package models - -import ( - "github.com/stretchr/testify/assert" - "reflect" - "testing" -) - -func TestList_ReadAll(t *testing.T) { - // Create test database - //assert.NoError(t, LoadFixtures()) - - // Get all lists for our namespace - lists, err := GetListsByNamespaceID(1, &User{}) - assert.NoError(t, err) - assert.Equal(t, len(lists), 2) - - // Get all lists our user has access to - u, err := GetUserByID(1) - assert.NoError(t, err) - - lists2 := List{} - lists3, _, _, err := lists2.ReadAll(u, "", 1, 50) - - assert.NoError(t, err) - assert.Equal(t, reflect.TypeOf(lists3).Kind(), reflect.Slice) - s := reflect.ValueOf(lists3) - assert.Equal(t, 16, s.Len()) - - // Try getting lists for a nonexistant user - _, _, _, err = lists2.ReadAll(&User{ID: 984234}, "", 1, 50) - assert.Error(t, err) - assert.True(t, IsErrUserDoesNotExist(err)) -} diff --git a/pkg/models/list_test.go b/pkg/models/list_test.go new file mode 100644 index 00000000..41baa240 --- /dev/null +++ b/pkg/models/list_test.go @@ -0,0 +1,157 @@ +// Vikunja is a todo-list application to facilitate your life. +// Copyright 2018-2019 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "github.com/stretchr/testify/assert" + "reflect" + "testing" +) + +func TestList_CreateOrUpdate(t *testing.T) { + user := &User{ + ID: 1, + Username: "user1", + Email: "user1@example.com", + } + + t.Run("create", func(t *testing.T) { + t.Run("normal", func(t *testing.T) { + initFixtures(t) + list := List{ + Title: "test", + Description: "Lorem Ipsum", + NamespaceID: 1, + } + err := list.Create(user) + assert.NoError(t, err) + }) + t.Run("nonexistant namespace", func(t *testing.T) { + initFixtures(t) + list := List{ + Title: "test", + Description: "Lorem Ipsum", + NamespaceID: 999999, + } + + err := list.Create(user) + assert.Error(t, err) + assert.True(t, IsErrNamespaceDoesNotExist(err)) + }) + t.Run("nonexistant owner", func(t *testing.T) { + initFixtures(t) + user := &User{ID: 9482385} + list := List{ + Title: "test", + Description: "Lorem Ipsum", + NamespaceID: 1, + } + err := list.Create(user) + assert.Error(t, err) + assert.True(t, IsErrUserDoesNotExist(err)) + }) + t.Run("existing identifier", func(t *testing.T) { + initFixtures(t) + list := List{ + Title: "test", + Description: "Lorem Ipsum", + Identifier: "test1", + NamespaceID: 1, + } + + err := list.Create(user) + assert.Error(t, err) + assert.True(t, IsErrListIdentifierIsNotUnique(err)) + }) + }) + + t.Run("update", func(t *testing.T) { + t.Run("normal", func(t *testing.T) { + initFixtures(t) + list := List{ + ID: 1, + Title: "test", + Description: "Lorem Ipsum", + NamespaceID: 1, + } + list.Description = "Lorem Ipsum dolor sit amet." + err := list.Update() + assert.NoError(t, err) + + }) + t.Run("nonexistant", func(t *testing.T) { + initFixtures(t) + list := List{ + ID: 99999999, + Title: "test", + } + err := list.Update() + assert.Error(t, err) + assert.True(t, IsErrListDoesNotExist(err)) + + }) + t.Run("existing identifier", func(t *testing.T) { + initFixtures(t) + list := List{ + Title: "test", + Description: "Lorem Ipsum", + Identifier: "test1", + NamespaceID: 1, + } + + err := list.Create(user) + assert.Error(t, err) + assert.True(t, IsErrListIdentifierIsNotUnique(err)) + }) + }) +} + +func TestList_Delete(t *testing.T) { + initFixtures(t) + list := List{ + ID: 1, + } + err := list.Delete() + assert.NoError(t, err) +} + +func TestList_ReadAll(t *testing.T) { + t.Run("all in namespace", func(t *testing.T) { + initFixtures(t) + // Get all lists for our namespace + lists, err := GetListsByNamespaceID(1, &User{}) + assert.NoError(t, err) + assert.Equal(t, len(lists), 2) + }) + t.Run("all lists for user", func(t *testing.T) { + u := &User{ID: 1} + list := List{} + lists3, _, _, err := list.ReadAll(u, "", 1, 50) + + assert.NoError(t, err) + assert.Equal(t, reflect.TypeOf(lists3).Kind(), reflect.Slice) + s := reflect.ValueOf(lists3) + assert.Equal(t, 16, s.Len()) + }) + t.Run("lists for nonexistant user", func(t *testing.T) { + user := &User{ID: 999999} + list := List{} + _, _, _, err := list.ReadAll(user, "", 1, 50) + assert.Error(t, err) + assert.True(t, IsErrUserDoesNotExist(err)) + }) +} diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index dae3cfae..e3cd925c 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -55,6 +55,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { ID: 1, Text: "task #1", Description: "Lorem Ipsum", + Identifier: "test1-1", + Index: 1, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -73,6 +75,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { { ID: 29, Text: "task #29 with parent task (1)", + Index: 14, CreatedByID: 1, ListID: 1, Created: 1543626724, @@ -109,6 +112,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task2 := &Task{ ID: 2, Text: "task #2 done", + Identifier: "test1-2", + Index: 2, Done: true, CreatedByID: 1, CreatedBy: user1, @@ -130,6 +135,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task3 := &Task{ ID: 3, Text: "task #3 high prio", + Identifier: "test1-3", + Index: 3, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -141,6 +148,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task4 := &Task{ ID: 4, Text: "task #4 low prio", + Identifier: "test1-4", + Index: 4, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -152,6 +161,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task5 := &Task{ ID: 5, Text: "task #5 higher due date", + Identifier: "test1-5", + Index: 5, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -163,6 +174,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task6 := &Task{ ID: 6, Text: "task #6 lower due date", + Identifier: "test1-6", + Index: 6, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -174,6 +187,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task7 := &Task{ ID: 7, Text: "task #7 with start date", + Identifier: "test1-7", + Index: 7, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -185,6 +200,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task8 := &Task{ ID: 8, Text: "task #8 with end date", + Identifier: "test1-8", + Index: 8, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -196,6 +213,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task9 := &Task{ ID: 9, Text: "task #9 with start and end date", + Identifier: "test1-9", + Index: 9, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -208,6 +227,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task10 := &Task{ ID: 10, Text: "task #10 basic", + Identifier: "test1-10", + Index: 10, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -218,6 +239,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task11 := &Task{ ID: 11, Text: "task #11 basic", + Identifier: "test1-11", + Index: 11, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -228,6 +251,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task12 := &Task{ ID: 12, Text: "task #12 basic", + Identifier: "test1-12", + Index: 12, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -238,6 +263,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task15 := &Task{ ID: 15, Text: "task #15", + Identifier: "test6-1", + Index: 1, CreatedByID: 6, CreatedBy: user6, ListID: 6, @@ -248,6 +275,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task16 := &Task{ ID: 16, Text: "task #16", + Identifier: "test7-1", + Index: 1, CreatedByID: 6, CreatedBy: user6, ListID: 7, @@ -258,6 +287,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task17 := &Task{ ID: 17, Text: "task #17", + Identifier: "test8-1", + Index: 1, CreatedByID: 6, CreatedBy: user6, ListID: 8, @@ -268,6 +299,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task18 := &Task{ ID: 18, Text: "task #18", + Identifier: "test9-1", + Index: 1, CreatedByID: 6, CreatedBy: user6, ListID: 9, @@ -278,6 +311,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task19 := &Task{ ID: 19, Text: "task #19", + Identifier: "test10-1", + Index: 1, CreatedByID: 6, CreatedBy: user6, ListID: 10, @@ -288,6 +323,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task20 := &Task{ ID: 20, Text: "task #20", + Identifier: "test11-1", + Index: 1, CreatedByID: 6, CreatedBy: user6, ListID: 11, @@ -298,6 +335,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task21 := &Task{ ID: 21, Text: "task #21", + Identifier: "test12-1", + Index: 1, CreatedByID: 6, CreatedBy: user6, ListID: 12, @@ -308,6 +347,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task22 := &Task{ ID: 22, Text: "task #22", + Identifier: "test13-1", + Index: 1, CreatedByID: 6, CreatedBy: user6, ListID: 13, @@ -318,6 +359,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task23 := &Task{ ID: 23, Text: "task #23", + Identifier: "test14-1", + Index: 1, CreatedByID: 6, CreatedBy: user6, ListID: 14, @@ -328,6 +371,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task24 := &Task{ ID: 24, Text: "task #24", + Identifier: "test15-1", + Index: 1, CreatedByID: 6, CreatedBy: user6, ListID: 15, @@ -338,6 +383,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task25 := &Task{ ID: 25, Text: "task #25", + Identifier: "test16-1", + Index: 1, CreatedByID: 6, CreatedBy: user6, ListID: 16, @@ -348,6 +395,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task26 := &Task{ ID: 26, Text: "task #26", + Identifier: "test17-1", + Index: 1, CreatedByID: 6, CreatedBy: user6, ListID: 17, @@ -358,6 +407,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task27 := &Task{ ID: 27, Text: "task #27 with reminders", + Identifier: "test1-12", + Index: 12, CreatedByID: 1, CreatedBy: user1, RemindersUnix: []int64{1543626724, 1543626824}, @@ -369,6 +420,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task28 := &Task{ ID: 28, Text: "task #28 with repeat after", + Identifier: "test1-13", + Index: 13, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -380,6 +433,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task29 := &Task{ ID: 29, Text: "task #29 with parent task (1)", + Identifier: "test1-14", + Index: 14, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -389,6 +444,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { ID: 1, Text: "task #1", Description: "Lorem Ipsum", + Index: 1, CreatedByID: 1, ListID: 1, Created: 1543626724, @@ -402,6 +458,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task30 := &Task{ ID: 30, Text: "task #30 with assignees", + Identifier: "test1-15", + Index: 15, CreatedByID: 1, CreatedBy: user1, ListID: 1, @@ -416,6 +474,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task31 := &Task{ ID: 31, Text: "task #31 with color", + Identifier: "test1-16", + Index: 16, HexColor: "f0f0f0", CreatedByID: 1, CreatedBy: user1, @@ -427,6 +487,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task32 := &Task{ ID: 32, Text: "task #32", + Identifier: "test3-1", + Index: 1, CreatedByID: 1, CreatedBy: user1, ListID: 3, @@ -437,6 +499,8 @@ func TestTaskCollection_ReadAll(t *testing.T) { task33 := &Task{ ID: 33, Text: "task #33 with percent done", + Identifier: "test1-17", + Index: 17, CreatedByID: 1, CreatedBy: user1, ListID: 1, diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index a4c9d428..fddb844d 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -23,6 +23,7 @@ import ( "code.vikunja.io/web" "github.com/imdario/mergo" "sort" + "strconv" "time" ) @@ -62,6 +63,11 @@ type Task struct { // Determines how far a task is left from being done PercentDone float64 `xorm:"DOUBLE null" json:"percentDone"` + // The task identifier, based on the list identifier and the task's index + Identifier string `xorm:"-" json:"identifier"` + // The task index, calculated per list + Index int64 `xorm:"int(11) not null default 0" json:"index"` + // The UID is currently not used for anything other than caldav, which is why we don't expose it over json UID string `xorm:"varchar(250) null" json:"-"` @@ -230,19 +236,6 @@ func getTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resultCo return tasks, resultCount, totalItems, err } -// GetTasksByListID gets all todotasks for a list -func GetTasksByListID(listID int64) (tasks []*Task, err error) { - // make a map so we can put in a lot of other stuff more easily - taskMap := make(map[int64]*Task, len(tasks)) - err = x.Where("list_id = ?", listID).Find(&taskMap) - if err != nil { - return - } - - tasks, err = addMoreInfoToTasks(taskMap) - return -} - // GetTaskByIDSimple returns a raw task without extra data by the task ID func GetTaskByIDSimple(taskID int64) (task Task, err error) { if taskID < 1 { @@ -314,9 +307,11 @@ func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) { // Get all users & task ids and put them into the array var userIDs []int64 var taskIDs []int64 + var listIDs []int64 for _, i := range taskMap { taskIDs = append(taskIDs, i.ID) userIDs = append(userIDs, i.CreatedByID) + listIDs = append(listIDs, i.ListID) } // Get all assignees @@ -399,6 +394,13 @@ func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) { taskRemindersUnix[r.TaskID] = append(taskRemindersUnix[r.TaskID], r.ReminderUnix) } + // Get all identifiers + lists := make(map[int64]*List, len(listIDs)) + err = x.In("id", listIDs).Find(&lists) + if err != nil { + return + } + // Add all user objects to the appropriate tasks for _, task := range taskMap { @@ -410,6 +412,9 @@ func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) { // Prepare the subtasks task.RelatedTasks = make(RelatedTaskMap) + + // Build the task identifier from the list identifier and task index + task.Identifier = lists[task.ListID].Identifier + "-" + strconv.FormatInt(task.Index, 10) } // Get all related tasks @@ -785,7 +790,7 @@ func (t *Task) Delete() (err error) { // @Success 200 {object} models.Task "The task" // @Failure 404 {object} models.Message "Task not found" // @Failure 500 {object} models.Message "Internal error" -// @Router /tasks/all [get] +// @Router /tasks/{ID} [get] func (t *Task) ReadOne() (err error) { taskMap := make(map[int64]*Task, 1) diff --git a/pkg/models/unit_tests.go b/pkg/models/unit_tests.go index 2fcb5345..72c96f49 100644 --- a/pkg/models/unit_tests.go +++ b/pkg/models/unit_tests.go @@ -24,9 +24,11 @@ import ( "code.vikunja.io/api/pkg/mail" "fmt" "github.com/go-xorm/xorm" + "github.com/stretchr/testify/assert" "gopkg.in/testfixtures.v2" "os" "path/filepath" + "testing" ) // SetupTests takes care of seting up the db, fixtures etc. @@ -84,3 +86,9 @@ func createTestEngine(fixturesDir string) error { func initSchema(tx *xorm.Engine) error { return tx.Sync2(GetTables()...) } + +func initFixtures(t *testing.T) { + // Init db fixtures + err := db.LoadFixtures() + assert.NoError(t, err) +} diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index b20677d1..80e20255 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1,6 +1,6 @@ // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // This file was generated by swaggo/swag at -// 2019-12-07 20:29:10.551783293 +0100 CET m=+0.172017440 +// 2019-12-07 22:54:02.661375666 +0100 CET m=+0.164990732 package swagger @@ -2649,7 +2649,7 @@ var doc = `{ "JWTKeyAuth": [] } ], - "description": "Returns one task by its ID", + "description": "Returns all tasks on any list the user has access to.", "consumes": [ "application/json" ], @@ -2659,27 +2659,53 @@ var doc = `{ "tags": [ "task" ], - "summary": "Get one task", + "summary": "Get tasks", "parameters": [ { "type": "integer", - "description": "The task ID", - "name": "ID", - "in": "path", - "required": true + "description": "The page number. Used for pagination. If not provided, the first page of results is returned.", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page.", + "name": "per_page", + "in": "query" + }, + { + "type": "string", + "description": "Search tasks by task text.", + "name": "s", + "in": "query" + }, + { + "type": "string", + "description": "The sorting parameter. Possible values to sort by are priority, prioritydesc, priorityasc, duedate, duedatedesc, duedateasc.", + "name": "sort", + "in": "query" + }, + { + "type": "integer", + "description": "The start date parameter to filter by. Expects a unix timestamp. If no end date, but a start date is specified, the end date is set to the current time.", + "name": "startdate", + "in": "query" + }, + { + "type": "integer", + "description": "The end date parameter to filter by. Expects a unix timestamp. If no start date, but an end date is specified, the start date is set to the current time.", + "name": "enddate", + "in": "query" } ], "responses": { "200": { - "description": "The task", + "description": "The tasks", "schema": { - "$ref": "#/definitions/models.Task" - } - }, - "404": { - "description": "Task not found", - "schema": { - "$ref": "#/definitions/models.Message" + "type": "array", + "items": { + "$ref": "#/definitions/models.Task" + } } }, "500": { @@ -2749,6 +2775,55 @@ var doc = `{ } } }, + "/tasks/{ID}": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Returns one task by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "task" + ], + "summary": "Get one task", + "parameters": [ + { + "type": "integer", + "description": "The task ID", + "name": "ID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The task", + "schema": { + "$ref": "#/definitions/models.Task" + } + }, + "404": { + "description": "Task not found", + "schema": { + "$ref": "#/definitions/models.Message" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/tasks/{id}": { "post": { "security": [ @@ -4481,6 +4556,14 @@ var doc = `{ "description": "The unique, numeric id of this task.", "type": "integer" }, + "identifier": { + "description": "The task identifier, based on the list identifier and the task's index", + "type": "string" + }, + "index": { + "description": "The task index, calculated per list", + "type": "integer" + }, "labels": { "description": "An array of labels which are associated with this task.", "type": "array", @@ -4663,6 +4746,12 @@ var doc = `{ "description": "The unique, numeric id of this list.", "type": "integer" }, + "identifier": { + "description": "The unique list short identifier. Used to build task identifiers.", + "type": "string", + "maxLength": 10, + "minLength": 0 + }, "owner": { "description": "The user who created this list.", "type": "object", @@ -4894,6 +4983,14 @@ var doc = `{ "description": "The unique, numeric id of this task.", "type": "integer" }, + "identifier": { + "description": "The task identifier, based on the list identifier and the task's index", + "type": "string" + }, + "index": { + "description": "The task index, calculated per list", + "type": "integer" + }, "labels": { "description": "An array of labels which are associated with this task.", "type": "array", @@ -5002,6 +5099,14 @@ var doc = `{ "description": "The unique, numeric id of this task.", "type": "integer" }, + "identifier": { + "description": "The task identifier, based on the list identifier and the task's index", + "type": "string" + }, + "index": { + "description": "The task index, calculated per list", + "type": "integer" + }, "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 eb0777eb..88ca4f39 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -2631,7 +2631,7 @@ "JWTKeyAuth": [] } ], - "description": "Returns one task by its ID", + "description": "Returns all tasks on any list the user has access to.", "consumes": [ "application/json" ], @@ -2641,27 +2641,53 @@ "tags": [ "task" ], - "summary": "Get one task", + "summary": "Get tasks", "parameters": [ { "type": "integer", - "description": "The task ID", - "name": "ID", - "in": "path", - "required": true + "description": "The page number. Used for pagination. If not provided, the first page of results is returned.", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page.", + "name": "per_page", + "in": "query" + }, + { + "type": "string", + "description": "Search tasks by task text.", + "name": "s", + "in": "query" + }, + { + "type": "string", + "description": "The sorting parameter. Possible values to sort by are priority, prioritydesc, priorityasc, duedate, duedatedesc, duedateasc.", + "name": "sort", + "in": "query" + }, + { + "type": "integer", + "description": "The start date parameter to filter by. Expects a unix timestamp. If no end date, but a start date is specified, the end date is set to the current time.", + "name": "startdate", + "in": "query" + }, + { + "type": "integer", + "description": "The end date parameter to filter by. Expects a unix timestamp. If no start date, but an end date is specified, the start date is set to the current time.", + "name": "enddate", + "in": "query" } ], "responses": { "200": { - "description": "The task", + "description": "The tasks", "schema": { - "$ref": "#/definitions/models.Task" - } - }, - "404": { - "description": "Task not found", - "schema": { - "$ref": "#/definitions/models.Message" + "type": "array", + "items": { + "$ref": "#/definitions/models.Task" + } } }, "500": { @@ -2731,6 +2757,55 @@ } } }, + "/tasks/{ID}": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Returns one task by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "task" + ], + "summary": "Get one task", + "parameters": [ + { + "type": "integer", + "description": "The task ID", + "name": "ID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The task", + "schema": { + "$ref": "#/definitions/models.Task" + } + }, + "404": { + "description": "Task not found", + "schema": { + "$ref": "#/definitions/models.Message" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/tasks/{id}": { "post": { "security": [ @@ -4462,6 +4537,14 @@ "description": "The unique, numeric id of this task.", "type": "integer" }, + "identifier": { + "description": "The task identifier, based on the list identifier and the task's index", + "type": "string" + }, + "index": { + "description": "The task index, calculated per list", + "type": "integer" + }, "labels": { "description": "An array of labels which are associated with this task.", "type": "array", @@ -4644,6 +4727,12 @@ "description": "The unique, numeric id of this list.", "type": "integer" }, + "identifier": { + "description": "The unique list short identifier. Used to build task identifiers.", + "type": "string", + "maxLength": 10, + "minLength": 0 + }, "owner": { "description": "The user who created this list.", "type": "object", @@ -4875,6 +4964,14 @@ "description": "The unique, numeric id of this task.", "type": "integer" }, + "identifier": { + "description": "The task identifier, based on the list identifier and the task's index", + "type": "string" + }, + "index": { + "description": "The task index, calculated per list", + "type": "integer" + }, "labels": { "description": "An array of labels which are associated with this task.", "type": "array", @@ -4983,6 +5080,14 @@ "description": "The unique, numeric id of this task.", "type": "integer" }, + "identifier": { + "description": "The task identifier, based on the list identifier and the task's index", + "type": "string" + }, + "index": { + "description": "The task index, calculated per list", + "type": "integer" + }, "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 11581e50..abd41bfa 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -84,6 +84,13 @@ definitions: id: description: The unique, numeric id of this task. type: integer + identifier: + description: The task identifier, based on the list identifier and the task's + index + type: string + index: + description: The task index, calculated per list + type: integer labels: description: An array of labels which are associated with this task. items: @@ -232,6 +239,11 @@ definitions: id: description: The unique, numeric id of this list. type: integer + identifier: + description: The unique list short identifier. Used to build task identifiers. + maxLength: 10 + minLength: 0 + type: string owner: $ref: '#/definitions/models.User' description: The user who created this list. @@ -413,6 +425,13 @@ definitions: id: description: The unique, numeric id of this task. type: integer + identifier: + description: The task identifier, based on the list identifier and the + task's index + type: string + index: + description: The task index, calculated per list + type: integer labels: description: An array of labels which are associated with this task. items: @@ -500,6 +519,13 @@ definitions: id: description: The unique, numeric id of this task. type: integer + identifier: + description: The task identifier, based on the list identifier and the task's + index + type: string + index: + description: The task index, calculated per list + type: integer labels: description: An array of labels which are associated with this task. items: @@ -2587,6 +2613,37 @@ paths: summary: Get an auth token for a share tags: - sharing + /tasks/{ID}: + get: + consumes: + - application/json + description: Returns one task by its ID + parameters: + - description: The task ID + in: path + name: ID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: The task + schema: + $ref: '#/definitions/models.Task' + "404": + description: Task not found + schema: + $ref: '#/definitions/models.Message' + "500": + description: Internal error + schema: + $ref: '#/definitions/models.Message' + security: + - JWTKeyAuth: [] + summary: Get one task + tags: + - task /tasks/{id}: delete: description: Deletes a task from a list. This does not mean "mark it done". @@ -3241,31 +3298,55 @@ paths: get: consumes: - application/json - description: Returns one task by its ID + description: Returns all tasks on any list the user has access to. parameters: - - description: The task ID - in: path - name: ID - required: true + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. + in: query + name: page + type: integer + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. + in: query + name: per_page + type: integer + - description: Search tasks by task text. + in: query + name: s + type: string + - description: The sorting parameter. Possible values to sort by are priority, + prioritydesc, priorityasc, duedate, duedatedesc, duedateasc. + in: query + name: sort + type: string + - description: The start date parameter to filter by. Expects a unix timestamp. + If no end date, but a start date is specified, the end date is set to the + current time. + in: query + name: startdate + type: integer + - description: The end date parameter to filter by. Expects a unix timestamp. + If no start date, but an end date is specified, the start date is set to + the current time. + in: query + name: enddate type: integer produces: - application/json responses: "200": - description: The task + description: The tasks schema: - $ref: '#/definitions/models.Task' - "404": - description: Task not found - schema: - $ref: '#/definitions/models.Message' + items: + $ref: '#/definitions/models.Task' + type: array "500": description: Internal error schema: $ref: '#/definitions/models.Message' security: - JWTKeyAuth: [] - summary: Get one task + summary: Get tasks tags: - task /tasks/bulk: