forked from vikunja/vikunja
Compare commits
59 Commits
main
...
feature/na
Author | SHA1 | Date |
---|---|---|
kolaente | 518bf7ded3 | |
kolaente | dbbc99c581 | |
kolaente | 2260688c3b | |
kolaente | c3a099d71b | |
kolaente | 31553ef11d | |
kolaente | 6948648e62 | |
kolaente | 8118795a86 | |
kolaente | bc0724c2ad | |
kolaente | 798d575c05 | |
kolaente | 0223c7f660 | |
kolaente | 877dedcb70 | |
kolaente | ae9668b3c3 | |
kolaente | 82bcc726a8 | |
kolaente | 7f175f7f1a | |
kolaente | 5570e44c1b | |
kolaente | 091390c8ab | |
kolaente | 0eca53e49a | |
kolaente | 6008046505 | |
kolaente | 36819a8cca | |
kolaente | 1defe1a860 | |
kolaente | 3c3bf638d6 | |
kolaente | 92b7fb71d8 | |
kolaente | 4c884246df | |
kolaente | 7d5e687236 | |
kolaente | 1e2dd17386 | |
kolaente | 17898f7a1c | |
kolaente | 8fcc68f7d4 | |
kolaente | 83750abe02 | |
kolaente | 2b161ad112 | |
kolaente | 7e5c336b03 | |
kolaente | b4ef089973 | |
kolaente | 1af82f6686 | |
kolaente | fd9b2e15d9 | |
kolaente | 68baafa990 | |
kolaente | c056c4ea9b | |
kolaente | ad0d9da90c | |
kolaente | 05fc0d6add | |
kolaente | 7575a19dbe | |
kolaente | c05ed95ba9 | |
kolaente | e8e2e205f4 | |
kolaente | 92f0a50996 | |
kolaente | c244a0f145 | |
kolaente | ce7021f152 | |
kolaente | 2f3672ee4f | |
kolaente | 8d052687f2 | |
kolaente | 01bd4ccf7a | |
kolaente | 3a9757632b | |
kolaente | 5df54ddfb8 | |
kolaente | 0a03676aee | |
kolaente | 3e3f0aaa36 | |
kolaente | 68d339baf7 | |
kolaente | e1eee7b4ca | |
kolaente | b8158b14f0 | |
kolaente | b7ba2a7730 | |
kolaente | 496c8f691b | |
kolaente | 24ddc10a66 | |
kolaente | 0284baa96d | |
kolaente | da7ef8a38c | |
kolaente | 6e36332361 |
|
@ -88,3 +88,5 @@ issues:
|
|||
- path: pkg/models/favorites\.go
|
||||
linters:
|
||||
- nilerr
|
||||
- path: pkg/models/prject\.go
|
||||
text: "string `parent_project_id` has 3 occurrences, make it a constant"
|
||||
|
|
|
@ -60,8 +60,7 @@ This document describes the different errors Vikunja can return.
|
|||
| 3006 | 404 | The list share does not exist. |
|
||||
| 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. |
|
||||
| 3009 | 412 | The list cannot belong to a dynamically generated namespace like "Favorites". |
|
||||
| 3010 | 412 | The list must belong to a namespace. |
|
||||
| 3009 | 412 | The list cannot belong to a dynamically generated parent project like "Favorites". |
|
||||
|
||||
## Task
|
||||
|
||||
|
@ -89,27 +88,15 @@ This document describes the different errors Vikunja can return.
|
|||
| 4020 | 400 | The provided attachment does not belong to that task. |
|
||||
| 4021 | 400 | This user is already assigned to that task. |
|
||||
|
||||
## Namespace
|
||||
|
||||
| ErrorCode | HTTP Status Code | Description |
|
||||
|-----------|------------------|-------------|
|
||||
| 5001 | 404 | The namspace does not exist. |
|
||||
| 5003 | 403 | The user does not have access to the specified namespace. |
|
||||
| 5006 | 400 | The namespace name cannot be empty. |
|
||||
| 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. |
|
||||
| 5011 | 409 | This user has already access to that namespace. |
|
||||
| 5012 | 412 | The namespace is archived and can therefore only be accessed read only. |
|
||||
|
||||
## Team
|
||||
|
||||
| ErrorCode | HTTP Status Code | Description |
|
||||
|-----------|------------------|-------------|
|
||||
| 6001 | 400 | The team name cannot be emtpy. |
|
||||
| 6002 | 404 | The team does not exist. |
|
||||
| 6004 | 409 | The team already has access to that namespace or list. |
|
||||
| 6005 | 409 | The user is already a member of that team. |
|
||||
| 6006 | 400 | Cannot delete the last team member. |
|
||||
| ErrorCode | HTTP Status Code | Description |
|
||||
|-----------|------------------|-------------------------------------------------------------------|
|
||||
| 6001 | 400 | The team name cannot be emtpy. |
|
||||
| 6002 | 404 | The team does not exist. |
|
||||
| 6004 | 409 | The team already has access to that project. |
|
||||
| 6005 | 409 | The user is already a member of that team. |
|
||||
| 6006 | 400 | Cannot delete the last team member. |
|
||||
| 6007 | 403 | The team does not have access to the list to perform that action. |
|
||||
|
||||
## User List Access
|
||||
|
|
1
go.mod
1
go.mod
|
@ -104,6 +104,7 @@ require (
|
|||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.0 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -384,6 +384,8 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv
|
|||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
|
|
|
@ -27,11 +27,11 @@ import (
|
|||
ics "github.com/arran4/golang-ical"
|
||||
)
|
||||
|
||||
func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*models.TaskWithComments) string {
|
||||
func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectTasks []*models.TaskWithComments) string {
|
||||
|
||||
// Make caldav todos from Vikunja todos
|
||||
var caldavtodos []*Todo
|
||||
for _, t := range listTasks {
|
||||
for _, t := range projectTasks {
|
||||
|
||||
duration := t.EndDate.Sub(t.StartDate)
|
||||
|
||||
|
@ -55,7 +55,7 @@ func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*m
|
|||
}
|
||||
|
||||
caldavConfig := &Config{
|
||||
Name: list.Title,
|
||||
Name: project.Title,
|
||||
ProdID: "Vikunja Todo App",
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ func init() {
|
|||
// User deletion flags
|
||||
userDeleteCmd.Flags().BoolVarP(&userFlagDeleteNow, "now", "n", false, "If provided, deletes the user immediately instead of sending them an email first.")
|
||||
|
||||
userCmd.AddCommand(userListCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd, userDeleteCmd)
|
||||
userCmd.AddCommand(userProjectCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd, userDeleteCmd)
|
||||
rootCmd.AddCommand(userCmd)
|
||||
}
|
||||
|
||||
|
@ -117,9 +117,9 @@ var userCmd = &cobra.Command{
|
|||
Short: "Manage users locally through the cli.",
|
||||
}
|
||||
|
||||
var userListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Shows a list of all users.",
|
||||
var userProjectCmd = &cobra.Command{
|
||||
Use: "project",
|
||||
Short: "Shows a project of all users.",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
initialize.FullInit()
|
||||
},
|
||||
|
@ -127,7 +127,7 @@ var userListCmd = &cobra.Command{
|
|||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
users, err := user.ListAllUsers(s)
|
||||
users, err := user.ProjectAllUsers(s)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Fatalf("Error getting users: %s", err)
|
||||
|
@ -188,10 +188,10 @@ var userCreateCmd = &cobra.Command{
|
|||
log.Fatalf("Error creating new user: %s", err)
|
||||
}
|
||||
|
||||
err = models.CreateNewNamespaceForUser(s, newUser)
|
||||
err = models.CreateNewProjectForUser(s, newUser)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Fatalf("Error creating new namespace for user: %s", err)
|
||||
log.Fatalf("Error creating new project for user: %s", err)
|
||||
}
|
||||
|
||||
if err := s.Commit(); err != nil {
|
||||
|
|
|
@ -164,7 +164,7 @@ const (
|
|||
DefaultSettingsDiscoverableByName Key = `defaultsettings.discoverable_by_name`
|
||||
DefaultSettingsDiscoverableByEmail Key = `defaultsettings.discoverable_by_email`
|
||||
DefaultSettingsOverdueTaskRemindersEnabled Key = `defaultsettings.overdue_tasks_reminders_enabled`
|
||||
DefaultSettingsDefaultListID Key = `defaultsettings.default_list_id`
|
||||
DefaultSettingsDefaultProjectID Key = `defaultsettings.default_project_id`
|
||||
DefaultSettingsWeekStart Key = `defaultsettings.week_start`
|
||||
DefaultSettingsLanguage Key = `defaultsettings.language`
|
||||
DefaultSettingsTimezone Key = `defaultsettings.timezone`
|
||||
|
@ -370,7 +370,7 @@ func InitDefaultConfig() {
|
|||
MigrationMicrosoftTodoEnable.setDefault(false)
|
||||
// Avatar
|
||||
AvatarGravaterExpiration.setDefault(3600)
|
||||
// List Backgrounds
|
||||
// Project Backgrounds
|
||||
BackgroundsEnabled.setDefault(true)
|
||||
BackgroundsUploadEnabled.setDefault(true)
|
||||
BackgroundsUnsplashEnabled.setDefault(false)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- id: 1
|
||||
title: testbucket1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
created_by_id: 1
|
||||
limit: 9999999 # This bucket has a limit we will never exceed in the tests to make sure the logic allows for buckets with limits
|
||||
position: 1
|
||||
|
@ -8,7 +8,7 @@
|
|||
updated: 2020-04-18 21:13:52
|
||||
- id: 2
|
||||
title: testbucket2
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
created_by_id: 1
|
||||
limit: 3
|
||||
position: 2
|
||||
|
@ -16,15 +16,15 @@
|
|||
updated: 2020-04-18 21:13:52
|
||||
- id: 3
|
||||
title: testbucket3
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
created_by_id: 1
|
||||
is_done_bucket: 1
|
||||
position: 3
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 4
|
||||
title: testbucket4 - other list
|
||||
list_id: 2
|
||||
title: testbucket4 - other project
|
||||
project_id: 2
|
||||
created_by_id: 1
|
||||
is_done_bucket: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
|
@ -32,189 +32,201 @@
|
|||
# The following are not or only partly owned by user 1
|
||||
- id: 5
|
||||
title: testbucket5
|
||||
list_id: 20
|
||||
project_id: 20
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 6
|
||||
title: testbucket6
|
||||
list_id: 6
|
||||
project_id: 6
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 7
|
||||
title: testbucket7
|
||||
list_id: 7
|
||||
project_id: 7
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 8
|
||||
title: testbucket8
|
||||
list_id: 8
|
||||
project_id: 8
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 9
|
||||
title: testbucket9
|
||||
list_id: 9
|
||||
project_id: 9
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 10
|
||||
title: testbucket10
|
||||
list_id: 10
|
||||
project_id: 10
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 11
|
||||
title: testbucket11
|
||||
list_id: 11
|
||||
project_id: 11
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 12
|
||||
title: testbucket13
|
||||
list_id: 12
|
||||
project_id: 12
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 13
|
||||
title: testbucket13
|
||||
list_id: 13
|
||||
project_id: 13
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 14
|
||||
title: testbucket14
|
||||
list_id: 14
|
||||
project_id: 14
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 15
|
||||
title: testbucket15
|
||||
list_id: 15
|
||||
project_id: 15
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 16
|
||||
title: testbucket16
|
||||
list_id: 16
|
||||
project_id: 16
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 17
|
||||
title: testbucket17
|
||||
list_id: 17
|
||||
project_id: 17
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 18
|
||||
title: testbucket18
|
||||
list_id: 5
|
||||
project_id: 5
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 19
|
||||
title: testbucket19
|
||||
list_id: 21
|
||||
project_id: 21
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 20
|
||||
title: testbucket20
|
||||
list_id: 22
|
||||
project_id: 22
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 21
|
||||
title: testbucket21
|
||||
list_id: 3
|
||||
project_id: 3
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
# Duplicate buckets to make deletion of one of them possible
|
||||
- id: 22
|
||||
title: testbucket22
|
||||
list_id: 6
|
||||
project_id: 6
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 23
|
||||
title: testbucket23
|
||||
list_id: 7
|
||||
project_id: 7
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 24
|
||||
title: testbucket24
|
||||
list_id: 8
|
||||
project_id: 8
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 25
|
||||
title: testbucket25
|
||||
list_id: 9
|
||||
project_id: 9
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 26
|
||||
title: testbucket26
|
||||
list_id: 10
|
||||
project_id: 10
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 27
|
||||
title: testbucket27
|
||||
list_id: 11
|
||||
project_id: 11
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 28
|
||||
title: testbucket28
|
||||
list_id: 12
|
||||
project_id: 12
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 29
|
||||
title: testbucket29
|
||||
list_id: 13
|
||||
project_id: 13
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 30
|
||||
title: testbucket30
|
||||
list_id: 14
|
||||
project_id: 14
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 31
|
||||
title: testbucket31
|
||||
list_id: 15
|
||||
project_id: 15
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 32
|
||||
title: testbucket32
|
||||
list_id: 16
|
||||
project_id: 16
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 33
|
||||
title: testbucket33
|
||||
list_id: 17
|
||||
project_id: 17
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
# This bucket is the last one in its list
|
||||
# This bucket is the last one in its project
|
||||
- id: 34
|
||||
title: testbucket34
|
||||
list_id: 18
|
||||
project_id: 18
|
||||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 35
|
||||
title: testbucket35
|
||||
list_id: 23
|
||||
project_id: 23
|
||||
created_by_id: -2
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 36
|
||||
title: testbucket36
|
||||
project_id: 33
|
||||
created_by_id: 6
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 37
|
||||
title: testbucket37
|
||||
project_id: 34
|
||||
created_by_id: 6
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- id: 1
|
||||
hash: test
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
right: 0
|
||||
sharing_type: 1
|
||||
shared_by_id: 1
|
||||
|
@ -8,7 +8,7 @@
|
|||
updated: 2018-12-02 15:13:12
|
||||
- id: 2
|
||||
hash: test2
|
||||
list_id: 2
|
||||
project_id: 2
|
||||
right: 1
|
||||
sharing_type: 1
|
||||
shared_by_id: 1
|
||||
|
@ -16,7 +16,7 @@
|
|||
updated: 2018-12-02 15:13:12
|
||||
- id: 3
|
||||
hash: test3
|
||||
list_id: 3
|
||||
project_id: 3
|
||||
right: 2
|
||||
sharing_type: 1
|
||||
shared_by_id: 1
|
||||
|
@ -24,7 +24,7 @@
|
|||
updated: 2018-12-02 15:13:12
|
||||
- id: 4
|
||||
hash: testWithPassword
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
right: 0
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
sharing_type: 2
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test1
|
||||
owner_id: 1
|
||||
namespace_id: 1
|
||||
position: 3
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -14,7 +13,6 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test2
|
||||
owner_id: 3
|
||||
namespace_id: 1
|
||||
position: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -24,7 +22,6 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test3
|
||||
owner_id: 3
|
||||
namespace_id: 2
|
||||
position: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -34,7 +31,6 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test4
|
||||
owner_id: 3
|
||||
namespace_id: 3
|
||||
position: 4
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -44,7 +40,6 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test5
|
||||
owner_id: 5
|
||||
namespace_id: 5
|
||||
position: 5
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -54,7 +49,6 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test6
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 6
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -64,7 +58,6 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test7
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 7
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -74,7 +67,6 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test8
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 8
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -84,7 +76,6 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test9
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 9
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -94,7 +85,6 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test10
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 10
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -104,7 +94,6 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test11
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 11
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -114,8 +103,8 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test12
|
||||
owner_id: 6
|
||||
namespace_id: 7
|
||||
position: 12
|
||||
parent_project_id: 27
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -124,8 +113,8 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test13
|
||||
owner_id: 6
|
||||
namespace_id: 8
|
||||
position: 13
|
||||
parent_project_id: 28
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -134,8 +123,8 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test14
|
||||
owner_id: 6
|
||||
namespace_id: 9
|
||||
position: 14
|
||||
parent_project_id: 29
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -144,8 +133,8 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test15
|
||||
owner_id: 6
|
||||
namespace_id: 10
|
||||
position: 15
|
||||
parent_project_id: 32
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -154,8 +143,8 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test16
|
||||
owner_id: 6
|
||||
namespace_id: 11
|
||||
position: 16
|
||||
parent_project_id: 33
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -164,19 +153,18 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test17
|
||||
owner_id: 6
|
||||
namespace_id: 12
|
||||
position: 17
|
||||
parent_project_id: 34
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# This list is owned by user 7, and several other users have access to it via different methods.
|
||||
# It is used to test the listUsers method.
|
||||
# This project is owned by user 7, and several other users have access to it via different methods.
|
||||
# It is used to test the projectUsers method.
|
||||
-
|
||||
id: 18
|
||||
title: Test18
|
||||
description: Lorem Ipsum
|
||||
identifier: test18
|
||||
owner_id: 7
|
||||
namespace_id: 13
|
||||
position: 18
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -186,29 +174,28 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test19
|
||||
owner_id: 7
|
||||
namespace_id: 14
|
||||
position: 19
|
||||
parent_project_id: 29
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# User 1 does not have access to this list
|
||||
# User 1 does not have access to this project
|
||||
-
|
||||
id: 20
|
||||
title: Test20
|
||||
description: Lorem Ipsum
|
||||
identifier: test20
|
||||
owner_id: 13
|
||||
namespace_id: 15
|
||||
position: 20
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 21
|
||||
title: Test21 archived through namespace
|
||||
title: Test21 archived through parent list
|
||||
description: Lorem Ipsum
|
||||
identifier: test21
|
||||
owner_id: 1
|
||||
namespace_id: 16
|
||||
position: 21
|
||||
parent_project_id: 22
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -217,7 +204,6 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test22
|
||||
owner_id: 1
|
||||
namespace_id: 1
|
||||
is_archived: 1
|
||||
position: 22
|
||||
updated: 2018-12-02 15:13:12
|
||||
|
@ -228,7 +214,6 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test23
|
||||
owner_id: 12
|
||||
namespace_id: 17
|
||||
position: 23
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -238,7 +223,76 @@
|
|||
description: Lorem Ipsum
|
||||
identifier: test6
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 7
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 25
|
||||
title: Test25
|
||||
owner_id: 6
|
||||
parent_project_id: 12
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 26
|
||||
title: Test26
|
||||
owner_id: 6
|
||||
parent_project_id: 25
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 27
|
||||
title: Test27
|
||||
owner_id: 6
|
||||
position: 2700
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 28
|
||||
title: Test28
|
||||
owner_id: 6
|
||||
position: 2800
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 29
|
||||
title: Test29
|
||||
owner_id: 6
|
||||
position: 2900
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 30
|
||||
title: Test30
|
||||
owner_id: 6
|
||||
position: 3000
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 31
|
||||
title: Test31
|
||||
owner_id: 6
|
||||
position: 3100
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 32
|
||||
title: Test32
|
||||
owner_id: 6
|
||||
position: 3200
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 33
|
||||
title: Test33
|
||||
owner_id: 6
|
||||
position: 3300
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 34
|
||||
title: Test34
|
||||
owner_id: 6
|
||||
position: 3400
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
|
@ -3,28 +3,18 @@
|
|||
entity_id: 2
|
||||
user_id: 1
|
||||
created: 2021-02-01 15:13:12
|
||||
- id: 2
|
||||
entity_type: 1 # Namespace
|
||||
entity_id: 6
|
||||
user_id: 6
|
||||
created: 2021-02-01 15:13:12
|
||||
- id: 3
|
||||
entity_type: 2 # List
|
||||
entity_id: 12 # belongs to namespace 7
|
||||
entity_type: 2 # project
|
||||
entity_id: 12 # belongs to parent project 7
|
||||
user_id: 6
|
||||
created: 2021-02-01 15:13:12
|
||||
- id: 4
|
||||
entity_type: 3 # Task
|
||||
entity_id: 22 # belongs to list 13 which belongs to namespace 8
|
||||
user_id: 6
|
||||
created: 2021-02-01 15:13:12
|
||||
- id: 5
|
||||
entity_type: 1 # Namespace
|
||||
entity_id: 8
|
||||
entity_id: 22 # belongs to project 13
|
||||
user_id: 6
|
||||
created: 2021-02-01 15:13:12
|
||||
- id: 6
|
||||
entity_type: 2 # List
|
||||
entity_type: 2 # project
|
||||
entity_id: 13
|
||||
user_id: 6
|
||||
created: 2021-02-01 15:13:12
|
||||
|
@ -33,3 +23,8 @@
|
|||
entity_id: 26
|
||||
user_id: 6
|
||||
created: 2021-02-01 15:13:12
|
||||
- id: 8
|
||||
entity_type: 2 # Project
|
||||
entity_id: 32
|
||||
user_id: 6
|
||||
created: 2021-02-01 15:13:12
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
description: 'Lorem Ipsum'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 1
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
|
@ -13,7 +13,7 @@
|
|||
title: 'task #2 done'
|
||||
done: true
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 2
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
|
@ -23,7 +23,7 @@
|
|||
title: 'task #3 high prio'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 3
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
|
@ -33,7 +33,7 @@
|
|||
title: 'task #4 low prio'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 4
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
|
@ -43,7 +43,7 @@
|
|||
title: 'task #5 higher due date'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 5
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
|
@ -53,7 +53,7 @@
|
|||
title: 'task #6 lower due date'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 6
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
|
@ -63,7 +63,7 @@
|
|||
title: 'task #7 with start date'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 7
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
|
@ -73,7 +73,7 @@
|
|||
title: 'task #8 with end date'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 8
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
|
@ -83,7 +83,7 @@
|
|||
title: 'task #9 with start and end date'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 9
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
|
@ -94,7 +94,7 @@
|
|||
title: 'task #10 basic'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 10
|
||||
bucket_id: 1
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -103,7 +103,7 @@
|
|||
title: 'task #11 basic'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 11
|
||||
bucket_id: 1
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -112,25 +112,25 @@
|
|||
title: 'task #12 basic'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 12
|
||||
bucket_id: 1
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
- id: 13
|
||||
title: 'task #13 basic other list'
|
||||
title: 'task #13 basic other project'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 2
|
||||
project_id: 2
|
||||
index: 1
|
||||
bucket_id: 4
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
- id: 14
|
||||
title: 'task #14 basic other list'
|
||||
title: 'task #14 basic other project'
|
||||
done: false
|
||||
created_by_id: 5
|
||||
list_id: 5
|
||||
project_id: 5
|
||||
index: 1
|
||||
bucket_id: 18
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -139,7 +139,7 @@
|
|||
title: 'task #15'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 6
|
||||
project_id: 6
|
||||
index: 1
|
||||
bucket_id: 6
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -148,7 +148,7 @@
|
|||
title: 'task #16'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 7
|
||||
project_id: 7
|
||||
index: 1
|
||||
bucket_id: 7
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -157,7 +157,7 @@
|
|||
title: 'task #17'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 8
|
||||
project_id: 8
|
||||
index: 1
|
||||
bucket_id: 8
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -166,7 +166,7 @@
|
|||
title: 'task #18'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 9
|
||||
project_id: 9
|
||||
index: 1
|
||||
bucket_id: 9
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -175,7 +175,7 @@
|
|||
title: 'task #19'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 10
|
||||
project_id: 10
|
||||
index: 1
|
||||
bucket_id: 10
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -184,7 +184,7 @@
|
|||
title: 'task #20'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 11
|
||||
project_id: 11
|
||||
index: 1
|
||||
bucket_id: 11
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -193,7 +193,7 @@
|
|||
title: 'task #21'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 12
|
||||
project_id: 32
|
||||
index: 1
|
||||
bucket_id: 12
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -202,25 +202,25 @@
|
|||
title: 'task #22'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 13
|
||||
project_id: 33
|
||||
index: 1
|
||||
bucket_id: 13
|
||||
bucket_id: 36
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
- id: 23
|
||||
title: 'task #23'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 14
|
||||
project_id: 34
|
||||
index: 1
|
||||
bucket_id: 14
|
||||
bucket_id: 37
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
- id: 24
|
||||
title: 'task #24'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 15
|
||||
project_id: 15
|
||||
index: 1
|
||||
bucket_id: 15
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -229,7 +229,7 @@
|
|||
title: 'task #25'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 16
|
||||
project_id: 16
|
||||
index: 1
|
||||
bucket_id: 16
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -238,7 +238,7 @@
|
|||
title: 'task #26'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 17
|
||||
project_id: 17
|
||||
index: 1
|
||||
bucket_id: 17
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -247,7 +247,7 @@
|
|||
title: 'task #27 with reminders'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 12
|
||||
bucket_id: 1
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -257,7 +257,7 @@
|
|||
done: false
|
||||
created_by_id: 1
|
||||
repeat_after: 3600
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 13
|
||||
bucket_id: 1
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -266,7 +266,7 @@
|
|||
title: 'task #29 with parent task (1)'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 14
|
||||
bucket_id: 1
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -275,7 +275,7 @@
|
|||
title: 'task #30 with assignees'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 15
|
||||
bucket_id: 1
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -284,7 +284,7 @@
|
|||
title: 'task #31 with color'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 16
|
||||
hex_color: f0f0f0
|
||||
bucket_id: 1
|
||||
|
@ -294,7 +294,7 @@
|
|||
title: 'task #32'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 3
|
||||
project_id: 3
|
||||
index: 1
|
||||
bucket_id: 21
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -303,7 +303,7 @@
|
|||
title: 'task #33 with percent done'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
project_id: 1
|
||||
index: 17
|
||||
percent_done: 0.5
|
||||
bucket_id: 1
|
||||
|
@ -314,7 +314,7 @@
|
|||
title: 'task #34'
|
||||
done: false
|
||||
created_by_id: 13
|
||||
list_id: 20
|
||||
project_id: 20
|
||||
index: 20
|
||||
bucket_id: 5
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -323,7 +323,7 @@
|
|||
title: 'task #35'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 21
|
||||
project_id: 21
|
||||
index: 1
|
||||
bucket_id: 19
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -332,7 +332,7 @@
|
|||
title: 'task #36'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 22
|
||||
project_id: 22
|
||||
index: 1
|
||||
bucket_id: 20
|
||||
created: 2018-12-01 01:12:04
|
||||
|
@ -342,7 +342,7 @@
|
|||
title: 'task #37'
|
||||
done: false
|
||||
created_by_id: -2
|
||||
list_id: 2
|
||||
project_id: 2
|
||||
index: 2
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
|
@ -350,8 +350,14 @@
|
|||
title: 'task #37 done with due date'
|
||||
done: true
|
||||
created_by_id: 1
|
||||
list_id: 22
|
||||
project_id: 22
|
||||
index: 2
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
due_date: 2018-10-30 22:25:24
|
||||
- id: 39
|
||||
title: 'task #39'
|
||||
created_by_id: 1
|
||||
project_id: 25
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
- id: 1
|
||||
team_id: 1
|
||||
list_id: 3
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# This team has read only access on list 6
|
||||
- id: 2
|
||||
team_id: 2
|
||||
list_id: 6
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# This team has write access on list 7
|
||||
- id: 3
|
||||
team_id: 3
|
||||
list_id: 7
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# This team has admin access on list 8
|
||||
- id: 4
|
||||
team_id: 4
|
||||
list_id: 8
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# Readonly acces on list 19
|
||||
- id: 5
|
||||
team_id: 8
|
||||
list_id: 19
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# Write acces on list 19
|
||||
- id: 6
|
||||
team_id: 9
|
||||
list_id: 19
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# Admin acces on list 19
|
||||
- id: 7
|
||||
team_id: 10
|
||||
list_id: 19
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 8
|
||||
team_id: 1
|
||||
list_id: 21
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
|
@ -1,52 +0,0 @@
|
|||
- id: 1
|
||||
team_id: 1
|
||||
namespace_id: 3
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
||||
- id: 2
|
||||
team_id: 2
|
||||
namespace_id: 3
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
||||
- id: 3
|
||||
team_id: 5
|
||||
namespace_id: 7
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
||||
- id: 4
|
||||
team_id: 6
|
||||
namespace_id: 8
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
||||
- id: 5
|
||||
team_id: 7
|
||||
namespace_id: 9
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 6
|
||||
team_id: 11
|
||||
namespace_id: 14
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 7
|
||||
team_id: 12
|
||||
namespace_id: 14
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 8
|
||||
team_id: 13
|
||||
namespace_id: 14
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
|
@ -0,0 +1,96 @@
|
|||
- id: 1
|
||||
team_id: 1
|
||||
project_id: 3
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# This team has read only access on project 6
|
||||
- id: 2
|
||||
team_id: 2
|
||||
project_id: 6
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# This team has write access on project 7
|
||||
- id: 3
|
||||
team_id: 3
|
||||
project_id: 7
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# This team has admin access on project 8
|
||||
- id: 4
|
||||
team_id: 4
|
||||
project_id: 8
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# Readonly acces on project 19
|
||||
- id: 5
|
||||
team_id: 8
|
||||
project_id: 19
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# Write acces on project 19
|
||||
- id: 6
|
||||
team_id: 9
|
||||
project_id: 19
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# Admin acces on project 19
|
||||
- id: 7
|
||||
team_id: 10
|
||||
project_id: 19
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 8
|
||||
team_id: 1
|
||||
project_id: 21
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 9
|
||||
team_id: 1
|
||||
project_id: 28
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 10
|
||||
team_id: 11
|
||||
project_id: 29
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 11
|
||||
team_id: 12
|
||||
project_id: 29
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 12
|
||||
team_id: 13
|
||||
project_id: 29
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 13
|
||||
team_id: 1
|
||||
project_id: 32
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 14
|
||||
team_id: 1
|
||||
project_id: 33
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 15
|
||||
team_id: 1
|
||||
project_id: 34
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
|
@ -3,22 +3,13 @@
|
|||
description: Lorem Ipsum
|
||||
created_by_id: 1
|
||||
- id: 2
|
||||
name: testteam2_read_only_on_list6
|
||||
name: testteam2_read_only_on_project6
|
||||
created_by_id: 1
|
||||
- id: 3
|
||||
name: testteam3_write_on_list7
|
||||
name: testteam3_write_on_project7
|
||||
created_by_id: 1
|
||||
- id: 4
|
||||
name: testteam4_admin_on_list8
|
||||
created_by_id: 1
|
||||
- id: 5
|
||||
name: testteam2_read_only_on_namespace7
|
||||
created_by_id: 1
|
||||
- id: 6
|
||||
name: testteam3_write_on_namespace8
|
||||
created_by_id: 1
|
||||
- id: 7
|
||||
name: testteam4_admin_on_namespace9
|
||||
name: testteam4_admin_on_project8
|
||||
created_by_id: 1
|
||||
- id: 8
|
||||
name: testteam8
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# This use is used to create a whole bunch of lists which are then shared directly with a user
|
||||
# This use is used to create a whole bunch of projects which are then shared directly with a user
|
||||
- id: 6
|
||||
username: 'user6'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
- id: 1
|
||||
user_id: 1
|
||||
list_id: 3
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 2
|
||||
user_id: 2
|
||||
list_id: 3
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 3
|
||||
user_id: 1
|
||||
list_id: 9
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 4
|
||||
user_id: 1
|
||||
list_id: 10
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 5
|
||||
user_id: 1
|
||||
list_id: 11
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 6
|
||||
user_id: 4
|
||||
list_id: 19
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 7
|
||||
user_id: 5
|
||||
list_id: 19
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 8
|
||||
user_id: 6
|
||||
list_id: 19
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
|
@ -1,52 +0,0 @@
|
|||
- id: 1
|
||||
user_id: 1
|
||||
namespace_id: 3
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
||||
- id: 2
|
||||
user_id: 2
|
||||
namespace_id: 3
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
||||
- id: 3
|
||||
user_id: 1
|
||||
namespace_id: 10
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
||||
- id: 4
|
||||
user_id: 1
|
||||
namespace_id: 11
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
||||
- id: 5
|
||||
user_id: 1
|
||||
namespace_id: 12
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 6
|
||||
user_id: 11
|
||||
namespace_id: 14
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 7
|
||||
user_id: 12
|
||||
namespace_id: 14
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 8
|
||||
user_id: 13
|
||||
namespace_id: 14
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
|
@ -1,90 +1,96 @@
|
|||
- id: 1
|
||||
title: testnamespace
|
||||
description: Lorem Ipsum
|
||||
owner_id: 1
|
||||
user_id: 1
|
||||
project_id: 3
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 2
|
||||
title: testnamespace2
|
||||
description: Lorem Ipsum
|
||||
owner_id: 2
|
||||
user_id: 2
|
||||
project_id: 3
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 3
|
||||
title: testnamespace3
|
||||
description: Lorem Ipsum
|
||||
owner_id: 3
|
||||
user_id: 1
|
||||
project_id: 9
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 4
|
||||
user_id: 1
|
||||
project_id: 10
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 5
|
||||
user_id: 1
|
||||
project_id: 11
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 6
|
||||
title: testnamespace6
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
user_id: 4
|
||||
project_id: 19
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 7
|
||||
title: testnamespace7
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
user_id: 5
|
||||
project_id: 19
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 8
|
||||
title: testnamespace8
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
user_id: 6
|
||||
project_id: 19
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 9
|
||||
title: testnamespace9
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
user_id: 1
|
||||
project_id: 27
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 10
|
||||
title: testnamespace10
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
user_id: 11
|
||||
project_id: 29
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 11
|
||||
title: testnamespace11
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
user_id: 12
|
||||
project_id: 29
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 12
|
||||
title: testnamespace12
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
user_id: 13
|
||||
project_id: 29
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 13
|
||||
title: testnamespace13
|
||||
description: Lorem Ipsum
|
||||
owner_id: 7
|
||||
user_id: 1
|
||||
project_id: 30
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 14
|
||||
title: testnamespace14
|
||||
description: Lorem Ipsum
|
||||
owner_id: 7
|
||||
user_id: 1
|
||||
project_id: 31
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 15
|
||||
title: testnamespace15
|
||||
description: Lorem Ipsum
|
||||
owner_id: 13
|
||||
user_id: 1
|
||||
project_id: 28
|
||||
right: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 16
|
||||
title: Archived testnamespace16
|
||||
owner_id: 1
|
||||
is_archived: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 17
|
||||
title: testnamespace17
|
||||
description: Lorem Ipsum
|
||||
owner_id: 12
|
||||
user_id: 1
|
||||
project_id: 29
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
|
@ -17,6 +17,7 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
@ -92,7 +93,16 @@ func AssertExists(t *testing.T, table string, values map[string]interface{}, cus
|
|||
exists, err = x.Table(table).Where(values).Get(&v)
|
||||
}
|
||||
assert.NoError(t, err, fmt.Sprintf("Failed to assert entries exist in db, error was: %s", err))
|
||||
assert.True(t, exists, fmt.Sprintf("Entries %v do not exist in table %s", values, table))
|
||||
if !exists {
|
||||
|
||||
all := []map[string]interface{}{}
|
||||
err = x.Table(table).Find(&all)
|
||||
assert.NoError(t, err, fmt.Sprintf("Failed to assert entries exist in db, error was: %s", err))
|
||||
pretty, err := json.MarshalIndent(all, "", " ")
|
||||
assert.NoError(t, err, fmt.Sprintf("Failed to assert entries exist in db, error was: %s", err))
|
||||
|
||||
t.Errorf(fmt.Sprintf("Entries %v do not exist in table %s\n\nFound entries instead: %v", values, table, string(pretty)))
|
||||
}
|
||||
}
|
||||
|
||||
// AssertMissing checks and asserts the nonexiste nce of certain entries in the db
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
package integrations
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
|
@ -26,46 +25,34 @@ import (
|
|||
)
|
||||
|
||||
// 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.
|
||||
// 2. A project which belongs to an archived project cannot be edited.
|
||||
// 3. An archived project 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.
|
||||
// 4. It is not possible to un-archive a project individually if its parent project is archived.
|
||||
// 5. Creating new child projects in an archived project should not work.
|
||||
// 6. Creating new tasks on an archived project should not work.
|
||||
// 7. Creating new tasks on a project whose parent project is archived should not work.
|
||||
// 8. Editing tasks on an archived project should not work.
|
||||
// 9. Editing tasks on a project whose parent project is archived should not work.
|
||||
// 11. Archived projects should not appear in the list with all projects.
|
||||
// 12. Projects whose parent project is archived should not appear in the project with all projects.
|
||||
//
|
||||
// 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.
|
||||
// Maybe the inheritance of projects from parents could be solved with some kind of is_archived_inherited flag -
|
||||
// that way I'd only need to implement the checking on a project level and update the flag for all projects once the
|
||||
// project is archived. The archived flag would then be used to not accedentially unarchive projects which were
|
||||
// already individually archived when the parent project was archived.
|
||||
//
|
||||
// Namespace 16 is archived
|
||||
// List 21 belongs to namespace 16
|
||||
// List 22 is archived individually
|
||||
// Project 21 belongs to project 16
|
||||
// Project 22 is archived individually
|
||||
|
||||
func TestArchived(t *testing.T) {
|
||||
testListHandler := webHandlerTest{
|
||||
testProjectHandler := webHandlerTest{
|
||||
user: &testuser1,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.List{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
testNamespaceHandler := webHandlerTest{
|
||||
user: &testuser1,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.Namespace{}
|
||||
return &models.Project{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
|
@ -105,134 +92,103 @@ func TestArchived(t *testing.T) {
|
|||
t: t,
|
||||
}
|
||||
|
||||
t.Run("namespace", 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{"projecttask": taskID}, `{"title":"TestIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, errCode)
|
||||
})
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
_, err := testTaskHandler.testDeleteWithUser(nil, map[string]string{"projecttask": 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{"projecttask": 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{"projecttask": 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{"projecttask": 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{"projecttask": 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 project belongs to an archived parent project
|
||||
t.Run("archived parent project", func(t *testing.T) {
|
||||
t.Run("not editable", func(t *testing.T) {
|
||||
_, err := testNamespaceHandler.testUpdateWithUser(nil, map[string]string{"namespace": "16"}, `{"title":"TestIpsum","is_archived":true}`)
|
||||
_, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "21"}, `{"title":"TestIpsum","is_archived":true}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
|
||||
})
|
||||
t.Run("no new tasks", func(t *testing.T) {
|
||||
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"project": "21"}, `{"title":"Lorem"}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
|
||||
})
|
||||
t.Run("not unarchivable", func(t *testing.T) {
|
||||
_, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "21"}, `{"title":"LoremIpsum","is_archived":false}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
|
||||
})
|
||||
|
||||
taskTests("35", models.ErrCodeProjectIsArchived, t)
|
||||
})
|
||||
// The project itself is archived
|
||||
t.Run("archived individually", func(t *testing.T) {
|
||||
t.Run("not editable", func(t *testing.T) {
|
||||
_, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "22"}, `{"title":"TestIpsum","is_archived":true}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
|
||||
})
|
||||
t.Run("no new tasks", func(t *testing.T) {
|
||||
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"project": "22"}, `{"title":"Lorem"}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
|
||||
})
|
||||
t.Run("unarchivable", func(t *testing.T) {
|
||||
rec, err := testNamespaceHandler.testUpdateWithUser(nil, map[string]string{"namespace": "16"}, `{"title":"TestIpsum","is_archived":false}`)
|
||||
rec, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "22"}, `{"title":"LoremIpsum","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(), `"title":"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(), `"title":"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}, `{"title":"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"}, `{"title":"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"}, `{"title":"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,"namespace_id":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"is_archived":false`)
|
||||
})
|
||||
|
||||
taskTests("36", models.ErrCodeListIsArchived, t)
|
||||
})
|
||||
taskTests("36", models.ErrCodeProjectIsArchived, t)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ func TestBucket(t *testing.T) {
|
|||
linkShare: &models.LinkSharing{
|
||||
ID: 2,
|
||||
Hash: "test2",
|
||||
ListID: 2,
|
||||
ProjectID: 2,
|
||||
Right: models.RightWrite,
|
||||
SharingType: models.SharingTypeWithoutPassword,
|
||||
SharedByID: 1,
|
||||
|
@ -51,17 +51,17 @@ func TestBucket(t *testing.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"})
|
||||
rec, err := testHandler.testReadAllWithUser(nil, map[string]string{"project": "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
|
||||
assert.NotContains(t, rec.Body.String(), `testbucket4`) // Different Project
|
||||
})
|
||||
})
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
// Check the list was loaded successfully afterwards, see testReadOneWithUser
|
||||
// Check the project 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"`)
|
||||
|
@ -115,33 +115,33 @@ func TestBucket(t *testing.T) {
|
|||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project User 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) {
|
||||
t.Run("Shared Via Parent Project User 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) {
|
||||
t.Run("Shared Via Parent Project User 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) {
|
||||
t.Run("Shared Via Parent Project Team 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) {
|
||||
t.Run("Shared Via Parent Project Team 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) {
|
||||
t.Run("Shared Via Parent Project Team 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"`)
|
||||
|
@ -150,7 +150,7 @@ func TestBucket(t *testing.T) {
|
|||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "1", "bucket": "1"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "1", "bucket": "1"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||
})
|
||||
|
@ -162,70 +162,70 @@ func TestBucket(t *testing.T) {
|
|||
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{"list": "20", "bucket": "5"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "20", "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{"list": "6", "bucket": "6"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "6", "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{"list": "7", "bucket": "7"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "7", "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{"list": "8", "bucket": "8"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "8", "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{"list": "9", "bucket": "9"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "9", "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{"list": "10", "bucket": "10"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "10", "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{"list": "11", "bucket": "11"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "11", "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{"list": "12", "bucket": "12"})
|
||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "12", "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{"list": "13", "bucket": "13"})
|
||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "13", "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{"list": "14", "bucket": "14"})
|
||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "14", "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{"list": "15", "bucket": "15"})
|
||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "15", "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{"list": "16", "bucket": "16"})
|
||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "16", "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{"list": "17", "bucket": "17"})
|
||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "17", "bucket": "17"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||
})
|
||||
|
@ -233,92 +233,92 @@ func TestBucket(t *testing.T) {
|
|||
})
|
||||
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"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9999"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
|
||||
})
|
||||
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"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "17"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
})
|
||||
t.Run("Link Share", func(t *testing.T) {
|
||||
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
db.AssertExists(t, "buckets", map[string]interface{}{
|
||||
"list_id": 2,
|
||||
"project_id": 2,
|
||||
"created_by_id": -2,
|
||||
"title": "Lorem Ipsum",
|
||||
}, false)
|
||||
|
|
|
@ -31,14 +31,14 @@ func TestLinkSharingAuth(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Contains(t, rec.Body.String(), `"token":"`)
|
||||
assert.Contains(t, rec.Body.String(), `"list_id":1`)
|
||||
assert.Contains(t, rec.Body.String(), `"project_id":1`)
|
||||
})
|
||||
t.Run("Without Password, Password Provided", func(t *testing.T) {
|
||||
rec, err := newTestRequest(t, http.MethodPost, apiv1.AuthenticateLinkShare, `{"password":"somethingsomething"}`, nil, map[string]string{"share": "test"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Contains(t, rec.Body.String(), `"token":"`)
|
||||
assert.Contains(t, rec.Body.String(), `"list_id":1`)
|
||||
assert.Contains(t, rec.Body.String(), `"project_id":1`)
|
||||
})
|
||||
t.Run("With Password, No Password Provided", func(t *testing.T) {
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.AuthenticateLinkShare, ``, nil, map[string]string{"share": "testWithPassword"})
|
||||
|
@ -50,7 +50,7 @@ func TestLinkSharingAuth(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Contains(t, rec.Body.String(), `"token":"`)
|
||||
assert.Contains(t, rec.Body.String(), `"list_id":1`)
|
||||
assert.Contains(t, rec.Body.String(), `"project_id":1`)
|
||||
})
|
||||
t.Run("With Wrong Password", func(t *testing.T) {
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.AuthenticateLinkShare, `{"password":"someWrongPassword"}`, nil, map[string]string{"share": "testWithPassword"})
|
||||
|
|
|
@ -31,7 +31,7 @@ func TestLinkSharing(t *testing.T) {
|
|||
linkshareRead := &models.LinkSharing{
|
||||
ID: 1,
|
||||
Hash: "test1",
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
Right: models.RightRead,
|
||||
SharingType: models.SharingTypeWithoutPassword,
|
||||
SharedByID: 1,
|
||||
|
@ -40,7 +40,7 @@ func TestLinkSharing(t *testing.T) {
|
|||
linkShareWrite := &models.LinkSharing{
|
||||
ID: 2,
|
||||
Hash: "test2",
|
||||
ListID: 2,
|
||||
ProjectID: 2,
|
||||
Right: models.RightWrite,
|
||||
SharingType: models.SharingTypeWithoutPassword,
|
||||
SharedByID: 1,
|
||||
|
@ -49,7 +49,7 @@ func TestLinkSharing(t *testing.T) {
|
|||
linkShareAdmin := &models.LinkSharing{
|
||||
ID: 3,
|
||||
Hash: "test3",
|
||||
ListID: 3,
|
||||
ProjectID: 3,
|
||||
Right: models.RightAdmin,
|
||||
SharingType: models.SharingTypeWithoutPassword,
|
||||
SharedByID: 1,
|
||||
|
@ -65,102 +65,102 @@ func TestLinkSharing(t *testing.T) {
|
|||
}
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
t.Run("read only", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"right":0}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "20"}, `{"right":0}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("write", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"right":1}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "20"}, `{"right":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("admin", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"right":2}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "20"}, `{"right":2}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
t.Run("Read only access", func(t *testing.T) {
|
||||
t.Run("read only", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"right":0}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9"}, `{"right":0}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("write", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"right":1}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9"}, `{"right":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("admin", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"right":2}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9"}, `{"right":2}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
t.Run("Write access", func(t *testing.T) {
|
||||
t.Run("read only", func(t *testing.T) {
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"right":0}`)
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "10"}, `{"right":0}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, req.Body.String(), `"hash":`)
|
||||
})
|
||||
t.Run("write", func(t *testing.T) {
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"right":1}`)
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "10"}, `{"right":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, req.Body.String(), `"hash":`)
|
||||
})
|
||||
t.Run("admin", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"right":2}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "10"}, `{"right":2}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
t.Run("Admin access", func(t *testing.T) {
|
||||
t.Run("read only", func(t *testing.T) {
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"right":0}`)
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "11"}, `{"right":0}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, req.Body.String(), `"hash":`)
|
||||
})
|
||||
t.Run("write", func(t *testing.T) {
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"right":1}`)
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "11"}, `{"right":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, req.Body.String(), `"hash":`)
|
||||
})
|
||||
t.Run("admin", func(t *testing.T) {
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"right":2}`)
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "11"}, `{"right":2}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, req.Body.String(), `"hash":`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Lists", func(t *testing.T) {
|
||||
testHandlerListReadOnly := webHandlerTest{
|
||||
t.Run("Projects", func(t *testing.T) {
|
||||
testHandlerProjectReadOnly := webHandlerTest{
|
||||
linkShare: linkshareRead,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.List{}
|
||||
return &models.Project{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerListWrite := webHandlerTest{
|
||||
testHandlerProjectWrite := webHandlerTest{
|
||||
linkShare: linkShareWrite,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.List{}
|
||||
return &models.Project{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerListAdmin := webHandlerTest{
|
||||
testHandlerProjectAdmin := webHandlerTest{
|
||||
linkShare: linkShareAdmin,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.List{}
|
||||
return &models.Project{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
|
||||
t.Run("ReadAll", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
rec, err := testHandlerListReadOnly.testReadAllWithLinkShare(nil, nil)
|
||||
rec, err := testHandlerProjectReadOnly.testReadAllWithLinkShare(nil, nil)
|
||||
assert.NoError(t, err)
|
||||
// Should only return the shared list, nothing else
|
||||
// Should only return the shared project, nothing else
|
||||
assert.Contains(t, rec.Body.String(), `Test1`)
|
||||
assert.NotContains(t, rec.Body.String(), `Test2`)
|
||||
assert.NotContains(t, rec.Body.String(), `Test3`)
|
||||
|
@ -168,9 +168,9 @@ func TestLinkSharing(t *testing.T) {
|
|||
assert.NotContains(t, rec.Body.String(), `Test5`)
|
||||
})
|
||||
t.Run("Search", func(t *testing.T) {
|
||||
rec, err := testHandlerListReadOnly.testReadAllWithLinkShare(url.Values{"s": []string{"est1"}}, nil)
|
||||
rec, err := testHandlerProjectReadOnly.testReadAllWithLinkShare(url.Values{"s": []string{"est1"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
// Should only return the shared list, nothing else
|
||||
// Should only return the shared project, nothing else
|
||||
assert.Contains(t, rec.Body.String(), `Test1`)
|
||||
assert.NotContains(t, rec.Body.String(), `Test2`)
|
||||
assert.NotContains(t, rec.Body.String(), `Test3`)
|
||||
|
@ -180,35 +180,35 @@ func TestLinkSharing(t *testing.T) {
|
|||
})
|
||||
t.Run("ReadOne", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
rec, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "1"})
|
||||
rec, err := testHandlerProjectReadOnly.testReadOneWithLinkShare(nil, map[string]string{"project": "1"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test1"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"title":"Test2"`)
|
||||
})
|
||||
t.Run("Nonexisting", func(t *testing.T) {
|
||||
_, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "9999999"})
|
||||
_, err := testHandlerProjectReadOnly.testReadOneWithLinkShare(nil, map[string]string{"project": "9999999"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
|
||||
})
|
||||
t.Run("Rights check", func(t *testing.T) {
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
// List 2, not shared with this token
|
||||
_, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "2"})
|
||||
// Project 2, not shared with this token
|
||||
_, err := testHandlerProjectReadOnly.testReadOneWithLinkShare(nil, map[string]string{"project": "2"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `You don't have the right to see this`)
|
||||
})
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
rec, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "1"})
|
||||
rec, err := testHandlerProjectReadOnly.testReadOneWithLinkShare(nil, map[string]string{"project": "1"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test1"`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
rec, err := testHandlerListWrite.testReadOneWithLinkShare(nil, map[string]string{"list": "2"})
|
||||
rec, err := testHandlerProjectWrite.testReadOneWithLinkShare(nil, map[string]string{"project": "2"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test2"`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
rec, err := testHandlerListAdmin.testReadOneWithLinkShare(nil, map[string]string{"list": "3"})
|
||||
rec, err := testHandlerProjectAdmin.testReadOneWithLinkShare(nil, map[string]string{"project": "3"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test3"`)
|
||||
})
|
||||
|
@ -216,28 +216,28 @@ func TestLinkSharing(t *testing.T) {
|
|||
})
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Nonexisting", func(t *testing.T) {
|
||||
_, err := testHandlerListReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "9999999"}, `{"title":"TestLoremIpsum"}`)
|
||||
_, err := testHandlerProjectReadOnly.testUpdateWithLinkShare(nil, map[string]string{"project": "9999999"}, `{"title":"TestLoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
|
||||
})
|
||||
t.Run("Rights check", func(t *testing.T) {
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
_, err := testHandlerListReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"TestLoremIpsum"}`)
|
||||
_, err := testHandlerProjectReadOnly.testUpdateWithLinkShare(nil, map[string]string{"project": "2"}, `{"title":"TestLoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerListReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum"}`)
|
||||
_, err := testHandlerProjectReadOnly.testUpdateWithLinkShare(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
rec, err := testHandlerListWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"TestLoremIpsum","namespace_id":1}`)
|
||||
rec, err := testHandlerProjectWrite.testUpdateWithLinkShare(nil, map[string]string{"project": "2"}, `{"title":"TestLoremIpsum","namespace_id":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
rec, err := testHandlerListAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"title":"TestLoremIpsum","namespace_id":2}`)
|
||||
rec, err := testHandlerProjectAdmin.testUpdateWithLinkShare(nil, map[string]string{"project": "3"}, `{"title":"TestLoremIpsum","namespace_id":2}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
|
@ -245,54 +245,54 @@ func TestLinkSharing(t *testing.T) {
|
|||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("Nonexisting", func(t *testing.T) {
|
||||
_, err := testHandlerListReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "9999999"})
|
||||
_, err := testHandlerProjectReadOnly.testDeleteWithLinkShare(nil, map[string]string{"project": "9999999"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
|
||||
})
|
||||
t.Run("Rights check", func(t *testing.T) {
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
_, err := testHandlerListReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"})
|
||||
_, err := testHandlerProjectReadOnly.testDeleteWithLinkShare(nil, map[string]string{"project": "1"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerListReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"})
|
||||
_, err := testHandlerProjectReadOnly.testDeleteWithLinkShare(nil, map[string]string{"project": "1"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerListWrite.testDeleteWithLinkShare(nil, map[string]string{"list": "2"})
|
||||
_, err := testHandlerProjectWrite.testDeleteWithLinkShare(nil, map[string]string{"project": "2"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
rec, err := testHandlerListAdmin.testDeleteWithLinkShare(nil, map[string]string{"list": "3"})
|
||||
rec, err := testHandlerProjectAdmin.testDeleteWithLinkShare(nil, map[string]string{"project": "3"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Creating a list should always be forbidden, since users need access to a namespace to create a list
|
||||
// Creating a project should always be forbidden
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Run("Nonexisting", func(t *testing.T) {
|
||||
_, err := testHandlerListReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "999999"}, `{"title":"Lorem"}`)
|
||||
_, err := testHandlerProjectReadOnly.testCreateWithLinkShare(nil, nil, `{"title":"Lorem"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Rights check", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerListReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
|
||||
_, err := testHandlerProjectReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerListWrite.testCreateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
|
||||
_, err := testHandlerProjectWrite.testCreateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerListAdmin.testCreateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
|
||||
_, err := testHandlerProjectAdmin.testCreateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
@ -301,74 +301,74 @@ func TestLinkSharing(t *testing.T) {
|
|||
|
||||
t.Run("Right Management", func(t *testing.T) {
|
||||
t.Run("Users", func(t *testing.T) {
|
||||
testHandlerListUserReadOnly := webHandlerTest{
|
||||
testHandlerProjectUserReadOnly := webHandlerTest{
|
||||
linkShare: linkshareRead,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.ListUser{}
|
||||
return &models.ProjectUser{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerListUserWrite := webHandlerTest{
|
||||
testHandlerProjectUserWrite := webHandlerTest{
|
||||
linkShare: linkShareWrite,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.ListUser{}
|
||||
return &models.ProjectUser{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerListUserAdmin := webHandlerTest{
|
||||
testHandlerProjectUserAdmin := webHandlerTest{
|
||||
linkShare: linkShareAdmin,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.ListUser{}
|
||||
return &models.ProjectUser{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
t.Run("ReadAll", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
rec, err := testHandlerListUserReadOnly.testReadAllWithLinkShare(nil, map[string]string{"list": "1"})
|
||||
rec, err := testHandlerProjectUserReadOnly.testReadAllWithLinkShare(nil, map[string]string{"project": "1"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[]`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
rec, err := testHandlerListUserWrite.testReadAllWithLinkShare(nil, map[string]string{"list": "2"})
|
||||
rec, err := testHandlerProjectUserWrite.testReadAllWithLinkShare(nil, map[string]string{"project": "2"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[]`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
rec, err := testHandlerListUserAdmin.testReadAllWithLinkShare(nil, map[string]string{"list": "3"})
|
||||
rec, err := testHandlerProjectUserAdmin.testReadAllWithLinkShare(nil, map[string]string{"project": "3"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"username":"user1"`)
|
||||
})
|
||||
})
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerListUserReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"user_id":"user1"}`)
|
||||
_, err := testHandlerProjectUserReadOnly.testCreateWithLinkShare(nil, map[string]string{"project": "1"}, `{"user_id":"user1"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerListUserWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"user_id":"user1"}`)
|
||||
_, err := testHandlerProjectUserWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{"user_id":"user1"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerListUserAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"user_id":"user1"}`)
|
||||
_, err := testHandlerProjectUserAdmin.testCreateWithLinkShare(nil, map[string]string{"project": "3"}, `{"user_id":"user1"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerListUserReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "1"}, `{"user_id":"user1"}`)
|
||||
_, err := testHandlerProjectUserReadOnly.testUpdateWithLinkShare(nil, map[string]string{"project": "1"}, `{"user_id":"user1"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerListUserWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"user_id":"user1"}`)
|
||||
_, err := testHandlerProjectUserWrite.testUpdateWithLinkShare(nil, map[string]string{"project": "2"}, `{"user_id":"user1"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerListUserAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"user_id":"user1"}`)
|
||||
_, err := testHandlerProjectUserAdmin.testUpdateWithLinkShare(nil, map[string]string{"project": "3"}, `{"user_id":"user1"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
@ -376,91 +376,91 @@ func TestLinkSharing(t *testing.T) {
|
|||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerListUserReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"})
|
||||
_, err := testHandlerProjectUserReadOnly.testDeleteWithLinkShare(nil, map[string]string{"project": "1"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerListUserWrite.testDeleteWithLinkShare(nil, map[string]string{"list": "2"})
|
||||
_, err := testHandlerProjectUserWrite.testDeleteWithLinkShare(nil, map[string]string{"project": "2"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerListUserAdmin.testDeleteWithLinkShare(nil, map[string]string{"list": "3"})
|
||||
_, err := testHandlerProjectUserAdmin.testDeleteWithLinkShare(nil, map[string]string{"project": "3"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
})
|
||||
t.Run("Teams", func(t *testing.T) {
|
||||
testHandlerListTeamReadOnly := webHandlerTest{
|
||||
testHandlerProjectTeamReadOnly := webHandlerTest{
|
||||
linkShare: linkshareRead,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.TeamList{}
|
||||
return &models.TeamProject{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerListTeamWrite := webHandlerTest{
|
||||
testHandlerProjectTeamWrite := webHandlerTest{
|
||||
linkShare: linkShareWrite,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.TeamList{}
|
||||
return &models.TeamProject{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerListTeamAdmin := webHandlerTest{
|
||||
testHandlerProjectTeamAdmin := webHandlerTest{
|
||||
linkShare: linkShareAdmin,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.TeamList{}
|
||||
return &models.TeamProject{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
t.Run("ReadAll", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
rec, err := testHandlerListTeamReadOnly.testReadAllWithLinkShare(nil, map[string]string{"list": "1"})
|
||||
rec, err := testHandlerProjectTeamReadOnly.testReadAllWithLinkShare(nil, map[string]string{"project": "1"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[]`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
rec, err := testHandlerListTeamWrite.testReadAllWithLinkShare(nil, map[string]string{"list": "2"})
|
||||
rec, err := testHandlerProjectTeamWrite.testReadAllWithLinkShare(nil, map[string]string{"project": "2"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[]`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
rec, err := testHandlerListTeamAdmin.testReadAllWithLinkShare(nil, map[string]string{"list": "3"})
|
||||
rec, err := testHandlerProjectTeamAdmin.testReadAllWithLinkShare(nil, map[string]string{"project": "3"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"name":"testteam1"`)
|
||||
})
|
||||
})
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerListTeamReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"team_id":1}`)
|
||||
_, err := testHandlerProjectTeamReadOnly.testCreateWithLinkShare(nil, map[string]string{"project": "1"}, `{"team_id":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerListTeamWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"team_id":1}`)
|
||||
_, err := testHandlerProjectTeamWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{"team_id":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerListTeamAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"team_id":1}`)
|
||||
_, err := testHandlerProjectTeamAdmin.testCreateWithLinkShare(nil, map[string]string{"project": "3"}, `{"team_id":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerListTeamReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "1"}, `{"team_id":1}`)
|
||||
_, err := testHandlerProjectTeamReadOnly.testUpdateWithLinkShare(nil, map[string]string{"project": "1"}, `{"team_id":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerListTeamWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"team_id":1}`)
|
||||
_, err := testHandlerProjectTeamWrite.testUpdateWithLinkShare(nil, map[string]string{"project": "2"}, `{"team_id":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerListTeamAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"team_id":1}`)
|
||||
_, err := testHandlerProjectTeamAdmin.testUpdateWithLinkShare(nil, map[string]string{"project": "3"}, `{"team_id":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
@ -468,17 +468,17 @@ func TestLinkSharing(t *testing.T) {
|
|||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerListTeamReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"})
|
||||
_, err := testHandlerProjectTeamReadOnly.testDeleteWithLinkShare(nil, map[string]string{"project": "1"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerListTeamWrite.testDeleteWithLinkShare(nil, map[string]string{"list": "2"})
|
||||
_, err := testHandlerProjectTeamWrite.testDeleteWithLinkShare(nil, map[string]string{"project": "2"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerListTeamAdmin.testDeleteWithLinkShare(nil, map[string]string{"list": "3"})
|
||||
_, err := testHandlerProjectTeamAdmin.testDeleteWithLinkShare(nil, map[string]string{"project": "3"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
@ -586,34 +586,34 @@ func TestLinkSharing(t *testing.T) {
|
|||
})
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerTaskReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum"}`)
|
||||
_, err := testHandlerTaskReadOnly.testCreateWithLinkShare(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
rec, err := testHandlerTaskWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandlerTaskWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
rec, err := testHandlerTaskAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandlerTaskAdmin.testCreateWithLinkShare(nil, map[string]string{"project": "3"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
})
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerTaskReadOnly.testUpdateWithLinkShare(nil, map[string]string{"listtask": "1"}, `{"title":"Lorem Ipsum"}`)
|
||||
_, err := testHandlerTaskReadOnly.testUpdateWithLinkShare(nil, map[string]string{"projecttask": "1"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
rec, err := testHandlerTaskWrite.testUpdateWithLinkShare(nil, map[string]string{"listtask": "13"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandlerTaskWrite.testUpdateWithLinkShare(nil, map[string]string{"projecttask": "13"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
rec, err := testHandlerTaskAdmin.testUpdateWithLinkShare(nil, map[string]string{"listtask": "32"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandlerTaskAdmin.testUpdateWithLinkShare(nil, map[string]string{"projecttask": "32"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
|
@ -621,17 +621,17 @@ func TestLinkSharing(t *testing.T) {
|
|||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerTaskReadOnly.testDeleteWithLinkShare(nil, map[string]string{"listtask": "1"})
|
||||
_, err := testHandlerTaskReadOnly.testDeleteWithLinkShare(nil, map[string]string{"projecttask": "1"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
rec, err := testHandlerTaskWrite.testDeleteWithLinkShare(nil, map[string]string{"listtask": "13"})
|
||||
rec, err := testHandlerTaskWrite.testDeleteWithLinkShare(nil, map[string]string{"projecttask": "13"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
rec, err := testHandlerTaskAdmin.testDeleteWithLinkShare(nil, map[string]string{"listtask": "32"})
|
||||
rec, err := testHandlerTaskAdmin.testDeleteWithLinkShare(nil, map[string]string{"projecttask": "32"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
})
|
||||
|
@ -738,34 +738,34 @@ func TestLinkSharing(t *testing.T) {
|
|||
}
|
||||
t.Run("ReadAll", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
rec, err := testHandlerLinkShareReadOnly.testReadAllWithLinkShare(nil, map[string]string{"list": "1"})
|
||||
rec, err := testHandlerLinkShareReadOnly.testReadAllWithLinkShare(nil, map[string]string{"project": "1"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"hash":"test"`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
rec, err := testHandlerLinkShareWrite.testReadAllWithLinkShare(nil, map[string]string{"list": "2"})
|
||||
rec, err := testHandlerLinkShareWrite.testReadAllWithLinkShare(nil, map[string]string{"project": "2"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"hash":"test2"`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
rec, err := testHandlerLinkShareAdmin.testReadAllWithLinkShare(nil, map[string]string{"list": "3"})
|
||||
rec, err := testHandlerLinkShareAdmin.testReadAllWithLinkShare(nil, map[string]string{"project": "3"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"hash":"test3"`)
|
||||
})
|
||||
})
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerLinkShareReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{}`)
|
||||
_, err := testHandlerLinkShareReadOnly.testCreateWithLinkShare(nil, map[string]string{"project": "1"}, `{}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{}`)
|
||||
_, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerLinkShareAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{}`)
|
||||
_, err := testHandlerLinkShareAdmin.testCreateWithLinkShare(nil, map[string]string{"project": "3"}, `{}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
@ -806,284 +806,4 @@ func TestLinkSharing(t *testing.T) {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Namespace", func(t *testing.T) {
|
||||
testHandlerNamespaceReadOnly := webHandlerTest{
|
||||
linkShare: linkshareRead,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.Namespace{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerNamespaceWrite := webHandlerTest{
|
||||
linkShare: linkShareWrite,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.Namespace{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerNamespaceAdmin := webHandlerTest{
|
||||
linkShare: linkShareAdmin,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.Namespace{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
t.Run("ReadAll", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceReadOnly.testReadAllWithLinkShare(nil, map[string]string{"namespace": "1"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceWrite.testReadAllWithLinkShare(nil, map[string]string{"namespace": "2"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceAdmin.testReadAllWithLinkShare(nil, map[string]string{"namespace": "3"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
|
||||
})
|
||||
})
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceReadOnly.testCreateWithLinkShare(nil, nil, `{"title":"LoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceWrite.testCreateWithLinkShare(nil, nil, `{"title":"LoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceAdmin.testCreateWithLinkShare(nil, nil, `{"title":"LoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"title":"LoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"title":"LoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"title":"LoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceReadOnly.testDeleteWithLinkShare(nil, map[string]string{"namespace": "1"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceWrite.testDeleteWithLinkShare(nil, map[string]string{"namespace": "2"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceAdmin.testDeleteWithLinkShare(nil, map[string]string{"namespace": "3"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Right Management", func(t *testing.T) {
|
||||
t.Run("Users", func(t *testing.T) {
|
||||
testHandlerNamespaceUserReadOnly := webHandlerTest{
|
||||
linkShare: linkshareRead,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.NamespaceUser{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerNamespaceUserWrite := webHandlerTest{
|
||||
linkShare: linkShareWrite,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.NamespaceUser{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerNamespaceUserAdmin := webHandlerTest{
|
||||
linkShare: linkShareAdmin,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.NamespaceUser{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
t.Run("ReadAll", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceUserReadOnly.testReadAllWithLinkShare(nil, map[string]string{"namespace": "1"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceUserWrite.testReadAllWithLinkShare(nil, map[string]string{"namespace": "2"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceUserAdmin.testReadAllWithLinkShare(nil, map[string]string{"namespace": "3"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
|
||||
})
|
||||
})
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceUserReadOnly.testCreateWithLinkShare(nil, nil, `{"user_id":"user1"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceUserWrite.testCreateWithLinkShare(nil, nil, `{"user_id":"user1"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceUserAdmin.testCreateWithLinkShare(nil, nil, `{"user_id":"user1"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceUserReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"user_id":"user1"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceUserWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"user_id":"user1"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceUserAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"user_id":"user1"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceUserReadOnly.testDeleteWithLinkShare(nil, map[string]string{"namespace": "1"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceUserWrite.testDeleteWithLinkShare(nil, map[string]string{"namespace": "2"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceUserAdmin.testDeleteWithLinkShare(nil, map[string]string{"namespace": "3"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
})
|
||||
t.Run("Teams", func(t *testing.T) {
|
||||
testHandlerNamespaceTeamReadOnly := webHandlerTest{
|
||||
linkShare: linkshareRead,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.TeamNamespace{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerNamespaceTeamWrite := webHandlerTest{
|
||||
linkShare: linkShareWrite,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.TeamNamespace{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerNamespaceTeamAdmin := webHandlerTest{
|
||||
linkShare: linkShareAdmin,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.TeamNamespace{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
t.Run("ReadAll", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceTeamReadOnly.testReadAllWithLinkShare(nil, map[string]string{"namespace": "1"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceTeamWrite.testReadAllWithLinkShare(nil, map[string]string{"namespace": "2"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceTeamAdmin.testReadAllWithLinkShare(nil, map[string]string{"namespace": "3"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
|
||||
})
|
||||
})
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceTeamReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"team_id":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceTeamWrite.testCreateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"team_id":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceTeamAdmin.testCreateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"team_id":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceTeamReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"team_id":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceTeamWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"team_id":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceTeamAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"team_id":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceTeamReadOnly.testDeleteWithLinkShare(nil, map[string]string{"namespace": "1"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceTeamWrite.testDeleteWithLinkShare(nil, map[string]string{"namespace": "2"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceTeamAdmin.testDeleteWithLinkShare(nil, map[string]string{"namespace": "3"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -26,11 +26,11 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
func TestProject(t *testing.T) {
|
||||
testHandler := webHandlerTest{
|
||||
user: &testuser1,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.List{}
|
||||
return &models.Project{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
|
@ -40,10 +40,10 @@ func TestList(t *testing.T) {
|
|||
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.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_project
|
||||
assert.Contains(t, rec.Body.String(), `Test12`) // Shared via parent project
|
||||
assert.NotContains(t, rec.Body.String(), `Test5`)
|
||||
assert.NotContains(t, rec.Body.String(), `Test21`) // Archived through namespace
|
||||
assert.NotContains(t, rec.Body.String(), `Test21`) // Archived through parent project
|
||||
assert.NotContains(t, rec.Body.String(), `Test22`) // Archived directly
|
||||
})
|
||||
t.Run("Search", func(t *testing.T) {
|
||||
|
@ -55,113 +55,113 @@ func TestList(t *testing.T) {
|
|||
assert.NotContains(t, rec.Body.String(), `Test4`)
|
||||
assert.NotContains(t, rec.Body.String(), `Test5`)
|
||||
})
|
||||
t.Run("Normal with archived lists", func(t *testing.T) {
|
||||
t.Run("Normal with archived projects", 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.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_project
|
||||
assert.Contains(t, rec.Body.String(), `Test12`) // Shared via parent project
|
||||
assert.NotContains(t, rec.Body.String(), `Test5`)
|
||||
assert.Contains(t, rec.Body.String(), `Test21`) // Archived through namespace
|
||||
assert.Contains(t, rec.Body.String(), `Test21`) // Archived through project
|
||||
assert.Contains(t, rec.Body.String(), `Test22`) // Archived directly
|
||||
})
|
||||
})
|
||||
t.Run("ReadOne", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "1"})
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "1"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test1"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"title":"Test2"`)
|
||||
assert.Contains(t, rec.Body.String(), `"owner":{"id":1,"name":"","username":"user1",`)
|
||||
assert.NotContains(t, rec.Body.String(), `"owner":{"id":2,"name":"","username":"user2",`)
|
||||
assert.NotContains(t, rec.Body.String(), `"tasks":`)
|
||||
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right")) // User 1 is owner so they should have admin rights.
|
||||
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right")) // User 1 is owner, so they should have admin rights.
|
||||
})
|
||||
t.Run("Nonexisting", func(t *testing.T) {
|
||||
_, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "9999"})
|
||||
_, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "9999"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
|
||||
})
|
||||
t.Run("Rights check", func(t *testing.T) {
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
// Owned by user13
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "20"})
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "20"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `You don't have the right to see this`)
|
||||
assert.Empty(t, rec.Result().Header.Get("x-max-rights"))
|
||||
})
|
||||
t.Run("Shared Via Team readonly", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "6"})
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "6"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test6"`)
|
||||
assert.Equal(t, "0", rec.Result().Header.Get("x-max-right"))
|
||||
})
|
||||
t.Run("Shared Via Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "7"})
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "7"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test7"`)
|
||||
assert.Equal(t, "1", rec.Result().Header.Get("x-max-right"))
|
||||
})
|
||||
t.Run("Shared Via Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "8"})
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "8"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test8"`)
|
||||
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right"))
|
||||
})
|
||||
|
||||
t.Run("Shared Via User readonly", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "9"})
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "9"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test9"`)
|
||||
assert.Equal(t, "0", rec.Result().Header.Get("x-max-right"))
|
||||
})
|
||||
t.Run("Shared Via User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "10"})
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "10"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test10"`)
|
||||
assert.Equal(t, "1", rec.Result().Header.Get("x-max-right"))
|
||||
})
|
||||
t.Run("Shared Via User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "11"})
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "11"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test11"`)
|
||||
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right"))
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "12"})
|
||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "12"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test12"`)
|
||||
assert.Equal(t, "0", rec.Result().Header.Get("x-max-right"))
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "13"})
|
||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "13"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test13"`)
|
||||
assert.Equal(t, "1", rec.Result().Header.Get("x-max-right"))
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "14"})
|
||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "14"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test14"`)
|
||||
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right"))
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "15"})
|
||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "15"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test15"`)
|
||||
assert.Equal(t, "0", rec.Result().Header.Get("x-max-right"))
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "16"})
|
||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "16"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test16"`)
|
||||
assert.Equal(t, "1", rec.Result().Header.Get("x-max-right"))
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "17"})
|
||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "17"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Test17"`)
|
||||
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right"))
|
||||
|
@ -170,101 +170,101 @@ func TestList(t *testing.T) {
|
|||
})
|
||||
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{"list": "1"}, `{"title":"TestLoremIpsum","namespace_id":1}`)
|
||||
// Check the project was loaded successfully afterwards, see testReadOneWithUser
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
// The description should not be updated but returned correctly
|
||||
assert.Contains(t, rec.Body.String(), `description":"Lorem Ipsum`)
|
||||
})
|
||||
t.Run("Nonexisting", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "9999"}, `{"title":"TestLoremIpsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "9999"}, `{"title":"TestLoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
|
||||
})
|
||||
t.Run("Normal with updating the description", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet","namespace_id":1}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum dolor sit amet`)
|
||||
})
|
||||
t.Run("Empty title", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":""}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":""}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required")
|
||||
})
|
||||
t.Run("Title too long", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(1|250)")
|
||||
})
|
||||
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{"list": "20"}, `{"title":"TestLoremIpsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "20"}, `{"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{"list": "6"}, `{"title":"TestLoremIpsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "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{"list": "7"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "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{"list": "8"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "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{"list": "9"}, `{"title":"TestLoremIpsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "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{"list": "10"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "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{"list": "11"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "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{"list": "12"}, `{"title":"TestLoremIpsum"}`)
|
||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "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{"list": "13"}, `{"title":"TestLoremIpsum","namespace_id":8}`)
|
||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "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{"list": "14"}, `{"title":"TestLoremIpsum","namespace_id":9}`)
|
||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "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{"list": "15"}, `{"title":"TestLoremIpsum"}`)
|
||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "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{"list": "16"}, `{"title":"TestLoremIpsum","namespace_id":11}`)
|
||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "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{"list": "17"}, `{"title":"TestLoremIpsum","namespace_id":12}`)
|
||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "17"}, `{"title":"TestLoremIpsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
|
@ -272,82 +272,82 @@ func TestList(t *testing.T) {
|
|||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "1"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "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{"list": "999"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "999"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
|
||||
})
|
||||
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{"list": "20"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "20"})
|
||||
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{"list": "6"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "6"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via Team write", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "7"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "7"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "8"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "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{"list": "9"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "9"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via User write", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "10"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "10"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "11"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "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{"list": "12"})
|
||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "12"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "13"})
|
||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "13"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "14"})
|
||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "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{"list": "15"})
|
||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "15"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "16"})
|
||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "16"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "17"})
|
||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "17"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||
})
|
||||
|
@ -355,8 +355,8 @@ func TestList(t *testing.T) {
|
|||
})
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
// Check the list was loaded successfully after update, see testReadOneWithUser
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem"}`)
|
||||
// Check the project was loaded successfully after update, see testReadOneWithUser
|
||||
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
|
||||
assert.Contains(t, rec.Body.String(), `"description":""`)
|
||||
|
@ -364,52 +364,50 @@ func TestList(t *testing.T) {
|
|||
assert.NotContains(t, rec.Body.String(), `"tasks":`)
|
||||
})
|
||||
t.Run("Normal with description", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","description":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
|
||||
assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"owner":{"id":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `"tasks":`)
|
||||
})
|
||||
t.Run("Nonexisting Namespace", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "999999"}, `{"title":"Lorem"}`)
|
||||
t.Run("Nonexisting parent project", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":99999}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceDoesNotExist)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
|
||||
})
|
||||
t.Run("Empty title", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":""}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":""}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required")
|
||||
})
|
||||
t.Run("Title too long", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(1|250)")
|
||||
})
|
||||
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{"namespace": "15"}, `{"title":"Lorem"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":20}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "7"}, `{"title":"Lorem"}`)
|
||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":32}`)
|
||||
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{"namespace": "8"}, `{"title":"Lorem"}`)
|
||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":33}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
|
||||
assert.Contains(t, rec.Body.String(), `"description":""`)
|
||||
assert.Contains(t, rec.Body.String(), `"owner":{"id":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `"tasks":`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "9"}, `{"title":"Lorem"}`)
|
||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":34}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
|
||||
assert.Contains(t, rec.Body.String(), `"description":""`)
|
||||
|
@ -417,21 +415,21 @@ func TestList(t *testing.T) {
|
|||
assert.NotContains(t, rec.Body.String(), `"tasks":`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "10"}, `{"title":"Lorem"}`)
|
||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":9}`)
|
||||
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{"namespace": "11"}, `{"title":"Lorem"}`)
|
||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":10}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
|
||||
assert.Contains(t, rec.Body.String(), `"description":""`)
|
||||
assert.Contains(t, rec.Body.String(), `"owner":{"id":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `"tasks":`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "12"}, `{"title":"Lorem"}`)
|
||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "12"}, `{"title":"Lorem","parent_project_id":11}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
|
||||
assert.Contains(t, rec.Body.String(), `"description":""`)
|
|
@ -33,9 +33,9 @@ func TestTaskCollection(t *testing.T) {
|
|||
},
|
||||
t: t,
|
||||
}
|
||||
t.Run("ReadAll on list", func(t *testing.T) {
|
||||
t.Run("ReadAll on project", func(t *testing.T) {
|
||||
|
||||
urlParams := map[string]string{"list": "1"}
|
||||
urlParams := map[string]string{"project": "1"}
|
||||
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(nil, urlParams)
|
||||
|
@ -113,49 +113,49 @@ 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,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
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,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":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,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
// should equal duedate asc
|
||||
t.Run("by due_date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("by duedate desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
})
|
||||
// Due date without unix suffix
|
||||
t.Run("by duedate asc without suffix", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("by due_date without suffix", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("by duedate desc without suffix", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"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"}, "order_by": []string{"asc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("invalid sort parameter", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
|
||||
|
@ -287,7 +287,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("date range", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
nil,
|
||||
map[string]string{"list": "-2"}, // Actually a saved filter - contains the same filter arguments as the start and end date filter from above
|
||||
map[string]string{"project": "-2"}, // Actually a saved filter - contains the same filter arguments as the start and end date filter from above
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
|
@ -341,7 +341,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
assert.Contains(t, rec.Body.String(), `task #24`) // Shared via namespace user readonly
|
||||
assert.Contains(t, rec.Body.String(), `task #25`) // Shared via namespace user write
|
||||
assert.Contains(t, rec.Body.String(), `task #26`) // Shared via namespace user admin
|
||||
// TODO: Add some cases where the user has access to the list, somhow shared
|
||||
// TODO: Add some cases where the user has access to the project, somhow shared
|
||||
})
|
||||
t.Run("Search", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"task #6"}}, nil)
|
||||
|
@ -366,33 +366,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,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
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,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":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,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
// should equal duedate asc
|
||||
t.Run("by due_date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("by duedate desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"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"}, "order_by": []string{"asc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("invalid parameter", func(t *testing.T) {
|
||||
// Invalid parameter should not sort at all
|
||||
|
|
|
@ -39,7 +39,7 @@ func TestTaskComments(t *testing.T) {
|
|||
linkShare: &models.LinkSharing{
|
||||
ID: 2,
|
||||
Hash: "test2",
|
||||
ListID: 2,
|
||||
ProjectID: 2,
|
||||
Right: models.RightWrite,
|
||||
SharingType: models.SharingTypeWithoutPassword,
|
||||
SharedByID: 1,
|
||||
|
@ -101,33 +101,33 @@ func TestTaskComments(t *testing.T) {
|
|||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "21", "commentid": "9"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "22", "commentid": "10"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "23", "commentid": "11"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "24", "commentid": "12"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "25", "commentid": "13"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "26", "commentid": "14"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
|
@ -184,33 +184,33 @@ func TestTaskComments(t *testing.T) {
|
|||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "21", "commentid": "9"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "22", "commentid": "10"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "23", "commentid": "11"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "24", "commentid": "12"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "25", "commentid": "13"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "26", "commentid": "14"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
|
@ -267,33 +267,33 @@ func TestTaskComments(t *testing.T) {
|
|||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"task": "21"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"task": "22"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"task": "23"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"task": "24"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"task": "25"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"task": "26"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
|
|
|
@ -39,7 +39,7 @@ func TestTask(t *testing.T) {
|
|||
linkShare: &models.LinkSharing{
|
||||
ID: 2,
|
||||
Hash: "test2",
|
||||
ListID: 2,
|
||||
ProjectID: 2,
|
||||
Right: models.RightWrite,
|
||||
SharingType: models.SharingTypeWithoutPassword,
|
||||
SharedByID: 1,
|
||||
|
@ -54,157 +54,157 @@ func TestTask(t *testing.T) {
|
|||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Update task items", func(t *testing.T) {
|
||||
t.Run("Title", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"title":"task #1"`)
|
||||
})
|
||||
t.Run("Description", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"description":"Dolor sit amet"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"description":"Dolor sit amet"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"description":"Dolor sit amet"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"description":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Description to empty", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"description":""}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"description":""}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"description":""`)
|
||||
assert.NotContains(t, rec.Body.String(), `"description":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Done", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"done":true}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"done":true}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"done":true`)
|
||||
assert.NotContains(t, rec.Body.String(), `"done":false`)
|
||||
})
|
||||
t.Run("Undone", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "2"}, `{"done":false}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "2"}, `{"done":false}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"done":false`)
|
||||
assert.NotContains(t, rec.Body.String(), `"done":true`)
|
||||
})
|
||||
t.Run("Due date", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"due_date": "2020-02-10T10:00:00Z"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"due_date": "2020-02-10T10:00:00Z"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"due_date":"2020-02-10T10:00:00Z"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"due_date":0`)
|
||||
})
|
||||
t.Run("Due date unset", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "5"}, `{"due_date": null}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "5"}, `{"due_date": null}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"due_date":"0001-01-01T00:00:00Z"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"due_date":"2020-02-10T10:00:00Z"`)
|
||||
})
|
||||
t.Run("Reminders", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"reminder_dates": ["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"reminder_dates": ["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"reminder_dates":["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]`)
|
||||
assert.NotContains(t, rec.Body.String(), `"reminder_dates": null`)
|
||||
})
|
||||
t.Run("Reminders unset to empty array", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminder_dates": []}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "27"}, `{"reminder_dates": []}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"reminder_dates":null`)
|
||||
assert.NotContains(t, rec.Body.String(), `"reminder_dates":[1543626724,1543626824]`)
|
||||
})
|
||||
t.Run("Reminders unset to null", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminder_dates": null}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "27"}, `{"reminder_dates": null}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"reminder_dates":null`)
|
||||
assert.NotContains(t, rec.Body.String(), `"reminder_dates":[1543626724,1543626824]`)
|
||||
})
|
||||
t.Run("Repeat after", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"repeat_after":3600}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"repeat_after":3600}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"repeat_after":3600`)
|
||||
assert.NotContains(t, rec.Body.String(), `"repeat_after":0`)
|
||||
})
|
||||
t.Run("Repeat after unset", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "28"}, `{"repeat_after":0}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "28"}, `{"repeat_after":0}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"repeat_after":0`)
|
||||
assert.NotContains(t, rec.Body.String(), `"repeat_after":3600`)
|
||||
})
|
||||
t.Run("Repeat after update done", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "28"}, `{"done":true}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "28"}, `{"done":true}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"done":false`)
|
||||
assert.NotContains(t, rec.Body.String(), `"done":true`)
|
||||
})
|
||||
t.Run("Assignees", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"assignees":[{"id":1}]}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"assignees":[{"id":1}]}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"assignees":[{"id":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `"assignees":[]`)
|
||||
})
|
||||
t.Run("Removing Assignees empty array", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "30"}, `{"assignees":[]}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "30"}, `{"assignees":[]}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"assignees":null`)
|
||||
assert.NotContains(t, rec.Body.String(), `"assignees":[{"id":1`)
|
||||
})
|
||||
t.Run("Removing Assignees null", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "30"}, `{"assignees":null}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "30"}, `{"assignees":null}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"assignees":null`)
|
||||
assert.NotContains(t, rec.Body.String(), `"assignees":[{"id":1`)
|
||||
})
|
||||
t.Run("Priority", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"priority":100}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"priority":100}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"priority":100`)
|
||||
assert.NotContains(t, rec.Body.String(), `"priority":0`)
|
||||
})
|
||||
t.Run("Priority to 0", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "3"}, `{"priority":0}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "3"}, `{"priority":0}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"priority":0`)
|
||||
assert.NotContains(t, rec.Body.String(), `"priority":100`)
|
||||
})
|
||||
t.Run("Start date", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"start_date":"2020-02-10T10:00:00Z"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"start_date":"2020-02-10T10:00:00Z"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"start_date":"2020-02-10T10:00:00Z"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"start_date":0`)
|
||||
})
|
||||
t.Run("Start date unset", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "7"}, `{"start_date":"0001-01-01T00:00:00Z"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "7"}, `{"start_date":"0001-01-01T00:00:00Z"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"start_date":"0001-01-01T00:00:00Z"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"start_date":"2020-02-10T10:00:00Z"`)
|
||||
})
|
||||
t.Run("End date", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"end_date":"2020-02-10T12:00:00Z"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"end_date":"2020-02-10T12:00:00Z"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"end_date":"2020-02-10T12:00:00Z"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"end_date":""`)
|
||||
})
|
||||
t.Run("End date unset", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "8"}, `{"end_date":"0001-01-01T00:00:00Z"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "8"}, `{"end_date":"0001-01-01T00:00:00Z"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"end_date":"0001-01-01T00:00:00Z"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"end_date":"2020-02-10T10:00:00Z"`)
|
||||
})
|
||||
t.Run("Color", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"hex_color":"f0f0f0"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"hex_color":"f0f0f0"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"hex_color":"f0f0f0"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"hex_color":""`)
|
||||
})
|
||||
t.Run("Color unset", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "31"}, `{"hex_color":""}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "31"}, `{"hex_color":""}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"hex_color":""`)
|
||||
assert.NotContains(t, rec.Body.String(), `"hex_color":"f0f0f0"`)
|
||||
})
|
||||
t.Run("Percent Done", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"percent_done":0.1}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"percent_done":0.1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"percent_done":0.1`)
|
||||
assert.NotContains(t, rec.Body.String(), `"percent_done":0,`)
|
||||
})
|
||||
t.Run("Percent Done unset", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "33"}, `{"percent_done":0}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "33"}, `{"percent_done":0}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"percent_done":0,`)
|
||||
assert.NotContains(t, rec.Body.String(), `"percent_done":0.1`)
|
||||
|
@ -212,112 +212,112 @@ func TestTask(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Nonexisting", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "99999"}, `{"title":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "99999"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist)
|
||||
})
|
||||
t.Run("Rights check", func(t *testing.T) {
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "14"}, `{"title":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "14"}, `{"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.testUpdateWithUser(nil, map[string]string{"listtask": "15"}, `{"title":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "15"}, `{"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.testUpdateWithUser(nil, map[string]string{"listtask": "16"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "16"}, `{"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.testUpdateWithUser(nil, map[string]string{"listtask": "17"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "17"}, `{"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.testUpdateWithUser(nil, map[string]string{"listtask": "18"}, `{"title":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "18"}, `{"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.testUpdateWithUser(nil, map[string]string{"listtask": "19"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "19"}, `{"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.testUpdateWithUser(nil, map[string]string{"listtask": "20"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "20"}, `{"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.testUpdateWithUser(nil, map[string]string{"listtask": "21"}, `{"title":"Lorem Ipsum"}`)
|
||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "21"}, `{"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.testUpdateWithUser(nil, map[string]string{"listtask": "22"}, `{"title":"Lorem Ipsum"}`)
|
||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "22"}, `{"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.testUpdateWithUser(nil, map[string]string{"listtask": "23"}, `{"title":"Lorem Ipsum"}`)
|
||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "23"}, `{"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.testUpdateWithUser(nil, map[string]string{"listtask": "24"}, `{"title":"Lorem Ipsum"}`)
|
||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "24"}, `{"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.testUpdateWithUser(nil, map[string]string{"listtask": "25"}, `{"title":"Lorem Ipsum"}`)
|
||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "25"}, `{"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.testUpdateWithUser(nil, map[string]string{"listtask": "26"}, `{"title":"Lorem Ipsum"}`)
|
||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "26"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
})
|
||||
t.Run("Move to other list", func(t *testing.T) {
|
||||
t.Run("Move to other project", func(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"list_id":7}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"project_id":7}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"list_id":7`)
|
||||
assert.NotContains(t, rec.Body.String(), `"list_id":1`)
|
||||
assert.Contains(t, rec.Body.String(), `"project_id":7`)
|
||||
assert.NotContains(t, rec.Body.String(), `"project_id":1`)
|
||||
})
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"list_id":20}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"project_id":20}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
|
||||
})
|
||||
t.Run("Read Only", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"list_id":6}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"project_id":6}`)
|
||||
assert.Error(t, err)
|
||||
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":3}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"bucket_id":3}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"bucket_id":3`)
|
||||
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}`)
|
||||
t.Run("Different Project", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"bucket_id":4}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToList)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToProject)
|
||||
})
|
||||
t.Run("Nonexisting Bucket", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"bucket_id":9999}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"bucket_id":9999}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
|
||||
})
|
||||
|
@ -325,81 +325,81 @@ func TestTask(t *testing.T) {
|
|||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "1"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "1"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
})
|
||||
t.Run("Nonexisting", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "99999"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "99999"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist)
|
||||
})
|
||||
t.Run("Rights check", func(t *testing.T) {
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "14"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "14"})
|
||||
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{"listtask": "15"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "15"})
|
||||
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{"listtask": "16"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "16"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
})
|
||||
t.Run("Shared Via Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "17"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "17"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "18"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "18"})
|
||||
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{"listtask": "19"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "19"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
})
|
||||
t.Run("Shared Via User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "20"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "20"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "21"})
|
||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "21"})
|
||||
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{"listtask": "22"})
|
||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "22"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "23"})
|
||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "23"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "24"})
|
||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "24"})
|
||||
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{"listtask": "25"})
|
||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "25"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "26"})
|
||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "26"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
})
|
||||
|
@ -407,110 +407,110 @@ func TestTask(t *testing.T) {
|
|||
})
|
||||
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"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9999"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
|
||||
})
|
||||
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"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "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"}`)
|
||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "17"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"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"}, `{"title":"Lorem Ipsum","bucket_id":3}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum","bucket_id":3}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"bucket_id":3`)
|
||||
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"}, `{"title":"Lorem Ipsum","bucket_id":4}`)
|
||||
t.Run("Different Project", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum","bucket_id":4}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToList)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToProject)
|
||||
})
|
||||
t.Run("Nonexisting Bucket", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum","bucket_id":9999}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum","bucket_id":9999}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
|
||||
})
|
||||
})
|
||||
t.Run("Link Share", func(t *testing.T) {
|
||||
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"list_id": 2,
|
||||
"project_id": 2,
|
||||
"title": "Lorem Ipsum",
|
||||
"created_by_id": -2,
|
||||
}, false)
|
||||
|
|
|
@ -24,7 +24,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserList(t *testing.T) {
|
||||
func TestUserProject(t *testing.T) {
|
||||
t.Run("Normal test", func(t *testing.T) {
|
||||
rec, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserList, &testuser1, "", nil, nil)
|
||||
assert.NoError(t, err)
|
|
@ -28,15 +28,12 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
// ListCountKey is the name of the key in which we save the list count
|
||||
ListCountKey = `listcount`
|
||||
// ProjectCountKey is the name of the key in which we save the project count
|
||||
ProjectCountKey = `projectcount`
|
||||
|
||||
// UserCountKey is the name of the key we use to store total users in redis
|
||||
UserCountKey = `usercount`
|
||||
|
||||
// NamespaceCountKey is the name of the key we use to store the amount of total namespaces in redis
|
||||
NamespaceCountKey = `namespacecount`
|
||||
|
||||
// TaskCountKey is the name of the key we use to store the amount of total tasks in redis
|
||||
TaskCountKey = `taskcount`
|
||||
|
||||
|
@ -65,16 +62,16 @@ func InitMetrics() {
|
|||
|
||||
GetRegistry()
|
||||
|
||||
// Register total list count metric
|
||||
// Register total project count metric
|
||||
err := registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_list_count",
|
||||
Help: "The number of lists on this instance",
|
||||
Name: "vikunja_project_count",
|
||||
Help: "The number of projects on this instance",
|
||||
}, func() float64 {
|
||||
count, _ := GetCount(ListCountKey)
|
||||
count, _ := GetCount(ProjectCountKey)
|
||||
return float64(count)
|
||||
}))
|
||||
if err != nil {
|
||||
log.Criticalf("Could not register metrics for %s: %s", ListCountKey, err)
|
||||
log.Criticalf("Could not register metrics for %s: %s", ProjectCountKey, err)
|
||||
}
|
||||
|
||||
// Register total user count metric
|
||||
|
@ -89,18 +86,6 @@ func InitMetrics() {
|
|||
log.Criticalf("Could not register metrics for %s: %s", UserCountKey, err)
|
||||
}
|
||||
|
||||
// Register total Namespaces count metric
|
||||
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_namespace_count",
|
||||
Help: "The total number of namespaces on this instance",
|
||||
}, func() float64 {
|
||||
count, _ := GetCount(NamespaceCountKey)
|
||||
return float64(count)
|
||||
}))
|
||||
if err != nil {
|
||||
log.Criticalf("Could not register metrics for %s: %s", NamespaceCountKey, err)
|
||||
}
|
||||
|
||||
// Register total Tasks count metric
|
||||
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_task_count",
|
||||
|
@ -147,7 +132,7 @@ func GetCount(key string) (count int64, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// SetCount sets the list count to a given value
|
||||
// SetCount sets the project count to a given value
|
||||
func SetCount(count int64, key string) error {
|
||||
return keyvalue.Put(key, count)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,432 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
const sqliteRename20221113170740 = `
|
||||
-- buckets
|
||||
create table buckets_dg_tmp
|
||||
(
|
||||
id INTEGER not null
|
||||
primary key autoincrement,
|
||||
title TEXT not null,
|
||||
project_id INTEGER not null,
|
||||
"limit" INTEGER default 0,
|
||||
is_done_bucket INTEGER,
|
||||
position REAL,
|
||||
created DATETIME not null,
|
||||
updated DATETIME not null,
|
||||
created_by_id INTEGER not null
|
||||
);
|
||||
|
||||
insert into buckets_dg_tmp(id, title, project_id, "limit", is_done_bucket, position, created, updated, created_by_id)
|
||||
select id,
|
||||
title,
|
||||
list_id,
|
||||
"limit",
|
||||
is_done_bucket,
|
||||
position,
|
||||
created,
|
||||
updated,
|
||||
created_by_id
|
||||
from buckets;
|
||||
|
||||
drop table buckets;
|
||||
|
||||
alter table buckets_dg_tmp
|
||||
rename to buckets;
|
||||
|
||||
create unique index UQE_buckets_id
|
||||
on buckets (id);
|
||||
|
||||
-- link shares
|
||||
create table link_shares_dg_tmp
|
||||
(
|
||||
id INTEGER not null
|
||||
primary key autoincrement,
|
||||
hash TEXT not null,
|
||||
name TEXT,
|
||||
project_id INTEGER not null,
|
||||
"right" INTEGER default 0 not null,
|
||||
sharing_type INTEGER default 0 not null,
|
||||
password TEXT,
|
||||
shared_by_id INTEGER not null,
|
||||
created DATETIME not null,
|
||||
updated DATETIME not null
|
||||
);
|
||||
|
||||
insert into link_shares_dg_tmp(id, hash, name, project_id, "right", sharing_type, password, shared_by_id, created,
|
||||
updated)
|
||||
select id,
|
||||
hash,
|
||||
name,
|
||||
list_id,
|
||||
"right",
|
||||
sharing_type,
|
||||
password,
|
||||
shared_by_id,
|
||||
created,
|
||||
updated
|
||||
from link_shares;
|
||||
|
||||
drop table link_shares;
|
||||
|
||||
alter table link_shares_dg_tmp
|
||||
rename to link_shares;
|
||||
|
||||
create index IDX_link_shares_right
|
||||
on link_shares ("right");
|
||||
|
||||
create index IDX_link_shares_shared_by_id
|
||||
on link_shares (shared_by_id);
|
||||
|
||||
create index IDX_link_shares_sharing_type
|
||||
on link_shares (sharing_type);
|
||||
|
||||
create unique index UQE_link_shares_hash
|
||||
on link_shares (hash);
|
||||
|
||||
create unique index UQE_link_shares_id
|
||||
on link_shares (id);
|
||||
|
||||
-- tasks
|
||||
create table tasks_dg_tmp
|
||||
(
|
||||
id INTEGER not null
|
||||
primary key autoincrement,
|
||||
title TEXT not null,
|
||||
description TEXT,
|
||||
done INTEGER,
|
||||
done_at DATETIME,
|
||||
due_date DATETIME,
|
||||
project_id INTEGER not null,
|
||||
repeat_after INTEGER,
|
||||
repeat_mode INTEGER default 0 not null,
|
||||
priority INTEGER,
|
||||
start_date DATETIME,
|
||||
end_date DATETIME,
|
||||
hex_color TEXT,
|
||||
percent_done REAL,
|
||||
"index" INTEGER default 0 not null,
|
||||
uid TEXT,
|
||||
cover_image_attachment_id INTEGER default 0,
|
||||
created DATETIME not null,
|
||||
updated DATETIME not null,
|
||||
bucket_id INTEGER,
|
||||
position REAL,
|
||||
kanban_position REAL,
|
||||
created_by_id INTEGER not null
|
||||
);
|
||||
|
||||
insert into tasks_dg_tmp(id, title, description, done, done_at, due_date, project_id, repeat_after, repeat_mode,
|
||||
priority, start_date, end_date, hex_color, percent_done, "index", uid,
|
||||
cover_image_attachment_id, created, updated, bucket_id, position, kanban_position,
|
||||
created_by_id)
|
||||
select id,
|
||||
title,
|
||||
description,
|
||||
done,
|
||||
done_at,
|
||||
due_date,
|
||||
list_id,
|
||||
repeat_after,
|
||||
repeat_mode,
|
||||
priority,
|
||||
start_date,
|
||||
end_date,
|
||||
hex_color,
|
||||
percent_done,
|
||||
"index",
|
||||
uid,
|
||||
cover_image_attachment_id,
|
||||
created,
|
||||
updated,
|
||||
bucket_id,
|
||||
position,
|
||||
kanban_position,
|
||||
created_by_id
|
||||
from tasks;
|
||||
|
||||
drop table tasks;
|
||||
|
||||
alter table tasks_dg_tmp
|
||||
rename to tasks;
|
||||
|
||||
create index IDX_tasks_done
|
||||
on tasks (done);
|
||||
|
||||
create index IDX_tasks_done_at
|
||||
on tasks (done_at);
|
||||
|
||||
create index IDX_tasks_due_date
|
||||
on tasks (due_date);
|
||||
|
||||
create index IDX_tasks_end_date
|
||||
on tasks (end_date);
|
||||
|
||||
create index IDX_tasks_list_id
|
||||
on tasks (project_id);
|
||||
|
||||
create index IDX_tasks_repeat_after
|
||||
on tasks (repeat_after);
|
||||
|
||||
create index IDX_tasks_start_date
|
||||
on tasks (start_date);
|
||||
|
||||
create unique index UQE_tasks_id
|
||||
on tasks (id);
|
||||
|
||||
--- team_lists
|
||||
create table team_lists_dg_tmp
|
||||
(
|
||||
id INTEGER not null
|
||||
primary key autoincrement,
|
||||
team_id INTEGER not null,
|
||||
project_id INTEGER not null,
|
||||
"right" INTEGER default 0 not null,
|
||||
created DATETIME not null,
|
||||
updated DATETIME not null
|
||||
);
|
||||
|
||||
insert into team_lists_dg_tmp(id, team_id, project_id, "right", created, updated)
|
||||
select id, team_id, list_id, "right", created, updated
|
||||
from team_lists;
|
||||
|
||||
drop table team_lists;
|
||||
|
||||
alter table team_lists_dg_tmp
|
||||
rename to team_lists;
|
||||
|
||||
create index IDX_team_lists_list_id
|
||||
on team_lists (project_id);
|
||||
|
||||
create index IDX_team_lists_right
|
||||
on team_lists ("right");
|
||||
|
||||
create index IDX_team_lists_team_id
|
||||
on team_lists (team_id);
|
||||
|
||||
create unique index UQE_team_lists_id
|
||||
on team_lists (id);
|
||||
|
||||
--- users
|
||||
create table users_dg_tmp
|
||||
(
|
||||
id INTEGER not null
|
||||
primary key autoincrement,
|
||||
name TEXT,
|
||||
username TEXT not null,
|
||||
password TEXT,
|
||||
email TEXT,
|
||||
status INTEGER default 0,
|
||||
avatar_provider TEXT,
|
||||
avatar_file_id INTEGER,
|
||||
issuer TEXT,
|
||||
subject TEXT,
|
||||
email_reminders_enabled INTEGER default 1,
|
||||
discoverable_by_name INTEGER default 0,
|
||||
discoverable_by_email INTEGER default 0,
|
||||
overdue_tasks_reminders_enabled INTEGER default 1,
|
||||
overdue_tasks_reminders_time TEXT default '09:00' not null,
|
||||
default_project_id INTEGER,
|
||||
week_start INTEGER,
|
||||
language TEXT,
|
||||
timezone TEXT,
|
||||
deletion_scheduled_at DATETIME,
|
||||
deletion_last_reminder_sent DATETIME,
|
||||
export_file_id INTEGER,
|
||||
created DATETIME not null,
|
||||
updated DATETIME not null
|
||||
);
|
||||
|
||||
insert into users_dg_tmp(id, name, username, password, email, status, avatar_provider, avatar_file_id, issuer, subject,
|
||||
email_reminders_enabled, discoverable_by_name, discoverable_by_email,
|
||||
overdue_tasks_reminders_enabled, overdue_tasks_reminders_time, default_project_id, week_start,
|
||||
language, timezone, deletion_scheduled_at, deletion_last_reminder_sent, export_file_id,
|
||||
created, updated)
|
||||
select id,
|
||||
name,
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
status,
|
||||
avatar_provider,
|
||||
avatar_file_id,
|
||||
issuer,
|
||||
subject,
|
||||
email_reminders_enabled,
|
||||
discoverable_by_name,
|
||||
discoverable_by_email,
|
||||
overdue_tasks_reminders_enabled,
|
||||
overdue_tasks_reminders_time,
|
||||
default_list_id,
|
||||
week_start,
|
||||
language,
|
||||
timezone,
|
||||
deletion_scheduled_at,
|
||||
deletion_last_reminder_sent,
|
||||
export_file_id,
|
||||
created,
|
||||
updated
|
||||
from users;
|
||||
|
||||
drop table users;
|
||||
|
||||
alter table users_dg_tmp
|
||||
rename to users;
|
||||
|
||||
create index IDX_users_default_list_id
|
||||
on users (default_project_id);
|
||||
|
||||
create index IDX_users_discoverable_by_email
|
||||
on users (discoverable_by_email);
|
||||
|
||||
create index IDX_users_discoverable_by_name
|
||||
on users (discoverable_by_name);
|
||||
|
||||
create index IDX_users_overdue_tasks_reminders_enabled
|
||||
on users (overdue_tasks_reminders_enabled);
|
||||
|
||||
create unique index UQE_users_id
|
||||
on users (id);
|
||||
|
||||
create unique index UQE_users_username
|
||||
on users (username);
|
||||
|
||||
--- users_list
|
||||
create table users_lists_dg_tmp
|
||||
(
|
||||
id INTEGER not null
|
||||
primary key autoincrement,
|
||||
user_id INTEGER not null,
|
||||
project_id INTEGER not null,
|
||||
"right" INTEGER default 0 not null,
|
||||
created DATETIME not null,
|
||||
updated DATETIME not null
|
||||
);
|
||||
|
||||
insert into users_lists_dg_tmp(id, user_id, project_id, "right", created, updated)
|
||||
select id, user_id, list_id, "right", created, updated
|
||||
from users_lists;
|
||||
|
||||
drop table users_lists;
|
||||
|
||||
alter table users_lists_dg_tmp
|
||||
rename to users_lists;
|
||||
|
||||
create index IDX_users_lists_list_id
|
||||
on users_lists (project_id);
|
||||
|
||||
create index IDX_users_lists_right
|
||||
on users_lists ("right");
|
||||
|
||||
create index IDX_users_lists_user_id
|
||||
on users_lists (user_id);
|
||||
|
||||
create unique index UQE_users_lists_id
|
||||
on users_lists (id);
|
||||
`
|
||||
|
||||
type colToRename struct {
|
||||
table string
|
||||
oldName string
|
||||
newName string
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20221113170740",
|
||||
Description: "Rename lists to projects",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
// SQLite does not support renaming columns. Instead, we'll need to run manual sql.
|
||||
if tx.Dialect().URI().DBType == schemas.SQLITE {
|
||||
_, err := tx.Exec(sqliteRename20221113170740)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
||||
colsToRename := []*colToRename{
|
||||
{
|
||||
table: "buckets",
|
||||
oldName: "list_id",
|
||||
newName: "project_id",
|
||||
},
|
||||
{
|
||||
table: "link_shares",
|
||||
oldName: "list_id",
|
||||
newName: "project_id",
|
||||
},
|
||||
{
|
||||
table: "tasks",
|
||||
oldName: "list_id",
|
||||
newName: "project_id",
|
||||
},
|
||||
{
|
||||
table: "team_lists",
|
||||
oldName: "list_id",
|
||||
newName: "project_id",
|
||||
},
|
||||
{
|
||||
table: "users",
|
||||
oldName: "default_list_id",
|
||||
newName: "default_project_id",
|
||||
},
|
||||
{
|
||||
table: "users_lists",
|
||||
oldName: "list_id",
|
||||
newName: "project_id",
|
||||
},
|
||||
}
|
||||
|
||||
for _, col := range colsToRename {
|
||||
if tx.Dialect().URI().DBType == schemas.POSTGRES || tx.Dialect().URI().DBType == schemas.MYSQL {
|
||||
_, err := tx.Exec("ALTER TABLE `" + col.table + "` RENAME COLUMN `" + col.oldName + "` TO `" + col.newName + "`")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := renameTable(tx, "lists", "projects")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = renameTable(tx, "team_lists", "team_projects")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = renameTable(tx, "users_lists", "users_projects")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,298 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
type projects20221228112131 struct {
|
||||
// This is the one new property
|
||||
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
|
||||
|
||||
// Those only exist to make the migration independent of future changes
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"project"`
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
|
||||
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
|
||||
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
NamespaceID int64 `xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace"`
|
||||
}
|
||||
|
||||
func (projects20221228112131) TableName() string {
|
||||
return "projects"
|
||||
}
|
||||
|
||||
type namespace20221228112131 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
OwnerID int64 `xorm:"bigint not null INDEX" json:"-"`
|
||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
|
||||
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
}
|
||||
|
||||
func (namespace20221228112131) TableName() string {
|
||||
return "namespaces"
|
||||
}
|
||||
|
||||
type teamNamespace20221228112131 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
|
||||
TeamID int64 `xorm:"bigint not null INDEX" json:"team_id" param:"team"`
|
||||
NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"`
|
||||
Right int `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
}
|
||||
|
||||
func (teamNamespace20221228112131) TableName() string {
|
||||
return "team_namespaces"
|
||||
}
|
||||
|
||||
type teamProject20221228112131 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
|
||||
TeamID int64 `xorm:"bigint not null INDEX" json:"team_id" param:"team"`
|
||||
ProjectID int64 `xorm:"bigint not null INDEX" json:"-" param:"project"`
|
||||
Right int `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
}
|
||||
|
||||
func (teamProject20221228112131) TableName() string {
|
||||
return "team_projects"
|
||||
}
|
||||
|
||||
type namespaceUser20221228112131 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
UserID int64 `xorm:"bigint not null INDEX" json:"-"`
|
||||
NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"`
|
||||
Right int `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
}
|
||||
|
||||
func (namespaceUser20221228112131) TableName() string {
|
||||
return "users_namespaces"
|
||||
}
|
||||
|
||||
type projectUser20221228112131 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
UserID int64 `xorm:"bigint not null INDEX" json:"-"`
|
||||
ProjectID int64 `xorm:"bigint not null INDEX" json:"-" param:"project"`
|
||||
Right int `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
}
|
||||
|
||||
func (projectUser20221228112131) TableName() string {
|
||||
return "users_projects"
|
||||
}
|
||||
|
||||
const sqliteRemoveNamespaceColumn20221228112131 = `
|
||||
create table projects_dg_tmp
|
||||
|
||||
(
|
||||
id INTEGER not null
|
||||
primary key autoincrement,
|
||||
title TEXT not null,
|
||||
description TEXT,
|
||||
identifier TEXT,
|
||||
hex_color TEXT,
|
||||
owner_id INTEGER not null,
|
||||
is_archived INTEGER default 0 not null,
|
||||
background_file_id INTEGER,
|
||||
background_blur_hash TEXT,
|
||||
position REAL,
|
||||
created DATETIME not null,
|
||||
updated DATETIME not null,
|
||||
parent_project_id INTEGER
|
||||
);
|
||||
|
||||
insert into projects_dg_tmp(id, title, description, identifier, hex_color, owner_id, is_archived, background_file_id,
|
||||
background_blur_hash, position, created, updated, parent_project_id)
|
||||
select id,
|
||||
title,
|
||||
description,
|
||||
identifier,
|
||||
hex_color,
|
||||
owner_id,
|
||||
is_archived,
|
||||
background_file_id,
|
||||
background_blur_hash,
|
||||
position,
|
||||
created,
|
||||
updated,
|
||||
parent_project_id
|
||||
from projects;
|
||||
|
||||
drop table projects;
|
||||
|
||||
alter table projects_dg_tmp
|
||||
rename to projects;
|
||||
|
||||
create index IDX_lists_owner_id
|
||||
on projects (owner_id);
|
||||
|
||||
create index IDX_projects_parent_project_id
|
||||
on projects (parent_project_id);
|
||||
|
||||
create unique index UQE_lists_id
|
||||
on projects (id);
|
||||
`
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20221228112131",
|
||||
Description: "make projects nestable",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(projects20221228112131{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allNamespaces := []*namespace20221228112131{}
|
||||
err = tx.Find(&allNamespaces)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// namespace id is the key
|
||||
namespacesToProjects := make(map[int64]*projects20221228112131)
|
||||
|
||||
for _, n := range allNamespaces {
|
||||
p := &projects20221228112131{
|
||||
Title: n.Title,
|
||||
Description: n.Description,
|
||||
OwnerID: n.OwnerID,
|
||||
HexColor: n.HexColor,
|
||||
IsArchived: n.IsArchived,
|
||||
Created: n.Created,
|
||||
Updated: n.Updated,
|
||||
}
|
||||
|
||||
_, err = tx.Insert(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
namespacesToProjects[n.ID] = p
|
||||
}
|
||||
|
||||
err = setParentProject(tx, namespacesToProjects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = setTeamNamespacesShare(tx, namespacesToProjects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = setUserNamespacesShare(tx, namespacesToProjects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return removeNamespaceLeftovers(tx)
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func setParentProject(tx *xorm.Engine, namespacesToProjects map[int64]*projects20221228112131) error {
|
||||
for namespaceID, project := range namespacesToProjects {
|
||||
_, err := tx.Where("namespace_id = ?", namespaceID).
|
||||
Update(&projects20221228112131{
|
||||
ParentProjectID: project.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setTeamNamespacesShare(tx *xorm.Engine, namespacesToProjects map[int64]*projects20221228112131) error {
|
||||
teamNamespaces := []*teamNamespace20221228112131{}
|
||||
err := tx.Find(&teamNamespaces)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tn := range teamNamespaces {
|
||||
_, err = tx.Insert(&teamProject20221228112131{
|
||||
TeamID: tn.TeamID,
|
||||
Right: tn.Right,
|
||||
Created: tn.Created,
|
||||
Updated: tn.Updated,
|
||||
ProjectID: namespacesToProjects[tn.NamespaceID].ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setUserNamespacesShare(tx *xorm.Engine, namespacesToProjects map[int64]*projects20221228112131) error {
|
||||
userNamespace := []*namespaceUser20221228112131{}
|
||||
err := tx.Find(&userNamespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, un := range userNamespace {
|
||||
_, err = tx.Insert(&projectUser20221228112131{
|
||||
UserID: un.UserID,
|
||||
Right: un.Right,
|
||||
Created: un.Created,
|
||||
Updated: un.Updated,
|
||||
ProjectID: namespacesToProjects[un.NamespaceID].ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeNamespaceLeftovers(tx *xorm.Engine) error {
|
||||
err := tx.DropTables("namespaces", "team_namespaces", "users_namespaces")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tx.Dialect().URI().DBType == schemas.SQLITE {
|
||||
_, err := tx.Exec(sqliteRemoveNamespaceColumn20221228112131)
|
||||
return err
|
||||
}
|
||||
|
||||
return dropTableColum(tx, "projects", "namespace_id")
|
||||
}
|
|
@ -24,13 +24,13 @@ import (
|
|||
|
||||
// BulkTask is the definition of a bulk update task
|
||||
type BulkTask struct {
|
||||
// A list of task ids to update
|
||||
// A project of task ids to update
|
||||
IDs []int64 `json:"task_ids"`
|
||||
Tasks []*Task `json:"-"`
|
||||
Task
|
||||
}
|
||||
|
||||
func (bt *BulkTask) checkIfTasksAreOnTheSameList(s *xorm.Session) (err error) {
|
||||
func (bt *BulkTask) checkIfTasksAreOnTheSameProject(s *xorm.Session) (err error) {
|
||||
// Get the tasks
|
||||
err = bt.GetTasksByIDs(s)
|
||||
if err != nil {
|
||||
|
@ -41,11 +41,11 @@ func (bt *BulkTask) checkIfTasksAreOnTheSameList(s *xorm.Session) (err error) {
|
|||
return ErrBulkTasksNeedAtLeastOne{}
|
||||
}
|
||||
|
||||
// Check if all tasks are in the same list
|
||||
var firstListID = bt.Tasks[0].ListID
|
||||
// Check if all tasks are in the same project
|
||||
var firstProjectID = bt.Tasks[0].ProjectID
|
||||
for _, t := range bt.Tasks {
|
||||
if t.ListID != firstListID {
|
||||
return ErrBulkTasksMustBeInSameList{firstListID, t.ListID}
|
||||
if t.ProjectID != firstProjectID {
|
||||
return ErrBulkTasksMustBeInSameProject{firstProjectID, t.ProjectID}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,13 +55,13 @@ func (bt *BulkTask) checkIfTasksAreOnTheSameList(s *xorm.Session) (err error) {
|
|||
// CanUpdate checks if a user is allowed to update a task
|
||||
func (bt *BulkTask) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
|
||||
err := bt.checkIfTasksAreOnTheSameList(s)
|
||||
err := bt.checkIfTasksAreOnTheSameProject(s)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// A user can update an task if he has write acces to its list
|
||||
l := &List{ID: bt.Tasks[0].ListID}
|
||||
// A user can update an task if he has write acces to its project
|
||||
l := &Project{ID: bt.Tasks[0].ProjectID}
|
||||
return l.CanWrite(s, a)
|
||||
}
|
||||
|
||||
|
@ -72,10 +72,10 @@ func (bt *BulkTask) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param task body models.BulkTask true "The task object. Looks like a normal task, the only difference is it uses an array of list_ids to update."
|
||||
// @Param task body models.BulkTask true "The task object. Looks like a normal task, the only difference is it uses an array of project_ids to update."
|
||||
// @Success 200 {object} models.Task "The updated task object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid task object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the task (aka its list)"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the task (aka its project)"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/bulk [post]
|
||||
func (bt *BulkTask) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
|
|
@ -47,7 +47,7 @@ func TestBulkTask_Update(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "Test with one task on different list",
|
||||
name: "Test with one task on different project",
|
||||
fields: fields{
|
||||
IDs: []int64{10, 11, 12, 13},
|
||||
Task: Task{
|
||||
|
|
|
@ -110,222 +110,194 @@ func (err ValidationHTTPError) Error() string {
|
|||
}
|
||||
|
||||
// ===========
|
||||
// List errors
|
||||
// Project errors
|
||||
// ===========
|
||||
|
||||
// ErrListDoesNotExist represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist.
|
||||
type ErrListDoesNotExist struct {
|
||||
// ErrProjectDoesNotExist represents a "ErrProjectDoesNotExist" kind of error. Used if the project does not exist.
|
||||
type ErrProjectDoesNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrListDoesNotExist checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrListDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrListDoesNotExist)
|
||||
// IsErrProjectDoesNotExist checks if an error is a ErrProjectDoesNotExist.
|
||||
func IsErrProjectDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrProjectDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrListDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("List does not exist [ID: %d]", err.ID)
|
||||
func (err ErrProjectDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("Project does not exist [ID: %d]", err.ID)
|
||||
}
|
||||
|
||||
// ErrCodeListDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeListDoesNotExist = 3001
|
||||
// ErrCodeProjectDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeProjectDoesNotExist = 3001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListDoesNotExist) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeListDoesNotExist, Message: "This list does not exist."}
|
||||
func (err ErrProjectDoesNotExist) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeProjectDoesNotExist, Message: "This project does not exist."}
|
||||
}
|
||||
|
||||
// ErrNeedToHaveListReadAccess represents an error, where the user dont has read access to that List
|
||||
type ErrNeedToHaveListReadAccess struct {
|
||||
ListID int64
|
||||
UserID int64
|
||||
// ErrNeedToHaveProjectReadAccess represents an error, where the user dont has read access to that Project
|
||||
type ErrNeedToHaveProjectReadAccess struct {
|
||||
ProjectID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrNeedToHaveListReadAccess checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrNeedToHaveListReadAccess(err error) bool {
|
||||
_, ok := err.(ErrNeedToHaveListReadAccess)
|
||||
// IsErrNeedToHaveProjectReadAccess checks if an error is a ErrProjectDoesNotExist.
|
||||
func IsErrNeedToHaveProjectReadAccess(err error) bool {
|
||||
_, ok := err.(ErrNeedToHaveProjectReadAccess)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNeedToHaveListReadAccess) Error() string {
|
||||
return fmt.Sprintf("User needs to have read access to that list [ListID: %d, UserID: %d]", err.ListID, err.UserID)
|
||||
func (err ErrNeedToHaveProjectReadAccess) Error() string {
|
||||
return fmt.Sprintf("User needs to have read access to that project [ProjectID: %d, UserID: %d]", err.ProjectID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeNeedToHaveListReadAccess holds the unique world-error code of this error
|
||||
const ErrCodeNeedToHaveListReadAccess = 3004
|
||||
// ErrCodeNeedToHaveProjectReadAccess holds the unique world-error code of this error
|
||||
const ErrCodeNeedToHaveProjectReadAccess = 3004
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNeedToHaveListReadAccess) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeNeedToHaveListReadAccess, Message: "You need to have read access to this list."}
|
||||
func (err ErrNeedToHaveProjectReadAccess) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeNeedToHaveProjectReadAccess, Message: "You need to have read access to this project."}
|
||||
}
|
||||
|
||||
// ErrListTitleCannotBeEmpty represents a "ErrListTitleCannotBeEmpty" kind of error. Used if the list does not exist.
|
||||
type ErrListTitleCannotBeEmpty struct{}
|
||||
// ErrProjectTitleCannotBeEmpty represents a "ErrProjectTitleCannotBeEmpty" kind of error. Used if the project does not exist.
|
||||
type ErrProjectTitleCannotBeEmpty struct{}
|
||||
|
||||
// IsErrListTitleCannotBeEmpty checks if an error is a ErrListTitleCannotBeEmpty.
|
||||
func IsErrListTitleCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrListTitleCannotBeEmpty)
|
||||
// IsErrProjectTitleCannotBeEmpty checks if an error is a ErrProjectTitleCannotBeEmpty.
|
||||
func IsErrProjectTitleCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrProjectTitleCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrListTitleCannotBeEmpty) Error() string {
|
||||
return "List title cannot be empty."
|
||||
func (err ErrProjectTitleCannotBeEmpty) Error() string {
|
||||
return "Project title cannot be empty."
|
||||
}
|
||||
|
||||
// ErrCodeListTitleCannotBeEmpty holds the unique world-error code of this error
|
||||
const ErrCodeListTitleCannotBeEmpty = 3005
|
||||
// ErrCodeProjectTitleCannotBeEmpty holds the unique world-error code of this error
|
||||
const ErrCodeProjectTitleCannotBeEmpty = 3005
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListTitleCannotBeEmpty) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeListTitleCannotBeEmpty, Message: "You must provide at least a list title."}
|
||||
func (err ErrProjectTitleCannotBeEmpty) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeProjectTitleCannotBeEmpty, Message: "You must provide at least a project title."}
|
||||
}
|
||||
|
||||
// ErrListShareDoesNotExist represents a "ErrListShareDoesNotExist" kind of error. Used if the list share does not exist.
|
||||
type ErrListShareDoesNotExist struct {
|
||||
// ErrProjectShareDoesNotExist represents a "ErrProjectShareDoesNotExist" kind of error. Used if the project share does not exist.
|
||||
type ErrProjectShareDoesNotExist struct {
|
||||
ID int64
|
||||
Hash string
|
||||
}
|
||||
|
||||
// IsErrListShareDoesNotExist checks if an error is a ErrListShareDoesNotExist.
|
||||
func IsErrListShareDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrListShareDoesNotExist)
|
||||
// IsErrProjectShareDoesNotExist checks if an error is a ErrProjectShareDoesNotExist.
|
||||
func IsErrProjectShareDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrProjectShareDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrListShareDoesNotExist) Error() string {
|
||||
return "List share does not exist."
|
||||
func (err ErrProjectShareDoesNotExist) Error() string {
|
||||
return "Project share does not exist."
|
||||
}
|
||||
|
||||
// ErrCodeListShareDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeListShareDoesNotExist = 3006
|
||||
// ErrCodeProjectShareDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeProjectShareDoesNotExist = 3006
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListShareDoesNotExist) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeListShareDoesNotExist, Message: "The list share does not exist."}
|
||||
func (err ErrProjectShareDoesNotExist) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeProjectShareDoesNotExist, Message: "The project share does not exist."}
|
||||
}
|
||||
|
||||
// ErrListIdentifierIsNotUnique represents a "ErrListIdentifierIsNotUnique" kind of error. Used if the provided list identifier is not unique.
|
||||
type ErrListIdentifierIsNotUnique struct {
|
||||
// ErrProjectIdentifierIsNotUnique represents a "ErrProjectIdentifierIsNotUnique" kind of error. Used if the provided project identifier is not unique.
|
||||
type ErrProjectIdentifierIsNotUnique struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
// IsErrListIdentifierIsNotUnique checks if an error is a ErrListIdentifierIsNotUnique.
|
||||
func IsErrListIdentifierIsNotUnique(err error) bool {
|
||||
_, ok := err.(ErrListIdentifierIsNotUnique)
|
||||
// IsErrProjectIdentifierIsNotUnique checks if an error is a ErrProjectIdentifierIsNotUnique.
|
||||
func IsErrProjectIdentifierIsNotUnique(err error) bool {
|
||||
_, ok := err.(ErrProjectIdentifierIsNotUnique)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrListIdentifierIsNotUnique) Error() string {
|
||||
return "List identifier is not unique."
|
||||
func (err ErrProjectIdentifierIsNotUnique) Error() string {
|
||||
return "Project identifier is not unique."
|
||||
}
|
||||
|
||||
// ErrCodeListIdentifierIsNotUnique holds the unique world-error code of this error
|
||||
const ErrCodeListIdentifierIsNotUnique = 3007
|
||||
// ErrCodeProjectIdentifierIsNotUnique holds the unique world-error code of this error
|
||||
const ErrCodeProjectIdentifierIsNotUnique = 3007
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListIdentifierIsNotUnique) HTTPError() web.HTTPError {
|
||||
func (err ErrProjectIdentifierIsNotUnique) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
Code: ErrCodeListIdentifierIsNotUnique,
|
||||
Message: "A list with this identifier already exists.",
|
||||
Code: ErrCodeProjectIdentifierIsNotUnique,
|
||||
Message: "A project with this identifier already exists.",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrListIsArchived represents an error, where a list is archived
|
||||
type ErrListIsArchived struct {
|
||||
ListID int64
|
||||
// ErrProjectIsArchived represents an error, where a project is archived
|
||||
type ErrProjectIsArchived struct {
|
||||
ProjectID int64
|
||||
}
|
||||
|
||||
// IsErrListIsArchived checks if an error is a list is archived error.
|
||||
func IsErrListIsArchived(err error) bool {
|
||||
_, ok := err.(ErrListIsArchived)
|
||||
// IsErrProjectIsArchived checks if an error is a project is archived error.
|
||||
func IsErrProjectIsArchived(err error) bool {
|
||||
_, ok := err.(ErrProjectIsArchived)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrListIsArchived) Error() string {
|
||||
return fmt.Sprintf("List is archived [ListID: %d]", err.ListID)
|
||||
func (err ErrProjectIsArchived) Error() string {
|
||||
return fmt.Sprintf("Project is archived [ProjectID: %d]", err.ProjectID)
|
||||
}
|
||||
|
||||
// ErrCodeListIsArchived holds the unique world-error code of this error
|
||||
const ErrCodeListIsArchived = 3008
|
||||
// ErrCodeProjectIsArchived holds the unique world-error code of this error
|
||||
const ErrCodeProjectIsArchived = 3008
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListIsArchived) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeListIsArchived, Message: "This list is archived. Editing or creating new tasks is not possible."}
|
||||
func (err ErrProjectIsArchived) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeProjectIsArchived, Message: "This project is archived. Editing or creating new tasks is not possible."}
|
||||
}
|
||||
|
||||
// ErrListCannotBelongToAPseudoNamespace represents an error where a list cannot belong to a pseudo namespace
|
||||
type ErrListCannotBelongToAPseudoNamespace struct {
|
||||
ListID int64
|
||||
NamespaceID int64
|
||||
// ErrProjectCannotBelongToAPseudoParentProject represents an error where a project cannot belong to a pseudo project
|
||||
type ErrProjectCannotBelongToAPseudoParentProject struct {
|
||||
ProjectID int64
|
||||
ParentProjectID int64
|
||||
}
|
||||
|
||||
// IsErrListCannotBelongToAPseudoNamespace checks if an error is a list is archived error.
|
||||
func IsErrListCannotBelongToAPseudoNamespace(err error) bool {
|
||||
_, ok := err.(*ErrListCannotBelongToAPseudoNamespace)
|
||||
// IsErrProjectCannotBelongToAPseudoParentProject checks if an error is a project is archived error.
|
||||
func IsErrProjectCannotBelongToAPseudoParentProject(err error) bool {
|
||||
_, ok := err.(*ErrProjectCannotBelongToAPseudoParentProject)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrListCannotBelongToAPseudoNamespace) Error() string {
|
||||
return fmt.Sprintf("List cannot belong to a pseudo namespace [ListID: %d, NamespaceID: %d]", err.ListID, err.NamespaceID)
|
||||
func (err *ErrProjectCannotBelongToAPseudoParentProject) Error() string {
|
||||
return fmt.Sprintf("Project cannot belong to a pseudo parent project [ProjectID: %d, ParentProjectID: %d]", err.ProjectID, err.ParentProjectID)
|
||||
}
|
||||
|
||||
// ErrCodeListCannotBelongToAPseudoNamespace holds the unique world-error code of this error
|
||||
const ErrCodeListCannotBelongToAPseudoNamespace = 3009
|
||||
// ErrCodeProjectCannotBelongToAPseudoParentProject holds the unique world-error code of this error
|
||||
const ErrCodeProjectCannotBelongToAPseudoParentProject = 3009
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err *ErrListCannotBelongToAPseudoNamespace) HTTPError() web.HTTPError {
|
||||
func (err *ErrProjectCannotBelongToAPseudoParentProject) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeListCannotBelongToAPseudoNamespace,
|
||||
Message: "This list cannot belong a dynamically generated namespace.",
|
||||
Code: ErrCodeProjectCannotBelongToAPseudoParentProject,
|
||||
Message: "This project cannot belong a dynamically generated project.",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrListMustBelongToANamespace represents an error where a list must belong to a namespace
|
||||
type ErrListMustBelongToANamespace struct {
|
||||
ListID int64
|
||||
NamespaceID int64
|
||||
}
|
||||
// ==============
|
||||
// Project errors
|
||||
// ==============
|
||||
|
||||
// IsErrListMustBelongToANamespace checks if an error is a list must belong to a namespace error.
|
||||
func IsErrListMustBelongToANamespace(err error) bool {
|
||||
_, ok := err.(*ErrListMustBelongToANamespace)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrListMustBelongToANamespace) Error() string {
|
||||
return fmt.Sprintf("List must belong to a namespace [ListID: %d, NamespaceID: %d]", err.ListID, err.NamespaceID)
|
||||
}
|
||||
|
||||
// ErrCodeListMustBelongToANamespace holds the unique world-error code of this error
|
||||
const ErrCodeListMustBelongToANamespace = 3010
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err *ErrListMustBelongToANamespace) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeListMustBelongToANamespace,
|
||||
Message: "This list must belong to a namespace.",
|
||||
}
|
||||
}
|
||||
|
||||
// ================
|
||||
// List task errors
|
||||
// ================
|
||||
|
||||
// ErrTaskCannotBeEmpty represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist.
|
||||
// ErrTaskCannotBeEmpty represents a "ErrProjectDoesNotExist" kind of error. Used if the project does not exist.
|
||||
type ErrTaskCannotBeEmpty struct{}
|
||||
|
||||
// IsErrTaskCannotBeEmpty checks if an error is a ErrListDoesNotExist.
|
||||
// IsErrTaskCannotBeEmpty checks if an error is a ErrProjectDoesNotExist.
|
||||
func IsErrTaskCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrTaskCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTaskCannotBeEmpty) Error() string {
|
||||
return "List task title cannot be empty."
|
||||
return "Project task title cannot be empty."
|
||||
}
|
||||
|
||||
// ErrCodeTaskCannotBeEmpty holds the unique world-error code of this error
|
||||
|
@ -333,22 +305,22 @@ const ErrCodeTaskCannotBeEmpty = 4001
|
|||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTaskCannotBeEmpty) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeTaskCannotBeEmpty, Message: "You must provide at least a list task title."}
|
||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeTaskCannotBeEmpty, Message: "You must provide at least a project task title."}
|
||||
}
|
||||
|
||||
// ErrTaskDoesNotExist represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist.
|
||||
// ErrTaskDoesNotExist represents a "ErrProjectDoesNotExist" kind of error. Used if the project does not exist.
|
||||
type ErrTaskDoesNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrTaskDoesNotExist checks if an error is a ErrListDoesNotExist.
|
||||
// IsErrTaskDoesNotExist checks if an error is a ErrProjectDoesNotExist.
|
||||
func IsErrTaskDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrTaskDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTaskDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("List task does not exist. [ID: %d]", err.ID)
|
||||
return fmt.Sprintf("Project task does not exist. [ID: %d]", err.ID)
|
||||
}
|
||||
|
||||
// ErrCodeTaskDoesNotExist holds the unique world-error code of this error
|
||||
|
@ -359,28 +331,28 @@ func (err ErrTaskDoesNotExist) HTTPError() web.HTTPError {
|
|||
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTaskDoesNotExist, Message: "This task does not exist"}
|
||||
}
|
||||
|
||||
// ErrBulkTasksMustBeInSameList represents a "ErrBulkTasksMustBeInSameList" kind of error.
|
||||
type ErrBulkTasksMustBeInSameList struct {
|
||||
// ErrBulkTasksMustBeInSameProject represents a "ErrBulkTasksMustBeInSameProject" kind of error.
|
||||
type ErrBulkTasksMustBeInSameProject struct {
|
||||
ShouldBeID int64
|
||||
IsID int64
|
||||
}
|
||||
|
||||
// IsErrBulkTasksMustBeInSameList checks if an error is a ErrBulkTasksMustBeInSameList.
|
||||
func IsErrBulkTasksMustBeInSameList(err error) bool {
|
||||
_, ok := err.(ErrBulkTasksMustBeInSameList)
|
||||
// IsErrBulkTasksMustBeInSameProject checks if an error is a ErrBulkTasksMustBeInSameProject.
|
||||
func IsErrBulkTasksMustBeInSameProject(err error) bool {
|
||||
_, ok := err.(ErrBulkTasksMustBeInSameProject)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrBulkTasksMustBeInSameList) Error() string {
|
||||
return fmt.Sprintf("All bulk editing tasks must be in the same list. [Should be: %d, is: %d]", err.ShouldBeID, err.IsID)
|
||||
func (err ErrBulkTasksMustBeInSameProject) Error() string {
|
||||
return fmt.Sprintf("All bulk editing tasks must be in the same project. [Should be: %d, is: %d]", err.ShouldBeID, err.IsID)
|
||||
}
|
||||
|
||||
// ErrCodeBulkTasksMustBeInSameList holds the unique world-error code of this error
|
||||
const ErrCodeBulkTasksMustBeInSameList = 4003
|
||||
// ErrCodeBulkTasksMustBeInSameProject holds the unique world-error code of this error
|
||||
const ErrCodeBulkTasksMustBeInSameProject = 4003
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrBulkTasksMustBeInSameList) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeBulkTasksMustBeInSameList, Message: "All tasks must be in the same list."}
|
||||
func (err ErrBulkTasksMustBeInSameProject) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeBulkTasksMustBeInSameProject, Message: "All tasks must be in the same project."}
|
||||
}
|
||||
|
||||
// ErrBulkTasksNeedAtLeastOne represents a "ErrBulkTasksNeedAtLeastOne" kind of error.
|
||||
|
@ -875,176 +847,6 @@ func (err ErrUserAlreadyAssigned) HTTPError() web.HTTPError {
|
|||
}
|
||||
}
|
||||
|
||||
// =================
|
||||
// Namespace errors
|
||||
// =================
|
||||
|
||||
// ErrNamespaceDoesNotExist represents a "ErrNamespaceDoesNotExist" kind of error. Used if the namespace does not exist.
|
||||
type ErrNamespaceDoesNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrNamespaceDoesNotExist checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrNamespaceDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrNamespaceDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNamespaceDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("Namespace does not exist [ID: %d]", err.ID)
|
||||
}
|
||||
|
||||
// ErrCodeNamespaceDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeNamespaceDoesNotExist = 5001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNamespaceDoesNotExist) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeNamespaceDoesNotExist, Message: "Namespace not found."}
|
||||
}
|
||||
|
||||
// ErrUserDoesNotHaveAccessToNamespace represents an error, where the user is not the owner of that namespace (used i.e. when deleting a namespace)
|
||||
type ErrUserDoesNotHaveAccessToNamespace struct {
|
||||
NamespaceID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrUserDoesNotHaveAccessToNamespace checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrUserDoesNotHaveAccessToNamespace(err error) bool {
|
||||
_, ok := err.(ErrUserDoesNotHaveAccessToNamespace)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserDoesNotHaveAccessToNamespace) Error() string {
|
||||
return fmt.Sprintf("User does not have access to the namespace [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeUserDoesNotHaveAccessToNamespace holds the unique world-error code of this error
|
||||
const ErrCodeUserDoesNotHaveAccessToNamespace = 5003
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserDoesNotHaveAccessToNamespace) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToNamespace, Message: "This user does not have access to the namespace."}
|
||||
}
|
||||
|
||||
// ErrNamespaceNameCannotBeEmpty represents an error, where a namespace name is empty.
|
||||
type ErrNamespaceNameCannotBeEmpty struct {
|
||||
NamespaceID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrNamespaceNameCannotBeEmpty checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrNamespaceNameCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrNamespaceNameCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNamespaceNameCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("Namespace name cannot be empty [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeNamespaceNameCannotBeEmpty holds the unique world-error code of this error
|
||||
const ErrCodeNamespaceNameCannotBeEmpty = 5006
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNamespaceNameCannotBeEmpty) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeNamespaceNameCannotBeEmpty, Message: "The namespace name cannot be empty."}
|
||||
}
|
||||
|
||||
// ErrNeedToHaveNamespaceReadAccess represents an error, where the user is not the owner of that namespace (used i.e. when deleting a namespace)
|
||||
type ErrNeedToHaveNamespaceReadAccess struct {
|
||||
NamespaceID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrNeedToHaveNamespaceReadAccess checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrNeedToHaveNamespaceReadAccess(err error) bool {
|
||||
_, ok := err.(ErrNeedToHaveNamespaceReadAccess)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNeedToHaveNamespaceReadAccess) Error() string {
|
||||
return fmt.Sprintf("User does not have access to that namespace [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeNeedToHaveNamespaceReadAccess holds the unique world-error code of this error
|
||||
const ErrCodeNeedToHaveNamespaceReadAccess = 5009
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNeedToHaveNamespaceReadAccess) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeNeedToHaveNamespaceReadAccess, Message: "You need to have namespace read access to do this."}
|
||||
}
|
||||
|
||||
// ErrTeamDoesNotHaveAccessToNamespace represents an error, where the Team is not the owner of that namespace (used i.e. when deleting a namespace)
|
||||
type ErrTeamDoesNotHaveAccessToNamespace struct {
|
||||
NamespaceID int64
|
||||
TeamID int64
|
||||
}
|
||||
|
||||
// IsErrTeamDoesNotHaveAccessToNamespace checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrTeamDoesNotHaveAccessToNamespace(err error) bool {
|
||||
_, ok := err.(ErrTeamDoesNotHaveAccessToNamespace)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamDoesNotHaveAccessToNamespace) Error() string {
|
||||
return fmt.Sprintf("Team does not have access to that namespace [NamespaceID: %d, TeamID: %d]", err.NamespaceID, err.TeamID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamDoesNotHaveAccessToNamespace holds the unique world-error code of this error
|
||||
const ErrCodeTeamDoesNotHaveAccessToNamespace = 5010
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTeamDoesNotHaveAccessToNamespace) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToNamespace, Message: "You need to have access to this namespace to do this."}
|
||||
}
|
||||
|
||||
// ErrUserAlreadyHasNamespaceAccess represents an error where a user already has access to a namespace
|
||||
type ErrUserAlreadyHasNamespaceAccess struct {
|
||||
UserID int64
|
||||
NamespaceID int64
|
||||
}
|
||||
|
||||
// IsErrUserAlreadyHasNamespaceAccess checks if an error is ErrUserAlreadyHasNamespaceAccess.
|
||||
func IsErrUserAlreadyHasNamespaceAccess(err error) bool {
|
||||
_, ok := err.(ErrUserAlreadyHasNamespaceAccess)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserAlreadyHasNamespaceAccess) Error() string {
|
||||
return fmt.Sprintf("User already has access to that namespace. [User ID: %d, Namespace ID: %d]", err.UserID, err.NamespaceID)
|
||||
}
|
||||
|
||||
// ErrCodeUserAlreadyHasNamespaceAccess holds the unique world-error code of this error
|
||||
const ErrCodeUserAlreadyHasNamespaceAccess = 5011
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserAlreadyHasNamespaceAccess) HTTPError() web.HTTPError {
|
||||
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
|
||||
// ============
|
||||
|
@ -1054,7 +856,7 @@ type ErrTeamNameCannotBeEmpty struct {
|
|||
TeamID int64
|
||||
}
|
||||
|
||||
// IsErrTeamNameCannotBeEmpty checks if an error is a ErrNamespaceDoesNotExist.
|
||||
// IsErrTeamNameCannotBeEmpty checks if an error is a ErrTeamNameCannotBeEmpty.
|
||||
func IsErrTeamNameCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrTeamNameCannotBeEmpty)
|
||||
return ok
|
||||
|
@ -1095,7 +897,7 @@ func (err ErrTeamDoesNotExist) HTTPError() web.HTTPError {
|
|||
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "This team does not exist."}
|
||||
}
|
||||
|
||||
// ErrTeamAlreadyHasAccess represents an error where a team already has access to a list/namespace
|
||||
// ErrTeamAlreadyHasAccess represents an error where a team already has access to a project
|
||||
type ErrTeamAlreadyHasAccess struct {
|
||||
TeamID int64
|
||||
ID int64
|
||||
|
@ -1167,38 +969,38 @@ func (err ErrCannotDeleteLastTeamMember) HTTPError() web.HTTPError {
|
|||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeCannotDeleteLastTeamMember, Message: "You cannot delete the last member of a team."}
|
||||
}
|
||||
|
||||
// ErrTeamDoesNotHaveAccessToList represents an error, where the Team is not the owner of that List (used i.e. when deleting a List)
|
||||
type ErrTeamDoesNotHaveAccessToList struct {
|
||||
ListID int64
|
||||
TeamID int64
|
||||
// ErrTeamDoesNotHaveAccessToProject represents an error, where the Team is not the owner of that Project (used i.e. when deleting a Project)
|
||||
type ErrTeamDoesNotHaveAccessToProject struct {
|
||||
ProjectID int64
|
||||
TeamID int64
|
||||
}
|
||||
|
||||
// IsErrTeamDoesNotHaveAccessToList checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrTeamDoesNotHaveAccessToList(err error) bool {
|
||||
_, ok := err.(ErrTeamDoesNotHaveAccessToList)
|
||||
// IsErrTeamDoesNotHaveAccessToProject checks if an error is a ErrProjectDoesNotExist.
|
||||
func IsErrTeamDoesNotHaveAccessToProject(err error) bool {
|
||||
_, ok := err.(ErrTeamDoesNotHaveAccessToProject)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamDoesNotHaveAccessToList) Error() string {
|
||||
return fmt.Sprintf("Team does not have access to the list [ListID: %d, TeamID: %d]", err.ListID, err.TeamID)
|
||||
func (err ErrTeamDoesNotHaveAccessToProject) Error() string {
|
||||
return fmt.Sprintf("Team does not have access to the project [ProjectID: %d, TeamID: %d]", err.ProjectID, err.TeamID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamDoesNotHaveAccessToList holds the unique world-error code of this error
|
||||
const ErrCodeTeamDoesNotHaveAccessToList = 6007
|
||||
// ErrCodeTeamDoesNotHaveAccessToProject holds the unique world-error code of this error
|
||||
const ErrCodeTeamDoesNotHaveAccessToProject = 6007
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTeamDoesNotHaveAccessToList) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToList, Message: "This team does not have access to the list."}
|
||||
func (err ErrTeamDoesNotHaveAccessToProject) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToProject, Message: "This team does not have access to the project."}
|
||||
}
|
||||
|
||||
// ====================
|
||||
// User <-> List errors
|
||||
// User <-> Project errors
|
||||
// ====================
|
||||
|
||||
// ErrUserAlreadyHasAccess represents an error where a user already has access to a list/namespace
|
||||
// ErrUserAlreadyHasAccess represents an error where a user already has access to a project
|
||||
type ErrUserAlreadyHasAccess struct {
|
||||
UserID int64
|
||||
ListID int64
|
||||
UserID int64
|
||||
ProjectID int64
|
||||
}
|
||||
|
||||
// IsErrUserAlreadyHasAccess checks if an error is ErrUserAlreadyHasAccess.
|
||||
|
@ -1208,7 +1010,7 @@ func IsErrUserAlreadyHasAccess(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrUserAlreadyHasAccess) Error() string {
|
||||
return fmt.Sprintf("User already has access to that list. [User ID: %d, List ID: %d]", err.UserID, err.ListID)
|
||||
return fmt.Sprintf("User already has access to that project. [User ID: %d, Project ID: %d]", err.UserID, err.ProjectID)
|
||||
}
|
||||
|
||||
// ErrCodeUserAlreadyHasAccess holds the unique world-error code of this error
|
||||
|
@ -1216,31 +1018,31 @@ const ErrCodeUserAlreadyHasAccess = 7002
|
|||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserAlreadyHasAccess) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserAlreadyHasAccess, Message: "This user already has access to this list."}
|
||||
return web.HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserAlreadyHasAccess, Message: "This user already has access to this project."}
|
||||
}
|
||||
|
||||
// ErrUserDoesNotHaveAccessToList represents an error, where the user is not the owner of that List (used i.e. when deleting a List)
|
||||
type ErrUserDoesNotHaveAccessToList struct {
|
||||
ListID int64
|
||||
UserID int64
|
||||
// ErrUserDoesNotHaveAccessToProject represents an error, where the user is not the owner of that Project (used i.e. when deleting a Project)
|
||||
type ErrUserDoesNotHaveAccessToProject struct {
|
||||
ProjectID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrUserDoesNotHaveAccessToList checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrUserDoesNotHaveAccessToList(err error) bool {
|
||||
_, ok := err.(ErrUserDoesNotHaveAccessToList)
|
||||
// IsErrUserDoesNotHaveAccessToProject checks if an error is a ErrProjectDoesNotExist.
|
||||
func IsErrUserDoesNotHaveAccessToProject(err error) bool {
|
||||
_, ok := err.(ErrUserDoesNotHaveAccessToProject)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserDoesNotHaveAccessToList) Error() string {
|
||||
return fmt.Sprintf("User does not have access to the list [ListID: %d, UserID: %d]", err.ListID, err.UserID)
|
||||
func (err ErrUserDoesNotHaveAccessToProject) Error() string {
|
||||
return fmt.Sprintf("User does not have access to the project [ProjectID: %d, UserID: %d]", err.ProjectID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeUserDoesNotHaveAccessToList holds the unique world-error code of this error
|
||||
const ErrCodeUserDoesNotHaveAccessToList = 7003
|
||||
// ErrCodeUserDoesNotHaveAccessToProject holds the unique world-error code of this error
|
||||
const ErrCodeUserDoesNotHaveAccessToProject = 7003
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserDoesNotHaveAccessToList) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToList, Message: "This user does not have access to the list."}
|
||||
func (err ErrUserDoesNotHaveAccessToProject) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToProject, Message: "This user does not have access to the project."}
|
||||
}
|
||||
|
||||
// =============
|
||||
|
@ -1392,38 +1194,38 @@ func (err ErrBucketDoesNotExist) HTTPError() web.HTTPError {
|
|||
}
|
||||
}
|
||||
|
||||
// ErrBucketDoesNotBelongToList represents an error where a kanban bucket does not belong to a list
|
||||
type ErrBucketDoesNotBelongToList struct {
|
||||
BucketID int64
|
||||
ListID int64
|
||||
// ErrBucketDoesNotBelongToProject represents an error where a kanban bucket does not belong to a project
|
||||
type ErrBucketDoesNotBelongToProject struct {
|
||||
BucketID int64
|
||||
ProjectID int64
|
||||
}
|
||||
|
||||
// IsErrBucketDoesNotBelongToList checks if an error is ErrBucketDoesNotBelongToList.
|
||||
func IsErrBucketDoesNotBelongToList(err error) bool {
|
||||
_, ok := err.(ErrBucketDoesNotBelongToList)
|
||||
// IsErrBucketDoesNotBelongToProject checks if an error is ErrBucketDoesNotBelongToProject.
|
||||
func IsErrBucketDoesNotBelongToProject(err error) bool {
|
||||
_, ok := err.(ErrBucketDoesNotBelongToProject)
|
||||
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)
|
||||
func (err ErrBucketDoesNotBelongToProject) Error() string {
|
||||
return fmt.Sprintf("Bucket does not not belong to project [BucketID: %d, ProjectID: %d]", err.BucketID, err.ProjectID)
|
||||
}
|
||||
|
||||
// ErrCodeBucketDoesNotBelongToList holds the unique world-error code of this error
|
||||
const ErrCodeBucketDoesNotBelongToList = 10002
|
||||
// ErrCodeBucketDoesNotBelongToProject holds the unique world-error code of this error
|
||||
const ErrCodeBucketDoesNotBelongToProject = 10002
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrBucketDoesNotBelongToList) HTTPError() web.HTTPError {
|
||||
func (err ErrBucketDoesNotBelongToProject) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
Code: ErrCodeBucketDoesNotBelongToList,
|
||||
Message: "This bucket does not belong to that list.",
|
||||
Code: ErrCodeBucketDoesNotBelongToProject,
|
||||
Message: "This bucket does not belong to that project.",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrCannotRemoveLastBucket represents an error where a kanban bucket is the last on a list and thus cannot be removed.
|
||||
// ErrCannotRemoveLastBucket represents an error where a kanban bucket is the last on a project and thus cannot be removed.
|
||||
type ErrCannotRemoveLastBucket struct {
|
||||
BucketID int64
|
||||
ListID int64
|
||||
BucketID int64
|
||||
ProjectID int64
|
||||
}
|
||||
|
||||
// IsErrCannotRemoveLastBucket checks if an error is ErrCannotRemoveLastBucket.
|
||||
|
@ -1433,7 +1235,7 @@ func IsErrCannotRemoveLastBucket(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrCannotRemoveLastBucket) Error() string {
|
||||
return fmt.Sprintf("Cannot remove last bucket of list [BucketID: %d, ListID: %d]", err.BucketID, err.ListID)
|
||||
return fmt.Sprintf("Cannot remove last bucket of project [BucketID: %d, ProjectID: %d]", err.BucketID, err.ProjectID)
|
||||
}
|
||||
|
||||
// ErrCodeCannotRemoveLastBucket holds the unique world-error code of this error
|
||||
|
@ -1444,7 +1246,7 @@ func (err ErrCannotRemoveLastBucket) HTTPError() web.HTTPError {
|
|||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeCannotRemoveLastBucket,
|
||||
Message: "You cannot remove the last bucket on this list.",
|
||||
Message: "You cannot remove the last bucket on this project.",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1477,32 +1279,32 @@ func (err ErrBucketLimitExceeded) HTTPError() web.HTTPError {
|
|||
}
|
||||
}
|
||||
|
||||
// ErrOnlyOneDoneBucketPerList represents an error where a bucket is set to the done bucket but one already exists for its list.
|
||||
type ErrOnlyOneDoneBucketPerList struct {
|
||||
// ErrOnlyOneDoneBucketPerProject represents an error where a bucket is set to the done bucket but one already exists for its project.
|
||||
type ErrOnlyOneDoneBucketPerProject struct {
|
||||
BucketID int64
|
||||
ListID int64
|
||||
ProjectID int64
|
||||
DoneBucketID int64
|
||||
}
|
||||
|
||||
// IsErrOnlyOneDoneBucketPerList checks if an error is ErrBucketLimitExceeded.
|
||||
func IsErrOnlyOneDoneBucketPerList(err error) bool {
|
||||
_, ok := err.(*ErrOnlyOneDoneBucketPerList)
|
||||
// IsErrOnlyOneDoneBucketPerProject checks if an error is ErrBucketLimitExceeded.
|
||||
func IsErrOnlyOneDoneBucketPerProject(err error) bool {
|
||||
_, ok := err.(*ErrOnlyOneDoneBucketPerProject)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrOnlyOneDoneBucketPerList) Error() string {
|
||||
return fmt.Sprintf("There can be only one done bucket per list [BucketID: %d, ListID: %d]", err.BucketID, err.ListID)
|
||||
func (err *ErrOnlyOneDoneBucketPerProject) Error() string {
|
||||
return fmt.Sprintf("There can be only one done bucket per project [BucketID: %d, ProjectID: %d]", err.BucketID, err.ProjectID)
|
||||
}
|
||||
|
||||
// ErrCodeOnlyOneDoneBucketPerList holds the unique world-error code of this error
|
||||
const ErrCodeOnlyOneDoneBucketPerList = 10005
|
||||
// ErrCodeOnlyOneDoneBucketPerProject holds the unique world-error code of this error
|
||||
const ErrCodeOnlyOneDoneBucketPerProject = 10005
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err *ErrOnlyOneDoneBucketPerList) HTTPError() web.HTTPError {
|
||||
func (err *ErrOnlyOneDoneBucketPerProject) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeOnlyOneDoneBucketPerList,
|
||||
Message: "There can be only one done bucket per list.",
|
||||
Code: ErrCodeOnlyOneDoneBucketPerProject,
|
||||
Message: "There can be only one done bucket per project.",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,16 +21,6 @@ import (
|
|||
"code.vikunja.io/web"
|
||||
)
|
||||
|
||||
// DataExportRequestEvent represents a DataExportRequestEvent event
|
||||
type DataExportRequestEvent struct {
|
||||
User *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for DataExportRequestEvent
|
||||
func (t *DataExportRequestEvent) Name() string {
|
||||
return "user.export.request"
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Task Events //
|
||||
/////////////////
|
||||
|
@ -104,130 +94,69 @@ func (t *TaskCommentUpdatedEvent) Name() string {
|
|||
return "task.comment.edited"
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Namespace Events //
|
||||
//////////////////////
|
||||
|
||||
// NamespaceCreatedEvent represents an event where a namespace has been created
|
||||
type NamespaceCreatedEvent struct {
|
||||
Namespace *Namespace
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for NamespaceCreatedEvent
|
||||
func (n *NamespaceCreatedEvent) Name() string {
|
||||
return "namespace.created"
|
||||
}
|
||||
|
||||
// NamespaceUpdatedEvent represents an event where a namespace has been updated
|
||||
type NamespaceUpdatedEvent struct {
|
||||
Namespace *Namespace
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for NamespaceUpdatedEvent
|
||||
func (n *NamespaceUpdatedEvent) Name() string {
|
||||
return "namespace.updated"
|
||||
}
|
||||
|
||||
// NamespaceDeletedEvent represents a NamespaceDeletedEvent event
|
||||
type NamespaceDeletedEvent struct {
|
||||
Namespace *Namespace
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// TopicName defines the name for NamespaceDeletedEvent
|
||||
func (t *NamespaceDeletedEvent) Name() string {
|
||||
return "namespace.deleted"
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// List Events //
|
||||
// Project Events //
|
||||
/////////////////
|
||||
|
||||
// ListCreatedEvent represents an event where a list has been created
|
||||
type ListCreatedEvent struct {
|
||||
List *List
|
||||
Doer *user.User
|
||||
// ProjectCreatedEvent represents an event where a project has been created
|
||||
type ProjectCreatedEvent struct {
|
||||
Project *Project
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for ListCreatedEvent
|
||||
func (l *ListCreatedEvent) Name() string {
|
||||
return "list.created"
|
||||
// Name defines the name for ProjectCreatedEvent
|
||||
func (l *ProjectCreatedEvent) Name() string {
|
||||
return "project.created"
|
||||
}
|
||||
|
||||
// ListUpdatedEvent represents an event where a list has been updated
|
||||
type ListUpdatedEvent struct {
|
||||
List *List
|
||||
Doer web.Auth
|
||||
// ProjectUpdatedEvent represents an event where a project has been updated
|
||||
type ProjectUpdatedEvent struct {
|
||||
Project *Project
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for ListUpdatedEvent
|
||||
func (l *ListUpdatedEvent) Name() string {
|
||||
return "list.updated"
|
||||
// Name defines the name for ProjectUpdatedEvent
|
||||
func (l *ProjectUpdatedEvent) Name() string {
|
||||
return "project.updated"
|
||||
}
|
||||
|
||||
// ListDeletedEvent represents an event where a list has been deleted
|
||||
type ListDeletedEvent struct {
|
||||
List *List
|
||||
Doer web.Auth
|
||||
// ProjectDeletedEvent represents an event where a project has been deleted
|
||||
type ProjectDeletedEvent struct {
|
||||
Project *Project
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for ListDeletedEvent
|
||||
func (t *ListDeletedEvent) Name() string {
|
||||
return "list.deleted"
|
||||
// Name defines the name for ProjectDeletedEvent
|
||||
func (t *ProjectDeletedEvent) Name() string {
|
||||
return "project.deleted"
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// Sharing Events //
|
||||
////////////////////
|
||||
|
||||
// ListSharedWithUserEvent represents an event where a list has been shared with a user
|
||||
type ListSharedWithUserEvent struct {
|
||||
List *List
|
||||
User *user.User
|
||||
Doer web.Auth
|
||||
// ProjectSharedWithUserEvent represents an event where a project has been shared with a user
|
||||
type ProjectSharedWithUserEvent struct {
|
||||
Project *Project
|
||||
User *user.User
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for ListSharedWithUserEvent
|
||||
func (l *ListSharedWithUserEvent) Name() string {
|
||||
return "list.shared.user"
|
||||
// Name defines the name for ProjectSharedWithUserEvent
|
||||
func (l *ProjectSharedWithUserEvent) Name() string {
|
||||
return "project.shared.user"
|
||||
}
|
||||
|
||||
// ListSharedWithTeamEvent represents an event where a list has been shared with a team
|
||||
type ListSharedWithTeamEvent struct {
|
||||
List *List
|
||||
Team *Team
|
||||
Doer web.Auth
|
||||
// ProjectSharedWithTeamEvent represents an event where a project has been shared with a team
|
||||
type ProjectSharedWithTeamEvent struct {
|
||||
Project *Project
|
||||
Team *Team
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for ListSharedWithTeamEvent
|
||||
func (l *ListSharedWithTeamEvent) Name() string {
|
||||
return "list.shared.team"
|
||||
}
|
||||
|
||||
// NamespaceSharedWithUserEvent represents an event where a namespace has been shared with a user
|
||||
type NamespaceSharedWithUserEvent struct {
|
||||
Namespace *Namespace
|
||||
User *user.User
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for NamespaceSharedWithUserEvent
|
||||
func (n *NamespaceSharedWithUserEvent) Name() string {
|
||||
return "namespace.shared.user"
|
||||
}
|
||||
|
||||
// NamespaceSharedWithTeamEvent represents an event where a namespace has been shared with a team
|
||||
type NamespaceSharedWithTeamEvent struct {
|
||||
Namespace *Namespace
|
||||
Team *Team
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for NamespaceSharedWithTeamEvent
|
||||
func (n *NamespaceSharedWithTeamEvent) Name() string {
|
||||
return "namespace.shared.team"
|
||||
// Name defines the name for ProjectSharedWithTeamEvent
|
||||
func (l *ProjectSharedWithTeamEvent) Name() string {
|
||||
return "project.shared.team"
|
||||
}
|
||||
|
||||
/////////////////
|
||||
|
|
|
@ -57,12 +57,12 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
|
|||
defer dumpWriter.Close()
|
||||
|
||||
// Get the data
|
||||
err = exportListsAndTasks(s, u, dumpWriter)
|
||||
taskIDs, err := exportProjectsAndTasks(s, u, dumpWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Task attachment files
|
||||
err = exportTaskAttachments(s, u, dumpWriter)
|
||||
err = exportTaskAttachments(s, dumpWriter, taskIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
|
|||
return err
|
||||
}
|
||||
// Background files
|
||||
err = exportListBackgrounds(s, u, dumpWriter)
|
||||
err = exportProjectBackgrounds(s, u, dumpWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -121,59 +121,44 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
|
|||
})
|
||||
}
|
||||
|
||||
func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
|
||||
func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (taskIDs []int64, err error) {
|
||||
|
||||
namspaces, _, _, err := (&Namespace{IsArchived: true}).ReadAll(s, u, "", -1, 0)
|
||||
// Get all projects
|
||||
rawProjects, _, _, err := getRawProjectsForUser(
|
||||
s,
|
||||
&projectOptions{
|
||||
search: "",
|
||||
user: u,
|
||||
page: 0,
|
||||
perPage: -1,
|
||||
getArchived: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return taskIDs, err
|
||||
}
|
||||
|
||||
namespaceIDs := []int64{}
|
||||
namespaces := []*NamespaceWithListsAndTasks{}
|
||||
listMap := make(map[int64]*ListWithTasksAndBuckets)
|
||||
listIDs := []int64{}
|
||||
for _, n := range namspaces.([]*NamespaceWithLists) {
|
||||
if n.ID < 1 {
|
||||
// Don't include filters
|
||||
continue
|
||||
if len(rawProjects) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
projects := []*ProjectWithTasksAndBuckets{}
|
||||
projectsMap := make(map[int64]*ProjectWithTasksAndBuckets, len(rawProjects))
|
||||
projectIDs := []int64{}
|
||||
for _, p := range rawProjects {
|
||||
pp := &ProjectWithTasksAndBuckets{
|
||||
Project: *p,
|
||||
}
|
||||
|
||||
nn := &NamespaceWithListsAndTasks{
|
||||
Namespace: n.Namespace,
|
||||
Lists: []*ListWithTasksAndBuckets{},
|
||||
}
|
||||
|
||||
for _, l := range n.Lists {
|
||||
ll := &ListWithTasksAndBuckets{
|
||||
List: *l,
|
||||
BackgroundFileID: l.BackgroundFileID,
|
||||
Tasks: []*TaskWithComments{},
|
||||
}
|
||||
nn.Lists = append(nn.Lists, ll)
|
||||
listMap[l.ID] = ll
|
||||
listIDs = append(listIDs, l.ID)
|
||||
}
|
||||
|
||||
namespaceIDs = append(namespaceIDs, n.ID)
|
||||
namespaces = append(namespaces, nn)
|
||||
projects = append(projects, pp)
|
||||
projectsMap[p.ID] = pp
|
||||
projectIDs = append(projectIDs, p.ID)
|
||||
}
|
||||
|
||||
if len(namespaceIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get all lists
|
||||
lists, err := getListsForNamespaces(s, namespaceIDs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tasks, _, _, err := getTasksForLists(s, lists, u, &taskOptions{
|
||||
tasks, _, _, err := getTasksForProjects(s, rawProjects, u, &taskOptions{
|
||||
page: 0,
|
||||
perPage: -1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return taskIDs, err
|
||||
}
|
||||
|
||||
taskMap := make(map[int64]*TaskWithComments, len(tasks))
|
||||
|
@ -181,17 +166,18 @@ func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err err
|
|||
taskMap[t.ID] = &TaskWithComments{
|
||||
Task: *t,
|
||||
}
|
||||
if _, exists := listMap[t.ListID]; !exists {
|
||||
log.Debugf("[User Data Export] List %d does not exist for task %d, omitting", t.ListID, t.ID)
|
||||
if _, exists := projectsMap[t.ProjectID]; !exists {
|
||||
log.Debugf("[User Data Export] Project %d does not exist for task %d, omitting", t.ProjectID, t.ID)
|
||||
continue
|
||||
}
|
||||
listMap[t.ListID].Tasks = append(listMap[t.ListID].Tasks, taskMap[t.ID])
|
||||
projectsMap[t.ProjectID].Tasks = append(projectsMap[t.ProjectID].Tasks, taskMap[t.ID])
|
||||
taskIDs = append(taskIDs, t.ID)
|
||||
}
|
||||
|
||||
comments := []*TaskComment{}
|
||||
err = s.
|
||||
Join("LEFT", "tasks", "tasks.id = task_comments.task_id").
|
||||
In("tasks.list_id", listIDs).
|
||||
In("tasks.project_id", projectIDs).
|
||||
Find(&comments)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -206,49 +192,28 @@ func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err err
|
|||
}
|
||||
|
||||
buckets := []*Bucket{}
|
||||
err = s.In("list_id", listIDs).Find(&buckets)
|
||||
err = s.In("project_id", projectIDs).Find(&buckets)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, b := range buckets {
|
||||
if _, exists := listMap[b.ListID]; !exists {
|
||||
log.Debugf("[User Data Export] List %d does not exist for bucket %d, omitting", b.ListID, b.ID)
|
||||
if _, exists := projectsMap[b.ProjectID]; !exists {
|
||||
log.Debugf("[User Data Export] Project %d does not exist for bucket %d, omitting", b.ProjectID, b.ID)
|
||||
continue
|
||||
}
|
||||
listMap[b.ListID].Buckets = append(listMap[b.ListID].Buckets, b)
|
||||
projectsMap[b.ProjectID].Buckets = append(projectsMap[b.ProjectID].Buckets, b)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(namespaces)
|
||||
data, err := json.Marshal(projects)
|
||||
if err != nil {
|
||||
return err
|
||||
return taskIDs, err
|
||||
}
|
||||
|
||||
return utils.WriteBytesToZip("data.json", data, wr)
|
||||
return taskIDs, utils.WriteBytesToZip("data.json", data, wr)
|
||||
}
|
||||
|
||||
func exportTaskAttachments(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
|
||||
lists, _, _, err := getRawListsForUser(
|
||||
s,
|
||||
&listOptions{
|
||||
user: u,
|
||||
page: -1,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tasks, _, _, err := getRawTasksForLists(s, lists, u, &taskOptions{page: -1})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
taskIDs := []int64{}
|
||||
for _, t := range tasks {
|
||||
taskIDs = append(taskIDs, t.ID)
|
||||
}
|
||||
|
||||
func exportTaskAttachments(s *xorm.Session, wr *zip.Writer, taskIDs []int64) (err error) {
|
||||
tas, err := getTaskAttachmentsByTaskIDs(s, taskIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -279,10 +244,10 @@ func exportSavedFilters(s *xorm.Session, u *user.User, wr *zip.Writer) (err erro
|
|||
return utils.WriteBytesToZip("filters.json", data, wr)
|
||||
}
|
||||
|
||||
func exportListBackgrounds(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
|
||||
lists, _, _, err := getRawListsForUser(
|
||||
func exportProjectBackgrounds(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
|
||||
projects, _, _, err := getRawProjectsForUser(
|
||||
s,
|
||||
&listOptions{
|
||||
&projectOptions{
|
||||
user: u,
|
||||
page: -1,
|
||||
},
|
||||
|
@ -292,7 +257,7 @@ func exportListBackgrounds(s *xorm.Session, u *user.User, wr *zip.Writer) (err e
|
|||
}
|
||||
|
||||
fs := make(map[int64]io.ReadCloser)
|
||||
for _, l := range lists {
|
||||
for _, l := range projects {
|
||||
if l.BackgroundFileID == 0 {
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ type FavoriteKind int
|
|||
const (
|
||||
FavoriteKindUnknown FavoriteKind = iota
|
||||
FavoriteKindTask
|
||||
FavoriteKindList
|
||||
FavoriteKindProject
|
||||
)
|
||||
|
||||
// Favorite represents an entity which is a favorite to someone
|
||||
|
|
|
@ -31,8 +31,8 @@ type Bucket struct {
|
|||
ID int64 `xorm:"bigint 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:"bigint not null" json:"list_id" param:"list"`
|
||||
// The project this bucket belongs to.
|
||||
ProjectID int64 `xorm:"bigint not null" json:"project_id" param:"project"`
|
||||
// All tasks which belong to this bucket.
|
||||
Tasks []*Task `xorm:"-" json:"tasks"`
|
||||
|
||||
|
@ -77,19 +77,19 @@ func getBucketByID(s *xorm.Session, id int64) (b *Bucket, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func getDefaultBucket(s *xorm.Session, listID int64) (bucket *Bucket, err error) {
|
||||
func getDefaultBucket(s *xorm.Session, projectID int64) (bucket *Bucket, err error) {
|
||||
bucket = &Bucket{}
|
||||
_, err = s.
|
||||
Where("list_id = ?", listID).
|
||||
Where("project_id = ?", projectID).
|
||||
OrderBy("position asc").
|
||||
Get(bucket)
|
||||
return
|
||||
}
|
||||
|
||||
func getDoneBucketForList(s *xorm.Session, listID int64) (bucket *Bucket, err error) {
|
||||
func getDoneBucketForProject(s *xorm.Session, projectID int64) (bucket *Bucket, err error) {
|
||||
bucket = &Bucket{}
|
||||
exists, err := s.
|
||||
Where("list_id = ? and is_done_bucket = ?", listID, true).
|
||||
Where("project_id = ? and is_done_bucket = ?", projectID, true).
|
||||
Get(bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -101,14 +101,14 @@ func getDoneBucketForList(s *xorm.Session, listID int64) (bucket *Bucket, err er
|
|||
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.
|
||||
// ReadAll returns all buckets with their tasks for a certain project
|
||||
// @Summary Get all kanban buckets of a project
|
||||
// @Description Returns all kanban buckets with belong to a project including their tasks.
|
||||
// @tags task
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List Id"
|
||||
// @Param id path int true "Project Id"
|
||||
// @Param page query int false "The page number for tasks. Used for pagination. If not provided, the first page of results is returned."
|
||||
// @Param per_page query int false "The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page."
|
||||
// @Param s query string false "Search tasks by task text."
|
||||
|
@ -119,15 +119,15 @@ func getDoneBucketForList(s *xorm.Session, listID int64) (bucket *Bucket, err er
|
|||
// @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`."
|
||||
// @Success 200 {array} models.Bucket "The buckets with their tasks"
|
||||
// @Failure 500 {object} models.Message "Internal server error"
|
||||
// @Router /lists/{id}/buckets [get]
|
||||
// @Router /projects/{id}/buckets [get]
|
||||
func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
|
||||
list, err := GetListSimpleByID(s, b.ListID)
|
||||
project, err := GetProjectSimpleByID(s, b.ProjectID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
can, _, err := list.CanRead(s, auth)
|
||||
can, _, err := project.CanRead(s, auth)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
@ -135,10 +135,10 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||
return nil, 0, 0, ErrGenericForbidden{}
|
||||
}
|
||||
|
||||
// Get all buckets for this list
|
||||
// Get all buckets for this project
|
||||
buckets := []*Bucket{}
|
||||
err = s.
|
||||
Where("list_id = ?", b.ListID).
|
||||
Where("project_id = ?", b.ProjectID).
|
||||
OrderBy("position").
|
||||
Find(&buckets)
|
||||
if err != nil {
|
||||
|
@ -202,7 +202,7 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||
|
||||
opts.filters[bucketFilterIndex].value = id
|
||||
|
||||
ts, _, _, err := getRawTasksForLists(s, []*List{{ID: bucket.ListID}}, auth, opts)
|
||||
ts, _, _, err := getRawTasksForProjects(s, []*Project{{ID: bucket.ProjectID}}, auth, opts)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
@ -226,7 +226,7 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||
for _, task := range tasks {
|
||||
// Check if the bucket exists in the map to prevent nil pointer panics
|
||||
if _, exists := bucketMap[task.BucketID]; !exists {
|
||||
log.Debugf("Tried to put task %d into bucket %d which does not exist in list %d", task.ID, task.BucketID, b.ListID)
|
||||
log.Debugf("Tried to put task %d into bucket %d which does not exist in project %d", task.ID, task.BucketID, b.ProjectID)
|
||||
continue
|
||||
}
|
||||
bucketMap[task.BucketID].Tasks = append(bucketMap[task.BucketID].Tasks, task)
|
||||
|
@ -237,18 +237,18 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||
|
||||
// Create creates a new bucket
|
||||
// @Summary Create a new bucket
|
||||
// @Description Creates a new kanban bucket on a list.
|
||||
// @Description Creates a new kanban bucket on a project.
|
||||
// @tags task
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List Id"
|
||||
// @Param id path int true "Project Id"
|
||||
// @Param bucket body models.Bucket true "The bucket object"
|
||||
// @Success 200 {object} models.Bucket "The created bucket object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid bucket object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The list does not exist."
|
||||
// @Failure 404 {object} web.HTTPError "The project does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id}/buckets [put]
|
||||
// @Router /projects/{id}/buckets [put]
|
||||
func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
b.CreatedBy, err = GetUserOrLinkShareUser(s, a)
|
||||
if err != nil {
|
||||
|
@ -273,24 +273,24 @@ func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param listID path int true "List Id"
|
||||
// @Param projectID path int true "Project 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} web.HTTPError "Invalid bucket object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/buckets/{bucketID} [post]
|
||||
// @Router /projects/{projectID}/buckets/{bucketID} [post]
|
||||
func (b *Bucket) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
doneBucket, err := getDoneBucketForList(s, b.ListID)
|
||||
doneBucket, err := getDoneBucketForProject(s, b.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if doneBucket != nil && doneBucket.IsDoneBucket && b.IsDoneBucket && doneBucket.ID != b.ID {
|
||||
return &ErrOnlyOneDoneBucketPerList{
|
||||
return &ErrOnlyOneDoneBucketPerProject{
|
||||
BucketID: b.ID,
|
||||
ListID: b.ListID,
|
||||
ProjectID: b.ProjectID,
|
||||
DoneBucketID: doneBucket.ID,
|
||||
}
|
||||
}
|
||||
|
@ -309,28 +309,28 @@ func (b *Bucket) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
|
||||
// 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. You cannot delete the last bucket on a list.
|
||||
// @Description Deletes an existing kanban bucket and dissociates all of its task. It does not delete any tasks. You cannot delete the last bucket on a project.
|
||||
// @tags task
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param listID path int true "List Id"
|
||||
// @Param projectID path int true "Project Id"
|
||||
// @Param bucketID path int true "Bucket Id"
|
||||
// @Success 200 {object} models.Message "Successfully deleted."
|
||||
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/buckets/{bucketID} [delete]
|
||||
// @Router /projects/{projectID}/buckets/{bucketID} [delete]
|
||||
func (b *Bucket) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Prevent removing the last bucket
|
||||
total, err := s.Where("list_id = ?", b.ListID).Count(&Bucket{})
|
||||
total, err := s.Where("project_id = ?", b.ProjectID).Count(&Bucket{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if total <= 1 {
|
||||
return ErrCannotRemoveLastBucket{
|
||||
BucketID: b.ID,
|
||||
ListID: b.ListID,
|
||||
BucketID: b.ID,
|
||||
ProjectID: b.ProjectID,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -341,7 +341,7 @@ func (b *Bucket) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
|
||||
// Get the default bucket
|
||||
defaultBucket, err := getDefaultBucket(s, b.ListID)
|
||||
defaultBucket, err := getDefaultBucket(s, b.ProjectID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import (
|
|||
|
||||
// CanCreate checks if a user can create a new bucket
|
||||
func (b *Bucket) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
l := &List{ID: b.ListID}
|
||||
l := &Project{ID: b.ProjectID}
|
||||
return l.CanWrite(s, a)
|
||||
}
|
||||
|
||||
|
@ -43,6 +43,6 @@ func (b *Bucket) canDoBucket(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
l := &List{ID: bb.ListID}
|
||||
l := &Project{ID: bb.ProjectID}
|
||||
return l.CanWrite(s, a)
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||
defer s.Close()
|
||||
|
||||
testuser := &user.User{ID: 1}
|
||||
b := &Bucket{ListID: 1}
|
||||
b := &Bucket{ProjectID: 1}
|
||||
bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", 0, 0)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -53,7 +53,7 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||
assert.Len(t, buckets[1].Tasks, 3)
|
||||
assert.Len(t, buckets[2].Tasks, 3)
|
||||
|
||||
// Assert we have bucket 1, 2, 3 but not 4 (that belongs to a different list) and their position
|
||||
// Assert we have bucket 1, 2, 3 but not 4 (that belongs to a different project) and their position
|
||||
assert.Equal(t, int64(1), buckets[0].ID)
|
||||
assert.Equal(t, int64(2), buckets[1].ID)
|
||||
assert.Equal(t, int64(3), buckets[2].ID)
|
||||
|
@ -77,7 +77,7 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||
|
||||
testuser := &user.User{ID: 1}
|
||||
b := &Bucket{
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
TaskCollection: TaskCollection{
|
||||
FilterBy: []string{"title"},
|
||||
FilterComparator: []string{"like"},
|
||||
|
@ -98,11 +98,11 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||
defer s.Close()
|
||||
|
||||
linkShare := &LinkSharing{
|
||||
ID: 1,
|
||||
ListID: 1,
|
||||
Right: RightRead,
|
||||
ID: 1,
|
||||
ProjectID: 1,
|
||||
Right: RightRead,
|
||||
}
|
||||
b := &Bucket{ListID: 1}
|
||||
b := &Bucket{ProjectID: 1}
|
||||
result, _, _, err := b.ReadAll(s, linkShare, "", 0, 0)
|
||||
assert.NoError(t, err)
|
||||
buckets, _ := result.([]*Bucket)
|
||||
|
@ -116,7 +116,7 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||
defer s.Close()
|
||||
|
||||
testuser := &user.User{ID: 12}
|
||||
b := &Bucket{ListID: 23}
|
||||
b := &Bucket{ProjectID: 23}
|
||||
result, _, _, err := b.ReadAll(s, testuser, "", 0, 0)
|
||||
assert.NoError(t, err)
|
||||
buckets, _ := result.([]*Bucket)
|
||||
|
@ -135,8 +135,8 @@ func TestBucket_Delete(t *testing.T) {
|
|||
defer s.Close()
|
||||
|
||||
b := &Bucket{
|
||||
ID: 2, // The second bucket only has 3 tasks
|
||||
ListID: 1,
|
||||
ID: 2, // The second bucket only has 3 tasks
|
||||
ProjectID: 1,
|
||||
}
|
||||
err := b.Delete(s, user)
|
||||
assert.NoError(t, err)
|
||||
|
@ -149,18 +149,18 @@ func TestBucket_Delete(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Len(t, tasks, 15)
|
||||
db.AssertMissing(t, "buckets", map[string]interface{}{
|
||||
"id": 2,
|
||||
"list_id": 1,
|
||||
"id": 2,
|
||||
"project_id": 1,
|
||||
})
|
||||
})
|
||||
t.Run("last bucket in list", func(t *testing.T) {
|
||||
t.Run("last bucket in project", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
b := &Bucket{
|
||||
ID: 34,
|
||||
ListID: 18,
|
||||
ID: 34,
|
||||
ProjectID: 18,
|
||||
}
|
||||
err := b.Delete(s, user)
|
||||
assert.Error(t, err)
|
||||
|
@ -169,8 +169,8 @@ func TestBucket_Delete(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
db.AssertExists(t, "buckets", map[string]interface{}{
|
||||
"id": 34,
|
||||
"list_id": 18,
|
||||
"id": 34,
|
||||
"project_id": 18,
|
||||
}, false)
|
||||
})
|
||||
}
|
||||
|
@ -217,19 +217,19 @@ func TestBucket_Update(t *testing.T) {
|
|||
|
||||
testAndAssertBucketUpdate(t, b, s)
|
||||
})
|
||||
t.Run("only one done bucket per list", func(t *testing.T) {
|
||||
t.Run("only one done bucket per project", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
b := &Bucket{
|
||||
ID: 1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
IsDoneBucket: true,
|
||||
}
|
||||
|
||||
err := b.Update(s, &user.User{ID: 1})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrOnlyOneDoneBucketPerList(err))
|
||||
assert.True(t, IsErrOnlyOneDoneBucketPerProject(err))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ func (l *Label) hasAccessToLabel(s *xorm.Session, a web.Auth) (has bool, maxRigh
|
|||
builder.
|
||||
Select("id").
|
||||
From("tasks").
|
||||
Where(builder.In("list_id", getUserListsStatement(u.ID).Select("l.id"))),
|
||||
Where(builder.In("project_id", getUserProjectsStatement(nil, u.ID, "", false).Select("l.id"))),
|
||||
)
|
||||
|
||||
ll := &LabelTask{}
|
||||
|
|
|
@ -35,7 +35,7 @@ import (
|
|||
type LabelTask struct {
|
||||
// The unique, numeric id of this label.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"-"`
|
||||
TaskID int64 `xorm:"bigint INDEX not null" json:"-" param:"listtask"`
|
||||
TaskID int64 `xorm:"bigint INDEX not null" json:"-" param:"projecttask"`
|
||||
// The label id you want to associate with a task.
|
||||
LabelID int64 `xorm:"bigint INDEX not null" json:"label_id" param:"label"`
|
||||
// A timestamp when this task was created. You cannot change this value.
|
||||
|
@ -52,7 +52,7 @@ func (LabelTask) TableName() string {
|
|||
|
||||
// Delete deletes a label on a task
|
||||
// @Summary Remove a label from a task
|
||||
// @Description Remove a label from a task. The user needs to have write-access to the list to be able do this.
|
||||
// @Description Remove a label from a task. The user needs to have write-access to the project to be able do this.
|
||||
// @tags labels
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
@ -71,7 +71,7 @@ func (lt *LabelTask) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
|
||||
// Create adds a label to a task
|
||||
// @Summary Add a label to a task
|
||||
// @Description Add a label to a task. The user needs to have write-access to the list to be able do this.
|
||||
// @Description Add a label to a task. The user needs to have write-access to the project to be able do this.
|
||||
// @tags labels
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
@ -100,7 +100,7 @@ func (lt *LabelTask) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
err = updateListByTaskID(s, lt.TaskID)
|
||||
err = updateProjectByTaskID(s, lt.TaskID)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -156,7 +156,7 @@ type LabelByTaskIDsOptions struct {
|
|||
}
|
||||
|
||||
// Helper function to get all labels for a set of tasks
|
||||
// Used when getting all labels for one task as well when getting all lables
|
||||
// Used when getting all labels for one task as well when getting all labels
|
||||
func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, resultCount int, totalEntries int64, err error) {
|
||||
// We still need the task ID when we want to get all labels for a task, but because of this, we get the same label
|
||||
// multiple times when it is associated to more than one task.
|
||||
|
@ -180,7 +180,7 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
|
|||
builder.
|
||||
Select("id").
|
||||
From("tasks").
|
||||
Where(builder.In("list_id", getUserListsStatement(opts.GetForUser).Select("l.id"))),
|
||||
Where(builder.In("project_id", getUserProjectsStatement(nil, opts.GetForUser, "", false).Select("l.id"))),
|
||||
), cond)
|
||||
}
|
||||
if opts.GetUnusedLabels {
|
||||
|
@ -293,17 +293,17 @@ func (t *Task) updateTaskLabels(s *xorm.Session, creator web.Auth, labels []*Lab
|
|||
for _, oldLabel := range allLabels {
|
||||
found = false
|
||||
if newLabels[oldLabel.ID] != nil {
|
||||
found = true // If a new label is already in the list with old labels
|
||||
found = true // If a new label is already in the project with old labels
|
||||
}
|
||||
|
||||
// Put all labels which are only on the old list to the trash
|
||||
// Put all labels which are only on the old project to the trash
|
||||
if !found {
|
||||
labelsToDelete = append(labelsToDelete, oldLabel.ID)
|
||||
} else {
|
||||
t.Labels = append(t.Labels, oldLabel)
|
||||
}
|
||||
|
||||
// Put it in a list with all old labels, just using the loop here
|
||||
// Put it in a project with all old labels, just using the loop here
|
||||
oldLabels[oldLabel.ID] = oldLabel
|
||||
}
|
||||
|
||||
|
@ -349,7 +349,7 @@ func (t *Task) updateTaskLabels(s *xorm.Session, creator web.Auth, labels []*Lab
|
|||
t.Labels = append(t.Labels, label)
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, &List{ID: t.ListID})
|
||||
err = updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -357,7 +357,7 @@ func (t *Task) updateTaskLabels(s *xorm.Session, creator web.Auth, labels []*Lab
|
|||
type LabelTaskBulk struct {
|
||||
// All labels you want to update at once.
|
||||
Labels []*Label `json:"labels"`
|
||||
TaskID int64 `json:"-" param:"listtask"`
|
||||
TaskID int64 `json:"-" param:"projecttask"`
|
||||
|
||||
web.CRUDable `json:"-"`
|
||||
web.Rights `json:"-"`
|
||||
|
|
|
@ -143,7 +143,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("LabelTask.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
t.Errorf("LabelTask.ReadAll() Wrong error type! Error = %v, want = %v, got = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name(), err)
|
||||
}
|
||||
if diff, equal := messagediff.PrettyDiff(gotLabels, tt.wantLabels); !equal {
|
||||
t.Errorf("LabelTask.ReadAll() = %v, want %v, diff: %v", l, tt.wantLabels, diff)
|
||||
|
|
|
@ -42,17 +42,17 @@ const (
|
|||
SharingTypeWithPassword
|
||||
)
|
||||
|
||||
// LinkSharing represents a shared list
|
||||
// LinkSharing represents a shared project
|
||||
type LinkSharing struct {
|
||||
// The ID of the shared thing
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"share"`
|
||||
// The public id to get this shared list
|
||||
// The public id to get this shared project
|
||||
Hash string `xorm:"varchar(40) not null unique" json:"hash" param:"hash"`
|
||||
// The name of this link share. All actions someone takes while being authenticated with that link will appear with that name.
|
||||
Name string `xorm:"text null" json:"name"`
|
||||
// The ID of the shared list
|
||||
ListID int64 `xorm:"bigint not null" json:"-" param:"list"`
|
||||
// The right this list is shared with. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
|
||||
// The ID of the shared project
|
||||
ProjectID int64 `xorm:"bigint not null" json:"-" param:"project"`
|
||||
// The right this project is shared with. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
|
||||
Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
|
||||
// The kind of this link. 0 = undefined, 1 = without password, 2 = with password.
|
||||
|
@ -61,11 +61,11 @@ type LinkSharing struct {
|
|||
// The password of this link share. You can only set it, not retrieve it after the link share has been created.
|
||||
Password string `xorm:"text null" json:"password"`
|
||||
|
||||
// The user who shared this list
|
||||
// The user who shared this project
|
||||
SharedBy *user.User `xorm:"-" json:"shared_by"`
|
||||
SharedByID int64 `xorm:"bigint INDEX not null" json:"-"`
|
||||
|
||||
// A timestamp when this list was shared. You cannot change this value.
|
||||
// A timestamp when this project was shared. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this share was last updated. You cannot change this value.
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
|
@ -89,7 +89,7 @@ func GetLinkShareFromClaims(claims jwt.MapClaims) (share *LinkSharing, err error
|
|||
share = &LinkSharing{}
|
||||
share.ID = int64(claims["id"].(float64))
|
||||
share.Hash = claims["hash"].(string)
|
||||
share.ListID = int64(claims["list_id"].(float64))
|
||||
share.ProjectID = int64(claims["project_id"].(float64))
|
||||
share.Right = Right(claims["right"].(float64))
|
||||
share.SharedByID = int64(claims["sharedByID"].(float64))
|
||||
return
|
||||
|
@ -114,21 +114,21 @@ func (share *LinkSharing) toUser() *user.User {
|
|||
}
|
||||
}
|
||||
|
||||
// Create creates a new link share for a given list
|
||||
// @Summary Share a list via link
|
||||
// @Description Share a list via link. The user needs to have write-access to the list to be able do this.
|
||||
// Create creates a new link share for a given project
|
||||
// @Summary Share a project via link
|
||||
// @Description Share a project via link. The user needs to have write-access to the project to be able do this.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param list path int true "List ID"
|
||||
// @Param project path int true "Project ID"
|
||||
// @Param label body models.LinkSharing true "The new link share object"
|
||||
// @Success 201 {object} models.LinkSharing "The created link share object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid link share object provided."
|
||||
// @Failure 403 {object} web.HTTPError "Not allowed to add the list share."
|
||||
// @Failure 404 {object} web.HTTPError "The list does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "Not allowed to add the project share."
|
||||
// @Failure 404 {object} web.HTTPError "The project does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{list}/shares [put]
|
||||
// @Router /projects/{project}/shares [put]
|
||||
func (share *LinkSharing) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
err = share.Right.isValid()
|
||||
|
@ -156,48 +156,48 @@ func (share *LinkSharing) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
|
||||
// ReadOne returns one share
|
||||
// @Summary Get one link shares for a list
|
||||
// @Summary Get one link shares for a project
|
||||
// @Description Returns one link share by its ID.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param list path int true "List ID"
|
||||
// @Param project path int true "Project ID"
|
||||
// @Param share path int true "Share ID"
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} models.LinkSharing "The share links"
|
||||
// @Failure 403 {object} web.HTTPError "No access to the list"
|
||||
// @Failure 403 {object} web.HTTPError "No access to the project"
|
||||
// @Failure 404 {object} web.HTTPError "Share Link not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{list}/shares/{share} [get]
|
||||
// @Router /projects/{project}/shares/{share} [get]
|
||||
func (share *LinkSharing) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
exists, err := s.Where("id = ?", share.ID).Get(share)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return ErrListShareDoesNotExist{ID: share.ID, Hash: share.Hash}
|
||||
return ErrProjectShareDoesNotExist{ID: share.ID, Hash: share.Hash}
|
||||
}
|
||||
share.Password = ""
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll returns all shares for a given list
|
||||
// @Summary Get all link shares for a list
|
||||
// @Description Returns all link shares which exist for a given list
|
||||
// ReadAll returns all shares for a given project
|
||||
// @Summary Get all link shares for a project
|
||||
// @Description Returns all link shares which exist for a given project
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param list path int true "List ID"
|
||||
// @Param project path int true "Project ID"
|
||||
// @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 s query string false "Search shares by hash."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.LinkSharing "The share links"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{list}/shares [get]
|
||||
// @Router /projects/{project}/shares [get]
|
||||
func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
|
||||
list := &List{ID: share.ListID}
|
||||
can, _, err := list.CanRead(s, a)
|
||||
project := &Project{ID: share.ProjectID}
|
||||
can, _, err := project.CanRead(s, a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
@ -210,7 +210,7 @@ func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
|||
var shares []*LinkSharing
|
||||
query := s.
|
||||
Where(builder.And(
|
||||
builder.Eq{"list_id": share.ListID},
|
||||
builder.Eq{"project_id": share.ProjectID},
|
||||
builder.Or(
|
||||
db.ILIKE("hash", search),
|
||||
db.ILIKE("name", search),
|
||||
|
@ -246,7 +246,7 @@ func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
|||
|
||||
// Total count
|
||||
totalItems, err = s.
|
||||
Where("list_id = ? AND hash LIKE ?", share.ListID, "%"+search+"%").
|
||||
Where("project_id = ? AND hash LIKE ?", share.ProjectID, "%"+search+"%").
|
||||
Count(&LinkSharing{})
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
|
@ -257,18 +257,18 @@ func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
|||
|
||||
// Delete removes a link share
|
||||
// @Summary Remove a link share
|
||||
// @Description Remove a link share. The user needs to have write-access to the list to be able do this.
|
||||
// @Description Remove a link share. The user needs to have write-access to the project to be able do this.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param list path int true "List ID"
|
||||
// @Param project path int true "Project ID"
|
||||
// @Param share path int true "Share Link ID"
|
||||
// @Success 200 {object} models.Message "The link was successfully removed."
|
||||
// @Failure 403 {object} web.HTTPError "Not allowed to remove the link."
|
||||
// @Failure 404 {object} web.HTTPError "Share Link not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{list}/shares/{share} [delete]
|
||||
// @Router /projects/{project}/shares/{share} [delete]
|
||||
func (share *LinkSharing) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
_, err = s.Where("id = ?", share.ID).Delete(share)
|
||||
return
|
||||
|
@ -282,19 +282,19 @@ func GetLinkShareByHash(s *xorm.Session, hash string) (share *LinkSharing, err e
|
|||
return
|
||||
}
|
||||
if !has {
|
||||
return share, ErrListShareDoesNotExist{Hash: hash}
|
||||
return share, ErrProjectShareDoesNotExist{Hash: hash}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetListByShareHash returns a link share by its hash
|
||||
func GetListByShareHash(s *xorm.Session, hash string) (list *List, err error) {
|
||||
// GetProjectByShareHash returns a link share by its hash
|
||||
func GetProjectByShareHash(s *xorm.Session, hash string) (project *Project, err error) {
|
||||
share, err := GetLinkShareByHash(s, hash)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
list, err = GetListSimpleByID(s, share.ListID)
|
||||
project, err = GetProjectSimpleByID(s, share.ProjectID)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -306,7 +306,7 @@ func GetLinkShareByID(s *xorm.Session, id int64) (share *LinkSharing, err error)
|
|||
return
|
||||
}
|
||||
if !has {
|
||||
return share, ErrListShareDoesNotExist{ID: id}
|
||||
return share, ErrProjectShareDoesNotExist{ID: id}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ func (share *LinkSharing) CanRead(s *xorm.Session, a web.Auth) (bool, int, error
|
|||
return false, 0, nil
|
||||
}
|
||||
|
||||
l, err := GetListByShareHash(s, share.Hash)
|
||||
l, err := GetProjectByShareHash(s, share.Hash)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ func (share *LinkSharing) canDoLinkShare(s *xorm.Session, a web.Auth) (bool, err
|
|||
return false, nil
|
||||
}
|
||||
|
||||
l, err := GetListSimpleByID(s, share.ListID)
|
||||
l, err := GetProjectSimpleByID(s, share.ProjectID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
|
@ -33,8 +33,8 @@ func TestLinkSharing_Create(t *testing.T) {
|
|||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
Right: RightRead,
|
||||
ProjectID: 1,
|
||||
Right: RightRead,
|
||||
}
|
||||
err := share.Create(s, doer)
|
||||
|
||||
|
@ -52,8 +52,8 @@ func TestLinkSharing_Create(t *testing.T) {
|
|||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
Right: Right(123),
|
||||
ProjectID: 1,
|
||||
Right: Right(123),
|
||||
}
|
||||
err := share.Create(s, doer)
|
||||
|
||||
|
@ -66,9 +66,9 @@ func TestLinkSharing_Create(t *testing.T) {
|
|||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
Right: RightRead,
|
||||
Password: "somePassword",
|
||||
ProjectID: 1,
|
||||
Right: RightRead,
|
||||
Password: "somePassword",
|
||||
}
|
||||
err := share.Create(s, doer)
|
||||
|
||||
|
@ -92,7 +92,7 @@ func TestLinkSharing_ReadAll(t *testing.T) {
|
|||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
}
|
||||
all, _, _, err := share.ReadAll(s, doer, "", 1, -1)
|
||||
shares := all.([]*LinkSharing)
|
||||
|
@ -109,7 +109,7 @@ func TestLinkSharing_ReadAll(t *testing.T) {
|
|||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
}
|
||||
all, _, _, err := share.ReadAll(s, doer, "wITHPASS", 1, -1)
|
||||
shares := all.([]*LinkSharing)
|
||||
|
|
|
@ -1,836 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// List represents a list of tasks
|
||||
type List struct {
|
||||
// The unique, numeric id of this list.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"list"`
|
||||
// The title of the list. You'll see this in the namespace overview.
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
// The description of the list.
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
// The unique list short identifier. Used to build task identifiers.
|
||||
Identifier string `xorm:"varchar(10) null" json:"identifier" valid:"runelength(0|10)" minLength:"0" maxLength:"10"`
|
||||
// The hex color of this list
|
||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
|
||||
|
||||
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
|
||||
NamespaceID int64 `xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace"`
|
||||
|
||||
// The user who created this list.
|
||||
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
|
||||
|
||||
// Whether or not a list is archived.
|
||||
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
|
||||
|
||||
// The id of the file this list has set as background
|
||||
BackgroundFileID int64 `xorm:"null" json:"-"`
|
||||
// Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /lists/{listID}/background
|
||||
BackgroundInformation interface{} `xorm:"-" json:"background_information"`
|
||||
// Contains a very small version of the list background to use as a blurry preview until the actual background is loaded. Check out https://blurha.sh/ to learn how it works.
|
||||
BackgroundBlurHash string `xorm:"varchar(50) null" json:"background_blur_hash"`
|
||||
|
||||
// True if a list is a favorite. Favorite lists show up in a separate namespace. This value depends on the user making the call to the api.
|
||||
IsFavorite bool `xorm:"-" json:"is_favorite"`
|
||||
|
||||
// The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.
|
||||
// Will only returned when retreiving one list.
|
||||
Subscription *Subscription `xorm:"-" json:"subscription,omitempty"`
|
||||
|
||||
// The position this list has when querying all lists. See the tasks.position property on how to use this.
|
||||
Position float64 `xorm:"double null" json:"position"`
|
||||
|
||||
// A timestamp when this list was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this list was last updated. You cannot change this value.
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
type ListWithTasksAndBuckets struct {
|
||||
List
|
||||
// An array of tasks which belong to the list.
|
||||
Tasks []*TaskWithComments `xorm:"-" json:"tasks"`
|
||||
// Only used for migration.
|
||||
Buckets []*Bucket `xorm:"-" json:"buckets"`
|
||||
BackgroundFileID int64 `xorm:"null" json:"background_file_id"`
|
||||
}
|
||||
|
||||
// TableName returns a better name for the lists table
|
||||
func (l *List) TableName() string {
|
||||
return "lists"
|
||||
}
|
||||
|
||||
// ListBackgroundType holds a list background type
|
||||
type ListBackgroundType struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
// ListBackgroundUpload represents the list upload background type
|
||||
const ListBackgroundUpload string = "upload"
|
||||
|
||||
// FavoritesPseudoList holds all tasks marked as favorites
|
||||
var FavoritesPseudoList = List{
|
||||
ID: -1,
|
||||
Title: "Favorites",
|
||||
Description: "This list has all tasks marked as favorites.",
|
||||
NamespaceID: FavoritesPseudoNamespace.ID,
|
||||
IsFavorite: true,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// GetListsByNamespaceID gets all lists in a namespace
|
||||
func GetListsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (lists []*List, err error) {
|
||||
switch nID {
|
||||
case SharedListsPseudoNamespace.ID:
|
||||
nnn, err := getSharedListsInNamespace(s, false, doer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nnn != nil && nnn.Lists != nil {
|
||||
lists = nnn.Lists
|
||||
}
|
||||
case FavoritesPseudoNamespace.ID:
|
||||
namespaces := make(map[int64]*NamespaceWithLists)
|
||||
_, err := getNamespacesWithLists(s, &namespaces, "", false, 0, -1, doer.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
namespaceIDs, _ := getNamespaceOwnerIDs(namespaces)
|
||||
ls, err := getListsForNamespaces(s, namespaceIDs, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nnn, err := getFavoriteLists(s, ls, namespaceIDs, doer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nnn != nil && nnn.Lists != nil {
|
||||
lists = nnn.Lists
|
||||
}
|
||||
case SavedFiltersPseudoNamespace.ID:
|
||||
nnn, err := getSavedFilters(s, doer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nnn != nil && nnn.Lists != nil {
|
||||
lists = nnn.Lists
|
||||
}
|
||||
default:
|
||||
err = s.Select("l.*").
|
||||
Alias("l").
|
||||
Join("LEFT", []string{"namespaces", "n"}, "l.namespace_id = n.id").
|
||||
Where("l.is_archived = false").
|
||||
Where("n.is_archived = false OR n.is_archived IS NULL").
|
||||
Where("namespace_id = ?", nID).
|
||||
Find(&lists)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get more list details
|
||||
err = addListDetails(s, lists, doer)
|
||||
return lists, err
|
||||
}
|
||||
|
||||
// ReadAll gets all lists a user has access to
|
||||
// @Summary Get all lists a user has access to
|
||||
// @Description Returns all lists a user has access to.
|
||||
// @tags list
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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 s query string false "Search lists by title."
|
||||
// @Param is_archived query bool false "If true, also returns all archived lists."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.List "The lists"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists [get]
|
||||
func (l *List) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
|
||||
// Check if we're dealing with a share auth
|
||||
shareAuth, ok := a.(*LinkSharing)
|
||||
if ok {
|
||||
list, err := GetListSimpleByID(s, shareAuth.ListID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
lists := []*List{list}
|
||||
err = addListDetails(s, lists, a)
|
||||
return lists, 0, 0, err
|
||||
}
|
||||
|
||||
lists, resultCount, totalItems, err := getRawListsForUser(
|
||||
s,
|
||||
&listOptions{
|
||||
search: search,
|
||||
user: &user.User{ID: a.GetID()},
|
||||
page: page,
|
||||
perPage: perPage,
|
||||
isArchived: l.IsArchived,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
// Add more list details
|
||||
err = addListDetails(s, lists, a)
|
||||
return lists, resultCount, totalItems, err
|
||||
}
|
||||
|
||||
// ReadOne gets one list by its ID
|
||||
// @Summary Gets one list
|
||||
// @Description Returns a list by its ID.
|
||||
// @tags list
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List ID"
|
||||
// @Success 200 {object} models.List "The list"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id} [get]
|
||||
func (l *List) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
if l.ID == FavoritesPseudoList.ID {
|
||||
// Already "built" the list in CanRead
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for saved filters
|
||||
if getSavedFilterIDFromListID(l.ID) > 0 {
|
||||
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromListID(l.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Title = sf.Title
|
||||
l.Description = sf.Description
|
||||
l.Created = sf.Created
|
||||
l.Updated = sf.Updated
|
||||
l.OwnerID = sf.OwnerID
|
||||
}
|
||||
|
||||
// Get list owner
|
||||
l.Owner, err = user.GetUserByID(s, l.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Check if the namespace is archived and set the namespace to archived if it is not already archived individually.
|
||||
if !l.IsArchived {
|
||||
err = l.CheckIsArchived(s)
|
||||
if err != nil {
|
||||
if !IsErrNamespaceIsArchived(err) && !IsErrListIsArchived(err) {
|
||||
return
|
||||
}
|
||||
l.IsArchived = true
|
||||
}
|
||||
}
|
||||
|
||||
// Get any background information if there is one set
|
||||
if l.BackgroundFileID != 0 {
|
||||
// Unsplash image
|
||||
l.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, l.BackgroundFileID)
|
||||
if err != nil && !files.IsErrFileIsNotUnsplashFile(err) {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
|
||||
l.BackgroundInformation = &ListBackgroundType{Type: ListBackgroundUpload}
|
||||
}
|
||||
}
|
||||
|
||||
l.IsFavorite, err = isFavorite(s, l.ID, a, FavoriteKindList)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
l.Subscription, err = GetSubscription(s, SubscriptionEntityList, l.ID, a)
|
||||
return
|
||||
}
|
||||
|
||||
// GetListSimpleByID gets a list with only the basic items, aka no tasks or user objects. Returns an error if the list does not exist.
|
||||
func GetListSimpleByID(s *xorm.Session, listID int64) (list *List, err error) {
|
||||
|
||||
list = &List{}
|
||||
|
||||
if listID < 1 {
|
||||
return nil, ErrListDoesNotExist{ID: listID}
|
||||
}
|
||||
|
||||
exists, err := s.
|
||||
Where("id = ?", listID).
|
||||
OrderBy("position").
|
||||
Get(list)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return nil, ErrListDoesNotExist{ID: listID}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetListSimplByTaskID gets a list by a task id
|
||||
func GetListSimplByTaskID(s *xorm.Session, taskID int64) (l *List, err error) {
|
||||
// We need to re-init our list object, because otherwise xorm creates a "where for every item in that list object,
|
||||
// leading to not finding anything if the id is good, but for example the title is different.
|
||||
var list List
|
||||
exists, err := s.
|
||||
Select("lists.*").
|
||||
Table(List{}).
|
||||
Join("INNER", "tasks", "lists.id = tasks.list_id").
|
||||
Where("tasks.id = ?", taskID).
|
||||
Get(&list)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return &List{}, ErrListDoesNotExist{ID: l.ID}
|
||||
}
|
||||
|
||||
return &list, nil
|
||||
}
|
||||
|
||||
// GetListsByIDs returns a map of lists from a slice with list ids
|
||||
func GetListsByIDs(s *xorm.Session, listIDs []int64) (lists map[int64]*List, err error) {
|
||||
lists = make(map[int64]*List, len(listIDs))
|
||||
|
||||
if len(listIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
err = s.In("id", listIDs).Find(&lists)
|
||||
return
|
||||
}
|
||||
|
||||
type listOptions struct {
|
||||
search string
|
||||
user *user.User
|
||||
page int
|
||||
perPage int
|
||||
isArchived bool
|
||||
}
|
||||
|
||||
func getUserListsStatement(userID int64) *builder.Builder {
|
||||
dialect := config.DatabaseType.GetString()
|
||||
if dialect == "sqlite" {
|
||||
dialect = builder.SQLITE
|
||||
}
|
||||
|
||||
return builder.Dialect(dialect).
|
||||
Select("l.*").
|
||||
From("lists", "l").
|
||||
Join("INNER", "namespaces n", "l.namespace_id = n.id").
|
||||
Join("LEFT", "team_namespaces tn", "tn.namespace_id = n.id").
|
||||
Join("LEFT", "team_members tm", "tm.team_id = tn.team_id").
|
||||
Join("LEFT", "team_lists tl", "l.id = tl.list_id").
|
||||
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
|
||||
Join("LEFT", "users_lists ul", "ul.list_id = l.id").
|
||||
Join("LEFT", "users_namespaces un", "un.namespace_id = l.namespace_id").
|
||||
Where(builder.Or(
|
||||
builder.Eq{"tm.user_id": userID},
|
||||
builder.Eq{"tm2.user_id": userID},
|
||||
builder.Eq{"ul.user_id": userID},
|
||||
builder.Eq{"un.user_id": userID},
|
||||
builder.Eq{"l.owner_id": userID},
|
||||
)).
|
||||
OrderBy("position").
|
||||
GroupBy("l.id")
|
||||
}
|
||||
|
||||
// Gets the lists only, without any tasks or so
|
||||
func getRawListsForUser(s *xorm.Session, opts *listOptions) (lists []*List, resultCount int, totalItems int64, err error) {
|
||||
fullUser, err := user.GetUserByID(s, opts.user.ID)
|
||||
if err != nil {
|
||||
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},
|
||||
)
|
||||
}
|
||||
|
||||
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
||||
|
||||
var filterCond builder.Cond
|
||||
ids := []int64{}
|
||||
if opts.search != "" {
|
||||
vals := strings.Split(opts.search, ",")
|
||||
for _, val := range vals {
|
||||
v, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
log.Debugf("List search string part '%s' is not a number: %s", val, err)
|
||||
continue
|
||||
}
|
||||
ids = append(ids, v)
|
||||
}
|
||||
}
|
||||
|
||||
filterCond = db.ILIKE("l.title", opts.search)
|
||||
if len(ids) > 0 {
|
||||
filterCond = builder.In("l.id", ids)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
query := getUserListsStatement(fullUser.ID).
|
||||
Where(filterCond).
|
||||
Where(isArchivedCond)
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
err = s.SQL(query).Find(&lists)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
query = getUserListsStatement(fullUser.ID).
|
||||
Where(filterCond).
|
||||
Where(isArchivedCond)
|
||||
totalItems, err = s.
|
||||
SQL(query.Select("count(*)")).
|
||||
Count(&List{})
|
||||
return lists, len(lists), totalItems, err
|
||||
}
|
||||
|
||||
// addListDetails adds owner user objects and list tasks to all lists in the slice
|
||||
func addListDetails(s *xorm.Session, lists []*List, a web.Auth) (err error) {
|
||||
if len(lists) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var ownerIDs []int64
|
||||
for _, l := range lists {
|
||||
ownerIDs = append(ownerIDs, l.OwnerID)
|
||||
}
|
||||
|
||||
// Get all list owners
|
||||
owners := map[int64]*user.User{}
|
||||
if len(ownerIDs) > 0 {
|
||||
err = s.In("id", ownerIDs).Find(&owners)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var fileIDs []int64
|
||||
var listIDs []int64
|
||||
for _, l := range lists {
|
||||
listIDs = append(listIDs, l.ID)
|
||||
if o, exists := owners[l.OwnerID]; exists {
|
||||
l.Owner = o
|
||||
}
|
||||
if l.BackgroundFileID != 0 {
|
||||
l.BackgroundInformation = &ListBackgroundType{Type: ListBackgroundUpload}
|
||||
}
|
||||
fileIDs = append(fileIDs, l.BackgroundFileID)
|
||||
}
|
||||
|
||||
favs, err := getFavorites(s, listIDs, a, FavoriteKindList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subscriptions, err := GetSubscriptions(s, SubscriptionEntityList, listIDs, a)
|
||||
if err != nil {
|
||||
log.Errorf("An error occurred while getting list subscriptions for a namespace item: %s", err.Error())
|
||||
subscriptions = make(map[int64]*Subscription)
|
||||
}
|
||||
|
||||
for _, list := range lists {
|
||||
// Don't override the favorite state if it was already set from before (favorite saved filters do this)
|
||||
if list.IsFavorite {
|
||||
continue
|
||||
}
|
||||
list.IsFavorite = favs[list.ID]
|
||||
|
||||
if subscription, exists := subscriptions[list.ID]; exists {
|
||||
list.Subscription = subscription
|
||||
}
|
||||
}
|
||||
|
||||
if len(fileIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Unsplash background file info
|
||||
us := []*UnsplashPhoto{}
|
||||
err = s.In("file_id", fileIDs).Find(&us)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
unsplashPhotos := make(map[int64]*UnsplashPhoto, len(us))
|
||||
for _, u := range us {
|
||||
unsplashPhotos[u.FileID] = u
|
||||
}
|
||||
|
||||
// Build it all into the lists slice
|
||||
for _, l := range lists {
|
||||
// Only override the file info if we have info for unsplash backgrounds
|
||||
if _, exists := unsplashPhotos[l.BackgroundFileID]; exists {
|
||||
l.BackgroundInformation = unsplashPhotos[l.BackgroundFileID]
|
||||
}
|
||||
}
|
||||
|
||||
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(s *xorm.Session) (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(s)
|
||||
}
|
||||
|
||||
nl := &NamespaceList{}
|
||||
exists, err := s.
|
||||
Table("lists").
|
||||
Join("LEFT", "namespaces", "lists.namespace_id = namespaces.id").
|
||||
Where("lists.id = ? AND (lists.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
|
||||
}
|
||||
|
||||
func checkListBeforeUpdateOrDelete(s *xorm.Session, list *List) error {
|
||||
if list.NamespaceID < 0 {
|
||||
return &ErrListCannotBelongToAPseudoNamespace{ListID: list.ID, NamespaceID: list.NamespaceID}
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
if list.NamespaceID > 0 {
|
||||
_, err := GetNamespaceByID(s, list.NamespaceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the identifier is unique and not empty
|
||||
if list.Identifier != "" {
|
||||
exists, err := s.
|
||||
Where("identifier = ?", list.Identifier).
|
||||
And("id != ?", list.ID).
|
||||
Exist(&List{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return ErrListIdentifierIsNotUnique{Identifier: list.Identifier}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateList(s *xorm.Session, list *List, auth web.Auth) (err error) {
|
||||
err = list.CheckIsArchived(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doer, err := user.GetFromAuth(auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list.OwnerID = doer.ID
|
||||
list.Owner = doer
|
||||
list.ID = 0 // Otherwise only the first time a new list would be created
|
||||
|
||||
err = checkListBeforeUpdateOrDelete(s, list)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.Insert(list)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
list.Position = calculateDefaultPosition(list.ID, list.Position)
|
||||
_, err = s.Where("id = ?", list.ID).Update(list)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list.IsFavorite {
|
||||
if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new first bucket for this list
|
||||
b := &Bucket{
|
||||
ListID: list.ID,
|
||||
Title: "Backlog",
|
||||
}
|
||||
err = b.Create(s, auth)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return events.Dispatch(&ListCreatedEvent{
|
||||
List: list,
|
||||
Doer: doer,
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateList(s *xorm.Session, list *List, auth web.Auth, updateListBackground bool) (err error) {
|
||||
err = checkListBeforeUpdateOrDelete(s, list)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if list.NamespaceID == 0 {
|
||||
return &ErrListMustBelongToANamespace{
|
||||
ListID: list.ID,
|
||||
NamespaceID: list.NamespaceID,
|
||||
}
|
||||
}
|
||||
|
||||
// We need to specify the cols we want to update here to be able to un-archive lists
|
||||
colsToUpdate := []string{
|
||||
"title",
|
||||
"is_archived",
|
||||
"identifier",
|
||||
"hex_color",
|
||||
"namespace_id",
|
||||
"position",
|
||||
}
|
||||
if list.Description != "" {
|
||||
colsToUpdate = append(colsToUpdate, "description")
|
||||
}
|
||||
|
||||
if updateListBackground {
|
||||
colsToUpdate = append(colsToUpdate, "background_file_id", "background_blur_hash")
|
||||
}
|
||||
|
||||
wasFavorite, err := isFavorite(s, list.ID, auth, FavoriteKindList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if list.IsFavorite && !wasFavorite {
|
||||
if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !list.IsFavorite && wasFavorite {
|
||||
if err := removeFromFavorite(s, list.ID, auth, FavoriteKindList); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
ID(list.ID).
|
||||
Cols(colsToUpdate...).
|
||||
Update(list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = events.Dispatch(&ListUpdatedEvent{
|
||||
List: list,
|
||||
Doer: auth,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l, err := GetListSimpleByID(s, list.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*list = *l
|
||||
err = list.ReadOne(s, auth)
|
||||
return
|
||||
}
|
||||
|
||||
// Update implements the update method of CRUDable
|
||||
// @Summary Updates a list
|
||||
// @Description Updates a list. This does not include adding a task (see below).
|
||||
// @tags list
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List ID"
|
||||
// @Param list body models.List true "The list with updated values you want to update."
|
||||
// @Success 200 {object} models.List "The updated list."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid list object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id} [post]
|
||||
func (l *List) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
fid := getSavedFilterIDFromListID(l.ID)
|
||||
if fid > 0 {
|
||||
f, err := getSavedFilterSimpleByID(s, fid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.Title = l.Title
|
||||
f.Description = l.Description
|
||||
f.IsFavorite = l.IsFavorite
|
||||
err = f.Update(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*l = *f.toList()
|
||||
return nil
|
||||
}
|
||||
|
||||
return UpdateList(s, l, a, false)
|
||||
}
|
||||
|
||||
func updateListLastUpdated(s *xorm.Session, list *List) error {
|
||||
_, err := s.ID(list.ID).Cols("updated").Update(list)
|
||||
return err
|
||||
}
|
||||
|
||||
func updateListByTaskID(s *xorm.Session, taskID int64) (err error) {
|
||||
// need to get the task to update the list last updated timestamp
|
||||
task, err := GetTaskByIDSimple(s, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateListLastUpdated(s, &List{ID: task.ListID})
|
||||
}
|
||||
|
||||
// Create implements the create method of CRUDable
|
||||
// @Summary Creates a new list
|
||||
// @Description Creates a new list in a given namespace. The user needs write-access to the namespace.
|
||||
// @tags list
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param namespaceID path int true "Namespace ID"
|
||||
// @Param list body models.List true "The list you want to create."
|
||||
// @Success 201 {object} models.List "The created list."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid list object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/lists [put]
|
||||
func (l *List) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
err = CreateList(s, l, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return l.ReadOne(s, a)
|
||||
}
|
||||
|
||||
// Delete implements the delete method of CRUDable
|
||||
// @Summary Deletes a list
|
||||
// @Description Delets a list
|
||||
// @tags list
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List ID"
|
||||
// @Success 200 {object} models.Message "The list was successfully deleted."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid list object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id} [delete]
|
||||
func (l *List) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Delete the list
|
||||
_, err = s.ID(l.ID).Delete(&List{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete all tasks on that list
|
||||
// Using the loop to make sure all related entities to all tasks are properly deleted as well.
|
||||
tasks, _, _, err := getRawTasksForLists(s, []*List{l}, a, &taskOptions{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
err = task.Delete(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return events.Dispatch(&ListDeletedEvent{
|
||||
List: l,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// SetListBackground sets a background file as list background in the db
|
||||
func SetListBackground(s *xorm.Session, listID int64, background *files.File, blurHash string) (err error) {
|
||||
l := &List{
|
||||
ID: listID,
|
||||
BackgroundFileID: background.ID,
|
||||
BackgroundBlurHash: blurHash,
|
||||
}
|
||||
_, err = s.
|
||||
Where("id = ?", l.ID).
|
||||
Cols("background_file_id", "background_blur_hash").
|
||||
Update(l)
|
||||
return
|
||||
}
|
|
@ -1,296 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CanWrite return whether the user can write on that list or not
|
||||
func (l *List) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
|
||||
// The favorite list can't be edited
|
||||
if l.ID == FavoritesPseudoList.ID {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get the list and check the right
|
||||
originalList, err := GetListSimpleByID(s, l.ID)
|
||||
if err != nil {
|
||||
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(s)
|
||||
|
||||
var canWrite bool
|
||||
|
||||
// Check if we're dealing with a share auth
|
||||
shareAuth, ok := a.(*LinkSharing)
|
||||
if ok {
|
||||
return originalList.ID == shareAuth.ListID &&
|
||||
(shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), errIsArchived
|
||||
}
|
||||
|
||||
// Check if the user is either owner or can write to the list
|
||||
if originalList.isOwner(&user.User{ID: a.GetID()}) {
|
||||
canWrite = true
|
||||
}
|
||||
|
||||
if canWrite {
|
||||
return canWrite, errIsArchived
|
||||
}
|
||||
|
||||
canWrite, _, err = originalList.checkRight(s, a, RightWrite, RightAdmin)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return canWrite, errIsArchived
|
||||
}
|
||||
|
||||
// CanRead checks if a user has read access to a list
|
||||
func (l *List) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
|
||||
// The favorite list needs a special treatment
|
||||
if l.ID == FavoritesPseudoList.ID {
|
||||
owner, err := user.GetFromAuth(a)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
*l = FavoritesPseudoList
|
||||
l.Owner = owner
|
||||
return true, int(RightRead), nil
|
||||
}
|
||||
|
||||
// Saved Filter Lists need a special case
|
||||
if getSavedFilterIDFromListID(l.ID) > 0 {
|
||||
sf := &SavedFilter{ID: getSavedFilterIDFromListID(l.ID)}
|
||||
return sf.CanRead(s, a)
|
||||
}
|
||||
|
||||
// Check if the user is either owner or can read
|
||||
var err error
|
||||
originalList, err := GetListSimpleByID(s, l.ID)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
*l = *originalList
|
||||
|
||||
// Check if we're dealing with a share auth
|
||||
shareAuth, ok := a.(*LinkSharing)
|
||||
if ok {
|
||||
return l.ID == shareAuth.ListID &&
|
||||
(shareAuth.Right == RightRead || shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), int(shareAuth.Right), nil
|
||||
}
|
||||
|
||||
if l.isOwner(&user.User{ID: a.GetID()}) {
|
||||
return true, int(RightAdmin), nil
|
||||
}
|
||||
return l.checkRight(s, a, RightRead, RightWrite, RightAdmin)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a list
|
||||
func (l *List) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) {
|
||||
// The favorite list can't be edited
|
||||
if l.ID == FavoritesPseudoList.ID {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get the list
|
||||
ol, err := GetListSimpleByID(s, l.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check if we're moving the list into a different namespace.
|
||||
// If that is the case, we need to verify permissions to do so.
|
||||
if l.NamespaceID != 0 && l.NamespaceID != ol.NamespaceID {
|
||||
newNamespace := &Namespace{ID: l.NamespaceID}
|
||||
can, err := newNamespace.CanWrite(s, a)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !can {
|
||||
return false, ErrGenericForbidden{}
|
||||
}
|
||||
}
|
||||
|
||||
fid := getSavedFilterIDFromListID(l.ID)
|
||||
if fid > 0 {
|
||||
sf, err := getSavedFilterSimpleByID(s, fid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return sf.CanUpdate(s, a)
|
||||
}
|
||||
|
||||
canUpdate, err = l.CanWrite(s, 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
|
||||
func (l *List) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return l.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
// CanCreate checks if the user can create a list
|
||||
func (l *List) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
// A user can create a list if they have write access to the namespace
|
||||
n := &Namespace{ID: l.NamespaceID}
|
||||
return n.CanWrite(s, a)
|
||||
}
|
||||
|
||||
// IsAdmin returns whether the user has admin rights on the list or not
|
||||
func (l *List) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
// The favorite list can't be edited
|
||||
if l.ID == FavoritesPseudoList.ID {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
originalList, err := GetListSimpleByID(s, l.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check if we're dealing with a share auth
|
||||
shareAuth, ok := a.(*LinkSharing)
|
||||
if ok {
|
||||
return originalList.ID == shareAuth.ListID && shareAuth.Right == RightAdmin, nil
|
||||
}
|
||||
|
||||
// Check all the things
|
||||
// Check if the user is either owner or can write to the list
|
||||
// Owners are always admins
|
||||
if originalList.isOwner(&user.User{ID: a.GetID()}) {
|
||||
return true, nil
|
||||
}
|
||||
is, _, err := originalList.checkRight(s, a, RightAdmin)
|
||||
return is, err
|
||||
}
|
||||
|
||||
// Little helper function to check if a user is list owner
|
||||
func (l *List) isOwner(u *user.User) bool {
|
||||
return l.OwnerID == u.ID
|
||||
}
|
||||
|
||||
// Checks n different rights for any given user
|
||||
func (l *List) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
|
||||
|
||||
/*
|
||||
The following loop creates a sql condition like this one:
|
||||
|
||||
(ul.user_id = 1 AND ul.right = 1) OR (un.user_id = 1 AND un.right = 1) OR
|
||||
(tm.user_id = 1 AND tn.right = 1) OR (tm2.user_id = 1 AND tl.right = 1) OR
|
||||
|
||||
for each passed right. That way, we can check with a single sql query (instead if 8)
|
||||
if the user has the right to see the list or not.
|
||||
*/
|
||||
|
||||
var conds []builder.Cond
|
||||
for _, r := range rights {
|
||||
// User conditions
|
||||
// If the list was shared directly with the user and the user has the right
|
||||
conds = append(conds, builder.And(
|
||||
builder.Eq{"ul.user_id": a.GetID()},
|
||||
builder.Eq{"ul.right": r},
|
||||
))
|
||||
// If the namespace this list belongs to was shared directly with the user and the user has the right
|
||||
conds = append(conds, builder.And(
|
||||
builder.Eq{"un.user_id": a.GetID()},
|
||||
builder.Eq{"un.right": r},
|
||||
))
|
||||
|
||||
// Team rights
|
||||
// If the list was shared directly with the team and the team has the right
|
||||
conds = append(conds, builder.And(
|
||||
builder.Eq{"tm2.user_id": a.GetID()},
|
||||
builder.Eq{"tl.right": r},
|
||||
))
|
||||
// If the namespace this list belongs to was shared directly with the team and the team has the right
|
||||
conds = append(conds, builder.And(
|
||||
builder.Eq{"tm.user_id": a.GetID()},
|
||||
builder.Eq{"tn.right": r},
|
||||
))
|
||||
}
|
||||
|
||||
// If the user is the owner of a namespace, it has any right, all the time
|
||||
conds = append(conds, builder.Eq{"n.owner_id": a.GetID()})
|
||||
|
||||
type allListRights struct {
|
||||
UserNamespace *NamespaceUser `xorm:"extends"`
|
||||
UserList *ListUser `xorm:"extends"`
|
||||
|
||||
TeamNamespace *TeamNamespace `xorm:"extends"`
|
||||
TeamList *TeamList `xorm:"extends"`
|
||||
|
||||
NamespaceOwnerID int64 `xorm:"namespaces_owner_id"`
|
||||
}
|
||||
|
||||
r := &allListRights{}
|
||||
var maxRight = 0
|
||||
exists, err := s.
|
||||
Select("l.*, un.right, ul.right, tn.right, tl.right, n.owner_id as namespaces_owner_id").
|
||||
Table("lists").
|
||||
Alias("l").
|
||||
// User stuff
|
||||
Join("LEFT", []string{"users_namespaces", "un"}, "un.namespace_id = l.namespace_id").
|
||||
Join("LEFT", []string{"users_lists", "ul"}, "ul.list_id = l.id").
|
||||
Join("LEFT", []string{"namespaces", "n"}, "n.id = l.namespace_id").
|
||||
// Team stuff
|
||||
Join("LEFT", []string{"team_namespaces", "tn"}, " l.namespace_id = tn.namespace_id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
|
||||
Join("LEFT", []string{"team_lists", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
|
||||
// The actual condition
|
||||
Where(builder.And(
|
||||
builder.Or(
|
||||
conds...,
|
||||
),
|
||||
builder.Eq{"l.id": l.ID},
|
||||
)).
|
||||
Get(r)
|
||||
|
||||
// Figure out the max right and return it
|
||||
if int(r.UserNamespace.Right) > maxRight {
|
||||
maxRight = int(r.UserNamespace.Right)
|
||||
}
|
||||
if int(r.UserList.Right) > maxRight {
|
||||
maxRight = int(r.UserList.Right)
|
||||
}
|
||||
if int(r.TeamNamespace.Right) > maxRight {
|
||||
maxRight = int(r.TeamNamespace.Right)
|
||||
}
|
||||
if int(r.TeamList.Right) > maxRight {
|
||||
maxRight = int(r.TeamList.Right)
|
||||
}
|
||||
if r.NamespaceOwnerID == a.GetID() {
|
||||
maxRight = int(RightAdmin)
|
||||
}
|
||||
|
||||
return exists, maxRight, err
|
||||
}
|
|
@ -33,10 +33,8 @@ import (
|
|||
|
||||
// RegisterListeners registers all event listeners
|
||||
func RegisterListeners() {
|
||||
events.RegisterListener((&ListCreatedEvent{}).Name(), &IncreaseListCounter{})
|
||||
events.RegisterListener((&ListDeletedEvent{}).Name(), &DecreaseListCounter{})
|
||||
events.RegisterListener((&NamespaceCreatedEvent{}).Name(), &IncreaseNamespaceCounter{})
|
||||
events.RegisterListener((&NamespaceDeletedEvent{}).Name(), &DecreaseNamespaceCounter{})
|
||||
events.RegisterListener((&ProjectCreatedEvent{}).Name(), &IncreaseProjectCounter{})
|
||||
events.RegisterListener((&ProjectDeletedEvent{}).Name(), &DecreaseProjectCounter{})
|
||||
events.RegisterListener((&TaskCreatedEvent{}).Name(), &IncreaseTaskCounter{})
|
||||
events.RegisterListener((&TaskDeletedEvent{}).Name(), &DecreaseTaskCounter{})
|
||||
events.RegisterListener((&TeamDeletedEvent{}).Name(), &DecreaseTeamCounter{})
|
||||
|
@ -44,7 +42,7 @@ func RegisterListeners() {
|
|||
events.RegisterListener((&TaskCommentCreatedEvent{}).Name(), &SendTaskCommentNotification{})
|
||||
events.RegisterListener((&TaskAssigneeCreatedEvent{}).Name(), &SendTaskAssignedNotification{})
|
||||
events.RegisterListener((&TaskDeletedEvent{}).Name(), &SendTaskDeletedNotification{})
|
||||
events.RegisterListener((&ListCreatedEvent{}).Name(), &SendListCreatedNotification{})
|
||||
events.RegisterListener((&ProjectCreatedEvent{}).Name(), &SendProjectCreatedNotification{})
|
||||
events.RegisterListener((&TaskAssigneeCreatedEvent{}).Name(), &SubscribeAssigneeToTask{})
|
||||
events.RegisterListener((&TeamMemberAddedEvent{}).Name(), &SendTeamMemberAddedNotification{})
|
||||
events.RegisterListener((&TaskCommentUpdatedEvent{}).Name(), &HandleTaskCommentEditMentions{})
|
||||
|
@ -409,42 +407,42 @@ func (s *HandleTaskUpdatedMentions) Handle(msg *message.Message) (err error) {
|
|||
}
|
||||
|
||||
///////
|
||||
// List Event Listeners
|
||||
// Project Event Listeners
|
||||
|
||||
type IncreaseListCounter struct {
|
||||
type IncreaseProjectCounter struct {
|
||||
}
|
||||
|
||||
func (s *IncreaseListCounter) Name() string {
|
||||
return "list.counter.increase"
|
||||
func (s *IncreaseProjectCounter) Name() string {
|
||||
return "project.counter.increase"
|
||||
}
|
||||
|
||||
func (s *IncreaseListCounter) Handle(msg *message.Message) (err error) {
|
||||
return keyvalue.IncrBy(metrics.ListCountKey, 1)
|
||||
func (s *IncreaseProjectCounter) Handle(msg *message.Message) (err error) {
|
||||
return keyvalue.IncrBy(metrics.ProjectCountKey, 1)
|
||||
}
|
||||
|
||||
type DecreaseListCounter struct {
|
||||
type DecreaseProjectCounter struct {
|
||||
}
|
||||
|
||||
func (s *DecreaseListCounter) Name() string {
|
||||
return "list.counter.decrease"
|
||||
func (s *DecreaseProjectCounter) Name() string {
|
||||
return "project.counter.decrease"
|
||||
}
|
||||
|
||||
func (s *DecreaseListCounter) Handle(msg *message.Message) (err error) {
|
||||
return keyvalue.DecrBy(metrics.ListCountKey, 1)
|
||||
func (s *DecreaseProjectCounter) Handle(msg *message.Message) (err error) {
|
||||
return keyvalue.DecrBy(metrics.ProjectCountKey, 1)
|
||||
}
|
||||
|
||||
// SendListCreatedNotification represents a listener
|
||||
type SendListCreatedNotification struct {
|
||||
// SendProjectCreatedNotification represents a listener
|
||||
type SendProjectCreatedNotification struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the SendListCreatedNotification listener
|
||||
func (s *SendListCreatedNotification) Name() string {
|
||||
return "send.list.created.notification"
|
||||
// Name defines the name for the SendProjectCreatedNotification listener
|
||||
func (s *SendProjectCreatedNotification) Name() string {
|
||||
return "send.project.created.notification"
|
||||
}
|
||||
|
||||
// Handle is executed when the event SendListCreatedNotification listens on is fired
|
||||
func (s *SendListCreatedNotification) Handle(msg *message.Message) (err error) {
|
||||
event := &ListCreatedEvent{}
|
||||
// Handle is executed when the event SendProjectCreatedNotification listens on is fired
|
||||
func (s *SendProjectCreatedNotification) Handle(msg *message.Message) (err error) {
|
||||
event := &ProjectCreatedEvent{}
|
||||
err = json.Unmarshal(msg.Payload, event)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -453,21 +451,21 @@ func (s *SendListCreatedNotification) Handle(msg *message.Message) (err error) {
|
|||
sess := db.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
subscribers, err := getSubscribersForEntity(sess, SubscriptionEntityList, event.List.ID)
|
||||
subscribers, err := getSubscribersForEntity(sess, SubscriptionEntityProject, event.Project.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Sending list created notifications to %d subscribers for list %d", len(subscribers), event.List.ID)
|
||||
log.Debugf("Sending project created notifications to %d subscribers for project %d", len(subscribers), event.Project.ID)
|
||||
|
||||
for _, subscriber := range subscribers {
|
||||
if subscriber.UserID == event.Doer.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
n := &ListCreatedNotification{
|
||||
Doer: event.Doer,
|
||||
List: event.List,
|
||||
n := &ProjectCreatedNotification{
|
||||
Doer: event.Doer,
|
||||
Project: event.Project,
|
||||
}
|
||||
err = notifications.Notify(subscriber.User, n)
|
||||
if err != nil {
|
||||
|
@ -478,37 +476,6 @@ func (s *SendListCreatedNotification) Handle(msg *message.Message) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
//////
|
||||
// Namespace events
|
||||
|
||||
// IncreaseNamespaceCounter represents a listener
|
||||
type IncreaseNamespaceCounter struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the IncreaseNamespaceCounter listener
|
||||
func (s *IncreaseNamespaceCounter) Name() string {
|
||||
return "namespace.counter.increase"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event IncreaseNamespaceCounter listens on is fired
|
||||
func (s *IncreaseNamespaceCounter) Handle(msg *message.Message) (err error) {
|
||||
return keyvalue.IncrBy(metrics.NamespaceCountKey, 1)
|
||||
}
|
||||
|
||||
// DecreaseNamespaceCounter represents a listener
|
||||
type DecreaseNamespaceCounter struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the DecreaseNamespaceCounter listener
|
||||
func (s *DecreaseNamespaceCounter) Name() string {
|
||||
return "namespace.counter.decrease"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event DecreaseNamespaceCounter listens on is fired
|
||||
func (s *DecreaseNamespaceCounter) Handle(msg *message.Message) (err error) {
|
||||
return keyvalue.DecrBy(metrics.NamespaceCountKey, 1)
|
||||
}
|
||||
|
||||
///////
|
||||
// Team Events
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ func TestSendingMentionNotification(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
tc := &TaskComment{
|
||||
Comment: "Lorem Ipsum @user1 @user2 @user3 @user4 @user5 @user6",
|
||||
TaskID: 32, // user2 has access to the list that task belongs to
|
||||
TaskID: 32, // user2 has access to the project that task belongs to
|
||||
}
|
||||
err = tc.Create(s, u)
|
||||
assert.NoError(t, err)
|
||||
|
@ -156,7 +156,7 @@ func TestSendingMentionNotification(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
tc := &TaskComment{
|
||||
Comment: "Lorem Ipsum @user2",
|
||||
TaskID: 32, // user2 has access to the list that task belongs to
|
||||
TaskID: 32, // user2 has access to the project that task belongs to
|
||||
}
|
||||
err = tc.Create(s, u)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -39,15 +39,12 @@ var (
|
|||
// GetTables returns all structs which are also a table.
|
||||
func GetTables() []interface{} {
|
||||
return []interface{}{
|
||||
&List{},
|
||||
&Project{},
|
||||
&Task{},
|
||||
&Team{},
|
||||
&TeamMember{},
|
||||
&TeamList{},
|
||||
&TeamNamespace{},
|
||||
&Namespace{},
|
||||
&ListUser{},
|
||||
&NamespaceUser{},
|
||||
&TeamProject{},
|
||||
&ProjectUser{},
|
||||
&TaskAssginee{},
|
||||
&Label{},
|
||||
&LabelTask{},
|
||||
|
|
|
@ -1,774 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// Namespace holds informations about a namespace
|
||||
type Namespace struct {
|
||||
// The unique, numeric id of this namespace.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
// The name of this namespace.
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
// The description of the namespace
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
OwnerID int64 `xorm:"bigint not null INDEX" json:"-"`
|
||||
|
||||
// The hex color of this namespace
|
||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
|
||||
|
||||
// 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
|
||||
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
|
||||
|
||||
// The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.
|
||||
// Will only returned when retreiving one namespace.
|
||||
Subscription *Subscription `xorm:"-" json:"subscription,omitempty"`
|
||||
|
||||
// A timestamp when this namespace was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this namespace was last updated. You cannot change this value.
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
|
||||
// If set to true, will only return the namespaces, not their lists.
|
||||
NamespacesOnly bool `xorm:"-" json:"-" query:"namespaces_only"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// SharedListsPseudoNamespace is a pseudo namespace used to hold shared lists
|
||||
var SharedListsPseudoNamespace = Namespace{
|
||||
ID: -1,
|
||||
Title: "Shared Lists",
|
||||
Description: "Lists of other users shared with you via teams or directly.",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// FavoritesPseudoNamespace is a pseudo namespace used to hold favorited lists and tasks
|
||||
var FavoritesPseudoNamespace = Namespace{
|
||||
ID: -2,
|
||||
Title: "Favorites",
|
||||
Description: "Favorite lists and tasks.",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// SavedFiltersPseudoNamespace is a pseudo namespace used to hold saved filters
|
||||
var SavedFiltersPseudoNamespace = Namespace{
|
||||
ID: -3,
|
||||
Title: "Filters",
|
||||
Description: "Saved filters.",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (Namespace) TableName() string {
|
||||
return "namespaces"
|
||||
}
|
||||
|
||||
// GetSimpleByID gets a namespace without things like the owner, it more or less only checks if it exists.
|
||||
func getNamespaceSimpleByID(s *xorm.Session, id int64) (namespace *Namespace, err error) {
|
||||
if id == 0 {
|
||||
return nil, ErrNamespaceDoesNotExist{ID: id}
|
||||
}
|
||||
|
||||
// Get the namesapce with shared lists
|
||||
if id == -1 {
|
||||
return &SharedListsPseudoNamespace, nil
|
||||
}
|
||||
|
||||
if id == FavoritesPseudoNamespace.ID {
|
||||
return &FavoritesPseudoNamespace, nil
|
||||
}
|
||||
|
||||
if id == SavedFiltersPseudoNamespace.ID {
|
||||
return &SavedFiltersPseudoNamespace, nil
|
||||
}
|
||||
|
||||
namespace = &Namespace{}
|
||||
|
||||
exists, err := s.Where("id = ?", id).Get(namespace)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
return nil, ErrNamespaceDoesNotExist{ID: id}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetNamespaceByID returns a namespace object by its ID
|
||||
func GetNamespaceByID(s *xorm.Session, id int64) (namespace *Namespace, err error) {
|
||||
namespace, err = getNamespaceSimpleByID(s, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the namespace Owner
|
||||
namespace.Owner, err = user.GetUserByID(s, namespace.OwnerID)
|
||||
return
|
||||
}
|
||||
|
||||
// CheckIsArchived returns an ErrNamespaceIsArchived if the namepace is archived.
|
||||
func (n *Namespace) CheckIsArchived(s *xorm.Session) error {
|
||||
exists, err := s.
|
||||
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
|
||||
// @Summary Gets one namespace
|
||||
// @Description Returns a namespace by its ID.
|
||||
// @tags namespace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Success 200 {object} models.Namespace "The Namespace"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to that namespace."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id} [get]
|
||||
func (n *Namespace) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
nn, err := GetNamespaceByID(s, n.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*n = *nn
|
||||
|
||||
n.Subscription, err = GetSubscription(s, SubscriptionEntityNamespace, n.ID, a)
|
||||
return
|
||||
}
|
||||
|
||||
// NamespaceWithLists represents a namespace with list meta informations
|
||||
type NamespaceWithLists struct {
|
||||
Namespace `xorm:"extends"`
|
||||
Lists []*List `xorm:"-" json:"lists"`
|
||||
}
|
||||
|
||||
type NamespaceWithListsAndTasks struct {
|
||||
Namespace
|
||||
Lists []*ListWithTasksAndBuckets `xorm:"-" json:"lists"`
|
||||
}
|
||||
|
||||
func makeNamespaceSlice(namespaces map[int64]*NamespaceWithLists, userMap map[int64]*user.User, subscriptions map[int64]*Subscription) []*NamespaceWithLists {
|
||||
all := make([]*NamespaceWithLists, 0, len(namespaces))
|
||||
for _, n := range namespaces {
|
||||
n.Owner = userMap[n.OwnerID]
|
||||
n.Subscription = subscriptions[n.ID]
|
||||
all = append(all, n)
|
||||
for _, l := range n.Lists {
|
||||
if n.Subscription != nil && l.Subscription == nil {
|
||||
l.Subscription = n.Subscription
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool {
|
||||
return all[i].ID < all[j].ID
|
||||
})
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
func getNamespaceFilterCond(search string) (filterCond builder.Cond) {
|
||||
filterCond = db.ILIKE("namespaces.title", search)
|
||||
|
||||
if search == "" {
|
||||
return
|
||||
}
|
||||
|
||||
vals := strings.Split(search, ",")
|
||||
|
||||
if len(vals) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ids := []int64{}
|
||||
for _, val := range vals {
|
||||
v, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
log.Debugf("Namespace search string part '%s' is not a number: %s", val, err)
|
||||
continue
|
||||
}
|
||||
ids = append(ids, v)
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
filterCond = builder.In("namespaces.id", ids)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getNamespaceArchivedCond(archived bool) builder.Cond {
|
||||
// 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 !archived {
|
||||
isArchivedCond = builder.And(
|
||||
builder.Eq{"namespaces.is_archived": false},
|
||||
)
|
||||
}
|
||||
|
||||
return isArchivedCond
|
||||
}
|
||||
|
||||
func getNamespacesWithLists(s *xorm.Session, namespaces *map[int64]*NamespaceWithLists, search string, isArchived bool, page, perPage int, userID int64) (numberOfTotalItems int64, err error) {
|
||||
isArchivedCond := getNamespaceArchivedCond(isArchived)
|
||||
filterCond := getNamespaceFilterCond(search)
|
||||
|
||||
limit, start := getLimitFromPageIndex(page, perPage)
|
||||
query := s.Select("namespaces.*").
|
||||
Table("namespaces").
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
|
||||
Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
|
||||
Where("team_members.user_id = ?", userID).
|
||||
Or("namespaces.owner_id = ?", userID).
|
||||
Or("users_namespaces.user_id = ?", userID).
|
||||
GroupBy("namespaces.id").
|
||||
Where(filterCond).
|
||||
Where(isArchivedCond)
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
err = query.Find(namespaces)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
numberOfTotalItems, err = s.
|
||||
Table("namespaces").
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
|
||||
Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
|
||||
Where("team_members.user_id = ?", userID).
|
||||
Or("namespaces.owner_id = ?", userID).
|
||||
Or("users_namespaces.user_id = ?", userID).
|
||||
And("namespaces.is_archived = false").
|
||||
GroupBy("namespaces.id").
|
||||
Where(filterCond).
|
||||
Where(isArchivedCond).
|
||||
Count(&NamespaceWithLists{})
|
||||
return numberOfTotalItems, err
|
||||
}
|
||||
|
||||
func getNamespaceOwnerIDs(namespaces map[int64]*NamespaceWithLists) (namespaceIDs, ownerIDs []int64) {
|
||||
for _, nsp := range namespaces {
|
||||
namespaceIDs = append(namespaceIDs, nsp.ID)
|
||||
ownerIDs = append(ownerIDs, nsp.OwnerID)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getNamespaceSubscriptions(s *xorm.Session, namespaceIDs []int64, userID int64) (map[int64]*Subscription, error) {
|
||||
subscriptionsMap := make(map[int64]*Subscription)
|
||||
if len(namespaceIDs) == 0 {
|
||||
return subscriptionsMap, nil
|
||||
}
|
||||
|
||||
subscriptions := []*Subscription{}
|
||||
err := s.
|
||||
Where("entity_type = ? AND user_id = ?", SubscriptionEntityNamespace, userID).
|
||||
In("entity_id", namespaceIDs).
|
||||
Find(&subscriptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, sub := range subscriptions {
|
||||
sub.Entity = sub.EntityType.String()
|
||||
subscriptionsMap[sub.EntityID] = sub
|
||||
}
|
||||
|
||||
return subscriptionsMap, err
|
||||
}
|
||||
|
||||
func getListsForNamespaces(s *xorm.Session, namespaceIDs []int64, archived bool) ([]*List, error) {
|
||||
lists := []*List{}
|
||||
listQuery := s.
|
||||
OrderBy("position").
|
||||
In("namespace_id", namespaceIDs)
|
||||
|
||||
if !archived {
|
||||
listQuery.And("is_archived = false")
|
||||
}
|
||||
err := listQuery.Find(&lists)
|
||||
return lists, err
|
||||
}
|
||||
|
||||
func getSharedListsInNamespace(s *xorm.Session, archived bool, doer *user.User) (sharedListsNamespace *NamespaceWithLists, err error) {
|
||||
// Create our pseudo namespace to hold the shared lists
|
||||
sharedListsPseudonamespace := SharedListsPseudoNamespace
|
||||
sharedListsPseudonamespace.OwnerID = doer.ID
|
||||
sharedListsNamespace = &NamespaceWithLists{
|
||||
sharedListsPseudonamespace,
|
||||
[]*List{},
|
||||
}
|
||||
|
||||
// Get all lists individually shared with our user (not via a namespace)
|
||||
individualLists := []*List{}
|
||||
iListQuery := s.Select("l.*").
|
||||
Table("lists").
|
||||
Alias("l").
|
||||
Join("LEFT", []string{"team_lists", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tl.team_id").
|
||||
Join("LEFT", []string{"users_lists", "ul"}, "ul.list_id = l.id").
|
||||
Where(builder.And(
|
||||
builder.Eq{"tm.user_id": doer.ID},
|
||||
builder.Neq{"l.owner_id": doer.ID},
|
||||
)).
|
||||
Or(builder.And(
|
||||
builder.Eq{"ul.user_id": doer.ID},
|
||||
builder.Neq{"l.owner_id": doer.ID},
|
||||
)).
|
||||
GroupBy("l.id")
|
||||
if !archived {
|
||||
iListQuery.And("l.is_archived = false")
|
||||
}
|
||||
err = iListQuery.Find(&individualLists)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Make the namespace -1 so we now later which one it was
|
||||
// + Append it to all lists we already have
|
||||
for _, l := range individualLists {
|
||||
l.NamespaceID = sharedListsNamespace.ID
|
||||
}
|
||||
|
||||
sharedListsNamespace.Lists = individualLists
|
||||
|
||||
// Remove the sharedListsPseudonamespace if we don't have any shared lists
|
||||
if len(individualLists) == 0 {
|
||||
sharedListsNamespace = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getFavoriteLists(s *xorm.Session, lists []*List, namespaceIDs []int64, doer *user.User) (favoriteNamespace *NamespaceWithLists, err error) {
|
||||
// Create our pseudo namespace with favorite lists
|
||||
pseudoFavoriteNamespace := FavoritesPseudoNamespace
|
||||
pseudoFavoriteNamespace.OwnerID = doer.ID
|
||||
favoriteNamespace = &NamespaceWithLists{
|
||||
Namespace: pseudoFavoriteNamespace,
|
||||
Lists: []*List{{}},
|
||||
}
|
||||
*favoriteNamespace.Lists[0] = FavoritesPseudoList // Copying the list to be able to modify it later
|
||||
favoriteNamespace.Lists[0].Owner = doer
|
||||
|
||||
for _, list := range lists {
|
||||
if !list.IsFavorite {
|
||||
continue
|
||||
}
|
||||
favoriteNamespace.Lists = append(favoriteNamespace.Lists, list)
|
||||
}
|
||||
|
||||
// Check if we have any favorites or favorited lists and remove the favorites namespace from the list if not
|
||||
cond := builder.
|
||||
Select("tasks.id").
|
||||
From("tasks").
|
||||
Join("INNER", "lists", "tasks.list_id = lists.id").
|
||||
Join("INNER", "namespaces", "lists.namespace_id = namespaces.id").
|
||||
Where(builder.In("namespaces.id", namespaceIDs))
|
||||
|
||||
var favoriteCount int64
|
||||
favoriteCount, err = s.
|
||||
Where(builder.And(
|
||||
builder.Eq{"user_id": doer.ID},
|
||||
builder.Eq{"kind": FavoriteKindTask},
|
||||
builder.In("entity_id", cond),
|
||||
)).
|
||||
Count(&Favorite{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If we don't have any favorites in the favorites pseudo list, remove that pseudo list from the namespace
|
||||
if favoriteCount == 0 {
|
||||
for in, l := range favoriteNamespace.Lists {
|
||||
if l.ID == FavoritesPseudoList.ID {
|
||||
favoriteNamespace.Lists = append(favoriteNamespace.Lists[:in], favoriteNamespace.Lists[in+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have any favorites in the namespace, remove it
|
||||
if len(favoriteNamespace.Lists) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *NamespaceWithLists, err error) {
|
||||
savedFilters, err := getSavedFiltersForUser(s, doer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(savedFilters) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
savedFiltersPseudoNamespace := SavedFiltersPseudoNamespace
|
||||
savedFiltersPseudoNamespace.OwnerID = doer.ID
|
||||
savedFiltersNamespace = &NamespaceWithLists{
|
||||
Namespace: savedFiltersPseudoNamespace,
|
||||
Lists: make([]*List, 0, len(savedFilters)),
|
||||
}
|
||||
|
||||
for _, filter := range savedFilters {
|
||||
filterList := filter.toList()
|
||||
filterList.NamespaceID = savedFiltersNamespace.ID
|
||||
filterList.Owner = doer
|
||||
savedFiltersNamespace.Lists = append(savedFiltersNamespace.Lists, filterList)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll gets all namespaces a user has access to
|
||||
// @Summary Get all namespaces a user has access to
|
||||
// @Description Returns all namespaces a user has access to.
|
||||
// @tags namespace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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 s query string false "Search namespaces by name."
|
||||
// @Param is_archived query bool false "If true, also returns all archived namespaces."
|
||||
// @Param namespaces_only query bool false "If true, also returns only namespaces without their lists."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.NamespaceWithLists "The Namespaces."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces [get]
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return nil, 0, 0, ErrGenericForbidden{}
|
||||
}
|
||||
|
||||
// This map will hold all namespaces and their lists. The key is usually the id of the namespace.
|
||||
// We're using a map here because it makes a few things like adding lists or removing pseudo namespaces easier.
|
||||
namespaces := make(map[int64]*NamespaceWithLists)
|
||||
|
||||
//////////////////////////////
|
||||
// Lists with their namespaces
|
||||
|
||||
doer, err := user.GetFromAuth(a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
numberOfTotalItems, err = getNamespacesWithLists(s, &namespaces, search, n.IsArchived, page, perPage, doer.ID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
namespaceIDs, ownerIDs := getNamespaceOwnerIDs(namespaces)
|
||||
|
||||
if len(namespaceIDs) == 0 {
|
||||
return nil, 0, 0, nil
|
||||
}
|
||||
|
||||
subscriptionsMap, err := getNamespaceSubscriptions(s, namespaceIDs, doer.ID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
ownerMap, err := user.GetUsersByIDs(s, ownerIDs)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
ownerMap[doer.ID] = doer
|
||||
|
||||
if n.NamespacesOnly {
|
||||
all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap)
|
||||
return all, len(all), numberOfTotalItems, nil
|
||||
}
|
||||
|
||||
// Get all lists
|
||||
lists, err := getListsForNamespaces(s, namespaceIDs, n.IsArchived)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
///////////////
|
||||
// Shared Lists
|
||||
|
||||
sharedListsNamespace, err := getSharedListsInNamespace(s, n.IsArchived, doer)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if sharedListsNamespace != nil {
|
||||
namespaces[sharedListsNamespace.ID] = sharedListsNamespace
|
||||
lists = append(lists, sharedListsNamespace.Lists...)
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Saved Filters
|
||||
|
||||
savedFiltersNamespace, err := getSavedFilters(s, doer)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if savedFiltersNamespace != nil {
|
||||
namespaces[savedFiltersNamespace.ID] = savedFiltersNamespace
|
||||
lists = append(lists, savedFiltersNamespace.Lists...)
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Add list details (favorite state, among other things)
|
||||
err = addListDetails(s, lists, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Favorite lists
|
||||
|
||||
favoritesNamespace, err := getFavoriteLists(s, lists, namespaceIDs, doer)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if favoritesNamespace != nil {
|
||||
namespaces[favoritesNamespace.ID] = favoritesNamespace
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Put it all together
|
||||
|
||||
for _, list := range lists {
|
||||
if list.NamespaceID == SharedListsPseudoNamespace.ID || list.NamespaceID == SavedFiltersPseudoNamespace.ID {
|
||||
// Shared lists and filtered lists are already in the namespace
|
||||
continue
|
||||
}
|
||||
namespaces[list.NamespaceID].Lists = append(namespaces[list.NamespaceID].Lists, list)
|
||||
}
|
||||
|
||||
all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap)
|
||||
return all, len(all), numberOfTotalItems, err
|
||||
}
|
||||
|
||||
// Create implements the creation method via the interface
|
||||
// @Summary Creates a new namespace
|
||||
// @Description Creates a new namespace.
|
||||
// @tags namespace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param namespace body models.Namespace true "The namespace you want to create."
|
||||
// @Success 201 {object} models.Namespace "The created namespace."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid namespace object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces [put]
|
||||
func (n *Namespace) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// Check if we have at least a title
|
||||
if n.Title == "" {
|
||||
return ErrNamespaceNameCannotBeEmpty{NamespaceID: 0, UserID: a.GetID()}
|
||||
}
|
||||
|
||||
n.Owner, err = user.GetUserByID(s, a.GetID())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n.OwnerID = n.Owner.ID
|
||||
|
||||
if _, err = s.Insert(n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = events.Dispatch(&NamespaceCreatedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CreateNewNamespaceForUser creates a new namespace for a user. To prevent import cycles, we can't do that
|
||||
// directly in the user.Create function.
|
||||
func CreateNewNamespaceForUser(s *xorm.Session, user *user.User) (err error) {
|
||||
newN := &Namespace{
|
||||
Title: user.Username,
|
||||
Description: user.Username + "'s namespace.",
|
||||
}
|
||||
return newN.Create(s, user)
|
||||
}
|
||||
|
||||
// Delete deletes a namespace
|
||||
// @Summary Deletes a namespace
|
||||
// @Description Delets a namespace
|
||||
// @tags namespace
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Success 200 {object} models.Message "The namespace was successfully deleted."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid namespace object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id} [delete]
|
||||
func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
return deleteNamespace(s, n, a, true)
|
||||
}
|
||||
|
||||
func deleteNamespace(s *xorm.Session, n *Namespace, a web.Auth, withLists bool) (err error) {
|
||||
// Check if the namespace exists
|
||||
_, err = GetNamespaceByID(s, n.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the namespace
|
||||
_, err = s.ID(n.ID).Delete(&Namespace{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
namespaceDeleted := &NamespaceDeletedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
}
|
||||
|
||||
if !withLists {
|
||||
return events.Dispatch(namespaceDeleted)
|
||||
}
|
||||
|
||||
// Delete all lists with their tasks
|
||||
lists, err := GetListsByNamespaceID(s, n.ID, &user.User{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(lists) == 0 {
|
||||
return events.Dispatch(namespaceDeleted)
|
||||
}
|
||||
|
||||
// Looping over all lists to let the list handle properly cleaning up the tasks and everything else associated with it.
|
||||
for _, list := range lists {
|
||||
err = list.Delete(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return events.Dispatch(namespaceDeleted)
|
||||
}
|
||||
|
||||
// Update implements the update method via the interface
|
||||
// @Summary Updates a namespace
|
||||
// @Description Updates a namespace.
|
||||
// @tags namespace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Param namespace body models.Namespace true "The namespace with updated values you want to update."
|
||||
// @Success 200 {object} models.Namespace "The updated namespace."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid namespace object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespace/{id} [post]
|
||||
func (n *Namespace) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
// Check if we have at least a name
|
||||
if n.Title == "" {
|
||||
return ErrNamespaceNameCannotBeEmpty{NamespaceID: n.ID}
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
currentNamespace, err := GetNamespaceByID(s, n.ID)
|
||||
if err != nil {
|
||||
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
|
||||
if n.Owner != nil {
|
||||
n.OwnerID = n.Owner.ID
|
||||
if currentNamespace.OwnerID != n.OwnerID {
|
||||
n.Owner, err = user.GetUserByID(s, n.OwnerID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to specify the cols we want to update here to be able to un-archive lists
|
||||
colsToUpdate := []string{
|
||||
"title",
|
||||
"is_archived",
|
||||
"hex_color",
|
||||
}
|
||||
if n.Description != "" {
|
||||
colsToUpdate = append(colsToUpdate, "description")
|
||||
}
|
||||
|
||||
// Do the actual update
|
||||
_, err = s.
|
||||
ID(currentNamespace.ID).
|
||||
Cols(colsToUpdate...).
|
||||
Update(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return events.Dispatch(&NamespaceUpdatedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CanWrite checks if a user has write access to a namespace
|
||||
func (n *Namespace) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
can, _, err := n.checkRight(s, a, RightWrite, RightAdmin)
|
||||
return can, err
|
||||
}
|
||||
|
||||
// IsAdmin returns true or false if the user is admin on that namespace or not
|
||||
func (n *Namespace) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
is, _, err := n.checkRight(s, a, RightAdmin)
|
||||
return is, err
|
||||
}
|
||||
|
||||
// CanRead checks if a user has read access to that namespace
|
||||
func (n *Namespace) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
return n.checkRight(s, a, RightRead, RightWrite, RightAdmin)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update the namespace
|
||||
func (n *Namespace) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return n.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a namespace
|
||||
func (n *Namespace) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return n.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
// CanCreate checks if the user can create a new namespace
|
||||
func (n *Namespace) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// This is currently a dummy function, later on we could imagine global limits etc.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (n *Namespace) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
|
||||
|
||||
// If the auth is a link share, don't do anything
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, 0, nil
|
||||
}
|
||||
|
||||
// Get the namespace and check the right
|
||||
nn, err := getNamespaceSimpleByID(s, n.ID)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
if a.GetID() == nn.OwnerID ||
|
||||
nn.ID == SharedListsPseudoNamespace.ID ||
|
||||
nn.ID == FavoritesPseudoNamespace.ID ||
|
||||
nn.ID == SavedFiltersPseudoNamespace.ID {
|
||||
return true, int(RightAdmin), nil
|
||||
}
|
||||
|
||||
/*
|
||||
The following loop creates an sql condition like this one:
|
||||
|
||||
namespaces.owner_id = 1 OR
|
||||
(users_namespaces.user_id = 1 AND users_namespaces.right = 1) OR
|
||||
(team_members.user_id = 1 AND team_namespaces.right = 1) OR
|
||||
|
||||
|
||||
for each passed right. That way, we can check with a single sql query (instead if 8)
|
||||
if the user has the right to see the list or not.
|
||||
*/
|
||||
|
||||
var conds []builder.Cond
|
||||
conds = append(conds, builder.Eq{"namespaces.owner_id": a.GetID()})
|
||||
for _, r := range rights {
|
||||
// User conditions
|
||||
// If the namespace was shared directly with the user and the user has the right
|
||||
conds = append(conds, builder.And(
|
||||
builder.Eq{"users_namespaces.user_id": a.GetID()},
|
||||
builder.Eq{"users_namespaces.right": r},
|
||||
))
|
||||
|
||||
// Team rights
|
||||
// If the namespace was shared directly with the team and the team has the right
|
||||
conds = append(conds, builder.And(
|
||||
builder.Eq{"team_members.user_id": a.GetID()},
|
||||
builder.Eq{"team_namespaces.right": r},
|
||||
))
|
||||
}
|
||||
|
||||
type allRights struct {
|
||||
UserNamespace NamespaceUser `xorm:"extends"`
|
||||
TeamNamespace TeamNamespace `xorm:"extends"`
|
||||
}
|
||||
|
||||
var maxRights = 0
|
||||
r := &allRights{}
|
||||
exists, err := s.
|
||||
Select("*").
|
||||
Table("namespaces").
|
||||
// User stuff
|
||||
Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
|
||||
// Teams stuff
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
|
||||
// The actual condition
|
||||
Where(builder.And(
|
||||
builder.Or(
|
||||
conds...,
|
||||
),
|
||||
builder.Eq{"namespaces.id": n.ID},
|
||||
)).
|
||||
Exist(r)
|
||||
|
||||
// Figure out the max right and return it
|
||||
if int(r.UserNamespace.Right) > maxRights {
|
||||
maxRights = int(r.UserNamespace.Right)
|
||||
}
|
||||
if int(r.TeamNamespace.Right) > maxRights {
|
||||
maxRights = int(r.TeamNamespace.Right)
|
||||
}
|
||||
|
||||
return exists, maxRights, err
|
||||
}
|
|
@ -1,244 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/web"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// TeamNamespace defines the relationship between a Team and a Namespace
|
||||
type TeamNamespace struct {
|
||||
// The unique, numeric id of this namespace <-> team relation.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
|
||||
// The team id.
|
||||
TeamID int64 `xorm:"bigint not null INDEX" json:"team_id" param:"team"`
|
||||
// The namespace id.
|
||||
NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"`
|
||||
// The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
|
||||
Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
|
||||
// A timestamp when this relation was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this relation was last updated. You cannot change this value.
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (TeamNamespace) TableName() string {
|
||||
return "team_namespaces"
|
||||
}
|
||||
|
||||
// Create creates a new team <-> namespace relation
|
||||
// @Summary Add a team to a namespace
|
||||
// @Description Gives a team access to a namespace.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Param namespace body models.TeamNamespace true "The team you want to add to the namespace."
|
||||
// @Success 201 {object} models.TeamNamespace "The created team<->namespace relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid team namespace object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The team does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "The team does not have access to the namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id}/teams [put]
|
||||
func (tn *TeamNamespace) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the rights are valid
|
||||
if err = tn.Right.isValid(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team exists
|
||||
team, err := GetTeamByID(s, tn.TeamID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
namespace, err := GetNamespaceByID(s, tn.NamespaceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the team already has access to the namespace
|
||||
exists, err := s.
|
||||
Where("team_id = ?", tn.TeamID).
|
||||
And("namespace_id = ?", tn.NamespaceID).
|
||||
Get(&TeamNamespace{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
return ErrTeamAlreadyHasAccess{tn.TeamID, tn.NamespaceID}
|
||||
}
|
||||
|
||||
// Insert the new team
|
||||
_, err = s.Insert(tn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return events.Dispatch(&NamespaceSharedWithTeamEvent{
|
||||
Namespace: namespace,
|
||||
Team: team,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete deletes a team <-> namespace relation based on the namespace & team id
|
||||
// @Summary Delete a team from a namespace
|
||||
// @Description Delets a team from a namespace. The team won't have access to the namespace anymore.
|
||||
// @tags sharing
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param namespaceID path int true "Namespace ID"
|
||||
// @Param teamID path int true "team ID"
|
||||
// @Success 200 {object} models.Message "The team was successfully deleted."
|
||||
// @Failure 403 {object} web.HTTPError "The team does not have access to the namespace"
|
||||
// @Failure 404 {object} web.HTTPError "team or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/teams/{teamID} [delete]
|
||||
func (tn *TeamNamespace) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(s, tn.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team has access to the namespace
|
||||
has, err := s.
|
||||
Where("team_id = ? AND namespace_id = ?", tn.TeamID, tn.NamespaceID).
|
||||
Get(&TeamNamespace{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
return ErrTeamDoesNotHaveAccessToNamespace{TeamID: tn.TeamID, NamespaceID: tn.NamespaceID}
|
||||
}
|
||||
|
||||
// Delete the relation
|
||||
_, err = s.
|
||||
Where("team_id = ?", tn.TeamID).
|
||||
And("namespace_id = ?", tn.NamespaceID).
|
||||
Delete(TeamNamespace{})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll implements the method to read all teams of a namespace
|
||||
// @Summary Get teams on a namespace
|
||||
// @Description Returns a namespace with all teams which have access on a given namespace.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @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 s query string false "Search teams by its name."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.TeamWithRight "The teams with the right they have."
|
||||
// @Failure 403 {object} web.HTTPError "No right to see the namespace."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id}/teams [get]
|
||||
func (tn *TeamNamespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
// Check if the user can read the namespace
|
||||
n := Namespace{ID: tn.NamespaceID}
|
||||
canRead, _, err := n.CanRead(s, a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
if !canRead {
|
||||
return nil, 0, 0, ErrNeedToHaveNamespaceReadAccess{NamespaceID: tn.NamespaceID, UserID: a.GetID()}
|
||||
}
|
||||
|
||||
// Get the teams
|
||||
all := []*TeamWithRight{}
|
||||
limit, start := getLimitFromPageIndex(page, perPage)
|
||||
query := s.
|
||||
Table("teams").
|
||||
Join("INNER", "team_namespaces", "team_id = teams.id").
|
||||
Where("team_namespaces.namespace_id = ?", tn.NamespaceID).
|
||||
Where(db.ILIKE("teams.name", search))
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
err = query.Find(&all)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
teams := []*Team{}
|
||||
for _, t := range all {
|
||||
teams = append(teams, &t.Team)
|
||||
}
|
||||
|
||||
err = addMoreInfoToTeams(s, teams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
numberOfTotalItems, err = s.
|
||||
Table("teams").
|
||||
Join("INNER", "team_namespaces", "team_id = teams.id").
|
||||
Where("team_namespaces.namespace_id = ?", tn.NamespaceID).
|
||||
Where("teams.name LIKE ?", "%"+search+"%").
|
||||
Count(&TeamWithRight{})
|
||||
|
||||
return all, len(all), numberOfTotalItems, err
|
||||
}
|
||||
|
||||
// Update updates a team <-> namespace relation
|
||||
// @Summary Update a team <-> namespace relation
|
||||
// @Description Update a team <-> namespace relation. Mostly used to update the right that team has.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param namespaceID path int true "Namespace ID"
|
||||
// @Param teamID path int true "Team ID"
|
||||
// @Param namespace body models.TeamNamespace true "The team you want to update."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} models.TeamNamespace "The updated team <-> namespace relation."
|
||||
// @Failure 403 {object} web.HTTPError "The team does not have admin-access to the namespace"
|
||||
// @Failure 404 {object} web.HTTPError "Team or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/teams/{teamID} [post]
|
||||
func (tn *TeamNamespace) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := tn.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
Where("namespace_id = ? AND team_id = ?", tn.NamespaceID, tn.TeamID).
|
||||
Cols("right").
|
||||
Update(tn)
|
||||
return
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CanCreate checks if one can create a new team <-> namespace relation
|
||||
func (tn *TeamNamespace) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
n := &Namespace{ID: tn.NamespaceID}
|
||||
return n.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
// CanDelete checks if a user can remove a team from a namespace. Only namespace admins can do that.
|
||||
func (tn *TeamNamespace) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
n := &Namespace{ID: tn.NamespaceID}
|
||||
return n.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
// CanUpdate checks if a user can update a team from a Only namespace admins can do that.
|
||||
func (tn *TeamNamespace) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
n := &Namespace{ID: tn.NamespaceID}
|
||||
return n.IsAdmin(s, a)
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
)
|
||||
|
||||
func TestTeamNamespace_CanDoSomething(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
TeamID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
type args struct {
|
||||
a web.Auth
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want map[string]bool
|
||||
}{
|
||||
{
|
||||
name: "CanDoSomething Normally",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
},
|
||||
want: map[string]bool{"CanCreate": true, "CanDelete": true, "CanUpdate": true},
|
||||
},
|
||||
{
|
||||
name: "CanDoSomething for a nonexistant namespace",
|
||||
fields: fields{
|
||||
NamespaceID: 300,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
},
|
||||
want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
|
||||
},
|
||||
{
|
||||
name: "CanDoSomething where the user does not have the rights",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 4},
|
||||
},
|
||||
want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
tn := &TeamNamespace{
|
||||
ID: tt.fields.ID,
|
||||
TeamID: tt.fields.TeamID,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
if got, _ := tn.CanCreate(s, tt.args.a); got != tt.want["CanCreate"] {
|
||||
t.Errorf("TeamNamespace.CanCreate() = %v, want %v", got, tt.want["CanCreate"])
|
||||
}
|
||||
if got, _ := tn.CanDelete(s, tt.args.a); got != tt.want["CanDelete"] {
|
||||
t.Errorf("TeamNamespace.CanDelete() = %v, want %v", got, tt.want["CanDelete"])
|
||||
}
|
||||
if got, _ := tn.CanUpdate(s, tt.args.a); got != tt.want["CanUpdate"] {
|
||||
t.Errorf("TeamNamespace.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"])
|
||||
}
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,298 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTeamNamespace_ReadAll(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
NamespaceID: 3,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
teams, _, _, err := tn.ReadAll(s, u, "", 1, 50)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
|
||||
ts := reflect.ValueOf(teams)
|
||||
assert.Equal(t, ts.Len(), 2)
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant namespace", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
NamespaceID: 9999,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
_, _, _, err := tn.ReadAll(s, u, "", 1, 50)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("no right for namespace", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
NamespaceID: 17,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
_, _, _, err := tn.ReadAll(s, u, "", 1, 50)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNeedToHaveNamespaceReadAccess(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("search", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
NamespaceID: 3,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
teams, _, _, err := tn.ReadAll(s, u, "READ_only_on_list6", 1, 50)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
|
||||
ts := teams.([]*TeamWithRight)
|
||||
assert.Len(t, ts, 1)
|
||||
assert.Equal(t, int64(2), ts[0].ID)
|
||||
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestTeamNamespace_Create(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 1,
|
||||
NamespaceID: 1,
|
||||
Right: RightAdmin,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
allowed, _ := tn.CanCreate(s, u)
|
||||
assert.True(t, allowed)
|
||||
err := tn.Create(s, u)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
db.AssertExists(t, "team_namespaces", map[string]interface{}{
|
||||
"team_id": 1,
|
||||
"namespace_id": 1,
|
||||
"right": RightAdmin,
|
||||
}, false)
|
||||
})
|
||||
t.Run("team already has access", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 1,
|
||||
NamespaceID: 3,
|
||||
Right: RightRead,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Create(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamAlreadyHasAccess(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("invalid team right", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 1,
|
||||
NamespaceID: 3,
|
||||
Right: RightUnknown,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Create(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidRight(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant team", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 9999,
|
||||
NamespaceID: 1,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Create(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant namespace", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 1,
|
||||
NamespaceID: 9999,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Create(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestTeamNamespace_Delete(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 7,
|
||||
NamespaceID: 9,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
allowed, _ := tn.CanDelete(s, u)
|
||||
assert.True(t, allowed)
|
||||
err := tn.Delete(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
db.AssertMissing(t, "team_namespaces", map[string]interface{}{
|
||||
"team_id": 7,
|
||||
"namespace_id": 9,
|
||||
})
|
||||
})
|
||||
t.Run("nonexistant team", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 9999,
|
||||
NamespaceID: 3,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Delete(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant namespace", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 1,
|
||||
NamespaceID: 9999,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Delete(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotHaveAccessToNamespace(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestTeamNamespace_Update(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
TeamID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
errType func(err error) bool
|
||||
}{
|
||||
{
|
||||
name: "Test Update Normally",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
TeamID: 1,
|
||||
Right: RightAdmin,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update to write",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
TeamID: 1,
|
||||
Right: RightWrite,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update to Read",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
TeamID: 1,
|
||||
Right: RightRead,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update with invalid right",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
TeamID: 1,
|
||||
Right: 500,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrInvalidRight,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
tl := &TeamNamespace{
|
||||
ID: tt.fields.ID,
|
||||
TeamID: tt.fields.TeamID,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := tl.Update(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("TeamNamespace.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("TeamNamespace.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "team_namespaces", map[string]interface{}{
|
||||
"team_id": tt.fields.TeamID,
|
||||
"namespace_id": tt.fields.NamespaceID,
|
||||
"right": tt.fields.Right,
|
||||
}, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,372 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNamespace_Create(t *testing.T) {
|
||||
|
||||
// Dummy namespace
|
||||
dummynamespace := Namespace{
|
||||
Title: "Test",
|
||||
Description: "Lorem Ipsum",
|
||||
}
|
||||
|
||||
user1 := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := dummynamespace.Create(s, user1)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
db.AssertExists(t, "namespaces", map[string]interface{}{
|
||||
"title": "Test",
|
||||
"description": "Lorem Ipsum",
|
||||
}, false)
|
||||
})
|
||||
t.Run("no title", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n2 := Namespace{}
|
||||
err := n2.Create(s, user1)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant user", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
nUser := &user.User{ID: 9482385}
|
||||
dnsp2 := dummynamespace
|
||||
err := dnsp2.Create(s, nUser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, user.IsErrUserDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestNamespace_ReadOne(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
n := &Namespace{ID: 1}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err := n.ReadOne(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, n.Title, "testnamespace")
|
||||
})
|
||||
t.Run("nonexistant", func(t *testing.T) {
|
||||
n := &Namespace{ID: 99999}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err := n.ReadOne(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
})
|
||||
t.Run("with subscription", func(t *testing.T) {
|
||||
n := &Namespace{ID: 8}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err := n.ReadOne(s, &user.User{ID: 6})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, n.Subscription)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNamespace_Update(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n := &Namespace{
|
||||
ID: 1,
|
||||
Title: "Lorem Ipsum",
|
||||
}
|
||||
err := n.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
db.AssertExists(t, "namespaces", map[string]interface{}{
|
||||
"id": 1,
|
||||
"title": "Lorem Ipsum",
|
||||
}, false)
|
||||
})
|
||||
t.Run("nonexisting", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n := &Namespace{
|
||||
ID: 99999,
|
||||
Title: "Lorem Ipsum",
|
||||
}
|
||||
err := n.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexisting owner", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n := &Namespace{
|
||||
ID: 1,
|
||||
Title: "Lorem Ipsum",
|
||||
Owner: &user.User{ID: 99999},
|
||||
}
|
||||
err := n.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, user.IsErrUserDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("no title", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n := &Namespace{
|
||||
ID: 1,
|
||||
}
|
||||
err := n.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestNamespace_Delete(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n := &Namespace{
|
||||
ID: 1,
|
||||
}
|
||||
err := n.Delete(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
db.AssertMissing(t, "namespaces", map[string]interface{}{
|
||||
"id": 1,
|
||||
})
|
||||
})
|
||||
t.Run("nonexisting", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n := &Namespace{
|
||||
ID: 9999,
|
||||
}
|
||||
err := n.Delete(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestNamespace_ReadAll(t *testing.T) {
|
||||
user1 := &user.User{ID: 1}
|
||||
user6 := &user.User{ID: 6}
|
||||
user7 := &user.User{ID: 7}
|
||||
user11 := &user.User{ID: 11}
|
||||
user12 := &user.User{ID: 12}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 11) // Total of 11 including shared, favorites and saved filters
|
||||
assert.Equal(t, int64(-3), namespaces[0].ID) // The first one should be the one with saved filters
|
||||
assert.Equal(t, int64(-2), namespaces[1].ID) // The second one should be the one with favorites
|
||||
assert.Equal(t, int64(-1), namespaces[2].ID) // The third one should be the one with the shared namespaces
|
||||
// Ensure every list and namespace are not archived
|
||||
for _, namespace := range namespaces {
|
||||
assert.False(t, namespace.IsArchived)
|
||||
for _, list := range namespace.Lists {
|
||||
assert.False(t, list.IsArchived)
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("no own shared lists", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user6, "", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Equal(t, int64(-1), namespaces[1].ID) // The third one should be the one with the shared namespaces
|
||||
|
||||
sharedListOccurences := make(map[int64]int64)
|
||||
for _, list := range namespaces[1].Lists {
|
||||
assert.NotEqual(t, user1.ID, list.OwnerID)
|
||||
sharedListOccurences[list.ID]++
|
||||
}
|
||||
|
||||
for listID, occ := range sharedListOccurences {
|
||||
assert.Equal(t, int64(1), occ, "shared list %d is present %d times, should be 1", listID, occ)
|
||||
}
|
||||
})
|
||||
t.Run("namespaces only", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{
|
||||
NamespacesOnly: true,
|
||||
}
|
||||
nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 8) // Total of 8 - excluding shared, favorites and saved filters (normally 11)
|
||||
// Ensure every namespace does not contain lists
|
||||
for _, namespace := range namespaces {
|
||||
assert.Nil(t, namespace.Lists)
|
||||
}
|
||||
})
|
||||
t.Run("ids only", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{
|
||||
NamespacesOnly: true,
|
||||
}
|
||||
nn, _, _, err := n.ReadAll(s, user7, "13,14", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 2)
|
||||
assert.Equal(t, int64(13), namespaces[0].ID)
|
||||
assert.Equal(t, int64(14), namespaces[1].ID)
|
||||
})
|
||||
t.Run("ids only but ids with other people's namespace", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{
|
||||
NamespacesOnly: true,
|
||||
}
|
||||
nn, _, _, err := n.ReadAll(s, user1, "1,w", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 1)
|
||||
assert.Equal(t, int64(1), namespaces[0].ID)
|
||||
})
|
||||
t.Run("archived", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{
|
||||
IsArchived: true,
|
||||
}
|
||||
nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 12) // Total of 12 including shared & favorites, one is archived
|
||||
assert.Equal(t, int64(-3), namespaces[0].ID) // The first one should be the one with shared filters
|
||||
assert.Equal(t, int64(-2), namespaces[1].ID) // The second one should be the one with favorites
|
||||
assert.Equal(t, int64(-1), namespaces[2].ID) // The third one should be the one with the shared namespaces
|
||||
})
|
||||
t.Run("no favorites", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user11, "", 1, -1)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NoError(t, err)
|
||||
// Assert the first namespace is not the favorites namespace
|
||||
assert.NotEqual(t, FavoritesPseudoNamespace.ID, namespaces[0].ID)
|
||||
})
|
||||
t.Run("no favorite tasks but namespace", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user12, "", 1, -1)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NoError(t, err)
|
||||
// Assert the first namespace is the favorites namespace and contains lists
|
||||
assert.Equal(t, FavoritesPseudoNamespace.ID, namespaces[0].ID)
|
||||
assert.NotEqual(t, 0, namespaces[0].Lists)
|
||||
})
|
||||
t.Run("no saved filters", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user11, "", 1, -1)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NoError(t, err)
|
||||
// Assert the first namespace is not the favorites namespace
|
||||
assert.NotEqual(t, SavedFiltersPseudoNamespace.ID, namespaces[0].ID)
|
||||
})
|
||||
t.Run("no results", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user1, "some search string which will never return results", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, nn)
|
||||
})
|
||||
t.Run("search", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user6, "NamespACE7", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 2)
|
||||
assert.Equal(t, int64(7), namespaces[1].ID)
|
||||
})
|
||||
}
|
|
@ -1,251 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
user2 "code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// NamespaceUser represents a namespace <-> user relation
|
||||
type NamespaceUser struct {
|
||||
// The unique, numeric id of this namespace <-> user relation.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
// The username.
|
||||
Username string `xorm:"-" json:"user_id" param:"user"`
|
||||
UserID int64 `xorm:"bigint not null INDEX" json:"-"`
|
||||
// The namespace id
|
||||
NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"`
|
||||
// The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
|
||||
Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
|
||||
// A timestamp when this relation was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this relation was last updated. You cannot change this value.
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName is the table name for NamespaceUser
|
||||
func (NamespaceUser) TableName() string {
|
||||
return "users_namespaces"
|
||||
}
|
||||
|
||||
// Create creates a new namespace <-> user relation
|
||||
// @Summary Add a user to a namespace
|
||||
// @Description Gives a user access to a namespace.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Param namespace body models.NamespaceUser true "The user you want to add to the namespace."
|
||||
// @Success 201 {object} models.NamespaceUser "The created user<->namespace relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid user namespace object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The user does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id}/users [put]
|
||||
func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// Reset the id
|
||||
nu.ID = 0
|
||||
|
||||
// Check if the right is valid
|
||||
if err := nu.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
n, err := GetNamespaceByID(s, nu.NamespaceID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
user, err := user2.GetUserByUsername(s, nu.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nu.UserID = user.ID
|
||||
|
||||
// Check if the user already has access or is owner of that namespace
|
||||
// We explicitly DO NOT check for teams here
|
||||
if n.OwnerID == nu.UserID {
|
||||
return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID}
|
||||
}
|
||||
|
||||
exist, err := s.
|
||||
Where("namespace_id = ? AND user_id = ?", nu.NamespaceID, nu.UserID).
|
||||
Get(&NamespaceUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID}
|
||||
}
|
||||
|
||||
// Insert user <-> namespace relation
|
||||
_, err = s.Insert(nu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return events.Dispatch(&NamespaceSharedWithUserEvent{
|
||||
Namespace: n,
|
||||
User: user,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete deletes a namespace <-> user relation
|
||||
// @Summary Delete a user from a namespace
|
||||
// @Description Delets a user from a namespace. The user won't have access to the namespace anymore.
|
||||
// @tags sharing
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param namespaceID path int true "Namespace ID"
|
||||
// @Param userID path int true "user ID"
|
||||
// @Success 200 {object} models.Message "The user was successfully deleted."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
// @Failure 404 {object} web.HTTPError "user or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/users/{userID} [delete]
|
||||
func (nu *NamespaceUser) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the user exists
|
||||
user, err := user2.GetUserByUsername(s, nu.Username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
nu.UserID = user.ID
|
||||
|
||||
// Check if the user has access to the namespace
|
||||
has, err := s.
|
||||
Where("user_id = ? AND namespace_id = ?", nu.UserID, nu.NamespaceID).
|
||||
Get(&NamespaceUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
return ErrUserDoesNotHaveAccessToNamespace{NamespaceID: nu.NamespaceID, UserID: nu.UserID}
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
Where("user_id = ? AND namespace_id = ?", nu.UserID, nu.NamespaceID).
|
||||
Delete(&NamespaceUser{})
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll gets all users who have access to a namespace
|
||||
// @Summary Get users on a namespace
|
||||
// @Description Returns a namespace with all users which have access on a given namespace.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @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 s query string false "Search users by its name."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.UserWithRight "The users with the right they have."
|
||||
// @Failure 403 {object} web.HTTPError "No right to see the namespace."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id}/users [get]
|
||||
func (nu *NamespaceUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
// Check if the user has access to the namespace
|
||||
l := Namespace{ID: nu.NamespaceID}
|
||||
canRead, _, err := l.CanRead(s, a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
if !canRead {
|
||||
return nil, 0, 0, ErrNeedToHaveNamespaceReadAccess{}
|
||||
}
|
||||
|
||||
// Get all users
|
||||
all := []*UserWithRight{}
|
||||
limit, start := getLimitFromPageIndex(page, perPage)
|
||||
query := s.
|
||||
Join("INNER", "users_namespaces", "user_id = users.id").
|
||||
Where("users_namespaces.namespace_id = ?", nu.NamespaceID).
|
||||
Where(db.ILIKE("users.username", search))
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
err = query.Find(&all)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
// Obfuscate all user emails
|
||||
for _, u := range all {
|
||||
u.Email = ""
|
||||
}
|
||||
|
||||
numberOfTotalItems, err = s.
|
||||
Join("INNER", "users_namespaces", "user_id = users.id").
|
||||
Where("users_namespaces.namespace_id = ?", nu.NamespaceID).
|
||||
Where("users.username LIKE ?", "%"+search+"%").
|
||||
Count(&UserWithRight{})
|
||||
|
||||
return all, len(all), numberOfTotalItems, err
|
||||
}
|
||||
|
||||
// Update updates a user <-> namespace relation
|
||||
// @Summary Update a user <-> namespace relation
|
||||
// @Description Update a user <-> namespace relation. Mostly used to update the right that user has.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param namespaceID path int true "Namespace ID"
|
||||
// @Param userID path int true "User ID"
|
||||
// @Param namespace body models.NamespaceUser true "The user you want to update."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} models.NamespaceUser "The updated user <-> namespace relation."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have admin-access to the namespace"
|
||||
// @Failure 404 {object} web.HTTPError "User or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/users/{userID} [post]
|
||||
func (nu *NamespaceUser) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := nu.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
user, err := user2.GetUserByUsername(s, nu.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nu.UserID = user.ID
|
||||
|
||||
_, err = s.
|
||||
Where("namespace_id = ? AND user_id = ?", nu.NamespaceID, nu.UserID).
|
||||
Cols("right").
|
||||
Update(nu)
|
||||
return
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can create a new user <-> namespace relation
|
||||
func (nu *NamespaceUser) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return nu.canDoNamespaceUser(s, a)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a user <-> namespace relation
|
||||
func (nu *NamespaceUser) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return nu.canDoNamespaceUser(s, a)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a user <-> namespace relation
|
||||
func (nu *NamespaceUser) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return nu.canDoNamespaceUser(s, a)
|
||||
}
|
||||
|
||||
func (nu *NamespaceUser) canDoNamespaceUser(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
n := &Namespace{ID: nu.NamespaceID}
|
||||
return n.IsAdmin(s, a)
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
)
|
||||
|
||||
func TestNamespaceUser_CanDoSomething(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
type args struct {
|
||||
a web.Auth
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want map[string]bool
|
||||
}{
|
||||
{
|
||||
name: "CanDoSomething Normally",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
},
|
||||
want: map[string]bool{"CanCreate": true, "CanDelete": true, "CanUpdate": true},
|
||||
},
|
||||
{
|
||||
name: "CanDoSomething for a nonexistant namespace",
|
||||
fields: fields{
|
||||
NamespaceID: 300,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
},
|
||||
want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
|
||||
},
|
||||
{
|
||||
name: "CanDoSomething where the user does not have the rights",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 4},
|
||||
},
|
||||
want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
nu := &NamespaceUser{
|
||||
ID: tt.fields.ID,
|
||||
UserID: tt.fields.UserID,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
if got, _ := nu.CanCreate(s, tt.args.a); got != tt.want["CanCreate"] {
|
||||
t.Errorf("NamespaceUser.CanCreate() = %v, want %v", got, tt.want["CanCreate"])
|
||||
}
|
||||
if got, _ := nu.CanDelete(s, tt.args.a); got != tt.want["CanDelete"] {
|
||||
t.Errorf("NamespaceUser.CanDelete() = %v, want %v", got, tt.want["CanDelete"])
|
||||
}
|
||||
if got, _ := nu.CanUpdate(s, tt.args.a); got != tt.want["CanUpdate"] {
|
||||
t.Errorf("NamespaceUser.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,436 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/d4l3k/messagediff.v1"
|
||||
)
|
||||
|
||||
func TestNamespaceUser_Create(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
Username string
|
||||
UserID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
type args struct {
|
||||
a web.Auth
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
errType func(err error) bool
|
||||
}{
|
||||
{
|
||||
name: "NamespaceUsers Create normally",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
UserID: 1,
|
||||
NamespaceID: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NamespaceUsers Create for duplicate",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
NamespaceID: 3,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrUserAlreadyHasNamespaceAccess,
|
||||
},
|
||||
{
|
||||
name: "NamespaceUsers Create with invalid right",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
NamespaceID: 2,
|
||||
Right: 500,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrInvalidRight,
|
||||
},
|
||||
{
|
||||
name: "NamespaceUsers Create with inexisting list",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
NamespaceID: 2000,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrNamespaceDoesNotExist,
|
||||
},
|
||||
{
|
||||
name: "NamespaceUsers Create with inexisting user",
|
||||
fields: fields{
|
||||
Username: "user500",
|
||||
NamespaceID: 2,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: user.IsErrUserDoesNotExist,
|
||||
},
|
||||
{
|
||||
name: "NamespaceUsers Create with the owner as shared user",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
NamespaceID: 1,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrUserAlreadyHasNamespaceAccess,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
un := &NamespaceUser{
|
||||
ID: tt.fields.ID,
|
||||
Username: tt.fields.Username,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := un.Create(s, tt.args.a)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NamespaceUser.Create() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("NamespaceUser.Create() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "users_namespaces", map[string]interface{}{
|
||||
"user_id": tt.fields.UserID,
|
||||
"namespace_id": tt.fields.NamespaceID,
|
||||
}, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceUser_ReadAll(t *testing.T) {
|
||||
user1 := &UserWithRight{
|
||||
User: user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
}
|
||||
user2 := &UserWithRight{
|
||||
User: user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
type args struct {
|
||||
search string
|
||||
a web.Auth
|
||||
page int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want interface{}
|
||||
wantErr bool
|
||||
errType func(err error) bool
|
||||
}{
|
||||
{
|
||||
name: "Test readall normal",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
},
|
||||
want: []*UserWithRight{
|
||||
user1,
|
||||
user2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test ReadAll by a user who does not have access to the list",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 4},
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrNeedToHaveNamespaceReadAccess,
|
||||
},
|
||||
{
|
||||
name: "Search",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
search: "usER2",
|
||||
},
|
||||
want: []*UserWithRight{
|
||||
user2,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
un := &NamespaceUser{
|
||||
ID: tt.fields.ID,
|
||||
UserID: tt.fields.UserID,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
got, _, _, err := un.ReadAll(s, tt.args.a, tt.args.search, tt.args.page, 50)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NamespaceUser.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("NamespaceUser.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
if diff, equal := messagediff.PrettyDiff(got, tt.want); !equal {
|
||||
t.Errorf("NamespaceUser.ReadAll() = %v, want %v, diff: %v", got, tt.want, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceUser_Update(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
Username string
|
||||
UserID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
errType func(err error) bool
|
||||
}{
|
||||
{
|
||||
name: "Test Update Normally",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
Username: "user1",
|
||||
UserID: 1,
|
||||
Right: RightAdmin,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update to write",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
Username: "user1",
|
||||
UserID: 1,
|
||||
Right: RightWrite,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update to Read",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
Username: "user1",
|
||||
UserID: 1,
|
||||
Right: RightRead,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update with invalid right",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
Username: "user1",
|
||||
Right: 500,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrInvalidRight,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
nu := &NamespaceUser{
|
||||
ID: tt.fields.ID,
|
||||
Username: tt.fields.Username,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := nu.Update(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NamespaceUser.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("NamespaceUser.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "users_namespaces", map[string]interface{}{
|
||||
"user_id": tt.fields.UserID,
|
||||
"namespace_id": tt.fields.NamespaceID,
|
||||
"right": tt.fields.Right,
|
||||
}, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceUser_Delete(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
Username string
|
||||
UserID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
errType func(err error) bool
|
||||
}{
|
||||
{
|
||||
name: "Try deleting some unexistant user",
|
||||
fields: fields{
|
||||
Username: "user1000",
|
||||
NamespaceID: 2,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: user.IsErrUserDoesNotExist,
|
||||
},
|
||||
{
|
||||
name: "Try deleting a user which does not has access but exists",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
NamespaceID: 4,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrUserDoesNotHaveAccessToNamespace,
|
||||
},
|
||||
{
|
||||
name: "Try deleting normally",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
UserID: 1,
|
||||
NamespaceID: 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
nu := &NamespaceUser{
|
||||
ID: tt.fields.ID,
|
||||
Username: tt.fields.Username,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := nu.Delete(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NamespaceUser.Delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("NamespaceUser.Delete() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertMissing(t, "users_namespaces", map[string]interface{}{
|
||||
"user_id": tt.fields.UserID,
|
||||
"namespace_id": tt.fields.NamespaceID,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -150,28 +150,28 @@ func (n *TaskDeletedNotification) Name() string {
|
|||
return "task.deleted"
|
||||
}
|
||||
|
||||
// ListCreatedNotification represents a ListCreatedNotification notification
|
||||
type ListCreatedNotification struct {
|
||||
Doer *user.User `json:"doer"`
|
||||
List *List `json:"list"`
|
||||
// ProjectCreatedNotification represents a ProjectCreatedNotification notification
|
||||
type ProjectCreatedNotification struct {
|
||||
Doer *user.User `json:"doer"`
|
||||
Project *Project `json:"project"`
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for ListCreatedNotification
|
||||
func (n *ListCreatedNotification) ToMail() *notifications.Mail {
|
||||
// ToMail returns the mail notification for ProjectCreatedNotification
|
||||
func (n *ProjectCreatedNotification) ToMail() *notifications.Mail {
|
||||
return notifications.NewMail().
|
||||
Subject(n.Doer.GetName()+` created the list "`+n.List.Title+`"`).
|
||||
Line(n.Doer.GetName()+` created the list "`+n.List.Title+`"`).
|
||||
Action("View List", config.ServiceFrontendurl.GetString()+"lists/")
|
||||
Subject(n.Doer.GetName()+` created the project "`+n.Project.Title+`"`).
|
||||
Line(n.Doer.GetName()+` created the project "`+n.Project.Title+`"`).
|
||||
Action("View Project", config.ServiceFrontendurl.GetString()+"projects/")
|
||||
}
|
||||
|
||||
// ToDB returns the ListCreatedNotification notification in a format which can be saved in the db
|
||||
func (n *ListCreatedNotification) ToDB() interface{} {
|
||||
// ToDB returns the ProjectCreatedNotification notification in a format which can be saved in the db
|
||||
func (n *ProjectCreatedNotification) ToDB() interface{} {
|
||||
return n
|
||||
}
|
||||
|
||||
// Name returns the name of the notification
|
||||
func (n *ListCreatedNotification) Name() string {
|
||||
return "list.created"
|
||||
func (n *ProjectCreatedNotification) Name() string {
|
||||
return "project.created"
|
||||
}
|
||||
|
||||
// TeamMemberAddedNotification represents a TeamMemberAddedNotification notification
|
||||
|
|
|
@ -0,0 +1,915 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// Project represents a project of tasks
|
||||
type Project struct {
|
||||
// The unique, numeric id of this project.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"project"`
|
||||
// The title of the project. You'll see this in the overview.
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
// The description of the project.
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
// The unique project short identifier. Used to build task identifiers.
|
||||
Identifier string `xorm:"varchar(10) null" json:"identifier" valid:"runelength(0|10)" minLength:"0" maxLength:"10"`
|
||||
// The hex color of this project
|
||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
|
||||
|
||||
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
|
||||
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
|
||||
ParentProject *Project `xorm:"-" json:"-"`
|
||||
|
||||
ChildProjects []*Project `xorm:"-" json:"child_projects"`
|
||||
|
||||
// The user who created this project.
|
||||
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
|
||||
|
||||
// Whether a project is archived.
|
||||
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
|
||||
|
||||
// The id of the file this project has set as background
|
||||
BackgroundFileID int64 `xorm:"null" json:"-"`
|
||||
// Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /projects/{projectID}/background
|
||||
BackgroundInformation interface{} `xorm:"-" json:"background_information"`
|
||||
// Contains a very small version of the project background to use as a blurry preview until the actual background is loaded. Check out https://blurha.sh/ to learn how it works.
|
||||
BackgroundBlurHash string `xorm:"varchar(50) null" json:"background_blur_hash"`
|
||||
|
||||
// True if a project is a favorite. Favorite projects show up in a separate parent project. This value depends on the user making the call to the api.
|
||||
IsFavorite bool `xorm:"-" json:"is_favorite"`
|
||||
|
||||
// The subscription status for the user reading this project. You can only read this property, use the subscription endpoints to modify it.
|
||||
// Will only returned when retreiving one project.
|
||||
Subscription *Subscription `xorm:"-" json:"subscription,omitempty"`
|
||||
|
||||
// The position this project has when querying all projects. See the tasks.position property on how to use this.
|
||||
Position float64 `xorm:"double null" json:"position"`
|
||||
|
||||
// A timestamp when this project was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this project was last updated. You cannot change this value.
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
type ProjectWithTasksAndBuckets struct {
|
||||
Project
|
||||
ChildProjects []*ProjectWithTasksAndBuckets `xorm:"-" json:"child_projects"`
|
||||
|
||||
// An array of tasks which belong to the project.
|
||||
Tasks []*TaskWithComments `xorm:"-" json:"tasks"`
|
||||
// Only used for migration.
|
||||
Buckets []*Bucket `xorm:"-" json:"buckets"`
|
||||
BackgroundFileID int64 `xorm:"null" json:"background_file_id"`
|
||||
}
|
||||
|
||||
// TableName returns a better name for the projects table
|
||||
func (p *Project) TableName() string {
|
||||
return "projects"
|
||||
}
|
||||
|
||||
// ProjectBackgroundType holds a project background type
|
||||
type ProjectBackgroundType struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
// ProjectBackgroundUpload represents the project upload background type
|
||||
const ProjectBackgroundUpload string = "upload"
|
||||
|
||||
// SharedProjectsPseudoProject is a pseudo project used to hold shared projects
|
||||
var SharedProjectsPseudoProject = &Project{
|
||||
ID: -1,
|
||||
Title: "Shared Projects",
|
||||
Description: "Projects of other users shared with you via teams or directly.",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// FavoriteProjectsPseudoProject is a pseudo parent project used to hold favorite projects and tasks
|
||||
var FavoriteProjectsPseudoProject = &Project{
|
||||
ID: -2,
|
||||
Title: "Favorites",
|
||||
Description: "Favorite projects and tasks.",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// SavedFiltersPseudoProject is a pseudo parent project used to hold saved filters
|
||||
var SavedFiltersPseudoProject = &Project{
|
||||
ID: -3,
|
||||
Title: "Filters",
|
||||
Description: "Saved filters.",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// FavoritesPseudoProject holds all tasks marked as favorites
|
||||
var FavoritesPseudoProject = Project{
|
||||
ID: -1,
|
||||
Title: "Favorites",
|
||||
Description: "This project has all tasks marked as favorites.",
|
||||
ParentProjectID: FavoriteProjectsPseudoProject.ID,
|
||||
IsFavorite: true,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// ReadAll gets all projects a user has access to
|
||||
// @Summary Get all projects a user has access to
|
||||
// @Description Returns all projects a user has access to.
|
||||
// @tags project
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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 s query string false "Search projects by title."
|
||||
// @Param is_archived query bool false "If true, also returns all archived projects."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.Project "The projects"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects [get]
|
||||
func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
|
||||
// Check if we're dealing with a share auth
|
||||
shareAuth, ok := a.(*LinkSharing)
|
||||
if ok {
|
||||
project, err := GetProjectSimpleByID(s, shareAuth.ProjectID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
projects := []*Project{project}
|
||||
err = addProjectDetails(s, projects, a)
|
||||
return projects, 0, 0, err
|
||||
}
|
||||
|
||||
doer, err := user.GetFromAuth(a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
prs, resultCount, totalItems, err := getRawProjectsForUser(
|
||||
s,
|
||||
&projectOptions{
|
||||
search: search,
|
||||
user: doer,
|
||||
page: page,
|
||||
perPage: perPage,
|
||||
getArchived: p.IsArchived,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
// FIXME: I wonder if we could get rid of this extra loop?
|
||||
allProjects := make(map[int64]*Project, len(prs))
|
||||
for _, p := range prs {
|
||||
allProjects[p.ID] = p
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Saved Filters
|
||||
|
||||
savedFiltersProject, err := getSavedFilterProjects(s, doer)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if savedFiltersProject != nil {
|
||||
allProjects[savedFiltersProject.ID] = savedFiltersProject
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Add project details (favorite state, among other things)
|
||||
err = addProjectDetails(s, prs, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
// Putting it all together
|
||||
|
||||
for _, p := range allProjects {
|
||||
if p.ParentProjectID != 0 {
|
||||
if allProjects[p.ParentProjectID].ChildProjects == nil {
|
||||
allProjects[p.ParentProjectID].ChildProjects = []*Project{}
|
||||
}
|
||||
allProjects[p.ParentProjectID].ChildProjects = append(allProjects[p.ParentProjectID].ChildProjects, p)
|
||||
continue
|
||||
}
|
||||
|
||||
// The projects variable will contain all projects which have no parents
|
||||
// And because we're using the same pointers for everything, those will contain child projects
|
||||
}
|
||||
|
||||
return prs, resultCount, totalItems, err
|
||||
}
|
||||
|
||||
// ReadOne gets one project by its ID
|
||||
// @Summary Gets one project
|
||||
// @Description Returns a project by its ID.
|
||||
// @tags project
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Project ID"
|
||||
// @Success 200 {object} models.Project "The project"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{id} [get]
|
||||
func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
if p.ID == FavoritesPseudoProject.ID {
|
||||
// Already "built" the project in CanRead
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for saved filters
|
||||
if getSavedFilterIDFromProjectID(p.ID) > 0 {
|
||||
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(p.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Title = sf.Title
|
||||
p.Description = sf.Description
|
||||
p.Created = sf.Created
|
||||
p.Updated = sf.Updated
|
||||
p.OwnerID = sf.OwnerID
|
||||
}
|
||||
|
||||
// Get project owner
|
||||
p.Owner, err = user.GetUserByID(s, p.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the project is archived and set it to archived if it is not already archived individually.
|
||||
if !p.IsArchived {
|
||||
err = p.CheckIsArchived(s)
|
||||
if err != nil {
|
||||
p.IsArchived = true
|
||||
}
|
||||
}
|
||||
|
||||
// Get any background information if there is one set
|
||||
if p.BackgroundFileID != 0 {
|
||||
// Unsplash image
|
||||
p.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, p.BackgroundFileID)
|
||||
if err != nil && !files.IsErrFileIsNotUnsplashFile(err) {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
|
||||
p.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
|
||||
}
|
||||
}
|
||||
|
||||
p.IsFavorite, err = isFavorite(s, p.ID, a, FavoriteKindProject)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.Subscription, err = GetSubscription(s, SubscriptionEntityProject, p.ID, a)
|
||||
return
|
||||
}
|
||||
|
||||
// GetProjectSimpleByID gets a project with only the basic items, aka no tasks or user objects. Returns an error if the project does not exist.
|
||||
func GetProjectSimpleByID(s *xorm.Session, projectID int64) (project *Project, err error) {
|
||||
|
||||
project = &Project{}
|
||||
|
||||
if projectID < 1 {
|
||||
return nil, ErrProjectDoesNotExist{ID: projectID}
|
||||
}
|
||||
|
||||
exists, err := s.
|
||||
Where("id = ?", projectID).
|
||||
OrderBy("position").
|
||||
Get(project)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return nil, ErrProjectDoesNotExist{ID: projectID}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetProjectSimplByTaskID gets a project by a task id
|
||||
func GetProjectSimplByTaskID(s *xorm.Session, taskID int64) (l *Project, err error) {
|
||||
// We need to re-init our project object, because otherwise xorm creates a "where for every item in that project object,
|
||||
// leading to not finding anything if the id is good, but for example the title is different.
|
||||
var project Project
|
||||
exists, err := s.
|
||||
Select("projects.*").
|
||||
Table(Project{}).
|
||||
Join("INNER", "tasks", "projects.id = tasks.project_id").
|
||||
Where("tasks.id = ?", taskID).
|
||||
Get(&project)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return &Project{}, ErrProjectDoesNotExist{ID: l.ID}
|
||||
}
|
||||
|
||||
return &project, nil
|
||||
}
|
||||
|
||||
// GetProjectsByIDs returns a map of projects from a slice with project ids
|
||||
func GetProjectsByIDs(s *xorm.Session, projectIDs []int64) (projects map[int64]*Project, err error) {
|
||||
projects = make(map[int64]*Project, len(projectIDs))
|
||||
|
||||
if len(projectIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
err = s.In("id", projectIDs).Find(&projects)
|
||||
return
|
||||
}
|
||||
|
||||
type projectOptions struct {
|
||||
search string
|
||||
user *user.User
|
||||
page int
|
||||
perPage int
|
||||
getArchived bool
|
||||
}
|
||||
|
||||
func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search string, getArchived bool) *builder.Builder {
|
||||
dialect := config.DatabaseType.GetString()
|
||||
if dialect == "sqlite" {
|
||||
dialect = builder.SQLITE
|
||||
}
|
||||
|
||||
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
|
||||
var getArchivedCond builder.Cond = builder.Eq{"1": 1}
|
||||
if !getArchived {
|
||||
getArchivedCond = builder.And(
|
||||
builder.Eq{"l.is_archived": false},
|
||||
)
|
||||
}
|
||||
|
||||
var filterCond builder.Cond
|
||||
ids := []int64{}
|
||||
if search != "" {
|
||||
vals := strings.Split(search, ",")
|
||||
for _, val := range vals {
|
||||
v, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
log.Debugf("Project search string part '%s' is not a number: %s", val, err)
|
||||
continue
|
||||
}
|
||||
ids = append(ids, v)
|
||||
}
|
||||
}
|
||||
|
||||
filterCond = db.ILIKE("l.title", search)
|
||||
if len(ids) > 0 {
|
||||
filterCond = builder.In("l.id", ids)
|
||||
}
|
||||
|
||||
var parentCondition builder.Cond
|
||||
parentCondition = builder.Or(
|
||||
builder.IsNull{"l.parent_project_id"},
|
||||
builder.Eq{"l.parent_project_id": 0},
|
||||
)
|
||||
projectCol := "id"
|
||||
if len(parentProjectIDs) > 0 {
|
||||
parentCondition = builder.In("l.parent_project_id", parentProjectIDs)
|
||||
projectCol = "parent_project_id"
|
||||
}
|
||||
|
||||
return builder.Dialect(dialect).
|
||||
Select("l.*").
|
||||
From("projects", "l").
|
||||
Join("LEFT", "team_projects tl", "tl.project_id = l."+projectCol).
|
||||
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
|
||||
Join("LEFT", "users_projects ul", "ul.project_id = l."+projectCol).
|
||||
Where(builder.And(
|
||||
builder.Or(
|
||||
builder.Eq{"tm2.user_id": userID},
|
||||
builder.Eq{"ul.user_id": userID},
|
||||
builder.Eq{"l.owner_id": userID},
|
||||
),
|
||||
filterCond,
|
||||
getArchivedCond,
|
||||
parentCondition,
|
||||
)).
|
||||
OrderBy("position").
|
||||
GroupBy("l.id")
|
||||
}
|
||||
|
||||
func getAllProjectsForUser(s *xorm.Session, userID int64, parentProjectIDs []int64, opts *projectOptions, projects *[]*Project, oldTotalCount int64) (resultCount int, totalCount int64, err error) {
|
||||
|
||||
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
||||
query := getUserProjectsStatement(parentProjectIDs, userID, opts.search, opts.getArchived)
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
|
||||
currentProjects := []*Project{}
|
||||
err = s.SQL(query).Find(¤tProjects)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if len(currentProjects) == 0 {
|
||||
return 0, oldTotalCount, err
|
||||
}
|
||||
|
||||
query = getUserProjectsStatement(parentProjectIDs, userID, opts.search, opts.getArchived)
|
||||
totalCount, err = s.
|
||||
SQL(query.Select("count(*)")).
|
||||
Count(&Project{})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
newParentIDs := []int64{}
|
||||
for _, project := range currentProjects {
|
||||
newParentIDs = append(newParentIDs, project.ID)
|
||||
}
|
||||
|
||||
*projects = append(*projects, currentProjects...)
|
||||
|
||||
return getAllProjectsForUser(s, userID, newParentIDs, opts, projects, oldTotalCount+totalCount)
|
||||
}
|
||||
|
||||
// Gets the projects with their children without any tasks
|
||||
func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*Project, resultCount int, totalItems int64, err error) {
|
||||
fullUser, err := user.GetUserByID(s, opts.user.ID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
allProjects := []*Project{}
|
||||
resultCount, totalItems, err = getAllProjectsForUser(s, fullUser.ID, nil, opts, &allProjects, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(allProjects) == 0 {
|
||||
return nil, 0, totalItems, nil
|
||||
}
|
||||
|
||||
return allProjects, len(allProjects), totalItems, err
|
||||
}
|
||||
|
||||
func getSavedFilterProjects(s *xorm.Session, doer *user.User) (savedFiltersProject *Project, err error) {
|
||||
savedFilters, err := getSavedFiltersForUser(s, doer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(savedFilters) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
savedFiltersPseudoParentProject := SavedFiltersPseudoProject
|
||||
savedFiltersPseudoParentProject.OwnerID = doer.ID
|
||||
savedFiltersProject = &Project{}
|
||||
*savedFiltersProject = *savedFiltersPseudoParentProject
|
||||
savedFiltersProject.ChildProjects = make([]*Project, 0, len(savedFilters))
|
||||
|
||||
for _, filter := range savedFilters {
|
||||
filterProject := filter.toProject()
|
||||
filterProject.ParentProjectID = savedFiltersProject.ID
|
||||
filterProject.Owner = doer
|
||||
savedFiltersProject.ChildProjects = append(savedFiltersProject.ChildProjects, filterProject)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetAllParentProjects returns all parents of a given project
|
||||
func (p *Project) GetAllParentProjects(s *xorm.Session) (err error) {
|
||||
if p.ParentProjectID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
parent, err := GetProjectSimpleByID(s, p.ParentProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.ParentProject = parent
|
||||
|
||||
return parent.GetAllParentProjects(s)
|
||||
}
|
||||
|
||||
// addProjectDetails adds owner user objects and project tasks to all projects in the slice
|
||||
func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err error) {
|
||||
if len(projects) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var ownerIDs []int64
|
||||
var projectIDs []int64
|
||||
var fileIDs []int64
|
||||
for _, p := range projects {
|
||||
ownerIDs = append(ownerIDs, p.OwnerID)
|
||||
projectIDs = append(projectIDs, p.ID)
|
||||
fileIDs = append(fileIDs, p.BackgroundFileID)
|
||||
}
|
||||
|
||||
owners, err := user.GetUsersByIDs(s, ownerIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
favs, err := getFavorites(s, projectIDs, a, FavoriteKindProject)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subscriptions, err := GetSubscriptions(s, SubscriptionEntityProject, projectIDs, a)
|
||||
if err != nil {
|
||||
log.Errorf("An error occurred while getting project subscriptions for a project: %s", err.Error())
|
||||
subscriptions = make(map[int64][]*Subscription)
|
||||
}
|
||||
|
||||
for _, p := range projects {
|
||||
if o, exists := owners[p.OwnerID]; exists {
|
||||
p.Owner = o
|
||||
}
|
||||
if p.BackgroundFileID != 0 {
|
||||
p.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
|
||||
}
|
||||
|
||||
// Don't override the favorite state if it was already set from before (favorite saved filters do this)
|
||||
if p.IsFavorite {
|
||||
continue
|
||||
}
|
||||
p.IsFavorite = favs[p.ID]
|
||||
|
||||
if subscription, exists := subscriptions[p.ID]; exists && len(subscription) > 0 {
|
||||
p.Subscription = subscription[0]
|
||||
}
|
||||
}
|
||||
|
||||
if len(fileIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Unsplash background file info
|
||||
us := []*UnsplashPhoto{}
|
||||
err = s.In("file_id", fileIDs).Find(&us)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
unsplashPhotos := make(map[int64]*UnsplashPhoto, len(us))
|
||||
for _, u := range us {
|
||||
unsplashPhotos[u.FileID] = u
|
||||
}
|
||||
|
||||
// Build it all into the projects slice
|
||||
for _, l := range projects {
|
||||
// Only override the file info if we have info for unsplash backgrounds
|
||||
if _, exists := unsplashPhotos[l.BackgroundFileID]; exists {
|
||||
l.BackgroundInformation = unsplashPhotos[l.BackgroundFileID]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CheckIsArchived returns an ErrProjectIsArchived if the project or any of its parent projects is archived.
|
||||
func (p *Project) CheckIsArchived(s *xorm.Session) (err error) {
|
||||
if p.ParentProjectID > 0 {
|
||||
p := &Project{ID: p.ParentProjectID}
|
||||
return p.CheckIsArchived(s)
|
||||
}
|
||||
|
||||
if p.ID == 0 { // don't check new projects
|
||||
return nil
|
||||
}
|
||||
|
||||
project, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if project.IsArchived {
|
||||
return ErrProjectIsArchived{ProjectID: p.ID}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkProjectBeforeUpdateOrDelete(s *xorm.Session, project *Project) error {
|
||||
if project.ParentProjectID < 0 {
|
||||
return &ErrProjectCannotBelongToAPseudoParentProject{ProjectID: project.ID, ParentProjectID: project.ParentProjectID}
|
||||
}
|
||||
|
||||
// Check if the parent project exists
|
||||
if project.ParentProjectID > 0 {
|
||||
_, err := GetProjectSimpleByID(s, project.ParentProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the identifier is unique and not empty
|
||||
if project.Identifier != "" {
|
||||
exists, err := s.
|
||||
Where("identifier = ?", project.Identifier).
|
||||
And("id != ?", project.ID).
|
||||
Exist(&Project{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return ErrProjectIdentifierIsNotUnique{Identifier: project.Identifier}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateProject(s *xorm.Session, project *Project, auth web.Auth) (err error) {
|
||||
err = project.CheckIsArchived(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doer, err := user.GetFromAuth(auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project.OwnerID = doer.ID
|
||||
project.Owner = doer
|
||||
|
||||
err = checkProjectBeforeUpdateOrDelete(s, project)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.Insert(project)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
project.Position = calculateDefaultPosition(project.ID, project.Position)
|
||||
_, err = s.Where("id = ?", project.ID).Update(project)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if project.IsFavorite {
|
||||
if err := addToFavorites(s, project.ID, auth, FavoriteKindProject); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new first bucket for this project
|
||||
b := &Bucket{
|
||||
ProjectID: project.ID,
|
||||
Title: "Backlog",
|
||||
}
|
||||
err = b.Create(s, auth)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return events.Dispatch(&ProjectCreatedEvent{
|
||||
Project: project,
|
||||
Doer: doer,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateNewProjectForUser creates a new inbox project for a user. To prevent import cycles, we can't do that
|
||||
// directly in the user.Create function.
|
||||
func CreateNewProjectForUser(s *xorm.Session, user *user.User) (err error) {
|
||||
p := &Project{
|
||||
Title: "Inbox",
|
||||
}
|
||||
return p.Create(s, user)
|
||||
}
|
||||
|
||||
func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProjectBackground bool) (err error) {
|
||||
err = checkProjectBeforeUpdateOrDelete(s, project)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// We need to specify the cols we want to update here to be able to un-archive projects
|
||||
colsToUpdate := []string{
|
||||
"title",
|
||||
"is_archived",
|
||||
"identifier",
|
||||
"hex_color",
|
||||
"parent_project_id",
|
||||
"position",
|
||||
}
|
||||
if project.Description != "" {
|
||||
colsToUpdate = append(colsToUpdate, "description")
|
||||
}
|
||||
|
||||
if updateProjectBackground {
|
||||
colsToUpdate = append(colsToUpdate, "background_file_id", "background_blur_hash")
|
||||
}
|
||||
|
||||
wasFavorite, err := isFavorite(s, project.ID, auth, FavoriteKindProject)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if project.IsFavorite && !wasFavorite {
|
||||
if err := addToFavorites(s, project.ID, auth, FavoriteKindProject); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !project.IsFavorite && wasFavorite {
|
||||
if err := removeFromFavorite(s, project.ID, auth, FavoriteKindProject); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
ID(project.ID).
|
||||
Cols(colsToUpdate...).
|
||||
Update(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = events.Dispatch(&ProjectUpdatedEvent{
|
||||
Project: project,
|
||||
Doer: auth,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l, err := GetProjectSimpleByID(s, project.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*project = *l
|
||||
err = project.ReadOne(s, auth)
|
||||
return
|
||||
}
|
||||
|
||||
// Update implements the update method of CRUDable
|
||||
// @Summary Updates a project
|
||||
// @Description Updates a project. This does not include adding a task (see below).
|
||||
// @tags project
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Project ID"
|
||||
// @Param project body models.Project true "The project with updated values you want to update."
|
||||
// @Success 200 {object} models.Project "The updated project."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid project object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{id} [post]
|
||||
func (p *Project) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
fid := getSavedFilterIDFromProjectID(p.ID)
|
||||
if fid > 0 {
|
||||
f, err := getSavedFilterSimpleByID(s, fid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.Title = p.Title
|
||||
f.Description = p.Description
|
||||
f.IsFavorite = p.IsFavorite
|
||||
err = f.Update(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*p = *f.toProject()
|
||||
return nil
|
||||
}
|
||||
|
||||
return UpdateProject(s, p, a, false)
|
||||
}
|
||||
|
||||
func updateProjectLastUpdated(s *xorm.Session, project *Project) error {
|
||||
_, err := s.ID(project.ID).Cols("updated").Update(project)
|
||||
return err
|
||||
}
|
||||
|
||||
func updateProjectByTaskID(s *xorm.Session, taskID int64) (err error) {
|
||||
// need to get the task to update the project last updated timestamp
|
||||
task, err := GetTaskByIDSimple(s, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateProjectLastUpdated(s, &Project{ID: task.ProjectID})
|
||||
}
|
||||
|
||||
// Create implements the create method of CRUDable
|
||||
// @Summary Creates a new project
|
||||
// @Description Creates a new project. If a parent project is provided the user needs to have write access to that project.
|
||||
// @tags project
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param project body models.Project true "The project you want to create."
|
||||
// @Success 201 {object} models.Project "The created project."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid project object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects [put]
|
||||
func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
err = CreateProject(s, p, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return p.ReadOne(s, a)
|
||||
}
|
||||
|
||||
// Delete implements the delete method of CRUDable
|
||||
// @Summary Deletes a project
|
||||
// @Description Delets a project
|
||||
// @tags project
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Project ID"
|
||||
// @Success 200 {object} models.Message "The project was successfully deleted."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid project object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{id} [delete]
|
||||
func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Delete the project
|
||||
_, err = s.ID(p.ID).Delete(&Project{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete all tasks on that project
|
||||
// Using the loop to make sure all related entities to all tasks are properly deleted as well.
|
||||
tasks, _, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskOptions{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
err = task.Delete(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return events.Dispatch(&ProjectDeletedEvent{
|
||||
Project: p,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// SetProjectBackground sets a background file as project background in the db
|
||||
func SetProjectBackground(s *xorm.Session, projectID int64, background *files.File, blurHash string) (err error) {
|
||||
l := &Project{
|
||||
ID: projectID,
|
||||
BackgroundFileID: background.ID,
|
||||
BackgroundBlurHash: blurHash,
|
||||
}
|
||||
_, err = s.
|
||||
Where("id = ?", l.ID).
|
||||
Cols("background_file_id", "background_blur_hash").
|
||||
Update(l)
|
||||
return
|
||||
}
|
|
@ -25,7 +25,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestList_CreateOrUpdate(t *testing.T) {
|
||||
func TestProject_CreateOrUpdate(t *testing.T) {
|
||||
usr := &user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
|
@ -36,45 +36,43 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
|||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
project := Project{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: 1,
|
||||
}
|
||||
err := list.Create(s, usr)
|
||||
err := project.Create(s, usr)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{
|
||||
"id": list.ID,
|
||||
"title": list.Title,
|
||||
"description": list.Description,
|
||||
"namespace_id": list.NamespaceID,
|
||||
db.AssertExists(t, "projects", map[string]interface{}{
|
||||
"id": project.ID,
|
||||
"title": project.Title,
|
||||
"description": project.Description,
|
||||
"parent_project_id": 0,
|
||||
}, false)
|
||||
})
|
||||
t.Run("nonexistant namespace", func(t *testing.T) {
|
||||
t.Run("nonexistant parent project", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: 999999,
|
||||
project := Project{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum",
|
||||
ParentProjectID: 999999,
|
||||
}
|
||||
err := list.Create(s, usr)
|
||||
err := project.Create(s, usr)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
assert.True(t, IsErrProjectDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant owner", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
usr := &user.User{ID: 9482385}
|
||||
list := List{
|
||||
project := Project{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: 1,
|
||||
}
|
||||
err := list.Create(s, usr)
|
||||
err := project.Create(s, usr)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, user.IsErrUserDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
|
@ -82,34 +80,31 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
|||
t.Run("existing identifier", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
project := Project{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum",
|
||||
Identifier: "test1",
|
||||
NamespaceID: 1,
|
||||
}
|
||||
err := list.Create(s, usr)
|
||||
err := project.Create(s, usr)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListIdentifierIsNotUnique(err))
|
||||
assert.True(t, IsErrProjectIdentifierIsNotUnique(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("non ascii characters", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
project := Project{
|
||||
Title: "приффки фсем",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: 1,
|
||||
}
|
||||
err := list.Create(s, usr)
|
||||
err := project.Create(s, usr)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{
|
||||
"id": list.ID,
|
||||
"title": list.Title,
|
||||
"description": list.Description,
|
||||
"namespace_id": list.NamespaceID,
|
||||
db.AssertExists(t, "projects", map[string]interface{}{
|
||||
"id": project.ID,
|
||||
"title": project.Title,
|
||||
"description": project.Description,
|
||||
}, false)
|
||||
})
|
||||
})
|
||||
|
@ -118,53 +113,49 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
|||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
project := Project{
|
||||
ID: 1,
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: 1,
|
||||
}
|
||||
list.Description = "Lorem Ipsum dolor sit amet."
|
||||
err := list.Update(s, usr)
|
||||
project.Description = "Lorem Ipsum dolor sit amet."
|
||||
err := project.Update(s, usr)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{
|
||||
"id": list.ID,
|
||||
"title": list.Title,
|
||||
"description": list.Description,
|
||||
"namespace_id": list.NamespaceID,
|
||||
db.AssertExists(t, "projects", map[string]interface{}{
|
||||
"id": project.ID,
|
||||
"title": project.Title,
|
||||
"description": project.Description,
|
||||
}, false)
|
||||
})
|
||||
t.Run("nonexistant", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
ID: 99999999,
|
||||
Title: "test",
|
||||
NamespaceID: 1,
|
||||
project := Project{
|
||||
ID: 99999999,
|
||||
Title: "test",
|
||||
}
|
||||
err := list.Update(s, usr)
|
||||
err := project.Update(s, usr)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
assert.True(t, IsErrProjectDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
|
||||
})
|
||||
t.Run("existing identifier", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
project := Project{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum",
|
||||
Identifier: "test1",
|
||||
NamespaceID: 1,
|
||||
}
|
||||
err := list.Create(s, usr)
|
||||
err := project.Create(s, usr)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListIdentifierIsNotUnique(err))
|
||||
assert.True(t, IsErrProjectIdentifierIsNotUnique(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("change namespace", func(t *testing.T) {
|
||||
t.Run("change parent project", func(t *testing.T) {
|
||||
t.Run("own", func(t *testing.T) {
|
||||
usr := &user.User{
|
||||
ID: 6,
|
||||
|
@ -174,55 +165,40 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
|||
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
ID: 6,
|
||||
Title: "Test6",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: 7, // from 6
|
||||
project := Project{
|
||||
ID: 6,
|
||||
Title: "Test6",
|
||||
Description: "Lorem Ipsum",
|
||||
ParentProjectID: 7, // from 6
|
||||
}
|
||||
can, err := list.CanUpdate(s, usr)
|
||||
can, err := project.CanUpdate(s, usr)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
err = list.Update(s, usr)
|
||||
err = project.Update(s, usr)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{
|
||||
"id": list.ID,
|
||||
"title": list.Title,
|
||||
"description": list.Description,
|
||||
"namespace_id": list.NamespaceID,
|
||||
db.AssertExists(t, "projects", map[string]interface{}{
|
||||
"id": project.ID,
|
||||
"title": project.Title,
|
||||
"description": project.Description,
|
||||
"parent_project_id": project.ParentProjectID,
|
||||
}, false)
|
||||
})
|
||||
// FIXME: The check for whether the namespace is archived is missing in namespace.CanWrite
|
||||
// t.Run("archived own", func(t *testing.T) {
|
||||
// db.LoadAndAssertFixtures(t)
|
||||
// s := db.NewSession()
|
||||
// list := List{
|
||||
// ID: 1,
|
||||
// Title: "Test1",
|
||||
// Description: "Lorem Ipsum",
|
||||
// NamespaceID: 16, // from 1
|
||||
// }
|
||||
// can, err := list.CanUpdate(s, usr)
|
||||
// assert.NoError(t, err)
|
||||
// assert.False(t, can) // namespace is archived and thus not writeable
|
||||
// _ = s.Close()
|
||||
// })
|
||||
t.Run("others", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
ID: 1,
|
||||
Title: "Test1",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: 2, // from 1
|
||||
project := Project{
|
||||
ID: 1,
|
||||
Title: "Test1",
|
||||
Description: "Lorem Ipsum",
|
||||
ParentProjectID: 2, // from 1
|
||||
}
|
||||
can, _ := list.CanUpdate(s, usr)
|
||||
assert.False(t, can) // namespace is not writeable by us
|
||||
can, _ := project.CanUpdate(s, usr)
|
||||
assert.False(t, can) // project is not writeable by us
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("pseudo namespace", func(t *testing.T) {
|
||||
t.Run("pseudo project", func(t *testing.T) {
|
||||
usr := &user.User{
|
||||
ID: 6,
|
||||
Username: "user6",
|
||||
|
@ -231,67 +207,70 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
|||
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
ID: 6,
|
||||
Title: "Test6",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: -1,
|
||||
project := Project{
|
||||
ID: 6,
|
||||
Title: "Test6",
|
||||
Description: "Lorem Ipsum",
|
||||
ParentProjectID: -1,
|
||||
}
|
||||
err := list.Update(s, usr)
|
||||
err := project.Update(s, usr)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListCannotBelongToAPseudoNamespace(err))
|
||||
assert.True(t, IsErrProjectCannotBelongToAPseudoParentProject(err))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestList_Delete(t *testing.T) {
|
||||
func TestProject_Delete(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
project := Project{
|
||||
ID: 1,
|
||||
}
|
||||
err := list.Delete(s, &user.User{ID: 1})
|
||||
err := project.Delete(s, &user.User{ID: 1})
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertMissing(t, "lists", map[string]interface{}{
|
||||
db.AssertMissing(t, "projects", map[string]interface{}{
|
||||
"id": 1,
|
||||
})
|
||||
}
|
||||
|
||||
func TestList_ReadAll(t *testing.T) {
|
||||
t.Run("all in namespace", func(t *testing.T) {
|
||||
func TestProject_ReadAll(t *testing.T) {
|
||||
t.Run("all", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
// Get all lists for our namespace
|
||||
lists, err := GetListsByNamespaceID(s, 1, &user.User{})
|
||||
projects := []*Project{}
|
||||
_, _, err := getAllProjectsForUser(s, 1, nil, &projectOptions{}, &projects, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(lists), 2)
|
||||
assert.Equal(t, 23, len(projects))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("all lists for user", func(t *testing.T) {
|
||||
t.Run("only child projects for one project", func(t *testing.T) {
|
||||
// TODO
|
||||
})
|
||||
t.Run("all projects for user", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
u := &user.User{ID: 1}
|
||||
list := List{}
|
||||
lists3, _, _, err := list.ReadAll(s, u, "", 1, 50)
|
||||
project := Project{}
|
||||
projects3, _, _, err := project.ReadAll(s, u, "", 1, 50)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(lists3).Kind(), reflect.Slice)
|
||||
ls := lists3.([]*List)
|
||||
assert.Equal(t, 16, len(ls))
|
||||
assert.Equal(t, int64(3), ls[0].ID) // List 3 has a position of 1 and should be sorted first
|
||||
assert.Equal(t, reflect.TypeOf(projects3).Kind(), reflect.Slice)
|
||||
ls := projects3.([]*Project)
|
||||
assert.Equal(t, 23, len(ls))
|
||||
assert.Equal(t, int64(3), ls[0].ID) // Project 3 has a position of 1 and should be sorted first
|
||||
assert.Equal(t, int64(1), ls[1].ID)
|
||||
assert.Equal(t, int64(4), ls[2].ID)
|
||||
assert.Equal(t, int64(6), ls[2].ID)
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("lists for nonexistant user", func(t *testing.T) {
|
||||
t.Run("projects for nonexistant user", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
usr := &user.User{ID: 999999}
|
||||
list := List{}
|
||||
_, _, _, err := list.ReadAll(s, usr, "", 1, 50)
|
||||
project := Project{}
|
||||
_, _, _, err := project.ReadAll(s, usr, "", 1, 50)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, user.IsErrUserDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
|
@ -300,25 +279,25 @@ func TestList_ReadAll(t *testing.T) {
|
|||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
u := &user.User{ID: 1}
|
||||
list := List{}
|
||||
lists3, _, _, err := list.ReadAll(s, u, "TEST10", 1, 50)
|
||||
project := Project{}
|
||||
projects3, _, _, err := project.ReadAll(s, u, "TEST10", 1, 50)
|
||||
|
||||
assert.NoError(t, err)
|
||||
ls := lists3.([]*List)
|
||||
ls := projects3.([]*Project)
|
||||
assert.Equal(t, 1, len(ls))
|
||||
assert.Equal(t, int64(10), ls[0].ID)
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestList_ReadOne(t *testing.T) {
|
||||
func TestProject_ReadOne(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
u := &user.User{ID: 1}
|
||||
l := &List{ID: 1}
|
||||
l := &Project{ID: 1}
|
||||
can, _, err := l.CanRead(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
|
@ -332,7 +311,7 @@ func TestList_ReadOne(t *testing.T) {
|
|||
defer s.Close()
|
||||
|
||||
u := &user.User{ID: 6}
|
||||
l := &List{ID: 12}
|
||||
l := &Project{ID: 12}
|
||||
can, _, err := l.CanRead(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, can)
|
|
@ -24,89 +24,93 @@ import (
|
|||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// ListDuplicate holds everything needed to duplicate a list
|
||||
type ListDuplicate struct {
|
||||
// The list id of the list to duplicate
|
||||
ListID int64 `json:"-" param:"listid"`
|
||||
// The target namespace ID
|
||||
NamespaceID int64 `json:"namespace_id,omitempty"`
|
||||
// ProjectDuplicate holds everything needed to duplicate a project
|
||||
type ProjectDuplicate struct {
|
||||
// The project id of the project to duplicate
|
||||
ProjectID int64 `json:"-" param:"projectid"`
|
||||
// The target parent project
|
||||
ParentProjectID int64 `json:"parent_project_id,omitempty"`
|
||||
|
||||
// The copied list
|
||||
List *List `json:",omitempty"`
|
||||
// The copied project
|
||||
Project *Project `json:",omitempty"`
|
||||
|
||||
web.Rights `json:"-"`
|
||||
web.CRUDable `json:"-"`
|
||||
}
|
||||
|
||||
// CanCreate checks if a user has the right to duplicate a list
|
||||
func (ld *ListDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bool, err error) {
|
||||
// List Exists + user has read access to list
|
||||
ld.List = &List{ID: ld.ListID}
|
||||
canRead, _, err := ld.List.CanRead(s, a)
|
||||
// CanCreate checks if a user has the right to duplicate a project
|
||||
func (ld *ProjectDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bool, err error) {
|
||||
// Project Exists + user has read access to project
|
||||
ld.Project = &Project{ID: ld.ProjectID}
|
||||
canRead, _, err := ld.Project.CanRead(s, a)
|
||||
if err != nil || !canRead {
|
||||
return canRead, err
|
||||
}
|
||||
|
||||
// Namespace exists + user has write access to is (-> can create new lists)
|
||||
ld.List.NamespaceID = ld.NamespaceID
|
||||
return ld.List.CanCreate(s, a)
|
||||
if ld.ParentProjectID == 0 { // no parent project
|
||||
return canRead, err
|
||||
}
|
||||
|
||||
// Parent project exists + user has write access to is (-> can create new projects)
|
||||
parent := &Project{ID: ld.ParentProjectID}
|
||||
return parent.CanCreate(s, a)
|
||||
}
|
||||
|
||||
// Create duplicates a list
|
||||
// @Summary Duplicate an existing list
|
||||
// @Description Copies the list, tasks, files, kanban data, assignees, comments, attachments, lables, relations, backgrounds, user/team rights and link shares from one list to a new namespace. The user needs read access in the list and write access in the namespace of the new list.
|
||||
// @tags list
|
||||
// Create duplicates a project
|
||||
// @Summary Duplicate an existing project
|
||||
// @Description Copies the project, tasks, files, kanban data, assignees, comments, attachments, lables, relations, backgrounds, user/team rights and link shares from one project to a new one. The user needs read access in the project and write access in the parent of the new project.
|
||||
// @tags project
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param listID path int true "The list ID to duplicate"
|
||||
// @Param list body models.ListDuplicate true "The target namespace which should hold the copied list."
|
||||
// @Success 201 {object} models.ListDuplicate "The created list."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid list duplicate object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list or namespace"
|
||||
// @Param projectID path int true "The project ID to duplicate"
|
||||
// @Param project body models.ProjectDuplicate true "The target parent project which should hold the copied project."
|
||||
// @Success 201 {object} models.ProjectDuplicate "The created project."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid project duplicate object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project or its parent."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/duplicate [put]
|
||||
// @Router /projects/{projectID}/duplicate [put]
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
||||
func (ld *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
||||
|
||||
log.Debugf("Duplicating list %d", ld.ListID)
|
||||
log.Debugf("Duplicating project %d", ld.ProjectID)
|
||||
|
||||
ld.List.ID = 0
|
||||
ld.List.Identifier = "" // Reset the identifier to trigger regenerating a new one
|
||||
ld.Project.ID = 0
|
||||
ld.Project.Identifier = "" // Reset the identifier to trigger regenerating a new one
|
||||
// Set the owner to the current user
|
||||
ld.List.OwnerID = doer.GetID()
|
||||
if err := CreateList(s, ld.List, doer); err != nil {
|
||||
// If there is no available unique list identifier, just reset it.
|
||||
if IsErrListIdentifierIsNotUnique(err) {
|
||||
ld.List.Identifier = ""
|
||||
ld.Project.OwnerID = doer.GetID()
|
||||
if err := CreateProject(s, ld.Project, doer); err != nil {
|
||||
// If there is no available unique project identifier, just reset it.
|
||||
if IsErrProjectIdentifierIsNotUnique(err) {
|
||||
ld.Project.Identifier = ""
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated list %d into new list %d", ld.ListID, ld.List.ID)
|
||||
log.Debugf("Duplicated project %d into new project %d", ld.ProjectID, ld.Project.ID)
|
||||
|
||||
// Duplicate kanban buckets
|
||||
// Old bucket ID as key, new id as value
|
||||
// Used to map the newly created tasks to their new buckets
|
||||
bucketMap := make(map[int64]int64)
|
||||
buckets := []*Bucket{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&buckets)
|
||||
err = s.Where("project_id = ?", ld.ProjectID).Find(&buckets)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, b := range buckets {
|
||||
oldID := b.ID
|
||||
b.ID = 0
|
||||
b.ListID = ld.List.ID
|
||||
b.ProjectID = ld.Project.ID
|
||||
if err := b.Create(s, doer); err != nil {
|
||||
return err
|
||||
}
|
||||
bucketMap[oldID] = b.ID
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated all buckets from list %d into %d", ld.ListID, ld.List.ID)
|
||||
log.Debugf("Duplicated all buckets from project %d into %d", ld.ProjectID, ld.Project.ID)
|
||||
|
||||
err = duplicateTasks(s, doer, ld, bucketMap)
|
||||
if err != nil {
|
||||
|
@ -114,11 +118,11 @@ func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||
}
|
||||
|
||||
// Background files + unsplash info
|
||||
if ld.List.BackgroundFileID != 0 {
|
||||
if ld.Project.BackgroundFileID != 0 {
|
||||
|
||||
log.Debugf("Duplicating background %d from list %d into %d", ld.List.BackgroundFileID, ld.ListID, ld.List.ID)
|
||||
log.Debugf("Duplicating background %d from project %d into %d", ld.Project.BackgroundFileID, ld.ProjectID, ld.Project.ID)
|
||||
|
||||
f := &files.File{ID: ld.List.BackgroundFileID}
|
||||
f := &files.File{ID: ld.Project.BackgroundFileID}
|
||||
if err := f.LoadFileMetaByID(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -133,7 +137,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||
}
|
||||
|
||||
// Get unsplash info if applicable
|
||||
up, err := GetUnsplashPhotoByFileID(s, ld.List.BackgroundFileID)
|
||||
up, err := GetUnsplashPhotoByFileID(s, ld.Project.BackgroundFileID)
|
||||
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
|
||||
return err
|
||||
}
|
||||
|
@ -145,38 +149,38 @@ func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if err := SetListBackground(s, ld.List.ID, file, ld.List.BackgroundBlurHash); err != nil {
|
||||
if err := SetProjectBackground(s, ld.Project.ID, file, ld.Project.BackgroundBlurHash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated list background from list %d into %d", ld.ListID, ld.List.ID)
|
||||
log.Debugf("Duplicated project background from project %d into %d", ld.ProjectID, ld.Project.ID)
|
||||
}
|
||||
|
||||
// Rights / Shares
|
||||
// To keep it simple(r) we will only copy rights which are directly used with the list, no namespace changes.
|
||||
users := []*ListUser{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&users)
|
||||
// To keep it simple(r) we will only copy rights which are directly used with the project, not the parent
|
||||
users := []*ProjectUser{}
|
||||
err = s.Where("project_id = ?", ld.ProjectID).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, u := range users {
|
||||
u.ID = 0
|
||||
u.ListID = ld.List.ID
|
||||
u.ProjectID = ld.Project.ID
|
||||
if _, err := s.Insert(u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated user shares from list %d into %d", ld.ListID, ld.List.ID)
|
||||
log.Debugf("Duplicated user shares from project %d into %d", ld.ProjectID, ld.Project.ID)
|
||||
|
||||
teams := []*TeamList{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&teams)
|
||||
teams := []*TeamProject{}
|
||||
err = s.Where("project_id = ?", ld.ProjectID).Find(&teams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, t := range teams {
|
||||
t.ID = 0
|
||||
t.ListID = ld.List.ID
|
||||
t.ProjectID = ld.Project.ID
|
||||
if _, err := s.Insert(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -184,27 +188,27 @@ func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||
|
||||
// Generate new link shares if any are available
|
||||
linkShares := []*LinkSharing{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&linkShares)
|
||||
err = s.Where("project_id = ?", ld.ProjectID).Find(&linkShares)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, share := range linkShares {
|
||||
share.ID = 0
|
||||
share.ListID = ld.List.ID
|
||||
share.ProjectID = ld.Project.ID
|
||||
share.Hash = utils.MakeRandomString(40)
|
||||
if _, err := s.Insert(share); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated all link shares from list %d into %d", ld.ListID, ld.List.ID)
|
||||
log.Debugf("Duplicated all link shares from project %d into %d", ld.ProjectID, ld.Project.ID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap map[int64]int64) (err error) {
|
||||
func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucketMap map[int64]int64) (err error) {
|
||||
// Get all tasks + all task details
|
||||
tasks, _, _, err := getTasksForLists(s, []*List{{ID: ld.ListID}}, doer, &taskOptions{})
|
||||
tasks, _, _, err := getTasksForProjects(s, []*Project{{ID: ld.ProjectID}}, doer, &taskOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -221,7 +225,7 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap
|
|||
for _, t := range tasks {
|
||||
oldID := t.ID
|
||||
t.ID = 0
|
||||
t.ListID = ld.List.ID
|
||||
t.ProjectID = ld.Project.ID
|
||||
t.BucketID = bucketMap[t.BucketID]
|
||||
t.UID = ""
|
||||
err := createTask(s, t, doer, false)
|
||||
|
@ -232,11 +236,11 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap
|
|||
oldTaskIDs = append(oldTaskIDs, oldID)
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated all tasks from list %d into %d", ld.ListID, ld.List.ID)
|
||||
log.Debugf("Duplicated all tasks from project %d into %d", ld.ProjectID, ld.Project.ID)
|
||||
|
||||
// Save all attachments
|
||||
// We also duplicate all underlying files since they could be modified in one list which would result in
|
||||
// file changes in the other list which is not something we want.
|
||||
// We also duplicate all underlying files since they could be modified in one project which would result in
|
||||
// file changes in the other project which is not something we want.
|
||||
attachments, err := getTaskAttachmentsByTaskIDs(s, oldTaskIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -254,7 +258,7 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap
|
|||
attachment.File = &files.File{ID: attachment.FileID}
|
||||
if err := attachment.File.LoadFileMetaByID(); err != nil {
|
||||
if files.IsErrFileDoesNotExist(err) {
|
||||
log.Debugf("Not duplicating attachment %d (file %d) because it does not exist from list %d into %d", oldAttachmentID, attachment.FileID, ld.ListID, ld.List.ID)
|
||||
log.Debugf("Not duplicating attachment %d (file %d) because it does not exist from project %d into %d", oldAttachmentID, attachment.FileID, ld.ProjectID, ld.Project.ID)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
|
@ -272,10 +276,10 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap
|
|||
_ = attachment.File.File.Close()
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated attachment %d into %d from list %d into %d", oldAttachmentID, attachment.ID, ld.ListID, ld.List.ID)
|
||||
log.Debugf("Duplicated attachment %d into %d from project %d into %d", oldAttachmentID, attachment.ID, ld.ProjectID, ld.Project.ID)
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated all attachments from list %d into %d", ld.ListID, ld.List.ID)
|
||||
log.Debugf("Duplicated all attachments from project %d into %d", ld.ProjectID, ld.Project.ID)
|
||||
|
||||
// Copy label tasks (not the labels)
|
||||
labelTasks := []*LabelTask{}
|
||||
|
@ -292,7 +296,7 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap
|
|||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated all labels from list %d into %d", ld.ListID, ld.List.ID)
|
||||
log.Debugf("Duplicated all labels from project %d into %d", ld.ProjectID, ld.Project.ID)
|
||||
|
||||
// Assignees
|
||||
// Only copy those assignees who have access to the task
|
||||
|
@ -303,18 +307,18 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap
|
|||
}
|
||||
for _, a := range assignees {
|
||||
t := &Task{
|
||||
ID: taskMap[a.TaskID],
|
||||
ListID: ld.List.ID,
|
||||
ID: taskMap[a.TaskID],
|
||||
ProjectID: ld.Project.ID,
|
||||
}
|
||||
if err := t.addNewAssigneeByID(s, a.UserID, ld.List, doer); err != nil {
|
||||
if IsErrUserDoesNotHaveAccessToList(err) {
|
||||
if err := t.addNewAssigneeByID(s, a.UserID, ld.Project, doer); err != nil {
|
||||
if IsErrUserDoesNotHaveAccessToProject(err) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated all assignees from list %d into %d", ld.ListID, ld.List.ID)
|
||||
log.Debugf("Duplicated all assignees from project %d into %d", ld.ProjectID, ld.Project.ID)
|
||||
|
||||
// Comments
|
||||
comments := []*TaskComment{}
|
||||
|
@ -330,10 +334,10 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap
|
|||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated all comments from list %d into %d", ld.ListID, ld.List.ID)
|
||||
log.Debugf("Duplicated all comments from project %d into %d", ld.ProjectID, ld.Project.ID)
|
||||
|
||||
// Relations in that list
|
||||
// Low-Effort: Only copy those relations which are between tasks in the same list
|
||||
// Relations in that project
|
||||
// Low-Effort: Only copy those relations which are between tasks in the same project
|
||||
// because we can do that without a lot of hassle
|
||||
relations := []*TaskRelation{}
|
||||
err = s.In("task_id", oldTaskIDs).Find(&relations)
|
||||
|
@ -353,7 +357,7 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap
|
|||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated all task relations from list %d into %d", ld.ListID, ld.List.ID)
|
||||
log.Debugf("Duplicated all task relations from project %d into %d", ld.ProjectID, ld.Project.ID)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -25,7 +25,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestListDuplicate(t *testing.T) {
|
||||
func TestProjectDuplicate(t *testing.T) {
|
||||
|
||||
db.LoadAndAssertFixtures(t)
|
||||
files.InitTestFileFixtures(t)
|
||||
|
@ -36,9 +36,8 @@ func TestListDuplicate(t *testing.T) {
|
|||
ID: 1,
|
||||
}
|
||||
|
||||
l := &ListDuplicate{
|
||||
ListID: 1,
|
||||
NamespaceID: 1,
|
||||
l := &ProjectDuplicate{
|
||||
ProjectID: 1,
|
||||
}
|
||||
can, err := l.CanCreate(s, u)
|
||||
assert.NoError(t, err)
|
|
@ -0,0 +1,276 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CanWrite return whether the user can write on that project or not
|
||||
func (p *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
|
||||
// The favorite project can't be edited
|
||||
if p.ID == FavoritesPseudoProject.ID {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get the project and check the right
|
||||
originalProject, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
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 := originalProject.CheckIsArchived(s)
|
||||
|
||||
var canWrite bool
|
||||
|
||||
// Check if we're dealing with a share auth
|
||||
shareAuth, ok := a.(*LinkSharing)
|
||||
if ok {
|
||||
return originalProject.ID == shareAuth.ProjectID &&
|
||||
(shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), errIsArchived
|
||||
}
|
||||
|
||||
// Check if the user is either owner or can write to the project
|
||||
if originalProject.isOwner(&user.User{ID: a.GetID()}) {
|
||||
canWrite = true
|
||||
}
|
||||
|
||||
if canWrite {
|
||||
return canWrite, errIsArchived
|
||||
}
|
||||
|
||||
canWrite, _, err = originalProject.checkRight(s, a, RightWrite, RightAdmin)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return canWrite, errIsArchived
|
||||
}
|
||||
|
||||
// CanRead checks if a user has read access to a project
|
||||
func (p *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
|
||||
// The favorite project needs a special treatment
|
||||
if p.ID == FavoritesPseudoProject.ID {
|
||||
owner, err := user.GetFromAuth(a)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
*p = FavoritesPseudoProject
|
||||
p.Owner = owner
|
||||
return true, int(RightRead), nil
|
||||
}
|
||||
|
||||
// Saved Filter Projects need a special case
|
||||
if getSavedFilterIDFromProjectID(p.ID) > 0 {
|
||||
sf := &SavedFilter{ID: getSavedFilterIDFromProjectID(p.ID)}
|
||||
return sf.CanRead(s, a)
|
||||
}
|
||||
|
||||
// Check if the user is either owner or can read
|
||||
var err error
|
||||
originalProject, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
*p = *originalProject
|
||||
|
||||
// Check if we're dealing with a share auth
|
||||
shareAuth, ok := a.(*LinkSharing)
|
||||
if ok {
|
||||
return p.ID == shareAuth.ProjectID &&
|
||||
(shareAuth.Right == RightRead || shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), int(shareAuth.Right), nil
|
||||
}
|
||||
|
||||
if p.isOwner(&user.User{ID: a.GetID()}) {
|
||||
return true, int(RightAdmin), nil
|
||||
}
|
||||
return p.checkRight(s, a, RightRead, RightWrite, RightAdmin)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a project
|
||||
func (p *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) {
|
||||
// The favorite project can't be edited
|
||||
if p.ID == FavoritesPseudoProject.ID {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get the project
|
||||
ol, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check if we're moving the project to a different parent project.
|
||||
// If that is the case, we need to verify permissions to do so.
|
||||
if p.ParentProjectID != 0 && p.ParentProjectID != ol.ParentProjectID {
|
||||
newProject := &Project{ID: p.ParentProjectID}
|
||||
can, err := newProject.CanWrite(s, a)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !can {
|
||||
return false, ErrGenericForbidden{}
|
||||
}
|
||||
}
|
||||
|
||||
fid := getSavedFilterIDFromProjectID(p.ID)
|
||||
if fid > 0 {
|
||||
sf, err := getSavedFilterSimpleByID(s, fid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return sf.CanUpdate(s, a)
|
||||
}
|
||||
|
||||
canUpdate, err = p.CanWrite(s, a)
|
||||
// If the project is archived and the user tries to un-archive it, let the request through
|
||||
archivedErr := ErrProjectIsArchived{}
|
||||
is := errors.As(err, &archivedErr)
|
||||
if is && !p.IsArchived && archivedErr.ProjectID == p.ID {
|
||||
err = nil
|
||||
}
|
||||
return canUpdate, err
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a project
|
||||
func (p *Project) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return p.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
// CanCreate checks if the user can create a project
|
||||
func (p *Project) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
if p.ParentProjectID != 0 {
|
||||
parent := &Project{ID: p.ParentProjectID}
|
||||
return parent.CanWrite(s, a)
|
||||
}
|
||||
// Check if we're dealing with a share auth
|
||||
_, is := a.(*LinkSharing)
|
||||
if is {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// IsAdmin returns whether the user has admin rights on the project or not
|
||||
func (p *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
// The favorite project can't be edited
|
||||
if p.ID == FavoritesPseudoProject.ID {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
originalProject, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check if we're dealing with a share auth
|
||||
shareAuth, ok := a.(*LinkSharing)
|
||||
if ok {
|
||||
return originalProject.ID == shareAuth.ProjectID && shareAuth.Right == RightAdmin, nil
|
||||
}
|
||||
|
||||
// Check all the things
|
||||
// Check if the user is either owner or can write to the project
|
||||
// Owners are always admins
|
||||
if originalProject.isOwner(&user.User{ID: a.GetID()}) {
|
||||
return true, nil
|
||||
}
|
||||
is, _, err := originalProject.checkRight(s, a, RightAdmin)
|
||||
return is, err
|
||||
}
|
||||
|
||||
// Little helper function to check if a user is project owner
|
||||
func (p *Project) isOwner(u *user.User) bool {
|
||||
return p.OwnerID == u.ID
|
||||
}
|
||||
|
||||
// Checks n different rights for any given user
|
||||
func (p *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
|
||||
|
||||
var conds []builder.Cond
|
||||
for _, r := range rights {
|
||||
// User conditions
|
||||
// If the project was shared directly with the user and the user has the right
|
||||
conds = append(conds, builder.And(
|
||||
builder.Eq{"ul.user_id": a.GetID()},
|
||||
builder.Eq{"ul.right": r},
|
||||
))
|
||||
|
||||
// Team rights
|
||||
// If the project was shared directly with the team and the team has the right
|
||||
conds = append(conds, builder.And(
|
||||
builder.Eq{"tm2.user_id": a.GetID()},
|
||||
builder.Eq{"tl.right": r},
|
||||
))
|
||||
}
|
||||
|
||||
type allProjectRights struct {
|
||||
UserProject *ProjectUser `xorm:"extends"`
|
||||
TeamProject *TeamProject `xorm:"extends"`
|
||||
}
|
||||
|
||||
r := &allProjectRights{}
|
||||
var maxRight = 0
|
||||
exists, err := s.
|
||||
Select("p.*, ul.right, tl.right").
|
||||
Table("projects").
|
||||
Alias("p").
|
||||
// User stuff
|
||||
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = p.id").
|
||||
// Team stuff
|
||||
Join("LEFT", []string{"team_projects", "tl"}, "p.id = tl.project_id").
|
||||
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
|
||||
// The actual condition
|
||||
Where(builder.And(
|
||||
builder.Or(
|
||||
conds...,
|
||||
),
|
||||
builder.Eq{"p.id": p.ID},
|
||||
)).
|
||||
Get(r)
|
||||
|
||||
// If there's noting shared for this project, and it has a parent, go up the tree
|
||||
if !exists && p.ParentProjectID > 0 {
|
||||
parent, err := GetProjectSimpleByID(s, p.ParentProjectID)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
return parent.checkRight(s, a, rights...)
|
||||
}
|
||||
|
||||
// Figure out the max right and return it
|
||||
if int(r.UserProject.Right) > maxRight {
|
||||
maxRight = int(r.UserProject.Right)
|
||||
}
|
||||
if int(r.TeamProject.Right) > maxRight {
|
||||
maxRight = int(r.TeamProject.Right)
|
||||
}
|
||||
|
||||
return exists, maxRight, err
|
||||
}
|
|
@ -27,14 +27,14 @@ import (
|
|||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// TeamList defines the relation between a team and a list
|
||||
type TeamList struct {
|
||||
// The unique, numeric id of this list <-> team relation.
|
||||
// TeamProject defines the relation between a team and a project
|
||||
type TeamProject struct {
|
||||
// The unique, numeric id of this project <-> team relation.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
|
||||
// The team id.
|
||||
TeamID int64 `xorm:"bigint not null INDEX" json:"team_id" param:"team"`
|
||||
// The list id.
|
||||
ListID int64 `xorm:"bigint not null INDEX" json:"-" param:"list"`
|
||||
// The project id.
|
||||
ProjectID int64 `xorm:"bigint not null INDEX" json:"-" param:"project"`
|
||||
// The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
|
||||
Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
|
||||
|
@ -48,8 +48,8 @@ type TeamList struct {
|
|||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (TeamList) TableName() string {
|
||||
return "team_lists"
|
||||
func (TeamProject) TableName() string {
|
||||
return "team_projects"
|
||||
}
|
||||
|
||||
// TeamWithRight represents a team, combined with rights.
|
||||
|
@ -58,22 +58,22 @@ type TeamWithRight struct {
|
|||
Right Right `json:"right"`
|
||||
}
|
||||
|
||||
// Create creates a new team <-> list relation
|
||||
// @Summary Add a team to a list
|
||||
// @Description Gives a team access to a list.
|
||||
// Create creates a new team <-> project relation
|
||||
// @Summary Add a team to a project
|
||||
// @Description Gives a team access to a project.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List ID"
|
||||
// @Param list body models.TeamList true "The team you want to add to the list."
|
||||
// @Success 201 {object} models.TeamList "The created team<->list relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid team list object provided."
|
||||
// @Param id path int true "Project ID"
|
||||
// @Param project body models.TeamProject true "The team you want to add to the project."
|
||||
// @Success 201 {object} models.TeamProject "The created team<->project relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid team project object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The team does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id}/teams [put]
|
||||
func (tl *TeamList) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Router /projects/{id}/teams [put]
|
||||
func (tl *TeamProject) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the rights are valid
|
||||
if err = tl.Right.isValid(); err != nil {
|
||||
|
@ -86,21 +86,21 @@ func (tl *TeamList) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// Check if the list exists
|
||||
l, err := GetListSimpleByID(s, tl.ListID)
|
||||
// Check if the project exists
|
||||
l, err := GetProjectSimpleByID(s, tl.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the team is already on the list
|
||||
// Check if the team is already on the project
|
||||
exists, err := s.Where("team_id = ?", tl.TeamID).
|
||||
And("list_id = ?", tl.ListID).
|
||||
Get(&TeamList{})
|
||||
And("project_id = ?", tl.ProjectID).
|
||||
Get(&TeamProject{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
return ErrTeamAlreadyHasAccess{tl.TeamID, tl.ListID}
|
||||
return ErrTeamAlreadyHasAccess{tl.TeamID, tl.ProjectID}
|
||||
}
|
||||
|
||||
// Insert the new team
|
||||
|
@ -109,33 +109,33 @@ func (tl *TeamList) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
err = events.Dispatch(&ListSharedWithTeamEvent{
|
||||
List: l,
|
||||
Team: team,
|
||||
Doer: a,
|
||||
err = events.Dispatch(&ProjectSharedWithTeamEvent{
|
||||
Project: l,
|
||||
Team: team,
|
||||
Doer: a,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, l)
|
||||
err = updateProjectLastUpdated(s, l)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete deletes a team <-> list relation based on the list & team id
|
||||
// @Summary Delete a team from a list
|
||||
// @Description Delets a team from a list. The team won't have access to the list anymore.
|
||||
// Delete deletes a team <-> project relation based on the project & team id
|
||||
// @Summary Delete a team from a project
|
||||
// @Description Delets a team from a project. The team won't have access to the project anymore.
|
||||
// @tags sharing
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param listID path int true "List ID"
|
||||
// @Param projectID path int true "Project ID"
|
||||
// @Param teamID path int true "Team ID"
|
||||
// @Success 200 {object} models.Message "The team was successfully deleted."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 404 {object} web.HTTPError "Team or list does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 404 {object} web.HTTPError "Team or project does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/teams/{teamID} [delete]
|
||||
func (tl *TeamList) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Router /projects/{projectID}/teams/{teamID} [delete]
|
||||
func (tl *TeamProject) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(s, tl.TeamID)
|
||||
|
@ -143,53 +143,53 @@ func (tl *TeamList) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// Check if the team has access to the list
|
||||
// Check if the team has access to the project
|
||||
has, err := s.
|
||||
Where("team_id = ? AND list_id = ?", tl.TeamID, tl.ListID).
|
||||
Get(&TeamList{})
|
||||
Where("team_id = ? AND project_id = ?", tl.TeamID, tl.ProjectID).
|
||||
Get(&TeamProject{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
return ErrTeamDoesNotHaveAccessToList{TeamID: tl.TeamID, ListID: tl.ListID}
|
||||
return ErrTeamDoesNotHaveAccessToProject{TeamID: tl.TeamID, ProjectID: tl.ProjectID}
|
||||
}
|
||||
|
||||
// Delete the relation
|
||||
_, err = s.Where("team_id = ?", tl.TeamID).
|
||||
And("list_id = ?", tl.ListID).
|
||||
Delete(TeamList{})
|
||||
And("project_id = ?", tl.ProjectID).
|
||||
Delete(TeamProject{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, &List{ID: tl.ListID})
|
||||
err = updateProjectLastUpdated(s, &Project{ID: tl.ProjectID})
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll implements the method to read all teams of a list
|
||||
// @Summary Get teams on a list
|
||||
// @Description Returns a list with all teams which have access on a given list.
|
||||
// ReadAll implements the method to read all teams of a project
|
||||
// @Summary Get teams on a project
|
||||
// @Description Returns a project with all teams which have access on a given project.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "List ID"
|
||||
// @Param id path int true "Project ID"
|
||||
// @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 s query string false "Search teams by its name."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.TeamWithRight "The teams with their right."
|
||||
// @Failure 403 {object} web.HTTPError "No right to see the list."
|
||||
// @Failure 403 {object} web.HTTPError "No right to see the project."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id}/teams [get]
|
||||
func (tl *TeamList) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
|
||||
// Check if the user can read the namespace
|
||||
l := &List{ID: tl.ListID}
|
||||
// @Router /projects/{id}/teams [get]
|
||||
func (tl *TeamProject) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
|
||||
// Check if the user can read the project
|
||||
l := &Project{ID: tl.ProjectID}
|
||||
canRead, _, err := l.CanRead(s, a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
if !canRead {
|
||||
return nil, 0, 0, ErrNeedToHaveListReadAccess{ListID: tl.ListID, UserID: a.GetID()}
|
||||
return nil, 0, 0, ErrNeedToHaveProjectReadAccess{ProjectID: tl.ProjectID, UserID: a.GetID()}
|
||||
}
|
||||
|
||||
limit, start := getLimitFromPageIndex(page, perPage)
|
||||
|
@ -198,8 +198,8 @@ func (tl *TeamList) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
all := []*TeamWithRight{}
|
||||
query := s.
|
||||
Table("teams").
|
||||
Join("INNER", "team_lists", "team_id = teams.id").
|
||||
Where("team_lists.list_id = ?", tl.ListID).
|
||||
Join("INNER", "team_projects", "team_id = teams.id").
|
||||
Where("team_projects.project_id = ?", tl.ProjectID).
|
||||
Where(db.ILIKE("teams.name", search))
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
|
@ -221,8 +221,8 @@ func (tl *TeamList) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
|
||||
totalItems, err = s.
|
||||
Table("teams").
|
||||
Join("INNER", "team_lists", "team_id = teams.id").
|
||||
Where("team_lists.list_id = ?", tl.ListID).
|
||||
Join("INNER", "team_projects", "team_id = teams.id").
|
||||
Where("team_projects.project_id = ?", tl.ProjectID).
|
||||
Where("teams.name LIKE ?", "%"+search+"%").
|
||||
Count(&TeamWithRight{})
|
||||
if err != nil {
|
||||
|
@ -232,22 +232,22 @@ func (tl *TeamList) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
return all, len(all), totalItems, err
|
||||
}
|
||||
|
||||
// Update updates a team <-> list relation
|
||||
// @Summary Update a team <-> list relation
|
||||
// @Description Update a team <-> list relation. Mostly used to update the right that team has.
|
||||
// Update updates a team <-> project relation
|
||||
// @Summary Update a team <-> project relation
|
||||
// @Description Update a team <-> project relation. Mostly used to update the right that team has.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param listID path int true "List ID"
|
||||
// @Param projectID path int true "Project ID"
|
||||
// @Param teamID path int true "Team ID"
|
||||
// @Param list body models.TeamList true "The team you want to update."
|
||||
// @Param project body models.TeamProject true "The team you want to update."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} models.TeamList "The updated team <-> list relation."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have admin-access to the list"
|
||||
// @Failure 404 {object} web.HTTPError "Team or list does not exist."
|
||||
// @Success 200 {object} models.TeamProject "The updated team <-> project relation."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have admin-access to the project"
|
||||
// @Failure 404 {object} web.HTTPError "Team or project does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/teams/{teamID} [post]
|
||||
func (tl *TeamList) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Router /projects/{projectID}/teams/{teamID} [post]
|
||||
func (tl *TeamProject) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := tl.Right.isValid(); err != nil {
|
||||
|
@ -255,13 +255,13 @@ func (tl *TeamList) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
|
||||
_, err = s.
|
||||
Where("list_id = ? AND team_id = ?", tl.ListID, tl.TeamID).
|
||||
Where("project_id = ? AND team_id = ?", tl.ProjectID, tl.TeamID).
|
||||
Cols("right").
|
||||
Update(tl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, &List{ID: tl.ListID})
|
||||
err = updateProjectLastUpdated(s, &Project{ID: tl.ProjectID})
|
||||
return
|
||||
}
|
|
@ -21,27 +21,27 @@ import (
|
|||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can create a team <-> list relation
|
||||
func (tl *TeamList) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return tl.canDoTeamList(s, a)
|
||||
// CanCreate checks if the user can create a team <-> project relation
|
||||
func (tl *TeamProject) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return tl.canDoTeamProject(s, a)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a team <-> list relation
|
||||
func (tl *TeamList) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return tl.canDoTeamList(s, a)
|
||||
// CanDelete checks if the user can delete a team <-> project relation
|
||||
func (tl *TeamProject) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return tl.canDoTeamProject(s, a)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a team <-> list relation
|
||||
func (tl *TeamList) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return tl.canDoTeamList(s, a)
|
||||
// CanUpdate checks if the user can update a team <-> project relation
|
||||
func (tl *TeamProject) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return tl.canDoTeamProject(s, a)
|
||||
}
|
||||
|
||||
func (tl *TeamList) canDoTeamList(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
func (tl *TeamProject) canDoTeamProject(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
// Link shares aren't allowed to do anything
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
l := List{ID: tl.ListID}
|
||||
l := Project{ID: tl.ProjectID}
|
||||
return l.IsAdmin(s, a)
|
||||
}
|
|
@ -28,13 +28,13 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTeamList_ReadAll(t *testing.T) {
|
||||
func TestTeamProject_ReadAll(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
tl := TeamList{
|
||||
TeamID: 1,
|
||||
ListID: 3,
|
||||
tl := TeamProject{
|
||||
TeamID: 1,
|
||||
ProjectID: 3,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
@ -45,45 +45,33 @@ func TestTeamList_ReadAll(t *testing.T) {
|
|||
assert.Equal(t, ts.Len(), 1)
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant list", func(t *testing.T) {
|
||||
tl := TeamList{
|
||||
ListID: 99999,
|
||||
t.Run("nonexistant project", func(t *testing.T) {
|
||||
tl := TeamProject{
|
||||
ProjectID: 99999,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
_, _, _, err := tl.ReadAll(s, u, "", 1, 50)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("namespace owner", func(t *testing.T) {
|
||||
tl := TeamList{
|
||||
TeamID: 1,
|
||||
ListID: 2,
|
||||
Right: RightAdmin,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
_, _, _, err := tl.ReadAll(s, u, "", 1, 50)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, IsErrProjectDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("no access", func(t *testing.T) {
|
||||
tl := TeamList{
|
||||
TeamID: 1,
|
||||
ListID: 5,
|
||||
Right: RightAdmin,
|
||||
tl := TeamProject{
|
||||
TeamID: 1,
|
||||
ProjectID: 5,
|
||||
Right: RightAdmin,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
_, _, _, err := tl.ReadAll(s, u, "", 1, 50)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNeedToHaveListReadAccess(err))
|
||||
assert.True(t, IsErrNeedToHaveProjectReadAccess(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("search", func(t *testing.T) {
|
||||
tl := TeamList{
|
||||
ListID: 19,
|
||||
tl := TeamProject{
|
||||
ProjectID: 19,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
@ -97,15 +85,15 @@ func TestTeamList_ReadAll(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestTeamList_Create(t *testing.T) {
|
||||
func TestTeamProject_Create(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
tl := TeamList{
|
||||
TeamID: 1,
|
||||
ListID: 1,
|
||||
Right: RightAdmin,
|
||||
tl := TeamProject{
|
||||
TeamID: 1,
|
||||
ProjectID: 1,
|
||||
Right: RightAdmin,
|
||||
}
|
||||
allowed, _ := tl.CanCreate(s, u)
|
||||
assert.True(t, allowed)
|
||||
|
@ -113,19 +101,19 @@ func TestTeamList_Create(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertExists(t, "team_lists", map[string]interface{}{
|
||||
"team_id": 1,
|
||||
"list_id": 1,
|
||||
"right": RightAdmin,
|
||||
db.AssertExists(t, "team_projects", map[string]interface{}{
|
||||
"team_id": 1,
|
||||
"project_id": 1,
|
||||
"right": RightAdmin,
|
||||
}, false)
|
||||
})
|
||||
t.Run("team already has access", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
tl := TeamList{
|
||||
TeamID: 1,
|
||||
ListID: 3,
|
||||
Right: RightAdmin,
|
||||
tl := TeamProject{
|
||||
TeamID: 1,
|
||||
ProjectID: 3,
|
||||
Right: RightAdmin,
|
||||
}
|
||||
err := tl.Create(s, u)
|
||||
assert.Error(t, err)
|
||||
|
@ -135,10 +123,10 @@ func TestTeamList_Create(t *testing.T) {
|
|||
t.Run("wrong rights", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
tl := TeamList{
|
||||
TeamID: 1,
|
||||
ListID: 1,
|
||||
Right: RightUnknown,
|
||||
tl := TeamProject{
|
||||
TeamID: 1,
|
||||
ProjectID: 1,
|
||||
Right: RightUnknown,
|
||||
}
|
||||
err := tl.Create(s, u)
|
||||
assert.Error(t, err)
|
||||
|
@ -148,84 +136,84 @@ func TestTeamList_Create(t *testing.T) {
|
|||
t.Run("nonexistant team", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
tl := TeamList{
|
||||
TeamID: 9999,
|
||||
ListID: 1,
|
||||
tl := TeamProject{
|
||||
TeamID: 9999,
|
||||
ProjectID: 1,
|
||||
}
|
||||
err := tl.Create(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant list", func(t *testing.T) {
|
||||
t.Run("nonexistant project", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
tl := TeamList{
|
||||
TeamID: 1,
|
||||
ListID: 9999,
|
||||
tl := TeamProject{
|
||||
TeamID: 1,
|
||||
ProjectID: 9999,
|
||||
}
|
||||
err := tl.Create(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
assert.True(t, IsErrProjectDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestTeamList_Delete(t *testing.T) {
|
||||
func TestTeamProject_Delete(t *testing.T) {
|
||||
user := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
tl := TeamList{
|
||||
TeamID: 1,
|
||||
ListID: 3,
|
||||
tl := TeamProject{
|
||||
TeamID: 1,
|
||||
ProjectID: 3,
|
||||
}
|
||||
err := tl.Delete(s, user)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertMissing(t, "team_lists", map[string]interface{}{
|
||||
"team_id": 1,
|
||||
"list_id": 3,
|
||||
db.AssertMissing(t, "team_projects", map[string]interface{}{
|
||||
"team_id": 1,
|
||||
"project_id": 3,
|
||||
})
|
||||
})
|
||||
t.Run("nonexistant team", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
tl := TeamList{
|
||||
TeamID: 9999,
|
||||
ListID: 1,
|
||||
tl := TeamProject{
|
||||
TeamID: 9999,
|
||||
ProjectID: 1,
|
||||
}
|
||||
err := tl.Delete(s, user)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant list", func(t *testing.T) {
|
||||
t.Run("nonexistant project", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
tl := TeamList{
|
||||
TeamID: 1,
|
||||
ListID: 9999,
|
||||
tl := TeamProject{
|
||||
TeamID: 1,
|
||||
ProjectID: 9999,
|
||||
}
|
||||
err := tl.Delete(s, user)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotHaveAccessToList(err))
|
||||
assert.True(t, IsErrTeamDoesNotHaveAccessToProject(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestTeamList_Update(t *testing.T) {
|
||||
func TestTeamProject_Update(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
TeamID int64
|
||||
ListID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
ID int64
|
||||
TeamID int64
|
||||
ProjectID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -236,33 +224,33 @@ func TestTeamList_Update(t *testing.T) {
|
|||
{
|
||||
name: "Test Update Normally",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
TeamID: 1,
|
||||
Right: RightAdmin,
|
||||
ProjectID: 3,
|
||||
TeamID: 1,
|
||||
Right: RightAdmin,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update to write",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
TeamID: 1,
|
||||
Right: RightWrite,
|
||||
ProjectID: 3,
|
||||
TeamID: 1,
|
||||
Right: RightWrite,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update to Read",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
TeamID: 1,
|
||||
Right: RightRead,
|
||||
ProjectID: 3,
|
||||
TeamID: 1,
|
||||
Right: RightRead,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update with invalid right",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
TeamID: 1,
|
||||
Right: 500,
|
||||
ProjectID: 3,
|
||||
TeamID: 1,
|
||||
Right: 500,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrInvalidRight,
|
||||
|
@ -273,30 +261,30 @@ func TestTeamList_Update(t *testing.T) {
|
|||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
tl := &TeamList{
|
||||
ID: tt.fields.ID,
|
||||
TeamID: tt.fields.TeamID,
|
||||
ListID: tt.fields.ListID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
tl := &TeamProject{
|
||||
ID: tt.fields.ID,
|
||||
TeamID: tt.fields.TeamID,
|
||||
ProjectID: tt.fields.ProjectID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := tl.Update(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("TeamList.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("TeamProject.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("TeamList.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
t.Errorf("TeamProject.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "team_lists", map[string]interface{}{
|
||||
"list_id": tt.fields.ListID,
|
||||
"team_id": tt.fields.TeamID,
|
||||
"right": tt.fields.Right,
|
||||
db.AssertExists(t, "team_projects", map[string]interface{}{
|
||||
"project_id": tt.fields.ProjectID,
|
||||
"team_id": tt.fields.TeamID,
|
||||
"right": tt.fields.Right,
|
||||
}, false)
|
||||
}
|
||||
})
|
|
@ -28,16 +28,16 @@ import (
|
|||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// ListUser represents a list <-> user relation
|
||||
type ListUser struct {
|
||||
// The unique, numeric id of this list <-> user relation.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
// ProjectUser represents a project <-> user relation
|
||||
type ProjectUser struct {
|
||||
// The unique, numeric id of this project <-> user relation.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
|
||||
// The username.
|
||||
Username string `xorm:"-" json:"user_id" param:"user"`
|
||||
// Used internally to reference the user
|
||||
UserID int64 `xorm:"bigint not null INDEX" json:"-"`
|
||||
// The list id.
|
||||
ListID int64 `xorm:"bigint not null INDEX" json:"-" param:"list"`
|
||||
// The project id.
|
||||
ProjectID int64 `xorm:"bigint not null INDEX" json:"-" param:"project"`
|
||||
// The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
|
||||
Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
|
||||
|
@ -50,41 +50,41 @@ type ListUser struct {
|
|||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName is the table name for ListUser
|
||||
func (ListUser) TableName() string {
|
||||
return "users_lists"
|
||||
// TableName is the table name for ProjectUser
|
||||
func (ProjectUser) TableName() string {
|
||||
return "users_projects"
|
||||
}
|
||||
|
||||
// UserWithRight represents a user in combination with the right it can have on a list/namespace
|
||||
// UserWithRight represents a user in combination with the right it can have on a project
|
||||
type UserWithRight struct {
|
||||
user.User `xorm:"extends"`
|
||||
Right Right `json:"right"`
|
||||
}
|
||||
|
||||
// Create creates a new list <-> user relation
|
||||
// @Summary Add a user to a list
|
||||
// @Description Gives a user access to a list.
|
||||
// Create creates a new project <-> user relation
|
||||
// @Summary Add a user to a project
|
||||
// @Description Gives a user access to a project.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List ID"
|
||||
// @Param list body models.ListUser true "The user you want to add to the list."
|
||||
// @Success 201 {object} models.ListUser "The created user<->list relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid user list object provided."
|
||||
// @Param id path int true "Project ID"
|
||||
// @Param project body models.ProjectUser true "The user you want to add to the project."
|
||||
// @Success 201 {object} models.ProjectUser "The created user<->project relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid user project object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The user does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id}/users [put]
|
||||
func (lu *ListUser) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Router /projects/{id}/users [put]
|
||||
func (lu *ProjectUser) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := lu.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the list exists
|
||||
l, err := GetListSimpleByID(s, lu.ListID)
|
||||
// Check if the project exists
|
||||
l, err := GetProjectSimpleByID(s, lu.ProjectID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -96,53 +96,53 @@ func (lu *ListUser) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
lu.UserID = u.ID
|
||||
|
||||
// Check if the user already has access or is owner of that list
|
||||
// Check if the user already has access or is owner of that project
|
||||
// We explicitly DONT check for teams here
|
||||
if l.OwnerID == lu.UserID {
|
||||
return ErrUserAlreadyHasAccess{UserID: lu.UserID, ListID: lu.ListID}
|
||||
return ErrUserAlreadyHasAccess{UserID: lu.UserID, ProjectID: lu.ProjectID}
|
||||
}
|
||||
|
||||
exist, err := s.Where("list_id = ? AND user_id = ?", lu.ListID, lu.UserID).Get(&ListUser{})
|
||||
exist, err := s.Where("project_id = ? AND user_id = ?", lu.ProjectID, lu.UserID).Get(&ProjectUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
return ErrUserAlreadyHasAccess{UserID: lu.UserID, ListID: lu.ListID}
|
||||
return ErrUserAlreadyHasAccess{UserID: lu.UserID, ProjectID: lu.ProjectID}
|
||||
}
|
||||
|
||||
// Insert user <-> list relation
|
||||
// Insert user <-> project relation
|
||||
_, err = s.Insert(lu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = events.Dispatch(&ListSharedWithUserEvent{
|
||||
List: l,
|
||||
User: u,
|
||||
Doer: a,
|
||||
err = events.Dispatch(&ProjectSharedWithUserEvent{
|
||||
Project: l,
|
||||
User: u,
|
||||
Doer: a,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, l)
|
||||
err = updateProjectLastUpdated(s, l)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete deletes a list <-> user relation
|
||||
// @Summary Delete a user from a list
|
||||
// @Description Delets a user from a list. The user won't have access to the list anymore.
|
||||
// Delete deletes a project <-> user relation
|
||||
// @Summary Delete a user from a project
|
||||
// @Description Delets a user from a project. The user won't have access to the project anymore.
|
||||
// @tags sharing
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param listID path int true "List ID"
|
||||
// @Param projectID path int true "Project ID"
|
||||
// @Param userID path int true "User ID"
|
||||
// @Success 200 {object} models.Message "The user was successfully removed from the list."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 404 {object} web.HTTPError "user or list does not exist."
|
||||
// @Success 200 {object} models.Message "The user was successfully removed from the project."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 404 {object} web.HTTPError "user or project does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/users/{userID} [delete]
|
||||
func (lu *ListUser) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Router /projects/{projectID}/users/{userID} [delete]
|
||||
func (lu *ProjectUser) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the user exists
|
||||
u, err := user.GetUserByUsername(s, lu.Username)
|
||||
|
@ -151,52 +151,52 @@ func (lu *ListUser) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
lu.UserID = u.ID
|
||||
|
||||
// Check if the user has access to the list
|
||||
// Check if the user has access to the project
|
||||
has, err := s.
|
||||
Where("user_id = ? AND list_id = ?", lu.UserID, lu.ListID).
|
||||
Get(&ListUser{})
|
||||
Where("user_id = ? AND project_id = ?", lu.UserID, lu.ProjectID).
|
||||
Get(&ProjectUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
return ErrUserDoesNotHaveAccessToList{ListID: lu.ListID, UserID: lu.UserID}
|
||||
return ErrUserDoesNotHaveAccessToProject{ProjectID: lu.ProjectID, UserID: lu.UserID}
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
Where("user_id = ? AND list_id = ?", lu.UserID, lu.ListID).
|
||||
Delete(&ListUser{})
|
||||
Where("user_id = ? AND project_id = ?", lu.UserID, lu.ProjectID).
|
||||
Delete(&ProjectUser{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, &List{ID: lu.ListID})
|
||||
err = updateProjectLastUpdated(s, &Project{ID: lu.ProjectID})
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll gets all users who have access to a list
|
||||
// @Summary Get users on a list
|
||||
// @Description Returns a list with all users which have access on a given list.
|
||||
// ReadAll gets all users who have access to a project
|
||||
// @Summary Get users on a project
|
||||
// @Description Returns a project with all users which have access on a given project.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "List ID"
|
||||
// @Param id path int true "Project ID"
|
||||
// @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 s query string false "Search users by its name."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.UserWithRight "The users with the right they have."
|
||||
// @Failure 403 {object} web.HTTPError "No right to see the list."
|
||||
// @Failure 403 {object} web.HTTPError "No right to see the project."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id}/users [get]
|
||||
func (lu *ListUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
// Check if the user has access to the list
|
||||
l := &List{ID: lu.ListID}
|
||||
// @Router /projects/{id}/users [get]
|
||||
func (lu *ProjectUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
// Check if the user has access to the project
|
||||
l := &Project{ID: lu.ProjectID}
|
||||
canRead, _, err := l.CanRead(s, a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
if !canRead {
|
||||
return nil, 0, 0, ErrNeedToHaveListReadAccess{UserID: a.GetID(), ListID: lu.ListID}
|
||||
return nil, 0, 0, ErrNeedToHaveProjectReadAccess{UserID: a.GetID(), ProjectID: lu.ProjectID}
|
||||
}
|
||||
|
||||
limit, start := getLimitFromPageIndex(page, perPage)
|
||||
|
@ -204,8 +204,8 @@ func (lu *ListUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
// Get all users
|
||||
all := []*UserWithRight{}
|
||||
query := s.
|
||||
Join("INNER", "users_lists", "user_id = users.id").
|
||||
Where("users_lists.list_id = ?", lu.ListID).
|
||||
Join("INNER", "users_projects", "user_id = users.id").
|
||||
Where("users_projects.project_id = ?", lu.ProjectID).
|
||||
Where(db.ILIKE("users.username", search))
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
|
@ -221,30 +221,30 @@ func (lu *ListUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
}
|
||||
|
||||
numberOfTotalItems, err = s.
|
||||
Join("INNER", "users_lists", "user_id = users.id").
|
||||
Where("users_lists.list_id = ?", lu.ListID).
|
||||
Join("INNER", "users_projects", "user_id = users.id").
|
||||
Where("users_projects.project_id = ?", lu.ProjectID).
|
||||
Where("users.username LIKE ?", "%"+search+"%").
|
||||
Count(&UserWithRight{})
|
||||
|
||||
return all, len(all), numberOfTotalItems, err
|
||||
}
|
||||
|
||||
// Update updates a user <-> list relation
|
||||
// @Summary Update a user <-> list relation
|
||||
// @Description Update a user <-> list relation. Mostly used to update the right that user has.
|
||||
// Update updates a user <-> project relation
|
||||
// @Summary Update a user <-> project relation
|
||||
// @Description Update a user <-> project relation. Mostly used to update the right that user has.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param listID path int true "List ID"
|
||||
// @Param projectID path int true "Project ID"
|
||||
// @Param userID path int true "User ID"
|
||||
// @Param list body models.ListUser true "The user you want to update."
|
||||
// @Param project body models.ProjectUser true "The user you want to update."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} models.ListUser "The updated user <-> list relation."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have admin-access to the list"
|
||||
// @Failure 404 {object} web.HTTPError "User or list does not exist."
|
||||
// @Success 200 {object} models.ProjectUser "The updated user <-> project relation."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have admin-access to the project"
|
||||
// @Failure 404 {object} web.HTTPError "User or project does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/users/{userID} [post]
|
||||
func (lu *ListUser) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Router /projects/{projectID}/users/{userID} [post]
|
||||
func (lu *ProjectUser) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := lu.Right.isValid(); err != nil {
|
||||
|
@ -259,13 +259,13 @@ func (lu *ListUser) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
lu.UserID = u.ID
|
||||
|
||||
_, err = s.
|
||||
Where("list_id = ? AND user_id = ?", lu.ListID, lu.UserID).
|
||||
Where("project_id = ? AND user_id = ?", lu.ProjectID, lu.UserID).
|
||||
Cols("right").
|
||||
Update(lu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, &List{ID: lu.ListID})
|
||||
err = updateProjectLastUpdated(s, &Project{ID: lu.ProjectID})
|
||||
return
|
||||
}
|
|
@ -21,28 +21,28 @@ import (
|
|||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can create a new user <-> list relation
|
||||
func (lu *ListUser) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return lu.canDoListUser(s, a)
|
||||
// CanCreate checks if the user can create a new user <-> project relation
|
||||
func (lu *ProjectUser) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return lu.canDoProjectUser(s, a)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a user <-> list relation
|
||||
func (lu *ListUser) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return lu.canDoListUser(s, a)
|
||||
// CanDelete checks if the user can delete a user <-> project relation
|
||||
func (lu *ProjectUser) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return lu.canDoProjectUser(s, a)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a user <-> list relation
|
||||
func (lu *ListUser) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return lu.canDoListUser(s, a)
|
||||
// CanUpdate checks if the user can update a user <-> project relation
|
||||
func (lu *ProjectUser) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return lu.canDoProjectUser(s, a)
|
||||
}
|
||||
|
||||
func (lu *ListUser) canDoListUser(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
func (lu *ProjectUser) canDoProjectUser(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
// Link shares aren't allowed to do anything
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get the list and check if the user has write access on it
|
||||
l := List{ID: lu.ListID}
|
||||
// Get the project and check if the user has write access on it
|
||||
l := Project{ID: lu.ProjectID}
|
||||
return l.IsAdmin(s, a)
|
||||
}
|
|
@ -26,16 +26,16 @@ import (
|
|||
"code.vikunja.io/web"
|
||||
)
|
||||
|
||||
func TestListUser_CanDoSomething(t *testing.T) {
|
||||
func TestProjectUser_CanDoSomething(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
ListID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
ID int64
|
||||
UserID int64
|
||||
ProjectID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
type args struct {
|
||||
a web.Auth
|
||||
|
@ -49,7 +49,7 @@ func TestListUser_CanDoSomething(t *testing.T) {
|
|||
{
|
||||
name: "CanDoSomething Normally",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
ProjectID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
|
@ -57,9 +57,9 @@ func TestListUser_CanDoSomething(t *testing.T) {
|
|||
want: map[string]bool{"CanCreate": true, "CanDelete": true, "CanUpdate": true},
|
||||
},
|
||||
{
|
||||
name: "CanDoSomething for a nonexistant list",
|
||||
name: "CanDoSomething for a nonexistant project",
|
||||
fields: fields{
|
||||
ListID: 300,
|
||||
ProjectID: 300,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
|
@ -69,7 +69,7 @@ func TestListUser_CanDoSomething(t *testing.T) {
|
|||
{
|
||||
name: "CanDoSomething where the user does not have the rights",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
ProjectID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 4},
|
||||
|
@ -82,24 +82,24 @@ func TestListUser_CanDoSomething(t *testing.T) {
|
|||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
lu := &ListUser{
|
||||
ID: tt.fields.ID,
|
||||
UserID: tt.fields.UserID,
|
||||
ListID: tt.fields.ListID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
lu := &ProjectUser{
|
||||
ID: tt.fields.ID,
|
||||
UserID: tt.fields.UserID,
|
||||
ProjectID: tt.fields.ProjectID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
if got, _ := lu.CanCreate(s, tt.args.a); got != tt.want["CanCreate"] {
|
||||
t.Errorf("ListUser.CanCreate() = %v, want %v", got, tt.want["CanCreate"])
|
||||
t.Errorf("ProjectUser.CanCreate() = %v, want %v", got, tt.want["CanCreate"])
|
||||
}
|
||||
if got, _ := lu.CanDelete(s, tt.args.a); got != tt.want["CanDelete"] {
|
||||
t.Errorf("ListUser.CanDelete() = %v, want %v", got, tt.want["CanDelete"])
|
||||
t.Errorf("ProjectUser.CanDelete() = %v, want %v", got, tt.want["CanDelete"])
|
||||
}
|
||||
if got, _ := lu.CanUpdate(s, tt.args.a); got != tt.want["CanUpdate"] {
|
||||
t.Errorf("ListUser.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"])
|
||||
t.Errorf("ProjectUser.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"])
|
||||
}
|
||||
_ = s.Close()
|
||||
})
|
|
@ -29,17 +29,17 @@ import (
|
|||
"gopkg.in/d4l3k/messagediff.v1"
|
||||
)
|
||||
|
||||
func TestListUser_Create(t *testing.T) {
|
||||
func TestProjectUser_Create(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
Username string
|
||||
ListID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
ID int64
|
||||
UserID int64
|
||||
Username string
|
||||
ProjectID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
type args struct {
|
||||
a web.Auth
|
||||
|
@ -54,15 +54,15 @@ func TestListUser_Create(t *testing.T) {
|
|||
{
|
||||
name: "ListUsers Create normally",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
ListID: 2,
|
||||
Username: "user1",
|
||||
ProjectID: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ListUsers Create for duplicate",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
ListID: 3,
|
||||
Username: "user1",
|
||||
ProjectID: 3,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrUserAlreadyHasAccess,
|
||||
|
@ -70,27 +70,27 @@ func TestListUser_Create(t *testing.T) {
|
|||
{
|
||||
name: "ListUsers Create with invalid right",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
ListID: 2,
|
||||
Right: 500,
|
||||
Username: "user1",
|
||||
ProjectID: 2,
|
||||
Right: 500,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrInvalidRight,
|
||||
},
|
||||
{
|
||||
name: "ListUsers Create with inexisting list",
|
||||
name: "ListUsers Create with inexisting project",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
ListID: 2000,
|
||||
Username: "user1",
|
||||
ProjectID: 2000,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrListDoesNotExist,
|
||||
errType: IsErrProjectDoesNotExist,
|
||||
},
|
||||
{
|
||||
name: "ListUsers Create with inexisting user",
|
||||
fields: fields{
|
||||
Username: "user500",
|
||||
ListID: 2,
|
||||
Username: "user500",
|
||||
ProjectID: 2,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: user.IsErrUserDoesNotExist,
|
||||
|
@ -98,8 +98,8 @@ func TestListUser_Create(t *testing.T) {
|
|||
{
|
||||
name: "ListUsers Create with the owner as shared user",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
ListID: 1,
|
||||
Username: "user1",
|
||||
ProjectID: 1,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrUserAlreadyHasAccess,
|
||||
|
@ -110,39 +110,39 @@ func TestListUser_Create(t *testing.T) {
|
|||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
ul := &ListUser{
|
||||
ID: tt.fields.ID,
|
||||
UserID: tt.fields.UserID,
|
||||
Username: tt.fields.Username,
|
||||
ListID: tt.fields.ListID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
ul := &ProjectUser{
|
||||
ID: tt.fields.ID,
|
||||
UserID: tt.fields.UserID,
|
||||
Username: tt.fields.Username,
|
||||
ProjectID: tt.fields.ProjectID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := ul.Create(s, tt.args.a)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ListUser.Create() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("ProjectUser.Create() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("ListUser.Create() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
t.Errorf("ProjectUser.Create() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "users_lists", map[string]interface{}{
|
||||
"user_id": ul.UserID,
|
||||
"list_id": tt.fields.ListID,
|
||||
db.AssertExists(t, "users_projects", map[string]interface{}{
|
||||
"user_id": ul.UserID,
|
||||
"project_id": tt.fields.ProjectID,
|
||||
}, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUser_ReadAll(t *testing.T) {
|
||||
func TestProjectUser_ReadAll(t *testing.T) {
|
||||
user1Read := &UserWithRight{
|
||||
User: user.User{
|
||||
ID: 1,
|
||||
|
@ -173,14 +173,14 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
}
|
||||
|
||||
type fields struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
ListID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
ID int64
|
||||
UserID int64
|
||||
ProjectID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
type args struct {
|
||||
search string
|
||||
|
@ -198,7 +198,7 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "Test readall normal",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
ProjectID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
|
@ -209,20 +209,20 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "Test ReadAll by a user who does not have access to the list",
|
||||
name: "Test ReadAll by a user who does not have access to the project",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
ProjectID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 4},
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrNeedToHaveListReadAccess,
|
||||
errType: IsErrNeedToHaveProjectReadAccess,
|
||||
},
|
||||
{
|
||||
name: "Search",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
ProjectID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
|
@ -238,41 +238,41 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
ul := &ListUser{
|
||||
ID: tt.fields.ID,
|
||||
UserID: tt.fields.UserID,
|
||||
ListID: tt.fields.ListID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
ul := &ProjectUser{
|
||||
ID: tt.fields.ID,
|
||||
UserID: tt.fields.UserID,
|
||||
ProjectID: tt.fields.ProjectID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
got, _, _, err := ul.ReadAll(s, tt.args.a, tt.args.search, tt.args.page, 50)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ListUser.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("ProjectUser.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("ListUser.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
t.Errorf("ProjectUser.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
if diff, equal := messagediff.PrettyDiff(got, tt.want); !equal {
|
||||
t.Errorf("ListUser.ReadAll() = %v, want %v, diff: %v", got, tt.want, diff)
|
||||
t.Errorf("ProjectUser.ReadAll() = %v, want %v, diff: %v", got, tt.want, diff)
|
||||
}
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUser_Update(t *testing.T) {
|
||||
func TestProjectUser_Update(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
Username string
|
||||
ListID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
ID int64
|
||||
Username string
|
||||
ProjectID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -283,33 +283,33 @@ func TestListUser_Update(t *testing.T) {
|
|||
{
|
||||
name: "Test Update Normally",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
Username: "user1",
|
||||
Right: RightAdmin,
|
||||
ProjectID: 3,
|
||||
Username: "user1",
|
||||
Right: RightAdmin,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update to write",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
Username: "user1",
|
||||
Right: RightWrite,
|
||||
ProjectID: 3,
|
||||
Username: "user1",
|
||||
Right: RightWrite,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update to Read",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
Username: "user1",
|
||||
Right: RightRead,
|
||||
ProjectID: 3,
|
||||
Username: "user1",
|
||||
Right: RightRead,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update with invalid right",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
Username: "user1",
|
||||
Right: 500,
|
||||
ProjectID: 3,
|
||||
Username: "user1",
|
||||
Right: 500,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrInvalidRight,
|
||||
|
@ -320,49 +320,49 @@ func TestListUser_Update(t *testing.T) {
|
|||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
lu := &ListUser{
|
||||
ID: tt.fields.ID,
|
||||
Username: tt.fields.Username,
|
||||
ListID: tt.fields.ListID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
lu := &ProjectUser{
|
||||
ID: tt.fields.ID,
|
||||
Username: tt.fields.Username,
|
||||
ProjectID: tt.fields.ProjectID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := lu.Update(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ListUser.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("ProjectUser.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("ListUser.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
t.Errorf("ProjectUser.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "users_lists", map[string]interface{}{
|
||||
"list_id": tt.fields.ListID,
|
||||
"user_id": lu.UserID,
|
||||
"right": tt.fields.Right,
|
||||
db.AssertExists(t, "users_projects", map[string]interface{}{
|
||||
"project_id": tt.fields.ProjectID,
|
||||
"user_id": lu.UserID,
|
||||
"right": tt.fields.Right,
|
||||
}, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUser_Delete(t *testing.T) {
|
||||
func TestProjectUser_Delete(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
Username string
|
||||
UserID int64
|
||||
ListID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
ID int64
|
||||
Username string
|
||||
UserID int64
|
||||
ProjectID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -373,8 +373,8 @@ func TestListUser_Delete(t *testing.T) {
|
|||
{
|
||||
name: "Try deleting some unexistant user",
|
||||
fields: fields{
|
||||
Username: "user1000",
|
||||
ListID: 2,
|
||||
Username: "user1000",
|
||||
ProjectID: 2,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: user.IsErrUserDoesNotExist,
|
||||
|
@ -382,18 +382,18 @@ func TestListUser_Delete(t *testing.T) {
|
|||
{
|
||||
name: "Try deleting a user which does not has access but exists",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
ListID: 4,
|
||||
Username: "user1",
|
||||
ProjectID: 4,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrUserDoesNotHaveAccessToList,
|
||||
errType: IsErrUserDoesNotHaveAccessToProject,
|
||||
},
|
||||
{
|
||||
name: "Try deleting normally",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
UserID: 1,
|
||||
ListID: 3,
|
||||
Username: "user1",
|
||||
UserID: 1,
|
||||
ProjectID: 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -402,31 +402,31 @@ func TestListUser_Delete(t *testing.T) {
|
|||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
lu := &ListUser{
|
||||
ID: tt.fields.ID,
|
||||
Username: tt.fields.Username,
|
||||
ListID: tt.fields.ListID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
lu := &ProjectUser{
|
||||
ID: tt.fields.ID,
|
||||
Username: tt.fields.Username,
|
||||
ProjectID: tt.fields.ProjectID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := lu.Delete(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ListUser.Delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("ProjectUser.Delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("ListUser.Delete() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
t.Errorf("ProjectUser.Delete() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertMissing(t, "users_lists", map[string]interface{}{
|
||||
"user_id": tt.fields.UserID,
|
||||
"list_id": tt.fields.ListID,
|
||||
db.AssertMissing(t, "users_projects", map[string]interface{}{
|
||||
"user_id": tt.fields.UserID,
|
||||
"project_id": tt.fields.ProjectID,
|
||||
})
|
||||
}
|
||||
})
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package models
|
||||
|
||||
// Right defines the rights users/teams can have for lists/namespaces
|
||||
// Right defines the rights users/teams can have for projects
|
||||
type Right int
|
||||
|
||||
// define unknown right
|
||||
|
@ -26,11 +26,11 @@ const (
|
|||
|
||||
// Enumerate all the team rights
|
||||
const (
|
||||
// Can read lists in a
|
||||
// Can read projects in a
|
||||
RightRead Right = iota
|
||||
// Can write in a like lists and tasks. Cannot create new lists.
|
||||
// Can write in a like projects and tasks. Cannot create new projects.
|
||||
RightWrite
|
||||
// Can manage a list/namespace, can do everything
|
||||
// Can manage a project, can do everything
|
||||
RightAdmin
|
||||
)
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ type SavedFilter struct {
|
|||
// The user who owns this filter
|
||||
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
|
||||
|
||||
// True if the filter is a favorite. Favorite filters show up in a separate namespace together with favorite lists.
|
||||
// True if the filter is a favorite. Favorite filters show up in a separate parent project together with favorite projects.
|
||||
IsFavorite bool `xorm:"default false" json:"is_favorite"`
|
||||
|
||||
// A timestamp when this filter was created. You cannot change this value.
|
||||
|
@ -57,28 +57,28 @@ func (sf *SavedFilter) TableName() string {
|
|||
}
|
||||
|
||||
func (sf *SavedFilter) getTaskCollection() *TaskCollection {
|
||||
// We're resetting the listID to return tasks from all lists
|
||||
sf.Filters.ListID = 0
|
||||
// We're resetting the projectID to return tasks from all projects
|
||||
sf.Filters.ProjectID = 0
|
||||
return sf.Filters
|
||||
}
|
||||
|
||||
// Returns the saved filter ID from a list ID. Will not check if the filter actually exists.
|
||||
// Returns the saved filter ID from a project ID. Will not check if the filter actually exists.
|
||||
// If the returned ID is zero, means that it is probably invalid.
|
||||
func getSavedFilterIDFromListID(listID int64) (filterID int64) {
|
||||
// We get the id of the saved filter by multiplying the ListID with -1 and subtracting one
|
||||
filterID = listID*-1 - 1
|
||||
// FilterIDs from listIDs are always positive
|
||||
func getSavedFilterIDFromProjectID(projectID int64) (filterID int64) {
|
||||
// We get the id of the saved filter by multiplying the ProjectID with -1 and subtracting one
|
||||
filterID = projectID*-1 - 1
|
||||
// FilterIDs from projectIDs are always positive
|
||||
if filterID < 0 {
|
||||
filterID = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getListIDFromSavedFilterID(filterID int64) (listID int64) {
|
||||
listID = filterID*-1 - 1
|
||||
// ListIDs from saved filters are always negative
|
||||
if listID > 0 {
|
||||
listID = 0
|
||||
func getProjectIDFromSavedFilterID(filterID int64) (projectID int64) {
|
||||
projectID = filterID*-1 - 1
|
||||
// ProjectIDs from saved filters are always negative
|
||||
if projectID > 0 {
|
||||
projectID = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -93,16 +93,16 @@ func getSavedFiltersForUser(s *xorm.Session, auth web.Auth) (filters []*SavedFil
|
|||
return
|
||||
}
|
||||
|
||||
func (sf *SavedFilter) toList() *List {
|
||||
return &List{
|
||||
ID: getListIDFromSavedFilterID(sf.ID),
|
||||
Title: sf.Title,
|
||||
Description: sf.Description,
|
||||
IsFavorite: sf.IsFavorite,
|
||||
Created: sf.Created,
|
||||
Updated: sf.Updated,
|
||||
Owner: sf.Owner,
|
||||
NamespaceID: SavedFiltersPseudoNamespace.ID,
|
||||
func (sf *SavedFilter) toProject() *Project {
|
||||
return &Project{
|
||||
ID: getProjectIDFromSavedFilterID(sf.ID),
|
||||
Title: sf.Title,
|
||||
Description: sf.Description,
|
||||
IsFavorite: sf.IsFavorite,
|
||||
Created: sf.Created,
|
||||
Updated: sf.Updated,
|
||||
Owner: sf.Owner,
|
||||
ParentProjectID: SavedFiltersPseudoProject.ID,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,21 +25,21 @@ import (
|
|||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
func TestSavedFilter_getListIDFromFilter(t *testing.T) {
|
||||
func TestSavedFilter_getProjectIDFromFilter(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
assert.Equal(t, int64(-2), getListIDFromSavedFilterID(1))
|
||||
assert.Equal(t, int64(-2), getProjectIDFromSavedFilterID(1))
|
||||
})
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
assert.Equal(t, int64(0), getListIDFromSavedFilterID(-1))
|
||||
assert.Equal(t, int64(0), getProjectIDFromSavedFilterID(-1))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSavedFilter_getFilterIDFromListID(t *testing.T) {
|
||||
func TestSavedFilter_getFilterIDFromProjectID(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
assert.Equal(t, int64(1), getSavedFilterIDFromListID(-2))
|
||||
assert.Equal(t, int64(1), getSavedFilterIDFromProjectID(-2))
|
||||
})
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
assert.Equal(t, int64(0), getSavedFilterIDFromListID(2))
|
||||
assert.Equal(t, int64(0), getSavedFilterIDFromProjectID(2))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -30,16 +30,15 @@ import (
|
|||
type SubscriptionEntityType int
|
||||
|
||||
const (
|
||||
SubscriptionEntityUnknown = iota
|
||||
SubscriptionEntityNamespace
|
||||
SubscriptionEntityList
|
||||
SubscriptionEntityUnknown = iota
|
||||
SubscriptionEntityNamespace // Kept even though not used anymore since we don't want to manually change all ids
|
||||
SubscriptionEntityProject
|
||||
SubscriptionEntityTask
|
||||
)
|
||||
|
||||
const (
|
||||
entityNamespace = `namespace`
|
||||
entityList = `list`
|
||||
entityTask = `task`
|
||||
entityProject = `project`
|
||||
entityTask = `task`
|
||||
)
|
||||
|
||||
// Subscription represents a subscription for an entity
|
||||
|
@ -70,10 +69,8 @@ func (sb *Subscription) TableName() string {
|
|||
|
||||
func getEntityTypeFromString(entityType string) SubscriptionEntityType {
|
||||
switch entityType {
|
||||
case entityNamespace:
|
||||
return SubscriptionEntityNamespace
|
||||
case entityList:
|
||||
return SubscriptionEntityList
|
||||
case entityProject:
|
||||
return SubscriptionEntityProject
|
||||
case entityTask:
|
||||
return SubscriptionEntityTask
|
||||
}
|
||||
|
@ -84,10 +81,8 @@ func getEntityTypeFromString(entityType string) SubscriptionEntityType {
|
|||
// String returns a human-readable string of an entity
|
||||
func (et SubscriptionEntityType) String() string {
|
||||
switch et {
|
||||
case SubscriptionEntityNamespace:
|
||||
return entityNamespace
|
||||
case SubscriptionEntityList:
|
||||
return entityList
|
||||
case SubscriptionEntityProject:
|
||||
return entityProject
|
||||
case SubscriptionEntityTask:
|
||||
return entityTask
|
||||
}
|
||||
|
@ -96,8 +91,7 @@ func (et SubscriptionEntityType) String() string {
|
|||
}
|
||||
|
||||
func (et SubscriptionEntityType) validate() error {
|
||||
if et == SubscriptionEntityNamespace ||
|
||||
et == SubscriptionEntityList ||
|
||||
if et == SubscriptionEntityProject ||
|
||||
et == SubscriptionEntityTask {
|
||||
return nil
|
||||
}
|
||||
|
@ -112,7 +106,7 @@ func (et SubscriptionEntityType) validate() error {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param entity path string true "The entity the user subscribes to. Can be either `namespace`, `list` or `task`."
|
||||
// @Param entity path string true "The entity the user subscribes to. Can be either `project` or `task`."
|
||||
// @Param entityID path string true "The numeric id of the entity to subscribe to."
|
||||
// @Success 201 {object} models.Subscription "The subscription"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to subscribe to this entity."
|
||||
|
@ -153,7 +147,7 @@ func (sb *Subscription) Create(s *xorm.Session, auth web.Auth) (err error) {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param entity path string true "The entity the user subscribed to. Can be either `namespace`, `list` or `task`."
|
||||
// @Param entity path string true "The entity the user subscribed to. Can be either `project` or `task`."
|
||||
// @Param entityID path string true "The numeric id of the subscribed entity to."
|
||||
// @Success 200 {object} models.Subscription "The subscription"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to subscribe to this entity."
|
||||
|
@ -169,53 +163,28 @@ func (sb *Subscription) Delete(s *xorm.Session, auth web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int64) (cond builder.Cond) {
|
||||
if entityType == SubscriptionEntityNamespace {
|
||||
cond = builder.And(
|
||||
builder.Eq{"entity_id": entityID},
|
||||
builder.Eq{"entity_type": SubscriptionEntityNamespace},
|
||||
)
|
||||
}
|
||||
|
||||
if entityType == SubscriptionEntityList {
|
||||
cond = builder.Or(
|
||||
builder.And(
|
||||
builder.Eq{"entity_id": entityID},
|
||||
builder.Eq{"entity_type": SubscriptionEntityList},
|
||||
),
|
||||
builder.And(
|
||||
builder.Eq{"entity_id": builder.
|
||||
Select("namespace_id").
|
||||
From("lists").
|
||||
Where(builder.Eq{"id": entityID}),
|
||||
},
|
||||
builder.Eq{"entity_type": SubscriptionEntityNamespace},
|
||||
),
|
||||
func getSubscriberCondForEntities(entityType SubscriptionEntityType, entityIDs []int64) (cond builder.Cond) {
|
||||
if entityType == SubscriptionEntityProject {
|
||||
return builder.And(
|
||||
builder.In("entity_id", entityIDs),
|
||||
builder.Eq{"entity_type": SubscriptionEntityProject},
|
||||
)
|
||||
}
|
||||
|
||||
if entityType == SubscriptionEntityTask {
|
||||
cond = builder.Or(
|
||||
return builder.Or(
|
||||
builder.And(
|
||||
builder.Eq{"entity_id": entityID},
|
||||
builder.In("entity_id", entityIDs),
|
||||
builder.Eq{"entity_type": SubscriptionEntityTask},
|
||||
),
|
||||
builder.And(
|
||||
builder.Eq{"entity_id": builder.
|
||||
Select("namespace_id").
|
||||
From("lists").
|
||||
Join("INNER", "tasks", "lists.id = tasks.list_id").
|
||||
Where(builder.Eq{"tasks.id": entityID}),
|
||||
},
|
||||
builder.Eq{"entity_type": SubscriptionEntityNamespace},
|
||||
),
|
||||
builder.And(
|
||||
builder.Eq{"entity_id": builder.
|
||||
Select("list_id").
|
||||
Select("project_id").
|
||||
From("tasks").
|
||||
Where(builder.Eq{"id": entityID}),
|
||||
Where(builder.In("id", entityIDs)),
|
||||
// TODO parent project
|
||||
},
|
||||
builder.Eq{"entity_type": SubscriptionEntityList},
|
||||
builder.Eq{"entity_type": SubscriptionEntityProject},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -225,56 +194,160 @@ func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int6
|
|||
|
||||
// GetSubscription returns a matching subscription for an entity and user.
|
||||
// It will return the next parent of a subscription. That means for tasks, it will first look for a subscription for
|
||||
// that task, if there is none it will look for a subscription on the list the task belongs to and if that also
|
||||
// doesn't exist it will check for a subscription for the namespace the list is belonging to.
|
||||
// that task, if there is none it will look for a subscription on the project the task belongs to.
|
||||
func GetSubscription(s *xorm.Session, entityType SubscriptionEntityType, entityID int64, a web.Auth) (subscription *Subscription, err error) {
|
||||
subs, err := GetSubscriptions(s, entityType, []int64{entityID}, a)
|
||||
if err != nil || len(subs) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
if sub, exists := subs[entityID]; exists {
|
||||
return sub, nil // Take exact match first, if available
|
||||
if sub, exists := subs[entityID]; exists && len(sub) > 0 {
|
||||
return sub[0], nil // Take exact match first, if available
|
||||
}
|
||||
for _, sub := range subs {
|
||||
return sub, nil // For parents, take next available
|
||||
if len(sub) > 0 {
|
||||
return sub[0], nil // For parents, take next available
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetSubscriptions returns a map of subscriptions to a set of given entity IDs
|
||||
func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entityIDs []int64, a web.Auth) (listsToSubscriptions map[int64]*Subscription, err error) {
|
||||
func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entityIDs []int64, a web.Auth) (projectsToSubscriptions map[int64][]*Subscription, err error) {
|
||||
u, is := a.(*user.User)
|
||||
if !is {
|
||||
if u != nil && !is {
|
||||
return
|
||||
}
|
||||
if err := entityType.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var entitiesFilter builder.Cond
|
||||
for _, eID := range entityIDs {
|
||||
if entitiesFilter == nil {
|
||||
entitiesFilter = getSubscriberCondForEntity(entityType, eID)
|
||||
continue
|
||||
switch entityType {
|
||||
case SubscriptionEntityProject:
|
||||
return getSubscriptionsForProjects(s, entityIDs, u)
|
||||
case SubscriptionEntityTask:
|
||||
subs, err := getSubscriptionsForTasks(s, entityIDs, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entitiesFilter = entitiesFilter.Or(getSubscriberCondForEntity(entityType, eID))
|
||||
|
||||
// If the task does not have a subscription directly or from its project, get the one
|
||||
// from the parent and return it instead.
|
||||
for _, eID := range entityIDs {
|
||||
if _, has := subs[eID]; has {
|
||||
continue
|
||||
}
|
||||
|
||||
task, err := GetTaskByIDSimple(s, eID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projectSubscriptions, err := getSubscriptionsForProjects(s, []int64{task.ProjectID}, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, subscription := range projectSubscriptions {
|
||||
subs[eID] = subscription // The first project subscription is the subscription we're looking for
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return subs, nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getSubscriptionsForProjects(s *xorm.Session, projectIDs []int64, u *user.User) (projectsToSubscriptions map[int64][]*Subscription, err error) {
|
||||
origEntityIDs := projectIDs
|
||||
var ps = make(map[int64]*Project)
|
||||
|
||||
for _, eID := range projectIDs {
|
||||
ps[eID], err = GetProjectSimpleByID(s, eID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = ps[eID].GetAllParentProjects(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parentIDs := []int64{}
|
||||
var parent = ps[eID].ParentProject
|
||||
for parent != nil {
|
||||
parentIDs = append(parentIDs, parent.ID)
|
||||
parent = parent.ParentProject
|
||||
}
|
||||
|
||||
// Now we have all parent ids
|
||||
projectIDs = append(projectIDs, parentIDs...) // the child project id is already in there
|
||||
}
|
||||
|
||||
var subscriptions []*Subscription
|
||||
err = s.
|
||||
Where("user_id = ?", u.ID).
|
||||
And(entitiesFilter).
|
||||
Find(&subscriptions)
|
||||
if u != nil {
|
||||
err = s.
|
||||
Where("user_id = ?", u.ID).
|
||||
And(getSubscriberCondForEntities(SubscriptionEntityProject, projectIDs)).
|
||||
Find(&subscriptions)
|
||||
} else {
|
||||
err = s.
|
||||
And(getSubscriberCondForEntities(SubscriptionEntityProject, projectIDs)).
|
||||
Find(&subscriptions)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listsToSubscriptions = make(map[int64]*Subscription)
|
||||
projectsToSubscriptions = make(map[int64][]*Subscription)
|
||||
for _, sub := range subscriptions {
|
||||
sub.Entity = sub.EntityType.String()
|
||||
listsToSubscriptions[sub.EntityID] = sub
|
||||
projectsToSubscriptions[sub.EntityID] = append(projectsToSubscriptions[sub.EntityID], sub)
|
||||
}
|
||||
return listsToSubscriptions, nil
|
||||
|
||||
// Rearrange so that subscriptions trickle down
|
||||
|
||||
for _, eID := range origEntityIDs {
|
||||
// If the current project does not have a subscription, climb up the tree until a project has one,
|
||||
// then use that subscription for all child projects
|
||||
_, has := projectsToSubscriptions[eID]
|
||||
if !has {
|
||||
var parent = ps[eID].ParentProject
|
||||
for parent != nil {
|
||||
sub, has := projectsToSubscriptions[parent.ID]
|
||||
projectsToSubscriptions[eID] = sub
|
||||
parent = parent.ParentProject
|
||||
if has { // reached the top of the tree
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return projectsToSubscriptions, nil
|
||||
}
|
||||
|
||||
func getSubscriptionsForTasks(s *xorm.Session, taskIDs []int64, u *user.User) (projectsToSubscriptions map[int64][]*Subscription, err error) {
|
||||
var subscriptions []*Subscription
|
||||
if u != nil {
|
||||
err = s.
|
||||
Where("user_id = ?", u.ID).
|
||||
And(getSubscriberCondForEntities(SubscriptionEntityTask, taskIDs)).
|
||||
Find(&subscriptions)
|
||||
} else {
|
||||
err = s.
|
||||
And(getSubscriberCondForEntities(SubscriptionEntityTask, taskIDs)).
|
||||
Find(&subscriptions)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectsToSubscriptions = make(map[int64][]*Subscription)
|
||||
for _, sub := range subscriptions {
|
||||
sub.Entity = sub.EntityType.String()
|
||||
projectsToSubscriptions[sub.EntityID] = append(projectsToSubscriptions[sub.EntityID], sub)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getSubscribersForEntity(s *xorm.Session, entityType SubscriptionEntityType, entityID int64) (subscriptions []*Subscription, err error) {
|
||||
|
@ -282,17 +355,18 @@ func getSubscribersForEntity(s *xorm.Session, entityType SubscriptionEntityType,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
cond := getSubscriberCondForEntity(entityType, entityID)
|
||||
err = s.
|
||||
Where(cond).
|
||||
Find(&subscriptions)
|
||||
subs, err := GetSubscriptions(s, entityType, []int64{entityID}, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userIDs := []int64{}
|
||||
for _, subscription := range subscriptions {
|
||||
userIDs = append(userIDs, subscription.UserID)
|
||||
subscriptions = make([]*Subscription, 0, len(subs))
|
||||
for _, subss := range subs {
|
||||
for _, subscription := range subss {
|
||||
userIDs = append(userIDs, subscription.UserID)
|
||||
subscriptions = append(subscriptions, subscription)
|
||||
}
|
||||
}
|
||||
|
||||
users, err := user.GetUsersByIDs(s, userIDs)
|
||||
|
|
|
@ -30,11 +30,8 @@ func (sb *Subscription) CanCreate(s *xorm.Session, a web.Auth) (can bool, err er
|
|||
sb.EntityType = getEntityTypeFromString(sb.Entity)
|
||||
|
||||
switch sb.EntityType {
|
||||
case SubscriptionEntityNamespace:
|
||||
n := &Namespace{ID: sb.EntityID}
|
||||
can, _, err = n.CanRead(s, a)
|
||||
case SubscriptionEntityList:
|
||||
l := &List{ID: sb.EntityID}
|
||||
case SubscriptionEntityProject:
|
||||
l := &Project{ID: sb.EntityID}
|
||||
can, _, err = l.CanRead(s, a)
|
||||
case SubscriptionEntityTask:
|
||||
t := &Task{ID: sb.EntityID}
|
||||
|
|
|
@ -25,13 +25,9 @@ import (
|
|||
)
|
||||
|
||||
func TestSubscriptionGetTypeFromString(t *testing.T) {
|
||||
t.Run("namespace", func(t *testing.T) {
|
||||
entityType := getEntityTypeFromString("namespace")
|
||||
assert.Equal(t, SubscriptionEntityType(SubscriptionEntityNamespace), entityType)
|
||||
})
|
||||
t.Run("list", func(t *testing.T) {
|
||||
entityType := getEntityTypeFromString("list")
|
||||
assert.Equal(t, SubscriptionEntityType(SubscriptionEntityList), entityType)
|
||||
t.Run("project", func(t *testing.T) {
|
||||
entityType := getEntityTypeFromString("project")
|
||||
assert.Equal(t, SubscriptionEntityType(SubscriptionEntityProject), entityType)
|
||||
})
|
||||
t.Run("task", func(t *testing.T) {
|
||||
entityType := getEntityTypeFromString("task")
|
||||
|
@ -88,36 +84,20 @@ func TestSubscription_Create(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("noneixsting namespace", func(t *testing.T) {
|
||||
t.Run("noneixsting project", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "namespace",
|
||||
Entity: "project",
|
||||
EntityID: 99999999,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanCreate(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("noneixsting list", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "list",
|
||||
EntityID: 99999999,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanCreate(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
assert.True(t, IsErrProjectDoesNotExist(err))
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("noneixsting task", func(t *testing.T) {
|
||||
|
@ -136,28 +116,13 @@ func TestSubscription_Create(t *testing.T) {
|
|||
assert.True(t, IsErrTaskDoesNotExist(err))
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("no rights to see namespace", func(t *testing.T) {
|
||||
t.Run("no rights to see project", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "namespace",
|
||||
EntityID: 6,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanCreate(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("no rights to see list", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "list",
|
||||
Entity: "project",
|
||||
EntityID: 20,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
@ -268,22 +233,12 @@ func TestSubscriptionGet(t *testing.T) {
|
|||
u := &user.User{ID: 6}
|
||||
|
||||
t.Run("test each individually", func(t *testing.T) {
|
||||
t.Run("namespace", func(t *testing.T) {
|
||||
t.Run("project", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sub, err := GetSubscription(s, SubscriptionEntityNamespace, 6, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sub)
|
||||
assert.Equal(t, int64(2), sub.ID)
|
||||
})
|
||||
t.Run("list", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sub, err := GetSubscription(s, SubscriptionEntityList, 12, u)
|
||||
sub, err := GetSubscription(s, SubscriptionEntityProject, 12, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sub)
|
||||
assert.Equal(t, int64(3), sub.ID)
|
||||
|
@ -300,38 +255,51 @@ func TestSubscriptionGet(t *testing.T) {
|
|||
})
|
||||
})
|
||||
t.Run("inherited", func(t *testing.T) {
|
||||
t.Run("list from namespace", func(t *testing.T) {
|
||||
t.Run("project from parent", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
// List 6 belongs to namespace 6 where user 6 has subscribed to
|
||||
sub, err := GetSubscription(s, SubscriptionEntityList, 6, u)
|
||||
// Project 25 belongs to project 12 where user 6 has subscribed to
|
||||
sub, err := GetSubscription(s, SubscriptionEntityProject, 25, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sub)
|
||||
assert.Equal(t, int64(2), sub.ID)
|
||||
assert.Equal(t, int64(12), sub.EntityID)
|
||||
assert.Equal(t, int64(3), sub.ID)
|
||||
})
|
||||
t.Run("task from namespace", func(t *testing.T) {
|
||||
t.Run("project from parent's parent", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
// Task 20 belongs to list 11 which belongs to namespace 6 where the user has subscribed
|
||||
sub, err := GetSubscription(s, SubscriptionEntityTask, 20, u)
|
||||
// Project 26 belongs to project 25 which belongs to project 12 where user 6 has subscribed to
|
||||
sub, err := GetSubscription(s, SubscriptionEntityProject, 26, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sub)
|
||||
assert.Equal(t, int64(2), sub.ID)
|
||||
assert.Equal(t, int64(12), sub.EntityID)
|
||||
assert.Equal(t, int64(3), sub.ID)
|
||||
})
|
||||
t.Run("task from list", func(t *testing.T) {
|
||||
t.Run("task from parent", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
// Task 21 belongs to list 12 which the user has subscribed to
|
||||
// Task 39 belongs to project 25 which belongs to project 12 where the user has subscribed
|
||||
sub, err := GetSubscription(s, SubscriptionEntityTask, 39, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sub)
|
||||
// assert.Equal(t, int64(2), sub.ID) TODO
|
||||
})
|
||||
t.Run("task from project", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
// Task 21 belongs to project 32 which the user has subscribed to
|
||||
sub, err := GetSubscription(s, SubscriptionEntityTask, 21, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sub)
|
||||
assert.Equal(t, int64(3), sub.ID)
|
||||
assert.Equal(t, int64(8), sub.ID)
|
||||
})
|
||||
})
|
||||
t.Run("invalid type", func(t *testing.T) {
|
||||
|
|
|
@ -33,7 +33,7 @@ import (
|
|||
// TaskAssginee represents an assignment of a user to a task
|
||||
type TaskAssginee struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"-"`
|
||||
TaskID int64 `xorm:"bigint INDEX not null" json:"-" param:"listtask"`
|
||||
TaskID int64 `xorm:"bigint INDEX not null" json:"-" param:"projecttask"`
|
||||
UserID int64 `xorm:"bigint INDEX not null" json:"user_id" param:"user"`
|
||||
Created time.Time `xorm:"created not null"`
|
||||
|
||||
|
@ -102,10 +102,10 @@ func (t *Task) updateTaskAssignees(s *xorm.Session, assignees []*user.User, doer
|
|||
for _, oldAssignee := range t.Assignees {
|
||||
found = false
|
||||
if newAssignees[oldAssignee.ID] != nil {
|
||||
found = true // If a new assignee is already in the list with old assignees
|
||||
found = true // If a new assignee is already in the project with old assignees
|
||||
}
|
||||
|
||||
// Put all assignees which are only on the old list to the trash
|
||||
// Put all assignees which are only on the old project to the trash
|
||||
if !found {
|
||||
assigneesToDelete = append(assigneesToDelete, oldAssignee.ID)
|
||||
}
|
||||
|
@ -123,8 +123,8 @@ func (t *Task) updateTaskAssignees(s *xorm.Session, assignees []*user.User, doer
|
|||
}
|
||||
}
|
||||
|
||||
// Get the list to perform later checks
|
||||
list, err := GetListSimpleByID(s, t.ListID)
|
||||
// Get the project to perform later checks
|
||||
project, err := GetProjectSimpleByID(s, t.ProjectID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -138,7 +138,7 @@ func (t *Task) updateTaskAssignees(s *xorm.Session, assignees []*user.User, doer
|
|||
}
|
||||
|
||||
// Add the new assignee
|
||||
err = t.addNewAssigneeByID(s, u.ID, list, doer)
|
||||
err = t.addNewAssigneeByID(s, u.ID, project, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ func (t *Task) updateTaskAssignees(s *xorm.Session, assignees []*user.User, doer
|
|||
|
||||
t.setTaskAssignees(assignees)
|
||||
|
||||
err = updateListLastUpdated(s, &List{ID: t.ListID})
|
||||
err = updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -178,13 +178,13 @@ func (la *TaskAssginee) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
err = updateListByTaskID(s, la.TaskID)
|
||||
err = updateProjectByTaskID(s, la.TaskID)
|
||||
return
|
||||
}
|
||||
|
||||
// Create adds a new assignee to a task
|
||||
// @Summary Add a new assignee to a task
|
||||
// @Description Adds a new assignee to a task. The assignee needs to have access to the list, the doer must be able to edit this task.
|
||||
// @Description Adds a new assignee to a task. The assignee needs to have access to the project, the doer must be able to edit this task.
|
||||
// @tags assignees
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
@ -197,28 +197,28 @@ func (la *TaskAssginee) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Router /tasks/{taskID}/assignees [put]
|
||||
func (la *TaskAssginee) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Get the list to perform later checks
|
||||
list, err := GetListSimplByTaskID(s, la.TaskID)
|
||||
// Get the project to perform later checks
|
||||
project, err := GetProjectSimplByTaskID(s, la.TaskID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
task := &Task{ID: la.TaskID}
|
||||
return task.addNewAssigneeByID(s, la.UserID, list, a)
|
||||
return task.addNewAssigneeByID(s, la.UserID, project, a)
|
||||
}
|
||||
|
||||
func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, list *List, auth web.Auth) (err error) {
|
||||
// Check if the user exists and has access to the list
|
||||
func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, project *Project, auth web.Auth) (err error) {
|
||||
// Check if the user exists and has access to the project
|
||||
newAssignee, err := user.GetUserByID(s, newAssigneeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
canRead, _, err := list.CanRead(s, newAssignee)
|
||||
canRead, _, err := project.CanRead(s, newAssignee)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canRead {
|
||||
return ErrUserDoesNotHaveAccessToList{list.ID, newAssigneeID}
|
||||
return ErrUserDoesNotHaveAccessToProject{project.ID, newAssigneeID}
|
||||
}
|
||||
|
||||
exist, err := s.
|
||||
|
@ -252,7 +252,7 @@ func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, list *Li
|
|||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, &List{ID: t.ListID})
|
||||
err = updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -271,7 +271,7 @@ func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, list *Li
|
|||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/assignees [get]
|
||||
func (la *TaskAssginee) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
task, err := GetListSimplByTaskID(s, la.TaskID)
|
||||
task, err := GetProjectSimplByTaskID(s, la.TaskID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
@ -310,9 +310,9 @@ func (la *TaskAssginee) ReadAll(s *xorm.Session, a web.Auth, search string, page
|
|||
|
||||
// BulkAssignees is a helper struct used to update multiple assignees at once.
|
||||
type BulkAssignees struct {
|
||||
// A list with all assignees
|
||||
// A project with all assignees
|
||||
Assignees []*user.User `json:"assignees"`
|
||||
TaskID int64 `json:"-" param:"listtask"`
|
||||
TaskID int64 `json:"-" param:"projecttask"`
|
||||
|
||||
web.CRUDable `json:"-"`
|
||||
web.Rights `json:"-"`
|
||||
|
@ -320,7 +320,7 @@ type BulkAssignees struct {
|
|||
|
||||
// Create adds new assignees to a task
|
||||
// @Summary Add multiple new assignees to a task
|
||||
// @Description Adds multiple new assignees to a task. The assignee needs to have access to the list, the doer must be able to edit this task. Every user not in the list will be unassigned from the task, pass an empty array to unassign everyone.
|
||||
// @Description Adds multiple new assignees to a task. The assignee needs to have access to the project, the doer must be able to edit this task. Every user not in the project will be unassigned from the task, pass an empty array to unassign everyone.
|
||||
// @tags assignees
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
|
|
@ -37,10 +37,10 @@ func (la *TaskAssginee) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
}
|
||||
|
||||
func canDoTaskAssingee(s *xorm.Session, taskID int64, a web.Auth) (bool, error) {
|
||||
// Check if the current user can edit the list
|
||||
list, err := GetListSimplByTaskID(s, taskID)
|
||||
// Check if the current user can edit the project
|
||||
project, err := GetProjectSimplByTaskID(s, taskID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return list.CanUpdate(s, a)
|
||||
return project.CanUpdate(s, a)
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@ func (ta *TaskAttachment) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// ReadAll returns a list with all attachments
|
||||
// ReadAll returns a project with all attachments
|
||||
// @Summary Get all attachments for one task.
|
||||
// @Description Get all task attachments for one task.
|
||||
// @tags task
|
||||
|
|
|
@ -24,8 +24,7 @@ import (
|
|||
|
||||
// TaskCollection is a struct used to hold filter details and not clutter the Task struct with information not related to actual tasks.
|
||||
type TaskCollection struct {
|
||||
ListID int64 `param:"list" json:"-"`
|
||||
Lists []*List `json:"-"`
|
||||
ProjectID int64 `param:"project" json:"-"`
|
||||
|
||||
// The query parameter to sort by. This is for ex. done, priority, etc.
|
||||
SortBy []string `query:"sort_by" json:"sort_by"`
|
||||
|
@ -62,7 +61,7 @@ func validateTaskField(fieldName string) error {
|
|||
taskPropertyDoneAt,
|
||||
taskPropertyDueDate,
|
||||
taskPropertyCreatedByID,
|
||||
taskPropertyListID,
|
||||
taskPropertyProjectID,
|
||||
taskPropertyRepeatAfter,
|
||||
taskPropertyPriority,
|
||||
taskPropertyStartDate,
|
||||
|
@ -120,16 +119,16 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskOptions, err
|
|||
}
|
||||
|
||||
// ReadAll gets all tasks for a collection
|
||||
// @Summary Get tasks in a list
|
||||
// @Description Returns all tasks for the current list.
|
||||
// @Summary Get tasks in a project
|
||||
// @Description Returns all tasks for the current project.
|
||||
// @tags task
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param listID path int true "The list ID."
|
||||
// @Param projectID path int true "The project ID."
|
||||
// @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 s query string false "Search tasks by task text."
|
||||
// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`."
|
||||
// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `project_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`."
|
||||
// @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`."
|
||||
// @Param filter_by query string false "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match."
|
||||
// @Param filter_value query string false "The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)- or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style relative dates for all date fields like `due_date`, `start_date`, `end_date`, etc."
|
||||
|
@ -139,13 +138,13 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskOptions, err
|
|||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.Task "The tasks"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/tasks [get]
|
||||
// @Router /projects/{projectID}/tasks [get]
|
||||
func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
|
||||
|
||||
// If the list id is < -1 this means we're dealing with a saved filter - in that case we get and populate the filter
|
||||
// -1 is the favorites list which works as intended
|
||||
if tf.ListID < -1 {
|
||||
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromListID(tf.ListID))
|
||||
// If the project id is < -1 this means we're dealing with a saved filter - in that case we get and populate the filter
|
||||
// -1 is the favorites project which works as intended
|
||||
if tf.ProjectID < -1 {
|
||||
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(tf.ProjectID))
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
@ -169,19 +168,20 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
|||
|
||||
shareAuth, is := a.(*LinkSharing)
|
||||
if is {
|
||||
list, err := GetListSimpleByID(s, shareAuth.ListID)
|
||||
project, err := GetProjectSimpleByID(s, shareAuth.ProjectID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
return getTasksForLists(s, []*List{list}, a, taskopts)
|
||||
return getTasksForProjects(s, []*Project{project}, a, taskopts)
|
||||
}
|
||||
|
||||
// If the list ID is not set, we get all tasks for the user.
|
||||
// If the project 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.
|
||||
if tf.ListID == 0 {
|
||||
tf.Lists, _, _, err = getRawListsForUser(
|
||||
var projects []*Project
|
||||
if tf.ProjectID == 0 {
|
||||
projects, _, _, err = getRawProjectsForUser(
|
||||
s,
|
||||
&listOptions{
|
||||
&projectOptions{
|
||||
user: &user.User{ID: a.GetID()},
|
||||
page: -1,
|
||||
},
|
||||
|
@ -190,17 +190,17 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
|||
return nil, 0, 0, err
|
||||
}
|
||||
} else {
|
||||
// Check the list exists and the user has acess on it
|
||||
list := &List{ID: tf.ListID}
|
||||
canRead, _, err := list.CanRead(s, a)
|
||||
// Check the project exists and the user has access on it
|
||||
project := &Project{ID: tf.ProjectID}
|
||||
canRead, _, err := project.CanRead(s, a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
if !canRead {
|
||||
return nil, 0, 0, ErrUserDoesNotHaveAccessToList{ListID: tf.ListID}
|
||||
return nil, 0, 0, ErrUserDoesNotHaveAccessToProject{ProjectID: tf.ProjectID}
|
||||
}
|
||||
tf.Lists = []*List{{ID: tf.ListID}}
|
||||
projects = []*Project{{ID: tf.ProjectID}}
|
||||
}
|
||||
|
||||
return getTasksForLists(s, tf.Lists, a, taskopts)
|
||||
return getTasksForProjects(s, projects, a, taskopts)
|
||||
}
|
||||
|
|
|
@ -237,24 +237,6 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
|
|||
|
||||
realFieldName := strings.ReplaceAll(strcase.ToCamel(fieldName), "Id", "ID")
|
||||
|
||||
if realFieldName == "Namespace" {
|
||||
if comparator == taskFilterComparatorIn {
|
||||
vals := strings.Split(value, ",")
|
||||
valueSlice := []interface{}{}
|
||||
for _, val := range vals {
|
||||
v, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
valueSlice = append(valueSlice, v)
|
||||
}
|
||||
return nil, valueSlice, nil
|
||||
}
|
||||
|
||||
nativeValue, err = strconv.ParseInt(value, 10, 64)
|
||||
return
|
||||
}
|
||||
|
||||
if realFieldName == "Assignees" {
|
||||
vals := strings.Split(value, ",")
|
||||
valueSlice := append([]string{}, vals...)
|
||||
|
|
|
@ -33,7 +33,7 @@ const (
|
|||
taskPropertyDoneAt string = "done_at"
|
||||
taskPropertyDueDate string = "due_date"
|
||||
taskPropertyCreatedByID string = "created_by_id"
|
||||
taskPropertyListID string = "list_id"
|
||||
taskPropertyProjectID string = "project_id"
|
||||
taskPropertyRepeatAfter string = "repeat_after"
|
||||
taskPropertyPriority string = "priority"
|
||||
taskPropertyStartDate string = "start_date"
|
||||
|
|
|
@ -50,7 +50,7 @@ func TestSortParamValidation(t *testing.T) {
|
|||
taskPropertyDoneAt,
|
||||
taskPropertyDueDate,
|
||||
taskPropertyCreatedByID,
|
||||
taskPropertyListID,
|
||||
taskPropertyProjectID,
|
||||
taskPropertyRepeatAfter,
|
||||
taskPropertyPriority,
|
||||
taskPropertyStartDate,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -90,7 +91,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 1,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
BucketID: 1,
|
||||
IsFavorite: true,
|
||||
Position: 2,
|
||||
|
@ -104,7 +105,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Title: "task #29 with parent task (1)",
|
||||
Index: 14,
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
BucketID: 1,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -162,7 +163,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Done: true,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
BucketID: 1,
|
||||
Position: 4,
|
||||
Labels: []*Label{
|
||||
|
@ -182,7 +183,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 3,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -196,7 +197,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 4,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -210,7 +211,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 5,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -224,7 +225,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 6,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -238,7 +239,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 7,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -252,7 +253,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 8,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -266,7 +267,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 9,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 1,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -281,7 +282,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 10,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 1,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -294,7 +295,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 11,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 1,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -307,7 +308,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 12,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 1,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -320,7 +321,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 6,
|
||||
ProjectID: 6,
|
||||
IsFavorite: true,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 6,
|
||||
|
@ -334,7 +335,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 7,
|
||||
ProjectID: 7,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 7,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -347,7 +348,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 8,
|
||||
ProjectID: 8,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 8,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -360,7 +361,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 9,
|
||||
ProjectID: 9,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 9,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -373,7 +374,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 10,
|
||||
ProjectID: 10,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 10,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -386,7 +387,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 11,
|
||||
ProjectID: 11,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 11,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -395,11 +396,11 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
task21 := &Task{
|
||||
ID: 21,
|
||||
Title: "task #21",
|
||||
Identifier: "test12-1",
|
||||
Identifier: "-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 12,
|
||||
ProjectID: 32, // parent project is shared to user 1 via direct share
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 12,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -408,26 +409,26 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
task22 := &Task{
|
||||
ID: 22,
|
||||
Title: "task #22",
|
||||
Identifier: "test13-1",
|
||||
Identifier: "-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 13,
|
||||
ProjectID: 33,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 13,
|
||||
BucketID: 36,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
}
|
||||
task23 := &Task{
|
||||
ID: 23,
|
||||
Title: "task #23",
|
||||
Identifier: "test14-1",
|
||||
Identifier: "-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 14,
|
||||
ProjectID: 34,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 14,
|
||||
BucketID: 37,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
}
|
||||
|
@ -438,7 +439,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 15,
|
||||
ProjectID: 15, // parent project is shared to user 1 via team
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 15,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -451,7 +452,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 16,
|
||||
ProjectID: 16,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 16,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -464,7 +465,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 17,
|
||||
ProjectID: 17,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 17,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -481,7 +482,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
time.Unix(1543626724, 0).In(loc),
|
||||
time.Unix(1543626824, 0).In(loc),
|
||||
},
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
BucketID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -494,7 +495,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 13,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
RepeatAfter: 3600,
|
||||
BucketID: 1,
|
||||
|
@ -508,7 +509,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 14,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{
|
||||
RelationKindParenttask: {
|
||||
{
|
||||
|
@ -517,7 +518,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Description: "Lorem Ipsum",
|
||||
Index: 1,
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
IsFavorite: true,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -537,7 +538,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 15,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
Assignees: []*user.User{
|
||||
user1,
|
||||
user2,
|
||||
|
@ -555,7 +556,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
HexColor: "f0f0f0",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 1,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -568,7 +569,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 1,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 3,
|
||||
ProjectID: 3,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 21,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -581,7 +582,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Index: 17,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
PercentDone: 0.5,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 1,
|
||||
|
@ -590,10 +591,10 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
}
|
||||
|
||||
type fields struct {
|
||||
ListID int64
|
||||
Lists []*List
|
||||
SortBy []string // Is a string, since this is the place where a query string comes from the user
|
||||
OrderBy []string
|
||||
ProjectID int64
|
||||
Projects []*Project
|
||||
SortBy []string // Is a string, since this is the place where a query string comes from the user
|
||||
OrderBy []string
|
||||
|
||||
FilterBy []string
|
||||
FilterValue []string
|
||||
|
@ -612,7 +613,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
name string
|
||||
fields fields
|
||||
args args
|
||||
want interface{}
|
||||
want []*Task
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
|
@ -664,7 +665,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
},
|
||||
{
|
||||
// For more sorting tests see task_collection_sort_test.go
|
||||
name: "ReadAll Tasks sorted by done asc and id desc",
|
||||
name: "sorted by done asc and id desc",
|
||||
fields: fields{
|
||||
SortBy: []string{"done", "id"},
|
||||
OrderBy: []string{"asc", "desc"},
|
||||
|
@ -787,11 +788,13 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
task19,
|
||||
task20,
|
||||
task21,
|
||||
|
||||
task22,
|
||||
task23,
|
||||
task24,
|
||||
task25,
|
||||
task26,
|
||||
|
||||
task27,
|
||||
task28,
|
||||
task29,
|
||||
|
@ -876,12 +879,12 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
name: "favorited tasks",
|
||||
args: defaultArgs,
|
||||
fields: fields{
|
||||
ListID: FavoritesPseudoList.ID,
|
||||
ProjectID: FavoritesPseudoProject.ID,
|
||||
},
|
||||
want: []*Task{
|
||||
task1,
|
||||
task15,
|
||||
// Task 34 is also a favorite, but on a list user 1 has no access to.
|
||||
// Task 34 is also a favorite, but on a project user 1 has no access to.
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -1042,9 +1045,9 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter list",
|
||||
name: "filter project",
|
||||
fields: fields{
|
||||
FilterBy: []string{"list_id"},
|
||||
FilterBy: []string{"project_id"},
|
||||
FilterValue: []string{"6"},
|
||||
FilterComparator: []string{"equals"},
|
||||
},
|
||||
|
@ -1054,33 +1057,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter namespace",
|
||||
fields: fields{
|
||||
FilterBy: []string{"namespace"},
|
||||
FilterValue: []string{"7"},
|
||||
FilterComparator: []string{"equals"},
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task21,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter namespace in",
|
||||
fields: fields{
|
||||
FilterBy: []string{"namespace"},
|
||||
FilterValue: []string{"7,8"},
|
||||
FilterComparator: []string{"in"},
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task21,
|
||||
task22,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
// TODO filter parent project?
|
||||
{
|
||||
name: "filter by index",
|
||||
fields: fields{
|
||||
|
@ -1200,9 +1177,9 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "saved filter with sort order",
|
||||
fields: fields{
|
||||
ListID: -2,
|
||||
SortBy: []string{"title", "id"},
|
||||
OrderBy: []string{"desc", "asc"},
|
||||
ProjectID: -2,
|
||||
SortBy: []string{"title", "id"},
|
||||
OrderBy: []string{"desc", "asc"},
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 1},
|
||||
|
@ -1224,9 +1201,9 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
defer s.Close()
|
||||
|
||||
lt := &TaskCollection{
|
||||
ListID: tt.fields.ListID,
|
||||
SortBy: tt.fields.SortBy,
|
||||
OrderBy: tt.fields.OrderBy,
|
||||
ProjectID: tt.fields.ProjectID,
|
||||
SortBy: tt.fields.SortBy,
|
||||
OrderBy: tt.fields.OrderBy,
|
||||
|
||||
FilterBy: tt.fields.FilterBy,
|
||||
FilterValue: tt.fields.FilterValue,
|
||||
|
@ -1242,11 +1219,35 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if diff, equal := messagediff.PrettyDiff(got, tt.want); !equal {
|
||||
if len(got.([]*Task)) == 0 && len(tt.want.([]*Task)) == 0 {
|
||||
var is bool
|
||||
var gotTasks []*Task
|
||||
gotTasks, is = got.([]*Task)
|
||||
if !is {
|
||||
gotTasks = []*Task{}
|
||||
}
|
||||
if len(gotTasks) == 0 && len(tt.want) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
t.Errorf("Test %s, Task.ReadAll() = %v, want %v, \ndiff: %v", tt.name, got, tt.want, diff)
|
||||
gotIDs := []int64{}
|
||||
for _, t := range got.([]*Task) {
|
||||
gotIDs = append(gotIDs, t.ID)
|
||||
}
|
||||
|
||||
wantIDs := []int64{}
|
||||
for _, t := range tt.want {
|
||||
wantIDs = append(wantIDs, t.ID)
|
||||
}
|
||||
sort.Slice(wantIDs, func(i, j int) bool {
|
||||
return wantIDs[i] < wantIDs[j]
|
||||
})
|
||||
sort.Slice(gotIDs, func(i, j int) bool {
|
||||
return gotIDs[i] < gotIDs[j]
|
||||
})
|
||||
|
||||
diffIDs, _ := messagediff.PrettyDiff(gotIDs, wantIDs)
|
||||
|
||||
t.Errorf("Test %s, Task.ReadAll() = %v, want %v, \ndiff: %v \n\n diffIDs: %v", tt.name, got, tt.want, diff, diffIDs)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ func TestTaskComment_Create(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
tc := &TaskComment{
|
||||
Comment: "Lorem Ipsum @user2",
|
||||
TaskID: 32, // user2 has access to the list that task belongs to
|
||||
TaskID: 32, // user2 has access to the project that task belongs to
|
||||
}
|
||||
err = tc.Create(s, u)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -37,9 +37,8 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
|
|||
|
||||
var tasks []*Task
|
||||
err = s.
|
||||
Where("due_date is not null AND due_date < ? AND lists.is_archived = false AND namespaces.is_archived = false", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
|
||||
Join("LEFT", "lists", "lists.id = tasks.list_id").
|
||||
Join("LEFT", "namespaces", "lists.namespace_id = namespaces.id").
|
||||
Where("due_date is not null AND due_date < ? AND projects.is_archived = false", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
|
||||
Join("LEFT", "projects", "projects.id = tasks.project_id").
|
||||
And("done = false").
|
||||
Find(&tasks)
|
||||
if err != nil {
|
||||
|
@ -80,7 +79,7 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
|
|||
tzs[t.User.Timezone] = tz
|
||||
}
|
||||
|
||||
// If it is time for that current user, add the task to their list of overdue tasks
|
||||
// If it is time for that current user, add the task to their project of overdue tasks
|
||||
tm, err := time.Parse("15:04", t.User.OverdueTasksRemindersTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -108,7 +108,7 @@ type RelatedTaskMap map[RelationKind][]*Task
|
|||
|
||||
// Create creates a new task relation
|
||||
// @Summary Create a new relation between two tasks
|
||||
// @Description Creates a new relation between two tasks. The user needs to have update rights on the base task and at least read rights on the other task. Both tasks do not need to be on the same list. Take a look at the docs for available task relation kinds.
|
||||
// @Description Creates a new relation between two tasks. The user needs to have update rights on the base task and at least read rights on the other task. Both tasks do not need to be on the same project. Take a look at the docs for available task relation kinds.
|
||||
// @tags task
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
|
|
@ -42,7 +42,7 @@ func (rel *TaskRelation) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
// We explicitly don't check if the two tasks are on the same list.
|
||||
// We explicitly don't check if the two tasks are on the same project.
|
||||
otherTask := &Task{ID: rel.OtherTaskID}
|
||||
has, _, err = otherTask.CanRead(s, a)
|
||||
if err != nil {
|
||||
|
|
|
@ -46,7 +46,7 @@ func TestTaskRelation_Create(t *testing.T) {
|
|||
"created_by_id": 1,
|
||||
}, false)
|
||||
})
|
||||
t.Run("Two Tasks In Different Lists", func(t *testing.T) {
|
||||
t.Run("Two Tasks In Different Projects", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
@ -150,14 +150,14 @@ func TestTaskRelation_CanCreate(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
})
|
||||
t.Run("Two tasks on different lists", func(t *testing.T) {
|
||||
t.Run("Two tasks on different projects", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
rel := TaskRelation{
|
||||
TaskID: 1,
|
||||
OtherTaskID: 13,
|
||||
OtherTaskID: 32,
|
||||
RelationKind: RelationKindSubtask,
|
||||
}
|
||||
can, err := rel.CanCreate(s, &user.User{ID: 1})
|
||||
|
|
|
@ -47,11 +47,11 @@ const (
|
|||
TaskRepeatModeFromCurrentDate
|
||||
)
|
||||
|
||||
// Task represents an task in a todolist
|
||||
// Task represents an task in a project
|
||||
type Task struct {
|
||||
// The unique, numeric id of this task.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"listtask"`
|
||||
// The task text. This is what you'll see in the list.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"projecttask"`
|
||||
// The task text. This is what you'll see in the project.
|
||||
Title string `xorm:"TEXT not null" json:"title" valid:"minstringlength(1)" minLength:"1"`
|
||||
// The task description.
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
|
@ -63,8 +63,8 @@ type Task struct {
|
|||
DueDate time.Time `xorm:"DATETIME INDEX null 'due_date'" json:"due_date"`
|
||||
// An array of datetimes when the user wants to be reminded of the task.
|
||||
Reminders []time.Time `xorm:"-" json:"reminder_dates"`
|
||||
// The list this task belongs to.
|
||||
ListID int64 `xorm:"bigint INDEX not null" json:"list_id" param:"list"`
|
||||
// The project this task belongs to.
|
||||
ProjectID int64 `xorm:"bigint INDEX not null" json:"project_id" param:"project"`
|
||||
// An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then increase all remindes and the due date by its amount.
|
||||
RepeatAfter int64 `xorm:"bigint INDEX null" json:"repeat_after" valid:"range(0|9223372036854775807)"`
|
||||
// Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.
|
||||
|
@ -84,9 +84,9 @@ type Task struct {
|
|||
// Determines how far a task is left from being done
|
||||
PercentDone float64 `xorm:"DOUBLE null" json:"percent_done"`
|
||||
|
||||
// The task identifier, based on the list identifier and the task's index
|
||||
// The task identifier, based on the project identifier and the task's index
|
||||
Identifier string `xorm:"-" json:"identifier"`
|
||||
// The task index, calculated per list
|
||||
// The task index, calculated per project
|
||||
Index int64 `xorm:"bigint not null default 0" json:"index"`
|
||||
|
||||
// The UID is currently not used for anything other than caldav, which is why we don't expose it over json
|
||||
|
@ -101,7 +101,7 @@ type Task struct {
|
|||
// If this task has a cover image, the field will return the id of the attachment that is the cover image.
|
||||
CoverImageAttachmentID int64 `xorm:"bigint default 0" json:"cover_image_attachment_id"`
|
||||
|
||||
// True if a task is a favorite task. Favorite tasks show up in a separate "Important" list. This value depends on the user making the call to the api.
|
||||
// True if a task is a favorite task. Favorite tasks show up in a separate "Important" project. This value depends on the user making the call to the api.
|
||||
IsFavorite bool `xorm:"-" json:"is_favorite"`
|
||||
|
||||
// The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.
|
||||
|
@ -116,7 +116,7 @@ type Task struct {
|
|||
// BucketID is the ID of the kanban bucket this task belongs to.
|
||||
BucketID int64 `xorm:"bigint null" json:"bucket_id"`
|
||||
|
||||
// The position of the task - any task list can be sorted as usual by this parameter.
|
||||
// The position of the task - any task project can be sorted as usual by this parameter.
|
||||
// When accessing tasks via kanban buckets, this is primarily used to sort them based on a range
|
||||
// We're using a float64 here to make it possible to put any task within any two other tasks (by changing the number).
|
||||
// You would calculate the new position between two tasks with something like task3.position = (task2.position - task1.position) / 2.
|
||||
|
@ -128,7 +128,7 @@ type Task struct {
|
|||
|
||||
// The user who initially created the task.
|
||||
CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"`
|
||||
CreatedByID int64 `xorm:"bigint not null" json:"-"` // ID of the user who put that task on the list
|
||||
CreatedByID int64 `xorm:"bigint not null" json:"-"` // ID of the user who put that task on the project
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
|
@ -139,7 +139,7 @@ type TaskWithComments struct {
|
|||
Comments []*TaskComment `xorm:"-" json:"comments"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for listtasks
|
||||
// TableName returns the table name for projecttasks
|
||||
func (Task) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
@ -176,14 +176,14 @@ type taskOptions struct {
|
|||
|
||||
// ReadAll is a dummy function to still have that endpoint documented
|
||||
// @Summary Get tasks
|
||||
// @Description Returns all tasks on any list the user has access to.
|
||||
// @Description Returns all tasks on any project the user has access to.
|
||||
// @tags task
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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 s query string false "Search tasks by task text."
|
||||
// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`."
|
||||
// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `project_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`."
|
||||
// @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`."
|
||||
// @Param filter_by query string false "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match."
|
||||
// @Param filter_value query string false "The value to filter for."
|
||||
|
@ -263,10 +263,10 @@ func getTaskIndexFromSearchString(s string) (index int64) {
|
|||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
||||
func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
||||
|
||||
// If the user does not have any lists, don't try to get any tasks
|
||||
if len(lists) == 0 {
|
||||
// If the user does not have any projects, don't try to get any tasks
|
||||
if len(projects) == 0 {
|
||||
return nil, 0, 0, nil
|
||||
}
|
||||
|
||||
|
@ -275,15 +275,15 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
opts.filterConcat = filterConcatOr
|
||||
}
|
||||
|
||||
// Get all list IDs and get the tasks
|
||||
var listIDs []int64
|
||||
var hasFavoritesList bool
|
||||
for _, l := range lists {
|
||||
if l.ID == FavoritesPseudoList.ID {
|
||||
hasFavoritesList = true
|
||||
// Get all project IDs and get the tasks
|
||||
var projectIDs []int64
|
||||
var hasFavoritesProject bool
|
||||
for _, l := range projects {
|
||||
if l.ID == FavoritesPseudoProject.ID {
|
||||
hasFavoritesProject = true
|
||||
continue
|
||||
}
|
||||
listIDs = append(listIDs, l.ID)
|
||||
projectIDs = append(projectIDs, l.ID)
|
||||
}
|
||||
|
||||
// Add the id parameter as the last parameter to sorty by default, but only if it is not already passed as the last parameter.
|
||||
|
@ -329,7 +329,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
reminderFilters := []builder.Cond{}
|
||||
assigneeFilters := []builder.Cond{}
|
||||
labelFilters := []builder.Cond{}
|
||||
namespaceFilters := []builder.Cond{}
|
||||
projectFilters := []builder.Cond{}
|
||||
|
||||
var filters = make([]builder.Cond, 0, len(opts.filters))
|
||||
// To still find tasks with nil values, we exclude 0s when comparing with >/< values.
|
||||
|
@ -367,13 +367,13 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
continue
|
||||
}
|
||||
|
||||
if f.field == "namespace" || f.field == "namespace_id" {
|
||||
f.field = "namespace_id"
|
||||
if f.field == "parent_project" || f.field == "parent_project_id" {
|
||||
f.field = "parent_project_id"
|
||||
filter, err := getFilterCond(f, opts.filterIncludeNulls)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
namespaceFilters = append(namespaceFilters, filter)
|
||||
projectFilters = append(projectFilters, filter)
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -384,7 +384,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
filters = append(filters, filter)
|
||||
}
|
||||
|
||||
// Then return all tasks for that lists
|
||||
// Then return all tasks for that projects
|
||||
var where builder.Cond
|
||||
|
||||
if opts.search != "" {
|
||||
|
@ -396,18 +396,18 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
}
|
||||
}
|
||||
|
||||
var listIDCond builder.Cond
|
||||
var listCond builder.Cond
|
||||
if len(listIDs) > 0 {
|
||||
listIDCond = builder.In("list_id", listIDs)
|
||||
listCond = listIDCond
|
||||
var projectIDCond builder.Cond
|
||||
var projectCond builder.Cond
|
||||
if len(projectIDs) > 0 {
|
||||
projectIDCond = builder.In("project_id", projectIDs)
|
||||
projectCond = projectIDCond
|
||||
}
|
||||
|
||||
if hasFavoritesList {
|
||||
if hasFavoritesProject {
|
||||
// Make sure users can only see their favorites
|
||||
userLists, _, _, err := getRawListsForUser(
|
||||
userProjects, _, _, err := getRawProjectsForUser(
|
||||
s,
|
||||
&listOptions{
|
||||
&projectOptions{
|
||||
user: &user.User{ID: a.GetID()},
|
||||
page: -1,
|
||||
},
|
||||
|
@ -416,9 +416,9 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
userListIDs := make([]int64, 0, len(userLists))
|
||||
for _, l := range userLists {
|
||||
userListIDs = append(userListIDs, l.ID)
|
||||
userProjectIDs := make([]int64, 0, len(userProjects))
|
||||
for _, l := range userProjects {
|
||||
userProjectIDs = append(userProjectIDs, l.ID)
|
||||
}
|
||||
|
||||
// All favorite tasks for that user
|
||||
|
@ -431,7 +431,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
builder.Eq{"kind": FavoriteKindTask},
|
||||
))
|
||||
|
||||
listCond = builder.And(listCond, builder.And(builder.In("id", favCond), builder.In("list_id", userListIDs)))
|
||||
projectCond = builder.And(projectCond, builder.And(builder.In("id", favCond), builder.In("project_id", userProjectIDs)))
|
||||
}
|
||||
|
||||
if len(reminderFilters) > 0 {
|
||||
|
@ -452,20 +452,20 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
filters = append(filters, getFilterCondForSeparateTable("label_tasks", opts.filterConcat, labelFilters))
|
||||
}
|
||||
|
||||
if len(namespaceFilters) > 0 {
|
||||
if len(projectFilters) > 0 {
|
||||
var filtercond builder.Cond
|
||||
if opts.filterConcat == filterConcatOr {
|
||||
filtercond = builder.Or(namespaceFilters...)
|
||||
filtercond = builder.Or(projectFilters...)
|
||||
}
|
||||
if opts.filterConcat == filterConcatAnd {
|
||||
filtercond = builder.And(namespaceFilters...)
|
||||
filtercond = builder.And(projectFilters...)
|
||||
}
|
||||
|
||||
cond := builder.In(
|
||||
"list_id",
|
||||
"project_id",
|
||||
builder.
|
||||
Select("id").
|
||||
From("lists").
|
||||
From("projects").
|
||||
Where(filtercond),
|
||||
)
|
||||
filters = append(filters, cond)
|
||||
|
@ -482,7 +482,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
}
|
||||
|
||||
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
||||
cond := builder.And(listCond, where, filterCond)
|
||||
cond := builder.And(projectCond, where, filterCond)
|
||||
|
||||
query := s.Where(cond)
|
||||
if limit > 0 {
|
||||
|
@ -505,9 +505,9 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
return tasks, len(tasks), totalItems, nil
|
||||
}
|
||||
|
||||
func getTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
||||
func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
||||
|
||||
tasks, resultCount, totalItems, err = getRawTasksForLists(s, lists, a, opts)
|
||||
tasks, resultCount, totalItems, err = getRawTasksForProjects(s, projects, a, opts)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
@ -548,7 +548,7 @@ func GetTaskSimple(s *xorm.Session, t *Task) (task Task, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// GetTasksByIDs returns all tasks for a list of ids
|
||||
// GetTasksByIDs returns all tasks for a project of ids
|
||||
func (bt *BulkTask) GetTasksByIDs(s *xorm.Session) (err error) {
|
||||
for _, id := range bt.IDs {
|
||||
if id < 1 {
|
||||
|
@ -589,8 +589,8 @@ func getRemindersForTasks(s *xorm.Session, taskIDs []int64) (reminders []*TaskRe
|
|||
return
|
||||
}
|
||||
|
||||
func (t *Task) setIdentifier(list *List) {
|
||||
t.Identifier = list.Identifier + "-" + strconv.FormatInt(t.Index, 10)
|
||||
func (t *Task) setIdentifier(project *Project) {
|
||||
t.Identifier = project.Identifier + "-" + strconv.FormatInt(t.Index, 10)
|
||||
}
|
||||
|
||||
// Get all assignees
|
||||
|
@ -715,7 +715,7 @@ func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]
|
|||
// It adds more stuff like assignees/labels/etc to a bunch of tasks
|
||||
func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (err error) {
|
||||
|
||||
// No need to iterate over users and stuff if the list doesn't have tasks
|
||||
// No need to iterate over users and stuff if the project doesn't have tasks
|
||||
if len(taskMap) == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -723,11 +723,11 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e
|
|||
// Get all users & task ids and put them into the array
|
||||
var userIDs []int64
|
||||
var taskIDs []int64
|
||||
var listIDs []int64
|
||||
var projectIDs []int64
|
||||
for _, i := range taskMap {
|
||||
taskIDs = append(taskIDs, i.ID)
|
||||
userIDs = append(userIDs, i.CreatedByID)
|
||||
listIDs = append(listIDs, i.ListID)
|
||||
projectIDs = append(projectIDs, i.ProjectID)
|
||||
}
|
||||
|
||||
err = addAssigneesToTasks(s, taskIDs, taskMap)
|
||||
|
@ -761,7 +761,7 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e
|
|||
}
|
||||
|
||||
// Get all identifiers
|
||||
lists, err := GetListsByIDs(s, listIDs)
|
||||
projects, err := GetProjectsByIDs(s, projectIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -778,8 +778,8 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e
|
|||
// Prepare the subtasks
|
||||
task.RelatedTasks = make(RelatedTaskMap)
|
||||
|
||||
// Build the task identifier from the list identifier and task index
|
||||
task.setIdentifier(lists[task.ListID])
|
||||
// Build the task identifier from the project identifier and task index
|
||||
task.setIdentifier(projects[task.ProjectID])
|
||||
|
||||
task.IsFavorite = taskFavorites[task.ID]
|
||||
}
|
||||
|
@ -789,11 +789,11 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e
|
|||
return
|
||||
}
|
||||
|
||||
func checkBucketAndTaskBelongToSameList(fullTask *Task, bucket *Bucket) (err error) {
|
||||
if fullTask.ListID != bucket.ListID {
|
||||
return ErrBucketDoesNotBelongToList{
|
||||
ListID: fullTask.ListID,
|
||||
BucketID: fullTask.BucketID,
|
||||
func checkBucketAndTaskBelongToSameProject(fullTask *Task, bucket *Bucket) (err error) {
|
||||
if fullTask.ProjectID != bucket.ProjectID {
|
||||
return ErrBucketDoesNotBelongToProject{
|
||||
ProjectID: fullTask.ProjectID,
|
||||
BucketID: fullTask.BucketID,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -821,7 +821,7 @@ func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucke
|
|||
// Make sure we have a bucket
|
||||
var bucket *Bucket
|
||||
if task.Done && originalTask != nil && !originalTask.Done {
|
||||
bucket, err := getDoneBucketForList(s, task.ListID)
|
||||
bucket, err := getDoneBucketForProject(s, task.ProjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -834,9 +834,9 @@ func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucke
|
|||
task.BucketID = originalTask.BucketID
|
||||
}
|
||||
|
||||
// Either no bucket was provided or the task was moved between lists
|
||||
if task.BucketID == 0 || (originalTask != nil && task.ListID != 0 && originalTask.ListID != task.ListID) {
|
||||
bucket, err = getDefaultBucket(s, task.ListID)
|
||||
// Either no bucket was provided or the task was moved between projects
|
||||
if task.BucketID == 0 || (originalTask != nil && task.ProjectID != 0 && originalTask.ProjectID != task.ProjectID) {
|
||||
bucket, err = getDefaultBucket(s, task.ProjectID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -850,8 +850,8 @@ func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucke
|
|||
}
|
||||
}
|
||||
|
||||
// If there is a bucket set, make sure they belong to the same list as the task
|
||||
err = checkBucketAndTaskBelongToSameList(task, bucket)
|
||||
// If there is a bucket set, make sure they belong to the same project as the task
|
||||
err = checkBucketAndTaskBelongToSameProject(task, bucket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -879,10 +879,10 @@ func calculateDefaultPosition(entityID int64, position float64) float64 {
|
|||
return position
|
||||
}
|
||||
|
||||
func getNextTaskIndex(s *xorm.Session, listID int64) (nextIndex int64, err error) {
|
||||
func getNextTaskIndex(s *xorm.Session, projectID int64) (nextIndex int64, err error) {
|
||||
latestTask := &Task{}
|
||||
_, err = s.
|
||||
Where("list_id = ?", listID).
|
||||
Where("project_id = ?", projectID).
|
||||
OrderBy("`index` desc").
|
||||
Get(latestTask)
|
||||
if err != nil {
|
||||
|
@ -892,20 +892,20 @@ func getNextTaskIndex(s *xorm.Session, listID int64) (nextIndex int64, err error
|
|||
return latestTask.Index + 1, nil
|
||||
}
|
||||
|
||||
// Create is the implementation to create a list task
|
||||
// Create is the implementation to create a project task
|
||||
// @Summary Create a task
|
||||
// @Description Inserts a task into a list.
|
||||
// @Description Inserts a task into a project.
|
||||
// @tags task
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List ID"
|
||||
// @Param id path int true "Project ID"
|
||||
// @Param task body models.Task true "The task object"
|
||||
// @Success 201 {object} models.Task "The created task object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid task object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id} [put]
|
||||
// @Router /projects/{id} [put]
|
||||
func (t *Task) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
return createTask(s, t, a, true)
|
||||
}
|
||||
|
@ -919,8 +919,8 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||
return ErrTaskCannotBeEmpty{}
|
||||
}
|
||||
|
||||
// Check if the list exists
|
||||
l, err := GetListSimpleByID(s, t.ListID)
|
||||
// Check if the project exists
|
||||
l, err := GetProjectSimpleByID(s, t.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -943,7 +943,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||
}
|
||||
|
||||
// Get the index for this task
|
||||
t.Index, err = getNextTaskIndex(s, t.ListID)
|
||||
t.Index, err = getNextTaskIndex(s, t.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -985,11 +985,11 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, &List{ID: t.ListID})
|
||||
err = updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
|
||||
return
|
||||
}
|
||||
|
||||
// Update updates a list task
|
||||
// Update updates a project task
|
||||
// @Summary Update a task
|
||||
// @Description Updates a task. This includes marking it as done. Assignees you pass will be updated, see their individual endpoints for more details on how this is done. To update labels, see the description of the endpoint.
|
||||
// @tags task
|
||||
|
@ -1000,7 +1000,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||
// @Param task body models.Task true "The task object"
|
||||
// @Success 200 {object} models.Task "The updated task object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid task object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the task (aka its list)"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the task (aka its project)"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{id} [post]
|
||||
//
|
||||
|
@ -1013,8 +1013,8 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
if t.ListID == 0 {
|
||||
t.ListID = ot.ListID
|
||||
if t.ProjectID == 0 {
|
||||
t.ProjectID = ot.ProjectID
|
||||
}
|
||||
|
||||
// Get the reminders
|
||||
|
@ -1066,7 +1066,7 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
"hex_color",
|
||||
"done_at",
|
||||
"percent_done",
|
||||
"list_id",
|
||||
"project_id",
|
||||
"bucket_id",
|
||||
"position",
|
||||
"repeat_mode",
|
||||
|
@ -1074,9 +1074,9 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
"cover_image_attachment_id",
|
||||
}
|
||||
|
||||
// If the task is being moved between lists, make sure to move the bucket + index as well
|
||||
if t.ListID != 0 && ot.ListID != t.ListID {
|
||||
t.Index, err = getNextTaskIndex(s, t.ListID)
|
||||
// If the task is being moved between projects, make sure to move the bucket + index as well
|
||||
if t.ProjectID != 0 && ot.ProjectID != t.ProjectID {
|
||||
t.Index, err = getNextTaskIndex(s, t.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1225,7 +1225,7 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
return updateListLastUpdated(s, &List{ID: t.ListID})
|
||||
return updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
|
||||
}
|
||||
|
||||
func addOneMonthToDate(d time.Time) time.Time {
|
||||
|
@ -1437,20 +1437,20 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []time.Time) (err erro
|
|||
t.Reminders = nil
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, &List{ID: t.ListID})
|
||||
err = updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete implements the delete method for listTask
|
||||
// Delete implements the delete method for projectTask
|
||||
// @Summary Delete a task
|
||||
// @Description Deletes a task from a list. This does not mean "mark it done".
|
||||
// @Description Deletes a task from a project. This does not mean "mark it done".
|
||||
// @tags task
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Task ID"
|
||||
// @Success 200 {object} models.Message "The created task object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid task ID provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{id} [delete]
|
||||
func (t *Task) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
@ -1516,7 +1516,7 @@ func (t *Task) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, &List{ID: t.ListID})
|
||||
err = updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -26,15 +26,15 @@ func (t *Task) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
return t.canDoTask(s, a)
|
||||
}
|
||||
|
||||
// CanUpdate determines if a user has the right to update a list task
|
||||
// CanUpdate determines if a user has the right to update a project task
|
||||
func (t *Task) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return t.canDoTask(s, a)
|
||||
}
|
||||
|
||||
// CanCreate determines if a user has the right to create a list task
|
||||
// CanCreate determines if a user has the right to create a project task
|
||||
func (t *Task) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
// A user can do a task if he has write acces to its list
|
||||
l := &List{ID: t.ListID}
|
||||
// A user can do a task if he has write acces to its project
|
||||
l := &Project{ID: t.ProjectID}
|
||||
return l.CanWrite(s, a)
|
||||
}
|
||||
|
||||
|
@ -46,8 +46,8 @@ func (t *Task) CanRead(s *xorm.Session, a web.Auth) (canRead bool, maxRight int,
|
|||
return
|
||||
}
|
||||
|
||||
// A user can read a task if it has access to the list
|
||||
l := &List{ID: t.ListID}
|
||||
// A user can read a task if it has access to the project
|
||||
l := &Project{ID: t.ProjectID}
|
||||
return l.CanRead(s, a)
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@ func (t *Task) CanWrite(s *xorm.Session, a web.Auth) (canWrite bool, err error)
|
|||
return t.canDoTask(s, a)
|
||||
}
|
||||
|
||||
// Helper function to check if a user can do stuff on a list task
|
||||
// Helper function to check if a user can do stuff on a project task
|
||||
func (t *Task) canDoTask(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
// Get the task
|
||||
ot, err := GetTaskByIDSimple(s, t.ID)
|
||||
|
@ -64,10 +64,10 @@ func (t *Task) canDoTask(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
// Check if we're moving the task into a different list to check if the user has sufficient rights for that on the new list
|
||||
if t.ListID != 0 && t.ListID != ot.ListID {
|
||||
newList := &List{ID: t.ListID}
|
||||
can, err := newList.CanWrite(s, a)
|
||||
// Check if we're moving the task into a different project to check if the user has sufficient rights for that on the new project
|
||||
if t.ProjectID != 0 && t.ProjectID != ot.ProjectID {
|
||||
newProject := &Project{ID: t.ProjectID}
|
||||
can, err := newProject.CanWrite(s, a)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ func (t *Task) canDoTask(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// A user can do a task if it has write acces to its list
|
||||
l := &List{ID: ot.ListID}
|
||||
// A user can do a task if it has write acces to its project
|
||||
l := &Project{ID: ot.ProjectID}
|
||||
return l.CanWrite(s, a)
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ func TestTask_Create(t *testing.T) {
|
|||
task := &Task{
|
||||
Title: "Lorem",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
}
|
||||
err := task.Create(s, usr)
|
||||
assert.NoError(t, err)
|
||||
|
@ -62,7 +62,7 @@ func TestTask_Create(t *testing.T) {
|
|||
"id": task.ID,
|
||||
"title": "Lorem",
|
||||
"description": "Lorem Ipsum Dolor",
|
||||
"list_id": 1,
|
||||
"project_id": 1,
|
||||
"created_by_id": 1,
|
||||
"bucket_id": 1,
|
||||
}, false)
|
||||
|
@ -77,13 +77,13 @@ func TestTask_Create(t *testing.T) {
|
|||
task := &Task{
|
||||
Title: "",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
}
|
||||
err := task.Create(s, usr)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTaskCannotBeEmpty(err))
|
||||
})
|
||||
t.Run("nonexistant list", func(t *testing.T) {
|
||||
t.Run("nonexistant project", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
@ -91,11 +91,11 @@ func TestTask_Create(t *testing.T) {
|
|||
task := &Task{
|
||||
Title: "Test",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 9999999,
|
||||
ProjectID: 9999999,
|
||||
}
|
||||
err := task.Create(s, usr)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
assert.True(t, IsErrProjectDoesNotExist(err))
|
||||
})
|
||||
t.Run("noneixtant user", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
|
@ -106,7 +106,7 @@ func TestTask_Create(t *testing.T) {
|
|||
task := &Task{
|
||||
Title: "Test",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
}
|
||||
err := task.Create(s, nUser)
|
||||
assert.Error(t, err)
|
||||
|
@ -120,7 +120,7 @@ func TestTask_Create(t *testing.T) {
|
|||
task := &Task{
|
||||
Title: "Lorem",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
|
||||
}
|
||||
err := task.Create(s, usr)
|
||||
|
@ -141,7 +141,7 @@ func TestTask_Update(t *testing.T) {
|
|||
ID: 1,
|
||||
Title: "test10000",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
|
@ -152,7 +152,7 @@ func TestTask_Update(t *testing.T) {
|
|||
"id": 1,
|
||||
"title": "test10000",
|
||||
"description": "Lorem Ipsum Dolor",
|
||||
"list_id": 1,
|
||||
"project_id": 1,
|
||||
}, false)
|
||||
})
|
||||
t.Run("nonexistant task", func(t *testing.T) {
|
||||
|
@ -164,7 +164,7 @@ func TestTask_Update(t *testing.T) {
|
|||
ID: 9999999,
|
||||
Title: "test10000",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
|
@ -179,7 +179,7 @@ func TestTask_Update(t *testing.T) {
|
|||
ID: 1,
|
||||
Title: "test10000",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
|
@ -196,13 +196,13 @@ func TestTask_Update(t *testing.T) {
|
|||
Title: "test10000",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
KanbanPosition: 10,
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("bucket on other list", func(t *testing.T) {
|
||||
t.Run("bucket on other project", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
@ -211,12 +211,12 @@ func TestTask_Update(t *testing.T) {
|
|||
ID: 1,
|
||||
Title: "test10000",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
BucketID: 4, // Bucket 4 belongs to list 2
|
||||
ProjectID: 1,
|
||||
BucketID: 4, // Bucket 4 belongs to project 2
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrBucketDoesNotBelongToList(err))
|
||||
assert.True(t, IsErrBucketDoesNotBelongToProject(err))
|
||||
})
|
||||
t.Run("moving a task to the done bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
|
@ -224,10 +224,10 @@ func TestTask_Update(t *testing.T) {
|
|||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
Title: "test",
|
||||
ListID: 1,
|
||||
BucketID: 3, // Bucket 3 is the done bucket
|
||||
ID: 1,
|
||||
Title: "test",
|
||||
ProjectID: 1,
|
||||
BucketID: 3, // Bucket 3 is the done bucket
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
|
@ -236,11 +236,11 @@ func TestTask_Update(t *testing.T) {
|
|||
assert.True(t, task.Done)
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": 1,
|
||||
"done": true,
|
||||
"title": "test",
|
||||
"list_id": 1,
|
||||
"bucket_id": 3,
|
||||
"id": 1,
|
||||
"done": true,
|
||||
"title": "test",
|
||||
"project_id": 1,
|
||||
"bucket_id": 3,
|
||||
}, false)
|
||||
})
|
||||
t.Run("moving a repeating task to the done bucket", func(t *testing.T) {
|
||||
|
@ -251,7 +251,7 @@ func TestTask_Update(t *testing.T) {
|
|||
task := &Task{
|
||||
ID: 28,
|
||||
Title: "test updated",
|
||||
ListID: 1,
|
||||
ProjectID: 1,
|
||||
BucketID: 3, // Bucket 3 is the done bucket
|
||||
RepeatAfter: 3600,
|
||||
}
|
||||
|
@ -263,28 +263,28 @@ func TestTask_Update(t *testing.T) {
|
|||
assert.Equal(t, int64(1), task.BucketID) // Bucket should not be updated
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": 28,
|
||||
"done": false,
|
||||
"title": "test updated",
|
||||
"list_id": 1,
|
||||
"bucket_id": 1,
|
||||
"id": 28,
|
||||
"done": false,
|
||||
"title": "test updated",
|
||||
"project_id": 1,
|
||||
"bucket_id": 1,
|
||||
}, false)
|
||||
})
|
||||
t.Run("default bucket when moving a task between lists", func(t *testing.T) {
|
||||
t.Run("default bucket when moving a task between projects", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
ListID: 2,
|
||||
ID: 1,
|
||||
ProjectID: 2,
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, int64(4), task.BucketID) // bucket 4 is the default bucket on list 2
|
||||
assert.Equal(t, int64(4), task.BucketID) // bucket 4 is the default bucket on project 2
|
||||
assert.True(t, task.Done) // bucket 4 is the done bucket, so the task should be marked as done as well
|
||||
})
|
||||
t.Run("marking a task as done should move it to the done bucket", func(t *testing.T) {
|
||||
|
@ -309,14 +309,14 @@ func TestTask_Update(t *testing.T) {
|
|||
"bucket_id": 3,
|
||||
}, false)
|
||||
})
|
||||
t.Run("move task to another list", func(t *testing.T) {
|
||||
t.Run("move task to another project", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
ListID: 2,
|
||||
ID: 1,
|
||||
ProjectID: 2,
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
|
@ -324,9 +324,9 @@ func TestTask_Update(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": 1,
|
||||
"list_id": 2,
|
||||
"bucket_id": 4,
|
||||
"id": 1,
|
||||
"project_id": 2,
|
||||
"bucket_id": 4,
|
||||
}, false)
|
||||
})
|
||||
t.Run("repeating tasks should not be moved to the done bucket", func(t *testing.T) {
|
||||
|
@ -352,14 +352,14 @@ func TestTask_Update(t *testing.T) {
|
|||
"bucket_id": 1,
|
||||
}, false)
|
||||
})
|
||||
t.Run("moving a task between lists should give it a correct index", func(t *testing.T) {
|
||||
t.Run("moving a task between projects should give it a correct index", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 12,
|
||||
ListID: 2, // From list 1
|
||||
ID: 12,
|
||||
ProjectID: 2, // From project 1
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -46,7 +46,7 @@ func (tm *TeamMember) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
|
||||
// IsAdmin checks if the user is team admin
|
||||
func (tm *TeamMember) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
// Don't allow anything if we're dealing with a list share here
|
||||
// Don't allow anything if we're dealing with a project share here
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, nil
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue