diff --git a/.golangci.yml b/.golangci.yml index 36ce6ef4b..1f3586d04 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -89,6 +89,8 @@ issues: - path: pkg/models/favorites\.go linters: - nilerr + - path: pkg/models/project\.go + text: "string `parent_project_id` has 3 occurrences, make it a constant" - path: pkg/models/events\.go linters: - musttag diff --git a/README.md b/README.md index c6ed8aaa8..5a5236616 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,7 @@ If you find any security-related issues you don't want to disclose publicly, ple ## Features -* Create TODO lists with tasks - * Reminder for tasks -* Namespaces: A "group" which bundles multiple lists -* Share lists and namespaces with teams and users with granular permissions -* Plenty of details for tasks - -See [the features page](https://vikunja.io/en/features/) on our website for a more exaustive list or +See [the features page](https://vikunja.io/features/) on our website for a more exaustive list or try it on [try.vikunja.io](https://try.vikunja.io)! ## Docs diff --git a/docs/content/doc/development/migration.md b/docs/content/doc/development/migration.md index 7bcb2f281..4da0412b0 100644 --- a/docs/content/doc/development/migration.md +++ b/docs/content/doc/development/migration.md @@ -100,9 +100,10 @@ You should also document the routes with [swagger annotations]({{< ref "swagger- ## Insertion helper method There is a method available in the `migration` package which takes a fully nested Vikunja structure and creates it with all relations. -This means you start by adding a namespace, then add projects inside that namespace, then tasks in the lists and so on. +This means you start by adding a project, then add projects inside that project, then tasks in the lists and so on. +In general, it is reccommended to have one root project with all projects of the other service as child projects. -The root structure must be present as `[]*models.NamespaceWithProjectsAndTasks`. It allows to represent all of Vikunja's hierarchy as a single data structure. +The root structure must be present as `[]*models.ProjectWithTasksAndBuckets`. It allows to represent all of Vikunja's hierarchy as a single data structure. Then call the method like so: diff --git a/docs/content/doc/development/swagger-docs.md b/docs/content/doc/development/swagger-docs.md index fa1ae43a8..e52577cf7 100644 --- a/docs/content/doc/development/swagger-docs.md +++ b/docs/content/doc/development/swagger-docs.md @@ -25,7 +25,7 @@ As an example, this is the definition of a project with all comments: 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"` @@ -34,13 +34,14 @@ 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:"-"` // 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 @@ -50,7 +51,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. diff --git a/docs/content/doc/development/translation-instructions-german.md b/docs/content/doc/development/translation-instructions-german.md index 09396fd3a..739474436 100644 --- a/docs/content/doc/development/translation-instructions-german.md +++ b/docs/content/doc/development/translation-instructions-german.md @@ -67,7 +67,6 @@ Beispiel: „Benutzer:in“ | Englisches Original | Verwendung in deutscher Übersetzung | | ------------------- | -------------------- | | Bucket | Spalte | -| Namespace | Namespace | | Link Share | Linkfreigabe | | Username | Anmeldename | diff --git a/docs/content/doc/usage/errors.md b/docs/content/doc/usage/errors.md index 5c03255ea..f6670737b 100644 --- a/docs/content/doc/usage/errors.md +++ b/docs/content/doc/usage/errors.md @@ -54,16 +54,17 @@ This document describes the different errors Vikunja can return. ## Project -| ErrorCode | HTTP Status Code | Description | -|-----------|------------------|-------------------------------------------------------------------------------------------------------------------------------| -| 3001 | 404 | The project does not exist. | -| 3004 | 403 | The user needs to have read permissions on that project to perform that action. | -| 3005 | 400 | The project title cannot be empty. | -| 3006 | 404 | The project share does not exist. | -| 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. | +| ErrorCode | HTTP Status Code | Description | +|-----------|------------------|-----------------------------------------------------------------------------------------------------------------------------------------| +| 3001 | 404 | The project does not exist. | +| 3004 | 403 | The user needs to have read permissions on that project to perform that action. | +| 3005 | 400 | The project title cannot be empty. | +| 3006 | 404 | The project share does not exist. | +| 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 parent project like "Favorites". | +| 3010 | 412 | This project cannot be a child of itself. | +| 3011 | 412 | This project cannot have a cyclic relationship to a parent project. | ## Task @@ -92,27 +93,15 @@ This document describes the different errors Vikunja can return. | 4021 | 400 | This user is already assigned to that task. | | 4022 | 400 | The task has a relative reminder which does not specify relative to what. | -## Namespace - -| ErrorCode | HTTP Status Code | Description | -|-----------|------------------|-------------| -| 5001 | 404 | The namespace 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 empty. | -| 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 empty. | +| 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 diff --git a/docs/content/doc/usage/rights.md b/docs/content/doc/usage/rights.md index 1f7864056..18433c11f 100644 --- a/docs/content/doc/usage/rights.md +++ b/docs/content/doc/usage/rights.md @@ -8,20 +8,20 @@ menu: parent: "usage" --- -# Project and namespace rights for teams and users +# Project rights for teams and users -Whenever you share a project or namespace with a user or team, you can specify a `rights` parameter. +Whenever you share a project with a user or team, you can specify a `rights` parameter. This parameter controls the rights that team or user is going to have (or has, if you request the current sharing status). Rights are being specified using integers. The following values are possible: -| Right (int) | Meaning | -|-------------|---------------------------------------------------------------------------------------------------------------| -| 0 (Default) | Read only. Anything which is shared with this right cannot be edited. | -| 1 | Read and write. Namespaces or projects shared with this right can be read and written to by the team or user. | -| 2 | Admin. Can do anything like read and write, but can additionally manage sharing options. | +| Right (int) | Meaning | +|-------------|-------------------------------------------------------------------------------------------------| +| 0 (Default) | Read only. Anything which is shared with this right cannot be edited. | +| 1 | Read and write. Projects shared with this right can be read and written to by the team or user. | +| 2 | Admin. Can do anything like read and write, but can additionally manage sharing options. | ## Team admins diff --git a/go.mod b/go.mod index ccbdf9113..d52bd96b1 100644 --- a/go.mod +++ b/go.mod @@ -35,10 +35,10 @@ require ( github.com/go-sql-driver/mysql v1.7.1 github.com/go-testfixtures/testfixtures/v3 v3.9.0 github.com/gocarina/gocsv v0.0.0-20230513223533-9ddd7fd60602 - github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-jwt/jwt/v5 v5.0.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.15 github.com/jinzhu/copier v0.3.5 @@ -149,6 +149,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 go.opentelemetry.io/otel v1.15.0 // indirect go.opentelemetry.io/otel/trace v1.15.0 // indirect golang.org/x/mod v0.9.0 // indirect diff --git a/go.sum b/go.sum index be92024bf..70ee2a41e 100644 --- a/go.sum +++ b/go.sum @@ -24,7 +24,6 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -82,10 +81,6 @@ github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0 h1:VVPogIxPiZ6WK5G4Pve5VSQ4HEFiJ8GChpqRjo1gN2c= -github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0/go.mod h1:BSTTrYHuM12oAL8jDdcmPdw02SBThKYWNFHQlvEG6b0= -github.com/arran4/golang-ical v0.0.0-20230318005454-19abf92700cc h1:up1aDcTCZ3KrL2ukKxNqjMRx/CCaXyn9Wl6N7ea3EWc= -github.com/arran4/golang-ical v0.0.0-20230318005454-19abf92700cc/go.mod h1:BSTTrYHuM12oAL8jDdcmPdw02SBThKYWNFHQlvEG6b0= github.com/arran4/golang-ical v0.0.0-20230425234049-f69e132f2b0c h1:bmHPCBB1T8YZpQI+Ch0RuICrozVFmPAjiBQZvAjtpRI= github.com/arran4/golang-ical v0.0.0-20230425234049-f69e132f2b0c/go.mod h1:BSTTrYHuM12oAL8jDdcmPdw02SBThKYWNFHQlvEG6b0= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= @@ -105,8 +100,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ= -github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8= +github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= +github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b h1:6+ZFm0flnudZzdSE0JxlhR2hKnGPcNB35BjQf4RYQDY= github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= @@ -128,8 +123,6 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw= -github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM= github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o= github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -152,7 +145,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/denisenkom/go-mssqldb v0.12.2 h1:1OcPn5GBIobjWNd+8yjfHNIaFX14B1pWI3F9HZy5KXw= +github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= @@ -181,18 +174,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/getsentry/sentry-go v0.20.0 h1:bwXW98iMRIWxn+4FgPW7vMrjmbym6HblXALmhjHmQaQ= -github.com/getsentry/sentry-go v0.20.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/getsentry/sentry-go v0.21.0 h1:c9l5F1nPF30JIppulk4veau90PK6Smu3abgVtVQWon4= github.com/getsentry/sentry-go v0.21.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= @@ -229,28 +214,15 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= -github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-testfixtures/testfixtures/v3 v3.8.1 h1:uonwvepqRvSgddcrReZQhojTlWlmOlHkYAb9ZaOMWgU= -github.com/go-testfixtures/testfixtures/v3 v3.8.1/go.mod h1:Kdu7YeMC0KRXVHdaQ91Vmx3pcjoTF63h4f1qTJDdXLA= github.com/go-testfixtures/testfixtures/v3 v3.9.0 h1:938g5V+GWLVejm3Hc+nWCuEXRlcglZDDlN/t1gWzcSY= github.com/go-testfixtures/testfixtures/v3 v3.9.0/go.mod h1:cdsKD2ApFBjdog9jRsz6EJqF+LClq/hrwE9K/1Dzo4s= -github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a h1:/5o1ejt5M0fNAN2lU1NBLtPzUSZru689EWJq01ptr+E= -github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= -github.com/gocarina/gocsv v0.0.0-20230325173030-9a18a846a479 h1:KaCpc4e48emF9hYmMB9INyfpGJHAZxEAS9EqWFkpTig= -github.com/gocarina/gocsv v0.0.0-20230325173030-9a18a846a479/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= -github.com/gocarina/gocsv v0.0.0-20230406101422-6445c2b15027 h1:LCGzZb4kMUUjMUzLxxqSJBwo9szUO0tK8cOxnEOT4Jc= -github.com/gocarina/gocsv v0.0.0-20230406101422-6445c2b15027/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= -github.com/gocarina/gocsv v0.0.0-20230510095315-7f30c79fd20c h1:ZaB8yqPWgWQ3HelTDCiJREs8yh1LutQaAhE/e1PqDLc= -github.com/gocarina/gocsv v0.0.0-20230510095315-7f30c79fd20c/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/gocarina/gocsv v0.0.0-20230513223533-9ddd7fd60602 h1:HSpPf+lPYwzoJNup34uegmOQk5Qm83S+wpu8anTDJkg= github.com/gocarina/gocsv v0.0.0-20230513223533-9ddd7fd60602/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= @@ -263,8 +235,6 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= @@ -299,7 +269,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -320,7 +289,6 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -370,6 +338,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= @@ -385,15 +355,9 @@ github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHL github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/imdario/mergo v0.3.14 h1:fOqeC1+nCuuk6PKQdg9YmosXX7Y7mHX6R/0ZldI9iHo= -github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= @@ -412,7 +376,7 @@ github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpT github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= +github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= @@ -428,10 +392,10 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1: github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= +github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0= github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= @@ -440,7 +404,7 @@ github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkAL github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE= github.com/jackc/pgtype v1.8.0/go.mod h1:PqDKcEBtllAtk/2p6z6SHdXW5UB+MhE75tUol2OKexE= -github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= +github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= github.com/jackc/pgx v3.6.0+incompatible h1:bJeo4JdVbDAW8KB2m8XkFeo8CPipREoG37BwEoKGz+Q= github.com/jackc/pgx v3.6.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= @@ -451,7 +415,7 @@ github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6 github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc= github.com/jackc/pgx/v4 v4.12.0/go.mod h1:fE547h6VulLPA3kySjfnSG/e2D861g/50JlVUa/ub60= -github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= +github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= @@ -461,7 +425,7 @@ github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= @@ -482,8 +446,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -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= @@ -497,8 +459,6 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo-jwt/v4 v4.1.0 h1:eYGBxauPkyzBM78KJbR5OSz5uhKMDkhJZhTTIuoH6Pg= -github.com/labstack/echo-jwt/v4 v4.1.0/go.mod h1:DHSSaL6cTgczdPXjf8qrTHRbrau2flcddV7CPMs2U/Y= github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c= github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU= github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI= @@ -515,10 +475,6 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.8 h1:3fdt97i/cwSU83+E0hZTC/Xpc9mTZxc6UWSCRcSbxiE= -github.com/lib/pq v1.10.8/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= @@ -526,8 +482,6 @@ github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0U github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8= github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= -github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -556,8 +510,6 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-oci8 v0.0.0-20191108001511-cbd8d5bc1da0/go.mod h1:/M9VLO+lUPmxvoOK2PfWRZ8mTtB4q1Hy9lEGijv9Nr8= @@ -639,7 +591,6 @@ github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvI github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -659,10 +610,6 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= -github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= -github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -675,8 +622,6 @@ github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3d github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= -github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= -github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -686,10 +631,6 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSxfE= -github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= -github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k= -github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/redis/go-redis/v9 v9.0.4 h1:FC82T+CHJ/Q/PdyLW++GeCO+Ol59Y4T7R4jbgjvktgc= github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= @@ -698,7 +639,7 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -725,15 +666,11 @@ 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= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= @@ -760,16 +697,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo= -github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= -github.com/swaggo/swag v1.8.11 h1:Fp1dNNtDvbCf+8kvehZbHQnlF6AxHGjmw6H/xAMrZfY= -github.com/swaggo/swag v1.8.11/go.mod h1:2GXgpNI9iy5OdsYWu8zXfRAGnOAPxYxTWTyM0XOTYZQ= github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= @@ -778,10 +709,6 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV 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/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA= github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -796,18 +723,16 @@ 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/wneessen/go-mail v0.3.9 h1:Q4DbCk3htT5DtDWKeMgNXCiHc4bBY/vv/XQPT6XDXzc= github.com/wneessen/go-mail v0.3.9/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= 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/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -864,13 +789,8 @@ 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-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -886,8 +806,6 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= -golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw= golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -912,7 +830,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -960,15 +877,8 @@ 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= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -980,11 +890,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= -golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= -golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -999,7 +904,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1069,23 +973,14 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1096,10 +991,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1170,7 +1062,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= @@ -1282,11 +1173,6 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM= -google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -1318,7 +1204,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1443,16 +1328,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/xgo v1.7.1-0.20230404174715-bff48e481f81 h1:GNyJiosmWbazA1OYNZ1yF+GWOjIW0ZfJmkwEJDiU18g= -src.techknowlogick.com/xgo v1.7.1-0.20230404174715-bff48e481f81/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU= -src.techknowlogick.com/xgo v1.7.1-0.20230426011930-e65295a11a0f h1:4/OzEYNoSOP1s3v0cBolS7uSR/AceOtwXcZ1Iqusrbw= -src.techknowlogick.com/xgo v1.7.1-0.20230426011930-e65295a11a0f/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU= src.techknowlogick.com/xgo v1.7.1-0.20230502175921-52d704db7dce h1:gxVOs4LYBv+/gGImaYvs8p14kOTbVLl6835JYKAzUaw= src.techknowlogick.com/xgo v1.7.1-0.20230502175921-52d704db7dce/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU= src.techknowlogick.com/xormigrate v1.5.0 h1:6mWTh8d0sWjMTLUgJqiLe0e0Teu+1j+RgI7ErAeOEV0= diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index 7e9f09f83..1a6e3d152 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -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 { diff --git a/pkg/db/fixtures/buckets.yml b/pkg/db/fixtures/buckets.yml index e49816b2a..93dc7c364 100644 --- a/pkg/db/fixtures/buckets.yml +++ b/pkg/db/fixtures/buckets.yml @@ -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 diff --git a/pkg/db/fixtures/favorites.yml b/pkg/db/fixtures/favorites.yml index ec2dd6324..196489686 100644 --- a/pkg/db/fixtures/favorites.yml +++ b/pkg/db/fixtures/favorites.yml @@ -10,9 +10,6 @@ - entity_id: 34 user_id: 13 # owner kind: 1 -- entity_id: 34 - user_id: 1 - kind: 1 - entity_id: 23 user_id: 12 # owner kind: 2 diff --git a/pkg/db/fixtures/label_tasks.yml b/pkg/db/fixtures/label_tasks.yml index bdf836f72..3d6149c24 100644 --- a/pkg/db/fixtures/label_tasks.yml +++ b/pkg/db/fixtures/label_tasks.yml @@ -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 diff --git a/pkg/db/fixtures/namespaces.yml b/pkg/db/fixtures/namespaces.yml deleted file mode 100644 index 282409f9b..000000000 --- a/pkg/db/fixtures/namespaces.yml +++ /dev/null @@ -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 diff --git a/pkg/db/fixtures/projects.yml b/pkg/db/fixtures/projects.yml index f2df5cf12..d9cbdd175 100644 --- a/pkg/db/fixtures/projects.yml +++ b/pkg/db/fixtures/projects.yml @@ -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 diff --git a/pkg/db/fixtures/subscriptions.yml b/pkg/db/fixtures/subscriptions.yml index 301bfbf5b..7f97ca40b 100644 --- a/pkg/db/fixtures/subscriptions.yml +++ b/pkg/db/fixtures/subscriptions.yml @@ -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 diff --git a/pkg/db/fixtures/task_reminders.yml b/pkg/db/fixtures/task_reminders.yml index a5e5ec07a..75f31fd90 100644 --- a/pkg/db/fixtures/task_reminders.yml +++ b/pkg/db/fixtures/task_reminders.yml @@ -13,6 +13,6 @@ reminder: 2018-12-01 01:13:44 created: 2018-12-01 01:12:04 - id: 4 - task_id: 39 + task_id: 40 reminder: 2023-03-04 15:00:00 created: 2018-12-01 01:12:04 diff --git a/pkg/db/fixtures/tasks.yml b/pkg/db/fixtures/tasks.yml index 851daadce..77e11ae4d 100644 --- a/pkg/db/fixtures/tasks.yml +++ b/pkg/db/fixtures/tasks.yml @@ -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 @@ -357,13 +357,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 diff --git a/pkg/db/fixtures/team_namespaces.yml b/pkg/db/fixtures/team_namespaces.yml deleted file mode 100644 index 925447830..000000000 --- a/pkg/db/fixtures/team_namespaces.yml +++ /dev/null @@ -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 diff --git a/pkg/db/fixtures/team_projects.yml b/pkg/db/fixtures/team_projects.yml index 98a9ed13b..c1c6c7b48 100644 --- a/pkg/db/fixtures/team_projects.yml +++ b/pkg/db/fixtures/team_projects.yml @@ -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 diff --git a/pkg/db/fixtures/teams.yml b/pkg/db/fixtures/teams.yml index 64ba4aaa9..b7d347df4 100644 --- a/pkg/db/fixtures/teams.yml +++ b/pkg/db/fixtures/teams.yml @@ -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 diff --git a/pkg/db/fixtures/users_namespaces.yml b/pkg/db/fixtures/users_namespaces.yml deleted file mode 100644 index 4ed1be570..000000000 --- a/pkg/db/fixtures/users_namespaces.yml +++ /dev/null @@ -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 diff --git a/pkg/db/fixtures/users_projects.yml b/pkg/db/fixtures/users_projects.yml index 14b3d74ed..4fb583c46 100644 --- a/pkg/db/fixtures/users_projects.yml +++ b/pkg/db/fixtures/users_projects.yml @@ -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 diff --git a/pkg/db/test.go b/pkg/db/test.go index 146643ffb..eb35126ca 100644 --- a/pkg/db/test.go +++ b/pkg/db/test.go @@ -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 diff --git a/pkg/integrations/archived_test.go b/pkg/integrations/archived_test.go index 50ab31e5d..448713ce2 100644 --- a/pkg/integrations/archived_test.go +++ b/pkg/integrations/archived_test.go @@ -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) }) } diff --git a/pkg/integrations/caldav_test.go b/pkg/integrations/caldav_test.go index 7ab704446..92487f2e7 100644 --- a/pkg/integrations/caldav_test.go +++ b/pkg/integrations/caldav_test.go @@ -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 @@ -46,22 +46,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") diff --git a/pkg/integrations/kanban_test.go b/pkg/integrations/kanban_test.go index 13c0c1f9d..74747ff74 100644 --- a/pkg/integrations/kanban_test.go +++ b/pkg/integrations/kanban_test.go @@ -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"`) diff --git a/pkg/integrations/link_sharing_test.go b/pkg/integrations/link_sharing_test.go index 790fe85c0..4bb1e44df 100644 --- a/pkg/integrations/link_sharing_test.go +++ b/pkg/integrations/link_sharing_test.go @@ -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`) - }) - }) - }) - }) - }) } diff --git a/pkg/integrations/project_test.go b/pkg/integrations/project_test.go index 94165d3bd..e2bcfef9e 100644 --- a/pkg/integrations/project_test.go +++ b/pkg/integrations/project_test.go @@ -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":""`) diff --git a/pkg/integrations/task_comment_test.go b/pkg/integrations/task_comment_test.go index 6d1959e87..4e367faa8 100644 --- a/pkg/integrations/task_comment_test.go +++ b/pkg/integrations/task_comment_test.go @@ -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"`) diff --git a/pkg/integrations/task_test.go b/pkg/integrations/task_test.go index 0a337451e..bcae46cae 100644 --- a/pkg/integrations/task_test.go +++ b/pkg/integrations/task_test.go @@ -277,33 +277,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"`) @@ -395,33 +395,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.`) @@ -478,33 +478,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"`) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 76dfe3201..cfb363e35 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -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", diff --git a/pkg/migration/20221228112131.go b/pkg/migration/20221228112131.go new file mode 100644 index 000000000..9b9d59c08 --- /dev/null +++ b/pkg/migration/20221228112131.go @@ -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 . + +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") +} diff --git a/pkg/models/error.go b/pkg/models/error.go index 9e05abcb6..6f9283e6d 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -19,6 +19,7 @@ package models import ( "fmt" "net/http" + "strings" "code.vikunja.io/api/pkg/config" "code.vikunja.io/web" @@ -255,65 +256,100 @@ 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 +// ErrProjectCannotBeChildOfItself represents an error where a project cannot become a child of its own +type ErrProjectCannotBeChildOfItself struct { + ProjectID int64 } -// IsErrProjectMustBelongToANamespace checks if an error is a project must belong to a namespace error. -func IsErrProjectMustBelongToANamespace(err error) bool { - _, ok := err.(*ErrProjectMustBelongToANamespace) +// IsErrProjectCannotBeChildOfItsOwn checks if an error is a project is archived error. +func IsErrProjectCannotBeChildOfItsOwn(err error) bool { + _, ok := err.(*ErrProjectCannotBeChildOfItself) return ok } -func (err *ErrProjectMustBelongToANamespace) Error() string { - return fmt.Sprintf("Project must belong to a namespace [ProjectID: %d, NamespaceID: %d]", err.ProjectID, err.NamespaceID) +func (err *ErrProjectCannotBeChildOfItself) Error() string { + return fmt.Sprintf("Project cannot be made a child of itself [ProjectID: %d]", err.ProjectID) } -// ErrCodeProjectMustBelongToANamespace holds the unique world-error code of this error -const ErrCodeProjectMustBelongToANamespace = 3010 +// ErrCodeProjectCannotBeChildOfItself holds the unique world-error code of this error +const ErrCodeProjectCannotBeChildOfItself = 3010 // HTTPError holds the http error description -func (err *ErrProjectMustBelongToANamespace) HTTPError() web.HTTPError { +func (err *ErrProjectCannotBeChildOfItself) HTTPError() web.HTTPError { return web.HTTPError{ HTTPCode: http.StatusPreconditionFailed, - Code: ErrCodeProjectMustBelongToANamespace, - Message: "This project must belong to a namespace.", + Code: ErrCodeProjectCannotBeChildOfItself, + Message: "This project cannot be a child of itself.", } } -// ================ -// Project task errors -// ================ +// ErrProjectCannotHaveACyclicRelationship represents an error where a project cannot have a cyclic parent relationship +type ErrProjectCannotHaveACyclicRelationship struct { + ProjectID int64 + CycleIDs []int64 +} + +// IsErrProjectCannotHaveACyclicRelationship checks if an error is a project is archived error. +func IsErrProjectCannotHaveACyclicRelationship(err error) bool { + _, ok := err.(*ErrProjectCannotHaveACyclicRelationship) + return ok +} + +func (err *ErrProjectCannotHaveACyclicRelationship) CycleString() string { + var cycle string + for _, projectID := range err.CycleIDs { + cycle += fmt.Sprintf("%d -> ", projectID) + } + return strings.TrimSuffix(cycle, " -> ") +} + +func (err *ErrProjectCannotHaveACyclicRelationship) Error() string { + return fmt.Sprintf("Project cannot have a cyclic relationship [ProjectID: %d]", err.ProjectID) +} + +// ErrCodeProjectCannotHaveACyclicRelationship holds the unique world-error code of this error +const ErrCodeProjectCannotHaveACyclicRelationship = 3011 + +// HTTPError holds the http error description +func (err *ErrProjectCannotHaveACyclicRelationship) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusPreconditionFailed, + Code: ErrCodeProjectCannotHaveACyclicRelationship, + Message: "This project cannot have a cyclic relationship to a parent project.", + } +} + +// ============== +// Task errors +// ============== // ErrTaskCannotBeEmpty represents a "ErrProjectDoesNotExist" kind of error. Used if the project does not exist. type ErrTaskCannotBeEmpty struct{} @@ -902,176 +938,6 @@ func (err ErrReminderRelativeToMissing) 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 // ============ @@ -1081,7 +947,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 @@ -1122,7 +988,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 @@ -1222,7 +1088,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 diff --git a/pkg/models/events.go b/pkg/models/events.go index c1fcbcbe4..2953c566e 100644 --- a/pkg/models/events.go +++ b/pkg/models/events.go @@ -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 // ///////////////// diff --git a/pkg/models/export.go b/pkg/models/export.go index 1ae87881d..28f45b55a 100644 --- a/pkg/models/export.go +++ b/pkg/models/export.go @@ -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 diff --git a/pkg/models/label_rights.go b/pkg/models/label_rights.go index b05b4c704..9d81ce616 100644 --- a/pkg/models/label_rights.go +++ b/pkg/models/label_rights.go @@ -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{} diff --git a/pkg/models/label_task.go b/pkg/models/label_task.go index 190db7fe2..c5b4d383b 100644 --- a/pkg/models/label_task.go +++ b/pkg/models/label_task.go @@ -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 { diff --git a/pkg/models/label_task_test.go b/pkg/models/label_task_test.go index 7f86255b9..52d84d57e 100644 --- a/pkg/models/label_task_test.go +++ b/pkg/models/label_task_test.go @@ -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) diff --git a/pkg/models/listeners.go b/pkg/models/listeners.go index 183553993..6e88a3495 100644 --- a/pkg/models/listeners.go +++ b/pkg/models/listeners.go @@ -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(_ *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" -} - -// Handle is executed when the event DecreaseNamespaceCounter listens on is fired -func (s *DecreaseNamespaceCounter) Handle(_ *message.Message) (err error) { - return keyvalue.DecrBy(metrics.NamespaceCountKey, 1) -} - /////// // Team Events diff --git a/pkg/models/models.go b/pkg/models/models.go index 66191e1f0..56186f3d3 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -44,10 +44,7 @@ func GetTables() []interface{} { &Team{}, &TeamMember{}, &TeamProject{}, - &TeamNamespace{}, - &Namespace{}, &ProjectUser{}, - &NamespaceUser{}, &TaskAssginee{}, &Label{}, &LabelTask{}, diff --git a/pkg/models/namespace.go b/pkg/models/namespace.go deleted file mode 100644 index c78a23096..000000000 --- a/pkg/models/namespace.go +++ /dev/null @@ -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 . - -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, - }) -} diff --git a/pkg/models/namespace_rights.go b/pkg/models/namespace_rights.go deleted file mode 100644 index f03dbf698..000000000 --- a/pkg/models/namespace_rights.go +++ /dev/null @@ -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 . - -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(_ *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 -} diff --git a/pkg/models/namespace_team.go b/pkg/models/namespace_team.go deleted file mode 100644 index 883256182..000000000 --- a/pkg/models/namespace_team.go +++ /dev/null @@ -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 . - -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, _ 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, _ 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 -} diff --git a/pkg/models/namespace_team_rights.go b/pkg/models/namespace_team_rights.go deleted file mode 100644 index edde7125a..000000000 --- a/pkg/models/namespace_team_rights.go +++ /dev/null @@ -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 . - -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) -} diff --git a/pkg/models/namespace_team_rights_test.go b/pkg/models/namespace_team_rights_test.go deleted file mode 100644 index e9dc37c67..000000000 --- a/pkg/models/namespace_team_rights_test.go +++ /dev/null @@ -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 . - -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() - }) - } -} diff --git a/pkg/models/namespace_team_test.go b/pkg/models/namespace_team_test.go deleted file mode 100644 index 9417df459..000000000 --- a/pkg/models/namespace_team_test.go +++ /dev/null @@ -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 . - -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) - } - }) - } -} diff --git a/pkg/models/namespace_test.go b/pkg/models/namespace_test.go deleted file mode 100644 index 3b6e54f51..000000000 --- a/pkg/models/namespace_test.go +++ /dev/null @@ -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 . - -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) - }) -} diff --git a/pkg/models/namespace_users.go b/pkg/models/namespace_users.go deleted file mode 100644 index 716df0181..000000000 --- a/pkg/models/namespace_users.go +++ /dev/null @@ -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 . - -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, _ 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, _ 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 -} diff --git a/pkg/models/namespace_users_rights.go b/pkg/models/namespace_users_rights.go deleted file mode 100644 index 00f7e77ca..000000000 --- a/pkg/models/namespace_users_rights.go +++ /dev/null @@ -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 . - -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) -} diff --git a/pkg/models/namespace_users_rights_test.go b/pkg/models/namespace_users_rights_test.go deleted file mode 100644 index f8a8f7304..000000000 --- a/pkg/models/namespace_users_rights_test.go +++ /dev/null @@ -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 . - -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"]) - } - }) - } -} diff --git a/pkg/models/namespace_users_test.go b/pkg/models/namespace_users_test.go deleted file mode 100644 index 634e796b8..000000000 --- a/pkg/models/namespace_users_test.go +++ /dev/null @@ -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 . - -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, - }) - } - }) - } -} diff --git a/pkg/models/project.go b/pkg/models/project.go index 3439efc93..5cf9ee107 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -17,6 +17,7 @@ package models import ( + "math" "strconv" "strings" "time" @@ -37,7 +38,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 +47,14 @@ 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:"-"` // 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 +64,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 +85,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 +95,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,72 +107,35 @@ type ProjectBackgroundType struct { // ProjectBackgroundUpload represents the project upload background type const ProjectBackgroundUpload string = "upload" +// SharedProjectsPseudoProject is a pseudo project used to hold shared projects +var SharedProjectsPseudoProject = &Project{ + ID: -1, + Title: "Shared Projects", + Description: "Projects of other users shared with you via teams or directly.", + Created: time.Now(), + Updated: time.Now(), +} + +// 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.", - NamespaceID: FavoritesPseudoNamespace.ID, IsFavorite: true, + Position: -1, 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 - } - - // get more project details - err = addProjectDetails(s, projects, doer) - return projects, err -} - // ReadAll gets all projects a user has access to // @Summary Get all projects a user has access to // @Description Returns all projects a user has access to. @@ -184,7 +151,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 +164,47 @@ 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 + ///////////////// + // Saved Filters + + savedFiltersProject, err := getSavedFilterProjects(s, doer) + if err != nil { + return nil, 0, 0, err + } + + if savedFiltersProject != nil { + prs = append(prs, savedFiltersProject) + } + + ///////////////// + // Add project details (favorite state, among other things) + err = addProjectDetails(s, prs, a) + if err != nil { + return + } + + ////////////////////////// + // Putting it all together + + return prs, resultCount, totalItems, err } // ReadOne gets one project by its ID @@ -227,61 +219,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 +334,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 +369,152 @@ 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(¤tProjects) + if err != nil { + return 0, 0, err + } + + if len(currentProjects) == 0 { + return 0, oldTotalCount, err + } + + query = getUserProjectsStatement(parentProjectIDs, userID, opts.search, opts.getArchived) + totalCount, err = s. + SQL(query.Select("count(*)")). + Count(&Project{}) + if err != nil { + return 0, 0, err + } + + newParentIDs := []int64{} + for _, project := range currentProjects { + newParentIDs = append(newParentIDs, project.ID) + } + + *projects = append(*projects, currentProjects...) + + return getAllProjectsForUser(s, userID, newParentIDs, opts, projects, oldTotalCount+totalCount) +} + +// Gets the projects with their children without any tasks +func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*Project, resultCount int, totalItems int64, err error) { + fullUser, err := user.GetUserByID(s, opts.user.ID) if err != nil { return nil, 0, 0, err } - 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 + } + + favoriteCount, err := s. + Where(builder.And( + builder.Eq{"user_id": opts.user.ID}, + builder.Eq{"kind": FavoriteKindTask}, + )). + Count(&Favorite{}) + if err != nil { + return + } + + if favoriteCount > 0 { + favoritesProject := &Project{} + *favoritesProject = FavoritesPseudoProject + allProjects = append(allProjects, favoritesProject) + } + + 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 + + for _, filter := range savedFilters { + filterProject := filter.toProject() + filterProject.ParentProjectID = savedFiltersProject.ID + filterProject.Owner = doer + } + + 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 +524,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 +544,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,49 +593,70 @@ 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} +func checkProjectBeforeUpdateOrDelete(s *xorm.Session, project *Project) (err error) { + 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 { + if project.ParentProjectID == project.ID { + return &ErrProjectCannotBeChildOfItself{ + ProjectID: project.ID, + } + } + + var parent *Project + parent, err = GetProjectSimpleByID(s, project.ParentProjectID) if err != nil { return err } + + // Check if there's a cycle in the parent relation + parentsVisited := make(map[int64]bool) + parentsVisited[project.ID] = true + for { + if parent.ParentProjectID == 0 { + break + } + + // FIXME: Can we do this with better performance? + parent, err = GetProjectSimpleByID(s, parent.ParentProjectID) + if err != nil { + return err + } + + if parentsVisited[parent.ID] { + return &ErrProjectCannotHaveACyclicRelationship{ + ProjectID: project.ID, + } + } + + parentsVisited[parent.ID] = true + } } // Check if the identifier is unique and not empty @@ -595,7 +689,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 +727,39 @@ 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, u *user.User) (err error) { + p := &Project{ + Title: "Inbox", + } + err = p.Create(s, u) + if err != nil { + return err + } + + if u.DefaultProjectID != 0 { + return err + } + + u.DefaultProjectID = p.ID + _, err = user.UpdateUser(s, u, false) + return err +} + 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 != "" { @@ -664,6 +770,13 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje colsToUpdate = append(colsToUpdate, "background_file_id", "background_blur_hash") } + if project.Position < 0.1 { + err = recalculateProjectPositions(s, project.ParentProjectID) + if err != nil { + return err + } + } + wasFavorite, err := isFavorite(s, project.ID, auth, FavoriteKindProject) if err != nil { return err @@ -706,6 +819,34 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje return } +func recalculateProjectPositions(s *xorm.Session, parentProjectID int64) (err error) { + + allProjects := []*Project{} + err = s. + Where("parent_project_id = ?", parentProjectID). + OrderBy("position asc"). + Find(&allProjects) + if err != nil { + return + } + + maxPosition := math.Pow(2, 32) + + for i, project := range allProjects { + + currentPosition := maxPosition / float64(len(allProjects)) * (float64(i + 1)) + + _, err = s.Cols("position"). + Where("id = ?", project.ID). + Update(&Project{Position: currentPosition}) + if err != nil { + return + } + } + + return +} + // Update implements the update method of CRUDable // @Summary Updates a project // @Description Updates a project. This does not include adding a task (see below). @@ -720,27 +861,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 +901,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 +933,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 +966,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() } diff --git a/pkg/models/project_duplicate.go b/pkg/models/project_duplicate.go index 3307935af..f4f8d99ad 100644 --- a/pkg/models/project_duplicate.go +++ b/pkg/models/project_duplicate.go @@ -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 { diff --git a/pkg/models/project_duplicate_test.go b/pkg/models/project_duplicate_test.go index 70ff0f95e..0768c20b1 100644 --- a/pkg/models/project_duplicate_test.go +++ b/pkg/models/project_duplicate_test.go @@ -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) diff --git a/pkg/models/project_rights.go b/pkg/models/project_rights.go index da8285fb2..01be9d8d8 100644 --- a/pkg/models/project_rights.go +++ b/pkg/models/project_rights.go @@ -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 } diff --git a/pkg/models/project_team.go b/pkg/models/project_team.go index e89413741..b87d51387 100644 --- a/pkg/models/project_team.go +++ b/pkg/models/project_team.go @@ -182,7 +182,7 @@ func (tl *TeamProject) Delete(s *xorm.Session, _ 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 { diff --git a/pkg/models/project_team_test.go b/pkg/models/project_team_test.go index 0b56e93e0..6de71a3b8 100644 --- a/pkg/models/project_team_test.go +++ b/pkg/models/project_team_test.go @@ -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, diff --git a/pkg/models/project_test.go b/pkg/models/project_test.go index 4e80919d8..c051c194b 100644 --- a/pkg/models/project_test.go +++ b/pkg/models/project_test.go @@ -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,12 @@ 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, 25, 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) + assert.Equal(t, int64(-1), ls[23].ID) + assert.Equal(t, int64(-3), ls[24].ID) _ = s.Close() }) t.Run("projects for nonexistant user", func(t *testing.T) { @@ -365,8 +346,10 @@ func TestProject_ReadAll(t *testing.T) { assert.NoError(t, err) ls := projects3.([]*Project) - assert.Equal(t, 1, len(ls)) + assert.Equal(t, 3, len(ls)) assert.Equal(t, int64(10), ls[0].ID) + assert.Equal(t, int64(-1), ls[1].ID) + assert.Equal(t, int64(-3), ls[2].ID) _ = s.Close() }) } diff --git a/pkg/models/project_users.go b/pkg/models/project_users.go index 1cf7cfbbf..26c9508d1 100644 --- a/pkg/models/project_users.go +++ b/pkg/models/project_users.go @@ -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"` diff --git a/pkg/models/project_users_test.go b/pkg/models/project_users_test.go index 69d2d97a7..82eb7e7f2 100644 --- a/pkg/models/project_users_test.go +++ b/pkg/models/project_users_test.go @@ -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, diff --git a/pkg/models/rights.go b/pkg/models/rights.go index 67f3585ec..ddfdbc22e 100644 --- a/pkg/models/rights.go +++ b/pkg/models/rights.go @@ -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 ) diff --git a/pkg/models/saved_filters.go b/pkg/models/saved_filters.go index b2a5958f6..595b9ac30 100644 --- a/pkg/models/saved_filters.go +++ b/pkg/models/saved_filters.go @@ -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, } } diff --git a/pkg/models/subscription.go b/pkg/models/subscription.go index 51aac4874..67a84d929 100644 --- a/pkg/models/subscription.go +++ b/pkg/models/subscription.go @@ -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) diff --git a/pkg/models/subscription_rights.go b/pkg/models/subscription_rights.go index 88f45438a..123dd47b2 100644 --- a/pkg/models/subscription_rights.go +++ b/pkg/models/subscription_rights.go @@ -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) diff --git a/pkg/models/subscription_test.go b/pkg/models/subscription_test.go index 1b23e1f24..a5b5b3867 100644 --- a/pkg/models/subscription_test.go +++ b/pkg/models/subscription_test.go @@ -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) { diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index b5e526f98..5c2c038bc 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -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"` @@ -181,8 +180,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()}, @@ -193,7 +193,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 { @@ -202,8 +202,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) } diff --git a/pkg/models/task_collection_filter.go b/pkg/models/task_collection_filter.go index 3e5331eb9..4644b98f6 100644 --- a/pkg/models/task_collection_filter.go +++ b/pkg/models/task_collection_filter.go @@ -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...) diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index 557568367..39b051d2e 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -17,6 +17,7 @@ package models import ( + "sort" "testing" "time" @@ -403,11 +404,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), @@ -416,26 +417,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), } @@ -446,7 +447,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), @@ -637,7 +638,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { name string fields fields args args - want interface{} + want []*Task wantErr bool } @@ -689,7 +690,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"}, @@ -812,11 +813,13 @@ func TestTaskCollection_ReadAll(t *testing.T) { task19, task20, task21, + task22, task23, task24, task25, task26, + task27, task28, task29, @@ -1079,33 +1082,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{ @@ -1267,11 +1244,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, \nwant %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, \nwant %v, \ndiff: %v \n\n diffIDs: %v", tt.name, got, tt.want, diff, diffIDs) } }) } diff --git a/pkg/models/task_overdue_reminder.go b/pkg/models/task_overdue_reminder.go index 288906f12..6a77682d3 100644 --- a/pkg/models/task_overdue_reminder.go +++ b/pkg/models/task_overdue_reminder.go @@ -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 { diff --git a/pkg/models/task_relation_test.go b/pkg/models/task_relation_test.go index b037d224d..ca7e92abc 100644 --- a/pkg/models/task_relation_test.go +++ b/pkg/models/task_relation_test.go @@ -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}) diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index ab1445c9e..8dda06fde 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -333,7 +333,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. @@ -371,13 +371,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 } @@ -401,30 +401,12 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op } var projectIDCond builder.Cond - var projectCond builder.Cond + var favoritesCond builder.Cond if len(projectIDs) > 0 { projectIDCond = builder.In("project_id", projectIDs) - projectCond = projectIDCond } if hasFavoritesProject { - // Make sure users can only see their favorites - userProjects, _, _, err := getRawProjectsForUser( - s, - &projectOptions{ - user: &user.User{ID: a.GetID()}, - page: -1, - }, - ) - if err != nil { - return nil, 0, 0, err - } - - userProjectIDs := make([]int64, 0, len(userProjects)) - for _, l := range userProjects { - userProjectIDs = append(userProjectIDs, l.ID) - } - // All favorite tasks for that user favCond := builder. Select("entity_id"). @@ -435,7 +417,7 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op builder.Eq{"kind": FavoriteKindTask}, )) - projectCond = builder.And(projectCond, builder.And(builder.In("id", favCond), builder.In("project_id", userProjectIDs))) + favoritesCond = builder.In("id", favCond) } if len(reminderFilters) > 0 { @@ -456,13 +438,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( @@ -486,7 +468,7 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op } limit, start := getLimitFromPageIndex(opts.page, opts.perPage) - cond := builder.And(projectCond, where, filterCond) + cond := builder.And(builder.Or(projectIDCond, favoritesCond), where, filterCond) query := s.Where(cond) if limit > 0 { diff --git a/pkg/models/teams.go b/pkg/models/teams.go index cd067dbf4..03e966862 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -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 { diff --git a/pkg/models/teams_rights_test.go b/pkg/models/teams_rights_test.go index f152fb362..1d4b49666 100644 --- a/pkg/models/teams_rights_test.go +++ b/pkg/models/teams_rights_test.go @@ -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{ diff --git a/pkg/models/teams_test.go b/pkg/models/teams_test.go index 5bbaf6518..cfbb80d2f 100644 --- a/pkg/models/teams_test.go +++ b/pkg/models/teams_test.go @@ -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() diff --git a/pkg/models/unit_tests.go b/pkg/models/unit_tests.go index ce9b80714..3dd56dad3 100644 --- a/pkg/models/unit_tests.go +++ b/pkg/models/unit_tests.go @@ -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", diff --git a/pkg/models/user_delete.go b/pkg/models/user_delete.go index 55858c149..bfba53a00 100644 --- a/pkg/models/user_delete.go +++ b/pkg/models/user_delete.go @@ -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) diff --git a/pkg/models/user_delete_test.go b/pkg/models/user_delete_test.go index 0d1811b08..e589d10e7 100644 --- a/pkg/models/user_delete_test.go +++ b/pkg/models/user_delete_test.go @@ -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 }) } diff --git a/pkg/models/user_project.go b/pkg/models/user_project.go index 75588b1bd..2397e2dbc 100644 --- a/pkg/models/user_project.go +++ b/pkg/models/user_project.go @@ -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(¤tUserIDs) + 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)) diff --git a/pkg/models/user_project_test.go b/pkg/models/user_project_test.go index 0a630b261..38ab1e885 100644 --- a/pkg/models/user_project_test.go +++ b/pkg/models/user_project_test.go @@ -205,13 +205,13 @@ func TestListUsersFromProject(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 }, }, { diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index a5649eb84..577760c19 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -243,8 +243,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 } diff --git a/pkg/modules/background/upload/upload.go b/pkg/modules/background/upload/upload.go index b0c5d2569..8103e1795 100644 --- a/pkg/modules/background/upload/upload.go +++ b/pkg/modules/background/upload/upload.go @@ -54,9 +54,11 @@ func (p *Provider) Search(_ *xorm.Session, _ string, _ int64) (result []*backgro // @Router /projects/{id}/backgrounds/upload [put] func (p *Provider) Set(s *xorm.Session, img *background.Image, project *models.Project, _ 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{} diff --git a/pkg/modules/migration/create_from_structure.go b/pkg/modules/migration/create_from_structure.go index d392a3c96..eda0fcd40 100644 --- a/pkg/modules/migration/create_from_structure.go +++ b/pkg/modules/migration/create_from_structure.go @@ -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 +} diff --git a/pkg/modules/migration/create_from_structure_test.go b/pkg/modules/migration/create_from_structure_test.go index ec3a37240..188ec7678 100644 --- a/pkg/modules/migration/create_from_structure_test.go +++ b/pkg/modules/migration/create_from_structure_test.go @@ -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 }) } diff --git a/pkg/modules/migration/microsoft-todo/microsoft_todo.go b/pkg/modules/migration/microsoft-todo/microsoft_todo.go index f44ea0e66..d2d44480c 100644 --- a/pkg/modules/migration/microsoft-todo/microsoft_todo.go +++ b/pkg/modules/migration/microsoft-todo/microsoft_todo.go @@ -218,7 +218,7 @@ func getMicrosoftTodoData(token string) (microsoftTodoData []*project, err error microsoftTodoData = []*project{} projects := &projectsResponse{} - err = makeAuthenticatedGetRequest(token, "projects", projects) + err = makeAuthenticatedGetRequest(token, "lists", projects) if err != nil { log.Errorf("[Microsoft Todo Migration] Could not get projects: %s", err) return @@ -227,7 +227,7 @@ func getMicrosoftTodoData(token string) (microsoftTodoData []*project, err error log.Debugf("[Microsoft Todo Migration] Got %d projects", len(projects.Value)) for _, project := range projects.Value { - link := "projects/" + project.ID + "/tasks" + link := "lists/" + project.ID + "/tasks" project.Tasks = []*task{} // Microsoft's Graph API has pagination, so we're going through all pages to get all tasks @@ -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{}, }, } @@ -364,7 +364,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) } diff --git a/pkg/modules/migration/microsoft-todo/microsoft_todo_test.go b/pkg/modules/migration/microsoft-todo/microsoft_todo_test.go index 79f5692b8..6b8b123cb 100644 --- a/pkg/modules/migration/microsoft-todo/microsoft_todo_test.go +++ b/pkg/modules/migration/microsoft-todo/microsoft_todo_test.go @@ -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", diff --git a/pkg/modules/migration/ticktick/ticktick.go b/pkg/modules/migration/ticktick/ticktick.go index 6e49e6ae9..9987a4557 100644 --- a/pkg/modules/migration/ticktick/ticktick.go +++ b/pkg/modules/migration/ticktick/ticktick.go @@ -40,7 +40,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:"-"` @@ -74,21 +74,21 @@ func (date *tickTickTime) UnmarshalCSV(csv string) (err error) { return err } -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, }, } } @@ -130,18 +130,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. diff --git a/pkg/modules/migration/ticktick/ticktick_test.go b/pkg/modules/migration/ticktick/ticktick_test.go index c1ddf5a67..c7674c24c 100644 --- a/pkg/modules/migration/ticktick/ticktick_test.go +++ b/pkg/modules/migration/ticktick/ticktick_test.go @@ -40,77 +40,77 @@ 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[0].RelativeTo, models.ReminderRelation("due_date")) - assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Reminders[0].RelativePeriod, int64(-24*3600)) - 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[0].RelativeTo, models.ReminderRelation("due_date")) + assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Reminders[0].RelativePeriod, int64(-24*3600)) + 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, @@ -118,24 +118,24 @@ 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[2].Reminders[0].RelativeTo, models.ReminderRelation("due_date")) - assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[2].Reminders[0].RelativePeriod, int64(-24*3600)) - 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[2].Reminders[0].RelativeTo, models.ReminderRelation("due_date")) + assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].Reminders[0].RelativePeriod, int64(-24*3600)) + 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) } diff --git a/pkg/modules/migration/todoist/todoist.go b/pkg/modules/migration/todoist/todoist.go index 4df6dae3a..86a743f14 100644 --- a/pkg/modules/migration/todoist/todoist.go +++ b/pkg/modules/migration/todoist/todoist.go @@ -246,10 +246,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", }, } @@ -276,7 +276,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 { @@ -472,9 +472,7 @@ func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVi ) } - return []*models.NamespaceWithProjectsAndTasks{ - newNamespace, - }, err + return []*models.ProjectWithTasksAndBuckets{parent}, err } func getAccessTokenFromAuthToken(authToken string) (accessToken string, err error) { diff --git a/pkg/modules/migration/todoist/todoist_test.go b/pkg/modules/migration/todoist/todoist_test.go index 13d6dbf5a..8c65cbd40 100644 --- a/pkg/modules/migration/todoist/todoist_test.go +++ b/pkg/modules/migration/todoist/todoist_test.go @@ -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", diff --git a/pkg/modules/migration/trello/trello.go b/pkg/modules/migration/trello/trello.go index 85062a55d..de5b83b78 100644 --- a/pkg/modules/migration/trello/trello.go +++ b/pkg/modules/migration/trello/trello.go @@ -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 diff --git a/pkg/modules/migration/trello/trello_test.go b/pkg/modules/migration/trello/trello_test.go index df439c57f..8594fe3b4 100644 --- a/pkg/modules/migration/trello/trello_test.go +++ b/pkg/modules/migration/trello/trello_test.go @@ -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", diff --git a/pkg/modules/migration/vikunja-file/export.zip b/pkg/modules/migration/vikunja-file/export.zip index c22c81289..0e9442ecb 100644 Binary files a/pkg/modules/migration/vikunja-file/export.zip and b/pkg/modules/migration/vikunja-file/export.zip differ diff --git a/pkg/modules/migration/vikunja-file/export_pre_0.21.0.zip b/pkg/modules/migration/vikunja-file/export_pre_0.21.0.zip new file mode 100644 index 000000000..9289c3224 Binary files /dev/null and b/pkg/modules/migration/vikunja-file/export_pre_0.21.0.zip differ diff --git a/pkg/modules/migration/vikunja-file/vikunja.go b/pkg/modules/migration/vikunja-file/vikunja.go index ac810c953..7ee90b6f1 100644 --- a/pkg/modules/migration/vikunja-file/vikunja.go +++ b/pkg/modules/migration/vikunja-file/vikunja.go @@ -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,57 +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 { - attachmentFile, exists := storedFiles[attachment.File.ID] - if !exists { - log.Debugf(logPrefix+"Could not find attachment file %d for attachment %d", attachment.File.ID, attachment.ID) - continue - } - af, err := attachmentFile.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) } @@ -207,3 +201,64 @@ 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 { + attachmentFile, exists := storedFiles[attachment.File.ID] + if !exists { + log.Debugf(logPrefix+"Could not find attachment file %d for attachment %d", attachment.File.ID, attachment.ID) + continue + } + af, err := attachmentFile.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 +} diff --git a/pkg/modules/migration/vikunja-file/vikunja_test.go b/pkg/modules/migration/vikunja-file/vikunja_test.go index d63becb54..676a5ebc3 100644 --- a/pkg/modules/migration/vikunja-file/vikunja_test.go +++ b/pkg/modules/migration/vikunja-file/vikunja_test.go @@ -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") + }) } diff --git a/pkg/routes/api/v1/project_by_namespace.go b/pkg/routes/api/v1/project_by_namespace.go deleted file mode 100644 index 1229e35c7..000000000 --- a/pkg/routes/api/v1/project_by_namespace.go +++ /dev/null @@ -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 . - -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 namespaceID 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/{namespaceID}/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 -} diff --git a/pkg/routes/api/v1/user_register.go b/pkg/routes/api/v1/user_register.go index 40f7d233a..1fcc77b48 100644 --- a/pkg/routes/api/v1/user_register.go +++ b/pkg/routes/api/v1/user_register.go @@ -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) diff --git a/pkg/routes/metrics.go b/pkg/routes/metrics.go index ebe279436..2676f9dfa 100644 --- a/pkg/routes/metrics.go +++ b/pkg/routes/metrics.go @@ -51,10 +51,6 @@ func setupMetrics(a *echo.Group) { metrics.UserCountKey, user.User{}, }, - { - metrics.NamespaceCountKey, - models.Namespace{}, - }, { metrics.TaskCountKey, models.Task{}, diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 379debe12..68beb2a7a 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -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 `-header to authenticate successfully. @@ -324,7 +324,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() { @@ -487,38 +487,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{} diff --git a/pkg/user/notifications.go b/pkg/user/notifications.go index 768ac9e24..96a91d93d 100644 --- a/pkg/user/notifications.go +++ b/pkg/user/notifications.go @@ -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!") } diff --git a/pkg/user/user_create.go b/pkg/user/user_create.go index b51e94bb3..6f09ed66c 100644 --- a/pkg/user/user_create.go +++ b/pkg/user/user_create.go @@ -87,7 +87,7 @@ func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) { return nil, err } - // Dont send a mail if no mailer is configured + // Don't send a mail if no mailer is configured if !config.MailerEnabled.GetBool() || user.Issuer != IssuerLocal { return newUserOut, err } diff --git a/pkg/user/user_test.go b/pkg/user/user_test.go index a755fe882..bfe7d5d06 100644 --- a/pkg/user/user_test.go +++ b/pkg/user/user_test.go @@ -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() diff --git a/pkg/user/users_project.go b/pkg/user/users_project.go index e74b31102..299a50c01 100644 --- a/pkg/user/users_project.go +++ b/pkg/user/users_project.go @@ -32,7 +32,7 @@ type ProjectUserOpts struct { MatchFuzzily 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{}