diff --git a/docs/content/doc/usage/errors.md b/docs/content/doc/usage/errors.md
index 1077a114..93fd2340 100644
--- a/docs/content/doc/usage/errors.md
+++ b/docs/content/doc/usage/errors.md
@@ -73,4 +73,6 @@ This document describes the different errors Vikunja can return.
| 8001 | 403 | This label already exists on that task. |
| 8002 | 404 | The label does not exist. |
| 8003 | 403 | The user does not have access to this label. |
-| 9001 | 403 | The right is invalid. |
\ No newline at end of file
+| 9001 | 403 | The right is invalid. |
+| 10001 | 404 | The bucket does not exist. |
+| 10002 | 400 | The bucket does not belong to that list. |
diff --git a/pkg/db/db.go b/pkg/db/db.go
index 4e461505..713ace74 100644
--- a/pkg/db/db.go
+++ b/pkg/db/db.go
@@ -70,7 +70,7 @@ func CreateDBEngine() (engine *xorm.Engine, err error) {
}
engine.SetMapper(core.GonicMapper{})
- logger := log.NewXormLogger()
+ logger := log.NewXormLogger("")
engine.SetLogger(logger)
// Cache
diff --git a/pkg/db/fixtures/buckets.yml b/pkg/db/fixtures/buckets.yml
new file mode 100644
index 00000000..d7d23b72
--- /dev/null
+++ b/pkg/db/fixtures/buckets.yml
@@ -0,0 +1,103 @@
+- id: 1
+ title: testbucket1
+ list_id: 1
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 2
+ title: testbucket2
+ list_id: 1
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 3
+ title: testbucket3
+ list_id: 1
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 4
+ title: testbucket4 - other list
+ list_id: 2
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+# The following are not or only partly owned by user 1
+- id: 5
+ title: testbucket5
+ list_id: 20
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 6
+ title: testbucket6
+ list_id: 6
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 7
+ title: testbucket7
+ list_id: 7
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 8
+ title: testbucket8
+ list_id: 8
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 9
+ title: testbucket9
+ list_id: 9
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 10
+ title: testbucket10
+ list_id: 10
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 11
+ title: testbucket11
+ list_id: 11
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 12
+ title: testbucket13
+ list_id: 12
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 13
+ title: testbucket13
+ list_id: 13
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 14
+ title: testbucket14
+ list_id: 14
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 15
+ title: testbucket15
+ list_id: 15
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 16
+ title: testbucket16
+ list_id: 16
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
+- id: 17
+ title: testbucket17
+ list_id: 17
+ created_by_id: 1
+ created: 1587244432
+ updated: 1587244432
diff --git a/pkg/db/fixtures/tasks.yml b/pkg/db/fixtures/tasks.yml
index 72eb3018..7ee8f7c4 100644
--- a/pkg/db/fixtures/tasks.yml
+++ b/pkg/db/fixtures/tasks.yml
@@ -7,6 +7,7 @@
index: 1
created: 1543626724
updated: 1543626724
+ bucket_id: 1
- id: 2
text: 'task #2 done'
done: true
@@ -15,6 +16,7 @@
index: 2
created: 1543626724
updated: 1543626724
+ bucket_id: 1
- id: 3
text: 'task #3 high prio'
done: false
@@ -24,6 +26,7 @@
created: 1543626724
updated: 1543626724
priority: 100
+ bucket_id: 2
- id: 4
text: 'task #4 low prio'
done: false
@@ -33,6 +36,7 @@
created: 1543626724
updated: 1543626724
priority: 1
+ bucket_id: 2
- id: 5
text: 'task #5 higher due date'
done: false
@@ -42,6 +46,7 @@
created: 1543626724
updated: 1543626724
due_date_unix: 1543636724
+ bucket_id: 2
- id: 6
text: 'task #6 lower due date'
done: false
@@ -51,6 +56,7 @@
created: 1543626724
updated: 1543626724
due_date_unix: 1543616724
+ bucket_id: 3
- id: 7
text: 'task #7 with start date'
done: false
@@ -60,6 +66,7 @@
created: 1543626724
updated: 1543626724
start_date_unix: 1544600000
+ bucket_id: 3
- id: 8
text: 'task #8 with end date'
done: false
@@ -69,6 +76,7 @@
created: 1543626724
updated: 1543626724
end_date_unix: 1544700000
+ bucket_id: 3
- id: 9
text: 'task #9 with start and end date'
done: false
diff --git a/pkg/db/test.go b/pkg/db/test.go
index 8752dd47..f6274e36 100644
--- a/pkg/db/test.go
+++ b/pkg/db/test.go
@@ -46,7 +46,7 @@ func CreateTestEngine() (engine *xorm.Engine, err error) {
}
engine.SetMapper(core.GonicMapper{})
- logger := log.NewXormLogger()
+ logger := log.NewXormLogger("DEBUG")
logger.ShowSQL(os.Getenv("UNIT_TESTS_VERBOSE") == "1")
engine.SetLogger(logger)
x = engine
diff --git a/pkg/integrations/kanban_test.go b/pkg/integrations/kanban_test.go
new file mode 100644
index 00000000..5d30e13e
--- /dev/null
+++ b/pkg/integrations/kanban_test.go
@@ -0,0 +1,300 @@
+// Vikunja is a to-do list application to facilitate your life.
+// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package integrations
+
+import (
+ "code.vikunja.io/api/pkg/models"
+ "code.vikunja.io/web/handler"
+ "github.com/labstack/echo/v4"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestBucket(t *testing.T) {
+ testHandler := webHandlerTest{
+ user: &testuser1,
+ strFunc: func() handler.CObject {
+ return &models.Bucket{}
+ },
+ t: t,
+ }
+ t.Run("ReadAll", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ rec, err := testHandler.testReadAllWithUser(nil, map[string]string{"list": "1"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `testbucket1`)
+ assert.Contains(t, rec.Body.String(), `testbucket2`)
+ assert.Contains(t, rec.Body.String(), `testbucket3`)
+ assert.NotContains(t, rec.Body.String(), `testbucket4`) // Different List
+ })
+ })
+ t.Run("Update", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ // Check the list was loaded successfully afterwards, see testReadOneWithUser
+ rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "1"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+ t.Run("Nonexisting Bucket", func(t *testing.T) {
+ _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "9999"}, `{"title":"TestLoremIpsum"}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
+ })
+ t.Run("Empty title", func(t *testing.T) {
+ _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "1"}, `{"title":""}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required")
+ })
+ t.Run("Rights check", func(t *testing.T) {
+ t.Run("Forbidden", func(t *testing.T) {
+ // Owned by user13
+ _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "5"}, `{"title":"TestLoremIpsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "6"}, `{"title":"TestLoremIpsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team write", func(t *testing.T) {
+ rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "7"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+ t.Run("Shared Via Team admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "8"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+
+ t.Run("Shared Via User readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "9"}, `{"title":"TestLoremIpsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via User write", func(t *testing.T) {
+ rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "10"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+ t.Run("Shared Via User admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "11"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "12"}, `{"title":"TestLoremIpsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "13"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "14"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "15"}, `{"title":"TestLoremIpsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "16"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "17"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+ })
+ })
+ t.Run("Delete", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "1"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+ t.Run("Nonexisting", func(t *testing.T) {
+ _, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "999"})
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
+ })
+ t.Run("Rights check", func(t *testing.T) {
+ t.Run("Forbidden", func(t *testing.T) {
+ // Owned by user13
+ _, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "5"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team readonly", func(t *testing.T) {
+ _, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "6"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team write", func(t *testing.T) {
+ rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "7"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+ t.Run("Shared Via Team admin", func(t *testing.T) {
+ rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "8"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+
+ t.Run("Shared Via User readonly", func(t *testing.T) {
+ _, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "9"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via User write", func(t *testing.T) {
+ rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "10"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+ t.Run("Shared Via User admin", func(t *testing.T) {
+ rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "11"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ _, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "12"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "13"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "14"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ _, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "15"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "16"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "17"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+ })
+ })
+ t.Run("Create", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
+ })
+ t.Run("Nonexisting", func(t *testing.T) {
+ _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9999"}, `{"title":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
+ })
+ t.Run("Rights check", func(t *testing.T) {
+ t.Run("Forbidden", func(t *testing.T) {
+ // Owned by user13
+ _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"title":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team readonly", func(t *testing.T) {
+ _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "6"}, `{"title":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team write", func(t *testing.T) {
+ rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "7"}, `{"title":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
+ })
+ t.Run("Shared Via Team admin", func(t *testing.T) {
+ rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "8"}, `{"title":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
+ })
+
+ t.Run("Shared Via User readonly", func(t *testing.T) {
+ _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"title":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via User write", func(t *testing.T) {
+ rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"title":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
+ })
+ t.Run("Shared Via User admin", func(t *testing.T) {
+ rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"title":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "12"}, `{"title":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "13"}, `{"title":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "14"}, `{"title":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "15"}, `{"title":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "16"}, `{"title":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "17"}, `{"title":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
+ })
+ })
+ })
+}
diff --git a/pkg/integrations/task_collection_test.go b/pkg/integrations/task_collection_test.go
index 05bb7c07..fcd04ce2 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,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
+ assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
})
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,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`)
+ assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`)
})
t.Run("by priority asc", func(t *testing.T) {
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,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
+ assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
})
// should equal duedate asc
t.Run("by due_date", 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,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
+ assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
})
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,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`)
+ assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`)
})
t.Run("by duedate asc", func(t *testing.T) {
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,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
+ assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
})
t.Run("invalid sort parameter", func(t *testing.T) {
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
@@ -270,33 +270,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,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
+ assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
})
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,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`)
+ assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`)
})
t.Run("by priority asc", func(t *testing.T) {
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,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
+ assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
})
// should equal duedate asc
t.Run("by due_date", 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,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
+ assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
})
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,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`)
+ assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`)
})
t.Run("by duedate asc", func(t *testing.T) {
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,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
+ assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
})
t.Run("invalid parameter", func(t *testing.T) {
// Invalid parameter should not sort at all
diff --git a/pkg/integrations/task_test.go b/pkg/integrations/task_test.go
index c5cc1078..a428a7bf 100644
--- a/pkg/integrations/task_test.go
+++ b/pkg/integrations/task_test.go
@@ -287,6 +287,24 @@ func TestTask(t *testing.T) {
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
})
})
+ t.Run("Bucket", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"bucket_id":2}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"bucket_id":2`)
+ assert.NotContains(t, rec.Body.String(), `"bucket_id":1`)
+ })
+ t.Run("Different List", func(t *testing.T) {
+ _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"bucket_id":4}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToList)
+ })
+ t.Run("Nonexisting Bucket", func(t *testing.T) {
+ _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"bucket_id":9999}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
+ })
+ })
})
t.Run("Delete", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
@@ -452,5 +470,23 @@ func TestTask(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
})
})
+ t.Run("Bucket", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum","bucket_id":2}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"bucket_id":2`)
+ assert.NotContains(t, rec.Body.String(), `"bucket_id":1`)
+ })
+ t.Run("Different List", func(t *testing.T) {
+ _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum","bucket_id":4}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToList)
+ })
+ t.Run("Nonexisting Bucket", func(t *testing.T) {
+ _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum","bucket_id":9999}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
+ })
+ })
})
}
diff --git a/pkg/log/xorm_logger.go b/pkg/log/xorm_logger.go
index a3f3dcb4..f948118d 100644
--- a/pkg/log/xorm_logger.go
+++ b/pkg/log/xorm_logger.go
@@ -37,8 +37,11 @@ type XormLogger struct {
}
// NewXormLogger creates and initializes a new xorm logger
-func NewXormLogger() *XormLogger {
- level, err := logging.LogLevel(strings.ToUpper(config.LogDatabaseLevel.GetString()))
+func NewXormLogger(lvl string) *XormLogger {
+ if lvl == "" {
+ lvl = strings.ToUpper(config.LogDatabaseLevel.GetString())
+ }
+ level, err := logging.LogLevel(lvl)
if err != nil {
Critical("Error setting database log level: %s", err.Error())
}
diff --git a/pkg/migration/20200418230432.go b/pkg/migration/20200418230432.go
new file mode 100644
index 00000000..213379a4
--- /dev/null
+++ b/pkg/migration/20200418230432.go
@@ -0,0 +1,43 @@
+// Vikunja is a to-do list application to facilitate your life.
+// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package migration
+
+import (
+ "src.techknowlogick.com/xormigrate"
+ "xorm.io/xorm"
+)
+
+type task20200418230432 struct {
+ BucketID int64 `xorm:"int(11) null"`
+}
+
+func (s task20200418230432) TableName() string {
+ return "tasks"
+}
+
+func init() {
+ migrations = append(migrations, &xormigrate.Migration{
+ ID: "20200418230432",
+ Description: "Add bucket id property to task",
+ Migrate: func(tx *xorm.Engine) error {
+ return tx.Sync2(task20200418230432{})
+ },
+ Rollback: func(tx *xorm.Engine) error {
+ return tx.DropTables(task20200418230432{})
+ },
+ })
+}
diff --git a/pkg/migration/20200418230605.go b/pkg/migration/20200418230605.go
new file mode 100644
index 00000000..5cdf1a7d
--- /dev/null
+++ b/pkg/migration/20200418230605.go
@@ -0,0 +1,49 @@
+// Vikunja is a to-do list application to facilitate your life.
+// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package migration
+
+import (
+ "code.vikunja.io/api/pkg/timeutil"
+ "src.techknowlogick.com/xormigrate"
+ "xorm.io/xorm"
+)
+
+type bucket20200418230605 struct {
+ ID int64 `xorm:"int(11) autoincr not null unique pk"`
+ Title string `xorm:"text not null"`
+ ListID int64 `xorm:"int(11) not null"`
+ Created timeutil.TimeStamp `xorm:"created not null"`
+ Updated timeutil.TimeStamp `xorm:"updated not null"`
+ CreatedByID int64 `xorm:"int(11) not null"`
+}
+
+func (b *bucket20200418230605) TableName() string {
+ return "buckets"
+}
+
+func init() {
+ migrations = append(migrations, &xormigrate.Migration{
+ ID: "20200418230605",
+ Description: "Add bucket table",
+ Migrate: func(tx *xorm.Engine) error {
+ return tx.Sync2(bucket20200418230605{})
+ },
+ Rollback: func(tx *xorm.Engine) error {
+ return tx.DropTables(bucket20200418230605{})
+ },
+ })
+}
diff --git a/pkg/models/error.go b/pkg/models/error.go
index 9ada2ba1..3b266af9 100644
--- a/pkg/models/error.go
+++ b/pkg/models/error.go
@@ -1178,7 +1178,7 @@ func IsErrInvalidRight(err error) bool {
}
func (err ErrInvalidRight) Error() string {
- return fmt.Sprintf(" right invalid [Right: %d]", err.Right)
+ return fmt.Sprintf("Right invalid [Right: %d]", err.Right)
}
// ErrCodeInvalidRight holds the unique world-error code of this error
@@ -1192,3 +1192,62 @@ func (err ErrInvalidRight) HTTPError() web.HTTPError {
Message: "The right is invalid.",
}
}
+
+// ========
+// Kanban
+// ========
+
+// ErrBucketDoesNotExist represents an error where a kanban bucket does not exist
+type ErrBucketDoesNotExist struct {
+ BucketID int64
+}
+
+// IsErrBucketDoesNotExist checks if an error is ErrBucketDoesNotExist.
+func IsErrBucketDoesNotExist(err error) bool {
+ _, ok := err.(ErrBucketDoesNotExist)
+ return ok
+}
+
+func (err ErrBucketDoesNotExist) Error() string {
+ return fmt.Sprintf("Bucket does not exist [BucketID: %d]", err.BucketID)
+}
+
+// ErrCodeBucketDoesNotExist holds the unique world-error code of this error
+const ErrCodeBucketDoesNotExist = 10001
+
+// HTTPError holds the http error description
+func (err ErrBucketDoesNotExist) HTTPError() web.HTTPError {
+ return web.HTTPError{
+ HTTPCode: http.StatusNotFound,
+ Code: ErrCodeBucketDoesNotExist,
+ Message: "This bucket does not exist.",
+ }
+}
+
+// ErrBucketDoesNotBelongToList represents an error where a kanban bucket does not belong to a list
+type ErrBucketDoesNotBelongToList struct {
+ BucketID int64
+ ListID int64
+}
+
+// IsErrBucketDoesNotBelongToList checks if an error is ErrBucketDoesNotBelongToList.
+func IsErrBucketDoesNotBelongToList(err error) bool {
+ _, ok := err.(ErrBucketDoesNotBelongToList)
+ return ok
+}
+
+func (err ErrBucketDoesNotBelongToList) Error() string {
+ return fmt.Sprintf("Bucket does not not belong to list [BucketID: %d, ListID: %d]", err.BucketID, err.ListID)
+}
+
+// ErrCodeBucketDoesNotBelongToList holds the unique world-error code of this error
+const ErrCodeBucketDoesNotBelongToList = 10002
+
+// HTTPError holds the http error description
+func (err ErrBucketDoesNotBelongToList) HTTPError() web.HTTPError {
+ return web.HTTPError{
+ HTTPCode: http.StatusBadRequest,
+ Code: ErrCodeBucketDoesNotBelongToList,
+ Message: "This bucket does not belong to that list.",
+ }
+}
diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go
new file mode 100644
index 00000000..40a4db2d
--- /dev/null
+++ b/pkg/models/kanban.go
@@ -0,0 +1,205 @@
+// Vikunja is a to-do list application to facilitate your life.
+// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package models
+
+import (
+ "code.vikunja.io/api/pkg/timeutil"
+ "code.vikunja.io/api/pkg/user"
+ "code.vikunja.io/web"
+ "time"
+)
+
+// Bucket represents a kanban bucket
+type Bucket struct {
+ // The unique, numeric id of this bucket.
+ ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"bucket"`
+ // The title of this bucket.
+ Title string `xorm:"text not null" valid:"required" minLength:"1" json:"title"`
+ // The list this bucket belongs to.
+ ListID int64 `xorm:"int(11) not null" json:"list_id" param:"list"`
+ // All tasks which belong to this bucket.
+ Tasks []*Task `xorm:"-" json:"tasks"`
+
+ // A timestamp when this bucket was created. You cannot change this value.
+ Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
+ // A timestamp when this bucket was last updated. You cannot change this value.
+ Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"`
+
+ // The user who initially created the bucket.
+ CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"`
+ CreatedByID int64 `xorm:"int(11) not null" json:"-"`
+
+ web.Rights `xorm:"-" json:"-"`
+ web.CRUDable `xorm:"-" json:"-"`
+}
+
+// TableName returns the table name for this bucket.
+func (b *Bucket) TableName() string {
+ return "buckets"
+}
+
+func getBucketByID(id int64) (b *Bucket, err error) {
+ b = &Bucket{}
+ exists, err := x.Where("id = ?", id).Get(b)
+ if err != nil {
+ return
+ }
+ if !exists {
+ return b, ErrBucketDoesNotExist{BucketID: id}
+ }
+ return
+}
+
+// ReadAll returns all buckets with their tasks for a certain list
+// @Summary Get all kanban buckets of a list
+// @Description Returns all kanban buckets with belong to a list including their tasks.
+// @tags task
+// @Accept json
+// @Produce json
+// @Security JWTKeyAuth
+// @Param id path int true "List Id"
+// @Success 200 {array} models.Bucket "The buckets with their tasks"
+// @Failure 500 {object} models.Message "Internal server error"
+// @Router /lists/{id}/buckets [get]
+func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
+
+ // Note: I'm ignoring pagination for now since I've yet to figure out a way on how to make it work
+ // I'll probably just don't do it and instead make individual tasks archivable.
+
+ // Get all buckets for this list
+ buckets := []*Bucket{
+ {
+ // This is the default bucket for all tasks which are not associated to a bucket.
+ ID: 0,
+ Title: "Not associated to a bucket",
+ ListID: b.ListID,
+ Created: timeutil.FromTime(time.Now()),
+ Updated: timeutil.FromTime(time.Now()),
+ CreatedByID: auth.GetID(),
+ },
+ }
+
+ buckets[0].CreatedBy, err = user.GetFromAuth(auth)
+ if err != nil {
+ return
+ }
+
+ err = x.Where("list_id = ?", b.ListID).Find(&buckets)
+ if err != nil {
+ return
+ }
+
+ // Make a map from the bucket slice with their id as key so that we can use it to put the tasks in their buckets
+ bucketMap := make(map[int64]*Bucket, len(buckets))
+ userIDs := make([]int64, 0, len(buckets))
+ for _, bb := range buckets {
+ bucketMap[bb.ID] = bb
+ userIDs = append(userIDs, bb.CreatedByID)
+ }
+
+ // Get all users
+ users := make(map[int64]*user.User)
+ err = x.In("id", userIDs).Find(&users)
+ if err != nil {
+ return
+ }
+
+ for _, bb := range buckets {
+ bb.CreatedBy = users[bb.CreatedByID]
+ }
+
+ // Get all tasks for this list
+ tasks, _, _, err := getTasksForLists([]*List{{ID: b.ListID}}, &taskOptions{})
+ if err != nil {
+ return
+ }
+
+ // Put all tasks in their buckets
+ // All tasks which are not associated to any bucket will have bucket id 0 which is the nil value for int64
+ // Since we created a bucked with that id at the beginning, all tasks should be in there.
+ for _, task := range tasks {
+ bucketMap[task.BucketID].Tasks = append(bucketMap[task.BucketID].Tasks, task)
+ }
+
+ return buckets, len(buckets), int64(len(buckets)), nil
+}
+
+// Create creates a new bucket
+// @Summary Create a new bucket
+// @Description Creates a new kanban bucket on a list.
+// @tags task
+// @Accept json
+// @Produce json
+// @Security JWTKeyAuth
+// @Param id path int true "List Id"
+// @Param bucket body models.Bucket true "The bucket object"
+// @Success 200 {object} models.Bucket "The created bucket object."
+// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid bucket object provided."
+// @Failure 404 {object} code.vikunja.io/web.HTTPError "The list does not exist."
+// @Failure 500 {object} models.Message "Internal error"
+// @Router /lists/{id}/buckets [put]
+func (b *Bucket) Create(a web.Auth) (err error) {
+ b.CreatedByID = a.GetID()
+
+ _, err = x.Insert(b)
+ return
+}
+
+// Update Updates an existing bucket
+// @Summary Update an existing bucket
+// @Description Updates an existing kanban bucket.
+// @tags task
+// @Accept json
+// @Produce json
+// @Security JWTKeyAuth
+// @Param listID path int true "List Id"
+// @Param bucketID path int true "Bucket Id"
+// @Param bucket body models.Bucket true "The bucket object"
+// @Success 200 {object} models.Bucket "The created bucket object."
+// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid bucket object provided."
+// @Failure 404 {object} code.vikunja.io/web.HTTPError "The bucket does not exist."
+// @Failure 500 {object} models.Message "Internal error"
+// @Router /lists/{listID}/buckets/{bucketID} [post]
+func (b *Bucket) Update() (err error) {
+ _, err = x.Where("id = ?", b.ID).Update(b)
+ return
+}
+
+// Delete removes a bucket, but no tasks
+// @Summary Deletes an existing bucket
+// @Description Deletes an existing kanban bucket and dissociates all of its task. It does not delete any tasks.
+// @tags task
+// @Accept json
+// @Produce json
+// @Security JWTKeyAuth
+// @Param listID path int true "List Id"
+// @Param bucketID path int true "Bucket Id"
+// @Success 200 {object} models.Message "Successfully deleted."
+// @Failure 404 {object} code.vikunja.io/web.HTTPError "The bucket does not exist."
+// @Failure 500 {object} models.Message "Internal error"
+// @Router /lists/{listID}/buckets/{bucketID} [delete]
+func (b *Bucket) Delete() (err error) {
+ // Remove all associations of tasks to that bucket
+ _, err = x.Where("bucket_id = ?", b.ID).Cols("bucket_id").Update(&Task{BucketID: 0})
+ if err != nil {
+ return
+ }
+
+ // Remove the bucket itself
+ _, err = x.Where("id = ?", b.ID).Delete(&Bucket{})
+ return
+}
diff --git a/pkg/models/kanban_rights.go b/pkg/models/kanban_rights.go
new file mode 100644
index 00000000..cdf38681
--- /dev/null
+++ b/pkg/models/kanban_rights.go
@@ -0,0 +1,45 @@
+// Vikunja is a to-do list application to facilitate your life.
+// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package models
+
+import "code.vikunja.io/web"
+
+// CanCreate checks if a user can create a new bucket
+func (b *Bucket) CanCreate(a web.Auth) (bool, error) {
+ l := &List{ID: b.ListID}
+ return l.CanWrite(a)
+}
+
+// CanUpdate checks if a user can update an existing bucket
+func (b *Bucket) CanUpdate(a web.Auth) (bool, error) {
+ return b.canDoBucket(a)
+}
+
+// CanDelete checks if a user can delete an existing bucket
+func (b *Bucket) CanDelete(a web.Auth) (bool, error) {
+ return b.canDoBucket(a)
+}
+
+// canDoBucket checks if the bucket exists and if the user has the right to act on it
+func (b *Bucket) canDoBucket(a web.Auth) (bool, error) {
+ bb, err := getBucketByID(b.ID)
+ if err != nil {
+ return false, err
+ }
+ l := &List{ID: bb.ListID}
+ return l.CanWrite(a)
+}
diff --git a/pkg/models/kanban_test.go b/pkg/models/kanban_test.go
new file mode 100644
index 00000000..52b77241
--- /dev/null
+++ b/pkg/models/kanban_test.go
@@ -0,0 +1,67 @@
+// Vikunja is a to-do list application to facilitate your life.
+// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package models
+
+import (
+ "code.vikunja.io/api/pkg/db"
+ "code.vikunja.io/api/pkg/user"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestBucket_ReadAll(t *testing.T) {
+ db.LoadAndAssertFixtures(t)
+
+ testuser := &user.User{ID: 1}
+ b := &Bucket{ListID: 1}
+ bucketsInterface, _, _, err := b.ReadAll(testuser, "", 0, 0)
+ assert.NoError(t, err)
+
+ buckets, is := bucketsInterface.([]*Bucket)
+ assert.True(t, is)
+
+ // Assert that we have a user for each bucket
+ assert.Equal(t, testuser.ID, buckets[0].CreatedBy.ID)
+ assert.Equal(t, testuser.ID, buckets[1].CreatedBy.ID)
+ assert.Equal(t, testuser.ID, buckets[2].CreatedBy.ID)
+ assert.Equal(t, testuser.ID, buckets[3].CreatedBy.ID)
+
+ // Assert our three test buckets + one for all tasks without a bucket
+ assert.Len(t, buckets, 4)
+
+ // Assert all tasks are in the right bucket
+ assert.Len(t, buckets[0].Tasks, 10)
+ assert.Len(t, buckets[1].Tasks, 2)
+ assert.Len(t, buckets[2].Tasks, 3)
+ assert.Len(t, buckets[3].Tasks, 3)
+
+ // Assert we have bucket 0, 1, 2, 3 but not 4 (that belongs to a different list)
+ assert.Equal(t, int64(1), buckets[1].ID)
+ assert.Equal(t, int64(2), buckets[2].ID)
+ assert.Equal(t, int64(3), buckets[3].ID)
+
+ // Kinda assert all tasks are in the right buckets
+ assert.Equal(t, int64(0), buckets[0].Tasks[0].BucketID)
+ assert.Equal(t, int64(1), buckets[1].Tasks[0].BucketID)
+ assert.Equal(t, int64(1), buckets[1].Tasks[1].BucketID)
+ assert.Equal(t, int64(2), buckets[2].Tasks[0].BucketID)
+ assert.Equal(t, int64(2), buckets[2].Tasks[1].BucketID)
+ assert.Equal(t, int64(2), buckets[2].Tasks[2].BucketID)
+ assert.Equal(t, int64(3), buckets[3].Tasks[0].BucketID)
+ assert.Equal(t, int64(3), buckets[3].Tasks[1].BucketID)
+ assert.Equal(t, int64(3), buckets[3].Tasks[2].BucketID)
+}
diff --git a/pkg/models/models.go b/pkg/models/models.go
index 5ad208bf..873aa6c5 100644
--- a/pkg/models/models.go
+++ b/pkg/models/models.go
@@ -51,6 +51,7 @@ func GetTables() []interface{} {
&TaskRelation{},
&TaskAttachment{},
&TaskComment{},
+ &Bucket{},
}
}
diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go
index fddc7560..25abfac3 100644
--- a/pkg/models/task_collection_test.go
+++ b/pkg/models/task_collection_test.go
@@ -56,6 +56,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
CreatedByID: 1,
CreatedBy: user1,
ListID: 1,
+ BucketID: 1,
Labels: []*Label{
{
ID: 4,
@@ -114,6 +115,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
CreatedByID: 1,
CreatedBy: user1,
ListID: 1,
+ BucketID: 1,
Labels: []*Label{
{
ID: 4,
@@ -140,6 +142,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Created: 1543626724,
Updated: 1543626724,
Priority: 100,
+ BucketID: 2,
}
task4 := &Task{
ID: 4,
@@ -153,6 +156,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Created: 1543626724,
Updated: 1543626724,
Priority: 1,
+ BucketID: 2,
}
task5 := &Task{
ID: 5,
@@ -166,6 +170,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Created: 1543626724,
Updated: 1543626724,
DueDate: 1543636724,
+ BucketID: 2,
}
task6 := &Task{
ID: 6,
@@ -179,6 +184,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Created: 1543626724,
Updated: 1543626724,
DueDate: 1543616724,
+ BucketID: 3,
}
task7 := &Task{
ID: 7,
@@ -192,6 +198,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Created: 1543626724,
Updated: 1543626724,
StartDate: 1544600000,
+ BucketID: 3,
}
task8 := &Task{
ID: 8,
@@ -205,6 +212,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Created: 1543626724,
Updated: 1543626724,
EndDate: 1544700000,
+ BucketID: 3,
}
task9 := &Task{
ID: 9,
@@ -445,6 +453,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
ListID: 1,
Created: 1543626724,
Updated: 1543626724,
+ BucketID: 1,
},
},
},
diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go
index af76bd1f..9740fd09 100644
--- a/pkg/models/tasks.go
+++ b/pkg/models/tasks.go
@@ -85,6 +85,9 @@ type Task struct {
// A timestamp when this task was last updated. You cannot change this value.
Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"`
+ // BucketID is the ID of the kanban bucket this task belongs to.
+ BucketID int64 `xorm:"int(11) null" json:"bucket_id"`
+
// The user who initially created the task.
CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"`
@@ -459,6 +462,22 @@ func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) {
return
}
+func checkBucketAndTaskBelongToSameList(fullTask *Task, bucketID int64) (err error) {
+ if bucketID != 0 {
+ b, err := getBucketByID(bucketID)
+ if err != nil {
+ return err
+ }
+ if fullTask.ListID != b.ListID {
+ return ErrBucketDoesNotBelongToList{
+ ListID: fullTask.ListID,
+ BucketID: fullTask.BucketID,
+ }
+ }
+ }
+ return
+}
+
// Create is the implementation to create a list task
// @Summary Create a task
// @Description Inserts a task into a list.
@@ -498,6 +517,12 @@ func (t *Task) Create(a web.Auth) (err error) {
t.UID = utils.MakeRandomString(40)
}
+ // If there is a bucket set, make sure they belong to the same list as the task
+ err = checkBucketAndTaskBelongToSameList(t, t.BucketID)
+ if err != nil {
+ return
+ }
+
// Get the index for this task
latestTask := &Task{}
_, err = x.Where("list_id = ?", t.ListID).OrderBy("id desc").Get(latestTask)
@@ -573,6 +598,12 @@ func (t *Task) Update() (err error) {
return err
}
+ // If there is a bucket set, make sure they belong to the same list as the task
+ err = checkBucketAndTaskBelongToSameList(&ot, t.BucketID)
+ if err != nil {
+ return
+ }
+
// Update the labels
//
// Maybe FIXME:
diff --git a/pkg/models/unit_tests.go b/pkg/models/unit_tests.go
index 442a6be0..4fc37320 100644
--- a/pkg/models/unit_tests.go
+++ b/pkg/models/unit_tests.go
@@ -56,7 +56,9 @@ func SetupTests() {
"teams",
"users",
"users_list",
- "users_namespace")
+ "users_namespace",
+ "buckets",
+ )
if err != nil {
log.Fatal(err)
}
diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go
index f6136d9a..af5c53a0 100644
--- a/pkg/routes/routes.go
+++ b/pkg/routes/routes.go
@@ -246,6 +246,16 @@ func registerAPIRoutes(a *echo.Group) {
}
a.GET("/lists/:list/tasks", taskCollectionHandler.ReadAllWeb)
+ kanbanBucketHandler := &handler.WebHandler{
+ EmptyStruct: func() handler.CObject {
+ return &models.Bucket{}
+ },
+ }
+ a.GET("/lists/:list/buckets", kanbanBucketHandler.ReadAllWeb)
+ a.PUT("/lists/:list/buckets", kanbanBucketHandler.CreateWeb)
+ a.POST("/lists/:list/buckets/:bucket", kanbanBucketHandler.UpdateWeb)
+ a.DELETE("/lists/:list/buckets/:bucket", kanbanBucketHandler.DeleteWeb)
+
taskHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.Task{}