Compare commits

...

53 Commits

Author SHA1 Message Date
kolaente 1c07bce373
fix: rename project receiver variable 2023-03-14 17:43:56 +01:00
kolaente a7c2ca146c
fix(test): adjust fixture bucket and list ids 2023-03-14 17:42:35 +01:00
kolaente cf1e088a8f
fix(test): adjust fixture id 2023-03-14 17:42:35 +01:00
kolaente f9169df37a
fix(test): fixtures 2023-03-14 17:42:35 +01:00
kolaente f6e29169c4
fix: compile errors 2023-03-14 17:42:35 +01:00
kolaente 2f78ed825b
chore: go mod tidy 2023-03-14 17:42:34 +01:00
kolaente 7a7767c9df
fix: lint errors 2023-03-14 17:42:34 +01:00
kolaente 622ae0fe17
chore(test): show table content when db assertion failed 2023-03-14 17:42:34 +01:00
kolaente 53dfaa12c0
fix(migration): enable insert from structure work recursively 2023-03-14 17:42:34 +01:00
kolaente 5d2fc8a72f
fix(migration): make file migration work with new structure 2023-03-14 17:42:34 +01:00
kolaente be9f33ba81
chore(export): remove unused events 2023-03-14 17:42:34 +01:00
kolaente d872b9947d
fix(tests): task collection fixtures 2023-03-14 17:42:33 +01:00
kolaente f9f9c4ea90
fix(tests): subscription test fixtures 2023-03-14 17:42:33 +01:00
kolaente bc7cd3304f
fix(tests): task permissions from parents 2023-03-14 17:42:33 +01:00
kolaente 28014d40c6
fix(tests): adjust parent projects 2023-03-14 17:42:33 +01:00
kolaente 8b7dd7de61
fix(projects): properly check if a user or link share is allowed to create a new project 2023-03-14 17:42:33 +01:00
kolaente 93802e78c2
fix(tests): permission tests for parent projects 2023-03-14 17:42:33 +01:00
kolaente fea89a8716
chore(project): fmt 2023-03-14 17:42:33 +01:00
kolaente ea3a53ad54
fix(project): don't allow un-archiving a project when its parent project is archived 2023-03-14 17:42:32 +01:00
kolaente cc2de54ef5
fix(project): recursively get all users from all parent projects 2023-03-14 17:42:32 +01:00
kolaente 5bafef8aff
fix(tasks): task relation test 2023-03-14 17:42:32 +01:00
kolaente 70dbbddcc6
fix(tasks): read all tests 2023-03-14 17:42:32 +01:00
kolaente 59725412ed
fix(migration): remove wunderlist leftovers 2023-03-14 17:42:32 +01:00
kolaente d4e9ed7ffb
fix(tasks): get all tasks from parent projects 2023-03-14 17:42:32 +01:00
kolaente c7736e09d4
feat(subscriptions): make sure all subscriptions are inherited properly 2023-03-14 17:42:31 +01:00
kolaente 99aba86bd0
fix(tests): make the tests compile again 2023-03-14 17:42:31 +01:00
kolaente 1f3148b07e
fix(projects): permission check now works 2023-03-14 17:42:31 +01:00
kolaente eed194ae2a
fix(projects): don't check if new projects are archived 2023-03-14 17:42:31 +01:00
kolaente b6751cac2f
chore(projects) use a slice again 2023-03-14 17:42:31 +01:00
kolaente f652576d21
chore(docs): update error docs 2023-03-14 17:42:29 +01:00
kolaente 03041c71e2
feat(projects): check parent project when checking archived status 2023-03-14 17:41:43 +01:00
kolaente 7bde334344
feat(projects): check all parent projects for permissions 2023-03-14 17:41:43 +01:00
kolaente 9c63be4576
feat(projects): get all projects recursively 2023-03-14 17:41:43 +01:00
kolaente 1c6c605e76
fix: typo 2023-03-14 17:41:43 +01:00
kolaente 0d70e58585
fix: rename after rebase 2023-03-14 17:41:43 +01:00
kolaente f8ffc9ad7d
fix: make it compile again 2023-03-14 17:41:42 +01:00
kolaente 05c1fbc411
chore: generate swagger docs 2023-03-14 17:41:42 +01:00
kolaente 832577b2d5
fix(tasks): don't check for namespaces in filters 2023-03-14 17:41:42 +01:00
kolaente 9b6e5dd047
fix(project): don't check for namespaces in overdue reminders 2023-03-14 17:41:42 +01:00
kolaente ee40755f1f
fix(project): remove comments, clarifications, notifications about namespaces 2023-03-14 17:41:42 +01:00
kolaente d2bf250aab
fix(project): remove namespaces checks 2023-03-14 17:41:42 +01:00
kolaente f6fc56a05d
fix(project): remove namespaces from creating projects 2023-03-14 17:41:41 +01:00
kolaente 9d05f1ae75
fix(project): remove namespaces from getting projects 2023-03-14 17:41:41 +01:00
kolaente 2e1befdabc
feat(migration): ignore namespace changes 2023-03-14 17:41:41 +01:00
kolaente 14bb868c67
feat(migration): use new structure for migration 2023-03-14 17:41:41 +01:00
kolaente 65cff79dbd
feat(projects): cleanup namespace leftovers 2023-03-14 17:41:41 +01:00
kolaente 532c4279e7
feat(projects): remove namespaces 2023-03-14 17:41:41 +01:00
kolaente 8558f52874
feat(projects): add parent project, migrate namespaces 2023-03-14 17:41:41 +01:00
kolaente 30d0bff003
feat: rename lists to projects 2023-03-14 17:41:40 +01:00
kolaente 8acf1ad45d
feat: rename lists to projects 2023-03-14 17:41:40 +01:00
kolaente 3e76fdbf47
fix(migration): remove wunderlist leftovers 2023-03-14 17:41:40 +01:00
kolaente 2a70b3a2f0
fix: lint 2023-03-14 17:41:40 +01:00
kolaente 2115f450fc
feat: rename lists to projects 2023-03-14 17:41:40 +01:00
99 changed files with 2322 additions and 8518 deletions

View File

@ -88,6 +88,8 @@ 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"
- path: pkg/models/events\.go
linters:
- musttag

View File

@ -63,7 +63,6 @@ This document describes the different errors Vikunja can return.
| 3007 | 400 | A project with this identifier already exists. |
| 3008 | 412 | The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project. |
| 3009 | 412 | The project cannot belong to a dynamically generated namespace like "Favorites". |
| 3010 | 412 | The project must belong to a namespace. |
## Task
@ -91,27 +90,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 project. |
| 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 project to perform that action. |
## User Project Access

2
go.mod
View File

@ -38,6 +38,7 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/google/uuid v1.3.0
github.com/hashicorp/go-version v1.6.0
github.com/iancoleman/strcase v0.2.0
github.com/imdario/mergo v0.3.13
github.com/jinzhu/copier v0.3.5
@ -138,6 +139,7 @@ require (
github.com/urfave/cli/v2 v2.3.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/text v0.8.0 // indirect

22
go.sum
View File

@ -169,14 +169,10 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0=
github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
github.com/getsentry/sentry-go v0.19.0 h1:BcCH3CN5tXt5aML+gwmbFwVptLLQA+eT866fCO9wVOM=
github.com/getsentry/sentry-go v0.19.0/go.mod h1:y3+lGEFEFexZtpbG1GUE2WD/f9zGyKYwpEqryTOC/nE=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
@ -331,6 +327,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=
@ -433,8 +431,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kolaente/caldav-go v3.0.1-0.20190524174923-9e5cd1688227+incompatible h1:PkEEpmbrFXlMul8cOplR8nkcIM/NDbx+H6fq2+vaKAA=
github.com/kolaente/caldav-go v3.0.1-0.20190524174923-9e5cd1688227+incompatible/go.mod h1:y1UhTNI4g0hVymJrI6yJ5/ohy09hNBeU8iJEZjgdDOw=
github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible h1:q7DbyV+sFjEoTuuUdRDNl2nlyfztkZgxVVCV7JhzIkY=
github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible/go.mod h1:y1UhTNI4g0hVymJrI6yJ5/ohy09hNBeU8iJEZjgdDOw=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -645,8 +641,6 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/afero v1.9.4 h1:Sd43wM1IWz/s1aVXdOBkjJvuP8UdyqioeE4AmM0QsBs=
github.com/spf13/afero v1.9.4/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
@ -689,8 +683,6 @@ github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpP
github.com/tkuchiki/go-timezone v0.2.2 h1:MdHR65KwgVTwWFQrota4SKzc4L5EfuH5SdZZGtk/P2Q=
github.com/tkuchiki/go-timezone v0.2.2/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ulule/limiter/v3 v3.11.0 h1:9hXMyS0K8Z+EYfrtwPMwmWYflPimswsC/EOMsO2sHx4=
github.com/ulule/limiter/v3 v3.11.0/go.mod h1:OiKIiMs9dXLMk5TwtIBZlswhPigov9fGmwO4xYbmFkY=
github.com/ulule/limiter/v3 v3.11.1 h1:wm6YaA2JwIXc0S+z8TK8/neWMOTf4m20I5jL1dwLRcw=
github.com/ulule/limiter/v3 v3.11.1/go.mod h1:4nk/9RHEJthkjD+mmkqYxaPfD4pkB91PTH7k8ozB80g=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
@ -705,13 +697,13 @@ github.com/valyala/fasttemplate v1.2.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93 h1:bT0ZMfsMi2Xh8dopgxhFT+OJH88QITHpdppdkG1rXJQ=
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93/go.mod h1:PnwzbSst7KD3vpBzzlntZU5gjVa455Uqa5QPiKSYJzQ=
github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae h1:oyiy3uBj1F4O3AaFh7hUGBrJjAssJhKyAbwxtkslxqo=
github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae/go.mod h1:PnwzbSst7KD3vpBzzlntZU5gjVa455Uqa5QPiKSYJzQ=
github.com/wneessen/go-mail v0.3.8 h1:ja5D/o/RVwrtRIYFlrO7GmtcjDNeMakGQuwQRZYv0JM=
github.com/wneessen/go-mail v0.3.8/go.mod h1:m25lkU2GYQnlVr6tdwK533/UXxo57V0kLOjaFYmub0E=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -762,7 +754,6 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
@ -849,7 +840,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
@ -1302,10 +1292,6 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
src.techknowlogick.com/xgo v1.7.1-0.20230117190652-94aee174ab86 h1:VybPMHRdCLbdCttI8fMXOaGpoJGSG9+W/5cfRgr1Xjc=
src.techknowlogick.com/xgo v1.7.1-0.20230117190652-94aee174ab86/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xgo v1.7.1-0.20230214195350-44f7e66f9b20 h1:Wye8Ljlv2AZvYPW1twGbW9sQWGtjurbQECnnkNx6gd0=
src.techknowlogick.com/xgo v1.7.1-0.20230214195350-44f7e66f9b20/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xgo v1.7.1-0.20230307171022-b60708668fc7 h1:nPPnMdR4wih62PSsnHK/SlYM1lOZk/St0k7DkJadMV4=
src.techknowlogick.com/xgo v1.7.1-0.20230307171022-b60708668fc7/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xormigrate v1.5.0 h1:6mWTh8d0sWjMTLUgJqiLe0e0Teu+1j+RgI7ErAeOEV0=

View File

@ -127,7 +127,7 @@ var userProjectCmd = &cobra.Command{
s := db.NewSession()
defer s.Close()
users, err := user.ProjectAllUsers(s)
users, err := user.ListAllUsers(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 {

View File

@ -220,7 +220,19 @@
updated: 2020-04-18 21:13:52
- id: 36
title: testbucket36
project_id: 26
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
- id: 38
title: testbucket36
project_id: 36
created_by_id: 15
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52

View File

@ -15,6 +15,6 @@
label_id: 4
created: 2018-12-01 15:13:12
- id: 5
task_id: 39
task_id: 40
label_id: 4
created: 2018-12-01 15:13:12

View File

@ -1,96 +0,0 @@
- id: 1
title: testnamespace
description: Lorem Ipsum
owner_id: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 2
title: testnamespace2
description: Lorem Ipsum
owner_id: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 3
title: testnamespace3
description: Lorem Ipsum
owner_id: 3
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 6
title: testnamespace6
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 7
title: testnamespace7
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 8
title: testnamespace8
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 9
title: testnamespace9
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 10
title: testnamespace10
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 11
title: testnamespace11
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 12
title: testnamespace12
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 13
title: testnamespace13
description: Lorem Ipsum
owner_id: 7
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 14
title: testnamespace14
description: Lorem Ipsum
owner_id: 7
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 15
title: testnamespace15
description: Lorem Ipsum
owner_id: 13
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
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 18
title: testnamespace18
description: Lorem Ipsum
owner_id: 15
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -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,8 +153,8 @@
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 project is owned by user 7, and several other users have access to it via different methods.
@ -176,7 +165,6 @@
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,8 +174,8 @@
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 project
@ -197,18 +185,17 @@
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,28 +223,95 @@
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 with background
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
-
id: 35
title: Test35 with background
description: Lorem Ipsum
identifier: test6
owner_id: 6
namespace_id: 6
background_file_id: 1
position: 8
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 26
title: List 26 for Caldav tests
id: 36
title: Project 36 for Caldav tests
description: Lorem Ipsum
identifier: test26
identifier: test36
owner_id: 15
namespace_id: 18
position: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -3,24 +3,14 @@
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 # project
entity_id: 12 # belongs to namespace 7
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 project 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
@ -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

View File

@ -193,7 +193,7 @@
title: 'task #21'
done: false
created_by_id: 6
project_id: 12
project_id: 32
index: 1
bucket_id: 12
created: 2018-12-01 01:12:04
@ -202,18 +202,18 @@
title: 'task #22'
done: false
created_by_id: 6
project_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
project_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
@ -356,13 +356,19 @@
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
- id: 40
uid: 'uid-caldav-test'
title: 'Title Caldav Test'
description: 'Description Caldav Test'
priority: 3
done: false
created_by_id: 15
project_id: 26
project_id: 36
index: 39
due_date: 2023-03-01 15:00:00
created: 2018-12-01 01:12:04

View File

@ -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

View File

@ -52,3 +52,45 @@
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

View File

@ -11,15 +11,6 @@
- id: 4
name: testteam4_admin_on_project8
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
created_by_id: 1
- id: 8
name: testteam8
created_by_id: 7

View File

@ -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

View File

@ -47,6 +47,54 @@
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 9
user_id: 1
project_id: 27
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 10
user_id: 11
project_id: 29
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 11
user_id: 12
project_id: 29
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 12
user_id: 13
project_id: 29
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 13
user_id: 1
project_id: 30
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 14
user_id: 1
project_id: 31
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 15
user_id: 1
project_id: 28
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 16
user_id: 1
project_id: 29
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 17
user_id: 15
project_id: 26
right: 0

View File

@ -17,6 +17,7 @@
package db
import (
"encoding/json"
"fmt"
"os"
"testing"
@ -93,7 +94,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

View File

@ -17,7 +17,6 @@
package integrations
import (
"net/url"
"testing"
"code.vikunja.io/api/pkg/models"
@ -26,32 +25,27 @@ 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 project which belongs to an archived namespace cannot be edited.
// 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 project individually if its namespace is archived.
// 5. Creating new projects on an archived namespace should not work.
// 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 who's namespace is archived 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 who's namespace is archived should not work.
// 10. Archived namespaces should not appear in the project with all namespaces.
// 11. Archived projects should not appear in the project with all projects.
// 12. Projects who's namespace is archived should not appear in the project with all projects.
// 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 projects from namespaces could be solved with some kind of is_archived_inherited flag -
// 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
// namespace is archived. The archived flag would then be used to not accedentially unarchive projects which were
// already individually archived when the namespace was archived.
// Should still test it all though.
// 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
// Project 21 belongs to namespace 16
// Project 21 belongs to project 16
// Project 22 is archived individually
func TestArchived(t *testing.T) {
@ -62,13 +56,6 @@ func TestArchived(t *testing.T) {
},
t: t,
}
testNamespaceHandler := webHandlerTest{
user: &testuser1,
strFunc: func() handler.CObject {
return &models.Namespace{}
},
t: t,
}
testTaskHandler := webHandlerTest{
user: &testuser1,
strFunc: func() handler.CObject {
@ -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 projects", func(t *testing.T) {
_, err := testProjectHandler.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 project", 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 project 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("project", 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 namespace
t.Run("archived namespace", func(t *testing.T) {
t.Run("not editable", func(t *testing.T) {
_, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "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{"project": "21"}, `{"title":"Lorem"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
})
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.ErrCodeNamespaceIsArchived)
})
taskTests("35", models.ErrCodeNamespaceIsArchived, 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 := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "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.ErrCodeProjectIsArchived, t)
})
taskTests("36", models.ErrCodeProjectIsArchived, t)
})
}

View File

@ -28,7 +28,7 @@ const vtodo = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:List 26 for Caldav tests
X-WR-CALNAME:List 36 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid
@ -42,22 +42,22 @@ END:VCALENDAR`
func TestCaldav(t *testing.T) {
t.Run("Delivers VTODO for project", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "26"})
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "PRODID:-//Vikunja Todo App//EN")
assert.Contains(t, rec.Body.String(), "X-WR-CALNAME:List 26 for Caldav tests")
assert.Contains(t, rec.Body.String(), "X-WR-CALNAME:Project 36 for Caldav tests")
assert.Contains(t, rec.Body.String(), "BEGIN:VTODO")
assert.Contains(t, rec.Body.String(), "END:VTODO")
assert.Contains(t, rec.Body.String(), "END:VCALENDAR")
})
t.Run("Import VTODO", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodo, nil, map[string]string{"project": "26", "task": "uid"})
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodo, nil, map[string]string{"project": "36", "task": "uid"})
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
assert.Equal(t, 201, rec.Result().StatusCode)
})
t.Run("Export VTODO", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid-caldav-test"})
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "SUMMARY:Title Caldav Test")

View File

@ -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"`)
@ -198,33 +198,33 @@ func TestBucket(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
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{"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) {
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) {
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) {
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) {
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) {
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."`)
@ -281,33 +281,33 @@ func TestBucket(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"title":"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{"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) {
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) {
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) {
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) {
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) {
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"`)

View File

@ -273,10 +273,10 @@ func TestLinkSharing(t *testing.T) {
})
})
// Creating a project should always be forbidden, since users need access to a namespace to create a project
// Creating a project should always be forbidden
t.Run("Create", func(t *testing.T) {
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandlerProjectReadOnly.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`)
})
@ -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`)
})
})
})
})
})
}

View File

@ -40,10 +40,10 @@ func TestProject(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_project
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) {
@ -60,10 +60,10 @@ func TestProject(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_project
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
})
})
@ -76,7 +76,7 @@ func TestProject(t *testing.T) {
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{"project": "9999"})
@ -129,38 +129,38 @@ func TestProject(t *testing.T) {
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
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) {
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) {
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) {
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) {
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) {
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"`)
@ -171,7 +171,7 @@ func TestProject(t *testing.T) {
t.Run("Update", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
// Check the project was loaded successfully afterwards, see testReadOneWithUser
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum","namespace_id":1}`)
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
@ -183,7 +183,7 @@ func TestProject(t *testing.T) {
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
})
t.Run("Normal with updating the description", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "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`)
@ -211,12 +211,12 @@ func TestProject(t *testing.T) {
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{"project": "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{"project": "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"`)
})
@ -227,44 +227,44 @@ func TestProject(t *testing.T) {
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{"project": "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{"project": "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) {
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{"project": "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{"project": "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) {
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{"project": "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{"project": "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"`)
})
@ -320,33 +320,33 @@ func TestProject(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
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{"project": "12"})
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{"project": "13"})
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) {
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) {
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) {
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) {
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."`)
@ -356,7 +356,7 @@ func TestProject(t *testing.T) {
t.Run("Create", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
// Check the project was loaded successfully after update, see testReadOneWithUser
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem"}`)
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 TestProject(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 TestProject(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":""`)

View File

@ -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"`)

View File

@ -254,33 +254,33 @@ func TestTask(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"title":"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.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) {
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) {
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) {
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) {
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) {
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"`)
@ -372,33 +372,33 @@ func TestTask(t *testing.T) {
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
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{"projecttask": "21"})
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.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) {
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) {
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) {
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) {
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.`)
@ -455,33 +455,33 @@ func TestTask(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"title":"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{"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) {
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) {
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) {
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) {
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) {
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"`)

View File

@ -34,9 +34,6 @@ const (
// 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`
@ -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",

View File

@ -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")
}

View File

@ -255,65 +255,37 @@ 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."}
}
// ErrProjectCannotBelongToAPseudoNamespace represents an error where a project cannot belong to a pseudo namespace
type ErrProjectCannotBelongToAPseudoNamespace struct {
ProjectID int64
NamespaceID int64
// ErrProjectCannotBelongToAPseudoParentProject represents an error where a project cannot belong to a pseudo project
type ErrProjectCannotBelongToAPseudoParentProject struct {
ProjectID int64
ParentProjectID int64
}
// IsErrProjectCannotBelongToAPseudoNamespace checks if an error is a project is archived error.
func IsErrProjectCannotBelongToAPseudoNamespace(err error) bool {
_, ok := err.(*ErrProjectCannotBelongToAPseudoNamespace)
// IsErrProjectCannotBelongToAPseudoParentProject checks if an error is a project is archived error.
func IsErrProjectCannotBelongToAPseudoParentProject(err error) bool {
_, ok := err.(*ErrProjectCannotBelongToAPseudoParentProject)
return ok
}
func (err *ErrProjectCannotBelongToAPseudoNamespace) Error() string {
return fmt.Sprintf("Project cannot belong to a pseudo namespace [ProjectID: %d, NamespaceID: %d]", err.ProjectID, 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)
}
// ErrCodeProjectCannotBelongToAPseudoNamespace holds the unique world-error code of this error
const ErrCodeProjectCannotBelongToAPseudoNamespace = 3009
// ErrCodeProjectCannotBelongToAPseudoParentProject holds the unique world-error code of this error
const ErrCodeProjectCannotBelongToAPseudoParentProject = 3009
// HTTPError holds the http error description
func (err *ErrProjectCannotBelongToAPseudoNamespace) HTTPError() web.HTTPError {
func (err *ErrProjectCannotBelongToAPseudoParentProject) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeProjectCannotBelongToAPseudoNamespace,
Message: "This project cannot belong a dynamically generated namespace.",
Code: ErrCodeProjectCannotBelongToAPseudoParentProject,
Message: "This project cannot belong a dynamically generated project.",
}
}
// ErrProjectMustBelongToANamespace represents an error where a project must belong to a namespace
type ErrProjectMustBelongToANamespace struct {
ProjectID int64
NamespaceID int64
}
// IsErrProjectMustBelongToANamespace checks if an error is a project must belong to a namespace error.
func IsErrProjectMustBelongToANamespace(err error) bool {
_, ok := err.(*ErrProjectMustBelongToANamespace)
return ok
}
func (err *ErrProjectMustBelongToANamespace) Error() string {
return fmt.Sprintf("Project must belong to a namespace [ProjectID: %d, NamespaceID: %d]", err.ProjectID, err.NamespaceID)
}
// ErrCodeProjectMustBelongToANamespace holds the unique world-error code of this error
const ErrCodeProjectMustBelongToANamespace = 3010
// HTTPError holds the http error description
func (err *ErrProjectMustBelongToANamespace) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeProjectMustBelongToANamespace,
Message: "This project must belong to a namespace.",
}
}
// ================
// Project task errors
// ================
// ==============
// Project errors
// ==============
// ErrTaskCannotBeEmpty represents a "ErrProjectDoesNotExist" kind of error. Used if the project does not exist.
type ErrTaskCannotBeEmpty struct{}
@ -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 projects 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 project/namespace
// ErrTeamAlreadyHasAccess represents an error where a team already has access to a project
type ErrTeamAlreadyHasAccess struct {
TeamID int64
ID int64
@ -1195,7 +997,7 @@ func (err ErrTeamDoesNotHaveAccessToProject) HTTPError() web.HTTPError {
// User <-> Project errors
// ====================
// ErrUserAlreadyHasAccess represents an error where a user already has access to a project/namespace
// ErrUserAlreadyHasAccess represents an error where a user already has access to a project
type ErrUserAlreadyHasAccess struct {
UserID int64
ProjectID int64

View File

@ -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 //
/////////////////
@ -176,46 +166,9 @@ func (t *TaskRelationDeletedEvent) Name() string {
return "task.relation.deleted"
}
//////////////////////
// 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"
}
/////////////////
////////////////////
// Project Events //
/////////////////
////////////////////
// ProjectCreatedEvent represents an event where a project has been created
type ProjectCreatedEvent struct {
@ -278,30 +231,6 @@ func (l *ProjectSharedWithTeamEvent) Name() string {
return "project.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"
}
/////////////////
// Team Events //
/////////////////

View File

@ -57,12 +57,12 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
defer dumpWriter.Close()
// Get the data
err = exportProjectsAndTasks(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
}
@ -121,59 +121,44 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
})
}
func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
namspaces, _, _, err := (&Namespace{IsArchived: true}).ReadAll(s, u, "", -1, 0)
if err != nil {
return err
}
namespaceIDs := []int64{}
namespaces := []*NamespaceWithProjectsAndTasks{}
projectMap := make(map[int64]*ProjectWithTasksAndBuckets)
projectIDs := []int64{}
for _, n := range namspaces.([]*NamespaceWithProjects) {
if n.ID < 1 {
// Don't include filters
continue
}
nn := &NamespaceWithProjectsAndTasks{
Namespace: n.Namespace,
Projects: []*ProjectWithTasksAndBuckets{},
}
for _, l := range n.Projects {
ll := &ProjectWithTasksAndBuckets{
Project: *l,
BackgroundFileID: l.BackgroundFileID,
Tasks: []*TaskWithComments{},
}
nn.Projects = append(nn.Projects, ll)
projectMap[l.ID] = ll
projectIDs = append(projectIDs, l.ID)
}
namespaceIDs = append(namespaceIDs, n.ID)
namespaces = append(namespaces, nn)
}
if len(namespaceIDs) == 0 {
return nil
}
func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (taskIDs []int64, err error) {
// Get all projects
projects, err := getProjectsForNamespaces(s, namespaceIDs, true)
rawProjects, _, _, err := getRawProjectsForUser(
s,
&projectOptions{
search: "",
user: u,
page: 0,
perPage: -1,
getArchived: true,
})
if err != nil {
return err
return taskIDs, err
}
tasks, _, _, err := getTasksForProjects(s, projects, u, &taskOptions{
if len(rawProjects) == 0 {
return
}
projects := []*ProjectWithTasksAndBuckets{}
projectsMap := make(map[int64]*ProjectWithTasksAndBuckets, len(rawProjects))
projectIDs := []int64{}
for _, p := range rawProjects {
pp := &ProjectWithTasksAndBuckets{
Project: *p,
}
projects = append(projects, pp)
projectsMap[p.ID] = pp
projectIDs = append(projectIDs, p.ID)
}
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,11 +166,12 @@ func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err
taskMap[t.ID] = &TaskWithComments{
Task: *t,
}
if _, exists := projectMap[t.ProjectID]; !exists {
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
}
projectMap[t.ProjectID].Tasks = append(projectMap[t.ProjectID].Tasks, taskMap[t.ID])
projectsMap[t.ProjectID].Tasks = append(projectsMap[t.ProjectID].Tasks, taskMap[t.ID])
taskIDs = append(taskIDs, t.ID)
}
comments := []*TaskComment{}
@ -212,43 +198,22 @@ func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err
}
for _, b := range buckets {
if _, exists := projectMap[b.ProjectID]; !exists {
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
}
projectMap[b.ProjectID].Buckets = append(projectMap[b.ProjectID].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) {
projects, _, _, err := getRawProjectsForUser(
s,
&projectOptions{
user: u,
page: -1,
},
)
if err != nil {
return err
}
tasks, _, _, err := getRawTasksForProjects(s, projects, 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

View File

@ -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("project_id", getUserProjectsStatement(u.ID).Select("l.id"))),
Where(builder.In("project_id", getUserProjectsStatement(nil, u.ID, "", false).Select("l.id"))),
)
ll := &LabelTask{}

View File

@ -180,7 +180,7 @@ func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*Lab
builder.
Select("id").
From("tasks").
Where(builder.In("project_id", getUserProjectsStatement(opts.GetForUser).Select("l.id"))),
Where(builder.In("project_id", getUserProjectsStatement(nil, opts.GetForUser, "", false).Select("l.id"))),
), cond)
}
if opts.GetUnusedLabels {

View File

@ -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)

View File

@ -35,8 +35,6 @@ import (
func RegisterListeners() {
events.RegisterListener((&ProjectCreatedEvent{}).Name(), &IncreaseProjectCounter{})
events.RegisterListener((&ProjectDeletedEvent{}).Name(), &DecreaseProjectCounter{})
events.RegisterListener((&NamespaceCreatedEvent{}).Name(), &IncreaseNamespaceCounter{})
events.RegisterListener((&NamespaceDeletedEvent{}).Name(), &DecreaseNamespaceCounter{})
events.RegisterListener((&TaskCreatedEvent{}).Name(), &IncreaseTaskCounter{})
events.RegisterListener((&TaskDeletedEvent{}).Name(), &DecreaseTaskCounter{})
events.RegisterListener((&TeamDeletedEvent{}).Name(), &DecreaseTeamCounter{})
@ -540,37 +538,6 @@ func (s *SendProjectCreatedNotification) 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

View File

@ -44,10 +44,7 @@ func GetTables() []interface{} {
&Team{},
&TeamMember{},
&TeamProject{},
&TeamNamespace{},
&Namespace{},
&ProjectUser{},
&NamespaceUser{},
&TaskAssginee{},
&Label{},
&LabelTask{},

View File

@ -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 projects.
NamespacesOnly bool `xorm:"-" json:"-" query:"namespaces_only"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
}
// SharedProjectsPseudoNamespace is a pseudo namespace used to hold shared projects
var SharedProjectsPseudoNamespace = Namespace{
ID: -1,
Title: "Shared Projects",
Description: "Projects 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 projects and tasks
var FavoritesPseudoNamespace = Namespace{
ID: -2,
Title: "Favorites",
Description: "Favorite projects 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 projects
if id == -1 {
return &SharedProjectsPseudoNamespace, 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
}
// NamespaceWithProjects represents a namespace with project meta informations
type NamespaceWithProjects struct {
Namespace `xorm:"extends"`
Projects []*Project `xorm:"-" json:"projects"`
}
type NamespaceWithProjectsAndTasks struct {
Namespace
Projects []*ProjectWithTasksAndBuckets `xorm:"-" json:"projects"`
}
func makeNamespaceSlice(namespaces map[int64]*NamespaceWithProjects, userMap map[int64]*user.User, subscriptions map[int64]*Subscription) []*NamespaceWithProjects {
all := make([]*NamespaceWithProjects, 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.Projects {
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 getNamespacesWithProjects(s *xorm.Session, namespaces *map[int64]*NamespaceWithProjects, 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(&NamespaceWithProjects{})
return numberOfTotalItems, err
}
func getNamespaceOwnerIDs(namespaces map[int64]*NamespaceWithProjects) (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 getProjectsForNamespaces(s *xorm.Session, namespaceIDs []int64, archived bool) ([]*Project, error) {
projects := []*Project{}
projectQuery := s.
OrderBy("position").
In("namespace_id", namespaceIDs)
if !archived {
projectQuery.And("is_archived = false")
}
err := projectQuery.Find(&projects)
return projects, err
}
func getSharedProjectsInNamespace(s *xorm.Session, archived bool, doer *user.User) (sharedProjectsNamespace *NamespaceWithProjects, err error) {
// Create our pseudo namespace to hold the shared projects
sharedProjectsPseudonamespace := SharedProjectsPseudoNamespace
sharedProjectsPseudonamespace.OwnerID = doer.ID
sharedProjectsNamespace = &NamespaceWithProjects{
sharedProjectsPseudonamespace,
[]*Project{},
}
// Get all projects individually shared with our user (not via a namespace)
individualProjects := []*Project{}
iProjectQuery := s.Select("l.*").
Table("projects").
Alias("l").
Join("LEFT", []string{"team_projects", "tl"}, "l.id = tl.project_id").
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tl.team_id").
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_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 {
iProjectQuery.And("l.is_archived = false")
}
err = iProjectQuery.Find(&individualProjects)
if err != nil {
return
}
// Make the namespace -1 so we now later which one it was
// + Append it to all projects we already have
for _, l := range individualProjects {
l.NamespaceID = sharedProjectsNamespace.ID
}
sharedProjectsNamespace.Projects = individualProjects
// Remove the sharedProjectsPseudonamespace if we don't have any shared projects
if len(individualProjects) == 0 {
sharedProjectsNamespace = nil
}
return
}
func getFavoriteProjects(s *xorm.Session, projects []*Project, namespaceIDs []int64, doer *user.User) (favoriteNamespace *NamespaceWithProjects, err error) {
// Create our pseudo namespace with favorite projects
pseudoFavoriteNamespace := FavoritesPseudoNamespace
pseudoFavoriteNamespace.OwnerID = doer.ID
favoriteNamespace = &NamespaceWithProjects{
Namespace: pseudoFavoriteNamespace,
Projects: []*Project{{}},
}
*favoriteNamespace.Projects[0] = FavoritesPseudoProject // Copying the project to be able to modify it later
favoriteNamespace.Projects[0].Owner = doer
for _, project := range projects {
if !project.IsFavorite {
continue
}
favoriteNamespace.Projects = append(favoriteNamespace.Projects, project)
}
// Check if we have any favorites or favorited projects and remove the favorites namespace from the project if not
cond := builder.
Select("tasks.id").
From("tasks").
Join("INNER", "projects", "tasks.project_id = projects.id").
Join("INNER", "namespaces", "projects.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 project, remove that pseudo project from the namespace
if favoriteCount == 0 {
for in, l := range favoriteNamespace.Projects {
if l.ID == FavoritesPseudoProject.ID {
favoriteNamespace.Projects = append(favoriteNamespace.Projects[:in], favoriteNamespace.Projects[in+1:]...)
break
}
}
}
// If we don't have any favorites in the namespace, remove it
if len(favoriteNamespace.Projects) == 0 {
return nil, nil
}
return
}
func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *NamespaceWithProjects, 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 = &NamespaceWithProjects{
Namespace: savedFiltersPseudoNamespace,
Projects: make([]*Project, 0, len(savedFilters)),
}
for _, filter := range savedFilters {
filterProject := filter.toProject()
filterProject.NamespaceID = savedFiltersNamespace.ID
filterProject.Owner = doer
savedFiltersNamespace.Projects = append(savedFiltersNamespace.Projects, filterProject)
}
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 projects."
// @Security JWTKeyAuth
// @Success 200 {array} models.NamespaceWithProjects "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 projects. The key is usually the id of the namespace.
// We're using a map here because it makes a few things like adding projects or removing pseudo namespaces easier.
namespaces := make(map[int64]*NamespaceWithProjects)
//////////////////////////////
// Projects with their namespaces
doer, err := user.GetFromAuth(a)
if err != nil {
return nil, 0, 0, err
}
numberOfTotalItems, err = getNamespacesWithProjects(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 projects
projects, err := getProjectsForNamespaces(s, namespaceIDs, n.IsArchived)
if err != nil {
return nil, 0, 0, err
}
///////////////
// Shared Projects
sharedProjectsNamespace, err := getSharedProjectsInNamespace(s, n.IsArchived, doer)
if err != nil {
return nil, 0, 0, err
}
if sharedProjectsNamespace != nil {
namespaces[sharedProjectsNamespace.ID] = sharedProjectsNamespace
projects = append(projects, sharedProjectsNamespace.Projects...)
}
/////////////////
// Saved Filters
savedFiltersNamespace, err := getSavedFilters(s, doer)
if err != nil {
return nil, 0, 0, err
}
if savedFiltersNamespace != nil {
namespaces[savedFiltersNamespace.ID] = savedFiltersNamespace
projects = append(projects, savedFiltersNamespace.Projects...)
}
/////////////////
// Add project details (favorite state, among other things)
err = addProjectDetails(s, projects, a)
if err != nil {
return
}
/////////////////
// Favorite projects
favoritesNamespace, err := getFavoriteProjects(s, projects, namespaceIDs, doer)
if err != nil {
return nil, 0, 0, err
}
if favoritesNamespace != nil {
namespaces[favoritesNamespace.ID] = favoritesNamespace
}
//////////////////////
// Put it all together
for _, project := range projects {
if project.NamespaceID == SharedProjectsPseudoNamespace.ID || project.NamespaceID == SavedFiltersPseudoNamespace.ID {
// Shared projects and filtered projects are already in the namespace
continue
}
namespaces[project.NamespaceID].Projects = append(namespaces[project.NamespaceID].Projects, project)
}
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, withProjects 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 !withProjects {
return events.Dispatch(namespaceDeleted)
}
// Delete all projects with their tasks
projects, err := GetProjectsByNamespaceID(s, n.ID, &user.User{})
if err != nil {
return
}
if len(projects) == 0 {
return events.Dispatch(namespaceDeleted)
}
// Looping over all projects to let the project handle properly cleaning up the tasks and everything else associated with it.
for _, project := range projects {
err = project.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 projects
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,
})
}

View File

@ -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 == SharedProjectsPseudoNamespace.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 project 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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()
})
}
}

View File

@ -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_project6", 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)
}
})
}
}

View File

@ -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.([]*NamespaceWithProjects)
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 project and namespace are not archived
for _, namespace := range namespaces {
assert.False(t, namespace.IsArchived)
for _, project := range namespace.Projects {
assert.False(t, project.IsArchived)
}
}
})
t.Run("no own shared projects", 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.([]*NamespaceWithProjects)
assert.NotNil(t, namespaces)
assert.Equal(t, int64(-1), namespaces[1].ID) // The third one should be the one with the shared namespaces
sharedProjectOccurences := make(map[int64]int64)
for _, project := range namespaces[1].Projects {
assert.NotEqual(t, user1.ID, project.OwnerID)
sharedProjectOccurences[project.ID]++
}
for projectID, occ := range sharedProjectOccurences {
assert.Equal(t, int64(1), occ, "shared project %d is present %d times, should be 1", projectID, 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.([]*NamespaceWithProjects)
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 projects
for _, namespace := range namespaces {
assert.Nil(t, namespace.Projects)
}
})
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.([]*NamespaceWithProjects)
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.([]*NamespaceWithProjects)
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.([]*NamespaceWithProjects)
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.([]*NamespaceWithProjects)
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.([]*NamespaceWithProjects)
assert.NoError(t, err)
// Assert the first namespace is the favorites namespace and contains projects
assert.Equal(t, FavoritesPseudoNamespace.ID, namespaces[0].ID)
assert.NotEqual(t, 0, namespaces[0].Projects)
})
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.([]*NamespaceWithProjects)
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.([]*NamespaceWithProjects)
assert.NotNil(t, namespaces)
assert.Len(t, namespaces, 2)
assert.Equal(t, int64(7), namespaces[1].ID)
})
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"])
}
})
}
}

View File

@ -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 project",
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 project",
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,
})
}
})
}
}

View File

@ -37,7 +37,7 @@ import (
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 namespace overview.
// 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"`
@ -46,13 +46,16 @@ type Project struct {
// 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:"-"`
NamespaceID int64 `xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace"`
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 or not a project is archived.
// 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
@ -62,7 +65,7 @@ type Project struct {
// 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 namespace. This value depends on the user making the call to the api.
// 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.
@ -83,6 +86,8 @@ type Project struct {
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.
@ -91,7 +96,7 @@ type ProjectWithTasksAndBuckets struct {
}
// TableName returns a better name for the projects table
func (l *Project) TableName() string {
func (p *Project) TableName() string {
return "projects"
}
@ -103,70 +108,42 @@ type ProjectBackgroundType struct {
// ProjectBackgroundUpload represents the project upload background type
const ProjectBackgroundUpload string = "upload"
// FavoritesPseudoProject holds all tasks marked as favorites
var FavoritesPseudoProject = Project{
// SharedProjectsPseudoProject is a pseudo project used to hold shared projects
var SharedProjectsPseudoProject = &Project{
ID: -1,
Title: "Favorites",
Description: "This project has all tasks marked as favorites.",
NamespaceID: FavoritesPseudoNamespace.ID,
IsFavorite: true,
Title: "Shared Projects",
Description: "Projects of other users shared with you via teams or directly.",
Created: time.Now(),
Updated: time.Now(),
}
// GetProjectsByNamespaceID gets all projects in a namespace
func GetProjectsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (projects []*Project, err error) {
switch nID {
case SharedProjectsPseudoNamespace.ID:
nnn, err := getSharedProjectsInNamespace(s, false, doer)
if err != nil {
return nil, err
}
if nnn != nil && nnn.Projects != nil {
projects = nnn.Projects
}
case FavoritesPseudoNamespace.ID:
namespaces := make(map[int64]*NamespaceWithProjects)
_, err := getNamespacesWithProjects(s, &namespaces, "", false, 0, -1, doer.ID)
if err != nil {
return nil, err
}
namespaceIDs, _ := getNamespaceOwnerIDs(namespaces)
ls, err := getProjectsForNamespaces(s, namespaceIDs, false)
if err != nil {
return nil, err
}
nnn, err := getFavoriteProjects(s, ls, namespaceIDs, doer)
if err != nil {
return nil, err
}
if nnn != nil && nnn.Projects != nil {
projects = nnn.Projects
}
case SavedFiltersPseudoNamespace.ID:
nnn, err := getSavedFilters(s, doer)
if err != nil {
return nil, err
}
if nnn != nil && nnn.Projects != nil {
projects = nnn.Projects
}
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(&projects)
}
if err != nil {
return nil, err
}
// 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(),
}
// get more project details
err = addProjectDetails(s, projects, doer)
return projects, err
// 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
@ -184,7 +161,7 @@ func GetProjectsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (proj
// @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 (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
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 {
@ -197,22 +174,66 @@ func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
return projects, 0, 0, err
}
projects, resultCount, totalItems, err := getRawProjectsForUser(
doer, err := user.GetFromAuth(a)
if err != nil {
return nil, 0, 0, err
}
prs, resultCount, totalItems, err := getRawProjectsForUser(
s,
&projectOptions{
search: search,
user: &user.User{ID: a.GetID()},
page: page,
perPage: perPage,
isArchived: l.IsArchived,
search: search,
user: doer,
page: page,
perPage: perPage,
getArchived: p.IsArchived,
})
if err != nil {
return nil, 0, 0, err
}
// Add more project details
err = addProjectDetails(s, projects, a)
return projects, resultCount, totalItems, 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
@ -227,61 +248,59 @@ func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
// @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 (l *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
if l.ID == FavoritesPseudoProject.ID {
if p.ID == FavoritesPseudoProject.ID {
// Already "built" the project in CanRead
return nil
}
// Check for saved filters
if getSavedFilterIDFromProjectID(l.ID) > 0 {
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(l.ID))
if getSavedFilterIDFromProjectID(p.ID) > 0 {
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(p.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
p.Title = sf.Title
p.Description = sf.Description
p.Created = sf.Created
p.Updated = sf.Updated
p.OwnerID = sf.OwnerID
}
// Get project owner
l.Owner, err = user.GetUserByID(s, l.OwnerID)
p.Owner, err = user.GetUserByID(s, p.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)
// 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 {
if !IsErrNamespaceIsArchived(err) && !IsErrProjectIsArchived(err) {
return
}
l.IsArchived = true
p.IsArchived = true
}
}
// Get any background information if there is one set
if l.BackgroundFileID != 0 {
if p.BackgroundFileID != 0 {
// Unsplash image
l.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, l.BackgroundFileID)
p.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, p.BackgroundFileID)
if err != nil && !files.IsErrFileIsNotUnsplashFile(err) {
return
}
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
l.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
p.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
}
}
l.IsFavorite, err = isFavorite(s, l.ID, a, FavoriteKindProject)
p.IsFavorite, err = isFavorite(s, p.ID, a, FavoriteKindProject)
if err != nil {
return
}
l.Subscription, err = GetSubscription(s, SubscriptionEntityProject, l.ID, a)
p.Subscription, err = GetSubscription(s, SubscriptionEntityProject, p.ID, a)
return
}
@ -344,62 +363,31 @@ func GetProjectsByIDs(s *xorm.Session, projectIDs []int64) (projects map[int64]*
}
type projectOptions struct {
search string
user *user.User
page int
perPage int
isArchived bool
search string
user *user.User
page int
perPage int
getArchived bool
}
func getUserProjectsStatement(userID int64) *builder.Builder {
func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search string, getArchived bool) *builder.Builder {
dialect := config.DatabaseType.GetString()
if dialect == "sqlite" {
dialect = builder.SQLITE
}
return builder.Dialect(dialect).
Select("l.*").
From("projects", "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_projects tl", "l.id = tl.project_id").
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
Join("LEFT", "users_projects ul", "ul.project_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 projects only, without any tasks or so
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
}
// 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(
var getArchivedCond builder.Cond = builder.Eq{"1": 1}
if !getArchived {
getArchivedCond = 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, ",")
if search != "" {
vals := strings.Split(search, ",")
for _, val := range vals {
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
@ -410,32 +398,138 @@ func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*P
}
}
filterCond = db.ILIKE("l.title", opts.search)
filterCond = db.ILIKE("l.title", search)
if len(ids) > 0 {
filterCond = builder.In("l.id", ids)
}
// Gets all Projects where the user is either owner or in a team which has access to the project
// Or in a team which has namespace read access
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"
}
query := getUserProjectsStatement(fullUser.ID).
Where(filterCond).
Where(isArchivedCond)
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)
}
err = s.SQL(query).Find(&projects)
currentProjects := []*Project{}
err = s.SQL(query).Find(&currentProjects)
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
}
query = getUserProjectsStatement(fullUser.ID).
Where(filterCond).
Where(isArchivedCond)
totalItems, err = s.
SQL(query.Select("count(*)")).
Count(&Project{})
return projects, len(projects), totalItems, 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
@ -445,30 +539,17 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
}
var ownerIDs []int64
for _, l := range projects {
ownerIDs = append(ownerIDs, l.OwnerID)
}
// Get all project 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 projectIDs []int64
for _, l := range projects {
projectIDs = append(projectIDs, l.ID)
if o, exists := owners[l.OwnerID]; exists {
l.Owner = o
}
if l.BackgroundFileID != 0 {
l.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
}
fileIDs = append(fileIDs, l.BackgroundFileID)
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)
@ -478,19 +559,26 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
subscriptions, err := GetSubscriptions(s, SubscriptionEntityProject, projectIDs, a)
if err != nil {
log.Errorf("An error occurred while getting project subscriptions for a namespace item: %s", err.Error())
subscriptions = make(map[int64]*Subscription)
log.Errorf("An error occurred while getting project subscriptions for a project: %s", err.Error())
subscriptions = make(map[int64][]*Subscription)
}
for _, project := range projects {
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 project.IsFavorite {
if p.IsFavorite {
continue
}
project.IsFavorite = favs[project.ID]
p.IsFavorite = favs[p.ID]
if subscription, exists := subscriptions[project.ID]; exists {
project.Subscription = subscription
if subscription, exists := subscriptions[p.ID]; exists && len(subscription) > 0 {
p.Subscription = subscription[0]
}
}
@ -520,46 +608,37 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
return
}
// NamespaceProject is a meta type to be able to join a project with its namespace
type NamespaceProject struct {
Project Project `xorm:"extends"`
Namespace Namespace `xorm:"extends"`
}
// CheckIsArchived returns an ErrProjectIsArchived or ErrNamespaceIsArchived if the project or its namespace is archived.
func (l *Project) CheckIsArchived(s *xorm.Session) (err error) {
// When creating a new project, we check if the namespace is archived
if l.ID == 0 {
n := &Namespace{ID: l.NamespaceID}
return n.CheckIsArchived(s)
// 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)
}
nl := &NamespaceProject{}
exists, err := s.
Table("projects").
Join("LEFT", "namespaces", "projects.namespace_id = namespaces.id").
Where("projects.id = ? AND (projects.is_archived = true OR namespaces.is_archived = true)", l.ID).
Get(nl)
if p.ID == 0 { // don't check new projects
return nil
}
project, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return
return err
}
if exists && nl.Project.ID != 0 && nl.Project.IsArchived {
return ErrProjectIsArchived{ProjectID: l.ID}
}
if exists && nl.Namespace.ID != 0 && nl.Namespace.IsArchived {
return ErrNamespaceIsArchived{NamespaceID: nl.Namespace.ID}
if project.IsArchived {
return ErrProjectIsArchived{ProjectID: p.ID}
}
return nil
}
func checkProjectBeforeUpdateOrDelete(s *xorm.Session, project *Project) error {
if project.NamespaceID < 0 {
return &ErrProjectCannotBelongToAPseudoNamespace{ProjectID: project.ID, NamespaceID: project.NamespaceID}
if project.ParentProjectID < 0 {
return &ErrProjectCannotBelongToAPseudoParentProject{ProjectID: project.ID, ParentProjectID: project.ParentProjectID}
}
// Check if the namespace exists
if project.NamespaceID > 0 {
_, err := GetNamespaceByID(s, project.NamespaceID)
// Check if the parent project exists
if project.ParentProjectID > 0 {
_, err := GetProjectSimpleByID(s, project.ParentProjectID)
if err != nil {
return err
}
@ -595,7 +674,6 @@ func CreateProject(s *xorm.Session, project *Project, auth web.Auth) (err error)
project.OwnerID = doer.ID
project.Owner = doer
project.ID = 0 // Otherwise only the first time a new project would be created
err = checkProjectBeforeUpdateOrDelete(s, project)
if err != nil {
@ -634,26 +712,28 @@ func CreateProject(s *xorm.Session, project *Project, auth web.Auth) (err error)
})
}
// 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
}
if project.NamespaceID == 0 {
return &ErrProjectMustBelongToANamespace{
ProjectID: project.ID,
NamespaceID: project.NamespaceID,
}
}
// 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",
"namespace_id",
"parent_project_id",
"position",
}
if project.Description != "" {
@ -720,27 +800,27 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje
// @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 (l *Project) Update(s *xorm.Session, a web.Auth) (err error) {
fid := getSavedFilterIDFromProjectID(l.ID)
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 = l.Title
f.Description = l.Description
f.IsFavorite = l.IsFavorite
f.Title = p.Title
f.Description = p.Description
f.IsFavorite = p.IsFavorite
err = f.Update(s, a)
if err != nil {
return err
}
*l = *f.toProject()
*p = *f.toProject()
return nil
}
return UpdateProject(s, l, a, false)
return UpdateProject(s, p, a, false)
}
func updateProjectLastUpdated(s *xorm.Session, project *Project) error {
@ -760,25 +840,24 @@ func updateProjectByTaskID(s *xorm.Session, taskID int64) (err error) {
// Create implements the create method of CRUDable
// @Summary Creates a new project
// @Description Creates a new project in a given namespace. The user needs write-access to the namespace.
// @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 namespaceID path int true "Namespace ID"
// @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 /namespaces/{namespaceID}/projects [put]
func (l *Project) Create(s *xorm.Session, a web.Auth) (err error) {
err = CreateProject(s, l, a)
// @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 l.ReadOne(s, a)
return p.ReadOne(s, a)
}
// Delete implements the delete method of CRUDable
@ -793,22 +872,22 @@ func (l *Project) Create(s *xorm.Session, a web.Auth) (err error) {
// @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 (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
fullList, err := GetProjectSimpleByID(s, l.ID)
fullList, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return
}
// Delete the project
_, err = s.ID(l.ID).Delete(&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{l}, a, &taskOptions{})
tasks, _, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskOptions{})
if err != nil {
return
}
@ -826,19 +905,19 @@ func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
}
return events.Dispatch(&ProjectDeletedEvent{
Project: l,
Project: p,
Doer: a,
})
}
// DeleteBackgroundFileIfExists deletes the list's background file from the db and the filesystem,
// if one exists
func (l *Project) DeleteBackgroundFileIfExists() (err error) {
if l.BackgroundFileID == 0 {
func (p *Project) DeleteBackgroundFileIfExists() (err error) {
if p.BackgroundFileID == 0 {
return
}
file := files.File{ID: l.BackgroundFileID}
file := files.File{ID: p.BackgroundFileID}
return file.Delete()
}

View File

@ -40,30 +40,29 @@ func TestProject_CreateOrUpdate(t *testing.T) {
project := Project{
Title: "test",
Description: "Lorem Ipsum",
NamespaceID: 1,
}
err := project.Create(s, usr)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
db.AssertExists(t, "projects", map[string]interface{}{
"id": project.ID,
"title": project.Title,
"description": project.Description,
"namespace_id": project.NamespaceID,
"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()
project := Project{
Title: "test",
Description: "Lorem Ipsum",
NamespaceID: 999999,
Title: "test",
Description: "Lorem Ipsum",
ParentProjectID: 999999,
}
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) {
@ -73,7 +72,6 @@ func TestProject_CreateOrUpdate(t *testing.T) {
project := Project{
Title: "test",
Description: "Lorem Ipsum",
NamespaceID: 1,
}
err := project.Create(s, usr)
assert.Error(t, err)
@ -87,7 +85,6 @@ func TestProject_CreateOrUpdate(t *testing.T) {
Title: "test",
Description: "Lorem Ipsum",
Identifier: "test1",
NamespaceID: 1,
}
err := project.Create(s, usr)
assert.Error(t, err)
@ -100,17 +97,15 @@ func TestProject_CreateOrUpdate(t *testing.T) {
project := Project{
Title: "приффки фсем",
Description: "Lorem Ipsum",
NamespaceID: 1,
}
err := project.Create(s, usr)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
db.AssertExists(t, "projects", map[string]interface{}{
"id": project.ID,
"title": project.Title,
"description": project.Description,
"namespace_id": project.NamespaceID,
"id": project.ID,
"title": project.Title,
"description": project.Description,
}, false)
})
})
@ -123,7 +118,6 @@ func TestProject_CreateOrUpdate(t *testing.T) {
ID: 1,
Title: "test",
Description: "Lorem Ipsum",
NamespaceID: 1,
}
project.Description = "Lorem Ipsum dolor sit amet."
err := project.Update(s, usr)
@ -131,19 +125,17 @@ func TestProject_CreateOrUpdate(t *testing.T) {
err = s.Commit()
assert.NoError(t, err)
db.AssertExists(t, "projects", map[string]interface{}{
"id": project.ID,
"title": project.Title,
"description": project.Description,
"namespace_id": project.NamespaceID,
"id": project.ID,
"title": project.Title,
"description": project.Description,
}, false)
})
t.Run("nonexistant", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
project := Project{
ID: 99999999,
Title: "test",
NamespaceID: 1,
ID: 99999999,
Title: "test",
}
err := project.Update(s, usr)
assert.Error(t, err)
@ -158,14 +150,13 @@ func TestProject_CreateOrUpdate(t *testing.T) {
Title: "test",
Description: "Lorem Ipsum",
Identifier: "test1",
NamespaceID: 1,
}
err := project.Create(s, usr)
assert.Error(t, 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,
@ -176,10 +167,10 @@ func TestProject_CreateOrUpdate(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
project := Project{
ID: 6,
Title: "Test6",
Description: "Lorem Ipsum",
NamespaceID: 7, // from 6
ID: 6,
Title: "Test6",
Description: "Lorem Ipsum",
ParentProjectID: 7, // from 6
}
can, err := project.CanUpdate(s, usr)
assert.NoError(t, err)
@ -189,41 +180,26 @@ func TestProject_CreateOrUpdate(t *testing.T) {
err = s.Commit()
assert.NoError(t, err)
db.AssertExists(t, "projects", map[string]interface{}{
"id": project.ID,
"title": project.Title,
"description": project.Description,
"namespace_id": project.NamespaceID,
"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()
// project := Project{
// ID: 1,
// Title: "Test1",
// Description: "Lorem Ipsum",
// NamespaceID: 16, // from 1
// }
// can, err := project.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()
project := Project{
ID: 1,
Title: "Test1",
Description: "Lorem Ipsum",
NamespaceID: 2, // from 1
ID: 1,
Title: "Test1",
Description: "Lorem Ipsum",
ParentProjectID: 2, // from 1
}
can, _ := project.CanUpdate(s, usr)
assert.False(t, can) // namespace is not writeable by us
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",
@ -233,14 +209,14 @@ func TestProject_CreateOrUpdate(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
project := Project{
ID: 6,
Title: "Test6",
Description: "Lorem Ipsum",
NamespaceID: -1,
ID: 6,
Title: "Test6",
Description: "Lorem Ipsum",
ParentProjectID: -1,
}
err := project.Update(s, usr)
assert.Error(t, err)
assert.True(t, IsErrProjectCannotBelongToAPseudoNamespace(err))
assert.True(t, IsErrProjectCannotBelongToAPseudoParentProject(err))
})
})
})
@ -266,14 +242,14 @@ func TestProject_Delete(t *testing.T) {
files.InitTestFileFixtures(t)
s := db.NewSession()
project := Project{
ID: 25,
ID: 35,
}
err := project.Delete(s, &user.User{ID: 6})
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
db.AssertMissing(t, "projects", map[string]interface{}{
"id": 25,
"id": 35,
})
db.AssertMissing(t, "files", map[string]interface{}{
"id": 1,
@ -321,15 +297,18 @@ func TestProject_DeleteBackgroundFileIfExists(t *testing.T) {
}
func TestProject_ReadAll(t *testing.T) {
t.Run("all in namespace", func(t *testing.T) {
t.Run("all", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
// Get all projects for our namespace
projects, err := GetProjectsByNamespaceID(s, 1, &user.User{})
projects := []*Project{}
_, _, err := getAllProjectsForUser(s, 1, nil, &projectOptions{}, &projects, 0)
assert.NoError(t, err)
assert.Equal(t, len(projects), 2)
assert.Equal(t, 23, len(projects))
_ = s.Close()
})
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()
@ -340,10 +319,10 @@ func TestProject_ReadAll(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(projects3).Kind(), reflect.Slice)
ls := projects3.([]*Project)
assert.Equal(t, 16, len(ls))
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("projects for nonexistant user", func(t *testing.T) {

View File

@ -28,8 +28,8 @@ import (
type ProjectDuplicate struct {
// The project id of the project to duplicate
ProjectID int64 `json:"-" param:"projectid"`
// The target namespace ID
NamespaceID int64 `json:"namespace_id,omitempty"`
// The target parent project
ParentProjectID int64 `json:"parent_project_id,omitempty"`
// The copied project
Project *Project `json:",omitempty"`
@ -47,23 +47,27 @@ func (ld *ProjectDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bo
return canRead, err
}
// Namespace exists + user has write access to is (-> can create new projects)
ld.Project.NamespaceID = ld.NamespaceID
return ld.Project.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 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 namespace. The user needs read access in the project and write access in the namespace of the new 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 projectID path int true "The project ID to duplicate"
// @Param project body models.ProjectDuplicate true "The target namespace which should hold the copied project."
// @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 namespace"
// @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 /projects/{projectID}/duplicate [put]
//
@ -153,7 +157,7 @@ func (ld *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
}
// Rights / Shares
// To keep it simple(r) we will only copy rights which are directly used with the project, no namespace changes.
// 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 {

View File

@ -37,8 +37,7 @@ func TestProjectDuplicate(t *testing.T) {
}
l := &ProjectDuplicate{
ProjectID: 1,
NamespaceID: 1,
ProjectID: 1,
}
can, err := l.CanCreate(s, u)
assert.NoError(t, err)

View File

@ -17,6 +17,8 @@
package models
import (
"errors"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/builder"
@ -24,15 +26,15 @@ import (
)
// CanWrite return whether the user can write on that project or not
func (l *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
func (p *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
// The favorite project can't be edited
if l.ID == FavoritesPseudoProject.ID {
if p.ID == FavoritesPseudoProject.ID {
return false, nil
}
// Get the project and check the right
originalProject, err := GetProjectSimpleByID(s, l.ID)
originalProject, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return false, err
}
@ -67,66 +69,66 @@ func (l *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
}
// CanRead checks if a user has read access to a project
func (l *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
func (p *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
// The favorite project needs a special treatment
if l.ID == FavoritesPseudoProject.ID {
if p.ID == FavoritesPseudoProject.ID {
owner, err := user.GetFromAuth(a)
if err != nil {
return false, 0, err
}
*l = FavoritesPseudoProject
l.Owner = owner
*p = FavoritesPseudoProject
p.Owner = owner
return true, int(RightRead), nil
}
// Saved Filter Projects need a special case
if getSavedFilterIDFromProjectID(l.ID) > 0 {
sf := &SavedFilter{ID: getSavedFilterIDFromProjectID(l.ID)}
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, l.ID)
originalProject, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return false, 0, err
}
*l = *originalProject
*p = *originalProject
// Check if we're dealing with a share auth
shareAuth, ok := a.(*LinkSharing)
if ok {
return l.ID == shareAuth.ProjectID &&
return p.ID == shareAuth.ProjectID &&
(shareAuth.Right == RightRead || shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), int(shareAuth.Right), nil
}
if l.isOwner(&user.User{ID: a.GetID()}) {
if p.isOwner(&user.User{ID: a.GetID()}) {
return true, int(RightAdmin), nil
}
return l.checkRight(s, a, RightRead, RightWrite, RightAdmin)
return p.checkRight(s, a, RightRead, RightWrite, RightAdmin)
}
// CanUpdate checks if the user can update a project
func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) {
func (p *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) {
// The favorite project can't be edited
if l.ID == FavoritesPseudoProject.ID {
if p.ID == FavoritesPseudoProject.ID {
return false, nil
}
// Get the project
ol, err := GetProjectSimpleByID(s, l.ID)
ol, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return false, err
}
// Check if we're moving the project into a different namespace.
// 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 l.NamespaceID != 0 && l.NamespaceID != ol.NamespaceID {
newNamespace := &Namespace{ID: l.NamespaceID}
can, err := newNamespace.CanWrite(s, a)
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
}
@ -135,7 +137,7 @@ func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er
}
}
fid := getSavedFilterIDFromProjectID(l.ID)
fid := getSavedFilterIDFromProjectID(p.ID)
if fid > 0 {
sf, err := getSavedFilterSimpleByID(s, fid)
if err != nil {
@ -145,34 +147,43 @@ func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er
return sf.CanUpdate(s, a)
}
canUpdate, err = l.CanWrite(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
if IsErrProjectIsArchived(err) && !l.IsArchived {
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 (l *Project) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
return l.IsAdmin(s, a)
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 (l *Project) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
// A user can create a project if they have write access to the namespace
n := &Namespace{ID: l.NamespaceID}
return n.CanWrite(s, a)
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 (l *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
func (p *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
// The favorite project can't be edited
if l.ID == FavoritesPseudoProject.ID {
if p.ID == FavoritesPseudoProject.ID {
return false, nil
}
originalProject, err := GetProjectSimpleByID(s, l.ID)
originalProject, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return false, err
}
@ -194,22 +205,12 @@ func (l *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
}
// Little helper function to check if a user is project owner
func (l *Project) isOwner(u *user.User) bool {
return l.OwnerID == u.ID
func (p *Project) isOwner(u *user.User) bool {
return p.OwnerID == u.ID
}
// Checks n different rights for any given user
func (l *Project) 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 project or not.
*/
func (p *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
var conds []builder.Cond
for _, r := range rights {
@ -219,11 +220,6 @@ func (l *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool
builder.Eq{"ul.user_id": a.GetID()},
builder.Eq{"ul.right": r},
))
// If the namespace this project 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 project was shared directly with the team and the team has the right
@ -231,66 +227,50 @@ func (l *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool
builder.Eq{"tm2.user_id": a.GetID()},
builder.Eq{"tl.right": r},
))
// If the namespace this project 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 allProjectRights struct {
UserNamespace *NamespaceUser `xorm:"extends"`
UserProject *ProjectUser `xorm:"extends"`
TeamNamespace *TeamNamespace `xorm:"extends"`
TeamProject *TeamProject `xorm:"extends"`
NamespaceOwnerID int64 `xorm:"namespaces_owner_id"`
UserProject *ProjectUser `xorm:"extends"`
TeamProject *TeamProject `xorm:"extends"`
}
r := &allProjectRights{}
var maxRight = 0
exists, err := s.
Select("l.*, un.right, ul.right, tn.right, tl.right, n.owner_id as namespaces_owner_id").
Select("p.*, ul.right, tl.right").
Table("projects").
Alias("l").
Alias("p").
// User stuff
Join("LEFT", []string{"users_namespaces", "un"}, "un.namespace_id = l.namespace_id").
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = l.id").
Join("LEFT", []string{"namespaces", "n"}, "n.id = l.namespace_id").
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = p.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_projects", "tl"}, "l.id = tl.project_id").
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{"l.id": l.ID},
builder.Eq{"p.id": p.ID},
)).
Get(r)
// Figure out the max right and return it
if int(r.UserNamespace.Right) > maxRight {
maxRight = int(r.UserNamespace.Right)
// 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.TeamNamespace.Right) > maxRight {
maxRight = int(r.TeamNamespace.Right)
}
if int(r.TeamProject.Right) > maxRight {
maxRight = int(r.TeamProject.Right)
}
if r.NamespaceOwnerID == a.GetID() {
maxRight = int(RightAdmin)
}
return exists, maxRight, err
}

View File

@ -182,7 +182,7 @@ func (tl *TeamProject) Delete(s *xorm.Session, a web.Auth) (err error) {
// @Failure 500 {object} models.Message "Internal error"
// @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 namespace
// Check if the user can read the project
l := &Project{ID: tl.ProjectID}
canRead, _, err := l.CanRead(s, a)
if err != nil {

View File

@ -56,18 +56,6 @@ func TestTeamProject_ReadAll(t *testing.T) {
assert.True(t, IsErrProjectDoesNotExist(err))
_ = s.Close()
})
t.Run("namespace owner", func(t *testing.T) {
tl := TeamProject{
TeamID: 1,
ProjectID: 2,
Right: RightAdmin,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
_, _, _, err := tl.ReadAll(s, u, "", 1, 50)
assert.NoError(t, err)
_ = s.Close()
})
t.Run("no access", func(t *testing.T) {
tl := TeamProject{
TeamID: 1,

View File

@ -31,7 +31,7 @@ import (
// 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" param:"namespace"`
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
@ -55,7 +55,7 @@ func (ProjectUser) TableName() string {
return "users_projects"
}
// UserWithRight represents a user in combination with the right it can have on a project/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"`

View File

@ -52,14 +52,14 @@ func TestProjectUser_Create(t *testing.T) {
errType func(err error) bool
}{
{
name: "ProjectUsers Create normally",
name: "ListUsers Create normally",
fields: fields{
Username: "user1",
ProjectID: 2,
},
},
{
name: "ProjectUsers Create for duplicate",
name: "ListUsers Create for duplicate",
fields: fields{
Username: "user1",
ProjectID: 3,
@ -68,7 +68,7 @@ func TestProjectUser_Create(t *testing.T) {
errType: IsErrUserAlreadyHasAccess,
},
{
name: "ProjectUsers Create with invalid right",
name: "ListUsers Create with invalid right",
fields: fields{
Username: "user1",
ProjectID: 2,
@ -78,7 +78,7 @@ func TestProjectUser_Create(t *testing.T) {
errType: IsErrInvalidRight,
},
{
name: "ProjectUsers Create with inexisting project",
name: "ListUsers Create with inexisting project",
fields: fields{
Username: "user1",
ProjectID: 2000,
@ -87,7 +87,7 @@ func TestProjectUser_Create(t *testing.T) {
errType: IsErrProjectDoesNotExist,
},
{
name: "ProjectUsers Create with inexisting user",
name: "ListUsers Create with inexisting user",
fields: fields{
Username: "user500",
ProjectID: 2,
@ -96,7 +96,7 @@ func TestProjectUser_Create(t *testing.T) {
errType: user.IsErrUserDoesNotExist,
},
{
name: "ProjectUsers Create with the owner as shared user",
name: "ListUsers Create with the owner as shared user",
fields: fields{
Username: "user1",
ProjectID: 1,

View File

@ -16,7 +16,7 @@
package models
// Right defines the rights users/teams can have for projects/namespaces
// Right defines the rights users/teams can have for projects
type Right int
// define unknown right
@ -30,7 +30,7 @@ const (
RightRead Right = iota
// Can write in a like projects and tasks. Cannot create new projects.
RightWrite
// Can manage a project/namespace, can do everything
// Can manage a project, can do everything
RightAdmin
)

View File

@ -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 projects.
// 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.
@ -95,14 +95,14 @@ func getSavedFiltersForUser(s *xorm.Session, auth web.Auth) (filters []*SavedFil
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,
NamespaceID: SavedFiltersPseudoNamespace.ID,
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,
}
}

View File

@ -30,16 +30,15 @@ import (
type SubscriptionEntityType int
const (
SubscriptionEntityUnknown = iota
SubscriptionEntityNamespace
SubscriptionEntityUnknown = iota
SubscriptionEntityNamespace // Kept even though not used anymore since we don't want to manually change all ids
SubscriptionEntityProject
SubscriptionEntityTask
)
const (
entityNamespace = `namespace`
entityProject = `project`
entityTask = `task`
entityProject = `project`
entityTask = `task`
)
// Subscription represents a subscription for an entity
@ -70,8 +69,6 @@ func (sb *Subscription) TableName() string {
func getEntityTypeFromString(entityType string) SubscriptionEntityType {
switch entityType {
case entityNamespace:
return SubscriptionEntityNamespace
case entityProject:
return SubscriptionEntityProject
case entityTask:
@ -84,8 +81,6 @@ 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 SubscriptionEntityProject:
return entityProject
case SubscriptionEntityTask:
@ -96,8 +91,7 @@ func (et SubscriptionEntityType) String() string {
}
func (et SubscriptionEntityType) validate() error {
if et == SubscriptionEntityNamespace ||
et == SubscriptionEntityProject ||
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`, `project` 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`, `project` 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,51 +163,26 @@ 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},
)
}
func getSubscriberCondForEntities(entityType SubscriptionEntityType, entityIDs []int64) (cond builder.Cond) {
if entityType == SubscriptionEntityProject {
cond = builder.Or(
builder.And(
builder.Eq{"entity_id": entityID},
builder.Eq{"entity_type": SubscriptionEntityProject},
),
builder.And(
builder.Eq{"entity_id": builder.
Select("namespace_id").
From("projects").
Where(builder.Eq{"id": entityID}),
},
builder.Eq{"entity_type": SubscriptionEntityNamespace},
),
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("projects").
Join("INNER", "tasks", "projects.id = tasks.project_id").
Where(builder.Eq{"tasks.id": entityID}),
},
builder.Eq{"entity_type": SubscriptionEntityNamespace},
),
builder.And(
builder.Eq{"entity_id": builder.
Select("project_id").
From("tasks").
Where(builder.Eq{"id": entityID}),
Where(builder.In("id", entityIDs)),
// TODO parent project
},
builder.Eq{"entity_type": SubscriptionEntityProject},
),
@ -225,74 +194,179 @@ 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 project the task belongs to and if that also
// doesn't exist it will check for a subscription for the namespace the project 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) (projectsToSubscriptions 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
}
projectsToSubscriptions = make(map[int64]*Subscription)
projectsToSubscriptions = make(map[int64][]*Subscription)
for _, sub := range subscriptions {
sub.Entity = sub.EntityType.String()
projectsToSubscriptions[sub.EntityID] = sub
projectsToSubscriptions[sub.EntityID] = append(projectsToSubscriptions[sub.EntityID], sub)
}
// 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) {
if err := entityType.validate(); err != nil {
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)

View File

@ -30,9 +30,6 @@ 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 SubscriptionEntityProject:
l := &Project{ID: sb.EntityID}
can, _, err = l.CanRead(s, a)

View File

@ -25,10 +25,6 @@ import (
)
func TestSubscriptionGetTypeFromString(t *testing.T) {
t.Run("namespace", func(t *testing.T) {
entityType := getEntityTypeFromString("namespace")
assert.Equal(t, SubscriptionEntityType(SubscriptionEntityNamespace), entityType)
})
t.Run("project", func(t *testing.T) {
entityType := getEntityTypeFromString("project")
assert.Equal(t, SubscriptionEntityType(SubscriptionEntityProject), entityType)
@ -88,22 +84,6 @@ func TestSubscription_Create(t *testing.T) {
assert.Error(t, err)
assert.False(t, can)
})
t.Run("noneixsting namespace", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
sb := &Subscription{
Entity: "namespace",
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 project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -136,21 +116,6 @@ 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) {
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 project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -268,16 +233,6 @@ 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) {
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("project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -300,38 +255,51 @@ func TestSubscriptionGet(t *testing.T) {
})
})
t.Run("inherited", func(t *testing.T) {
t.Run("project from namespace", func(t *testing.T) {
t.Run("project from parent", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Project 6 belongs to namespace 6 where user 6 has subscribed to
sub, err := GetSubscription(s, SubscriptionEntityProject, 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 project 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 parent", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// 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 12 which the user has subscribed to
// 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) {

View File

@ -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 {
ProjectID int64 `param:"project" json:"-"`
Projects []*Project `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"`
@ -178,8 +177,9 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
// 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.
var projects []*Project
if tf.ProjectID == 0 {
tf.Projects, _, _, err = getRawProjectsForUser(
projects, _, _, err = getRawProjectsForUser(
s,
&projectOptions{
user: &user.User{ID: a.GetID()},
@ -190,7 +190,7 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
return nil, 0, 0, err
}
} else {
// Check the project exists and the user has acess on it
// 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 {
@ -199,8 +199,8 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
if !canRead {
return nil, 0, 0, ErrUserDoesNotHaveAccessToProject{ProjectID: tf.ProjectID}
}
tf.Projects = []*Project{{ID: tf.ProjectID}}
projects = []*Project{{ID: tf.ProjectID}}
}
return getTasksForProjects(s, tf.Projects, a, taskopts)
return getTasksForProjects(s, projects, a, taskopts)
}

View File

@ -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...)

View File

@ -17,6 +17,7 @@
package models
import (
"sort"
"testing"
"time"
@ -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,
ProjectID: 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,
ProjectID: 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,
ProjectID: 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,
ProjectID: 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),
@ -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,
@ -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{
@ -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)
}
})
}

View File

@ -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 projects.is_archived = false AND namespaces.is_archived = false", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
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").
Join("LEFT", "namespaces", "projects.namespace_id = namespaces.id").
And("done = false").
Find(&tasks)
if err != nil {

View File

@ -157,7 +157,7 @@ func TestTaskRelation_CanCreate(t *testing.T) {
rel := TaskRelation{
TaskID: 1,
OtherTaskID: 13,
OtherTaskID: 32,
RelationKind: RelationKindSubtask,
}
can, err := rel.CanCreate(s, &user.User{ID: 1})

View File

@ -329,7 +329,7 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
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 getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
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
}
@ -452,13 +452,13 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
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(

View File

@ -241,7 +241,7 @@ func (t *Team) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per
// Create is the handler to create a team
// @Summary Creates a new team
// @Description Creates a new team in a given namespace. The user needs write-access to the namespace.
// @Description Creates a new team.
// @tags team
// @Accept json
// @Produce json
@ -307,12 +307,6 @@ func (t *Team) Delete(s *xorm.Session, a web.Auth) (err error) {
return
}
// Delete team <-> namespace relations
_, err = s.Where("team_id = ?", t.ID).Delete(&TeamNamespace{})
if err != nil {
return
}
// Delete team <-> projects relations
_, err = s.Where("team_id = ?", t.ID).Delete(&TeamProject{})
if err != nil {

View File

@ -58,16 +58,6 @@ func TestTeam_CanDoSomething(t *testing.T) {
},
want: map[string]bool{"CanCreate": true, "IsAdmin": true, "CanRead": true, "CanDelete": true, "CanUpdate": true},
},
{
name: "CanDoSomething for a nonexistant namespace",
fields: fields{
ID: 300,
},
args: args{
a: &user.User{ID: 1},
},
want: map[string]bool{"CanCreate": true, "IsAdmin": false, "CanRead": false, "CanDelete": false, "CanUpdate": false},
},
{
name: "CanDoSomething where the user does not have the rights",
fields: fields{

View File

@ -110,7 +110,7 @@ func TestTeam_ReadAll(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
ts := reflect.ValueOf(teams)
assert.Equal(t, 8, ts.Len())
assert.Equal(t, 5, ts.Len())
})
t.Run("search", func(t *testing.T) {
s := db.NewSession()

View File

@ -48,7 +48,6 @@ func SetupTests() {
"labels",
"link_shares",
"projects",
"namespaces",
"task_assignees",
"task_attachments",
"task_comments",
@ -57,12 +56,10 @@ func SetupTests() {
"tasks",
"team_projects",
"team_members",
"team_namespaces",
"teams",
"users",
"user_tokens",
"users_projects",
"users_namespaces",
"buckets",
"saved_filters",
"subscriptions",

View File

@ -87,45 +87,6 @@ func deleteUsers() {
}
}
func getNamespacesToDelete(s *xorm.Session, u *user.User) (namespacesToDelete []*Namespace, err error) {
namespacesToDelete = []*Namespace{}
nm := &Namespace{IsArchived: true}
res, _, _, err := nm.ReadAll(s, u, "", 1, -1)
if err != nil {
return nil, err
}
if res == nil {
return nil, nil
}
namespaces := res.([]*NamespaceWithProjects)
for _, n := range namespaces {
if n.ID < 0 {
continue
}
hadUsers, err := ensureNamespaceAdminUser(s, &n.Namespace)
if err != nil {
return nil, err
}
if hadUsers {
continue
}
hadTeams, err := ensureNamespaceAdminTeam(s, &n.Namespace)
if err != nil {
return nil, err
}
if hadTeams {
continue
}
namespacesToDelete = append(namespacesToDelete, &n.Namespace)
}
return
}
func getProjectsToDelete(s *xorm.Session, u *user.User) (projectsToDelete []*Project, err error) {
projectsToDelete = []*Project{}
lm := &Project{IsArchived: true}
@ -166,28 +127,15 @@ func getProjectsToDelete(s *xorm.Session, u *user.User) (projectsToDelete []*Pro
return
}
// DeleteUser completely removes a user and all their associated projects, namespaces and tasks.
// DeleteUser completely removes a user and all their associated projects and tasks.
// This action is irrevocable.
// Public to allow deletion from the CLI.
func DeleteUser(s *xorm.Session, u *user.User) (err error) {
namespacesToDelete, err := getNamespacesToDelete(s, u)
if err != nil {
return err
}
projectsToDelete, err := getProjectsToDelete(s, u)
if err != nil {
return err
}
// Delete everything not shared with anybody else
for _, n := range namespacesToDelete {
err = deleteNamespace(s, n, u, false)
if err != nil {
return err
}
}
for _, l := range projectsToDelete {
err = l.Delete(s, u)
if err != nil {
@ -205,58 +153,6 @@ func DeleteUser(s *xorm.Session, u *user.User) (err error) {
})
}
func ensureNamespaceAdminUser(s *xorm.Session, n *Namespace) (hadUsers bool, err error) {
namespaceUsers := []*NamespaceUser{}
err = s.Where("namespace_id = ?", n.ID).Find(&namespaceUsers)
if err != nil {
return
}
if len(namespaceUsers) == 0 {
return false, nil
}
for _, lu := range namespaceUsers {
if lu.Right == RightAdmin {
// Project already has more than one admin, no need to do anything
return true, nil
}
}
firstUser := namespaceUsers[0]
firstUser.Right = RightAdmin
_, err = s.Where("id = ?", firstUser.ID).
Cols("right").
Update(firstUser)
return true, err
}
func ensureNamespaceAdminTeam(s *xorm.Session, n *Namespace) (hadTeams bool, err error) {
namespaceTeams := []*TeamNamespace{}
err = s.Where("namespace_id = ?", n.ID).Find(&namespaceTeams)
if err != nil {
return
}
if len(namespaceTeams) == 0 {
return false, nil
}
for _, lu := range namespaceTeams {
if lu.Right == RightAdmin {
// Project already has more than one admin, no need to do anything
return true, nil
}
}
firstTeam := namespaceTeams[0]
firstTeam.Right = RightAdmin
_, err = s.Where("id = ?", firstTeam.ID).
Cols("right").
Update(firstTeam)
return true, err
}
func ensureProjectAdminUser(s *xorm.Session, l *Project) (hadUsers bool, err error) {
projectUsers := []*ProjectUser{}
err = s.Where("project_id = ?", l.ID).Find(&projectUsers)

View File

@ -46,7 +46,7 @@ func TestDeleteUser(t *testing.T) {
db.AssertExists(t, "projects", map[string]interface{}{"id": 10}, false)
db.AssertExists(t, "projects", map[string]interface{}{"id": 11}, false)
})
t.Run("user with no namespaces", func(t *testing.T) {
t.Run("user with no projects", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
@ -56,6 +56,6 @@ func TestDeleteUser(t *testing.T) {
err := DeleteUser(s, u)
assert.NoError(t, err)
// No assertions for deleted projects and namespaces since that user doesn't have any
// No assertions for deleted projects since that user doesn't have any
})
}

View File

@ -22,14 +22,11 @@ import (
"xorm.io/xorm"
)
// ProjectUIDs hold all kinds of user IDs from accounts who have somehow access to a project
// ProjectUIDs hold all kinds of user IDs from accounts who have access to a project
type ProjectUIDs struct {
ProjectOwnerID int64 `xorm:"projectOwner"`
NamespaceUserID int64 `xorm:"unID"`
ProjectUserID int64 `xorm:"ulID"`
NamespaceOwnerUserID int64 `xorm:"nOwner"`
TeamNamespaceUserID int64 `xorm:"tnUID"`
TeamProjectUserID int64 `xorm:"tlUID"`
ProjectOwnerID int64 `xorm:"projectOwner"`
ProjectUserID int64 `xorm:"ulID"`
TeamProjectUserID int64 `xorm:"tlUID"`
}
// ListUsersFromProject returns a list with all users who have access to a project, regardless of the method which gave them access
@ -37,47 +34,58 @@ func ListUsersFromProject(s *xorm.Session, l *Project, search string) (users []*
userids := []*ProjectUIDs{}
err = s.
Select(`l.owner_id as projectOwner,
un.user_id as unID,
ul.user_id as ulID,
n.owner_id as nOwner,
tm.user_id as tnUID,
tm2.user_id as tlUID`).
Table("projects").
Alias("l").
// User stuff
Join("LEFT", []string{"users_namespaces", "un"}, "un.namespace_id = l.namespace_id").
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_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_projects", "tl"}, "l.id = tl.project_id").
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
// The actual condition
Where(
builder.Or(
builder.Or(builder.Eq{"ul.right": RightRead}),
builder.Or(builder.Eq{"un.right": RightRead}),
builder.Or(builder.Eq{"tl.right": RightRead}),
builder.Or(builder.Eq{"tn.right": RightRead}),
builder.Or(builder.Eq{"ul.right": RightWrite}),
builder.Or(builder.Eq{"un.right": RightWrite}),
builder.Or(builder.Eq{"tl.right": RightWrite}),
builder.Or(builder.Eq{"tn.right": RightWrite}),
builder.Or(builder.Eq{"ul.right": RightAdmin}),
builder.Or(builder.Eq{"un.right": RightAdmin}),
builder.Or(builder.Eq{"tl.right": RightAdmin}),
builder.Or(builder.Eq{"tn.right": RightAdmin}),
),
builder.Eq{"l.id": l.ID},
).
Find(&userids)
var currentProject *Project
currentProject, err = GetProjectSimpleByID(s, l.ID)
if err != nil {
return
return nil, err
}
for {
currentUserIDs := []*ProjectUIDs{}
err = s.
Select(`l.owner_id as projectOwner,
ul.user_id as ulID,
tm2.user_id as tlUID`).
Table("projects").
Alias("l").
// User stuff
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = l.id").
// Team stuff
Join("LEFT", []string{"team_projects", "tl"}, "l.id = tl.project_id").
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
// The actual condition
Where(
builder.Or(
builder.Or(builder.Eq{"ul.right": RightRead}),
builder.Or(builder.Eq{"tl.right": RightRead}),
builder.Or(builder.Eq{"ul.right": RightWrite}),
builder.Or(builder.Eq{"tl.right": RightWrite}),
builder.Or(builder.Eq{"ul.right": RightAdmin}),
builder.Or(builder.Eq{"tl.right": RightAdmin}),
),
builder.Eq{"l.id": currentProject.ID},
).
Find(&currentUserIDs)
if err != nil {
return
}
userids = append(userids, currentUserIDs...)
if currentProject.ParentProjectID == 0 {
break
}
parent, err := GetProjectSimpleByID(s, currentProject.ParentProjectID)
if err != nil && !IsErrProjectDoesNotExist(err) {
return nil, err
}
if err != nil && IsErrProjectDoesNotExist(err) {
break
}
currentProject = parent
}
// Remove duplicates from the project of ids and make it a slice
@ -85,10 +93,7 @@ func ListUsersFromProject(s *xorm.Session, l *Project, search string) (users []*
uidmap[l.OwnerID] = true
for _, u := range userids {
uidmap[u.ProjectUserID] = true
uidmap[u.NamespaceOwnerUserID] = true
uidmap[u.NamespaceUserID] = true
uidmap[u.TeamProjectUserID] = true
uidmap[u.TeamNamespaceUserID] = true
}
uids := make([]int64, 0, len(uidmap))

View File

@ -24,7 +24,7 @@ import (
"gopkg.in/d4l3k/messagediff.v1"
)
func TestProjectUsersFromProject(t *testing.T) {
func TestListUsersFromProject(t *testing.T) {
testuser1 := &user.User{
ID: 1,
Username: "user1",
@ -205,13 +205,13 @@ func TestProjectUsersFromProject(t *testing.T) {
testuser7, // Owner
testuser8, // Shared Via NamespaceTeam readonly
testuser9, // Shared Via NamespaceTeam write
testuser10, // Shared Via NamespaceTeam admin
testuser8, // Shared Via Parent Project Team readonly
testuser9, // Shared Via Parent Project Team write
testuser10, // Shared Via Parent Project Team admin
testuser11, // Shared Via NamespaceUser readonly
testuser12, // Shared Via NamespaceUser write
testuser13, // Shared Via NamespaceUser admin
testuser11, // Shared Via Parent Project User readonly
testuser12, // Shared Via Parent Project User write
testuser13, // Shared Via Parent Project User admin
},
},
{

View File

@ -242,8 +242,8 @@ func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *us
}
}
// And create its namespace
err = models.CreateNewNamespaceForUser(s, u)
// And create their project
err = models.CreateNewProjectForUser(s, u)
if err != nil {
return nil, err
}

View File

@ -54,9 +54,11 @@ func (p *Provider) Search(s *xorm.Session, search string, page int64) (result []
// @Router /projects/{id}/backgrounds/upload [put]
func (p *Provider) Set(s *xorm.Session, img *background.Image, project *models.Project, auth web.Auth) (err error) {
// Remove the old background if one exists
err = project.DeleteBackgroundFileIfExists()
if err != nil {
return err
if project.BackgroundFileID != 0 {
file := files.File{ID: project.BackgroundFileID}
if err := file.Delete(); err != nil {
return err
}
}
file := &files.File{}

View File

@ -30,8 +30,8 @@ import (
)
// InsertFromStructure takes a fully nested Vikunja data structure and a user and then creates everything for this user
// (Namespaces, tasks, etc. Even attachments and relations.)
func InsertFromStructure(str []*models.NamespaceWithProjectsAndTasks, user *user.User) (err error) {
// (Projects, tasks, etc. Even attachments and relations.)
func InsertFromStructure(str []*models.ProjectWithTasksAndBuckets, user *user.User) (err error) {
s := db.NewSession()
defer s.Close()
@ -45,238 +45,19 @@ func InsertFromStructure(str []*models.NamespaceWithProjectsAndTasks, user *user
return s.Commit()
}
func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithProjectsAndTasks, user *user.User) (err error) {
func insertFromStructure(s *xorm.Session, str []*models.ProjectWithTasksAndBuckets, user *user.User) (err error) {
log.Debugf("[creating structure] Creating %d namespaces", len(str))
log.Debugf("[creating structure] Creating %d projects", len(str))
labels := make(map[string]*models.Label)
archivedProjects := []int64{}
archivedNamespaces := []int64{}
// Create all namespaces
for _, n := range str {
n.ID = 0
// Saving the archived status to archive the namespace again after creating it
var wasArchived bool
if n.IsArchived {
n.IsArchived = false
wasArchived = true
}
err = n.Create(s, user)
// Create all projects
for _, p := range str {
p.ID = 0
err = createProjectWithChildren(s, p, 0, &archivedProjects, labels, user)
if err != nil {
return
}
if wasArchived {
archivedNamespaces = append(archivedNamespaces, n.ID)
}
log.Debugf("[creating structure] Created namespace %d", n.ID)
log.Debugf("[creating structure] Creating %d projects", len(n.Projects))
// Create all projects
for _, l := range n.Projects {
// The tasks and bucket slices are going to be reset during the creation of the project so we rescue it here
// to be able to still loop over them aftere the project was created.
tasks := l.Tasks
originalBuckets := l.Buckets
originalBackgroundInformation := l.BackgroundInformation
needsDefaultBucket := false
// Saving the archived status to archive the project again after creating it
var wasArchived bool
if l.IsArchived {
wasArchived = true
l.IsArchived = false
}
l.NamespaceID = n.ID
l.ID = 0
err = l.Create(s, user)
if err != nil {
return
}
if wasArchived {
archivedProjects = append(archivedProjects, l.ID)
}
log.Debugf("[creating structure] Created project %d", l.ID)
bf, is := originalBackgroundInformation.(*bytes.Buffer)
if is {
backgroundFile := bytes.NewReader(bf.Bytes())
log.Debugf("[creating structure] Creating a background file for project %d", l.ID)
err = handler.SaveBackgroundFile(s, user, &l.Project, backgroundFile, "", uint64(backgroundFile.Len()))
if err != nil {
return err
}
log.Debugf("[creating structure] Created a background file for project %d", l.ID)
}
// Create all buckets
buckets := make(map[int64]*models.Bucket) // old bucket id is the key
if len(l.Buckets) > 0 {
log.Debugf("[creating structure] Creating %d buckets", len(l.Buckets))
}
for _, bucket := range originalBuckets {
oldID := bucket.ID
bucket.ID = 0 // We want a new id
bucket.ProjectID = l.ID
err = bucket.Create(s, user)
if err != nil {
return
}
buckets[oldID] = bucket
log.Debugf("[creating structure] Created bucket %d, old ID was %d", bucket.ID, oldID)
}
log.Debugf("[creating structure] Creating %d tasks", len(tasks))
setBucketOrDefault := func(task *models.Task) {
bucket, exists := buckets[task.BucketID]
if exists {
task.BucketID = bucket.ID
} else if task.BucketID > 0 {
log.Debugf("[creating structure] No bucket created for original bucket id %d", task.BucketID)
task.BucketID = 0
}
if !exists || task.BucketID == 0 {
needsDefaultBucket = true
}
}
// Create all tasks
for _, t := range tasks {
setBucketOrDefault(&t.Task)
t.ProjectID = l.ID
err = t.Create(s, user)
if err != nil {
return
}
log.Debugf("[creating structure] Created task %d", t.ID)
if len(t.RelatedTasks) > 0 {
log.Debugf("[creating structure] Creating %d related task kinds", len(t.RelatedTasks))
}
// Create all relation for each task
for kind, tasks := range t.RelatedTasks {
if len(tasks) > 0 {
log.Debugf("[creating structure] Creating %d related tasks for kind %v", len(tasks), kind)
}
for _, rt := range tasks {
// First create the related tasks if they do not exist
if rt.ID == 0 {
setBucketOrDefault(rt)
rt.ProjectID = t.ProjectID
err = rt.Create(s, user)
if err != nil {
return
}
log.Debugf("[creating structure] Created related task %d", rt.ID)
}
// Then create the relation
taskRel := &models.TaskRelation{
TaskID: t.ID,
OtherTaskID: rt.ID,
RelationKind: kind,
}
err = taskRel.Create(s, user)
if err != nil {
return
}
log.Debugf("[creating structure] Created task relation between task %d and %d", t.ID, rt.ID)
}
}
// Create all attachments for each task
if len(t.Attachments) > 0 {
log.Debugf("[creating structure] Creating %d attachments", len(t.Attachments))
}
for _, a := range t.Attachments {
// Check if we have a file to create
if len(a.File.FileContent) > 0 {
a.TaskID = t.ID
fr := io.NopCloser(bytes.NewReader(a.File.FileContent))
err = a.NewAttachment(s, fr, a.File.Name, a.File.Size, user)
if err != nil {
return
}
log.Debugf("[creating structure] Created new attachment %d", a.ID)
}
}
// Create all labels
for _, label := range t.Labels {
// Check if we already have a label with that name + color combination and use it
// If not, create one and save it for later
var lb *models.Label
var exists bool
if label == nil {
continue
}
lb, exists = labels[label.Title+label.HexColor]
if !exists {
err = label.Create(s, user)
if err != nil {
return err
}
log.Debugf("[creating structure] Created new label %d", label.ID)
labels[label.Title+label.HexColor] = label
lb = label
}
lt := &models.LabelTask{
LabelID: lb.ID,
TaskID: t.ID,
}
err = lt.Create(s, user)
if err != nil && !models.IsErrLabelIsAlreadyOnTask(err) {
return err
}
log.Debugf("[creating structure] Associated task %d with label %d", t.ID, lb.ID)
}
for _, comment := range t.Comments {
comment.TaskID = t.ID
err = comment.Create(s, user)
if err != nil {
return
}
log.Debugf("[creating structure] Created new comment %d", comment.ID)
}
}
// All tasks brought their own bucket with them, therefore the newly created default bucket is just extra space
if !needsDefaultBucket {
b := &models.Bucket{ProjectID: l.ID}
bucketsIn, _, _, err := b.ReadAll(s, user, "", 1, 1)
if err != nil {
return err
}
buckets := bucketsIn.([]*models.Bucket)
err = buckets[0].Delete(s, user)
if err != nil && !models.IsErrCannotRemoveLastBucket(err) {
return err
}
}
l.Tasks = tasks
l.Buckets = originalBuckets
return err
}
}
@ -290,17 +71,241 @@ func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithProjectsAnd
}
}
if len(archivedNamespaces) > 0 {
_, err = s.
Cols("is_archived").
In("id", archivedNamespaces).
Update(&models.Namespace{IsArchived: true})
if err != nil {
return err
}
}
log.Debugf("[creating structure] Done inserting new task structure")
return nil
}
func createProjectWithChildren(s *xorm.Session, project *models.ProjectWithTasksAndBuckets, parentProjectID int64, archivedProjectIDs *[]int64, labels map[string]*models.Label, user *user.User) (err error) {
err = createProjectWithEverything(s, project, parentProjectID, archivedProjectIDs, labels, user)
if err != nil {
return err
}
log.Debugf("[creating structure] Created project %d", project.ID)
if len(project.ChildProjects) > 0 {
log.Debugf("[creating structure] Creating %d projects", len(project.ChildProjects))
// Create all projects
for _, cp := range project.ChildProjects {
err = createProjectWithChildren(s, cp, project.ID, archivedProjectIDs, labels, user)
if err != nil {
return err
}
}
}
return
}
func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTasksAndBuckets, parentProjectID int64, archivedProjects *[]int64, labels map[string]*models.Label, user *user.User) (err error) {
// The tasks and bucket slices are going to be reset during the creation of the project, so we rescue it here
// to be able to still loop over them aftere the project was created.
tasks := project.Tasks
originalBuckets := project.Buckets
originalBackgroundInformation := project.BackgroundInformation
needsDefaultBucket := false
// Saving the archived status to archive the project again after creating it
var wasArchived bool
if project.IsArchived {
wasArchived = true
project.IsArchived = false
}
project.ParentProjectID = parentProjectID
project.ID = 0
err = project.Create(s, user)
if err != nil {
return
}
if wasArchived {
*archivedProjects = append(*archivedProjects, project.ID)
}
log.Debugf("[creating structure] Created project %d", project.ID)
bf, is := originalBackgroundInformation.(*bytes.Buffer)
if is {
backgroundFile := bytes.NewReader(bf.Bytes())
log.Debugf("[creating structure] Creating a background file for project %d", project.ID)
err = handler.SaveBackgroundFile(s, user, &project.Project, backgroundFile, "", uint64(backgroundFile.Len()))
if err != nil {
return err
}
log.Debugf("[creating structure] Created a background file for project %d", project.ID)
}
// Create all buckets
buckets := make(map[int64]*models.Bucket) // old bucket id is the key
if len(project.Buckets) > 0 {
log.Debugf("[creating structure] Creating %d buckets", len(project.Buckets))
}
for _, bucket := range originalBuckets {
oldID := bucket.ID
bucket.ID = 0 // We want a new id
bucket.ProjectID = project.ID
err = bucket.Create(s, user)
if err != nil {
return
}
buckets[oldID] = bucket
log.Debugf("[creating structure] Created bucket %d, old ID was %d", bucket.ID, oldID)
}
log.Debugf("[creating structure] Creating %d tasks", len(tasks))
setBucketOrDefault := func(task *models.Task) {
bucket, exists := buckets[task.BucketID]
if exists {
task.BucketID = bucket.ID
} else if task.BucketID > 0 {
log.Debugf("[creating structure] No bucket created for original bucket id %d", task.BucketID)
task.BucketID = 0
}
if !exists || task.BucketID == 0 {
needsDefaultBucket = true
}
}
// Create all tasks
for _, t := range tasks {
setBucketOrDefault(&t.Task)
t.ProjectID = project.ID
err = t.Create(s, user)
if err != nil {
return
}
log.Debugf("[creating structure] Created task %d", t.ID)
if len(t.RelatedTasks) > 0 {
log.Debugf("[creating structure] Creating %d related task kinds", len(t.RelatedTasks))
}
// Create all relation for each task
for kind, tasks := range t.RelatedTasks {
if len(tasks) > 0 {
log.Debugf("[creating structure] Creating %d related tasks for kind %v", len(tasks), kind)
}
for _, rt := range tasks {
// First create the related tasks if they do not exist
if rt.ID == 0 {
setBucketOrDefault(rt)
rt.ProjectID = t.ProjectID
err = rt.Create(s, user)
if err != nil {
return
}
log.Debugf("[creating structure] Created related task %d", rt.ID)
}
// Then create the relation
taskRel := &models.TaskRelation{
TaskID: t.ID,
OtherTaskID: rt.ID,
RelationKind: kind,
}
err = taskRel.Create(s, user)
if err != nil {
return
}
log.Debugf("[creating structure] Created task relation between task %d and %d", t.ID, rt.ID)
}
}
// Create all attachments for each task
if len(t.Attachments) > 0 {
log.Debugf("[creating structure] Creating %d attachments", len(t.Attachments))
}
for _, a := range t.Attachments {
// Check if we have a file to create
if len(a.File.FileContent) > 0 {
a.TaskID = t.ID
fr := io.NopCloser(bytes.NewReader(a.File.FileContent))
err = a.NewAttachment(s, fr, a.File.Name, a.File.Size, user)
if err != nil {
return
}
log.Debugf("[creating structure] Created new attachment %d", a.ID)
}
}
// Create all labels
for _, label := range t.Labels {
// Check if we already have a label with that name + color combination and use it
// If not, create one and save it for later
var lb *models.Label
var exists bool
if label == nil {
continue
}
lb, exists = labels[label.Title+label.HexColor]
if !exists {
err = label.Create(s, user)
if err != nil {
return err
}
log.Debugf("[creating structure] Created new label %d", label.ID)
labels[label.Title+label.HexColor] = label
lb = label
}
lt := &models.LabelTask{
LabelID: lb.ID,
TaskID: t.ID,
}
err = lt.Create(s, user)
if err != nil && !models.IsErrLabelIsAlreadyOnTask(err) {
return err
}
log.Debugf("[creating structure] Associated task %d with label %d", t.ID, lb.ID)
}
for _, comment := range t.Comments {
comment.TaskID = t.ID
comment.ID = 0
err = comment.Create(s, user)
if err != nil {
return
}
log.Debugf("[creating structure] Created new comment %d", comment.ID)
}
}
// All tasks brought their own bucket with them, therefore the newly created default bucket is just extra space
if !needsDefaultBucket {
b := &models.Bucket{ProjectID: project.ID}
bucketsIn, _, _, err := b.ReadAll(s, user, "", 1, 1)
if err != nil {
return err
}
buckets := bucketsIn.([]*models.Bucket)
var newBacklogBucket *models.Bucket
for _, b := range buckets {
if b.Title == "Backlog" {
newBacklogBucket = b
break
}
}
err = newBacklogBucket.Delete(s, user)
if err != nil && !models.IsErrCannotRemoveLastBucket(err) {
return err
}
}
project.Tasks = tasks
project.Buckets = originalBuckets
return nil
}

View File

@ -32,13 +32,20 @@ func TestInsertFromStructure(t *testing.T) {
}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
testStructure := []*models.NamespaceWithProjectsAndTasks{
testStructure := []*models.ProjectWithTasksAndBuckets{
{
Namespace: models.Namespace{
Project: models.Project{
Title: "Test1",
Description: "Lorem Ipsum",
},
Projects: []*models.ProjectWithTasksAndBuckets{
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Task on parent",
},
},
},
ChildProjects: []*models.ProjectWithTasksAndBuckets{
{
Project: models.Project{
Title: "Testproject1",
@ -129,23 +136,22 @@ func TestInsertFromStructure(t *testing.T) {
}
err := InsertFromStructure(testStructure, u)
assert.NoError(t, err)
db.AssertExists(t, "namespaces", map[string]interface{}{
"title": testStructure[0].Namespace.Title,
"description": testStructure[0].Namespace.Description,
}, false)
db.AssertExists(t, "projects", map[string]interface{}{
"title": testStructure[0].Projects[0].Title,
"description": testStructure[0].Projects[0].Description,
"title": testStructure[0].ChildProjects[0].Title,
"description": testStructure[0].ChildProjects[0].Description,
}, false)
db.AssertExists(t, "tasks", map[string]interface{}{
"title": testStructure[0].Projects[0].Tasks[5].Title,
"bucket_id": testStructure[0].Projects[0].Buckets[0].ID,
"title": testStructure[0].ChildProjects[0].Tasks[5].Title,
"bucket_id": testStructure[0].ChildProjects[0].Buckets[0].ID,
}, false)
db.AssertMissing(t, "tasks", map[string]interface{}{
"title": testStructure[0].Projects[0].Tasks[6].Title,
"title": testStructure[0].ChildProjects[0].Tasks[6].Title,
"bucket_id": 1111, // No task with that bucket should exist
})
assert.NotEqual(t, 0, testStructure[0].Projects[0].Tasks[0].BucketID) // Should get the default bucket
assert.NotEqual(t, 0, testStructure[0].Projects[0].Tasks[6].BucketID) // Should get the default bucket
db.AssertExists(t, "tasks", map[string]interface{}{
"title": testStructure[0].Tasks[0].Title,
}, false)
assert.NotEqual(t, 0, testStructure[0].ChildProjects[0].Tasks[0].BucketID) // Should get the default bucket
assert.NotEqual(t, 0, testStructure[0].ChildProjects[0].Tasks[6].BucketID) // Should get the default bucket
})
}

View File

@ -259,15 +259,15 @@ func getMicrosoftTodoData(token string) (microsoftTodoData []*project, err error
return
}
func convertMicrosoftTodoData(todoData []*project) (vikunjsStructure []*models.NamespaceWithProjectsAndTasks, err error) {
func convertMicrosoftTodoData(todoData []*project) (vikunjsStructure []*models.ProjectWithTasksAndBuckets, err error) {
// One namespace with all projects
vikunjsStructure = []*models.NamespaceWithProjectsAndTasks{
// One project with all child projects
vikunjsStructure = []*models.ProjectWithTasksAndBuckets{
{
Namespace: models.Namespace{
Project: models.Project{
Title: "Migrated from Microsoft Todo",
},
Projects: []*models.ProjectWithTasksAndBuckets{},
ChildProjects: []*models.ProjectWithTasksAndBuckets{},
},
}
@ -362,7 +362,7 @@ func convertMicrosoftTodoData(todoData []*project) (vikunjsStructure []*models.N
log.Debugf("[Microsoft Todo Migration] Done converted %d tasks", len(l.Tasks))
}
vikunjsStructure[0].Projects = append(vikunjsStructure[0].Projects, project)
vikunjsStructure[0].ChildProjects = append(vikunjsStructure[0].ChildProjects, project)
log.Debugf("[Microsoft Todo Migration] Done converting project %s", l.ID)
}

View File

@ -102,12 +102,12 @@ func TestConverting(t *testing.T) {
},
}
expectedHierachie := []*models.NamespaceWithProjectsAndTasks{
expectedHierachie := []*models.ProjectWithTasksAndBuckets{
{
Namespace: models.Namespace{
Project: models.Project{
Title: "Migrated from Microsoft Todo",
},
Projects: []*models.ProjectWithTasksAndBuckets{
ChildProjects: []*models.ProjectWithTasksAndBuckets{
{
Project: models.Project{
Title: "Project 1",

View File

@ -41,7 +41,7 @@ type Migrator struct {
type tickTickTask struct {
FolderName string `csv:"Folder Name"`
ListName string `csv:"List Name"`
ProjectName string `csv:"List Name"`
Title string `csv:"Title"`
TagsList string `csv:"Tags"`
Tags []string `csv:"-"`
@ -105,21 +105,21 @@ func parseDurationPart(value string, unit time.Duration) time.Duration {
return 0
}
func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.NamespaceWithProjectsAndTasks) {
namespace := &models.NamespaceWithProjectsAndTasks{
Namespace: models.Namespace{
func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.ProjectWithTasksAndBuckets) {
parent := &models.ProjectWithTasksAndBuckets{
Project: models.Project{
Title: "Migrated from TickTick",
},
Projects: []*models.ProjectWithTasksAndBuckets{},
ChildProjects: []*models.ProjectWithTasksAndBuckets{},
}
projects := make(map[string]*models.ProjectWithTasksAndBuckets)
for _, t := range tasks {
_, has := projects[t.ListName]
_, has := projects[t.ProjectName]
if !has {
projects[t.ListName] = &models.ProjectWithTasksAndBuckets{
projects[t.ProjectName] = &models.ProjectWithTasksAndBuckets{
Project: models.Project{
Title: t.ListName,
Title: t.ProjectName,
},
}
}
@ -158,18 +158,18 @@ func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.Namespace
}
}
projects[t.ListName].Tasks = append(projects[t.ListName].Tasks, task)
projects[t.ProjectName].Tasks = append(projects[t.ProjectName].Tasks, task)
}
for _, l := range projects {
namespace.Projects = append(namespace.Projects, l)
parent.ChildProjects = append(parent.ChildProjects, l)
}
sort.Slice(namespace.Projects, func(i, j int) bool {
return namespace.Projects[i].Title < namespace.Projects[j].Title
sort.Slice(parent.ChildProjects, func(i, j int) bool {
return parent.ChildProjects[i].Title < parent.ChildProjects[j].Title
})
return []*models.NamespaceWithProjectsAndTasks{namespace}
return []*models.ProjectWithTasksAndBuckets{parent}
}
// Name is used to get the name of the ticktick migration - we're using the docs here to annotate the status route.

View File

@ -40,76 +40,76 @@ func TestConvertTicktickTasksToVikunja(t *testing.T) {
tickTickTasks := []*tickTickTask{
{
TaskID: 1,
ParentID: 0,
ListName: "Project 1",
Title: "Test task 1",
Tags: []string{"label1", "label2"},
Content: "Lorem Ipsum Dolor sit amet",
StartDate: time1,
DueDate: time2,
Reminder: duration,
Repeat: "FREQ=WEEKLY;INTERVAL=1;UNTIL=20190117T210000Z",
Status: "0",
Order: -1099511627776,
TaskID: 1,
ParentID: 0,
ProjectName: "Project 1",
Title: "Test task 1",
Tags: []string{"label1", "label2"},
Content: "Lorem Ipsum Dolor sit amet",
StartDate: time1,
DueDate: time2,
Reminder: duration,
Repeat: "FREQ=WEEKLY;INTERVAL=1;UNTIL=20190117T210000Z",
Status: "0",
Order: -1099511627776,
},
{
TaskID: 2,
ParentID: 1,
ListName: "Project 1",
ProjectName: "Project 1",
Title: "Test task 2",
Status: "1",
CompletedTime: time3,
Order: -1099511626,
},
{
TaskID: 3,
ParentID: 0,
ListName: "Project 1",
Title: "Test task 3",
Tags: []string{"label1", "label2", "other label"},
StartDate: time1,
DueDate: time2,
Reminder: duration,
Status: "0",
Order: -109951627776,
TaskID: 3,
ParentID: 0,
ProjectName: "Project 1",
Title: "Test task 3",
Tags: []string{"label1", "label2", "other label"},
StartDate: time1,
DueDate: time2,
Reminder: duration,
Status: "0",
Order: -109951627776,
},
{
TaskID: 4,
ParentID: 0,
ListName: "Project 2",
Title: "Test task 4",
Status: "0",
Order: -109951627777,
TaskID: 4,
ParentID: 0,
ProjectName: "Project 2",
Title: "Test task 4",
Status: "0",
Order: -109951627777,
},
}
vikunjaTasks := convertTickTickToVikunja(tickTickTasks)
assert.Len(t, vikunjaTasks, 1)
assert.Len(t, vikunjaTasks[0].Projects, 2)
assert.Len(t, vikunjaTasks[0].ChildProjects, 2)
assert.Len(t, vikunjaTasks[0].Projects[0].Tasks, 3)
assert.Equal(t, vikunjaTasks[0].Projects[0].Title, tickTickTasks[0].ListName)
assert.Len(t, vikunjaTasks[0].ChildProjects[0].Tasks, 3)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Title, tickTickTasks[0].ProjectName)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Title, tickTickTasks[0].Title)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Description, tickTickTasks[0].Content)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].StartDate, tickTickTasks[0].StartDate.Time)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].EndDate, tickTickTasks[0].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].DueDate, tickTickTasks[0].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Labels, []*models.Label{
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Title, tickTickTasks[0].Title)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Description, tickTickTasks[0].Content)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].StartDate, tickTickTasks[0].StartDate.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].EndDate, tickTickTasks[0].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].DueDate, tickTickTasks[0].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Labels, []*models.Label{
{Title: "label1"},
{Title: "label2"},
})
//assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Reminders, tickTickTasks[0].) // TODO
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Position, tickTickTasks[0].Order)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Done, false)
//assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Reminders, tickTickTasks[0].) // TODO
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Position, tickTickTasks[0].Order)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Done, false)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[1].Title, tickTickTasks[1].Title)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[1].Position, tickTickTasks[1].Order)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[1].Done, true)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[1].DoneAt, tickTickTasks[1].CompletedTime.Time)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[1].RelatedTasks, models.RelatedTaskMap{
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[1].Title, tickTickTasks[1].Title)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[1].Position, tickTickTasks[1].Order)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[1].Done, true)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[1].DoneAt, tickTickTasks[1].CompletedTime.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[1].RelatedTasks, models.RelatedTaskMap{
models.RelationKindParenttask: []*models.Task{
{
ID: tickTickTasks[1].ParentID,
@ -117,23 +117,23 @@ func TestConvertTicktickTasksToVikunja(t *testing.T) {
},
})
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[2].Title, tickTickTasks[2].Title)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[2].Description, tickTickTasks[2].Content)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[2].StartDate, tickTickTasks[2].StartDate.Time)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[2].EndDate, tickTickTasks[2].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[2].DueDate, tickTickTasks[2].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[2].Labels, []*models.Label{
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].Title, tickTickTasks[2].Title)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].Description, tickTickTasks[2].Content)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].StartDate, tickTickTasks[2].StartDate.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].EndDate, tickTickTasks[2].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].DueDate, tickTickTasks[2].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].Labels, []*models.Label{
{Title: "label1"},
{Title: "label2"},
{Title: "other label"},
})
//assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Reminders, tickTickTasks[0].) // TODO
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[2].Position, tickTickTasks[2].Order)
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[2].Done, false)
//assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Reminders, tickTickTasks[0].) // TODO
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].Position, tickTickTasks[2].Order)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].Done, false)
assert.Len(t, vikunjaTasks[0].Projects[1].Tasks, 1)
assert.Equal(t, vikunjaTasks[0].Projects[1].Title, tickTickTasks[3].ListName)
assert.Len(t, vikunjaTasks[0].ChildProjects[1].Tasks, 1)
assert.Equal(t, vikunjaTasks[0].ChildProjects[1].Title, tickTickTasks[3].ProjectName)
assert.Equal(t, vikunjaTasks[0].Projects[1].Tasks[0].Title, tickTickTasks[3].Title)
assert.Equal(t, vikunjaTasks[0].Projects[1].Tasks[0].Position, tickTickTasks[3].Order)
assert.Equal(t, vikunjaTasks[0].ChildProjects[1].Tasks[0].Title, tickTickTasks[3].Title)
assert.Equal(t, vikunjaTasks[0].ChildProjects[1].Tasks[0].Position, tickTickTasks[3].Order)
}

View File

@ -251,10 +251,10 @@ func parseDate(dateString string) (date time.Time, err error) {
return date, err
}
func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVikunjaHierachie []*models.NamespaceWithProjectsAndTasks, err error) {
func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVikunjaHierachie []*models.ProjectWithTasksAndBuckets, err error) {
newNamespace := &models.NamespaceWithProjectsAndTasks{
Namespace: models.Namespace{
parent := &models.ProjectWithTasksAndBuckets{
Project: models.Project{
Title: "Migrated from todoist",
},
}
@ -281,7 +281,7 @@ func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVi
lists[p.ID] = project
newNamespace.Projects = append(newNamespace.Projects, project)
parent.ChildProjects = append(parent.ChildProjects, project)
}
sort.Slice(sync.Sections, func(i, j int) bool {
@ -474,9 +474,7 @@ func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVi
tasks[r.ItemID].Reminders = append(tasks[r.ItemID].Reminders, date.In(config.GetTimeZone()))
}
return []*models.NamespaceWithProjectsAndTasks{
newNamespace,
}, err
return []*models.ProjectWithTasksAndBuckets{parent}, err
}
func getAccessTokenFromAuthToken(authToken string) (accessToken string, err error) {

View File

@ -363,12 +363,12 @@ func TestConvertTodoistToVikunja(t *testing.T) {
},
}
expectedHierachie := []*models.NamespaceWithProjectsAndTasks{
expectedHierachie := []*models.ProjectWithTasksAndBuckets{
{
Namespace: models.Namespace{
Project: models.Project{
Title: "Migrated from todoist",
},
Projects: []*models.ProjectWithTasksAndBuckets{
ChildProjects: []*models.ProjectWithTasksAndBuckets{
{
Project: models.Project{
Title: "Project1",

View File

@ -162,16 +162,16 @@ func getTrelloData(token string) (trelloData []*trello.Board, err error) {
// Converts all previously obtained data from trello into the vikunja format.
// `trelloData` should contain all boards with their projects and cards respectively.
func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullVikunjaHierachie []*models.NamespaceWithProjectsAndTasks, err error) {
func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullVikunjaHierachie []*models.ProjectWithTasksAndBuckets, err error) {
log.Debugf("[Trello Migration] ")
fullVikunjaHierachie = []*models.NamespaceWithProjectsAndTasks{
fullVikunjaHierachie = []*models.ProjectWithTasksAndBuckets{
{
Namespace: models.Namespace{
Project: models.Project{
Title: "Imported from Trello",
},
Projects: []*models.ProjectWithTasksAndBuckets{},
ChildProjects: []*models.ProjectWithTasksAndBuckets{},
},
}
@ -300,7 +300,7 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullV
log.Debugf("[Trello Migration] Converted all cards to tasks for board %s", board.ID)
fullVikunjaHierachie[0].Projects = append(fullVikunjaHierachie[0].Projects, project)
fullVikunjaHierachie[0].ChildProjects = append(fullVikunjaHierachie[0].ChildProjects, project)
}
return

View File

@ -187,12 +187,12 @@ func TestConvertTrelloToVikunja(t *testing.T) {
}
trelloData[0].Prefs.BackgroundImage = "https://vikunja.io/testimage.jpg" // Using an image which we are hosting, so it'll still be up
expectedHierachie := []*models.NamespaceWithProjectsAndTasks{
expectedHierachie := []*models.ProjectWithTasksAndBuckets{
{
Namespace: models.Namespace{
Project: models.Project{
Title: "Imported from Trello",
},
Projects: []*models.ProjectWithTasksAndBuckets{
ChildProjects: []*models.ProjectWithTasksAndBuckets{
{
Project: models.Project{
Title: "TestBoard",

View File

@ -30,6 +30,8 @@ import (
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/migration"
"code.vikunja.io/api/pkg/user"
"github.com/hashicorp/go-version"
)
const logPrefix = "[Vikunja File Import] "
@ -71,6 +73,7 @@ func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) er
var dataFile *zip.File
var filterFile *zip.File
var versionFile *zip.File
storedFiles := make(map[int64]*zip.File)
for _, f := range r.File {
if strings.HasPrefix(f.Name, "files/") {
@ -92,6 +95,10 @@ func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) er
filterFile = f
log.Debugf(logPrefix + "Found a filter file")
}
if f.Name == "VERSION" {
versionFile = f
log.Debugf(logPrefix + "Found a version file")
}
}
if dataFile == nil {
@ -100,6 +107,31 @@ func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) er
log.Debugf(logPrefix + "")
//////
// Check if we're able to import this dump
vf, err := versionFile.Open()
if err != nil {
return fmt.Errorf("could not open version file: %w", err)
}
var bufVersion bytes.Buffer
if _, err := bufVersion.ReadFrom(vf); err != nil {
return fmt.Errorf("could not read version file: %w", err)
}
dumpedVersion, err := version.NewVersion(bufVersion.String())
if err != nil {
return err
}
minVersion, err := version.NewVersion("0.20.1+61")
if err != nil {
return err
}
if dumpedVersion.LessThan(minVersion) {
return fmt.Errorf("export was created with an older version, need at least %s but the export needs at least %s", dumpedVersion, minVersion)
}
//////
// Import the bulk of Vikunja data
df, err := dataFile.Open()
@ -113,52 +145,19 @@ func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) er
return fmt.Errorf("could not read data file: %w", err)
}
namespaces := []*models.NamespaceWithProjectsAndTasks{}
if err := json.Unmarshal(bufData.Bytes(), &namespaces); err != nil {
projects := []*models.ProjectWithTasksAndBuckets{}
if err := json.Unmarshal(bufData.Bytes(), &projects); err != nil {
return fmt.Errorf("could not read data: %w", err)
}
for _, n := range namespaces {
for _, l := range n.Projects {
if b, exists := storedFiles[l.BackgroundFileID]; exists {
bf, err := b.Open()
if err != nil {
return fmt.Errorf("could not open project background file %d for reading: %w", l.BackgroundFileID, err)
}
var buf bytes.Buffer
if _, err := buf.ReadFrom(bf); err != nil {
return fmt.Errorf("could not read project background file %d: %w", l.BackgroundFileID, err)
}
l.BackgroundInformation = &buf
}
for _, t := range l.Tasks {
for _, label := range t.Labels {
label.ID = 0
}
for _, comment := range t.Comments {
comment.ID = 0
}
for _, attachment := range t.Attachments {
af, err := storedFiles[attachment.File.ID].Open()
if err != nil {
return fmt.Errorf("could not open attachment %d for reading: %w", attachment.ID, err)
}
var buf bytes.Buffer
if _, err := buf.ReadFrom(af); err != nil {
return fmt.Errorf("could not read attachment %d: %w", attachment.ID, err)
}
attachment.ID = 0
attachment.File.ID = 0
attachment.File.FileContent = buf.Bytes()
}
}
for _, p := range projects {
err = addDetailsToProjectAndChildren(p, storedFiles)
if err != nil {
return err
}
}
err = migration.InsertFromStructure(namespaces, user)
err = migration.InsertFromStructure(projects, user)
if err != nil {
return fmt.Errorf("could not insert data: %w", err)
}
@ -202,3 +201,59 @@ func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) er
return s.Commit()
}
func addDetailsToProjectAndChildren(p *models.ProjectWithTasksAndBuckets, storedFiles map[int64]*zip.File) (err error) {
err = addDetailsToProject(p, storedFiles)
if err != nil {
return err
}
for _, cp := range p.ChildProjects {
err = addDetailsToProjectAndChildren(cp, storedFiles)
if err != nil {
return
}
}
return
}
func addDetailsToProject(l *models.ProjectWithTasksAndBuckets, storedFiles map[int64]*zip.File) (err error) {
if b, exists := storedFiles[l.BackgroundFileID]; exists {
bf, err := b.Open()
if err != nil {
return fmt.Errorf("could not open project background file %d for reading: %w", l.BackgroundFileID, err)
}
var buf bytes.Buffer
if _, err := buf.ReadFrom(bf); err != nil {
return fmt.Errorf("could not read project background file %d: %w", l.BackgroundFileID, err)
}
l.BackgroundInformation = &buf
}
for _, t := range l.Tasks {
for _, label := range t.Labels {
label.ID = 0
}
for _, comment := range t.Comments {
comment.ID = 0
}
for _, attachment := range t.Attachments {
af, err := storedFiles[attachment.File.ID].Open()
if err != nil {
return fmt.Errorf("could not open attachment %d for reading: %w", attachment.ID, err)
}
var buf bytes.Buffer
if _, err := buf.ReadFrom(af); err != nil {
return fmt.Errorf("could not read attachment %d: %w", attachment.ID, err)
}
attachment.ID = 0
attachment.File.ID = 0
attachment.File.FileContent = buf.Bytes()
}
}
return
}

View File

@ -27,53 +27,71 @@ import (
)
func TestVikunjaFileMigrator_Migrate(t *testing.T) {
db.LoadAndAssertFixtures(t)
t.Run("migrate successfully", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
m := &FileMigrator{}
u := &user.User{ID: 1}
m := &FileMigrator{}
u := &user.User{ID: 1}
f, err := os.Open(config.ServiceRootpath.GetString() + "/pkg/modules/migration/vikunja-file/export.zip")
if err != nil {
t.Fatalf("Could not open file: %s", err)
}
defer f.Close()
s, err := f.Stat()
if err != nil {
t.Fatalf("Could not stat file: %s", err)
}
f, err := os.Open(config.ServiceRootpath.GetString() + "/pkg/modules/migration/vikunja-file/export.zip")
if err != nil {
t.Fatalf("Could not open file: %s", err)
}
defer f.Close()
s, err := f.Stat()
if err != nil {
t.Fatalf("Could not stat file: %s", err)
}
err = m.Migrate(u, f, s.Size())
assert.NoError(t, err)
db.AssertExists(t, "namespaces", map[string]interface{}{
"title": "test",
"owner_id": u.ID,
}, false)
db.AssertExists(t, "projects", map[string]interface{}{
"title": "Test project",
"owner_id": u.ID,
}, false)
db.AssertExists(t, "projects", map[string]interface{}{
"title": "A project with a background",
"owner_id": u.ID,
}, false)
db.AssertExists(t, "tasks", map[string]interface{}{
"title": "Some other task",
"created_by_id": u.ID,
}, false)
db.AssertExists(t, "task_comments", map[string]interface{}{
"comment": "This is a comment",
"author_id": u.ID,
}, false)
db.AssertExists(t, "files", map[string]interface{}{
"name": "cristiano-mozzillo-v3d5uBB26yA-unsplash.jpg",
"created_by_id": u.ID,
}, false)
db.AssertExists(t, "labels", map[string]interface{}{
"title": "test",
"created_by_id": u.ID,
}, false)
db.AssertExists(t, "buckets", map[string]interface{}{
"title": "Test Bucket",
"created_by_id": u.ID,
}, false)
err = m.Migrate(u, f, s.Size())
assert.NoError(t, err)
db.AssertExists(t, "projects", map[string]interface{}{
"title": "test project",
"owner_id": u.ID,
}, false)
db.AssertExists(t, "projects", map[string]interface{}{
"title": "Inbox",
"owner_id": u.ID,
}, false)
db.AssertExists(t, "tasks", map[string]interface{}{
"title": "some other task",
"created_by_id": u.ID,
}, false)
db.AssertExists(t, "task_comments", map[string]interface{}{
"comment": "This is a comment",
"author_id": u.ID,
}, false)
db.AssertExists(t, "files", map[string]interface{}{
"name": "grant-whitty-546453-unsplash.jpg",
"created_by_id": u.ID,
}, false)
db.AssertExists(t, "labels", map[string]interface{}{
"title": "test",
"created_by_id": u.ID,
}, false)
db.AssertExists(t, "buckets", map[string]interface{}{
"title": "Test Bucket",
"created_by_id": u.ID,
}, false)
})
t.Run("should not accept an old import", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
m := &FileMigrator{}
u := &user.User{ID: 1}
f, err := os.Open(config.ServiceRootpath.GetString() + "/pkg/modules/migration/vikunja-file/export_pre_0.21.0.zip")
if err != nil {
t.Fatalf("Could not open file: %s", err)
}
defer f.Close()
s, err := f.Stat()
if err != nil {
t.Fatalf("Could not stat file: %s", err)
}
err = m.Migrate(u, f, s.Size())
assert.Error(t, err)
assert.ErrorContainsf(t, err, "export was created with an older version", "Invalid error message")
})
}

View File

@ -1,97 +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 v1
import (
"net/http"
"strconv"
"code.vikunja.io/api/pkg/db"
"xorm.io/xorm"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web/handler"
"github.com/labstack/echo/v4"
)
// GetProjectsByNamespaceID is the web handler to get all projects belonging to a namespace
// TODO: deprecate this in favour of namespace.ReadOne() <-- should also return the projects
// @Summary Get all projects in a namespace
// @Description Returns all projects inside of a namespace.
// @tags namespace
// @Accept json
// @Produce json
// @Param id path int true "Namespace ID"
// @Security JWTKeyAuth
// @Success 200 {array} models.Project "The projects."
// @Failure 403 {object} models.Message "No access to that namespace."
// @Failure 404 {object} models.Message "The namespace does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{id}/projects [get]
func GetProjectsByNamespaceID(c echo.Context) error {
s := db.NewSession()
defer s.Close()
// Get our namespace
namespace, err := getNamespace(s, c)
if err != nil {
return handler.HandleHTTPError(err, c)
}
// Get the projects
doer, err := user.GetCurrentUser(c)
if err != nil {
return handler.HandleHTTPError(err, c)
}
projects, err := models.GetProjectsByNamespaceID(s, namespace.ID, doer)
if err != nil {
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusOK, projects)
}
func getNamespace(s *xorm.Session, c echo.Context) (namespace *models.Namespace, err error) {
// Check if we have our ID
id := c.Param("namespace")
// Make int
namespaceID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return
}
if namespaceID == -1 {
namespace = &models.SharedProjectsPseudoNamespace
return
}
// Check if the user has acces to that namespace
u, err := user.GetCurrentUser(c)
if err != nil {
return
}
namespace = &models.Namespace{ID: namespaceID}
canRead, _, err := namespace.CanRead(s, u)
if err != nil {
return namespace, err
}
if !canRead {
return nil, echo.ErrForbidden
}
return
}

View File

@ -62,8 +62,8 @@ func RegisterUser(c echo.Context) error {
return handler.HandleHTTPError(err, c)
}
// Add its namespace
err = models.CreateNewNamespaceForUser(s, newUser)
// Create their initial project
err = models.CreateNewProjectForUser(s, newUser)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)

View File

@ -51,10 +51,6 @@ func setupMetrics(a *echo.Group) {
metrics.UserCountKey,
user.User{},
},
{
metrics.NamespaceCountKey,
models.Namespace{},
},
{
metrics.TaskCountKey,
models.Task{},

View File

@ -22,7 +22,7 @@
// @description * `x-pagination-total-pages`: The total number of available pages for this request
// @description * `x-pagination-result-count`: The number of items returned for this request.
// @description # Rights
// @description All endpoints which return a single item (project, task, namespace, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.
// @description All endpoints which return a single item (project, task, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.
// @description This can be used to show or hide ui elements based on the rights the user has.
// @description # Authorization
// @description **JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer <jwt-token>`-header to authenticate successfully.
@ -325,7 +325,7 @@ func registerAPIRoutes(a *echo.Group) {
a.GET("/projects/:project", projectHandler.ReadOneWeb)
a.POST("/projects/:project", projectHandler.UpdateWeb)
a.DELETE("/projects/:project", projectHandler.DeleteWeb)
a.PUT("/namespaces/:namespace/projects", projectHandler.CreateWeb)
a.PUT("/projects", projectHandler.CreateWeb)
a.GET("/projects/:project/projectusers", apiv1.ListUsersForProject)
if config.ServiceEnableLinkSharing.GetBool() {
@ -488,38 +488,6 @@ func registerAPIRoutes(a *echo.Group) {
a.DELETE("/filters/:filter", savedFiltersHandler.DeleteWeb)
a.POST("/filters/:filter", savedFiltersHandler.UpdateWeb)
namespaceHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.Namespace{}
},
}
a.GET("/namespaces", namespaceHandler.ReadAllWeb)
a.PUT("/namespaces", namespaceHandler.CreateWeb)
a.GET("/namespaces/:namespace", namespaceHandler.ReadOneWeb)
a.POST("/namespaces/:namespace", namespaceHandler.UpdateWeb)
a.DELETE("/namespaces/:namespace", namespaceHandler.DeleteWeb)
a.GET("/namespaces/:namespace/projects", apiv1.GetProjectsByNamespaceID)
namespaceTeamHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.TeamNamespace{}
},
}
a.GET("/namespaces/:namespace/teams", namespaceTeamHandler.ReadAllWeb)
a.PUT("/namespaces/:namespace/teams", namespaceTeamHandler.CreateWeb)
a.DELETE("/namespaces/:namespace/teams/:team", namespaceTeamHandler.DeleteWeb)
a.POST("/namespaces/:namespace/teams/:team", namespaceTeamHandler.UpdateWeb)
namespaceUserHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.NamespaceUser{}
},
}
a.GET("/namespaces/:namespace/users", namespaceUserHandler.ReadAllWeb)
a.PUT("/namespaces/:namespace/users", namespaceUserHandler.CreateWeb)
a.DELETE("/namespaces/:namespace/users/:user", namespaceUserHandler.DeleteWeb)
a.POST("/namespaces/:namespace/users/:user", namespaceUserHandler.UpdateWeb)
teamHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.Team{}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -358,112 +358,6 @@ definitions:
description: A standard message.
type: string
type: object
models.Namespace:
properties:
created:
description: A timestamp when this namespace was created. You cannot change
this value.
type: string
description:
description: The description of the namespace
type: string
hex_color:
description: The hex color of this namespace
maxLength: 6
type: string
id:
description: The unique, numeric id of this namespace.
type: integer
is_archived:
description: Whether or not a namespace is archived.
type: boolean
owner:
allOf:
- $ref: '#/definitions/user.User'
description: The user who owns this namespace
subscription:
allOf:
- $ref: '#/definitions/models.Subscription'
description: |-
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.
title:
description: The name of this namespace.
maxLength: 250
minLength: 1
type: string
updated:
description: A timestamp when this namespace was last updated. You cannot
change this value.
type: string
type: object
models.NamespaceUser:
properties:
created:
description: A timestamp when this relation was created. You cannot change
this value.
type: string
id:
description: The unique, numeric id of this namespace <-> user relation.
type: integer
right:
allOf:
- $ref: '#/definitions/models.Right'
default: 0
description: The right this user has. 0 = Read only, 1 = Read & Write, 2 =
Admin. See the docs for more details.
maximum: 2
updated:
description: A timestamp when this relation was last updated. You cannot change
this value.
type: string
user_id:
description: The username.
type: string
type: object
models.NamespaceWithProjects:
properties:
created:
description: A timestamp when this namespace was created. You cannot change
this value.
type: string
description:
description: The description of the namespace
type: string
hex_color:
description: The hex color of this namespace
maxLength: 6
type: string
id:
description: The unique, numeric id of this namespace.
type: integer
is_archived:
description: Whether or not a namespace is archived.
type: boolean
owner:
allOf:
- $ref: '#/definitions/user.User'
description: The user who owns this namespace
projects:
items:
$ref: '#/definitions/models.Project'
type: array
subscription:
allOf:
- $ref: '#/definitions/models.Subscription'
description: |-
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.
title:
description: The name of this namespace.
maxLength: 250
minLength: 1
type: string
updated:
description: A timestamp when this namespace was last updated. You cannot
change this value.
type: string
type: object
models.Project:
properties:
background_blur_hash:
@ -475,6 +369,10 @@ definitions:
description: 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
child_projects:
items:
$ref: '#/definitions/models.Project'
type: array
created:
description: A timestamp when this project was created. You cannot change
this value.
@ -495,19 +393,19 @@ definitions:
minLength: 0
type: string
is_archived:
description: Whether or not a project is archived.
description: Whether a project is archived.
type: boolean
is_favorite:
description: True if a project is a favorite. Favorite projects show up in
a separate namespace. This value depends on the user making the call to
the api.
a separate parent project. This value depends on the user making the call
to the api.
type: boolean
namespace_id:
type: integer
owner:
allOf:
- $ref: '#/definitions/user.User'
description: The user who created this project.
parent_project_id:
type: integer
position:
description: The position this project has when querying all projects. See
the tasks.position property on how to use this.
@ -519,7 +417,7 @@ definitions:
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.
title:
description: The title of the project. You'll see this in the namespace overview.
description: The title of the project. You'll see this in the overview.
maxLength: 250
minLength: 1
type: string
@ -530,8 +428,8 @@ definitions:
type: object
models.ProjectDuplicate:
properties:
namespace_id:
description: The target namespace ID
parent_project_id:
description: The target parent project
type: integer
project:
allOf:
@ -624,7 +522,7 @@ definitions:
type: integer
is_favorite:
description: True if the filter is a favorite. Favorite filters show up in
a separate namespace together with favorite projects.
a separate parent project together with favorite projects.
type: boolean
owner:
allOf:
@ -948,30 +846,6 @@ definitions:
user id entering.
type: string
type: object
models.TeamNamespace:
properties:
created:
description: A timestamp when this relation was created. You cannot change
this value.
type: string
id:
description: The unique, numeric id of this namespace <-> team relation.
type: integer
right:
allOf:
- $ref: '#/definitions/models.Right'
default: 0
description: The right this team has. 0 = Read only, 1 = Read & Write, 2 =
Admin. See the docs for more details.
maximum: 2
team_id:
description: The team id.
type: integer
updated:
description: A timestamp when this relation was last updated. You cannot change
this value.
type: string
type: object
models.TeamProject:
properties:
created:
@ -1413,7 +1287,7 @@ info:
* `x-pagination-total-pages`: The total number of available pages for this request
* `x-pagination-result-count`: The number of items returned for this request.
# Rights
All endpoints which return a single item (project, task, namespace, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.
All endpoints which return a single item (project, task, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.
This can be used to show or hide ui elements based on the rights the user has.
# Authorization
**JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer <jwt-token>`-header to authenticate successfully.
@ -2250,624 +2124,6 @@ paths:
summary: Get migration status
tags:
- migration
/namespace/{id}:
post:
consumes:
- application/json
description: Updates a namespace.
parameters:
- description: Namespace ID
in: path
name: id
required: true
type: integer
- description: The namespace with updated values you want to update.
in: body
name: namespace
required: true
schema:
$ref: '#/definitions/models.Namespace'
produces:
- application/json
responses:
"200":
description: The updated namespace.
schema:
$ref: '#/definitions/models.Namespace'
"400":
description: Invalid namespace object provided.
schema:
$ref: '#/definitions/web.HTTPError'
"403":
description: The user does not have access to the namespace
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Updates a namespace
tags:
- namespace
/namespaces:
get:
consumes:
- application/json
description: Returns all namespaces a user has access to.
parameters:
- description: The page number. Used for pagination. If not provided, the first
page of results is returned.
in: query
name: page
type: integer
- description: The maximum number of items per page. Note this parameter is
limited by the configured maximum of items per page.
in: query
name: per_page
type: integer
- description: Search namespaces by name.
in: query
name: s
type: string
- description: If true, also returns all archived namespaces.
in: query
name: is_archived
type: boolean
- description: If true, also returns only namespaces without their projects.
in: query
name: namespaces_only
type: boolean
produces:
- application/json
responses:
"200":
description: The Namespaces.
schema:
items:
$ref: '#/definitions/models.NamespaceWithProjects'
type: array
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get all namespaces a user has access to
tags:
- namespace
put:
consumes:
- application/json
description: Creates a new namespace.
parameters:
- description: The namespace you want to create.
in: body
name: namespace
required: true
schema:
$ref: '#/definitions/models.Namespace'
produces:
- application/json
responses:
"201":
description: The created namespace.
schema:
$ref: '#/definitions/models.Namespace'
"400":
description: Invalid namespace object provided.
schema:
$ref: '#/definitions/web.HTTPError'
"403":
description: The user does not have access to the namespace
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Creates a new namespace
tags:
- namespace
/namespaces/{id}:
delete:
description: Delets a namespace
parameters:
- description: Namespace ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: The namespace was successfully deleted.
schema:
$ref: '#/definitions/models.Message'
"400":
description: Invalid namespace object provided.
schema:
$ref: '#/definitions/web.HTTPError'
"403":
description: The user does not have access to the namespace
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Deletes a namespace
tags:
- namespace
get:
consumes:
- application/json
description: Returns a namespace by its ID.
parameters:
- description: Namespace ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: The Namespace
schema:
$ref: '#/definitions/models.Namespace'
"403":
description: The user does not have access to that namespace.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Gets one namespace
tags:
- namespace
/namespaces/{id}/projects:
get:
consumes:
- application/json
description: Returns all projects inside of a namespace.
parameters:
- description: Namespace ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: The projects.
schema:
items:
$ref: '#/definitions/models.Project'
type: array
"403":
description: No access to that namespace.
schema:
$ref: '#/definitions/models.Message'
"404":
description: The namespace does not exist.
schema:
$ref: '#/definitions/models.Message'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get all projects in a namespace
tags:
- namespace
/namespaces/{id}/teams:
get:
consumes:
- application/json
description: Returns a namespace with all teams which have access on a given
namespace.
parameters:
- description: Namespace ID
in: path
name: id
required: true
type: integer
- description: The page number. Used for pagination. If not provided, the first
page of results is returned.
in: query
name: page
type: integer
- description: The maximum number of items per page. Note this parameter is
limited by the configured maximum of items per page.
in: query
name: per_page
type: integer
- description: Search teams by its name.
in: query
name: s
type: string
produces:
- application/json
responses:
"200":
description: The teams with the right they have.
schema:
items:
$ref: '#/definitions/models.TeamWithRight'
type: array
"403":
description: No right to see the namespace.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get teams on a namespace
tags:
- sharing
put:
consumes:
- application/json
description: Gives a team access to a namespace.
parameters:
- description: Namespace ID
in: path
name: id
required: true
type: integer
- description: The team you want to add to the namespace.
in: body
name: namespace
required: true
schema:
$ref: '#/definitions/models.TeamNamespace'
produces:
- application/json
responses:
"201":
description: The created team<->namespace relation.
schema:
$ref: '#/definitions/models.TeamNamespace'
"400":
description: Invalid team namespace object provided.
schema:
$ref: '#/definitions/web.HTTPError'
"403":
description: The team does not have access to the namespace
schema:
$ref: '#/definitions/web.HTTPError'
"404":
description: The team does not exist.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Add a team to a namespace
tags:
- sharing
/namespaces/{id}/users:
get:
consumes:
- application/json
description: Returns a namespace with all users which have access on a given
namespace.
parameters:
- description: Namespace ID
in: path
name: id
required: true
type: integer
- description: The page number. Used for pagination. If not provided, the first
page of results is returned.
in: query
name: page
type: integer
- description: The maximum number of items per page. Note this parameter is
limited by the configured maximum of items per page.
in: query
name: per_page
type: integer
- description: Search users by its name.
in: query
name: s
type: string
produces:
- application/json
responses:
"200":
description: The users with the right they have.
schema:
items:
$ref: '#/definitions/models.UserWithRight'
type: array
"403":
description: No right to see the namespace.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get users on a namespace
tags:
- sharing
put:
consumes:
- application/json
description: Gives a user access to a namespace.
parameters:
- description: Namespace ID
in: path
name: id
required: true
type: integer
- description: The user you want to add to the namespace.
in: body
name: namespace
required: true
schema:
$ref: '#/definitions/models.NamespaceUser'
produces:
- application/json
responses:
"201":
description: The created user<->namespace relation.
schema:
$ref: '#/definitions/models.NamespaceUser'
"400":
description: Invalid user namespace object provided.
schema:
$ref: '#/definitions/web.HTTPError'
"403":
description: The user does not have access to the namespace
schema:
$ref: '#/definitions/web.HTTPError'
"404":
description: The user does not exist.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Add a user to a namespace
tags:
- sharing
/namespaces/{namespaceID}/projects:
put:
consumes:
- application/json
description: Creates a new project in a given namespace. The user needs write-access
to the namespace.
parameters:
- description: Namespace ID
in: path
name: namespaceID
required: true
type: integer
- description: The project you want to create.
in: body
name: project
required: true
schema:
$ref: '#/definitions/models.Project'
produces:
- application/json
responses:
"201":
description: The created project.
schema:
$ref: '#/definitions/models.Project'
"400":
description: Invalid project object provided.
schema:
$ref: '#/definitions/web.HTTPError'
"403":
description: The user does not have access to the project
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Creates a new project
tags:
- project
/namespaces/{namespaceID}/teams/{teamID}:
delete:
description: Delets a team from a namespace. The team won't have access to the
namespace anymore.
parameters:
- description: Namespace ID
in: path
name: namespaceID
required: true
type: integer
- description: team ID
in: path
name: teamID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: The team was successfully deleted.
schema:
$ref: '#/definitions/models.Message'
"403":
description: The team does not have access to the namespace
schema:
$ref: '#/definitions/web.HTTPError'
"404":
description: team or namespace does not exist.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Delete a team from a namespace
tags:
- sharing
post:
consumes:
- application/json
description: Update a team <-> namespace relation. Mostly used to update the
right that team has.
parameters:
- description: Namespace ID
in: path
name: namespaceID
required: true
type: integer
- description: Team ID
in: path
name: teamID
required: true
type: integer
- description: The team you want to update.
in: body
name: namespace
required: true
schema:
$ref: '#/definitions/models.TeamNamespace'
produces:
- application/json
responses:
"200":
description: The updated team <-> namespace relation.
schema:
$ref: '#/definitions/models.TeamNamespace'
"403":
description: The team does not have admin-access to the namespace
schema:
$ref: '#/definitions/web.HTTPError'
"404":
description: Team or namespace does not exist.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Update a team <-> namespace relation
tags:
- sharing
/namespaces/{namespaceID}/users/{userID}:
delete:
description: Delets a user from a namespace. The user won't have access to the
namespace anymore.
parameters:
- description: Namespace ID
in: path
name: namespaceID
required: true
type: integer
- description: user ID
in: path
name: userID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: The user was successfully deleted.
schema:
$ref: '#/definitions/models.Message'
"403":
description: The user does not have access to the namespace
schema:
$ref: '#/definitions/web.HTTPError'
"404":
description: user or namespace does not exist.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Delete a user from a namespace
tags:
- sharing
post:
consumes:
- application/json
description: Update a user <-> namespace relation. Mostly used to update the
right that user has.
parameters:
- description: Namespace ID
in: path
name: namespaceID
required: true
type: integer
- description: User ID
in: path
name: userID
required: true
type: integer
- description: The user you want to update.
in: body
name: namespace
required: true
schema:
$ref: '#/definitions/models.NamespaceUser'
produces:
- application/json
responses:
"200":
description: The updated user <-> namespace relation.
schema:
$ref: '#/definitions/models.NamespaceUser'
"403":
description: The user does not have admin-access to the namespace
schema:
$ref: '#/definitions/web.HTTPError'
"404":
description: User or namespace does not exist.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Update a user <-> namespace relation
tags:
- sharing
/notifications:
get:
consumes:
@ -2988,6 +2244,42 @@ paths:
summary: Get all projects a user has access to
tags:
- project
put:
consumes:
- application/json
description: Creates a new project. If a parent project is provided the user
needs to have write access to that project.
parameters:
- description: The project you want to create.
in: body
name: project
required: true
schema:
$ref: '#/definitions/models.Project'
produces:
- application/json
responses:
"201":
description: The created project.
schema:
$ref: '#/definitions/models.Project'
"400":
description: Invalid project object provided.
schema:
$ref: '#/definitions/web.HTTPError'
"403":
description: The user does not have access to the project
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Creates a new project
tags:
- project
/projects/{id}:
delete:
description: Delets a project
@ -3876,15 +3168,15 @@ paths:
- application/json
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 namespace. The user needs read access in the project
and write access in the namespace of the new project.
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.
parameters:
- description: The project ID to duplicate
in: path
name: projectID
required: true
type: integer
- description: The target namespace which should hold the copied project.
- description: The target parent project which should hold the copied project.
in: body
name: project
required: true
@ -3902,7 +3194,7 @@ paths:
schema:
$ref: '#/definitions/web.HTTPError'
"403":
description: The user does not have access to the project or namespace
description: The user does not have access to the project or its parent.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
@ -4244,8 +3536,8 @@ paths:
- application/json
description: Unsubscribes the current user to an entity.
parameters:
- description: The entity the user subscribed to. Can be either `namespace`,
`project` or `task`.
- description: The entity the user subscribed to. Can be either `project` or
`task`.
in: path
name: entity
required: true
@ -4284,8 +3576,8 @@ paths:
- application/json
description: Subscribes the current user to an entity.
parameters:
- description: The entity the user subscribes to. Can be either `namespace`,
`project` or `task`.
- description: The entity the user subscribes to. Can be either `project` or
`task`.
in: path
name: entity
required: true
@ -5353,8 +4645,7 @@ paths:
put:
consumes:
- application/json
description: Creates a new team in a given namespace. The user needs write-access
to the namespace.
description: Creates a new team.
parameters:
- description: The team you want to create.
in: body

View File

@ -204,7 +204,7 @@ func (n *AccountDeletionConfirmNotification) ToMail() *notifications.Mail {
Action("Confirm the deletion of my account", config.ServiceFrontendurl.GetString()+"?accountDeletionConfirm="+n.ConfirmToken).
Line("This link will be valid for 24 hours.").
Line("Once you confirm the deletion we will schedule the deletion of your account in three days and send you another email until then.").
Line("If you proceed with the deletion of your account, we will remove all of your namespaces, projects and tasks you created. Everything you shared with another user or team will transfer ownership to them.").
Line("If you proceed with the deletion of your account, we will remove all of your projects and tasks you created. Everything you shared with another user or team will transfer ownership to them.").
Line("If you did not requested the deletion or changed your mind, you can simply ignore this email.").
Line("Have a nice day!")
}

View File

@ -371,7 +371,7 @@ func TestUpdateUserPassword(t *testing.T) {
})
}
func TestProjectUsers(t *testing.T) {
func TestListUsers(t *testing.T) {
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -397,7 +397,7 @@ func TestProjectUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ProjectAllUsers(s)
all, err := ListAllUsers(s)
assert.NoError(t, err)
assert.Len(t, all, 15)
})

View File

@ -31,7 +31,7 @@ type ProjectUserOpts struct {
ReturnAllIfNoSearchProvided bool
}
// ListUsers returns a project with all users, filtered by an optional search string
// ListUsers returns a list with all users, filtered by an optional search string
func ListUsers(s *xorm.Session, search string, opts *ProjectUserOpts) (users []*User, err error) {
if opts == nil {
opts = &ProjectUserOpts{}
@ -89,8 +89,8 @@ func ListUsers(s *xorm.Session, search string, opts *ProjectUserOpts) (users []*
return
}
// ProjectAllUsers returns all users
func ProjectAllUsers(s *xorm.Session) (users []*User, err error) {
// ListAllUsers returns all users
func ListAllUsers(s *xorm.Session) (users []*User, err error) {
err = s.Find(&users)
return
}