Add support for archiving lists and namespaces (#152)
Add query param to get all lists including archived ones Add query param to get all namespaces including archived ones Fix getting lists by namespace only not archived lists Fix misspell Fix lint Merge branch 'master' into feature/archive-lists-namespaces Add docs for error codes Fix archive error codes Don't let archived lists show up in general lists Fix updating description Fix updating lists with link shares More comments Fix un-archiving lists Move check for archiving a list to canWrite Check Add more tests Add more checks Add checks for namespaces and lists Add namespace edit Add tests Add migrations and filter Add basic tests Add is archived property to lists and namespaces Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/152
This commit is contained in:
parent
4472020ee9
commit
5126330a10
20 changed files with 654 additions and 27 deletions
|
@ -34,6 +34,7 @@ This document describes the different errors Vikunja can return.
|
||||||
| 3005 | 400 | The list title cannot be empty. |
|
| 3005 | 400 | The list title cannot be empty. |
|
||||||
| 3006 | 404 | The list share does not exist. |
|
| 3006 | 404 | The list share does not exist. |
|
||||||
| 3007 | 400 | A list with this identifier already exists. |
|
| 3007 | 400 | A list with this identifier already exists. |
|
||||||
|
| 3008 | 412 | The list is archived and can therefore only be accessed read only. This is also true for all tasks associated with this list. |
|
||||||
| 4001 | 400 | The list task text cannot be empty. |
|
| 4001 | 400 | The list task text cannot be empty. |
|
||||||
| 4002 | 404 | The list task does not exist. |
|
| 4002 | 404 | The list task does not exist. |
|
||||||
| 4003 | 403 | All bulk editing tasks must belong to the same list. |
|
| 4003 | 403 | All bulk editing tasks must belong to the same list. |
|
||||||
|
@ -55,6 +56,7 @@ This document describes the different errors Vikunja can return.
|
||||||
| 5009 | 403 | The user needs to have namespace read access to perform that action. |
|
| 5009 | 403 | The user needs to have namespace read access to perform that action. |
|
||||||
| 5010 | 403 | This team does not have access to that namespace. |
|
| 5010 | 403 | This team does not have access to that namespace. |
|
||||||
| 5011 | 409 | This user has already access to that namespace. |
|
| 5011 | 409 | This user has already access to that namespace. |
|
||||||
|
| 5012 | 412 | The namespace is archived and can therefore only be accessed read only. |
|
||||||
| 6001 | 400 | The team name cannot be emtpy. |
|
| 6001 | 400 | The team name cannot be emtpy. |
|
||||||
| 6002 | 404 | The team does not exist. |
|
| 6002 | 404 | The team does not exist. |
|
||||||
| 6004 | 409 | The team already has access to that namespace or list. |
|
| 6004 | 409 | The team already has access to that namespace or list. |
|
||||||
|
|
|
@ -6,3 +6,11 @@
|
||||||
task_id: 2
|
task_id: 2
|
||||||
label_id: 4
|
label_id: 4
|
||||||
created: 0
|
created: 0
|
||||||
|
- id: 3
|
||||||
|
task_id: 35
|
||||||
|
label_id: 4
|
||||||
|
created: 0
|
||||||
|
- id: 4
|
||||||
|
task_id: 36
|
||||||
|
label_id: 4
|
||||||
|
created: 0
|
||||||
|
|
|
@ -180,3 +180,22 @@
|
||||||
namespace_id: 15
|
namespace_id: 15
|
||||||
updated: 0
|
updated: 0
|
||||||
created: 0
|
created: 0
|
||||||
|
-
|
||||||
|
id: 21
|
||||||
|
title: Test21 archived through namespace
|
||||||
|
description: Lorem Ipsum
|
||||||
|
identifier: test21
|
||||||
|
owner_id: 1
|
||||||
|
namespace_id: 16
|
||||||
|
updated: 0
|
||||||
|
created: 0
|
||||||
|
-
|
||||||
|
id: 22
|
||||||
|
title: Test22 archived individually
|
||||||
|
description: Lorem Ipsum
|
||||||
|
identifier: test22
|
||||||
|
owner_id: 1
|
||||||
|
namespace_id: 1
|
||||||
|
is_archived: 1
|
||||||
|
updated: 0
|
||||||
|
created: 0
|
||||||
|
|
|
@ -76,3 +76,9 @@
|
||||||
owner_id: 13
|
owner_id: 13
|
||||||
updated: 0
|
updated: 0
|
||||||
created: 0
|
created: 0
|
||||||
|
- id: 16
|
||||||
|
name: Archived testnamespace16
|
||||||
|
owner_id: 1
|
||||||
|
is_archived: 1
|
||||||
|
updated: 0
|
||||||
|
created: 0
|
||||||
|
|
|
@ -6,3 +6,11 @@
|
||||||
task_id: 30
|
task_id: 30
|
||||||
user_id: 2
|
user_id: 2
|
||||||
created: 0
|
created: 0
|
||||||
|
- id: 3
|
||||||
|
task_id: 35
|
||||||
|
user_id: 2
|
||||||
|
created: 0
|
||||||
|
- id: 4
|
||||||
|
task_id: 36
|
||||||
|
user_id: 2
|
||||||
|
created: 0
|
||||||
|
|
|
@ -82,3 +82,15 @@
|
||||||
task_id: 26
|
task_id: 26
|
||||||
created: 1582135626
|
created: 1582135626
|
||||||
updated: 1582135626
|
updated: 1582135626
|
||||||
|
- id: 15
|
||||||
|
comment: comment 15
|
||||||
|
author_id: 1
|
||||||
|
task_id: 35
|
||||||
|
created: 1582135626
|
||||||
|
updated: 1582135626
|
||||||
|
- id: 16
|
||||||
|
comment: comment 16
|
||||||
|
author_id: 1
|
||||||
|
task_id: 36
|
||||||
|
created: 1582135626
|
||||||
|
updated: 1582135626
|
||||||
|
|
|
@ -10,3 +10,27 @@
|
||||||
relation_kind: 'parenttask'
|
relation_kind: 'parenttask'
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 0
|
created: 0
|
||||||
|
- id: 3
|
||||||
|
task_id: 35
|
||||||
|
other_task_id: 1
|
||||||
|
relation_kind: 'related'
|
||||||
|
created_by_id: 1
|
||||||
|
created: 0
|
||||||
|
- id: 4
|
||||||
|
task_id: 35
|
||||||
|
other_task_id: 1
|
||||||
|
relation_kind: 'related'
|
||||||
|
created_by_id: 1
|
||||||
|
created: 0
|
||||||
|
- id: 5
|
||||||
|
task_id: 36
|
||||||
|
other_task_id: 1
|
||||||
|
relation_kind: 'related'
|
||||||
|
created_by_id: 1
|
||||||
|
created: 0
|
||||||
|
- id: 6
|
||||||
|
task_id: 36
|
||||||
|
other_task_id: 1
|
||||||
|
relation_kind: 'related'
|
||||||
|
created_by_id: 1
|
||||||
|
created: 0
|
||||||
|
|
|
@ -251,5 +251,19 @@
|
||||||
index: 20
|
index: 20
|
||||||
created: 1543626724
|
created: 1543626724
|
||||||
updated: 1543626724
|
updated: 1543626724
|
||||||
|
- id: 35
|
||||||
|
text: 'task #35'
|
||||||
|
created_by_id: 1
|
||||||
|
list_id: 21
|
||||||
|
index: 1
|
||||||
|
created: 1543626724
|
||||||
|
updated: 1543626724
|
||||||
|
- id: 36
|
||||||
|
text: 'task #36'
|
||||||
|
created_by_id: 1
|
||||||
|
list_id: 22
|
||||||
|
index: 1
|
||||||
|
created: 1543626724
|
||||||
|
updated: 1543626724
|
||||||
|
|
||||||
|
|
||||||
|
|
238
pkg/integrations/archived_test.go
Normal file
238
pkg/integrations/archived_test.go
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
// Copyright 2020 Vikunja and contriubtors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This file is part of Vikunja.
|
||||||
|
//
|
||||||
|
// Vikunja is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Vikunja is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package integrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.vikunja.io/api/pkg/models"
|
||||||
|
"code.vikunja.io/web/handler"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This tests the following behaviour:
|
||||||
|
// 1. A namespace should not be editable if it is archived.
|
||||||
|
// 1. With the exception being to un-archive it.
|
||||||
|
// 2. A list which belongs to an archived namespace cannot be edited.
|
||||||
|
// 3. An archived list should not be editable.
|
||||||
|
// 1. Except for un-archiving it.
|
||||||
|
// 4. It is not possible to un-archive a list individually if its namespace is archived.
|
||||||
|
// 5. Creating new lists on an archived namespace should not work.
|
||||||
|
// 6. Creating new tasks on an archived list should not work.
|
||||||
|
// 7. Creating new tasks on a list who's namespace is archived should not work.
|
||||||
|
// 8. Editing tasks on an archived list should not work.
|
||||||
|
// 9. Editing tasks on a list who's namespace is archived should not work.
|
||||||
|
// 10. Archived namespaces should not appear in the list with all namespaces.
|
||||||
|
// 11. Archived lists should not appear in the list with all lists.
|
||||||
|
// 12. Lists who's namespace is archived should not appear in the list with all lists.
|
||||||
|
//
|
||||||
|
// All of this is tested through integration tests because it's not yet clear if this will be implemented directly
|
||||||
|
// or with some kind of middleware.
|
||||||
|
//
|
||||||
|
// Maybe the inheritance of lists from namespaces could be solved with some kind of is_archived_inherited flag -
|
||||||
|
// that way I'd only need to implement the checking on a list level and update the flag for all lists once the
|
||||||
|
// namespace is archived. The archived flag would then be used to not accedentially unarchive lists which were
|
||||||
|
// already individually archived when the namespace was archived.
|
||||||
|
// Should still test it all though.
|
||||||
|
//
|
||||||
|
// Namespace 16 is archived
|
||||||
|
// List 21 belongs to namespace 16
|
||||||
|
// List 22 is archived individually
|
||||||
|
|
||||||
|
func TestArchived(t *testing.T) {
|
||||||
|
testListHandler := webHandlerTest{
|
||||||
|
user: &testuser1,
|
||||||
|
strFunc: func() handler.CObject {
|
||||||
|
return &models.List{}
|
||||||
|
},
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
testNamespaceHandler := webHandlerTest{
|
||||||
|
user: &testuser1,
|
||||||
|
strFunc: func() handler.CObject {
|
||||||
|
return &models.Namespace{}
|
||||||
|
},
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
testTaskHandler := webHandlerTest{
|
||||||
|
user: &testuser1,
|
||||||
|
strFunc: func() handler.CObject {
|
||||||
|
return &models.Task{}
|
||||||
|
},
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
testLabelHandler := webHandlerTest{
|
||||||
|
user: &testuser1,
|
||||||
|
strFunc: func() handler.CObject {
|
||||||
|
return &models.LabelTask{}
|
||||||
|
},
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
testAssigneeHandler := webHandlerTest{
|
||||||
|
user: &testuser1,
|
||||||
|
strFunc: func() handler.CObject {
|
||||||
|
return &models.TaskAssginee{}
|
||||||
|
},
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
testRelationHandler := webHandlerTest{
|
||||||
|
user: &testuser1,
|
||||||
|
strFunc: func() handler.CObject {
|
||||||
|
return &models.TaskRelation{}
|
||||||
|
},
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
testCommentHandler := webHandlerTest{
|
||||||
|
user: &testuser1,
|
||||||
|
strFunc: func() handler.CObject {
|
||||||
|
return &models.TaskComment{}
|
||||||
|
},
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("namespace", func(t *testing.T) {
|
||||||
|
t.Run("not editable", func(t *testing.T) {
|
||||||
|
_, err := testNamespaceHandler.testUpdateWithUser(nil, map[string]string{"namespace": "16"}, `{"name":"TestIpsum","is_archived":true}`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
|
||||||
|
})
|
||||||
|
t.Run("unarchivable", func(t *testing.T) {
|
||||||
|
rec, err := testNamespaceHandler.testUpdateWithUser(nil, map[string]string{"namespace": "16"}, `{"name":"TestIpsum","is_archived":false}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, rec.Body.String(), `"is_archived":false`)
|
||||||
|
})
|
||||||
|
t.Run("no new lists", func(t *testing.T) {
|
||||||
|
_, err := testListHandler.testCreateWithUser(nil, map[string]string{"namespace": "16"}, `{"title":"Lorem"}`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
|
||||||
|
})
|
||||||
|
t.Run("should not appear in the list", func(t *testing.T) {
|
||||||
|
rec, err := testNamespaceHandler.testReadAllWithUser(nil, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotContains(t, rec.Body.String(), `"name":"Archived testnamespace16"`)
|
||||||
|
})
|
||||||
|
t.Run("should appear in the list if explicitly requested", func(t *testing.T) {
|
||||||
|
rec, err := testNamespaceHandler.testReadAllWithUser(url.Values{"is_archived": []string{"true"}}, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, rec.Body.String(), `"name":"Archived testnamespace16"`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("list", func(t *testing.T) {
|
||||||
|
|
||||||
|
taskTests := func(taskID string, errCode int, t *testing.T) {
|
||||||
|
t.Run("task", func(t *testing.T) {
|
||||||
|
t.Run("edit task", func(t *testing.T) {
|
||||||
|
_, err := testTaskHandler.testUpdateWithUser(nil, map[string]string{"listtask": taskID}, `{"text":"TestIpsum"}`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, errCode)
|
||||||
|
})
|
||||||
|
t.Run("delete", func(t *testing.T) {
|
||||||
|
_, err := testTaskHandler.testDeleteWithUser(nil, map[string]string{"listtask": taskID})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, errCode)
|
||||||
|
})
|
||||||
|
t.Run("add new labels", func(t *testing.T) {
|
||||||
|
_, err := testLabelHandler.testCreateWithUser(nil, map[string]string{"listtask": taskID}, `{"label_id":1}`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, errCode)
|
||||||
|
})
|
||||||
|
t.Run("remove lables", func(t *testing.T) {
|
||||||
|
_, err := testLabelHandler.testDeleteWithUser(nil, map[string]string{"listtask": taskID, "label": "4"})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, errCode)
|
||||||
|
})
|
||||||
|
t.Run("add assignees", func(t *testing.T) {
|
||||||
|
_, err := testAssigneeHandler.testCreateWithUser(nil, map[string]string{"listtask": taskID}, `{"user_id":3}`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, errCode)
|
||||||
|
})
|
||||||
|
t.Run("remove assignees", func(t *testing.T) {
|
||||||
|
_, err := testAssigneeHandler.testDeleteWithUser(nil, map[string]string{"listtask": taskID, "user": "2"})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, errCode)
|
||||||
|
})
|
||||||
|
t.Run("add relation", func(t *testing.T) {
|
||||||
|
_, err := testRelationHandler.testCreateWithUser(nil, map[string]string{"task": taskID}, `{"other_task_id":1,"relation_kind":"related"}`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, errCode)
|
||||||
|
})
|
||||||
|
t.Run("remove relation", func(t *testing.T) {
|
||||||
|
_, err := testRelationHandler.testDeleteWithUser(nil, map[string]string{"task": taskID}, `{"other_task_id":2,"relation_kind":"related"}`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, errCode)
|
||||||
|
})
|
||||||
|
t.Run("add comment", func(t *testing.T) {
|
||||||
|
_, err := testCommentHandler.testCreateWithUser(nil, map[string]string{"task": taskID}, `{"comment":"Lorem"}`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, errCode)
|
||||||
|
})
|
||||||
|
t.Run("remove comment", func(t *testing.T) {
|
||||||
|
var commentID = "15"
|
||||||
|
if taskID == "36" {
|
||||||
|
commentID = "16"
|
||||||
|
}
|
||||||
|
_, err := testCommentHandler.testDeleteWithUser(nil, map[string]string{"task": taskID, "commentid": commentID})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, errCode)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// The list belongs to an archived namespace
|
||||||
|
t.Run("archived namespace", func(t *testing.T) {
|
||||||
|
t.Run("not editable", func(t *testing.T) {
|
||||||
|
_, err := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "21"}, `{"title":"TestIpsum","is_archived":true}`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
|
||||||
|
})
|
||||||
|
t.Run("no new tasks", func(t *testing.T) {
|
||||||
|
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"list": "21"}, `{"text":"Lorem"}`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
|
||||||
|
})
|
||||||
|
t.Run("not unarchivable", func(t *testing.T) {
|
||||||
|
_, err := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "21"}, `{"title":"LoremIpsum","is_archived":false}`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
|
||||||
|
})
|
||||||
|
|
||||||
|
taskTests("35", models.ErrCodeNamespaceIsArchived, t)
|
||||||
|
})
|
||||||
|
// The list itself is archived
|
||||||
|
t.Run("archived individually", func(t *testing.T) {
|
||||||
|
t.Run("not editable", func(t *testing.T) {
|
||||||
|
_, err := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "22"}, `{"title":"TestIpsum","is_archived":true}`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, models.ErrCodeListIsArchived)
|
||||||
|
})
|
||||||
|
t.Run("no new tasks", func(t *testing.T) {
|
||||||
|
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"list": "22"}, `{"text":"Lorem"}`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assertHandlerErrorCode(t, err, models.ErrCodeListIsArchived)
|
||||||
|
})
|
||||||
|
t.Run("unarchivable", func(t *testing.T) {
|
||||||
|
rec, err := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "22"}, `{"title":"LoremIpsum","is_archived":false}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, rec.Body.String(), `"is_archived":false`)
|
||||||
|
})
|
||||||
|
|
||||||
|
taskTests("36", models.ErrCodeListIsArchived, t)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -221,9 +221,13 @@ func (h *webHandlerTest) testUpdateWithUser(queryParams url.Values, urlParams ma
|
||||||
return newTestRequestWithUser(h.t, http.MethodPost, hndl.UpdateWeb, h.user, payload, queryParams, urlParams)
|
return newTestRequestWithUser(h.t, http.MethodPost, hndl.UpdateWeb, h.user, payload, queryParams, urlParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *webHandlerTest) testDeleteWithUser(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
|
func (h *webHandlerTest) testDeleteWithUser(queryParams url.Values, urlParams map[string]string, payload ...string) (rec *httptest.ResponseRecorder, err error) {
|
||||||
|
pl := ""
|
||||||
|
if len(payload) > 0 {
|
||||||
|
pl = payload[0]
|
||||||
|
}
|
||||||
hndl := h.getHandler()
|
hndl := h.getHandler()
|
||||||
return newTestRequestWithUser(h.t, http.MethodDelete, hndl.DeleteWeb, h.user, "", queryParams, urlParams)
|
return newTestRequestWithUser(h.t, http.MethodDelete, hndl.DeleteWeb, h.user, pl, queryParams, urlParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *webHandlerTest) testReadAllWithLinkShare(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
|
func (h *webHandlerTest) testReadAllWithLinkShare(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
|
||||||
|
|
|
@ -38,10 +38,12 @@ func TestList(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(nil, nil)
|
rec, err := testHandler.testReadAllWithUser(nil, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `Test1`)
|
assert.Contains(t, rec.Body.String(), `Test1`)
|
||||||
assert.NotContains(t, rec.Body.String(), `Test2`)
|
assert.NotContains(t, rec.Body.String(), `Test2"`)
|
||||||
assert.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_list
|
assert.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_list
|
||||||
assert.Contains(t, rec.Body.String(), `Test4`) // Shared via namespace
|
assert.Contains(t, rec.Body.String(), `Test4`) // Shared via namespace
|
||||||
assert.NotContains(t, rec.Body.String(), `Test5`)
|
assert.NotContains(t, rec.Body.String(), `Test5`)
|
||||||
|
assert.NotContains(t, rec.Body.String(), `Test21`) // Archived through namespace
|
||||||
|
assert.NotContains(t, rec.Body.String(), `Test22`) // Archived directly
|
||||||
})
|
})
|
||||||
t.Run("Search", func(t *testing.T) {
|
t.Run("Search", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"Test1"}}, nil)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"Test1"}}, nil)
|
||||||
|
@ -52,6 +54,17 @@ func TestList(t *testing.T) {
|
||||||
assert.NotContains(t, rec.Body.String(), `Test4`)
|
assert.NotContains(t, rec.Body.String(), `Test4`)
|
||||||
assert.NotContains(t, rec.Body.String(), `Test5`)
|
assert.NotContains(t, rec.Body.String(), `Test5`)
|
||||||
})
|
})
|
||||||
|
t.Run("Normal with archived lists", func(t *testing.T) {
|
||||||
|
rec, err := testHandler.testReadAllWithUser(url.Values{"is_archived": []string{"true"}}, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, rec.Body.String(), `Test1`)
|
||||||
|
assert.NotContains(t, rec.Body.String(), `Test2"`)
|
||||||
|
assert.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_list
|
||||||
|
assert.Contains(t, rec.Body.String(), `Test4`) // Shared via namespace
|
||||||
|
assert.NotContains(t, rec.Body.String(), `Test5`)
|
||||||
|
assert.Contains(t, rec.Body.String(), `Test21`) // Archived through namespace
|
||||||
|
assert.Contains(t, rec.Body.String(), `Test22`) // Archived directly
|
||||||
|
})
|
||||||
})
|
})
|
||||||
t.Run("ReadOne", func(t *testing.T) {
|
t.Run("ReadOne", func(t *testing.T) {
|
||||||
t.Run("Normal", func(t *testing.T) {
|
t.Run("Normal", func(t *testing.T) {
|
||||||
|
|
44
pkg/migration/20200308205855.go
Normal file
44
pkg/migration/20200308205855.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// Vikunja is a todo-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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"src.techknowlogick.com/xormigrate"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type list20200308205855 struct {
|
||||||
|
IsArchived bool `xorm:"not null default false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s list20200308205855) TableName() string {
|
||||||
|
return "list"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
migrations = append(migrations, &xormigrate.Migration{
|
||||||
|
ID: "20200308205855",
|
||||||
|
Description: "Add is archived flag to lists",
|
||||||
|
Migrate: func(tx *xorm.Engine) error {
|
||||||
|
return tx.Sync2(list20200308205855{})
|
||||||
|
},
|
||||||
|
Rollback: func(tx *xorm.Engine) error {
|
||||||
|
return tx.DropTables(list20200308205855{})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
44
pkg/migration/20200308210130.go
Normal file
44
pkg/migration/20200308210130.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// Vikunja is a todo-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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"src.techknowlogick.com/xormigrate"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type namepsace20200308210130 struct {
|
||||||
|
IsArchived bool `xorm:"not null default false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s namepsace20200308210130) TableName() string {
|
||||||
|
return "namespaces"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
migrations = append(migrations, &xormigrate.Migration{
|
||||||
|
ID: "20200308210130",
|
||||||
|
Description: "Add is archived flag to namepaces",
|
||||||
|
Migrate: func(tx *xorm.Engine) error {
|
||||||
|
return tx.Sync2(namepsace20200308210130{})
|
||||||
|
},
|
||||||
|
Rollback: func(tx *xorm.Engine) error {
|
||||||
|
return tx.DropTables(namepsace20200308210130{})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
|
@ -231,6 +231,29 @@ func (err ErrListIdentifierIsNotUnique) HTTPError() web.HTTPError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrListIsArchived represents an error, where a list is archived
|
||||||
|
type ErrListIsArchived struct {
|
||||||
|
ListID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrListIsArchived checks if an error is a .
|
||||||
|
func IsErrListIsArchived(err error) bool {
|
||||||
|
_, ok := err.(ErrListIsArchived)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrListIsArchived) Error() string {
|
||||||
|
return fmt.Sprintf("List is archived [ListID: %d]", err.ListID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrCodeListIsArchived holds the unique world-error code of this error
|
||||||
|
const ErrCodeListIsArchived = 3008
|
||||||
|
|
||||||
|
// HTTPError holds the http error description
|
||||||
|
func (err ErrListIsArchived) HTTPError() web.HTTPError {
|
||||||
|
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeListIsArchived, Message: "This lists is archived. Editing or creating new tasks is not possible."}
|
||||||
|
}
|
||||||
|
|
||||||
// ================
|
// ================
|
||||||
// List task errors
|
// List task errors
|
||||||
// ================
|
// ================
|
||||||
|
@ -777,6 +800,29 @@ func (err ErrUserAlreadyHasNamespaceAccess) HTTPError() web.HTTPError {
|
||||||
return web.HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserAlreadyHasNamespaceAccess, Message: "This user already has access to this namespace."}
|
return web.HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserAlreadyHasNamespaceAccess, Message: "This user already has access to this namespace."}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrNamespaceIsArchived represents an error where a namespace is archived
|
||||||
|
type ErrNamespaceIsArchived struct {
|
||||||
|
NamespaceID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrNamespaceIsArchived checks if an error is a .
|
||||||
|
func IsErrNamespaceIsArchived(err error) bool {
|
||||||
|
_, ok := err.(ErrNamespaceIsArchived)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrNamespaceIsArchived) Error() string {
|
||||||
|
return fmt.Sprintf("Namespace is archived [NamespaceID: %d]", err.NamespaceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrCodeNamespaceIsArchived holds the unique world-error code of this error
|
||||||
|
const ErrCodeNamespaceIsArchived = 5012
|
||||||
|
|
||||||
|
// HTTPError holds the http error description
|
||||||
|
func (err ErrNamespaceIsArchived) HTTPError() web.HTTPError {
|
||||||
|
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeNamespaceIsArchived, Message: "This namespaces is archived. Editing or creating new lists is not possible."}
|
||||||
|
}
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Team errors
|
// Team errors
|
||||||
// ============
|
// ============
|
||||||
|
|
|
@ -203,7 +203,10 @@ func getLabelByIDSimple(labelID int64) (*Label, error) {
|
||||||
func getUserTaskIDs(u *user.User) (taskIDs []int64, err error) {
|
func getUserTaskIDs(u *user.User) (taskIDs []int64, err error) {
|
||||||
|
|
||||||
// Get all lists
|
// Get all lists
|
||||||
lists, _, _, err := getRawListsForUser("", u, -1, 0)
|
lists, _, _, err := getRawListsForUser(&listOptions{
|
||||||
|
user: u,
|
||||||
|
page: -1,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"code.vikunja.io/api/pkg/timeutil"
|
"code.vikunja.io/api/pkg/timeutil"
|
||||||
"code.vikunja.io/api/pkg/user"
|
"code.vikunja.io/api/pkg/user"
|
||||||
"code.vikunja.io/web"
|
"code.vikunja.io/web"
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// List represents a list of tasks
|
// List represents a list of tasks
|
||||||
|
@ -43,6 +44,9 @@ type List struct {
|
||||||
// Deprecated: you should use the dedicated task list endpoint because it has support for pagination and filtering
|
// Deprecated: you should use the dedicated task list endpoint because it has support for pagination and filtering
|
||||||
Tasks []*Task `xorm:"-" json:"-"`
|
Tasks []*Task `xorm:"-" json:"-"`
|
||||||
|
|
||||||
|
// Whether or not a list is archived.
|
||||||
|
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
|
||||||
|
|
||||||
// A timestamp when this list was created. You cannot change this value.
|
// A timestamp when this list was created. You cannot change this value.
|
||||||
Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
|
Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
|
||||||
// A timestamp when this list was last updated. You cannot change this value.
|
// A timestamp when this list was last updated. You cannot change this value.
|
||||||
|
@ -57,16 +61,24 @@ func GetListsByNamespaceID(nID int64, doer *user.User) (lists []*List, err error
|
||||||
if nID == -1 {
|
if nID == -1 {
|
||||||
err = x.Select("l.*").
|
err = x.Select("l.*").
|
||||||
Table("list").
|
Table("list").
|
||||||
Alias("l").
|
|
||||||
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
|
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
|
||||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tl.team_id").
|
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tl.team_id").
|
||||||
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
||||||
|
Join("LEFT", []string{"namespaces", "n"}, "l.namespace_id = n.id").
|
||||||
Where("tm.user_id = ?", doer.ID).
|
Where("tm.user_id = ?", doer.ID).
|
||||||
|
Where("l.is_archived = false").
|
||||||
|
Where("n.is_archived = false").
|
||||||
Or("ul.user_id = ?", doer.ID).
|
Or("ul.user_id = ?", doer.ID).
|
||||||
GroupBy("l.id").
|
GroupBy("l.id").
|
||||||
Find(&lists)
|
Find(&lists)
|
||||||
} else {
|
} else {
|
||||||
err = x.Where("namespace_id = ?", nID).Find(&lists)
|
err = x.Select("l.*").
|
||||||
|
Alias("l").
|
||||||
|
Join("LEFT", []string{"namespaces", "n"}, "l.namespace_id = n.id").
|
||||||
|
Where("l.is_archived = false").
|
||||||
|
Where("n.is_archived = false").
|
||||||
|
Where("namespace_id = ?", nID).
|
||||||
|
Find(&lists)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -86,6 +98,7 @@ func GetListsByNamespaceID(nID int64, doer *user.User) (lists []*List, err error
|
||||||
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
||||||
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
|
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
|
||||||
// @Param s query string false "Search lists by title."
|
// @Param s query string false "Search lists by title."
|
||||||
|
// @Param is_archived query bool false "If true, also returns all archived lists."
|
||||||
// @Security JWTKeyAuth
|
// @Security JWTKeyAuth
|
||||||
// @Success 200 {array} models.List "The lists"
|
// @Success 200 {array} models.List "The lists"
|
||||||
// @Failure 403 {object} code.vikunja.io/web.HTTPError "The user does not have access to the list"
|
// @Failure 403 {object} code.vikunja.io/web.HTTPError "The user does not have access to the list"
|
||||||
|
@ -105,7 +118,13 @@ func (l *List) ReadAll(a web.Auth, search string, page int, perPage int) (result
|
||||||
return lists, 0, 0, err
|
return lists, 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lists, resultCount, totalItems, err := getRawListsForUser(search, &user.User{ID: a.GetID()}, page, perPage)
|
lists, resultCount, totalItems, err := getRawListsForUser(&listOptions{
|
||||||
|
search: search,
|
||||||
|
user: &user.User{ID: a.GetID()},
|
||||||
|
page: page,
|
||||||
|
perPage: perPage,
|
||||||
|
isArchived: l.IsArchived,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
|
@ -177,13 +196,30 @@ func GetListSimplByTaskID(taskID int64) (l *List, err error) {
|
||||||
return &list, nil
|
return &list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type listOptions struct {
|
||||||
|
search string
|
||||||
|
user *user.User
|
||||||
|
page int
|
||||||
|
perPage int
|
||||||
|
isArchived bool
|
||||||
|
}
|
||||||
|
|
||||||
// Gets the lists only, without any tasks or so
|
// Gets the lists only, without any tasks or so
|
||||||
func getRawListsForUser(search string, u *user.User, page int, perPage int) (lists []*List, resultCount int, totalItems int64, err error) {
|
func getRawListsForUser(opts *listOptions) (lists []*List, resultCount int, totalItems int64, err error) {
|
||||||
fullUser, err := user.GetUserByID(u.ID)
|
fullUser, err := user.GetUserByID(opts.user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
|
||||||
|
var isArchivedCond builder.Cond = builder.Eq{"1": 1}
|
||||||
|
if !opts.isArchived {
|
||||||
|
isArchivedCond = builder.And(
|
||||||
|
builder.Eq{"l.is_archived": false},
|
||||||
|
builder.Eq{"n.is_archived": false},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Gets all Lists where the user is either owner or in a team which has access to the list
|
// Gets all Lists where the user is either owner or in a team which has access to the list
|
||||||
// Or in a team which has namespace read access
|
// Or in a team which has namespace read access
|
||||||
err = x.Select("l.*").
|
err = x.Select("l.*").
|
||||||
|
@ -202,8 +238,9 @@ func getRawListsForUser(search string, u *user.User, page int, perPage int) (lis
|
||||||
Or("ul.user_id = ?", fullUser.ID).
|
Or("ul.user_id = ?", fullUser.ID).
|
||||||
Or("un.user_id = ?", fullUser.ID).
|
Or("un.user_id = ?", fullUser.ID).
|
||||||
GroupBy("l.id").
|
GroupBy("l.id").
|
||||||
Limit(getLimitFromPageIndex(page, perPage)).
|
Limit(getLimitFromPageIndex(opts.page, opts.perPage)).
|
||||||
Where("l.title LIKE ?", "%"+search+"%").
|
Where("l.title LIKE ?", "%"+opts.search+"%").
|
||||||
|
Where(isArchivedCond).
|
||||||
Find(&lists)
|
Find(&lists)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
|
@ -225,8 +262,9 @@ func getRawListsForUser(search string, u *user.User, page int, perPage int) (lis
|
||||||
Or("ul.user_id = ?", fullUser.ID).
|
Or("ul.user_id = ?", fullUser.ID).
|
||||||
Or("un.user_id = ?", fullUser.ID).
|
Or("un.user_id = ?", fullUser.ID).
|
||||||
GroupBy("l.id").
|
GroupBy("l.id").
|
||||||
Limit(getLimitFromPageIndex(page, perPage)).
|
Limit(getLimitFromPageIndex(opts.page, opts.perPage)).
|
||||||
Where("l.title LIKE ?", "%"+search+"%").
|
Where("l.title LIKE ?", "%"+opts.search+"%").
|
||||||
|
Where(isArchivedCond).
|
||||||
Count(&List{})
|
Count(&List{})
|
||||||
return lists, len(lists), totalItems, err
|
return lists, len(lists), totalItems, err
|
||||||
}
|
}
|
||||||
|
@ -259,6 +297,38 @@ func AddListDetails(lists []*List) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NamespaceList is a meta type to be able to join a list with its namespace
|
||||||
|
type NamespaceList struct {
|
||||||
|
List List `xorm:"extends"`
|
||||||
|
Namespace Namespace `xorm:"extends"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckIsArchived returns an ErrListIsArchived or ErrNamespaceIsArchived if the list or its namespace is archived.
|
||||||
|
func (l *List) CheckIsArchived() (err error) {
|
||||||
|
// When creating a new list, we check if the namespace is archived
|
||||||
|
if l.ID == 0 {
|
||||||
|
n := &Namespace{ID: l.NamespaceID}
|
||||||
|
return n.CheckIsArchived()
|
||||||
|
}
|
||||||
|
|
||||||
|
nl := &NamespaceList{}
|
||||||
|
exists, err := x.
|
||||||
|
Table("list").
|
||||||
|
Join("LEFT", "namespaces", "list.namespace_id = namespaces.id").
|
||||||
|
Where("list.id = ? AND (list.is_archived = true OR namespaces.is_archived = true)", l.ID).
|
||||||
|
Get(nl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if exists && nl.List.ID != 0 && nl.List.IsArchived {
|
||||||
|
return ErrListIsArchived{ListID: l.ID}
|
||||||
|
}
|
||||||
|
if exists && nl.Namespace.ID != 0 && nl.Namespace.IsArchived {
|
||||||
|
return ErrNamespaceIsArchived{NamespaceID: nl.Namespace.ID}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateOrUpdateList updates a list or creates it if it doesn't exist
|
// CreateOrUpdateList updates a list or creates it if it doesn't exist
|
||||||
func CreateOrUpdateList(list *List) (err error) {
|
func CreateOrUpdateList(list *List) (err error) {
|
||||||
|
|
||||||
|
@ -285,7 +355,22 @@ func CreateOrUpdateList(list *List) (err error) {
|
||||||
_, err = x.Insert(list)
|
_, err = x.Insert(list)
|
||||||
metrics.UpdateCount(1, metrics.ListCountKey)
|
metrics.UpdateCount(1, metrics.ListCountKey)
|
||||||
} else {
|
} else {
|
||||||
_, err = x.ID(list.ID).Update(list)
|
// We need to specify the cols we want to update here to be able to un-archive lists
|
||||||
|
colsToUpdate := []string{
|
||||||
|
"title",
|
||||||
|
"is_archived",
|
||||||
|
}
|
||||||
|
if list.Description != "" {
|
||||||
|
colsToUpdate = append(colsToUpdate, "description")
|
||||||
|
}
|
||||||
|
if list.Identifier != "" {
|
||||||
|
colsToUpdate = append(colsToUpdate, "identifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = x.
|
||||||
|
ID(list.ID).
|
||||||
|
Cols(colsToUpdate...).
|
||||||
|
Update(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -350,6 +435,11 @@ func updateListByTaskID(taskID int64) (err error) {
|
||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
// @Router /namespaces/{namespaceID}/lists [put]
|
// @Router /namespaces/{namespaceID}/lists [put]
|
||||||
func (l *List) Create(a web.Auth) (err error) {
|
func (l *List) Create(a web.Auth) (err error) {
|
||||||
|
err = l.CheckIsArchived()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
doer, err := user.GetFromAuth(a)
|
doer, err := user.GetFromAuth(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -32,19 +32,33 @@ func (l *List) CanWrite(a web.Auth) (bool, error) {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We put the result of the is archived check in a separate variable to be able to return it later without
|
||||||
|
// needing to recheck it again
|
||||||
|
errIsArchived := originalList.CheckIsArchived()
|
||||||
|
|
||||||
|
var canWrite bool
|
||||||
|
|
||||||
// Check if we're dealing with a share auth
|
// Check if we're dealing with a share auth
|
||||||
shareAuth, ok := a.(*LinkSharing)
|
shareAuth, ok := a.(*LinkSharing)
|
||||||
if ok {
|
if ok {
|
||||||
return originalList.ID == shareAuth.ListID &&
|
return originalList.ID == shareAuth.ListID &&
|
||||||
(shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), nil
|
(shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), errIsArchived
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user is either owner or can write to the list
|
// Check if the user is either owner or can write to the list
|
||||||
if originalList.isOwner(&user.User{ID: a.GetID()}) {
|
if originalList.isOwner(&user.User{ID: a.GetID()}) {
|
||||||
return true, nil
|
canWrite = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalList.checkRight(a, RightWrite, RightAdmin)
|
if canWrite {
|
||||||
|
return canWrite, errIsArchived
|
||||||
|
}
|
||||||
|
|
||||||
|
canWrite, err = originalList.checkRight(a, RightWrite, RightAdmin)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return canWrite, errIsArchived
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanRead checks if a user has read access to a list
|
// CanRead checks if a user has read access to a list
|
||||||
|
@ -68,8 +82,13 @@ func (l *List) CanRead(a web.Auth) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanUpdate checks if the user can update a list
|
// CanUpdate checks if the user can update a list
|
||||||
func (l *List) CanUpdate(a web.Auth) (bool, error) {
|
func (l *List) CanUpdate(a web.Auth) (canUpdate bool, err error) {
|
||||||
return l.CanWrite(a)
|
canUpdate, err = l.CanWrite(a)
|
||||||
|
// If the list is archived and the user tries to un-archive it, let the request through
|
||||||
|
if IsErrListIsArchived(err) && !l.IsArchived {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return canUpdate, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanDelete checks if the user can delete a list
|
// CanDelete checks if the user can delete a list
|
||||||
|
|
|
@ -35,6 +35,9 @@ type Namespace struct {
|
||||||
Description string `xorm:"longtext null" json:"description"`
|
Description string `xorm:"longtext null" json:"description"`
|
||||||
OwnerID int64 `xorm:"int(11) not null INDEX" json:"-"`
|
OwnerID int64 `xorm:"int(11) not null INDEX" json:"-"`
|
||||||
|
|
||||||
|
// Whether or not a namespace is archived.
|
||||||
|
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
|
||||||
|
|
||||||
// The user who owns this namespace
|
// The user who owns this namespace
|
||||||
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
|
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
|
||||||
|
|
||||||
|
@ -103,6 +106,20 @@ func GetNamespaceByID(id int64) (namespace Namespace, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckIsArchived returns an ErrNamespaceIsArchived if the namepace is archived.
|
||||||
|
func (n *Namespace) CheckIsArchived() error {
|
||||||
|
exists, err := x.
|
||||||
|
Where("id = ? AND is_archived = true", n.ID).
|
||||||
|
Exist(&Namespace{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return ErrNamespaceIsArchived{NamespaceID: n.ID}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ReadOne gets one namespace
|
// ReadOne gets one namespace
|
||||||
// @Summary Gets one namespace
|
// @Summary Gets one namespace
|
||||||
// @Description Returns a namespace by its ID.
|
// @Description Returns a namespace by its ID.
|
||||||
|
@ -136,6 +153,7 @@ type NamespaceWithLists struct {
|
||||||
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
||||||
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
|
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
|
||||||
// @Param s query string false "Search namespaces by name."
|
// @Param s query string false "Search namespaces by name."
|
||||||
|
// @Param is_archived query bool false "If true, also returns all archived namespaces."
|
||||||
// @Security JWTKeyAuth
|
// @Security JWTKeyAuth
|
||||||
// @Success 200 {array} models.NamespaceWithLists "The Namespaces."
|
// @Success 200 {array} models.NamespaceWithLists "The Namespaces."
|
||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
@ -169,6 +187,7 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
||||||
Where("team_members.user_id = ?", doer.ID).
|
Where("team_members.user_id = ?", doer.ID).
|
||||||
Or("namespaces.owner_id = ?", doer.ID).
|
Or("namespaces.owner_id = ?", doer.ID).
|
||||||
Or("users_namespace.user_id = ?", doer.ID).
|
Or("users_namespace.user_id = ?", doer.ID).
|
||||||
|
And("namespaces.is_archived = ?", n.IsArchived).
|
||||||
GroupBy("namespaces.id").
|
GroupBy("namespaces.id").
|
||||||
Limit(getLimitFromPageIndex(page, perPage)).
|
Limit(getLimitFromPageIndex(page, perPage)).
|
||||||
Where("namespaces.name LIKE ?", "%"+search+"%").
|
Where("namespaces.name LIKE ?", "%"+search+"%").
|
||||||
|
@ -188,6 +207,7 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
||||||
Where("team_members.user_id = ?", doer.ID).
|
Where("team_members.user_id = ?", doer.ID).
|
||||||
Or("namespaces.owner_id = ?", doer.ID).
|
Or("namespaces.owner_id = ?", doer.ID).
|
||||||
Or("users_namespace.user_id = ?", doer.ID).
|
Or("users_namespace.user_id = ?", doer.ID).
|
||||||
|
And("namespaces.is_archived = ?", n.IsArchived).
|
||||||
GroupBy("users.id").
|
GroupBy("users.id").
|
||||||
Find(&users)
|
Find(&users)
|
||||||
|
|
||||||
|
@ -220,6 +240,7 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
||||||
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
||||||
Where("tm.user_id = ?", doer.ID).
|
Where("tm.user_id = ?", doer.ID).
|
||||||
Or("ul.user_id = ?", doer.ID).
|
Or("ul.user_id = ?", doer.ID).
|
||||||
|
And("l.is_archived = false").
|
||||||
GroupBy("l.id").
|
GroupBy("l.id").
|
||||||
Find(&individualLists)
|
Find(&individualLists)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -272,6 +293,7 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
||||||
Where("team_members.user_id = ?", doer.ID).
|
Where("team_members.user_id = ?", doer.ID).
|
||||||
Or("namespaces.owner_id = ?", doer.ID).
|
Or("namespaces.owner_id = ?", doer.ID).
|
||||||
Or("users_namespace.user_id = ?", doer.ID).
|
Or("users_namespace.user_id = ?", doer.ID).
|
||||||
|
And("namespaces.is_archived = false").
|
||||||
GroupBy("namespaces.id").
|
GroupBy("namespaces.id").
|
||||||
Where("namespaces.name LIKE ?", "%"+search+"%").
|
Where("namespaces.name LIKE ?", "%"+search+"%").
|
||||||
Count(&NamespaceWithLists{})
|
Count(&NamespaceWithLists{})
|
||||||
|
@ -400,12 +422,19 @@ func (n *Namespace) Update() (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the namespace is archived and the update is not un-archiving it
|
||||||
|
if currentNamespace.IsArchived && n.IsArchived {
|
||||||
|
return ErrNamespaceIsArchived{NamespaceID: n.ID}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the (new) owner exists
|
// Check if the (new) owner exists
|
||||||
n.OwnerID = n.Owner.ID
|
if n.Owner != nil {
|
||||||
if currentNamespace.OwnerID != n.OwnerID {
|
n.OwnerID = n.Owner.ID
|
||||||
n.Owner, err = user.GetUserByID(n.OwnerID)
|
if currentNamespace.OwnerID != n.OwnerID {
|
||||||
if err != nil {
|
n.Owner, err = user.GetUserByID(n.OwnerID)
|
||||||
return
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,8 @@ func (n *Namespace) checkRight(a web.Auth, rights ...Right) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the namespace and check the right
|
// Get the namespace and check the right
|
||||||
err := n.GetSimpleByID()
|
nn := &Namespace{ID: n.ID}
|
||||||
|
err := nn.GetSimpleByID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,7 +109,10 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i
|
||||||
// If the list ID is not set, we get all tasks for the user.
|
// If the list ID is not set, we get all tasks for the user.
|
||||||
// This allows to use this function in Task.ReadAll with a possibility to deprecate the latter at some point.
|
// This allows to use this function in Task.ReadAll with a possibility to deprecate the latter at some point.
|
||||||
if tf.ListID == 0 {
|
if tf.ListID == 0 {
|
||||||
tf.Lists, _, _, err = getRawListsForUser("", &user.User{ID: a.GetID()}, -1, 0)
|
tf.Lists, _, _, err = getRawListsForUser(&listOptions{
|
||||||
|
user: &user.User{ID: a.GetID()},
|
||||||
|
page: -1,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue