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: