From ecdecdd94e497b4627a12a74a4af832812886d12 Mon Sep 17 00:00:00 2001 From: konrad Date: Sun, 19 Apr 2020 07:27:28 +0000 Subject: [PATCH] Kanban (#393) Fix tests Add error docs Add swagger docs for bucket endpoints Add integration tests Fix tests Fix err shadow Make sure a bucket and a task belong to the same list when adding or updating a task Add tests Add getting users of a bucket Fix log level when testing Fix lint Add migration for buckets Cleanup/Comments/Reorganization Add Kanban bucket handling Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/api/pulls/393 --- docs/content/doc/usage/errors.md | 4 +- pkg/db/db.go | 2 +- pkg/db/fixtures/buckets.yml | 103 ++++++++ pkg/db/fixtures/tasks.yml | 8 + pkg/db/test.go | 2 +- pkg/integrations/kanban_test.go | 300 +++++++++++++++++++++++ pkg/integrations/task_collection_test.go | 24 +- pkg/integrations/task_test.go | 36 +++ pkg/log/xorm_logger.go | 7 +- pkg/migration/20200418230432.go | 43 ++++ pkg/migration/20200418230605.go | 49 ++++ pkg/models/error.go | 61 ++++- pkg/models/kanban.go | 205 ++++++++++++++++ pkg/models/kanban_rights.go | 45 ++++ pkg/models/kanban_test.go | 67 +++++ pkg/models/models.go | 1 + pkg/models/task_collection_test.go | 9 + pkg/models/tasks.go | 31 +++ pkg/models/unit_tests.go | 4 +- pkg/routes/routes.go | 10 + 20 files changed, 992 insertions(+), 19 deletions(-) create mode 100644 pkg/db/fixtures/buckets.yml create mode 100644 pkg/integrations/kanban_test.go create mode 100644 pkg/migration/20200418230432.go create mode 100644 pkg/migration/20200418230605.go create mode 100644 pkg/models/kanban.go create mode 100644 pkg/models/kanban_rights.go create mode 100644 pkg/models/kanban_test.go 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{}