Compare commits

...

204 Commits

Author SHA1 Message Date
WofWca 6aadaaaffc chore: rename files (fix typo) 2023-03-21 19:02:05 +00:00
renovate 6566f0e81d fix(deps): update module github.com/swaggo/swag to v1.8.11 2023-03-21 13:01:35 +00:00
renovate 6d8db0ce1e chore(deps): update goreleaser/nfpm docker tag to v2.27.1 (#1438)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1438
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-03-20 20:21:40 +00:00
renovate 085b9222bb fix(deps): update github.com/arran4/golang-ical digest to 19abf92 2023-03-18 09:01:35 +00:00
kolaente a0b3a444df
fix(lint): disable misspell linter on redoc 2023-03-18 09:55:18 +01:00
kolaente 8916de0366
fix: update redoc 2023-03-16 19:08:18 +01:00
renovate 769db0dab2 fix(deps): update module github.com/imdario/mergo to v0.3.14 2023-03-16 00:01:43 +00:00
kolaente 259cf7d25b
docs: update error references to list 2023-03-14 17:41:26 +01:00
kolaente 8dc6c95333
docs: update references to list 2023-03-14 17:39:46 +01:00
konrad 869d4a336c feat: rename lists to projects everywhere (#1318)
Reviewed-on: vikunja/api#1318
2023-03-14 13:19:47 +00:00
kolaente 7a9611c2da
chore: cleanup 2023-03-13 14:28:36 +01:00
kolaente 7cab3a77a9
fix(migration): rename TickTick migration 2023-03-13 14:28:25 +01:00
kolaente 77ad90d53e
fix(migration): remove wunderlist leftovers 2023-03-13 14:28:20 +01:00
kolaente 55410ea73d
chore: generate swagger docs 2023-03-13 14:28:19 +01:00
kolaente e4f841cf6a
fix(tasks): sql for overdue reminders 2023-03-13 14:28:19 +01:00
kolaente 2940eae1aa
fix(migration): use correct struct 2023-03-13 14:28:19 +01:00
kolaente 0a3fdc0344
fix: users_lists name in migration 2023-03-13 14:28:19 +01:00
kolaente 06f1d2e912
fix: test fixtures 2023-03-13 14:28:07 +01:00
kolaente 61a3380a94
fix: trello import tests 2023-03-13 14:28:07 +01:00
kolaente fb818ea186
fix: test import 2023-03-13 14:28:06 +01:00
kolaente 7e53a21407
fix: rename incorrectly named ProjectUsers method 2023-03-13 14:28:06 +01:00
kolaente 8f4abd2fe8
feat: rename all list files 2023-03-13 14:28:06 +01:00
kolaente 2fba7bdf02
feat: migrate lists to projects in db identifiers 2023-03-13 14:28:06 +01:00
kolaente 349e6a5905
feat: rename lists to projects 2023-03-13 14:28:06 +01:00
kolaente 80266d1383
fix(docker): don't chown everything in Vikunja's default root folder
This would try to chown a mounted Vikunja config file as well which failed when that config file was mounted read-only.
2023-03-13 11:23:51 +01:00
kolaente c0c523f0a8
fix: don't send bad request errors to sentry 2023-03-13 10:52:52 +01:00
kolaente 672fb35bcb
fix: check if usernames contain spaces when creating a new user 2023-03-12 15:02:34 +01:00
kolaente 1f13b5d7b4
docs: fix menu links 2023-03-12 10:46:17 +01:00
kolaente e79778e213
chore: 0.20.4 release preperations 2023-03-12 10:24:34 +01:00
kolaente a897ffc8ee
fix(docker): allow non-unique group id 2023-03-11 15:04:36 +01:00
kolaente 4de0efec1d
docs: add link to tutorial for installing Vikunja on Synology 2023-03-11 14:55:30 +01:00
kolaente 09ddd5a31a
chore: 0.20.3 release preperations 2023-03-10 14:55:01 +01:00
renovate 387aa2db93 fix(deps): update module github.com/gabriel-vasile/mimetype to v1.4.2 2023-03-08 15:01:24 +00:00
renovate 4f62e978ef fix(deps): update src.techknowlogick.com/xgo digest to b607086 2023-03-07 18:01:48 +00:00
renovate a5c241758a fix(deps): update module github.com/ulule/limiter/v3 to v3.11.1 2023-03-07 10:01:21 +00:00
renovate abf38defa2 fix(deps): update module github.com/spf13/afero to v1.9.5 2023-03-06 10:01:21 +00:00
renovate 29f8522de9 fix(deps): update module github.com/getsentry/sentry-go to v0.19.0 2023-03-06 08:01:20 +00:00
kolaente 9f14466dfa
fix: lint 2023-03-05 22:24:29 +01:00
renovate 4a82f3be3c fix(deps): update src.techknowlogick.com/xgo digest to 44f7e66 2023-03-05 20:31:58 +00:00
renovate a39b3aeb06 fix(deps): update github.com/vectordotdev/go-datemath digest to f3954d0 2023-03-05 19:01:25 +00:00
kolaente b4c215f4dd
chore(deps): remove fsnotify replacement 2023-03-05 19:13:19 +01:00
renovate f0a8516926 chore(deps): update github.com/kolaente/caldav-go digest to 2a4eb8b 2023-03-05 18:01:29 +00:00
kolaente 077baba2ea
fix: lint 2023-03-05 14:34:34 +01:00
kolaente 066c26f83e
fix(caldav): make sure only labels where the user has permission to use them are used
Follow-up for a62b57ac62
2023-03-05 14:03:09 +01:00
renovate eda5135b3c fix(deps): update module golang.org/x/image to v0.6.0 2023-03-05 09:01:16 +00:00
renovate 906c9fc06b fix(deps): update module golang.org/x/oauth2 to v0.6.0 2023-03-05 08:01:23 +00:00
renovate e37dbb1648 fix(deps): update module golang.org/x/crypto to v0.7.0 2023-03-05 07:28:11 +00:00
renovate cd13f86e0d fix(deps): update module golang.org/x/term to v0.6.0 2023-03-05 02:01:25 +00:00
renovate 7ad754dd3b fix(deps): update module golang.org/x/sys to v0.6.0 2023-03-04 16:01:23 +00:00
cernst a62b57ac62 feat(caldav): import caldav categories as Labels (#1413)
Resolves #1274

Co-authored-by: ce72 <christoph.ernst72@googlemail.com>
Reviewed-on: vikunja/api#1413
Reviewed-by: konrad <k@knt.li>
Co-authored-by: cernst <ce72@noreply.kolaente.de>
Co-committed-by: cernst <ce72@noreply.kolaente.de>
2023-03-02 15:25:26 +00:00
kolaente 534d04a1db
fix(task): correctly load tasks by id and uuid in caldav
Partially reverts 1afc72e190
2023-03-01 22:18:59 +01:00
kolaente e8c85562b1
fix(docs): clarify support for caldav reccurrence 2023-03-01 11:05:35 +01:00
cernst 1afc72e190 fix: Make sure labels are always exported as caldav (#1412)
Authored-by: ce72 <christoph.ernst72@googlemail.com>
Reviewed-on: vikunja/api#1412
Reviewed-by: konrad <k@knt.li>
Co-authored-by: cernst <ce72@noreply.kolaente.de>
Co-committed-by: cernst <ce72@noreply.kolaente.de>
2023-02-28 10:42:57 +00:00
cernst 53197b85e3 feat(caldav): Export Labels to Caldav (#1409)
Partially resolves vikunja/api#1274

Co-authored-by: ce72 <christoph.ernst72@googlemail.com>
Reviewed-on: vikunja/api#1409
Reviewed-by: konrad <k@knt.li>
Co-authored-by: cernst <ce72@noreply.kolaente.de>
Co-committed-by: cernst <ce72@noreply.kolaente.de>
2023-02-27 11:22:42 +00:00
renovate d47535b831 fix(deps): update github.com/gocarina/gocsv digest to 70c27cb 2023-02-26 14:01:15 +00:00
renovate c9ce9c2382 fix(deps): update module github.com/stretchr/testify to v1.8.2 2023-02-25 13:01:15 +00:00
kolaente 168287923f
fix(migration): make sure trello checklists are properly imported 2023-02-24 12:13:18 +01:00
kolaente ca6d1946da
fix(tasks): make sure tasks are sorted by position before recalculating them 2023-02-23 17:43:23 +01:00
renovate 8b745ad599 fix(deps): update github.com/gocarina/gocsv digest to dc4ee9d 2023-02-23 15:06:32 +00:00
kolaente 1efa1696bf
fix(tasks): recalculate position of all tasks in a list or bucket when it would hit 0 2023-02-23 16:00:41 +01:00
renovate 6908167fb2 fix(deps): update module github.com/spf13/afero to v1.9.4 2023-02-23 10:01:17 +00:00
renovate 26b280019b fix(deps): update module github.com/labstack/echo/v4 to v4.10.2 2023-02-22 00:01:46 +00:00
renovate d19529e84f fix(deps): update github.com/gocarina/gocsv digest to bee85ea 2023-02-21 18:01:17 +00:00
renovate 0f7f595cc3 fix(deps): update module github.com/labstack/echo/v4 to v4.10.1 2023-02-20 21:17:39 +00:00
kolaente c6769d407e
chore(deps): update golangci-lint to 1.51.2 2023-02-20 15:17:49 +01:00
renovate 8df94e8800 fix(deps): update github.com/gocarina/gocsv digest to bcce7dc 2023-02-19 21:01:13 +00:00
renovate 5af1147e0d fix(deps): update module github.com/golang-jwt/jwt/v4 to v4.5.0 (#1399)
Reviewed-on: vikunja/api#1399
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-19 12:38:39 +00:00
kolaente 8bf224d392
chore(deps): update golang.org/x/net to 0.7.0 2023-02-18 17:32:14 +01:00
kolaente 0a0374e268
chore(docs): remove sponsors 2023-02-15 11:11:10 +01:00
kolaente da9d25cf72
feat: disable events log by default
BREAKING CHANGE: events log level is now off unless explicitly enabled
2023-02-15 10:44:02 +01:00
kolaente eb33655c1c
fix(docker): make sure the vikunja user always exists and only modify the uid instead of recreating the user
Resolves #1392
2023-02-15 10:39:48 +01:00
kolaente 20a5994b17
fix: lint 2023-02-14 20:37:16 +01:00
renovate 5a0cee2fc5 fix(deps): update module golang.org/x/image to v0.5.0 (#1396)
Reviewed-on: vikunja/api#1396
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-14 19:21:12 +00:00
kolaente fceb5dae0f
fix(task): make sure the task's last updated timestamp is always updated when releated entities changed 2023-02-14 20:09:05 +01:00
renovate 659803fadf fix(deps): update module github.com/threedotslabs/watermill to v1.2.0 (#1384)
Reviewed-on: vikunja/api#1384
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-14 09:51:50 +00:00
renovate 4fbe2b313b fix(deps): update github.com/arran4/golang-ical digest to 07c6aad 2023-02-14 00:02:42 +00:00
renovate a6a47a27cb chore(deps): update goreleaser/nfpm docker tag to v2.26.0 (#1394)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1394
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-13 10:05:08 +00:00
renovate 247f678491 fix(deps): update module golang.org/x/image to v0.4.0 2023-02-09 08:01:12 +00:00
renovate 7d796cea0a fix(deps): update module golang.org/x/oauth2 to v0.5.0 2023-02-09 07:32:59 +00:00
renovate 984bcb90e1 fix(deps): update module golang.org/x/crypto to v0.6.0 2023-02-08 23:01:19 +00:00
renovate 6bbcd1399f fix(deps): update module golang.org/x/term to v0.5.0 2023-02-07 20:01:16 +00:00
kolaente 58da38adb6
fix(migration): don't try to add nonexistent tasks as related
Discussion: https://community.vikunja.io/t/todoist-migration-fails-after-51-iterations-19-minutes/1137
2023-02-07 17:06:04 +01:00
renovate 4a05d1a497 fix(deps): update module github.com/getsentry/sentry-go to v0.18.0 (#1386)
Reviewed-on: vikunja/api#1386
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-07 15:12:41 +00:00
renovate d6643f5af3 fix(deps): update module golang.org/x/sys to v0.5.0 2023-02-07 01:01:18 +00:00
kolaente aa25ccdc91
chore: update funding links 2023-02-03 11:48:44 +01:00
Jef Oliver cb96590611 fix(docd): Update Subdirectory Documentation (#1363)
Signed-off-by: Jef Oliver <jef@eljef.me>
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1363
Co-authored-by: Jef Oliver <jef@eljef.me>
Co-committed-by: Jef Oliver <jef@eljef.me>
2023-02-03 08:41:36 +00:00
kolaente 5d242f7e54
chore(deps): update xgo to 1.20 2023-02-02 17:44:57 +01:00
renovate 02352841dc chore(deps): update module go to 1.20 2023-02-02 16:37:47 +00:00
renovate d682a22cd5 fix(deps): update module github.com/yuin/goldmark to v1.5.4 2023-02-02 13:01:12 +00:00
kolaente fdbe110945
chore(deps): upgrade golangci-lint to 1.51.0 2023-02-02 11:16:07 +01:00
renovate 7b46446e03 chore(deps): update goreleaser/nfpm docker tag to v2.25.0 (#1382)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1382
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-02 10:12:02 +00:00
kolaente 3b1887e438
fix(docker): passing environment variables into the container 2023-02-01 18:50:40 +01:00
kolaente 32ff4b2cbd
fix(docker): re-add expose 2023-02-01 18:39:57 +01:00
kolaente 6ddadba573
fix(docker): cross compilation with buildx 2023-02-01 15:06:55 +01:00
clos afdceb0aff fix(list): when list background is removed, delete file from file system and DB (#1372)
Co-authored-by: testinho.testador <testinho.testador@noreply.kolaente.de>
Reviewed-on: vikunja/api#1372
Reviewed-by: konrad <k@knt.li>
Co-authored-by: clos <clos@noreply.kolaente.de>
Co-committed-by: clos <clos@noreply.kolaente.de>
2023-02-01 11:38:23 +00:00
renovate 437960b146 fix(deps): update module github.com/redis/go-redis/v9 to v9.0.2 2023-02-01 09:01:10 +00:00
renovate 75baba32a6 fix(deps): update module github.com/ulule/limiter/v3 to v3.11.0 (#1378)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1378
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-31 20:38:02 +00:00
renovate 9432b437fe fix(deps): update module github.com/labstack/echo-jwt/v4 to v4.1.0 2023-01-31 17:01:19 +00:00
renovate ef8e97f95e fix(deps): update module github.com/go-redis/redis/v8 to v9 (#1377)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1377
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-31 16:45:58 +00:00
Yurii Vlasov 522bf7d2fc feat: refactored Dockerfile (#1375)
- Removed VIKUNJA_VERSION and custom git checkout, because it is not found in the repository. So it is not used anywhere.
- Optimized runner commands order
- Removed run.sh (it is not needed in fact)

Co-authored-by: Yurii Vlasov <yv@itsvit.org>
Reviewed-on: vikunja/api#1375
Co-authored-by: Yurii Vlasov <yuriy@vlasov.pro>
Co-committed-by: Yurii Vlasov <yuriy@vlasov.pro>
2023-01-31 16:16:21 +00:00
rriski 88dd544fc9
fix(docs): fix traefik v2 example (#65) 2023-01-31 15:43:29 +01:00
clos f660badc3d feat(background): add Last-Modified header (#1376)
Reviewed-on: vikunja/api#1376
Co-authored-by: clos <clos@noreply.kolaente.de>
Co-committed-by: clos <clos@noreply.kolaente.de>
2023-01-29 22:07:46 +00:00
kolaente 491a142378
fix: lint 2023-01-29 22:42:24 +01:00
kolaente 46b261c9fe
chore(docs): adjust docs about frontend docker container 2023-01-29 15:53:10 +01:00
kolaente 40411e4100
chore(task): add test to check if a task's reminders are duplicated 2023-01-26 16:06:49 +01:00
renovate f2b4e9260b fix(deps): update module github.com/swaggo/swag to v1.8.10 (#1371)
Reviewed-on: vikunja/api#1371
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-25 21:11:25 +00:00
kolaente 682123a9c9
fix(migration): todoist pagination now avoids too many loops 2023-01-24 22:27:57 +01:00
renovate 0cd9cd324e chore(deps): update goreleaser/nfpm docker tag to v2.24.0 (#1367)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1367
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-24 18:21:45 +00:00
renovate 100897cc9d fix(deps): update github.com/gocarina/gocsv digest to 763e25b (#1370)
Reviewed-on: vikunja/api#1370
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-24 17:49:13 +00:00
kolaente c59e006453
fix(migration): remove unused todoist parameters 2023-01-24 18:44:33 +01:00
kolaente 6a87d919fa
fix(ci): sign drone config
(cherry picked from commit 6259dd8d42)
2023-01-24 17:48:00 +01:00
kolaente d52816c7d7
fix(ci): save generated .tags file to correctly tag docker releases 2023-01-24 17:47:05 +01:00
renovate 87d0134bb2 fix(deps): update module golang.org/x/oauth2 to v0.4.0 (#1354)
Reviewed-on: vikunja/api#1354
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-24 16:23:04 +00:00
kolaente d19fc80b8b
chore: 0.20.2 release preperations 2023-01-24 17:15:00 +01:00
renovate fecce19f06 fix(deps): update module github.com/labstack/echo-jwt/v4 to v4.0.1 (#1369)
Reviewed-on: vikunja/api#1369
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-24 15:34:54 +00:00
kolaente 1971df7b84
fix(migration): use the proper authorization method for Todoist's api, fix issues with importing deleted items 2023-01-24 15:45:56 +01:00
kooshi 31a1452839 fix(migration): import TickTick data by column name instead of index (#1356)
Resolves: https://github.com/go-vikunja/api/issues/61
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1356
Co-authored-by: kooshi <kolaente.dev@pat.de.com>
Co-committed-by: kooshi <kolaente.dev@pat.de.com>
2023-01-24 13:58:18 +00:00
kolaente 530bb0a63c
fix(user): make reset the user's name to empty actually work 2023-01-23 18:30:01 +01:00
kolaente 7bf7a13bb9
fix(reminders): prevent duplicate reminders when updating task details 2023-01-23 18:14:15 +01:00
renovate 10e6843b11 fix(deps): update module github.com/spf13/viper to v1.15.0 (#1365)
Reviewed-on: vikunja/api#1365
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-19 16:34:04 +00:00
renovate 78f43829cf fix(deps): update module src.techknowlogick.com/xgo to v1.7.0+1.19.5 (#1364)
Reviewed-on: vikunja/api#1364
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-17 21:05:33 +00:00
renovate acd31c4ff7 fix(deps): update module github.com/getsentry/sentry-go to v0.17.0 (#1361)
Reviewed-on: vikunja/api#1361
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-12 13:25:10 +00:00
renovate 1c02114cf5 chore(deps): update klakegg/hugo docker tag to v0.107.0 (#1272)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1272
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-11 19:48:22 +00:00
kolaente 82f4a5ad50
fix(export): ignore file size for export files 2023-01-11 18:56:30 +01:00
kolaente 04614614fe
fix(tasks): don't set a repeating task done when moving it do the done bucket 2023-01-11 18:46:24 +01:00
kolaente 608bde9806
fix(ci): tagging logic for release docker images 2023-01-11 18:13:11 +01:00
renovate 568cc16797 fix(deps): update module src.techknowlogick.com/xgo to v1.6.0+1.19.5 (#1358)
Reviewed-on: vikunja/api#1358
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-10 19:21:26 +00:00
Rein 4b5e65d4c2
fix(mailer): forcessl config (#60)
Co-authored-by: Rein-R3 <rein@reinsan.top>
Reviewed-At: https://github.com/go-vikunja/api/pull/60
2023-01-09 12:39:43 +01:00
renovate 3329d83363 fix(deps): update module github.com/wneessen/go-mail to v0.3.8 (#1357)
Reviewed-on: vikunja/api#1357
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-07 12:52:06 +00:00
renovate c4e5e722e4 chore(deps): update goreleaser/nfpm docker tag to v2.23.0 (#1347)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1347
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-06 11:05:47 +00:00
Dominik Pschenitschni 508a3157e2 fix(drone): add type, fix pull, remove group (#1355)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1355
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2023-01-06 09:46:52 +00:00
TheDubliner 321a8f7e2b
fix(docs): fix a few minor typos (#59) 2023-01-06 10:33:43 +01:00
renovate dd7dcdd0cc fix(deps): update module golang.org/x/crypto to v0.5.0 (#1353)
Reviewed-on: vikunja/api#1353
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-04 18:37:35 +00:00
renovate cb6368036c fix(deps): update module golang.org/x/term to v0.4.0 (#1352)
Reviewed-on: vikunja/api#1352
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-04 18:23:30 +00:00
renovate 36dfcb8ddb fix(deps): update module golang.org/x/image to v0.3.0 (#1350)
Reviewed-on: vikunja/api#1350
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-04 16:16:04 +00:00
renovate 7f8c85118e fix(deps): update module golang.org/x/sys to v0.4.0 (#1351)
Reviewed-on: vikunja/api#1351
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-04 16:15:58 +00:00
renovate f3476bec6c fix(deps): update module github.com/coreos/go-oidc/v3 to v3.5.0 (#1349)
Reviewed-on: vikunja/api#1349
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-04 11:03:19 +00:00
kolaente 392bdd1b94
fix(ci): set release path to /source
Should fix https://github.com/techknowlogick/xgo/issues/187
2023-01-03 16:02:17 +01:00
renovate 80634d43c1 fix(deps): update module github.com/wneessen/go-mail to v0.3.7 (#1348)
Reviewed-on: vikunja/api#1348
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-03 14:48:48 +00:00
kolaente 4fa45bf9dc
chore: remove custom gitea bug template in favor of githubs 2022-12-30 11:57:03 +01:00
konrad ef1d1e2b20 feat(migrators): remove wunderlist (#1346)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1346
2022-12-29 17:12:39 +00:00
Dominik Pschenitschni ca3580766e fix(docs): old helm charts url (#1344)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/api#1344
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-12-28 16:06:55 +00:00
renovate c6429c8b13 fix(deps): update module github.com/labstack/echo/v4 to v4.10.0 (#1343)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1343
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-28 10:32:12 +00:00
renovate 304481cf28 fix(deps): update module github.com/wneessen/go-mail to v0.3.6 (#1342)
Reviewed-on: vikunja/api#1342
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-26 17:29:28 +00:00
kolaente 897a6e5d5c
fix(caldav): use const for repeat modes 2022-12-24 14:34:59 +01:00
kolaente 194b88e2eb
fix(tasks): don't reset the kanban bucket when updating a task and not providing one
Resolves https://github.com/go-vikunja/api/issues/56
2022-12-24 14:30:34 +01:00
kolaente c5327845ee
feat(caldav): add support for repeating tasks
Resolves https://github.com/go-vikunja/api/issues/57#issuecomment-1364373103
2022-12-24 12:19:51 +01:00
kolaente ea1d06bda6
fix(list): return lists for a namespace id even if that namespace is deleted
This fixes a problem where a namespace was deleted and its list were not.
Forum discussion: https://community.vikunja.io/t/list-deleted-but-tasks-showing-up-as-archived-and-overdue/1025
2022-12-23 17:48:21 +01:00
kolaente 0104aa504b
fix(ci): pin nfpm container version and binary location 2022-12-19 15:37:01 +01:00
kolaente 6a97a214a3
fix(migration): use Todoist v9 api to migrate tasks from them
Discussion: https://community.vikunja.io/t/importing-tasks-from-todoist/322/7
2022-12-18 20:38:58 +01:00
viehlieb a79b1de2d0 feat: provide logout url for openid providers (#1340)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1340
Co-authored-by: viehlieb <pf@pragma-shift.net>
Co-committed-by: viehlieb <pf@pragma-shift.net>
2022-12-18 18:26:28 +00:00
renovate e9ce930230 fix(deps): update module github.com/swaggo/swag to v1.8.9 (#1327)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1327
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-18 16:43:30 +00:00
renovate 6cb48e430e fix(deps): update module github.com/pquerna/otp to v1.4.0 (#1341)
Reviewed-on: vikunja/api#1341
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-14 07:21:11 +00:00
renovate a2c8426d02 fix(deps): update module golang.org/x/crypto to v0.4.0 (#1339)
Reviewed-on: vikunja/api#1339
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-07 13:16:10 +00:00
renovate 3be10ca4a2 fix(deps): update module github.com/getsentry/sentry-go to v0.16.0 (#1338)
Reviewed-on: vikunja/api#1338
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-07 12:15:16 +00:00
renovate ec297009d3 fix(deps): update module golang.org/x/oauth2 to v0.3.0 (#1337)
Reviewed-on: vikunja/api#1337
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-06 16:25:24 +00:00
renovate dbc30284f3 fix(deps): update module golang.org/x/oauth2 to v0.2.0 (#1316)
Reviewed-on: vikunja/api#1316
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-06 08:14:40 +00:00
renovate 879324dcd0 fix(deps): update module golang.org/x/image to v0.2.0 (#1335)
Reviewed-on: vikunja/api#1335
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-06 07:58:51 +00:00
renovate 3be6e93a05 fix(deps): update module golang.org/x/term to v0.3.0 (#1336)
Reviewed-on: vikunja/api#1336
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-06 07:58:21 +00:00
kolaente f93317bf5d
fix(caldav): add Z suffix to dates make it clear dates are in UTC 2022-12-04 21:01:04 +01:00
renovate 1cfdb085e5 fix(deps): update module golang.org/x/sys to v0.3.0 (#1333)
Reviewed-on: vikunja/api#1333
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-04 16:56:44 +00:00
kolaente 51cf8beaed
feat(release): use compressed binaries for package releases
vikunja/api#1330
2022-12-02 16:31:06 +01:00
kolaente b8c3b570a4
fix(restore): check if we're really dealing with a string 2022-12-02 14:49:32 +01:00
renovate 8ae062a095 fix(deps): update module github.com/go-sql-driver/mysql to v1.7.0 (#1332)
Reviewed-on: vikunja/api#1332
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-02 13:39:25 +00:00
kolaente 941d1e06c5
fix(build): downgrade xgo to 1.19.2 so that builds work again
See https://github.com/techknowlogick/xgo/issues/187
2022-12-01 19:00:48 +01:00
kolaente 1f2eb57602
fix(reminders): make sure an overdue reminder is sent when there is only one overdue task 2022-12-01 18:41:24 +01:00
kolaente 51911a8868
fix(reminders): overdue tasks join condition 2022-12-01 18:30:05 +01:00
kolaente 47aae115df
fix(tasks): don't include undone overdue tasks from archived lists or namespaces in notification mails
Resolves vikunja/api#1324
2022-12-01 18:07:30 +01:00
kolaente fbc4b91e0f
fix(dump): make sure null dates are properly set when restoring from a dump 2022-12-01 17:53:02 +01:00
kolaente 8c67be558f
fix: restore notifications table from dump when it already had the correct format 2022-12-01 17:33:00 +01:00
renovate e27cd9b336 fix(deps): update module github.com/golang-jwt/jwt/v4 to v4.4.3 (#1328)
Reviewed-on: vikunja/api#1328
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-29 15:14:04 +00:00
renovate 23b01a1ff6 fix(deps): update github.com/arran4/golang-ical digest to 1093469 (#1326)
Reviewed-on: vikunja/api#1326
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-22 13:19:30 +00:00
renovate f47faf577a fix(deps): update module github.com/wneessen/go-mail to v0.3.5 (#1325)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1325
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-21 11:56:19 +00:00
renovate 312525ebef fix(deps): update github.com/arran4/golang-ical digest to a677353 (#1323)
Reviewed-on: vikunja/api#1323
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-21 10:49:41 +00:00
renovate a17d2f4288 fix(deps): update module golang.org/x/crypto to v0.3.0 (#1321)
Reviewed-on: vikunja/api#1321
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-16 22:16:31 +00:00
kolaente 7e0aa20658
fix(docs): add docs about cli user delete 2022-11-15 14:53:58 +01:00
renovate c5a55e39bf fix(deps): update module github.com/spf13/afero to v1.9.3 (#1320)
Reviewed-on: vikunja/api#1320
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-14 20:55:13 +00:00
the-darkvoid 4d4ffe8b34 Added Google & Google Workspace to OpenId examples (#1319)
Reviewed-on: vikunja/api#1319
Co-authored-by: the-darkvoid <darkvoid@gmail.com>
Co-committed-by: the-darkvoid <darkvoid@gmail.com>
2022-11-14 11:41:45 +00:00
kolaente 3d9fcb9ffb
fix(ci): pipeline dependency 2022-11-12 14:32:16 +01:00
kolaente a6e214b654
feat: use docker buildx to build multiarch images 2022-11-12 14:30:48 +01:00
renovate c47e07f9b0 fix(deps): update module golang.org/x/crypto to v0.2.0 (#1315)
Reviewed-on: vikunja/api#1315
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-12 13:14:07 +00:00
renovate 3a4a04ee8e fix(deps): update module github.com/yuin/goldmark to v1.5.3 (#1317)
Reviewed-on: vikunja/api#1317
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-12 12:15:44 +00:00
kolaente 96b5e93379
fix: swagger docs 2022-11-11 15:34:26 +01:00
kolaente 87c2e442f2
chore: 0.20.1 release preperations 2022-11-11 11:59:49 +01:00
kolaente 33e27c66a0
fix(filters): try parsing invalid dates like 2022-11-1 2022-11-11 11:05:21 +01:00
kolaente 986129a784
fix(filters): try parsing dates without time 2022-11-11 10:54:50 +01:00
kolaente 3d7605591e
fix(filters): try to parse date filter fields of the provided dates are not valid iso dates
Resolves https://community.vikunja.io/t/due-date-saved-filter-doesnt-seem-to-work/966/12
2022-11-10 16:39:44 +01:00
kolaente 811514855b
fix(metrics): make currently active users actually work 2022-11-09 21:12:17 +01:00
kolaente a9e6776abf
fix(tasks): allow sorting by task index 2022-11-09 14:43:31 +01:00
renovate 15828df041 fix(deps): update module github.com/getsentry/sentry-go to v0.15.0 (#1314)
Reviewed-on: vikunja/api#1314
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-09 11:15:57 +00:00
kolaente f9b48ec091
fix(filter): only check for 0 values in filter fields with numeric values 2022-11-08 17:03:07 +01:00
kolaente 3b0b4a8460
fix(task): duplicate reminders when adding different ones between winter / summer time
Resolves F-889
2022-11-08 16:50:19 +01:00
kolaente 2ef5e54588
fix(filter): also check for 0 values if the filter should include nulls
Resolves https://community.vikunja.io/t/due-date-saved-filter-doesnt-seem-to-work/966
2022-11-08 16:27:16 +01:00
renovate 65bca226e0 fix(deps): update module github.com/prometheus/client_golang to v1.14.0 (#1313)
Reviewed-on: vikunja/api#1313
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-08 13:11:14 +00:00
renovate 63a9148132 fix(deps): update module golang.org/x/term to v0.2.0 (#1312)
Reviewed-on: vikunja/api#1312
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-08 07:26:13 +00:00
renovate b880d0e300 fix(deps): update module golang.org/x/sys to v0.2.0 (#1311)
Reviewed-on: vikunja/api#1311
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-07 20:15:54 +00:00
renovate 2c46fc25d4 fix(deps): update module github.com/spf13/viper to v1.14.0 (#1309)
Reviewed-on: vikunja/api#1309
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-06 19:17:48 +00:00
konrad 641a9da93d fix: usage with postgres over unix socket (#1308)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1308
2022-11-04 12:25:06 +00:00
kolaente 4d4dca12ef
fix(docs): add explanation on how to run the cli in docker 2022-11-03 15:13:58 +01:00
kolaente 622f2f0562
fix: look for the default bucket based on the position instead of the index 2022-11-03 15:10:20 +01:00
kolaente c495096444
fix: make sure task indexes are calculated correctly when moving tasks between lists
Resolves https://github.com/go-vikunja/api/issues/52
2022-11-02 17:40:52 +01:00
renovate 649d1e3e6f fix(deps): update module github.com/prometheus/client_golang to v1.13.1 (#1307)
Reviewed-on: vikunja/api#1307
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-02 15:59:42 +00:00
yverry c83cb8480d nessecary is a common misspelling of necessary (#1304)
Co-authored-by: Yann Verry <yann@verry.org>
Reviewed-on: vikunja/api#1304
Co-authored-by: yverry <yann_kolaente@verry.org>
Co-committed-by: yverry <yann_kolaente@verry.org>
2022-10-31 20:45:28 +00:00
kolaente 556abcd9d2
feat(docs): add relase checklist 2022-10-28 13:55:53 +02:00
200 changed files with 14379 additions and 12241 deletions

View File

@ -1,4 +1,7 @@
files/
dist/
logs/
Dockerfile
docker-manifest.tmpl
docker-manifest-unstable.tmpl

View File

@ -1,5 +1,6 @@
---
kind: pipeline
type: docker
name: testing
workspace:
@ -111,7 +112,7 @@ steps:
# compiling the same magefile at the same time. It's also faster if each step does not need to compile it first.
- name: mage
image: vikunja/golang-build:latest
pull: true
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
commands:
@ -122,7 +123,7 @@ steps:
- name: build
image: vikunja/golang-build:latest
pull: true
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
depends_on: [ mage ]
@ -133,21 +134,21 @@ steps:
- name: lint
image: golang:1.19-alpine
pull: true
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
depends_on: [ build ]
commands:
- export "GOROOT=$(go env GOROOT)"
- apk --no-cache add build-base git
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.49.0
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.51.2
- ./mage-static check:all
when:
event: [ push, tag, pull_request ]
- name: test-migration-prepare
image: kolaente/toolbox:latest
pull: true
pull: always
commands:
# Get the latest version
- wget https://dl.vikunja.io/api/unstable/vikunja-unstable-linux-amd64-full.zip -q -O vikunja-latest.zip
@ -155,7 +156,7 @@ steps:
- name: test-migration-sqlite
image: vikunja/golang-build:latest
pull: true
pull: always
depends_on: [ test-migration-prepare, build ]
environment:
VIKUNJA_DATABASE_TYPE: sqlite
@ -174,7 +175,7 @@ steps:
- name: test-migration-mysql
image: vikunja/golang-build:latest
pull: true
pull: always
depends_on: [ test-migration-prepare, build ]
environment:
VIKUNJA_DATABASE_TYPE: mysql
@ -193,7 +194,7 @@ steps:
- name: test-migration-psql
image: vikunja/golang-build:latest
pull: true
pull: always
depends_on: [ test-migration-prepare, build ]
environment:
VIKUNJA_DATABASE_TYPE: postgres
@ -213,7 +214,7 @@ steps:
- name: test
image: vikunja/golang-build:latest
pull: true
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
commands:
@ -224,7 +225,7 @@ steps:
- name: test-sqlite
image: vikunja/golang-build:latest
pull: true
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
VIKUNJA_TESTS_USE_CONFIG: 1
@ -241,7 +242,7 @@ steps:
- name: test-mysql
image: vikunja/golang-build:latest
pull: true
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
VIKUNJA_TESTS_USE_CONFIG: 1
@ -258,7 +259,7 @@ steps:
- name: test-postgres
image: vikunja/golang-build:latest
pull: true
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
VIKUNJA_TESTS_USE_CONFIG: 1
@ -276,7 +277,7 @@ steps:
- name: integration-test
image: vikunja/golang-build:latest
pull: true
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
commands:
@ -287,7 +288,7 @@ steps:
- name: integration-test-sqlite
image: vikunja/golang-build:latest
pull: true
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
VIKUNJA_TESTS_USE_CONFIG: 1
@ -304,7 +305,7 @@ steps:
- name: integration-test-mysql
image: vikunja/golang-build:latest
pull: true
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
VIKUNJA_TESTS_USE_CONFIG: 1
@ -321,7 +322,7 @@ steps:
- name: integration-test-postgres
image: vikunja/golang-build:latest
pull: true
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
VIKUNJA_TESTS_USE_CONFIG: 1
@ -343,14 +344,15 @@ steps:
########
kind: pipeline
type: docker
name: release
depends_on:
- testing
workspace:
base: /go
path: src/code.vikunja.io/api
base: /source
path: /
trigger:
ref:
@ -368,7 +370,7 @@ steps:
# compiling the same magefile at the same time. It's also faster if each step does not need to compile it first.
- name: mage
image: vikunja/golang-build:latest
pull: true
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
commands:
@ -378,7 +380,7 @@ steps:
- name: before-static-build
image: techknowlogick/xgo:latest
pull: true
pull: always
commands:
- export PATH=$PATH:$GOPATH/bin
- go install github.com/magefile/mage
@ -387,7 +389,7 @@ steps:
- name: static-build-windows
image: techknowlogick/xgo:latest
pull: true
pull: always
environment:
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
# Leaving this here until we know how to resolve this properly.
@ -400,7 +402,7 @@ steps:
- name: static-build-linux
image: techknowlogick/xgo:latest
pull: true
pull: always
environment:
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
# Leaving this here until we know how to resolve this properly.
@ -413,7 +415,7 @@ steps:
- name: static-build-darwin
image: techknowlogick/xgo:latest
pull: true
pull: always
environment:
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
# Leaving this here until we know how to resolve this properly.
@ -426,7 +428,7 @@ steps:
- name: after-build-compress
image: kolaente/upx
pull: true
pull: always
depends_on:
- static-build-windows
- static-build-linux
@ -436,7 +438,7 @@ steps:
- name: after-build-static
image: techknowlogick/xgo:latest
pull: true
pull: always
depends_on:
- after-build-compress
commands:
@ -448,7 +450,7 @@ steps:
- name: sign-release
image: plugins/gpgsign:1
pull: true
pull: always
depends_on: [ after-build-static ]
settings:
key:
@ -462,7 +464,7 @@ steps:
# Push the releases to our pseudo-s3-bucket
- name: release-latest
image: plugins/s3
pull: true
pull: always
settings:
bucket: vikunja-releases
access_key:
@ -484,7 +486,7 @@ steps:
- name: release-version
image: plugins/s3
pull: true
pull: always
settings:
bucket: vikunja-releases
access_key:
@ -504,8 +506,8 @@ steps:
# Build os packages and push it to our bucket
- name: build-os-packages-unstable
image: goreleaser/nfpm
pull: true
image: goreleaser/nfpm:v2.27.1
pull: always
commands:
- apk add git go
- ./mage-static release:packages
@ -517,11 +519,11 @@ steps:
- main
event:
- push
depends_on: [ static-build-linux ]
depends_on: [ after-build-compress ]
- name: build-os-packages-version
image: goreleaser/nfpm
pull: true
image: goreleaser/nfpm:v2.27.1
pull: always
commands:
- apk add git go
- ./mage-static release:packages
@ -531,12 +533,12 @@ steps:
when:
event:
- tag
depends_on: [ static-build-linux ]
depends_on: [ after-build-compress ]
# Push the os releases to our pseudo-s3-bucket
- name: release-os-latest
image: plugins/s3
pull: true
pull: always
settings:
bucket: vikunja-releases
access_key:
@ -558,7 +560,7 @@ steps:
- name: release-os-version
image: plugins/s3
pull: true
pull: always
settings:
bucket: vikunja-releases
access_key:
@ -578,6 +580,7 @@ steps:
---
kind: pipeline
type: docker
name: deploy-docs
workspace:
@ -596,8 +599,7 @@ trigger:
steps:
- name: theme
image: kolaente/toolbox
pull: true
group: build-static
pull: always
commands:
- mkdir docs/themes/vikunja -p
- cd docs/themes/vikunja
@ -605,8 +607,8 @@ steps:
- tar -xzf vikunja-theme.tar.gz
- name: build
image: klakegg/hugo:0.104.2
pull: true
image: klakegg/hugo:0.107.0
pull: always
commands:
- cd docs
- hugo
@ -614,7 +616,7 @@ steps:
- name: docker
image: plugins/docker
pull: true
pull: always
settings:
username:
from_secret: docker_username
@ -627,102 +629,11 @@ steps:
---
kind: pipeline
type: docker
name: docker-arm-release
name: docker-release
depends_on:
- testing
platform:
os: linux
arch: arm64
trigger:
ref:
- refs/heads/main
- "refs/tags/**"
steps:
- name: fetch-tags
image: docker:git
commands:
- git fetch --tags
- name: docker-arm-unstable
image: plugins/docker:linux-arm
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/api
tags: unstable-linux-arm
dockerfile: Dockerfile.arm32
depends_on: [ fetch-tags ]
when:
ref:
- refs/heads/main
- name: docker-arm
image: plugins/docker:linux-arm
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/api
auto_tag: true
auto_tag_suffix: linux-arm
dockerfile: Dockerfile.arm32
depends_on: [ fetch-tags ]
when:
ref:
- "refs/tags/**"
- name: docker-arm64-unstable
image: plugins/docker:linux-arm64
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/api
tags: unstable-linux-arm64
depends_on: [ fetch-tags ]
when:
ref:
- refs/heads/main
- name: docker-arm64
image: plugins/docker:linux-arm64
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/api
auto_tag: true
auto_tag_suffix: linux-arm64
depends_on: [ fetch-tags ]
when:
ref:
- "refs/tags/**"
---
kind: pipeline
type: docker
name: docker-amd64-release
depends_on:
- testing
platform:
os: linux
arch: amd64
trigger:
ref:
- refs/heads/main
@ -735,94 +646,55 @@ steps:
- git fetch --tags
- name: docker-unstable
image: plugins/docker:linux-amd64
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/api
tags: unstable-linux-amd64
depends_on: [ fetch-tags ]
when:
ref:
- refs/heads/main
- name: docker
image: plugins/docker:linux-amd64
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/api
auto_tag: true
auto_tag_suffix: linux-amd64
depends_on: [ fetch-tags ]
when:
ref:
- "refs/tags/**"
---
kind: pipeline
type: docker
name: docker-manifest
trigger:
ref:
- refs/heads/main
- "refs/tags/**"
depends_on:
- docker-amd64-release
- docker-arm-release
steps:
- name: manifest-unstable
image: thegeeklab/drone-docker-buildx
privileged: true
pull: always
image: plugins/manifest
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/api
tags: unstable
ignore_missing: true
spec: docker-manifest-unstable.tmpl
password:
from_secret: docker_password
username:
from_secret: docker_username
platforms:
- linux/386
- linux/amd64
- linux/arm/v6
- linux/arm/v7
- linux/arm64/v8
depends_on: [ fetch-tags ]
when:
ref:
- refs/heads/main
- name: manifest-release
pull: always
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
spec: docker-manifest.tmpl
password:
from_secret: docker_password
username:
from_secret: docker_username
- name: generate-tags
image: thegeeklab/docker-autotag
environment:
DOCKER_AUTOTAG_VERSION: ${DRONE_TAG}
DOCKER_AUTOTAG_EXTRA_TAGS: latest
DOCKER_AUTOTAG_OUTPUT_FILE: .tags
depends_on: [ fetch-tags ]
when:
ref:
- "refs/tags/**"
- name: manifest-release-latest
- name: docker-release
image: thegeeklab/drone-docker-buildx
privileged: true
pull: always
image: plugins/manifest
depends_on:
- clone
settings:
tags: latest
ignore_missing: true
spec: docker-manifest.tmpl
password:
from_secret: docker_password
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/api
platforms:
- linux/386
- linux/amd64
- linux/arm/v6
- linux/arm/v7
- linux/arm64/v8
depends_on: [ generate-tags ]
when:
ref:
- "refs/tags/**"
@ -841,9 +713,7 @@ depends_on:
- testing
- release
- deploy-docs
- docker-arm-release
- docker-amd64-release
- docker-manifest
- docker-release
steps:
- name: notify
@ -861,6 +731,6 @@ steps:
- failure
---
kind: signature
hmac: 5500acb776acae4975592637767035c45af277f1f56c8d10ffd99f38761cd5c8
hmac: f1743e2bb4ca8e5d8df1e222720feb4eac91e15a5ef74d4b54f1df246f2353fe
...

View File

@ -1,58 +0,0 @@
name: Bug Report
description: Found something you weren't expecting? Report it here!
labels: kind/bug
body:
- type: markdown
attributes:
value: |
NOTE: If your issue is a security concern, please send an email to security@vikunja.io instead of opening a public issue.
- type: markdown
attributes:
value: |
Please fill out this issue template to report a bug.
1. If you want to propose a new feature, please open a discussion thread in the forum: https://community.vikunja.io
2. Please ask questions or configuration/deploy problems on our [Matrix Room](https://matrix.to/#/#vikunja:matrix.org) or forum (https://community.vikunja.io).
3. Make sure you are using the latest release and
take a moment to check that your issue hasn't been reported before.
4. Please give all relevant information below for bug reports, because
incomplete details will be handled as an invalid report and closed.
- type: textarea
id: description
attributes:
label: Description
description: |
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below).
- type: input
id: frontend-version
attributes:
label: Vikunja Frontend Version
description: Vikunja frontend version (or commit reference) of your instance
validations:
required: true
- type: input
id: api-version
attributes:
label: Vikunja API Version
description: Vikunja API version (or commit reference) of your instance
validations:
required: true
- type: input
id: browser-version
attributes:
label: Browser and version
description: If your issue is related to a frontend problem, please provide the browser and version you used to reproduce it.
- type: dropdown
id: can-reproduce
attributes:
label: Can you reproduce the bug on the Vikunja demo site?
options:
- "Yes"
- "No"
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If this issue involves the Web Interface, please provide one or more screenshots

3
.github/FUNDING.yml vendored
View File

@ -1,2 +1,3 @@
github: kolaente
custom: https://www.buymeacoffee.com/kolaente
open_collective: vikunja
custom: ["https://vikunja.cloud", "https://www.buymeacoffee.com/kolaente"]

View File

@ -79,6 +79,7 @@ issues:
- path: pkg/routes/api/v1/docs.go
linters:
- goheader
- misspell
- text: "Missed string"
linters:
- goheader
@ -88,3 +89,6 @@ issues:
- path: pkg/models/favorites\.go
linters:
- nilerr
- path: pkg/models/events\.go
linters:
- musttag

View File

@ -7,6 +7,307 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
All releases can be found on https://code.vikunja.io/api/releases.
## [0.20.4] - 2023-03-12
### Bug Fixes
* *(docker)* Allow non-unique group id
### Documentation
* Add link to tutorial for installing Vikunja on Synology ([4de0efe](4de0efec1dd7da95dbf936728d7e23791396a63a))
## [0.20.3] - 2023-03-10
### Bug Fixes
* *(build)* Downgrade xgo to 1.19.2 so that builds work again
* *(caldav)* Add Z suffix to dates make it clear dates are in UTC
* *(caldav)* Use const for repeat modes
* *(caldav)* Make sure only labels where the user has permission to use them are used
* *(ci)* Pipeline dependency
* *(ci)* Pin nfpm container version and binary location
* *(ci)* Set release path to /source
* *(ci)* Tagging logic for release docker images
* *(ci)* Save generated .tags file to correctly tag docker releases
* *(ci)* Sign drone config
* *(docd)* Update Subdirectory Documentation (#1363)
* *(docker)* Cross compilation with buildx
* *(docker)* Re-add expose
* *(docker)* Passing environment variables into the container
* *(docker)* Make sure the vikunja user always exists and only modify the uid instead of recreating the user
* *(docs)* Add docs about cli user delete
* *(docs)* Old helm charts url (#1344)
* *(docs)* Fix a few minor typos (#59)
* *(docs)* Fix traefik v2 example (#65)
* *(docs)* Clarify support for caldav reccurrence
* *(drone)* Add type, fix pull, remove group (#1355)
* *(dump)* Make sure null dates are properly set when restoring from a dump
* *(export)* Ignore file size for export files
* *(list)* Return lists for a namespace id even if that namespace is deleted
* *(list)* When list background is removed, delete file from file system and DB (#1372)
* *(mailer)* Forcessl config (#60)
* *(migration)* Use Todoist v9 api to migrate tasks from them
* *(migration)* Import TickTick data by column name instead of index (#1356)
* *(migration)* Use the proper authorization method for Todoist's api, fix issues with importing deleted items
* *(migration)* Remove unused todoist parameters
* *(migration)* Todoist pagination now avoids too many loops
* *(migration)* Don't try to add nonexistent tasks as related
* *(migration)* Make sure trello checklists are properly imported
* *(reminders)* Overdue tasks join condition
* *(reminders)* Make sure an overdue reminder is sent when there is only one overdue task
* *(reminders)* Prevent duplicate reminders when updating task details
* *(restore)* Check if we're really dealing with a string
* *(task)* Make sure the task's last updated timestamp is always updated when releated entities changed
* *(task)* Correctly load tasks by id and uuid in caldav
* *(tasks)* Don't include undone overdue tasks from archived lists or namespaces in notification mails
* *(tasks)* Don't reset the kanban bucket when updating a task and not providing one
* *(tasks)* Don't set a repeating task done when moving it do the done bucket
* *(tasks)* Recalculate position of all tasks in a list or bucket when it would hit 0
* *(tasks)* Make sure tasks are sorted by position before recalculating them
* *(user)* Make reset the user's name to empty actually work
* Swagger docs ([96b5e93](96b5e933796275e87f3007e31db0623688dbdb3a))
* Restore notifications table from dump when it already had the correct format ([8c67be5](8c67be558f697ab52740c51ab453092c0f8f7c14))
* Make sure labels are always exported as caldav (#1412) ([1afc72e](1afc72e1906c02b093bb6d9748235b93ab0eb181))
* Lint ([491a142](491a1423788b76f236d070071cb46f5b2f5d3fd0))
* Lint ([20a5994](20a5994b1717e7751750f14a9a164825a8e6ade6))
* Lint ([077baba](077baba2eaff2f10b97384f07375ece7f51ec0fa))
* Lint ([9f14466](9f14466dfa8660362a4e51b3c8c6810bf8d66a22))
### Dependencies
* *(deps)* Update module github.com/yuin/goldmark to v1.5.3 (#1317)
* *(deps)* Update module golang.org/x/crypto to v0.2.0 (#1315)
* *(deps)* Update module github.com/spf13/afero to v1.9.3 (#1320)
* *(deps)* Update module golang.org/x/crypto to v0.3.0 (#1321)
* *(deps)* Update github.com/arran4/golang-ical digest to a677353 (#1323)
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.5 (#1325)
* *(deps)* Update github.com/arran4/golang-ical digest to 1093469 (#1326)
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.4.3 (#1328)
* *(deps)* Update module github.com/go-sql-driver/mysql to v1.7.0 (#1332)
* *(deps)* Update module golang.org/x/sys to v0.3.0 (#1333)
* *(deps)* Update module golang.org/x/term to v0.3.0 (#1336)
* *(deps)* Update module golang.org/x/image to v0.2.0 (#1335)
* *(deps)* Update module golang.org/x/oauth2 to v0.2.0 (#1316)
* *(deps)* Update module golang.org/x/oauth2 to v0.3.0 (#1337)
* *(deps)* Update module github.com/getsentry/sentry-go to v0.16.0 (#1338)
* *(deps)* Update module golang.org/x/crypto to v0.4.0 (#1339)
* *(deps)* Update module github.com/pquerna/otp to v1.4.0 (#1341)
* *(deps)* Update module github.com/swaggo/swag to v1.8.9 (#1327)
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.6 (#1342)
* *(deps)* Update module github.com/labstack/echo/v4 to v4.10.0 (#1343)
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.7 (#1348)
* *(deps)* Update module github.com/coreos/go-oidc/v3 to v3.5.0 (#1349)
* *(deps)* Update module golang.org/x/sys to v0.4.0 (#1351)
* *(deps)* Update module golang.org/x/image to v0.3.0 (#1350)
* *(deps)* Update module golang.org/x/term to v0.4.0 (#1352)
* *(deps)* Update module golang.org/x/crypto to v0.5.0 (#1353)
* *(deps)* Update goreleaser/nfpm docker tag to v2.23.0 (#1347)
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.8 (#1357)
* *(deps)* Update module src.techknowlogick.com/xgo to v1.6.0+1.19.5 (#1358)
* *(deps)* Update klakegg/hugo docker tag to v0.107.0 (#1272)
* *(deps)* Update module github.com/getsentry/sentry-go to v0.17.0 (#1361)
* *(deps)* Update module src.techknowlogick.com/xgo to v1.7.0+1.19.5 (#1364)
* *(deps)* Update module github.com/spf13/viper to v1.15.0 (#1365)
* *(deps)* Update module github.com/labstack/echo-jwt/v4 to v4.0.1 (#1369)
* *(deps)* Update module golang.org/x/oauth2 to v0.4.0 (#1354)
* *(deps)* Update github.com/gocarina/gocsv digest to 763e25b (#1370)
* *(deps)* Update goreleaser/nfpm docker tag to v2.24.0 (#1367)
* *(deps)* Update module github.com/swaggo/swag to v1.8.10 (#1371)
* *(deps)* Update module github.com/go-redis/redis/v8 to v9 (#1377)
* *(deps)* Update module github.com/labstack/echo-jwt/v4 to v4.1.0
* *(deps)* Update module github.com/ulule/limiter/v3 to v3.11.0 (#1378)
* *(deps)* Update module github.com/redis/go-redis/v9 to v9.0.2
* *(deps)* Update goreleaser/nfpm docker tag to v2.25.0 (#1382)
* *(deps)* Upgrade golangci-lint to 1.51.0
* *(deps)* Update module github.com/yuin/goldmark to v1.5.4
* *(deps)* Update module go to 1.20
* *(deps)* Update xgo to 1.20
* *(deps)* Update module golang.org/x/sys to v0.5.0
* *(deps)* Update module github.com/getsentry/sentry-go to v0.18.0 (#1386)
* *(deps)* Update module golang.org/x/term to v0.5.0
* *(deps)* Update module golang.org/x/crypto to v0.6.0
* *(deps)* Update module golang.org/x/oauth2 to v0.5.0
* *(deps)* Update module golang.org/x/image to v0.4.0
* *(deps)* Update goreleaser/nfpm docker tag to v2.26.0 (#1394)
* *(deps)* Update github.com/arran4/golang-ical digest to 07c6aad
* *(deps)* Update module github.com/threedotslabs/watermill to v1.2.0 (#1384)
* *(deps)* Update module golang.org/x/image to v0.5.0 (#1396)
* *(deps)* Update golang.org/x/net to 0.7.0
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.5.0 (#1399)
* *(deps)* Update github.com/gocarina/gocsv digest to bcce7dc
* *(deps)* Update golangci-lint to 1.51.2
* *(deps)* Update module github.com/labstack/echo/v4 to v4.10.1
* *(deps)* Update github.com/gocarina/gocsv digest to bee85ea
* *(deps)* Update module github.com/labstack/echo/v4 to v4.10.2
* *(deps)* Update module github.com/spf13/afero to v1.9.4
* *(deps)* Update github.com/gocarina/gocsv digest to dc4ee9d
* *(deps)* Update module github.com/stretchr/testify to v1.8.2
* *(deps)* Update github.com/gocarina/gocsv digest to 70c27cb
* *(deps)* Update module golang.org/x/sys to v0.6.0
* *(deps)* Update module golang.org/x/term to v0.6.0
* *(deps)* Update module golang.org/x/crypto to v0.7.0
* *(deps)* Update module golang.org/x/oauth2 to v0.6.0
* *(deps)* Update module golang.org/x/image to v0.6.0
* *(deps)* Update github.com/kolaente/caldav-go digest to 2a4eb8b
* *(deps)* Remove fsnotify replacement
* *(deps)* Update github.com/vectordotdev/go-datemath digest to f3954d0
* *(deps)* Update src.techknowlogick.com/xgo digest to 44f7e66
* *(deps)* Update module github.com/getsentry/sentry-go to v0.19.0
* *(deps)* Update module github.com/spf13/afero to v1.9.5
* *(deps)* Update module github.com/ulule/limiter/v3 to v3.11.1
* *(deps)* Update src.techknowlogick.com/xgo digest to b607086
* *(deps)* Update module github.com/gabriel-vasile/mimetype to v1.4.2
### Features
* *(background)* Add Last-Modified header (#1376)
* *(caldav)* Add support for repeating tasks
* *(caldav)* Export Labels to Caldav (#1409)
* *(caldav)* Import caldav categories as Labels (#1413)
* *(migrators)* Remove wunderlist (#1346)
* *(release)* Use compressed binaries for package releases
* Use docker buildx to build multiarch images ([a6e214b](a6e214b654f28836cc8b93683dbfd5999282d11c))
* Provide logout url for openid providers (#1340) ([a79b1de](a79b1de2d0247a424f49cecaa267d30e8fa70a83))
* Refactored Dockerfile (#1375) ([522bf7d](522bf7d2fc3cc1704f58299b6435baccc7add533))
* Disable events log by default ([da9d25c](da9d25cf727c56acd7394b4b74e17a2959ee5242))
- **BREAKING**: events log level is now off unless explicitly enabled
### Miscellaneous Tasks
* *(docs)* Adjust docs about frontend docker container
* *(docs)* Remove sponsors
* *(task)* Add test to check if a task's reminders are duplicated
* Remove custom gitea bug template in favor of githubs ([4fa45bf](4fa45bf9dcbaa8a41a53fc2305c4c2c1aa15691c))
* 0.20.2 release preperations ([d19fc80](d19fc80b8be08673136d84e10187cadb293822bf))
* Update funding links ([aa25ccd](aa25ccdc917684583a9bff4b7cb272004386f0fa))
### Other
* *(other)* Added Google & Google Workspace to OpenId examples (#1319)
## [0.20.2] - 2023-01-24
### Bug Fixes
* *(build)* Downgrade xgo to 1.19.2 so that builds work again
* *(caldav)* Add Z suffix to dates make it clear dates are in UTC
* *(caldav)* Use const for repeat modes
* *(ci)* Pipeline dependency
* *(ci)* Pin nfpm container version and binary location
* *(ci)* Set release path to /source
* *(ci)* Tagging logic for release docker images
* *(docs)* Add docs about cli user delete
* *(docs)* Old helm charts url (#1344)
* *(docs)* Fix a few minor typos (#59)
* *(drone)* Add type, fix pull, remove group (#1355)
* *(dump)* Make sure null dates are properly set when restoring from a dump
* *(export)* Ignore file size for export files
* *(list)* Return lists for a namespace id even if that namespace is deleted
* *(mailer)* Forcessl config (#60)
* *(migration)* Use Todoist v9 api to migrate tasks from them
* *(migration)* Import TickTick data by column name instead of index (#1356)
* *(migration)* Use the proper authorization method for Todoist's api, fix issues with importing deleted items
* *(reminders)* Overdue tasks join condition
* *(reminders)* Make sure an overdue reminder is sent when there is only one overdue task
* *(reminders)* Prevent duplicate reminders when updating task details
* *(restore)* Check if we're really dealing with a string
* *(tasks)* Don't include undone overdue tasks from archived lists or namespaces in notification mails
* *(tasks)* Don't reset the kanban bucket when updating a task and not providing one
* *(tasks)* Don't set a repeating task done when moving it do the done bucket
* *(user)* Make reset the user's name to empty actually work* Swagger docs ([41c9e3f](41c9e3f9a47280887b56941280904aea6ef31f85))
* Restore notifications table from dump when it already had the correct format ([15811fd](15811fd4d4485cd25cf8d2f8fdd04ebfea8e6663))
### Dependencies
* *(deps)* Update module github.com/yuin/goldmark to v1.5.3 (#1317)
* *(deps)* Update module golang.org/x/crypto to v0.2.0 (#1315)
* *(deps)* Update module github.com/spf13/afero to v1.9.3 (#1320)
* *(deps)* Update module golang.org/x/crypto to v0.3.0 (#1321)
* *(deps)* Update github.com/arran4/golang-ical digest to a677353 (#1323)
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.5 (#1325)
* *(deps)* Update github.com/arran4/golang-ical digest to 1093469 (#1326)
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.4.3 (#1328)
* *(deps)* Update module github.com/go-sql-driver/mysql to v1.7.0 (#1332)
* *(deps)* Update module golang.org/x/sys to v0.3.0 (#1333)
* *(deps)* Update module golang.org/x/term to v0.3.0 (#1336)
* *(deps)* Update module golang.org/x/image to v0.2.0 (#1335)
* *(deps)* Update module golang.org/x/oauth2 to v0.2.0 (#1316)
* *(deps)* Update module golang.org/x/oauth2 to v0.3.0 (#1337)
* *(deps)* Update module github.com/getsentry/sentry-go to v0.16.0 (#1338)
* *(deps)* Update module golang.org/x/crypto to v0.4.0 (#1339)
* *(deps)* Update module github.com/pquerna/otp to v1.4.0 (#1341)
* *(deps)* Update module github.com/swaggo/swag to v1.8.9 (#1327)
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.6 (#1342)
* *(deps)* Update module github.com/labstack/echo/v4 to v4.10.0 (#1343)
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.7 (#1348)
* *(deps)* Update module github.com/coreos/go-oidc/v3 to v3.5.0 (#1349)
* *(deps)* Update module golang.org/x/sys to v0.4.0 (#1351)
* *(deps)* Update module golang.org/x/image to v0.3.0 (#1350)
* *(deps)* Update module golang.org/x/term to v0.4.0 (#1352)
* *(deps)* Update module golang.org/x/crypto to v0.5.0 (#1353)
* *(deps)* Update goreleaser/nfpm docker tag to v2.23.0 (#1347)
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.8 (#1357)
* *(deps)* Update module src.techknowlogick.com/xgo to v1.6.0+1.19.5 (#1358)
* *(deps)* Update klakegg/hugo docker tag to v0.107.0 (#1272)
* *(deps)* Update module github.com/getsentry/sentry-go to v0.17.0 (#1361)
* *(deps)* Update module src.techknowlogick.com/xgo to v1.7.0+1.19.5 (#1364)
* *(deps)* Update module github.com/spf13/viper to v1.15.0 (#1365)
* *(deps)* Update module github.com/labstack/echo-jwt/v4 to v4.0.1 (#1369)
### Features
* *(migrators)* Remove wunderlist (#1346)
* *(release)* Use compressed binaries for package releases
* Use docker buildx to build multiarch images ([9bd6795](9bd6795266fd54ae42664c20ed7633ac7daf6199))
### Miscellaneous Tasks
* Remove custom gitea bug template in favor of githubs ([7b1e1c7](7b1e1c79e358f3fcecb217259491f016402cdcc7))
### Other
* *(other)* Added Google & Google Workspace to OpenId examples (#1319)
## [0.20.1] - 2022-11-11
### Bug Fixes
* *(docs)* Add explanation on how to run the cli in docker
* *(filter)* Also check for 0 values if the filter should include nulls
* *(filter)* Only check for 0 values in filter fields with numeric values
* *(filters)* Try to parse date filter fields of the provided dates are not valid iso dates
* *(filters)* Try parsing dates without time
* *(filters)* Try parsing invalid dates like 2022-11-1
* *(metrics)* Make currently active users actually work
* *(task)* Duplicate reminders when adding different ones between winter / summer time
* *(tasks)* Allow sorting by task index* Make sure task indexes are calculated correctly when moving tasks between lists ([c495096](c4950964443a9bffc4cdd8fc25004ad951520f20))
* Look for the default bucket based on the position instead of the index ([622f2f0](622f2f0562bd8e3a5c97ec0b001c646a33a86c2b))
* Usage with postgres over unix socket (#1308) ([641a9da](641a9da93d24a18d6cbad2929eea1be6c1e0d0b2))
### Dependencies
* *(deps)* Update module github.com/prometheus/client_golang to v1.13.1 (#1307)
* *(deps)* Update module github.com/spf13/viper to v1.14.0 (#1309)
* *(deps)* Update module golang.org/x/sys to v0.2.0 (#1311)
* *(deps)* Update module golang.org/x/term to v0.2.0 (#1312)
* *(deps)* Update module github.com/prometheus/client_golang to v1.14.0 (#1313)
* *(deps)* Update module github.com/getsentry/sentry-go to v0.15.0 (#1314)
### Features
* *(docs)* Add relase checklist
### Other
* *(other)* Nessecary is a common misspelling of necessary (#1304)
## [0.20.0] - 2022-10-28
### Bug Fixes

View File

@ -1,49 +1,42 @@
# syntax=docker/dockerfile:1
# ┬─┐┬ ┐o┬ ┬─┐
# │─││ │││ │ │
# ┘─┘┘─┘┘┘─┘┘─┘
##############
# Build stage
FROM golang:1.19-alpine AS build-env
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:go-1.20.0 AS builder
RUN apk --no-cache add build-base git && \
go install github.com/magefile/mage@latest && \
mv /go/bin/mage /usr/local/go/bin
RUN go install github.com/magefile/mage@latest && \
mv /go/bin/mage /usr/local/go/bin
ARG VIKUNJA_VERSION
# Setup repo
COPY . /go/src/code.vikunja.io/api
WORKDIR /go/src/code.vikunja.io/api
COPY . ./
# Checkout version if set
RUN if [ -n "${VIKUNJA_VERSION}" ]; then git checkout "${VIKUNJA_VERSION}"; fi \
&& mage build:clean build
ARG TARGETOS TARGETARCH TARGETVARIANT
RUN mage build:clean && \
mage release:xgo "${TARGETOS}/${TARGETARCH}/${TARGETVARIANT}"
# ┬─┐┬ ┐┌┐┐┌┐┐┬─┐┬─┐
# │┬┘│ │││││││├─ │┬┘
# ┘└┘┘─┘┘└┘┘└┘┴─┘┘└┘
###################
# The actual image
# Note: I wanted to use the scratch image here, but unfortunatly the go-sqlite bindings require cgo and
# because of this, the container would not start when I compiled the image without cgo.
FROM alpine:3.16
FROM alpine:3.16 AS runner
LABEL maintainer="maintainers@vikunja.io"
WORKDIR /app/vikunja
ENTRYPOINT [ "/sbin/tini", "-g", "--", "/entrypoint.sh" ]
EXPOSE 3456
WORKDIR /app/vikunja/
COPY --from=build-env /go/src/code.vikunja.io/api/vikunja .
ENV VIKUNJA_SERVICE_ROOTPATH=/app/vikunja/
# Dynamic permission changing stuff
ENV PUID 1000
ENV PGID 1000
RUN apk --no-cache add shadow && \
addgroup -g ${PGID} vikunja && \
adduser -s /bin/sh -D -G vikunja -u ${PUID} vikunja -h /app/vikunja -H && \
chown vikunja -R /app/vikunja
COPY run.sh /run.sh
# Add time zone data
RUN apk --no-cache add tzdata
RUN apk --update --no-cache add tzdata tini shadow && \
addgroup vikunja && \
adduser -s /bin/sh -D -G vikunja vikunja -h /app/vikunja -H
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod 0755 /entrypoint.sh && mkdir files
# Files permissions
RUN mkdir /app/vikunja/files && \
chown -R vikunja /app/vikunja/files
VOLUME /app/vikunja/files
CMD ["/run.sh"]
EXPOSE 3456
COPY --from=builder /build/vikunja-* vikunja

View File

@ -1,48 +0,0 @@
##############
# Build stage
FROM golang:1.19-buster AS build-env
RUN go install github.com/magefile/mage@latest && \
mv /go/bin/mage /usr/local/go/bin
ARG VIKUNJA_VERSION
# Setup repo
COPY . /go/src/code.vikunja.io/api
WORKDIR /go/src/code.vikunja.io/api
# Checkout version if set
RUN if [ -n "${VIKUNJA_VERSION}" ]; then git checkout "${VIKUNJA_VERSION}"; fi \
&& mage build:clean build
###################
# The actual image
# Note: I wanted to use the scratch image here, but unfortunatly the go-sqlite bindings require cgo and
# because of this, the container would not start when I compiled the image without cgo.
# We're using debian as a base image here because the latest alpine image does not work with arm.
FROM debian:buster-slim
LABEL maintainer="maintainers@vikunja.io"
WORKDIR /app/vikunja/
COPY --from=build-env /go/src/code.vikunja.io/api/vikunja .
ENV VIKUNJA_SERVICE_ROOTPATH=/app/vikunja/
# Dynamic permission changing stuff
ENV PUID 1000
ENV PGID 1000
RUN addgroup --gid ${PGID} vikunja && \
chown ${PUID} -R /app/vikunja && \
useradd --shell /bin/sh --gid vikunja --uid ${PUID} --home-dir /app/vikunja vikunja
COPY run.sh /run.sh
# Fix time zone settings not working
RUN apt-get update && apt-get install -y tzdata && apt-get clean
# Files permissions
RUN mkdir /app/vikunja/files && \
chown -R vikunja /app/vikunja/files
VOLUME /app/vikunja/files
CMD ["/run.sh"]
EXPOSE 3456

View File

@ -2,7 +2,7 @@
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/api/status.svg)](https://drone.kolaente.de/vikunja/api)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.20.0-brightgreen.svg)](https://dl.vikunja.io)
[![Download](https://img.shields.io/badge/download-v0.20.4-brightgreen.svg)](https://dl.vikunja.io)
[![Docker Pulls](https://img.shields.io/docker/pulls/vikunja/api.svg)](https://hub.docker.com/r/vikunja/api/)
[![Swagger Docs](https://img.shields.io/badge/swagger-docs-brightgreen.svg)](https://try.vikunja.io/api/v1/docs)
[![Go Report Card](https://goreportcard.com/badge/kolaente.dev/vikunja/api)](https://goreportcard.com/report/kolaente.dev/vikunja/api)
@ -56,10 +56,6 @@ See [the roadmap](https://my.vikunja.cloud/share/QFyzYEmEYfSyQfTOmIRSwLUpkFjboaB
Fork -> Push -> Pull-Request. Also see the [dev docs](https://vikunja.io/docs/development/) for more info.
## Sponsors
[![Relm](https://vikunja.io/images/sponsors/relm.png)](https://relm.us)
## License
This project is licensed under the AGPLv3 License. See the [LICENSE](LICENSE) file for the full license text.

View File

@ -30,7 +30,7 @@ service:
enablecaldav: true
# Set the motd message, available from the /info endpoint
motd: ""
# Enable sharing of lists via a link
# Enable sharing of project via a link
enablelinksharing: true
# Whether to let new users registering themselves or not
enableregistration: true
@ -162,10 +162,10 @@ log:
databaselevel: "WARNING"
# Whether to log http requests or not. Possible values are stdout, stderr, file or off to disable http logging.
http: "stdout"
# Echo has its own logging which usually is unnessecary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
# Echo has its own logging which usually is unnecessary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
echo: "off"
# Whether or not to log events. Useful for debugging. Possible values are stdout, stderr, file or off to disable events logging.
events: "stdout"
events: "off"
# The log level for event log messages. Possible values (case-insensitive) are ERROR, INFO, DEBUG.
eventslevel: "info"
@ -191,21 +191,6 @@ files:
maxsize: 20MB
migration:
# These are the settings for the wunderlist migrator
wunderlist:
# Wheter to enable the wunderlist migrator or not
enable: false
# The client id, required for making requests to the wunderlist api
# You need to register your vikunja instance at https://developer.wunderlist.com/apps/new to get this
clientid:
# The client secret, also required for making requests to the wunderlist api
clientsecret:
# The url where clients are redirected after they authorized Vikunja to access their wunderlist stuff.
# This needs to match the url you entered when registering your Vikunja instance at wunderlist.
# This is usually the frontend url where the frontend then makes a request to /migration/wunderlist/migrate
# with the code obtained from the wunderlist api.
# Note that the vikunja frontend expects this to be /migrate/wunderlist
redirecturl:
todoist:
# Wheter to enable the todoist migrator or not
enable: false
@ -253,14 +238,14 @@ avatar:
gravatarexpiration: 3600
backgrounds:
# Whether to enable backgrounds for lists at all.
# Whether to enable backgrounds for projects at all.
enabled: true
providers:
upload:
# Whethere to enable uploaded list backgrounds
# Whether to enable uploaded project backgrounds
enabled: true
unsplash:
# Whether to enable setting backgrounds from unsplash as list backgrounds
# Whether to enable setting backgrounds from unsplash as project backgrounds
enabled: false
# You need to create an application for your installation at https://unsplash.com/oauth/applications/new
# and set the access token below.
@ -311,6 +296,9 @@ auth:
- name:
# The auth url to send users to if they want to authenticate using OpenID Connect.
authurl:
# The oidc logouturl that users will be redirected to on logout.
# Leave empty or delete key, if you do not want to be redirected.
logouturl:
# The client ID used to authenticate Vikunja at the OpenID Connect provider.
clientid:
# The client secret used to authenticate Vikunja at the OpenID Connect provider.
@ -341,8 +329,8 @@ defaultsettings:
overdue_tasks_reminders_enabled: true
# When to send the overdue task reminder email.
overdue_tasks_reminders_time: 9:00
# The id of the default list. Make sure users actually have access to this list when setting this value.
default_list_id: 0
# The id of the default project. Make sure users actually have access to this project when setting this value.
default_project_id: 0
# Start of the week for the user. `0` is sunday, `1` is monday and so on.
week_start: 0
# The language of the user interface. Must be an ISO 639-1 language code. Will default to the browser language the user uses when signing up.

View File

@ -1,17 +0,0 @@
image: vikunja/api:unstable
manifests:
-
image: vikunja/api:unstable-linux-amd64
platform:
architecture: amd64
os: linux
-
image: vikunja/api:unstable-linux-arm64
platform:
architecture: arm64
os: linux
-
image: vikunja/api:unstable-linux-arm
platform:
architecture: arm
os: linux

View File

@ -1,23 +0,0 @@
image: vikunja/api:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
-
image: vikunja/api:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
-
image: vikunja/api:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
-
image: vikunja/api:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux

15
docker/entrypoint.sh Normal file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env sh
set -e
if [ -n "$PUID" ] && [ "$PUID" -ne 0 ] && \
[ -n "$PGID" ] && [ "$PGID" -ne 0 ] ; then
echo "info: creating the new user vikunja with $PUID:$PGID"
groupmod -g "$PGID" -o vikunja
usermod -u "$PUID" -o vikunja
chown -R vikunja:vikunja ./files/
chown vikunja:vikunja .
exec su vikunja -c /app/vikunja/vikunja "$@"
else
echo "info: creation of non-root user is skipped"
exec /app/vikunja/vikunja "$@"
fi

0
docs/.hugo_build.lock Normal file
View File

View File

@ -28,7 +28,7 @@ markup:
menu:
page:
- name: Home
url: https://vikunja.io/en/
url: https://vikunja.io/
weight: 10
- name: Features
url: https://vikunja.io/features
@ -36,6 +36,9 @@ menu:
- name: Download
url: https://vikunja.io/download
weight: 30
- name: Blog
url: https://vikunja.io/blog/
weight: 35
- name: Docs
url: https://vikunja.io/docs
weight: 40
@ -45,6 +48,9 @@ menu:
- name: Community
url: https://community.vikunja.io/
weight: 60
- name: Stickers
url: https://vikunja.cloud/stickers?utm_source=io&utm_medium=io&utm_campaign=menu
weight: 65
- name: Get it Hosted
url: https://vikunja.cloud/?utm_source=io&utm_medium=io&utm_campaign=menu
weight: 70

View File

@ -49,7 +49,7 @@ func (err ErrUserDoesNotExist) Error() string {
// This needs to be unique, so you should check whether the error code exists or not.
// The general convention for error codes is as follows:
// * Every "group" errors lives in a thousend something. For example all user issues are 1000-something, all
// list errors are 3000-something and so on.
// project errors are 3000-something and so on.
// * New error codes should be the current max error code + 1. Don't take free numbers to prevent old errors
// which are depricated and removed from being "new ones". For example, if there are error codes 1001, 1002, 1004,
// a new error should be 1005 and not 1003.

View File

@ -26,8 +26,8 @@ It returns the `limit` (max-length) and `offset` parameters needed for SQL-Queri
You can feed this function directly into xorm's `Limit`-Function like so:
{{< highlight golang >}}
lists := []List{}
err := x.Limit(getLimitFromPageIndex(pageIndex, itemsPerPage)).Find(&lists)
projects := []*Project{}
err := x.Limit(getLimitFromPageIndex(pageIndex, itemsPerPage)).Find(&projects)
{{< /highlight >}}
// TODO: Add a full example from start to finish, like a tutorial on how to create a new endpoint?

View File

@ -20,8 +20,8 @@ There are two ways of migrating data from another service:
1. Through the auth-based flow where the user gives you access to their data at the third-party service through an
oauth flow. You can then call the service's api on behalf of your user to get all the data.
The Todoist, Trello and Microsoft To-Do Migrators use this pattern.
2. A file migration where the user uploads a file obtained from some third-party service. In your migrator, you need
to parse the file and create the lists, tasks etc.
2. A file migration where the user uploads a file obtained from some third-party service. In your migrator, you need
to parse the file and create the projects, tasks etc.
The Vikunja File Import uses this pattern.
To differentiate the two, there are two different interfaces you must implement.
@ -61,7 +61,7 @@ type FileMigrator interface {
// Name holds the name of the migration.
// This is used to show the name to users and to keep track of users who already migrated.
Name() string
// Migrate is the interface used to migrate a user's tasks, list and other things from a file to vikunja.
// Migrate is the interface used to migrate a user's tasks, projects and other things from a file to vikunja.
// The user object is the user who's tasks will be migrated.
Migrate(user *user.User, file io.ReaderAt, size int64) error
}
@ -103,9 +103,9 @@ 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 lists inside of that namespace, then tasks in the lists and so on.
This means you start by adding a namespace, then add projects inside that namespace, then tasks in the lists and so on.
The root structure must be present as `[]*models.NamespaceWithListsAndTasks`. It allows to represent all of Vikunja's
The root structure must be present as `[]*models.NamespaceWithProjectsAndTasks`. It allows to represent all of Vikunja's
hierachie as a single data structure.
Then call the method like so:

View File

@ -0,0 +1,39 @@
---
title: "Releasing a new Vikunja version"
date: 2022-10-28T13:06:05+02:00
draft: false
menu:
sidebar:
parent: "development"
---
# Releasing a new Vikunja version
This checklist is a collection of all steps usually involved when releasing a new version of Vikunja.
Not all steps are necessary for every release.
* Website update :
* New Features: If there are new features worth mentioning the feature page should be updated.
* New Screenshots: If an overhaul of an existing feature happend so that it now looks different from the existing screenshot, a new one is required.
* Generate changelogs: (with git-cliff)
* Frontend
* API
* Desktop
* Tag a new version: Include the changelog for that version as the tag message
* Frontend
* API
* Desktop
* Once built: Prune the cloudflare cache so that the new versions show up at dl.vikunja.io
* Release Highlights Blogpost:
* Include a section about Vikunja in general (totally fine to copy one from the earlier blog posts)
* New Features & Improvements: Mention bigger features, potentially with screenshots. Things like refactoring are sometimes also worth mentioneing.
* Publish:
* Reddit
* Twitter
* Mastodon
* Chat
* Newsletter
* Forum
* If features in the release were sponsored, send an email to relevant stakeholders
* Update Vikunja Cloud version and other instances

View File

@ -108,7 +108,7 @@ Contains all possible avatar providers a user can choose to set their avatar.
#### background
All list background providers are in sub-packages of this package.
All project background providers are in sub-packages of this package.
#### dump

View File

@ -19,29 +19,51 @@ The api documentation is generated using [swaggo](https://github.com/swaggo/swag
You should always comment every field which will be exposed as a json in the api.
These comments will show up in the documentation, it'll make it easier for developers using the api.
As an example, this is the definition of a list with all comments:
As an example, this is the definition of a project with all comments:
{{< highlight golang >}}
// List represents a list of tasks
type List struct {
// The unique, numeric id of this list.
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"list"`
// The title of the list. You'll see this in the namespace overview.
Title string `xorm:"varchar(250)" json:"title" valid:"required,runelength(3|250)" minLength:"3" maxLength:"250"`
// The description of the list.
Description string `xorm:"varchar(1000)" json:"description" valid:"runelength(0|1000)" maxLength:"1000"`
OwnerID int64 `xorm:"bigint INDEX" json:"-"`
NamespaceID int64 `xorm:"bigint INDEX" json:"-" param:"namespace"`
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.
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
// The description of the project.
Description string `xorm:"longtext null" json:"description"`
// The unique project short identifier. Used to build task identifiers.
Identifier string `xorm:"varchar(10) null" json:"identifier" valid:"runelength(0|10)" minLength:"0" maxLength:"10"`
// The hex color of this project
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
// The user who created this list.
Owner User `xorm:"-" json:"owner" valid:"-"`
// An array of tasks which belong to the list.
Tasks []*ListTask `xorm:"-" json:"tasks"`
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
NamespaceID int64 `xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace"`
// A unix timestamp when this list was created. You cannot change this value.
Created int64 `xorm:"created" json:"created"`
// A unix timestamp when this list was last updated. You cannot change this value.
Updated int64 `xorm:"updated" json:"updated"`
// The user who created this project.
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
// Whether or not a project is archived.
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
// The id of the file this project has set as background
BackgroundFileID int64 `xorm:"null" json:"-"`
// Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /projects/{projectID}/background
BackgroundInformation interface{} `xorm:"-" json:"background_information"`
// Contains a very small version of the project background to use as a blurry preview until the actual background is loaded. Check out https://blurha.sh/ to learn how it works.
BackgroundBlurHash string `xorm:"varchar(50) null" json:"background_blur_hash"`
// True if a project is a favorite. Favorite projects show up in a separate namespace. This value depends on the user making the call to the api.
IsFavorite bool `xorm:"-" json:"is_favorite"`
// The subscription status for the user reading this project. You can only read this property, use the subscription endpoints to modify it.
// Will only returned when retreiving one project.
Subscription *Subscription `xorm:"-" json:"subscription,omitempty"`
// The position this project has when querying all projects. See the tasks.position property on how to use this.
Position float64 `xorm:"double null" json:"position"`
// A timestamp when this project was created. You cannot change this value.
Created time.Time `xorm:"created not null" json:"created"`
// A timestamp when this project was last updated. You cannot change this value.
Updated time.Time `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`

View File

@ -208,7 +208,7 @@ Environment path: `VIKUNJA_SERVICE_MOTD`
### enablelinksharing
Enable sharing of lists via a link
Enable sharing of project via a link
Default: `true`
@ -840,7 +840,7 @@ Environment path: `VIKUNJA_LOG_HTTP`
### echo
Echo has its own logging which usually is unnessecary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
Echo has its own logging which usually is unnecessary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
Default: `off`
@ -853,7 +853,7 @@ Environment path: `VIKUNJA_LOG_ECHO`
Whether or not to log events. Useful for debugging. Possible values are stdout, stderr, file or off to disable events logging.
Default: `stdout`
Default: `off`
Full path: `log.events`
@ -969,17 +969,6 @@ Environment path: `VIKUNJA_FILES_MAXSIZE`
### wunderlist
These are the settings for the wunderlist migrator
Default: `<empty>`
Full path: `migration.wunderlist`
Environment path: `VIKUNJA_MIGRATION_WUNDERLIST`
### todoist
Default: `<empty>`
@ -1032,7 +1021,7 @@ Environment path: `VIKUNJA_AVATAR_GRAVATAREXPIRATION`
### enabled
Whether to enable backgrounds for lists at all.
Whether to enable backgrounds for projects at all.
Default: `true`
@ -1259,15 +1248,15 @@ Full path: `defaultsettings.overdue_tasks_reminders_time`
Environment path: `VIKUNJA_DEFAULTSETTINGS_OVERDUE_TASKS_REMINDERS_TIME`
### default_list_id
### default_project_id
The id of the default list. Make sure users actually have access to this list when setting this value.
The id of the default project. Make sure users actually have access to this project when setting this value.
Default: `0`
Full path: `defaultsettings.default_list_id`
Full path: `defaultsettings.default_project_id`
Environment path: `VIKUNJA_DEFAULTSETTINGS_DEFAULT_LIST_ID`
Environment path: `VIKUNJA_DEFAULTSETTINGS_DEFAULT_PROJECT_ID`
### week_start

View File

@ -76,7 +76,7 @@ This defines four services, each with their own container:
* An api service which runs the vikunja api. Most of the core logic lives here.
* The frontend which will make vikunja actually usable for most people.
* A database container which will store all lists, tasks, etc. We're using mariadb here, but you're free to use mysql or postgres if you want.
* A database container which will store all projects, tasks, etc. We're using mariadb here, but you're free to use mysql or postgres if you want.
* A proxy service which makes the frontend and api available on the same port, redirecting all requests to `/api` to the api container.
If you already have a proxy on your host, you may want to check out the [reverse proxy examples]() to use that.
By default, it uses port 80 on the host.
@ -201,7 +201,6 @@ You should see something like this:
"max_file_size": "20MB",
"registration_enabled": true,
"available_migrators": [
"wunderlist",
"todoist"
],
"task_attachments_enabled": true

View File

@ -10,7 +10,7 @@ menu:
# Full docker example
This docker compose configuration will run Vikunja with backend and frontend with a mariadb as database.
This docker compose configuration will run Vikunja with backend and frontend with a mariadb database.
It uses an nginx container or traefik on the host to proxy backend and frontend into a single port.
For all available configuration options, see [configuration]({{< ref "config.md">}}).
@ -76,7 +76,7 @@ This example lets you host Vikunja without any reverse proxy in front of it. Thi
you need to get something up and running. If you want to host Vikunja on one single port instead of two different ones
or need tls termination, check out one of the other examples.
Not that you need to change the `VIKUNJA_API_URL` environment variable to the ip (the docker host you're running this on)
Note that you need to change the `VIKUNJA_API_URL` environment variable to the ip (the docker host you're running this on)
is reachable at. Because the browser you'll use to access the Vikunja frontend uses that url to make the requests, it
has to be able to reach that ip + port from the outside. Putting everything in a private network won't work.
@ -125,7 +125,7 @@ services:
This example assumes [traefik](https://traefik.io) version 2 installed and configured to [use docker as a configuration provider](https://docs.traefik.io/providers/docker/).
We also make a few assumtions here which you'll most likely need to adjust for your traefik setup:
We also make a few assumptions here which you'll most likely need to adjust for your traefik setup:
* Your domain is `vikunja.example.com`
* The entrypoint you want to make vikunja available from is called `https`
@ -155,7 +155,7 @@ services:
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.vikunja-api.rule=Host(`vikunja.example.com`) && PathPrefix(`/api/v1`, `/dav/`, `/.well-known/`)"
- "traefik.http.routers.vikunja-api.rule=Host(`vikunja.example.com`) && (PathPrefix(`/api/v1`) || PathPrefix(`/dav/`) || PathPrefix(`/.well-known/`))"
- "traefik.http.routers.vikunja-api.entrypoints=https"
- "traefik.http.routers.vikunja-api.tls.certResolver=acme"
frontend:
@ -398,7 +398,7 @@ docker main folders:
* vikunja
* mariadb
Synology has it's own GUI for managing Docker containers... But it's easier via docker compose.
Synology has its own GUI for managing Docker containers... But it's easier via docker compose.
To do that, you can
@ -407,7 +407,7 @@ To do that, you can
* without activating SSH, by using Portainer (you have to install first, check out [this tutorial](https://www.portainer.io/blog/how-to-install-portainer-on-a-synology-nas) for exmple):
1. Go to **Dashboard / Stacks** click the button **"Add Stack"**
2. Give it the name Vikunja and paste the adapted docker compose file
3. Deploy the Stack with the "Delpoy Stack" button:
3. Deploy the Stack with the "Deploy Stack" button:
![Portainer Stack deploy](/docs/synology-proxy-2.png)

View File

@ -83,7 +83,7 @@ WantedBy=multi-user.target
If you've installed Vikunja to a directory other than `/opt/vikunja`, you need to adapt `WorkingDirectory` accordingly.
After you made all nessecary modifications, it's time to start the service:
After you made all necessary modifications, it's time to start the service:
{{< highlight bash >}}
sudo systemctl enable vikunja
@ -97,7 +97,7 @@ To build vikunja from source, see [building from source]({{< ref "build-from-sou
### Updating
Simply replace the binary and templates with the new version, then restart Vikunja.
It will automatically run all nessecary database migrations.
It will automatically run all necessary database migrations.
**Make sure to take a look at the changelog for the new version to not miss any manual steps the update may involve!**
## Docker

View File

@ -35,7 +35,7 @@ Just open the file with a text editor - there are comments which will explain ho
## Docker
The docker image is based on nginx and just contains all nessecary files for the frontend.
The docker image is based on nginx and just contains all necessary files for the frontend.
To run it, all you need is
@ -47,10 +47,7 @@ which will run the docker image and expose port 80 on the host.
See [full docker example]({{< ref "full-docker-example.md">}}) for more varations of this config.
### Setting user and group id of the user running vikunja
You can set the user and group id of the user running vikunja with the `PUID` and `PGID` evironment variables.
This follows the pattern used by [the linuxserver.io](https://docs.linuxserver.io/general/understanding-puid-and-pgid) docker images.
The docker container runs as an unprivileged user and does not mount anything.
### API URL configuration in docker

View File

@ -56,3 +56,4 @@ A third-party Helm Chart is available from the k8s-at-home project [here](https:
* [Install Vikunja in Docker for self-hosted Task Tracking](https://smarthomepursuits.com/install-vikunja-in-docker-for-self-hosted-task-tracking/)
* [Self-Hosted To-Do List with Vikunja in Docker](https://www.youtube.com/watch?v=DqyqDWpEvKI) (Youtube)
* [Vikunja self-hosted (step by step)](https://nguyenminhhung.com/vikunja-self-hosted-step-by-step/)
* [How to Install Vikunja on Your Synology NAS](https://mariushosting.com/how-to-install-vikunja-on-your-synology-nas/)

View File

@ -10,6 +10,6 @@ menu:
There are two third-party Helm-Charts which can be used to host Vikunja with k8s:
* [Truecharts](https://truecharts.org/docs/charts/stable/vikunja/)
* [Truecharts](https://truecharts.org/charts/stable/vikunja/)
* [k8s at Home](https://github.com/k8s-at-home/charts)

View File

@ -43,3 +43,26 @@ scopes:
- email
- profile
```
## Google / Google Workspace
Vikunja Config:
```yaml
openid:
enabled: true
redirecturl: https://vikunja.mydomain.com/auth/openid/ <---- slash at the end is important
providers:
- name: Google
authurl: https://accounts.google.com
clientid: <google-oauth-client-id>
clientsecret: <google-oauth-client-secret>
```
Google config:
- Navigate to https://console.cloud.google.com/apis/credentials in the target project
- Create a new OAuth client ID
- Configure an authorized redirect URI of https://vikunja.mydomain.com/auth/openid/google
Note that there currently seems to be no way to stop creation of new users, even when enableregistration is false in the configuration. This means that this approach works well only with an "Internal Organization" app for Google Workspace, which limits the allowed users to organizational accounts only. External / public applications will potentially allow every Google user to register.

View File

@ -17,15 +17,27 @@ However, you can still run it in a subdirectory but need to build the frontend y
First, make sure you're able to build the frontend from source.
Check [the guide about building from source]({{< ref "build-from-source.md">}}#frontend) about that.
Then, run
### Dynamicly set with build command
Run the build with the `VIKUNJA_FRONTEND_BASE` variable specified.
```
pnpm vite build --base=/SUBPATH
pnpm workbox copyLibraries dist/
VIKUNJA_FRONTEND_BASE=/SUBPATH/ pnpm run build
```
Where `SUBPATH` is the subdirectory you want to run Vikunja on.
### Set via .env.local
* Copy `.env.local.example` to `.env.local`
* Uncomment `VIKUNJA_FRONTEND_BASE` and set `/subpath/` to the desired path.
After saving, build Vikunja as normal.
```
pnpm run build
```
Once you have the build files you can deploy them as usual.
Note that when deploying in docker you'll need to put the files in a web container yourself, you
can't use the `Dockerfile` in the repo without modifications.

View File

@ -12,7 +12,7 @@ menu:
Vikunja itself is always fully capable of handling utf-8 characters.
However, your database might be not.
Vikunja itself will work just fine until you want to use non-latin characters in your tasks/lists/etc.
Vikunja itself will work just fine until you want to use non-latin characters in your tasks/projects/etc.
On this page, you will find information about how to fully ensure non-latin characters like aüäß or emojis work
with your installation.

View File

@ -10,9 +10,9 @@ menu:
# Caldav
> **Warning:** The caldav integration is in an early alpha stage and has bugs.
> **Warning:** The caldav integration is in an early alpha stage and has bugs.
> It works well with some clients while having issues with others.
> If you encounter issues, please [report them](https://code.vikunja.io/api/issues/new?body=[caldav])
> If you encounter issues, please [report them](https://code.vikunja.io/api/issues/new?body=[caldav])
Vikunja supports managing tasks via the [caldav VTODO](https://tools.ietf.org/html/rfc5545#section-3.6.2) extension.
@ -24,10 +24,10 @@ All urls are located under the `/dav` subspace.
Urls are:
* `/principals/<username>/`: Returns urls for list discovery. *Use this url to initially make connections to new clients.*
* `/lists/`: Used to manage lists
* `/lists/<List ID>/`: Used to manage a single list
* `/lists/<List ID>/<Task UID>`: Used to manage a task on a list
* `/principals/<username>/`: Returns urls for project discovery. *Use this url to initially make connections to new clients.*
* `/projects/`: Used to manage projects
* `/projects/<List ID>/`: Used to manage a single project
* `/projects/<List ID>/<Task UID>`: Used to manage a task on a project
## Supported properties
@ -37,6 +37,7 @@ Vikunja currently supports the following properties:
* `SUMMARY`
* `DESCRIPTION`
* `PRIORITY`
* `CATEGORIES`
* `COMPLETED`
* `DUE`
* `DTSTART`
@ -46,11 +47,11 @@ Vikunja currently supports the following properties:
* `CREATED`
* `DTSTAMP`
* `LAST-MODIFIED`
* Recurrence
Vikunja **currently does not** support these properties:
* `ATTACH`
* `CATEGORIES`
* `CLASS`
* `COMMENT`
* `GEO`
@ -61,7 +62,6 @@ Vikunja **currently does not** support these properties:
* `CONTACT`
* `RECURRENCE-ID`
* `URL`
* Recurrence
* `SEQUENCE`
## Tested Clients

View File

@ -26,6 +26,15 @@ If you don't specify a command, the [`web`](#web) command will be executed.
All commands use the same standard [config file]({{< ref "../setup/config.md">}}).
## Using the cli in docker
When running Vikunja in docker, you'll need to execute all commands in the `api` container.
Instead of running the `vikunja` binary directly, run it like this:
```
docker exec <name of the vikunja api container> /app/vikunja/vikunja <subcommand>
```
### `dump`
Creates a zip file with all vikunja-related files.
@ -127,6 +136,21 @@ Flags:
* `-p`, `--password`: The password of the new user. You will be asked to enter it if not provided through the flag.
* `-u`, `--username`: The username of the new user.
#### `user delete`
Start the user deletion process.
If called without the `--now` flag, this command will only trigger an email to the user in order for them to confirm and start the deletion process (this is the same behavoir as if the user requested their deletion via the web interface).
With the flag the user is deleted **immediately**.
**USE WITH CAUTION.**
{{< highlight bash >}}
$ vikunja user delete <id> <flags>
{{< /highlight >}}
Flags:
* `-n`, `--now` If provided, deletes the user immediately instead of emailing them first.
#### `user list`
Shows a list of all users.

View File

@ -24,24 +24,26 @@ This document describes the different errors Vikunja can return.
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------|
| 1001 | 400 | A user with this username already exists. |
| 1002 | 400 | A user with this email address already exists. |
| 1004 | 400 | No username and password specified. |
| 1005 | 404 | The user does not exist. |
| 1006 | 400 | Could not get the user id. |
| 1008 | 412 | No password reset token provided. |
| 1009 | 412 | Invalid password reset token. |
| 1010 | 412 | Invalid email confirm token. |
| 1011 | 412 | Wrong username or password. |
| 1012 | 412 | Email address of the user not confirmed. |
| 1013 | 412 | New password is empty. |
| 1014 | 412 | Old password is empty. |
| 1015 | 412 | Totp is already enabled for this user. |
| 1016 | 412 | Totp is not enabled for this user. |
| 1017 | 412 | The provided Totp passcode is invalid. |
| 1018 | 412 | The provided user avatar provider type setting is invalid. |
| 1019 | 412 | No openid email address was provided. |
| 1020 | 412 | This user account is disabled. |
| 1001 | 400 | A user with this username already exists. |
| 1002 | 400 | A user with this email address already exists. |
| 1004 | 400 | No username and password specified. |
| 1005 | 404 | The user does not exist. |
| 1006 | 400 | Could not get the user id. |
| 1008 | 412 | No password reset token provided. |
| 1009 | 412 | Invalid password reset token. |
| 1010 | 412 | Invalid email confirm token. |
| 1011 | 412 | Wrong username or password. |
| 1012 | 412 | Email address of the user not confirmed. |
| 1013 | 412 | New password is empty. |
| 1014 | 412 | Old password is empty. |
| 1015 | 412 | Totp is already enabled for this user. |
| 1016 | 412 | Totp is not enabled for this user. |
| 1017 | 412 | The provided Totp passcode is invalid. |
| 1018 | 412 | The provided user avatar provider type setting is invalid. |
| 1019 | 412 | No openid email address was provided. |
| 1020 | 412 | This user account is disabled. |
| 1021 | 412 | This account is managed by a third-party authentication provider. |
| 1021 | 412 | The username must not contain spaces. |
## Validation
@ -50,26 +52,26 @@ This document describes the different errors Vikunja can return.
| 2001 | 400 | ID cannot be empty or 0. |
| 2002 | 400 | Some of the request data was invalid. The response contains an aditional array with all invalid fields. |
## List
## Project
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------------------------------------------------------------------------------------------------------------------------|
| 3001 | 404 | The list does not exist. |
| 3004 | 403 | The user needs to have read permissions on that list to perform that action. |
| 3005 | 400 | The list title cannot be empty. |
| 3006 | 404 | The list share does not exist. |
| 3007 | 400 | A list with this identifier already exists. |
| 3008 | 412 | The list is archived and can therefore only be accessed read only. This is also true for all tasks associated with this list. |
| 3009 | 412 | The list cannot belong to a dynamically generated namespace like "Favorites". |
| 3010 | 412 | The list must belong to a namespace. |
| 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. |
## Task
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------|
| 4001 | 400 | The list task text cannot be empty. |
| 4002 | 404 | The list task does not exist. |
| 4003 | 403 | All bulk editing tasks must belong to the same list. |
| 4001 | 400 | The project task text cannot be empty. |
| 4002 | 404 | The project task does not exist. |
| 4003 | 403 | All bulk editing tasks must belong to the same project. |
| 4004 | 403 | Need at least one task when bulk editing tasks. |
| 4005 | 403 | The user does not have the right to see the task. |
| 4006 | 403 | The user tried to set a parent task as the task itself. |
@ -107,17 +109,17 @@ This document describes the different errors Vikunja can return.
|-----------|------------------|-------------|
| 6001 | 400 | The team name cannot be emtpy. |
| 6002 | 404 | The team does not exist. |
| 6004 | 409 | The team already has access to that namespace or list. |
| 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. |
| 6007 | 403 | The team does not have access to the list to perform that action. |
| 6007 | 403 | The team does not have access to the project to perform that action. |
## User List Access
## User Project Access
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------|
| 7002 | 409 | The user already has access to that list. |
| 7003 | 403 | The user does not have access to that list. |
| 7002 | 409 | The user already has access to that project. |
| 7003 | 403 | The user does not have access to that project. |
## Label
@ -138,10 +140,10 @@ This document describes the different errors Vikunja can return.
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------|
| 10001 | 404 | The bucket does not exist. |
| 10002 | 400 | The bucket does not belong to that list. |
| 10003 | 412 | You cannot remove the last bucket on a list. |
| 10002 | 400 | The bucket does not belong to that project. |
| 10003 | 412 | You cannot remove the last bucket on a project. |
| 10004 | 412 | You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold. |
| 10005 | 412 | There can be only one done bucket per list. |
| 10005 | 412 | There can be only one done bucket per project. |
## Saved Filters

View File

@ -8,20 +8,20 @@ menu:
parent: "usage"
---
# List and namespace rights for teams and users
# Project and namespace rights for teams and users
Whenever you share a list or namespace with a user or team, you can specify a `rights` parameter.
Whenever you share a project or namespace 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 lists 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. 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. |
## Team admins

108
go.mod
View File

@ -19,58 +19,60 @@ module code.vikunja.io/api
require (
code.vikunja.io/web v0.0.0-20210706160506-d85def955bd3
gitea.com/xorm/xorm-redis-cache v0.2.0
github.com/ThreeDotsLabs/watermill v1.1.1
github.com/ThreeDotsLabs/watermill v1.2.0
github.com/adlio/trello v1.10.0
github.com/arran4/golang-ical v0.0.0-20220517104411-fd89fefb0182
github.com/arran4/golang-ical v0.0.0-20230318005454-19abf92700cc
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
github.com/bbrks/go-blurhash v1.1.1
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
github.com/coreos/go-oidc/v3 v3.4.0
github.com/coreos/go-oidc/v3 v3.5.0
github.com/cweill/gotests v1.6.0
github.com/d4l3k/messagediff v1.2.1
github.com/disintegration/imaging v1.6.2
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
github.com/gabriel-vasile/mimetype v1.4.1
github.com/getsentry/sentry-go v0.14.0
github.com/go-redis/redis/v8 v8.11.5
github.com/go-sql-driver/mysql v1.6.0
github.com/gabriel-vasile/mimetype v1.4.2
github.com/getsentry/sentry-go v0.19.0
github.com/go-sql-driver/mysql v1.7.0
github.com/go-testfixtures/testfixtures/v3 v3.8.1
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/google/uuid v1.3.0
github.com/iancoleman/strcase v0.2.0
github.com/imdario/mergo v0.3.13
github.com/imdario/mergo v0.3.14
github.com/jinzhu/copier v0.3.5
github.com/labstack/echo/v4 v4.9.1
github.com/labstack/echo-jwt/v4 v4.1.0
github.com/labstack/echo/v4 v4.10.2
github.com/labstack/gommon v0.4.0
github.com/lib/pq v1.10.7
github.com/magefile/mage v1.14.0
github.com/mattn/go-sqlite3 v1.14.16
github.com/olekukonko/tablewriter v0.0.5
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pquerna/otp v1.3.0
github.com/prometheus/client_golang v1.13.0
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.14.0
github.com/redis/go-redis/v9 v9.0.2
github.com/robfig/cron/v3 v3.0.1
github.com/samedi/caldav-go v3.0.0+incompatible
github.com/spf13/afero v1.9.2
github.com/spf13/afero v1.9.5
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.13.0
github.com/stretchr/testify v1.8.1
github.com/swaggo/swag v1.8.7
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.2
github.com/swaggo/swag v1.8.11
github.com/tkuchiki/go-timezone v0.2.2
github.com/ulule/limiter/v3 v3.10.0
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93
github.com/wneessen/go-mail v0.3.4
github.com/yuin/goldmark v1.5.2
golang.org/x/crypto v0.1.0
golang.org/x/image v0.1.0
golang.org/x/oauth2 v0.1.0
github.com/ulule/limiter/v3 v3.11.1
github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae
github.com/wneessen/go-mail v0.3.8
github.com/yuin/goldmark v1.5.4
golang.org/x/crypto v0.7.0
golang.org/x/image v0.6.0
golang.org/x/oauth2 v0.6.0
golang.org/x/sync v0.1.0
golang.org/x/sys v0.1.0
golang.org/x/term v0.1.0
golang.org/x/sys v0.6.0
golang.org/x/term v0.6.0
gopkg.in/d4l3k/messagediff.v1 v1.2.1
gopkg.in/yaml.v3 v3.0.1
src.techknowlogick.com/xgo v1.5.1-0.20220906164532-735bfdfb90d9
src.techknowlogick.com/xgo v1.7.1-0.20230307171022-b60708668fc7
src.techknowlogick.com/xormigrate v1.5.0
xorm.io/builder v0.3.12
xorm.io/xorm v1.3.2
@ -83,15 +85,16 @@ require (
github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v3 v3.0.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/garyburd/redigo v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-chi/chi v4.0.2+incompatible // indirect
github.com/go-chi/chi/v5 v5.0.8 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
@ -100,55 +103,52 @@ require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef // indirect
github.com/lithammer/shortuuid/v3 v3.0.4 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/onsi/gomega v1.16.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.39.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/urfave/cli/v2 v2.3.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
golang.org/x/tools v0.1.12 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace (
github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20190524174923-9e5cd1688227+incompatible // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6 and https://github.com/samedi/caldav-go/pull/7
gopkg.in/fsnotify.v1 => github.com/kolaente/fsnotify v1.4.10-0.20200411160148-1bc3c8ff4048 // See https://github.com/fsnotify/fsnotify/issues/328 and https://github.com/golang/go/issues/26904
)
replace github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6 and https://github.com/samedi/caldav-go/pull/7
go 1.19
go 1.20

485
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -405,7 +405,7 @@ func checkGolangCiLintInstalled() {
mg.Deps(initVars)
if err := exec.Command("golangci-lint").Run(); err != nil && strings.Contains(err.Error(), "executable file not found") {
fmt.Println("Please manually install golangci-lint by running")
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.47.3")
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.51.2")
os.Exit(1)
}
}
@ -546,6 +546,20 @@ func (Release) Darwin() error {
return runXgo("darwin-10.15/*")
}
func (Release) Xgo(target string) error {
parts := strings.Split(target, "/")
if len(parts) < 2 {
return fmt.Errorf("invalid target")
}
variant := ""
if len(parts) > 2 && parts[2] != "" {
variant = "-" + strings.ReplaceAll(parts[2], "v", "")
}
return runXgo(parts[0] + "/" + parts[1] + variant)
}
// Compresses the built binaries in dist/binaries/ to reduce their filesize
func (Release) Compress(ctx context.Context) error {
// $(foreach file,$(filter-out $(wildcard $(wildcard $(DIST)/binaries/$(EXECUTABLE)-*mips*)),$(wildcard $(DIST)/binaries/$(EXECUTABLE)-*)), upx -9 $(file);)
@ -689,7 +703,7 @@ func (Release) Packages() error {
binpath := "nfpm"
err = exec.Command(binpath).Run()
if err != nil && strings.Contains(err.Error(), "executable file not found") {
binpath = "/nfpm"
binpath = "/usr/bin/nfpm"
err = exec.Command(binpath).Run()
}
if err != nil && strings.Contains(err.Error(), "executable file not found") {
@ -698,7 +712,7 @@ func (Release) Packages() error {
os.Exit(1)
}
// Because nfpm does not support templating, we replace the values in the config file and restore it after running
// Because nfpm does not support templating, we replace the values in the config file and restore it after running
nfpmConfigPath := RootPath + "/nfpm.yaml"
nfpmconfig, err := os.ReadFile(nfpmConfigPath)
if err != nil {

View File

@ -22,7 +22,8 @@ import (
"strings"
"time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils"
)
@ -57,11 +58,13 @@ type Todo struct {
Priority int64 // 0-9, 1 is highest
RelatedToUID string
Color string
Start time.Time
End time.Time
DueDate time.Time
Duration time.Duration
Categories []string
Start time.Time
End time.Time
DueDate time.Time
Duration time.Duration
RepeatAfter int64
RepeatMode models.TaskRepeatMode
Created time.Time
Updated time.Time // last-mod
@ -226,6 +229,21 @@ CREATED:` + makeCalDavTimeFromTimeStamp(t.Created)
PRIORITY:` + strconv.Itoa(mapPriorityToCaldav(t.Priority))
}
if t.RepeatAfter > 0 || t.RepeatMode == models.TaskRepeatModeMonth {
if t.RepeatMode == models.TaskRepeatModeMonth {
caldavtodos += `
RRULE:FREQ=MONTHLY;BYMONTHDAY=` + t.DueDate.Format("02") // Day of the month
} else {
caldavtodos += `
RRULE:FREQ=SECONDLY;INTERVAL=` + strconv.FormatInt(t.RepeatAfter, 10)
}
}
if len(t.Categories) > 0 {
caldavtodos += `
CATEGORIES:` + strings.Join(t.Categories, ",")
}
caldavtodos += `
LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated)
@ -240,7 +258,7 @@ END:VCALENDAR` // Need a line break
}
func makeCalDavTimeFromTimeStamp(ts time.Time) (caldavtime string) {
return ts.In(config.GetTimeZone()).Format(DateFormat)
return ts.In(time.UTC).Format(DateFormat) + "Z"
}
func calcAlarmDateFromReminder(eventStart, reminder time.Time) (alarmTime string) {

View File

@ -20,6 +20,8 @@ import (
"testing"
"time"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/config"
"github.com/stretchr/testify/assert"
)
@ -84,25 +86,25 @@ X-APPLE-CALENDAR-COLOR:#affffeFF
X-OUTLOOK-COLOR:#affffeFF
X-FUNAMBOL-COLOR:#affffeFF
DESCRIPTION:Lorem Ipsum
DTSTAMP:20181201T011204
DTSTART:20181201T011204
DTEND:20181201T013024
DTSTAMP:20181201T011204Z
DTSTART:20181201T011204Z
DTEND:20181201T013024Z
END:VEVENT
BEGIN:VEVENT
UID:randommduidd
SUMMARY:Event #2
DESCRIPTION:
DTSTAMP:20181202T045844
DTSTART:20181202T045844
DTEND:20181202T081844
DTSTAMP:20181202T045844Z
DTSTART:20181202T045844Z
DTEND:20181202T081844Z
END:VEVENT
BEGIN:VEVENT
UID:20181202T0600242aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83
SUMMARY:Event #3 with empty uid
DESCRIPTION:
DTSTAMP:20181202T050024
DTSTART:20181202T050024
DTEND:20181202T050320
DTSTAMP:20181202T050024Z
DTSTART:20181202T050024Z
DTEND:20181202T050320Z
END:VEVENT
END:VCALENDAR`,
},
@ -169,9 +171,9 @@ BEGIN:VEVENT
UID:randommduid
SUMMARY:Event #1
DESCRIPTION:Lorem Ipsum
DTSTAMP:20181201T011204
DTSTART:20181201T011204
DTEND:20181201T013024
DTSTAMP:20181201T011204Z
DTSTART:20181201T011204Z
DTEND:20181201T013024Z
BEGIN:VALARM
TRIGGER:-PT3M20S
ACTION:DISPLAY
@ -192,9 +194,9 @@ BEGIN:VEVENT
UID:randommduidd
SUMMARY:Event #2
DESCRIPTION:
DTSTAMP:20181202T045844
DTSTART:20181202T045844
DTEND:20181202T081844
DTSTAMP:20181202T045844Z
DTSTART:20181202T045844Z
DTEND:20181202T081844Z
BEGIN:VALARM
TRIGGER:-PT27H50M0S
ACTION:DISPLAY
@ -212,12 +214,12 @@ DESCRIPTION:Event #2
END:VALARM
END:VEVENT
BEGIN:VEVENT
UID:20181202T0500242aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83
UID:20181202T050024Z2aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83
SUMMARY:Event #3 with empty uid
DESCRIPTION:
DTSTAMP:20181202T050024
DTSTART:20181202T050024
DTEND:20181202T050320
DTSTAMP:20181202T050024Z
DTSTART:20181202T050024Z
DTEND:20181202T050320Z
BEGIN:VALARM
TRIGGER:-PT27H51M40S
ACTION:DISPLAY
@ -240,12 +242,12 @@ DESCRIPTION:Event #3 with empty uid
END:VALARM
END:VEVENT
BEGIN:VEVENT
UID:20181202T050024ae7548ce9556df85038abe90dc674d4741a61ce74d1cf
UID:20181202T050024Zae7548ce9556df85038abe90dc674d4741a61ce74d1cf
SUMMARY:Event #4 without any
DESCRIPTION:
DTSTAMP:20181202T050024
DTSTART:20181202T050024
DTEND:20181202T050320
DTSTAMP:20181202T050024Z
DTSTART:20181202T050024Z
DTEND:20181202T050320Z
END:VEVENT
END:VCALENDAR`,
},
@ -278,9 +280,9 @@ BEGIN:VEVENT
UID:randommduid
SUMMARY:Event #1
DESCRIPTION:Lorem Ipsum\nDolor sit amet
DTSTAMP:20181201T011204
DTSTART:20181201T011204
DTEND:20181201T013024
DTSTAMP:20181201T011204Z
DTSTART:20181201T011204Z
DTEND:20181201T013024Z
END:VEVENT
END:VCALENDAR`,
},
@ -333,13 +335,13 @@ X-OUTLOOK-COLOR:#ffffffFF
X-FUNAMBOL-COLOR:#ffffffFF
BEGIN:VTODO
UID:randommduid
DTSTAMP:20181201T011204
DTSTAMP:20181201T011204Z
SUMMARY:Todo #1
X-APPLE-CALENDAR-COLOR:#affffeFF
X-OUTLOOK-COLOR:#affffeFF
X-FUNAMBOL-COLOR:#affffeFF
DESCRIPTION:Lorem Ipsum\nDolor sit amet
LAST-MODIFIED:00010101T000000
LAST-MODIFIED:00010101T000000Z
END:VTODO
END:VCALENDAR`,
},
@ -368,12 +370,12 @@ X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randommduid
DTSTAMP:20181201T011204
DTSTAMP:20181201T011204Z
SUMMARY:Todo #1
DESCRIPTION:Lorem Ipsum
COMPLETED:20181201T013024
COMPLETED:20181201T013024Z
STATUS:COMPLETED
LAST-MODIFIED:00010101T000000
LAST-MODIFIED:00010101T000000Z
END:VTODO
END:VCALENDAR`,
},
@ -402,11 +404,121 @@ X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randommduid
DTSTAMP:20181201T011204
DTSTAMP:20181201T011204Z
SUMMARY:Todo #1
DESCRIPTION:Lorem Ipsum
PRIORITY:9
LAST-MODIFIED:00010101T000000
LAST-MODIFIED:00010101T000000Z
END:VTODO
END:VCALENDAR`,
},
{
name: "with repeating monthly",
args: args{
config: &Config{
Name: "test",
ProdID: "RandomProdID which is not random",
},
todos: []*Todo{
{
Summary: "Todo #1",
Description: "Lorem Ipsum",
UID: "randommduid",
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
RepeatMode: models.TaskRepeatModeMonth,
DueDate: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
},
wantCaldavtasks: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randommduid
DTSTAMP:20181201T011204Z
SUMMARY:Todo #1
DESCRIPTION:Lorem Ipsum
DUE:20181201T011204Z
RRULE:FREQ=MONTHLY;BYMONTHDAY=01
LAST-MODIFIED:00010101T000000Z
END:VTODO
END:VCALENDAR`,
},
{
name: "with repeat mode default",
args: args{
config: &Config{
Name: "test",
ProdID: "RandomProdID which is not random",
},
todos: []*Todo{
{
Summary: "Todo #1",
Description: "Lorem Ipsum",
UID: "randommduid",
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
RepeatMode: models.TaskRepeatModeDefault,
DueDate: time.Unix(1543626724, 0).In(config.GetTimeZone()),
RepeatAfter: 435,
},
},
},
wantCaldavtasks: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randommduid
DTSTAMP:20181201T011204Z
SUMMARY:Todo #1
DESCRIPTION:Lorem Ipsum
DUE:20181201T011204Z
RRULE:FREQ=SECONDLY;INTERVAL=435
LAST-MODIFIED:00010101T000000Z
END:VTODO
END:VCALENDAR`,
},
{
name: "with categories",
args: args{
config: &Config{
Name: "test",
ProdID: "RandomProdID which is not random",
Color: "ffffff",
},
todos: []*Todo{
{
Summary: "Todo #1",
UID: "randommduid",
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Color: "affffe",
Categories: []string{"label1", "label2"},
},
},
},
wantCaldavtasks: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
X-APPLE-CALENDAR-COLOR:#ffffffFF
X-OUTLOOK-COLOR:#ffffffFF
X-FUNAMBOL-COLOR:#ffffffFF
BEGIN:VTODO
UID:randommduid
DTSTAMP:20181201T011204Z
SUMMARY:Todo #1
X-APPLE-CALENDAR-COLOR:#affffeFF
X-OUTLOOK-COLOR:#affffeFF
X-FUNAMBOL-COLOR:#affffeFF
CATEGORIES:label1,label2
LAST-MODIFIED:00010101T000000Z
END:VTODO
END:VCALENDAR`,
},

View File

@ -27,13 +27,17 @@ import (
ics "github.com/arran4/golang-ical"
)
func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*models.TaskWithComments) string {
func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectTasks []*models.TaskWithComments) string {
// Make caldav todos from Vikunja todos
var caldavtodos []*Todo
for _, t := range listTasks {
for _, t := range projectTasks {
duration := t.EndDate.Sub(t.StartDate)
var categories []string
for _, label := range t.Labels {
categories = append(categories, label.Title)
}
caldavtodos = append(caldavtodos, &Todo{
Timestamp: t.Updated,
@ -42,18 +46,21 @@ func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*m
Description: t.Description,
Completed: t.DoneAt,
// Organizer: &t.CreatedBy, // Disabled until we figure out how this works
Priority: t.Priority,
Start: t.StartDate,
End: t.EndDate,
Created: t.Created,
Updated: t.Updated,
DueDate: t.DueDate,
Duration: duration,
Categories: categories,
Priority: t.Priority,
Start: t.StartDate,
End: t.EndDate,
Created: t.Created,
Updated: t.Updated,
DueDate: t.DueDate,
Duration: duration,
RepeatAfter: t.RepeatAfter,
RepeatMode: t.RepeatMode,
})
}
caldavConfig := &Config{
Name: list.Title,
Name: project.Title,
ProdID: "Vikunja Todo App",
}
@ -89,11 +96,23 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
description := strings.ReplaceAll(task["DESCRIPTION"], "\\,", ",")
description = strings.ReplaceAll(description, "\\n", "\n")
var labels []*models.Label
if val, ok := task["CATEGORIES"]; ok {
categories := strings.Split(val, ",")
labels = make([]*models.Label, 0, len(categories))
for _, category := range categories {
labels = append(labels, &models.Label{
Title: category,
})
}
}
vTask = &models.Task{
UID: task["UID"],
Title: task["SUMMARY"],
Description: description,
Priority: priority,
Labels: labels,
DueDate: caldavTimeToTimestamp(task["DUE"]),
Updated: caldavTimeToTimestamp(task["DTSTAMP"]),
StartDate: caldavTimeToTimestamp(task["DTSTART"]),

View File

@ -85,6 +85,39 @@ END:VCALENDAR`,
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
{
name: "With categories",
args: args{content: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randomuid
DTSTAMP:20181201T011204
SUMMARY:Todo #1
DESCRIPTION:Lorem Ipsum
CATEGORIES:cat1,cat2
LAST-MODIFIED:00010101T000000
END:VTODO
END:VCALENDAR`,
},
wantVTask: &models.Task{
Title: "Todo #1",
UID: "randomuid",
Description: "Lorem Ipsum",
Labels: []*models.Label{
{
Title: "cat1",
},
{
Title: "cat2",
},
},
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -99,3 +132,84 @@ END:VCALENDAR`,
})
}
}
func TestGetCaldavTodosForTasks(t *testing.T) {
type args struct {
list *models.ProjectWithTasksAndBuckets
tasks []*models.TaskWithComments
}
tests := []struct {
name string
args args
wantCaldav string
}{
{
name: "Format single Task as Caldav",
args: args{
list: &models.ProjectWithTasksAndBuckets{
Project: models.Project{
Title: "List title",
},
},
tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Task 1",
UID: "randomuid",
Description: "Description",
Priority: 3,
Created: time.Unix(1543626721, 0).In(config.GetTimeZone()),
DueDate: time.Unix(1543626722, 0).In(config.GetTimeZone()),
StartDate: time.Unix(1543626723, 0).In(config.GetTimeZone()),
EndDate: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Updated: time.Unix(1543626725, 0).In(config.GetTimeZone()),
DoneAt: time.Unix(1543626726, 0).In(config.GetTimeZone()),
RepeatAfter: 86400,
Labels: []*models.Label{
{
ID: 1,
Title: "label1",
},
{
ID: 2,
Title: "label2",
},
},
},
},
},
},
wantCaldav: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:List title
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:randomuid
DTSTAMP:20181201T011205Z
SUMMARY:Task 1
DTSTART:20181201T011203Z
DTEND:20181201T011204Z
DESCRIPTION:Description
COMPLETED:20181201T011206Z
STATUS:COMPLETED
DUE:20181201T011202Z
CREATED:20181201T011201Z
PRIORITY:3
RRULE:FREQ=SECONDLY;INTERVAL=86400
CATEGORIES:label1,label2
LAST-MODIFIED:20181201T011205Z
END:VTODO
END:VCALENDAR`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetCaldavTodosForTasks(tt.args.list, tt.args.tasks)
if diff, equal := messagediff.PrettyDiff(got, tt.wantCaldav); !equal {
t.Errorf("GetCaldavTodosForTasks() gotVTask = %v, want %v, diff = %s", got, tt.wantCaldav, diff)
}
})
}
}

View File

@ -73,7 +73,7 @@ func init() {
// User deletion flags
userDeleteCmd.Flags().BoolVarP(&userFlagDeleteNow, "now", "n", false, "If provided, deletes the user immediately instead of sending them an email first.")
userCmd.AddCommand(userListCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd, userDeleteCmd)
userCmd.AddCommand(userProjectCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd, userDeleteCmd)
rootCmd.AddCommand(userCmd)
}
@ -117,9 +117,9 @@ var userCmd = &cobra.Command{
Short: "Manage users locally through the cli.",
}
var userListCmd = &cobra.Command{
Use: "list",
Short: "Shows a list of all users.",
var userProjectCmd = &cobra.Command{
Use: "project",
Short: "Shows a project of all users.",
PreRun: func(cmd *cobra.Command, args []string) {
initialize.FullInit()
},
@ -127,7 +127,7 @@ var userListCmd = &cobra.Command{
s := db.NewSession()
defer s.Close()
users, err := user.ListAllUsers(s)
users, err := user.ProjectAllUsers(s)
if err != nil {
_ = s.Rollback()
log.Fatalf("Error getting users: %s", err)
@ -225,7 +225,7 @@ var userUpdateCmd = &cobra.Command{
u.AvatarProvider = userFlagAvatar
}
_, err := user.UpdateUser(s, u)
_, err := user.UpdateUser(s, u, false)
if err != nil {
_ = s.Rollback()
log.Fatalf("Error updating the user: %s", err)
@ -299,7 +299,7 @@ var userChangeEnabledCmd = &cobra.Command{
u.Status = user.StatusActive
}
}
_, err := user.UpdateUser(s, u)
_, err := user.UpdateUser(s, u, false)
if err != nil {
_ = s.Rollback()
log.Fatalf("Could not enable the user")

View File

@ -128,10 +128,6 @@ const (
FilesBasePath Key = `files.basepath`
FilesMaxSize Key = `files.maxsize`
MigrationWunderlistEnable Key = `migration.wunderlist.enable`
MigrationWunderlistClientID Key = `migration.wunderlist.clientid`
MigrationWunderlistClientSecret Key = `migration.wunderlist.clientsecret`
MigrationWunderlistRedirectURL Key = `migration.wunderlist.redirecturl`
MigrationTodoistEnable Key = `migration.todoist.enable`
MigrationTodoistClientID Key = `migration.todoist.clientid`
MigrationTodoistClientSecret Key = `migration.todoist.clientsecret`
@ -168,7 +164,7 @@ const (
DefaultSettingsDiscoverableByName Key = `defaultsettings.discoverable_by_name`
DefaultSettingsDiscoverableByEmail Key = `defaultsettings.discoverable_by_email`
DefaultSettingsOverdueTaskRemindersEnabled Key = `defaultsettings.overdue_tasks_reminders_enabled`
DefaultSettingsDefaultListID Key = `defaultsettings.default_list_id`
DefaultSettingsDefaultProjectID Key = `defaultsettings.default_project_id`
DefaultSettingsWeekStart Key = `defaultsettings.week_start`
DefaultSettingsLanguage Key = `defaultsettings.language`
DefaultSettingsTimezone Key = `defaultsettings.timezone`
@ -353,7 +349,7 @@ func InitDefaultConfig() {
LogHTTP.setDefault("stdout")
LogEcho.setDefault("off")
LogPath.setDefault(ServiceRootpath.GetString() + "/logs")
LogEvents.setDefault("stdout")
LogEvents.setDefault("off")
LogEventsLevel.setDefault("INFO")
// Rate Limit
RateLimitEnabled.setDefault(false)
@ -369,13 +365,12 @@ func InitDefaultConfig() {
CorsOrigins.setDefault([]string{"*"})
CorsMaxAge.setDefault(0)
// Migration
MigrationWunderlistEnable.setDefault(false)
MigrationTodoistEnable.setDefault(false)
MigrationTrelloEnable.setDefault(false)
MigrationMicrosoftTodoEnable.setDefault(false)
// Avatar
AvatarGravaterExpiration.setDefault(3600)
// List Backgrounds
// Project Backgrounds
BackgroundsEnabled.setDefault(true)
BackgroundsUploadEnabled.setDefault(true)
BackgroundsUnsplashEnabled.setDefault(false)

View File

@ -19,6 +19,7 @@ package db
import (
"encoding/gob"
"fmt"
"net/url"
"os"
"strconv"
"strings"
@ -147,14 +148,28 @@ func parsePostgreSQLHostPort(info string) (string, string) {
return host, port
}
// Copied and adopted from https://github.com/go-gitea/gitea/blob/f337c32e868381c6d2d948221aca0c59f8420c13/modules/setting/database.go#L176-L186
func getPostgreSQLConnectionString(dbHost, dbUser, dbPasswd, dbName, dbSslMode, dbSslCert, dbSslKey, dbSslRootCert string) (connStr string) {
dbParam := "?"
if strings.Contains(dbName, dbParam) {
dbParam = "&"
}
host, port := parsePostgreSQLHostPort(dbHost)
if host[0] == '/' { // looks like a unix socket
connStr = fmt.Sprintf("postgres://%s:%s@:%s/%s%ssslmode=%s&sslcert=%s&sslkey=%s&sslrootcert=%s&host=%s",
url.PathEscape(dbUser), url.PathEscape(dbPasswd), port, dbName, dbParam, dbSslMode, dbSslCert, dbSslKey, dbSslRootCert, host)
} else {
connStr = fmt.Sprintf("postgres://%s:%s@%s:%s/%s%ssslmode=%s&sslcert=%s&sslkey=%s&sslrootcert=%s",
url.PathEscape(dbUser), url.PathEscape(dbPasswd), host, port, dbName, dbParam, dbSslMode, dbSslCert, dbSslKey, dbSslRootCert)
}
return connStr
}
func initPostgresEngine() (engine *xorm.Engine, err error) {
host, port := parsePostgreSQLHostPort(config.DatabaseHost.GetString())
// postgresql://username:password@host:port/dbname[?paramspec]
connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s&sslcert=%s&sslkey=%s&sslrootcert=%s",
connStr := getPostgreSQLConnectionString(
config.DatabaseHost.GetString(),
config.DatabaseUser.GetString(),
config.DatabasePassword.GetString(),
host,
port,
config.DatabaseDatabase.GetString(),
config.DatabaseSslMode.GetString(),
config.DatabaseSslCert.GetString(),

View File

@ -18,6 +18,7 @@ package db
import (
"encoding/json"
"strings"
"code.vikunja.io/api/pkg/log"
@ -53,7 +54,35 @@ func Restore(table string, contents []map[string]interface{}) (err error) {
return err
}
meta, err := x.DBMetas()
if err != nil {
return err
}
var metaForCurrentTable *schemas.Table
for _, m := range meta {
if m.Name == table {
metaForCurrentTable = m
break
}
}
if metaForCurrentTable == nil {
log.Fatalf("Could not find table definition for table %s", table)
}
for _, content := range contents {
for colName, value := range content {
// Date fields might get restored as 0001-01-01 from null dates. This can have unintended side-effects like
// users being scheduled for deletion after a restore.
// To avoid this, we set these dates to nil so that they'll end up as null in the db.
col := metaForCurrentTable.GetColumn(colName)
strVal, is := value.(string)
if is && col.SQLType.IsTime() && (strVal == "" || strings.HasPrefix(strVal, "0001-")) {
content[colName] = nil
}
}
if _, err := x.Table(table).Insert(content); err != nil {
return err
}

View File

@ -1,30 +1,30 @@
- id: 1
title: testbucket1
list_id: 1
project_id: 1
created_by_id: 1
limit: 9999999 # This bucket has a limit we will never exceed in the tests to make sure the logic allows for buckets with limits
position: 2
position: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 2
title: testbucket2
list_id: 1
project_id: 1
created_by_id: 1
limit: 3
position: 1
position: 2
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 3
title: testbucket3
list_id: 1
project_id: 1
created_by_id: 1
is_done_bucket: 1
position: 3
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 4
title: testbucket4 - other list
list_id: 2
title: testbucket4 - other project
project_id: 2
created_by_id: 1
is_done_bucket: 1
created: 2020-04-18 21:13:52
@ -32,189 +32,195 @@
# The following are not or only partly owned by user 1
- id: 5
title: testbucket5
list_id: 20
project_id: 20
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 6
title: testbucket6
list_id: 6
project_id: 6
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 7
title: testbucket7
list_id: 7
project_id: 7
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 8
title: testbucket8
list_id: 8
project_id: 8
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 9
title: testbucket9
list_id: 9
project_id: 9
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 10
title: testbucket10
list_id: 10
project_id: 10
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 11
title: testbucket11
list_id: 11
project_id: 11
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 12
title: testbucket13
list_id: 12
project_id: 12
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 13
title: testbucket13
list_id: 13
project_id: 13
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 14
title: testbucket14
list_id: 14
project_id: 14
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 15
title: testbucket15
list_id: 15
project_id: 15
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 16
title: testbucket16
list_id: 16
project_id: 16
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 17
title: testbucket17
list_id: 17
project_id: 17
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 18
title: testbucket18
list_id: 5
project_id: 5
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 19
title: testbucket19
list_id: 21
project_id: 21
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 20
title: testbucket20
list_id: 22
project_id: 22
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 21
title: testbucket21
list_id: 3
project_id: 3
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
# Duplicate buckets to make deletion of one of them possible
- id: 22
title: testbucket22
list_id: 6
project_id: 6
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 23
title: testbucket23
list_id: 7
project_id: 7
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 24
title: testbucket24
list_id: 8
project_id: 8
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 25
title: testbucket25
list_id: 9
project_id: 9
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 26
title: testbucket26
list_id: 10
project_id: 10
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 27
title: testbucket27
list_id: 11
project_id: 11
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 28
title: testbucket28
list_id: 12
project_id: 12
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 29
title: testbucket29
list_id: 13
project_id: 13
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 30
title: testbucket30
list_id: 14
project_id: 14
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 31
title: testbucket31
list_id: 15
project_id: 15
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 32
title: testbucket32
list_id: 16
project_id: 16
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 33
title: testbucket33
list_id: 17
project_id: 17
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
# This bucket is the last one in its list
# This bucket is the last one in its project
- id: 34
title: testbucket34
list_id: 18
project_id: 18
created_by_id: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 35
title: testbucket35
list_id: 23
project_id: 23
created_by_id: -2
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 36
title: testbucket36
project_id: 26
created_by_id: 15
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52

View File

@ -14,3 +14,7 @@
task_id: 36
label_id: 4
created: 2018-12-01 15:13:12
- id: 5
task_id: 39
label_id: 4
created: 2018-12-01 15:13:12

View File

@ -1,6 +1,6 @@
- id: 1
hash: test
list_id: 1
project_id: 1
right: 0
sharing_type: 1
shared_by_id: 1
@ -8,7 +8,7 @@
updated: 2018-12-02 15:13:12
- id: 2
hash: test2
list_id: 2
project_id: 2
right: 1
sharing_type: 1
shared_by_id: 1
@ -16,7 +16,7 @@
updated: 2018-12-02 15:13:12
- id: 3
hash: test3
list_id: 3
project_id: 3
right: 2
sharing_type: 1
shared_by_id: 1
@ -24,7 +24,7 @@
updated: 2018-12-02 15:13:12
- id: 4
hash: testWithPassword
list_id: 1
project_id: 1
right: 0
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
sharing_type: 2

View File

@ -88,3 +88,9 @@
owner_id: 12
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 18
title: testnamespace18
description: Lorem Ipsum
owner_id: 15
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -168,8 +168,8 @@
position: 17
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# This list is owned by user 7, and several other users have access to it via different methods.
# It is used to test the listUsers method.
# This project is owned by user 7, and several other users have access to it via different methods.
# It is used to test the projectUsers method.
-
id: 18
title: Test18
@ -190,7 +190,7 @@
position: 19
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# User 1 does not have access to this list
# User 1 does not have access to this project
-
id: 20
title: Test20
@ -242,3 +242,24 @@
position: 7
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 25
title: Test25 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
description: Lorem Ipsum
identifier: test26
owner_id: 15
namespace_id: 18
position: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -9,13 +9,13 @@
user_id: 6
created: 2021-02-01 15:13:12
- id: 3
entity_type: 2 # List
entity_type: 2 # project
entity_id: 12 # belongs to namespace 7
user_id: 6
created: 2021-02-01 15:13:12
- id: 4
entity_type: 3 # Task
entity_id: 22 # belongs to list 13 which belongs to namespace 8
entity_id: 22 # belongs to project 13 which belongs to namespace 8
user_id: 6
created: 2021-02-01 15:13:12
- id: 5
@ -24,7 +24,7 @@
user_id: 6
created: 2021-02-01 15:13:12
- id: 6
entity_type: 2 # List
entity_type: 2 # project
entity_id: 13
user_id: 6
created: 2021-02-01 15:13:12

View File

@ -3,7 +3,7 @@
description: 'Lorem Ipsum'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 1
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
@ -13,7 +13,7 @@
title: 'task #2 done'
done: true
created_by_id: 1
list_id: 1
project_id: 1
index: 2
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
@ -23,7 +23,7 @@
title: 'task #3 high prio'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 3
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
@ -33,7 +33,7 @@
title: 'task #4 low prio'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 4
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
@ -43,7 +43,7 @@
title: 'task #5 higher due date'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 5
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
@ -53,7 +53,7 @@
title: 'task #6 lower due date'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 6
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
@ -63,7 +63,7 @@
title: 'task #7 with start date'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 7
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
@ -73,7 +73,7 @@
title: 'task #8 with end date'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 8
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
@ -83,7 +83,7 @@
title: 'task #9 with start and end date'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 9
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
@ -94,7 +94,7 @@
title: 'task #10 basic'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 10
bucket_id: 1
created: 2018-12-01 01:12:04
@ -103,7 +103,7 @@
title: 'task #11 basic'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 11
bucket_id: 1
created: 2018-12-01 01:12:04
@ -112,25 +112,25 @@
title: 'task #12 basic'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 12
bucket_id: 1
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
- id: 13
title: 'task #13 basic other list'
title: 'task #13 basic other project'
done: false
created_by_id: 1
list_id: 2
project_id: 2
index: 1
bucket_id: 4
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
- id: 14
title: 'task #14 basic other list'
title: 'task #14 basic other project'
done: false
created_by_id: 5
list_id: 5
project_id: 5
index: 1
bucket_id: 18
created: 2018-12-01 01:12:04
@ -139,7 +139,7 @@
title: 'task #15'
done: false
created_by_id: 6
list_id: 6
project_id: 6
index: 1
bucket_id: 6
created: 2018-12-01 01:12:04
@ -148,7 +148,7 @@
title: 'task #16'
done: false
created_by_id: 6
list_id: 7
project_id: 7
index: 1
bucket_id: 7
created: 2018-12-01 01:12:04
@ -157,7 +157,7 @@
title: 'task #17'
done: false
created_by_id: 6
list_id: 8
project_id: 8
index: 1
bucket_id: 8
created: 2018-12-01 01:12:04
@ -166,7 +166,7 @@
title: 'task #18'
done: false
created_by_id: 6
list_id: 9
project_id: 9
index: 1
bucket_id: 9
created: 2018-12-01 01:12:04
@ -175,7 +175,7 @@
title: 'task #19'
done: false
created_by_id: 6
list_id: 10
project_id: 10
index: 1
bucket_id: 10
created: 2018-12-01 01:12:04
@ -184,7 +184,7 @@
title: 'task #20'
done: false
created_by_id: 6
list_id: 11
project_id: 11
index: 1
bucket_id: 11
created: 2018-12-01 01:12:04
@ -193,7 +193,7 @@
title: 'task #21'
done: false
created_by_id: 6
list_id: 12
project_id: 12
index: 1
bucket_id: 12
created: 2018-12-01 01:12:04
@ -202,7 +202,7 @@
title: 'task #22'
done: false
created_by_id: 6
list_id: 13
project_id: 13
index: 1
bucket_id: 13
created: 2018-12-01 01:12:04
@ -211,7 +211,7 @@
title: 'task #23'
done: false
created_by_id: 6
list_id: 14
project_id: 14
index: 1
bucket_id: 14
created: 2018-12-01 01:12:04
@ -220,7 +220,7 @@
title: 'task #24'
done: false
created_by_id: 6
list_id: 15
project_id: 15
index: 1
bucket_id: 15
created: 2018-12-01 01:12:04
@ -229,7 +229,7 @@
title: 'task #25'
done: false
created_by_id: 6
list_id: 16
project_id: 16
index: 1
bucket_id: 16
created: 2018-12-01 01:12:04
@ -238,7 +238,7 @@
title: 'task #26'
done: false
created_by_id: 6
list_id: 17
project_id: 17
index: 1
bucket_id: 17
created: 2018-12-01 01:12:04
@ -247,7 +247,7 @@
title: 'task #27 with reminders'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 12
bucket_id: 1
created: 2018-12-01 01:12:04
@ -257,7 +257,7 @@
done: false
created_by_id: 1
repeat_after: 3600
list_id: 1
project_id: 1
index: 13
bucket_id: 1
created: 2018-12-01 01:12:04
@ -266,7 +266,7 @@
title: 'task #29 with parent task (1)'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 14
bucket_id: 1
created: 2018-12-01 01:12:04
@ -275,7 +275,7 @@
title: 'task #30 with assignees'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 15
bucket_id: 1
created: 2018-12-01 01:12:04
@ -284,7 +284,7 @@
title: 'task #31 with color'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 16
hex_color: f0f0f0
bucket_id: 1
@ -294,7 +294,7 @@
title: 'task #32'
done: false
created_by_id: 1
list_id: 3
project_id: 3
index: 1
bucket_id: 21
created: 2018-12-01 01:12:04
@ -303,7 +303,7 @@
title: 'task #33 with percent done'
done: false
created_by_id: 1
list_id: 1
project_id: 1
index: 17
percent_done: 0.5
bucket_id: 1
@ -314,7 +314,7 @@
title: 'task #34'
done: false
created_by_id: 13
list_id: 20
project_id: 20
index: 20
bucket_id: 5
created: 2018-12-01 01:12:04
@ -323,7 +323,7 @@
title: 'task #35'
done: false
created_by_id: 1
list_id: 21
project_id: 21
index: 1
bucket_id: 19
created: 2018-12-01 01:12:04
@ -332,16 +332,17 @@
title: 'task #36'
done: false
created_by_id: 1
list_id: 22
project_id: 22
index: 1
bucket_id: 20
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
due_date: 2018-10-30 22:25:24
- id: 37
title: 'task #37'
done: false
created_by_id: -2
list_id: 2
project_id: 2
index: 2
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
@ -349,8 +350,22 @@
title: 'task #37 done with due date'
done: true
created_by_id: 1
list_id: 22
project_id: 22
index: 2
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
due_date: 2018-10-30 22:25:24
- id: 39
uid: 'uid-caldav-test'
title: 'Title Caldav Test'
description: 'Description Caldav Test'
priority: 3
done: false
created_by_id: 15
project_id: 26
index: 39
due_date: 2023-03-01 15:00:00
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
bucket_id: 1
position: 39

View File

@ -1,54 +1,54 @@
- id: 1
team_id: 1
list_id: 3
project_id: 3
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# This team has read only access on list 6
# This team has read only access on project 6
- id: 2
team_id: 2
list_id: 6
project_id: 6
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# This team has write access on list 7
# This team has write access on project 7
- id: 3
team_id: 3
list_id: 7
project_id: 7
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# This team has admin access on list 8
# This team has admin access on project 8
- id: 4
team_id: 4
list_id: 8
project_id: 8
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# Readonly acces on list 19
# Readonly acces on project 19
- id: 5
team_id: 8
list_id: 19
project_id: 19
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# Write acces on list 19
# Write acces on project 19
- id: 6
team_id: 9
list_id: 19
project_id: 19
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# Admin acces on list 19
# Admin acces on project 19
- id: 7
team_id: 10
list_id: 19
project_id: 19
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 8
team_id: 1
list_id: 21
project_id: 21
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -3,13 +3,13 @@
description: Lorem Ipsum
created_by_id: 1
- id: 2
name: testteam2_read_only_on_list6
name: testteam2_read_only_on_project6
created_by_id: 1
- id: 3
name: testteam3_write_on_list7
name: testteam3_write_on_project7
created_by_id: 1
- id: 4
name: testteam4_admin_on_list8
name: testteam4_admin_on_project8
created_by_id: 1
- id: 5
name: testteam2_read_only_on_namespace7

View File

@ -40,7 +40,7 @@
issuer: local
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# This use is used to create a whole bunch of lists which are then shared directly with a user
# This use is used to create a whole bunch of projects which are then shared directly with a user
- id: 6
username: 'user6'
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
@ -109,3 +109,10 @@
subject: '12345'
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 15
username: 'user15'
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user15@example.com'
issuer: local
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -1,48 +1,54 @@
- id: 1
user_id: 1
list_id: 3
project_id: 3
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 2
user_id: 2
list_id: 3
project_id: 3
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 3
user_id: 1
list_id: 9
project_id: 9
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 4
user_id: 1
list_id: 10
project_id: 10
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 5
user_id: 1
list_id: 11
project_id: 11
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 6
user_id: 4
list_id: 19
project_id: 19
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 7
user_id: 5
list_id: 19
project_id: 19
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 8
user_id: 6
list_id: 19
project_id: 19
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 9
user_id: 15
project_id: 26
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -25,6 +25,7 @@ import (
"code.vikunja.io/api/pkg/log"
"github.com/stretchr/testify/assert"
"xorm.io/builder"
"xorm.io/xorm"
"xorm.io/xorm/names"
)
@ -102,3 +103,10 @@ func AssertMissing(t *testing.T, table string, values map[string]interface{}) {
assert.NoError(t, err, fmt.Sprintf("Failed to assert entries don't exist in db, error was: %s", err))
assert.False(t, exists, fmt.Sprintf("Entries %v exist in table %s", values, table))
}
// AssertCount checks if a number of entries exists in the database
func AssertCount(t *testing.T, table string, where builder.Cond, count int64) {
dbCount, err := x.Table(table).Where(where).Count()
assert.NoError(t, err, fmt.Sprintf("Failed to assert count in db, error was: %s", err))
assert.Equal(t, count, dbCount, fmt.Sprintf("Found %d entries instead of expected %d in table %s", dbCount, count, table))
}

View File

@ -83,7 +83,7 @@ func CreateWithMime(f io.Reader, realname string, realsize uint64, a web.Auth, m
s := db.NewSession()
defer s.Close()
file, err = CreateWithMimeAndSession(s, f, realname, realsize, a, mime)
file, err = CreateWithMimeAndSession(s, f, realname, realsize, a, mime, true)
if err != nil {
_ = s.Rollback()
return
@ -91,14 +91,14 @@ func CreateWithMime(f io.Reader, realname string, realsize uint64, a web.Auth, m
return
}
func CreateWithMimeAndSession(s *xorm.Session, f io.Reader, realname string, realsize uint64, a web.Auth, mime string) (file *File, err error) {
func CreateWithMimeAndSession(s *xorm.Session, f io.Reader, realname string, realsize uint64, a web.Auth, mime string, checkFileSizeLimit bool) (file *File, err error) {
// Get and parse the configured file size
var maxSize datasize.ByteSize
err = maxSize.UnmarshalText([]byte(config.FilesMaxSize.GetString()))
if err != nil {
return nil, err
}
if realsize > maxSize.Bytes() {
if realsize > maxSize.Bytes() && checkFileSizeLimit {
return nil, ErrFileIsTooLarge{Size: realsize}
}
@ -140,7 +140,7 @@ func (f *File) Delete() (err error) {
var perr *os.PathError
if errors.As(err, &perr) {
// Don't fail when removing the file failed
log.Errorf("Error deleting file %d: %w", err)
log.Errorf("Error deleting file %d: %w", f.ID, err)
return s.Commit()
}

View File

@ -28,37 +28,37 @@ import (
// This tests the following behaviour:
// 1. A namespace should not be editable if it is archived.
// 1. With the exception being to un-archive it.
// 2. A list which belongs to an archived namespace cannot be edited.
// 3. An archived list should not be editable.
// 2. A project which belongs to an archived namespace cannot be edited.
// 3. An archived project should not be editable.
// 1. Except for un-archiving it.
// 4. It is not possible to un-archive a list individually if its namespace is archived.
// 5. Creating new lists on an archived namespace should not work.
// 6. Creating new tasks on an archived list should not work.
// 7. Creating new tasks on a list who's namespace is archived should not work.
// 8. Editing tasks on an archived list should not work.
// 9. Editing tasks on a list who's namespace is archived should not work.
// 10. Archived namespaces should not appear in the list with all namespaces.
// 11. Archived lists should not appear in the list with all lists.
// 12. Lists who's namespace is archived should not appear in the list with all lists.
// 4. It is not possible to un-archive a project individually if its namespace is archived.
// 5. Creating new projects on an archived namespace 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.
// 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.
//
// All of this is tested through integration tests because it's not yet clear if this will be implemented directly
// or with some kind of middleware.
//
// Maybe the inheritance of lists from namespaces could be solved with some kind of is_archived_inherited flag -
// that way I'd only need to implement the checking on a list level and update the flag for all lists once the
// namespace is archived. The archived flag would then be used to not accedentially unarchive lists which were
// Maybe the inheritance of projects from namespaces could be solved with some kind of is_archived_inherited flag -
// that way I'd only need to implement the checking on a 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.
//
// Namespace 16 is archived
// List 21 belongs to namespace 16
// List 22 is archived individually
// Project 21 belongs to namespace 16
// Project 22 is archived individually
func TestArchived(t *testing.T) {
testListHandler := webHandlerTest{
testProjectHandler := webHandlerTest{
user: &testuser1,
strFunc: func() handler.CObject {
return &models.List{}
return &models.Project{}
},
t: t,
}
@ -116,54 +116,54 @@ func TestArchived(t *testing.T) {
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"is_archived":false`)
})
t.Run("no new lists", func(t *testing.T) {
_, err := testListHandler.testCreateWithUser(nil, map[string]string{"namespace": "16"}, `{"title":"Lorem"}`)
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 list", func(t *testing.T) {
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 list if explicitly requested", func(t *testing.T) {
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("list", func(t *testing.T) {
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{"listtask": taskID}, `{"title":"TestIpsum"}`)
_, 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{"listtask": taskID})
_, 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{"listtask": taskID}, `{"label_id":1}`)
_, 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{"listtask": taskID, "label": "4"})
_, 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{"listtask": taskID}, `{"user_id":3}`)
_, 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{"listtask": taskID, "user": "2"})
_, err := testAssigneeHandler.testDeleteWithUser(nil, map[string]string{"projecttask": taskID, "user": "2"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
@ -194,45 +194,45 @@ func TestArchived(t *testing.T) {
})
}
// The list belongs to an archived namespace
// The project belongs to an archived namespace
t.Run("archived namespace", func(t *testing.T) {
t.Run("not editable", func(t *testing.T) {
_, err := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "21"}, `{"title":"TestIpsum","is_archived":true}`)
_, 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{"list": "21"}, `{"title":"Lorem"}`)
_, 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 := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "21"}, `{"title":"LoremIpsum","is_archived":false}`)
_, 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 list itself is archived
// The project itself is archived
t.Run("archived individually", func(t *testing.T) {
t.Run("not editable", func(t *testing.T) {
_, err := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "22"}, `{"title":"TestIpsum","is_archived":true}`)
_, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "22"}, `{"title":"TestIpsum","is_archived":true}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListIsArchived)
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
})
t.Run("no new tasks", func(t *testing.T) {
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"list": "22"}, `{"title":"Lorem"}`)
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"project": "22"}, `{"title":"Lorem"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListIsArchived)
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
})
t.Run("unarchivable", func(t *testing.T) {
rec, err := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "22"}, `{"title":"LoremIpsum","is_archived":false,"namespace_id":1}`)
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.ErrCodeListIsArchived, t)
taskTests("36", models.ErrCodeProjectIsArchived, t)
})
})
}

View File

@ -0,0 +1,69 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package integrations
import (
"net/http"
"testing"
"code.vikunja.io/api/pkg/routes/caldav"
"github.com/stretchr/testify/assert"
)
const vtodo = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:List 26 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid
DTSTAMP:20230301T073337Z
SUMMARY:Caldav Task 1
CATEGORIES:tag1,tag2,tag3
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
END:VTODO
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"})
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(), "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"})
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
})
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"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "SUMMARY:Title Caldav Test")
assert.Contains(t, rec.Body.String(), "DESCRIPTION:Description Caldav Test")
assert.Contains(t, rec.Body.String(), "DUE:20230301T150000Z")
assert.Contains(t, rec.Body.String(), "PRIORITY:3")
assert.Contains(t, rec.Body.String(), "CATEGORIES:Label #4")
})
}

View File

@ -33,6 +33,7 @@ import (
"code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/api/pkg/modules/keyvalue"
"code.vikunja.io/api/pkg/routes"
"code.vikunja.io/api/pkg/routes/caldav"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"code.vikunja.io/web/handler"
@ -49,30 +50,11 @@ var (
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Email: "user1@example.com",
}
testuser2 = user.User{
ID: 2,
Username: "user2",
testuser15 = user.User{
ID: 15,
Username: "user15",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Email: "user2@example.com",
}
testuser3 = user.User{
ID: 3,
Username: "user3",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Email: "user3@example.com",
}
testuser4 = user.User{
ID: 4,
Username: "user4",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Email: "user4@example.com",
}
testuser5 = user.User{
ID: 4,
Username: "user5",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Email: "user5@example.com",
Status: user.StatusDisabled,
Email: "user15@example.com",
}
)
@ -170,6 +152,19 @@ func newTestRequestWithLinkShare(t *testing.T, method string, handler echo.Handl
return
}
func newCaldavTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams)
c.Request().Header.Set(echo.HeaderContentType, echo.MIMETextPlain)
result, _ := caldav.BasicAuth(user.Username, "1234", c)
if !result {
t.Error("BasicAuth for caldav failed")
t.FailNow()
}
err = handler(c)
return
}
func assertHandlerErrorCode(t *testing.T, err error, expectedErrorCode int) {
if err == nil {
t.Error("Error is nil")

View File

@ -39,7 +39,7 @@ func TestBucket(t *testing.T) {
linkShare: &models.LinkSharing{
ID: 2,
Hash: "test2",
ListID: 2,
ProjectID: 2,
Right: models.RightWrite,
SharingType: models.SharingTypeWithoutPassword,
SharedByID: 1,
@ -51,17 +51,17 @@ func TestBucket(t *testing.T) {
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(nil, map[string]string{"list": "1"})
rec, err := testHandler.testReadAllWithUser(nil, map[string]string{"project": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `testbucket1`)
assert.Contains(t, rec.Body.String(), `testbucket2`)
assert.Contains(t, rec.Body.String(), `testbucket3`)
assert.NotContains(t, rec.Body.String(), `testbucket4`) // Different List
assert.NotContains(t, rec.Body.String(), `testbucket4`) // Different Project
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
// Check the list was loaded successfully afterwards, see testReadOneWithUser
// Check the project was loaded successfully afterwards, see testReadOneWithUser
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "1"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
@ -150,7 +150,7 @@ func TestBucket(t *testing.T) {
})
t.Run("Delete", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "1", "bucket": "1"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "1", "bucket": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
@ -162,70 +162,70 @@ func TestBucket(t *testing.T) {
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
// Owned by user13
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "20", "bucket": "5"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "20", "bucket": "5"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "6", "bucket": "6"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "6", "bucket": "6"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "7", "bucket": "7"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "7", "bucket": "7"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "8", "bucket": "8"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "8", "bucket": "8"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "9", "bucket": "9"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "9", "bucket": "9"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "10", "bucket": "10"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "10", "bucket": "10"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "11", "bucket": "11"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "11", "bucket": "11"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "12", "bucket": "12"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "12", "bucket": "12"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "13", "bucket": "13"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "13", "bucket": "13"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "14", "bucket": "14"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "14", "bucket": "14"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "15", "bucket": "15"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "15", "bucket": "15"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "16", "bucket": "16"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "16", "bucket": "16"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "17", "bucket": "17"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "17", "bucket": "17"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
@ -233,92 +233,92 @@ func TestBucket(t *testing.T) {
})
t.Run("Create", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9999"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9999"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
// Owned by user13
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "20"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "6"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "6"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "7"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "7"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "8"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "8"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "10"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "11"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "12"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "12"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "13"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "13"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "14"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "14"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "15"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "15"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "16"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "16"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "17"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "17"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
})
t.Run("Link Share", func(t *testing.T) {
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
db.AssertExists(t, "buckets", map[string]interface{}{
"list_id": 2,
"project_id": 2,
"created_by_id": -2,
"title": "Lorem Ipsum",
}, false)

View File

@ -31,14 +31,14 @@ func TestLinkSharingAuth(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), `"token":"`)
assert.Contains(t, rec.Body.String(), `"list_id":1`)
assert.Contains(t, rec.Body.String(), `"project_id":1`)
})
t.Run("Without Password, Password Provided", func(t *testing.T) {
rec, err := newTestRequest(t, http.MethodPost, apiv1.AuthenticateLinkShare, `{"password":"somethingsomething"}`, nil, map[string]string{"share": "test"})
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), `"token":"`)
assert.Contains(t, rec.Body.String(), `"list_id":1`)
assert.Contains(t, rec.Body.String(), `"project_id":1`)
})
t.Run("With Password, No Password Provided", func(t *testing.T) {
_, err := newTestRequest(t, http.MethodPost, apiv1.AuthenticateLinkShare, ``, nil, map[string]string{"share": "testWithPassword"})
@ -50,7 +50,7 @@ func TestLinkSharingAuth(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), `"token":"`)
assert.Contains(t, rec.Body.String(), `"list_id":1`)
assert.Contains(t, rec.Body.String(), `"project_id":1`)
})
t.Run("With Wrong Password", func(t *testing.T) {
_, err := newTestRequest(t, http.MethodPost, apiv1.AuthenticateLinkShare, `{"password":"someWrongPassword"}`, nil, map[string]string{"share": "testWithPassword"})

View File

@ -31,7 +31,7 @@ func TestLinkSharing(t *testing.T) {
linkshareRead := &models.LinkSharing{
ID: 1,
Hash: "test1",
ListID: 1,
ProjectID: 1,
Right: models.RightRead,
SharingType: models.SharingTypeWithoutPassword,
SharedByID: 1,
@ -40,7 +40,7 @@ func TestLinkSharing(t *testing.T) {
linkShareWrite := &models.LinkSharing{
ID: 2,
Hash: "test2",
ListID: 2,
ProjectID: 2,
Right: models.RightWrite,
SharingType: models.SharingTypeWithoutPassword,
SharedByID: 1,
@ -49,7 +49,7 @@ func TestLinkSharing(t *testing.T) {
linkShareAdmin := &models.LinkSharing{
ID: 3,
Hash: "test3",
ListID: 3,
ProjectID: 3,
Right: models.RightAdmin,
SharingType: models.SharingTypeWithoutPassword,
SharedByID: 1,
@ -65,102 +65,102 @@ func TestLinkSharing(t *testing.T) {
}
t.Run("Forbidden", func(t *testing.T) {
t.Run("read only", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"right":0}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "20"}, `{"right":0}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("write", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"right":1}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "20"}, `{"right":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("admin", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"right":2}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "20"}, `{"right":2}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Read only access", func(t *testing.T) {
t.Run("read only", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"right":0}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9"}, `{"right":0}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("write", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"right":1}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9"}, `{"right":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("admin", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"right":2}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9"}, `{"right":2}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Write access", func(t *testing.T) {
t.Run("read only", func(t *testing.T) {
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"right":0}`)
req, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "10"}, `{"right":0}`)
assert.NoError(t, err)
assert.Contains(t, req.Body.String(), `"hash":`)
})
t.Run("write", func(t *testing.T) {
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"right":1}`)
req, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "10"}, `{"right":1}`)
assert.NoError(t, err)
assert.Contains(t, req.Body.String(), `"hash":`)
})
t.Run("admin", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"right":2}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "10"}, `{"right":2}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Admin access", func(t *testing.T) {
t.Run("read only", func(t *testing.T) {
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"right":0}`)
req, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "11"}, `{"right":0}`)
assert.NoError(t, err)
assert.Contains(t, req.Body.String(), `"hash":`)
})
t.Run("write", func(t *testing.T) {
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"right":1}`)
req, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "11"}, `{"right":1}`)
assert.NoError(t, err)
assert.Contains(t, req.Body.String(), `"hash":`)
})
t.Run("admin", func(t *testing.T) {
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"right":2}`)
req, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "11"}, `{"right":2}`)
assert.NoError(t, err)
assert.Contains(t, req.Body.String(), `"hash":`)
})
})
})
t.Run("Lists", func(t *testing.T) {
testHandlerListReadOnly := webHandlerTest{
t.Run("Projects", func(t *testing.T) {
testHandlerProjectReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.List{}
return &models.Project{}
},
t: t,
}
testHandlerListWrite := webHandlerTest{
testHandlerProjectWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.List{}
return &models.Project{}
},
t: t,
}
testHandlerListAdmin := webHandlerTest{
testHandlerProjectAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.List{}
return &models.Project{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandlerListReadOnly.testReadAllWithLinkShare(nil, nil)
rec, err := testHandlerProjectReadOnly.testReadAllWithLinkShare(nil, nil)
assert.NoError(t, err)
// Should only return the shared list, nothing else
// Should only return the shared project, nothing else
assert.Contains(t, rec.Body.String(), `Test1`)
assert.NotContains(t, rec.Body.String(), `Test2`)
assert.NotContains(t, rec.Body.String(), `Test3`)
@ -168,9 +168,9 @@ func TestLinkSharing(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `Test5`)
})
t.Run("Search", func(t *testing.T) {
rec, err := testHandlerListReadOnly.testReadAllWithLinkShare(url.Values{"s": []string{"est1"}}, nil)
rec, err := testHandlerProjectReadOnly.testReadAllWithLinkShare(url.Values{"s": []string{"est1"}}, nil)
assert.NoError(t, err)
// Should only return the shared list, nothing else
// Should only return the shared project, nothing else
assert.Contains(t, rec.Body.String(), `Test1`)
assert.NotContains(t, rec.Body.String(), `Test2`)
assert.NotContains(t, rec.Body.String(), `Test3`)
@ -180,35 +180,35 @@ func TestLinkSharing(t *testing.T) {
})
t.Run("ReadOne", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "1"})
rec, err := testHandlerProjectReadOnly.testReadOneWithLinkShare(nil, map[string]string{"project": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test1"`)
assert.NotContains(t, rec.Body.String(), `"title":"Test2"`)
})
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "9999999"})
_, err := testHandlerProjectReadOnly.testReadOneWithLinkShare(nil, map[string]string{"project": "9999999"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
// List 2, not shared with this token
_, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "2"})
// Project 2, not shared with this token
_, err := testHandlerProjectReadOnly.testReadOneWithLinkShare(nil, map[string]string{"project": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `You don't have the right to see this`)
})
t.Run("Shared readonly", func(t *testing.T) {
rec, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "1"})
rec, err := testHandlerProjectReadOnly.testReadOneWithLinkShare(nil, map[string]string{"project": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test1"`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerListWrite.testReadOneWithLinkShare(nil, map[string]string{"list": "2"})
rec, err := testHandlerProjectWrite.testReadOneWithLinkShare(nil, map[string]string{"project": "2"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test2"`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerListAdmin.testReadOneWithLinkShare(nil, map[string]string{"list": "3"})
rec, err := testHandlerProjectAdmin.testReadOneWithLinkShare(nil, map[string]string{"project": "3"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test3"`)
})
@ -216,28 +216,28 @@ func TestLinkSharing(t *testing.T) {
})
t.Run("Update", func(t *testing.T) {
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandlerListReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "9999999"}, `{"title":"TestLoremIpsum"}`)
_, err := testHandlerProjectReadOnly.testUpdateWithLinkShare(nil, map[string]string{"project": "9999999"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
_, err := testHandlerListReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"TestLoremIpsum"}`)
_, err := testHandlerProjectReadOnly.testUpdateWithLinkShare(nil, map[string]string{"project": "2"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum"}`)
_, err := testHandlerProjectReadOnly.testUpdateWithLinkShare(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerListWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"TestLoremIpsum","namespace_id":1}`)
rec, err := testHandlerProjectWrite.testUpdateWithLinkShare(nil, map[string]string{"project": "2"}, `{"title":"TestLoremIpsum","namespace_id":1}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerListAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"title":"TestLoremIpsum","namespace_id":2}`)
rec, err := testHandlerProjectAdmin.testUpdateWithLinkShare(nil, map[string]string{"project": "3"}, `{"title":"TestLoremIpsum","namespace_id":2}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
@ -245,54 +245,54 @@ func TestLinkSharing(t *testing.T) {
})
t.Run("Delete", func(t *testing.T) {
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandlerListReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "9999999"})
_, err := testHandlerProjectReadOnly.testDeleteWithLinkShare(nil, map[string]string{"project": "9999999"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
_, err := testHandlerListReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"})
_, err := testHandlerProjectReadOnly.testDeleteWithLinkShare(nil, map[string]string{"project": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"})
_, err := testHandlerProjectReadOnly.testDeleteWithLinkShare(nil, map[string]string{"project": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListWrite.testDeleteWithLinkShare(nil, map[string]string{"list": "2"})
_, err := testHandlerProjectWrite.testDeleteWithLinkShare(nil, map[string]string{"project": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerListAdmin.testDeleteWithLinkShare(nil, map[string]string{"list": "3"})
rec, err := testHandlerProjectAdmin.testDeleteWithLinkShare(nil, map[string]string{"project": "3"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
})
})
// Creating a list should always be forbidden, since users need access to a namespace to create a list
// Creating a project should always be forbidden, since users need access to a namespace to create a project
t.Run("Create", func(t *testing.T) {
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandlerListReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "999999"}, `{"title":"Lorem"}`)
_, err := testHandlerProjectReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "999999"}, `{"title":"Lorem"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
_, err := testHandlerProjectReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListWrite.testCreateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
_, err := testHandlerProjectWrite.testCreateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListAdmin.testCreateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
_, err := testHandlerProjectAdmin.testCreateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
@ -301,74 +301,74 @@ func TestLinkSharing(t *testing.T) {
t.Run("Right Management", func(t *testing.T) {
t.Run("Users", func(t *testing.T) {
testHandlerListUserReadOnly := webHandlerTest{
testHandlerProjectUserReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.ListUser{}
return &models.ProjectUser{}
},
t: t,
}
testHandlerListUserWrite := webHandlerTest{
testHandlerProjectUserWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.ListUser{}
return &models.ProjectUser{}
},
t: t,
}
testHandlerListUserAdmin := webHandlerTest{
testHandlerProjectUserAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.ListUser{}
return &models.ProjectUser{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
rec, err := testHandlerListUserReadOnly.testReadAllWithLinkShare(nil, map[string]string{"list": "1"})
rec, err := testHandlerProjectUserReadOnly.testReadAllWithLinkShare(nil, map[string]string{"project": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[]`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerListUserWrite.testReadAllWithLinkShare(nil, map[string]string{"list": "2"})
rec, err := testHandlerProjectUserWrite.testReadAllWithLinkShare(nil, map[string]string{"project": "2"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[]`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerListUserAdmin.testReadAllWithLinkShare(nil, map[string]string{"list": "3"})
rec, err := testHandlerProjectUserAdmin.testReadAllWithLinkShare(nil, map[string]string{"project": "3"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"username":"user1"`)
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListUserReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"user_id":"user1"}`)
_, err := testHandlerProjectUserReadOnly.testCreateWithLinkShare(nil, map[string]string{"project": "1"}, `{"user_id":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListUserWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"user_id":"user1"}`)
_, err := testHandlerProjectUserWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{"user_id":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListUserAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"user_id":"user1"}`)
_, err := testHandlerProjectUserAdmin.testCreateWithLinkShare(nil, map[string]string{"project": "3"}, `{"user_id":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListUserReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "1"}, `{"user_id":"user1"}`)
_, err := testHandlerProjectUserReadOnly.testUpdateWithLinkShare(nil, map[string]string{"project": "1"}, `{"user_id":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListUserWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"user_id":"user1"}`)
_, err := testHandlerProjectUserWrite.testUpdateWithLinkShare(nil, map[string]string{"project": "2"}, `{"user_id":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListUserAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"user_id":"user1"}`)
_, err := testHandlerProjectUserAdmin.testUpdateWithLinkShare(nil, map[string]string{"project": "3"}, `{"user_id":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
@ -376,91 +376,91 @@ func TestLinkSharing(t *testing.T) {
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListUserReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"})
_, err := testHandlerProjectUserReadOnly.testDeleteWithLinkShare(nil, map[string]string{"project": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListUserWrite.testDeleteWithLinkShare(nil, map[string]string{"list": "2"})
_, err := testHandlerProjectUserWrite.testDeleteWithLinkShare(nil, map[string]string{"project": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListUserAdmin.testDeleteWithLinkShare(nil, map[string]string{"list": "3"})
_, err := testHandlerProjectUserAdmin.testDeleteWithLinkShare(nil, map[string]string{"project": "3"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
})
t.Run("Teams", func(t *testing.T) {
testHandlerListTeamReadOnly := webHandlerTest{
testHandlerProjectTeamReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.TeamList{}
return &models.TeamProject{}
},
t: t,
}
testHandlerListTeamWrite := webHandlerTest{
testHandlerProjectTeamWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.TeamList{}
return &models.TeamProject{}
},
t: t,
}
testHandlerListTeamAdmin := webHandlerTest{
testHandlerProjectTeamAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.TeamList{}
return &models.TeamProject{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
rec, err := testHandlerListTeamReadOnly.testReadAllWithLinkShare(nil, map[string]string{"list": "1"})
rec, err := testHandlerProjectTeamReadOnly.testReadAllWithLinkShare(nil, map[string]string{"project": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[]`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerListTeamWrite.testReadAllWithLinkShare(nil, map[string]string{"list": "2"})
rec, err := testHandlerProjectTeamWrite.testReadAllWithLinkShare(nil, map[string]string{"project": "2"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[]`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerListTeamAdmin.testReadAllWithLinkShare(nil, map[string]string{"list": "3"})
rec, err := testHandlerProjectTeamAdmin.testReadAllWithLinkShare(nil, map[string]string{"project": "3"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"name":"testteam1"`)
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListTeamReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"team_id":1}`)
_, err := testHandlerProjectTeamReadOnly.testCreateWithLinkShare(nil, map[string]string{"project": "1"}, `{"team_id":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListTeamWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"team_id":1}`)
_, err := testHandlerProjectTeamWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{"team_id":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListTeamAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"team_id":1}`)
_, err := testHandlerProjectTeamAdmin.testCreateWithLinkShare(nil, map[string]string{"project": "3"}, `{"team_id":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListTeamReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "1"}, `{"team_id":1}`)
_, err := testHandlerProjectTeamReadOnly.testUpdateWithLinkShare(nil, map[string]string{"project": "1"}, `{"team_id":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListTeamWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"team_id":1}`)
_, err := testHandlerProjectTeamWrite.testUpdateWithLinkShare(nil, map[string]string{"project": "2"}, `{"team_id":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListTeamAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"team_id":1}`)
_, err := testHandlerProjectTeamAdmin.testUpdateWithLinkShare(nil, map[string]string{"project": "3"}, `{"team_id":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
@ -468,17 +468,17 @@ func TestLinkSharing(t *testing.T) {
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListTeamReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"})
_, err := testHandlerProjectTeamReadOnly.testDeleteWithLinkShare(nil, map[string]string{"project": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListTeamWrite.testDeleteWithLinkShare(nil, map[string]string{"list": "2"})
_, err := testHandlerProjectTeamWrite.testDeleteWithLinkShare(nil, map[string]string{"project": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListTeamAdmin.testDeleteWithLinkShare(nil, map[string]string{"list": "3"})
_, err := testHandlerProjectTeamAdmin.testDeleteWithLinkShare(nil, map[string]string{"project": "3"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
@ -586,34 +586,34 @@ func TestLinkSharing(t *testing.T) {
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerTaskReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandlerTaskReadOnly.testCreateWithLinkShare(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerTaskWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandlerTaskWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerTaskAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandlerTaskAdmin.testCreateWithLinkShare(nil, map[string]string{"project": "3"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerTaskReadOnly.testUpdateWithLinkShare(nil, map[string]string{"listtask": "1"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandlerTaskReadOnly.testUpdateWithLinkShare(nil, map[string]string{"projecttask": "1"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerTaskWrite.testUpdateWithLinkShare(nil, map[string]string{"listtask": "13"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandlerTaskWrite.testUpdateWithLinkShare(nil, map[string]string{"projecttask": "13"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerTaskAdmin.testUpdateWithLinkShare(nil, map[string]string{"listtask": "32"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandlerTaskAdmin.testUpdateWithLinkShare(nil, map[string]string{"projecttask": "32"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
@ -621,17 +621,17 @@ func TestLinkSharing(t *testing.T) {
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerTaskReadOnly.testDeleteWithLinkShare(nil, map[string]string{"listtask": "1"})
_, err := testHandlerTaskReadOnly.testDeleteWithLinkShare(nil, map[string]string{"projecttask": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerTaskWrite.testDeleteWithLinkShare(nil, map[string]string{"listtask": "13"})
rec, err := testHandlerTaskWrite.testDeleteWithLinkShare(nil, map[string]string{"projecttask": "13"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerTaskAdmin.testDeleteWithLinkShare(nil, map[string]string{"listtask": "32"})
rec, err := testHandlerTaskAdmin.testDeleteWithLinkShare(nil, map[string]string{"projecttask": "32"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
@ -738,34 +738,34 @@ func TestLinkSharing(t *testing.T) {
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
rec, err := testHandlerLinkShareReadOnly.testReadAllWithLinkShare(nil, map[string]string{"list": "1"})
rec, err := testHandlerLinkShareReadOnly.testReadAllWithLinkShare(nil, map[string]string{"project": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"hash":"test"`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerLinkShareWrite.testReadAllWithLinkShare(nil, map[string]string{"list": "2"})
rec, err := testHandlerLinkShareWrite.testReadAllWithLinkShare(nil, map[string]string{"project": "2"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"hash":"test2"`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerLinkShareAdmin.testReadAllWithLinkShare(nil, map[string]string{"list": "3"})
rec, err := testHandlerLinkShareAdmin.testReadAllWithLinkShare(nil, map[string]string{"project": "3"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"hash":"test3"`)
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerLinkShareReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{}`)
_, err := testHandlerLinkShareReadOnly.testCreateWithLinkShare(nil, map[string]string{"project": "1"}, `{}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{}`)
_, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerLinkShareAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{}`)
_, err := testHandlerLinkShareAdmin.testCreateWithLinkShare(nil, map[string]string{"project": "3"}, `{}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})

View File

@ -26,11 +26,11 @@ import (
"github.com/stretchr/testify/assert"
)
func TestList(t *testing.T) {
func TestProject(t *testing.T) {
testHandler := webHandlerTest{
user: &testuser1,
strFunc: func() handler.CObject {
return &models.List{}
return &models.Project{}
},
t: t,
}
@ -40,7 +40,7 @@ func TestList(t *testing.T) {
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Test1`)
assert.NotContains(t, rec.Body.String(), `Test2"`)
assert.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_list
assert.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_project
assert.Contains(t, rec.Body.String(), `Test4`) // Shared via namespace
assert.NotContains(t, rec.Body.String(), `Test5`)
assert.NotContains(t, rec.Body.String(), `Test21`) // Archived through namespace
@ -55,12 +55,12 @@ func TestList(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `Test4`)
assert.NotContains(t, rec.Body.String(), `Test5`)
})
t.Run("Normal with archived lists", func(t *testing.T) {
t.Run("Normal with archived projects", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"is_archived": []string{"true"}}, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Test1`)
assert.NotContains(t, rec.Body.String(), `Test2"`)
assert.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_list
assert.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_project
assert.Contains(t, rec.Body.String(), `Test4`) // Shared via namespace
assert.NotContains(t, rec.Body.String(), `Test5`)
assert.Contains(t, rec.Body.String(), `Test21`) // Archived through namespace
@ -69,7 +69,7 @@ func TestList(t *testing.T) {
})
t.Run("ReadOne", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "1"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test1"`)
assert.NotContains(t, rec.Body.String(), `"title":"Test2"`)
@ -79,89 +79,89 @@ func TestList(t *testing.T) {
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right")) // User 1 is owner so they should have admin rights.
})
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "9999"})
_, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "9999"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
// Owned by user13
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "20"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "20"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `You don't have the right to see this`)
assert.Empty(t, rec.Result().Header.Get("x-max-rights"))
})
t.Run("Shared Via Team readonly", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "6"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "6"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test6"`)
assert.Equal(t, "0", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "7"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "7"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test7"`)
assert.Equal(t, "1", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "8"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "8"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test8"`)
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via User readonly", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "9"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "9"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test9"`)
assert.Equal(t, "0", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "10"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "10"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test10"`)
assert.Equal(t, "1", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "11"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "11"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test11"`)
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "12"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "12"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test12"`)
assert.Equal(t, "0", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "13"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "13"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test13"`)
assert.Equal(t, "1", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "14"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "14"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test14"`)
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "15"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "15"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test15"`)
assert.Equal(t, "0", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "16"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "16"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test16"`)
assert.Equal(t, "1", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "17"})
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "17"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test17"`)
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right"))
@ -170,101 +170,101 @@ func TestList(t *testing.T) {
})
t.Run("Update", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
// Check the list was loaded successfully afterwards, see testReadOneWithUser
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum","namespace_id":1}`)
// Check the project was loaded successfully afterwards, see testReadOneWithUser
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum","namespace_id":1}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
// The description should not be updated but returned correctly
assert.Contains(t, rec.Body.String(), `description":"Lorem Ipsum`)
})
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "9999"}, `{"title":"TestLoremIpsum"}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "9999"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
})
t.Run("Normal with updating the description", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet","namespace_id":1}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet","namespace_id":1}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum dolor sit amet`)
})
t.Run("Empty title", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":""}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":""}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required")
})
t.Run("Title too long", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(1|250)")
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
// Owned by user13
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "20"}, `{"title":"TestLoremIpsum"}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "20"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "6"}, `{"title":"TestLoremIpsum"}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "6"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "7"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "7"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "8"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "8"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "9"}, `{"title":"TestLoremIpsum"}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "9"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "10"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "10"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "11"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "11"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "12"}, `{"title":"TestLoremIpsum"}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "12"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "13"}, `{"title":"TestLoremIpsum","namespace_id":8}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "13"}, `{"title":"TestLoremIpsum","namespace_id":8}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "14"}, `{"title":"TestLoremIpsum","namespace_id":9}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "14"}, `{"title":"TestLoremIpsum","namespace_id":9}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "15"}, `{"title":"TestLoremIpsum"}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "15"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "16"}, `{"title":"TestLoremIpsum","namespace_id":11}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "16"}, `{"title":"TestLoremIpsum","namespace_id":11}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "17"}, `{"title":"TestLoremIpsum","namespace_id":12}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "17"}, `{"title":"TestLoremIpsum","namespace_id":12}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
@ -272,82 +272,82 @@ func TestList(t *testing.T) {
})
t.Run("Delete", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "1"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "999"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "999"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
// Owned by user13
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "20"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "20"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "6"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "6"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team write", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "7"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "7"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "8"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "8"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "9"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "9"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User write", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "10"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "10"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "11"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "11"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "12"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "12"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "13"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "13"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "14"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "14"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "15"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "15"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "16"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "16"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "17"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "17"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
@ -355,7 +355,7 @@ func TestList(t *testing.T) {
})
t.Run("Create", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
// Check the list was loaded successfully after update, see testReadOneWithUser
// Check the project was loaded successfully after update, see testReadOneWithUser
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)

View File

@ -33,9 +33,9 @@ func TestTaskCollection(t *testing.T) {
},
t: t,
}
t.Run("ReadAll on list", func(t *testing.T) {
t.Run("ReadAll on project", func(t *testing.T) {
urlParams := map[string]string{"list": "1"}
urlParams := map[string]string{"project": "1"}
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(nil, urlParams)
@ -113,49 +113,49 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
t.Run("by priority desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
})
t.Run("by priority asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
// should equal duedate asc
t.Run("by due_date", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
})
t.Run("by duedate desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
})
// Due date without unix suffix
t.Run("by duedate asc without suffix", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
})
t.Run("by due_date without suffix", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
})
t.Run("by duedate desc without suffix", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
})
t.Run("by duedate asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
})
t.Run("invalid sort parameter", func(t *testing.T) {
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
@ -287,7 +287,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("date range", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(
nil,
map[string]string{"list": "-2"}, // Actually a saved filter - contains the same filter arguments as the start and end date filter from above
map[string]string{"project": "-2"}, // Actually a saved filter - contains the same filter arguments as the start and end date filter from above
)
assert.NoError(t, err)
assert.NotContains(t, rec.Body.String(), `task #1`)
@ -341,7 +341,7 @@ func TestTaskCollection(t *testing.T) {
assert.Contains(t, rec.Body.String(), `task #24`) // Shared via namespace user readonly
assert.Contains(t, rec.Body.String(), `task #25`) // Shared via namespace user write
assert.Contains(t, rec.Body.String(), `task #26`) // Shared via namespace user admin
// TODO: Add some cases where the user has access to the list, somhow shared
// TODO: Add some cases where the user has access to the project, somhow shared
})
t.Run("Search", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"task #6"}}, nil)
@ -366,33 +366,33 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
t.Run("by priority desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
})
t.Run("by priority asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
// should equal duedate asc
t.Run("by due_date", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
})
t.Run("by duedate desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
})
t.Run("by duedate asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
})
t.Run("invalid parameter", func(t *testing.T) {
// Invalid parameter should not sort at all

View File

@ -39,7 +39,7 @@ func TestTaskComments(t *testing.T) {
linkShare: &models.LinkSharing{
ID: 2,
Hash: "test2",
ListID: 2,
ProjectID: 2,
Right: models.RightWrite,
SharingType: models.SharingTypeWithoutPassword,
SharedByID: 1,

View File

@ -39,7 +39,7 @@ func TestTask(t *testing.T) {
linkShare: &models.LinkSharing{
ID: 2,
Hash: "test2",
ListID: 2,
ProjectID: 2,
Right: models.RightWrite,
SharingType: models.SharingTypeWithoutPassword,
SharedByID: 1,
@ -54,157 +54,157 @@ func TestTask(t *testing.T) {
t.Run("Update", func(t *testing.T) {
t.Run("Update task items", func(t *testing.T) {
t.Run("Title", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
assert.NotContains(t, rec.Body.String(), `"title":"task #1"`)
})
t.Run("Description", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"description":"Dolor sit amet"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"description":"Dolor sit amet"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"description":"Dolor sit amet"`)
assert.NotContains(t, rec.Body.String(), `"description":"Lorem Ipsum"`)
})
t.Run("Description to empty", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"description":""}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"description":""}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"description":""`)
assert.NotContains(t, rec.Body.String(), `"description":"Lorem Ipsum"`)
})
t.Run("Done", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"done":true}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"done":true}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"done":true`)
assert.NotContains(t, rec.Body.String(), `"done":false`)
})
t.Run("Undone", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "2"}, `{"done":false}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "2"}, `{"done":false}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"done":false`)
assert.NotContains(t, rec.Body.String(), `"done":true`)
})
t.Run("Due date", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"due_date": "2020-02-10T10:00:00Z"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"due_date": "2020-02-10T10:00:00Z"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"due_date":"2020-02-10T10:00:00Z"`)
assert.NotContains(t, rec.Body.String(), `"due_date":0`)
})
t.Run("Due date unset", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "5"}, `{"due_date": null}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "5"}, `{"due_date": null}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"due_date":"0001-01-01T00:00:00Z"`)
assert.NotContains(t, rec.Body.String(), `"due_date":"2020-02-10T10:00:00Z"`)
})
t.Run("Reminders", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"reminder_dates": ["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"reminder_dates": ["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"reminder_dates":["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]`)
assert.NotContains(t, rec.Body.String(), `"reminder_dates": null`)
})
t.Run("Reminders unset to empty array", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminder_dates": []}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "27"}, `{"reminder_dates": []}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"reminder_dates":null`)
assert.NotContains(t, rec.Body.String(), `"reminder_dates":[1543626724,1543626824]`)
})
t.Run("Reminders unset to null", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminder_dates": null}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "27"}, `{"reminder_dates": null}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"reminder_dates":null`)
assert.NotContains(t, rec.Body.String(), `"reminder_dates":[1543626724,1543626824]`)
})
t.Run("Repeat after", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"repeat_after":3600}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"repeat_after":3600}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"repeat_after":3600`)
assert.NotContains(t, rec.Body.String(), `"repeat_after":0`)
})
t.Run("Repeat after unset", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "28"}, `{"repeat_after":0}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "28"}, `{"repeat_after":0}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"repeat_after":0`)
assert.NotContains(t, rec.Body.String(), `"repeat_after":3600`)
})
t.Run("Repeat after update done", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "28"}, `{"done":true}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "28"}, `{"done":true}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"done":false`)
assert.NotContains(t, rec.Body.String(), `"done":true`)
})
t.Run("Assignees", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"assignees":[{"id":1}]}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"assignees":[{"id":1}]}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"assignees":[{"id":1`)
assert.NotContains(t, rec.Body.String(), `"assignees":[]`)
})
t.Run("Removing Assignees empty array", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "30"}, `{"assignees":[]}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "30"}, `{"assignees":[]}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"assignees":null`)
assert.NotContains(t, rec.Body.String(), `"assignees":[{"id":1`)
})
t.Run("Removing Assignees null", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "30"}, `{"assignees":null}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "30"}, `{"assignees":null}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"assignees":null`)
assert.NotContains(t, rec.Body.String(), `"assignees":[{"id":1`)
})
t.Run("Priority", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"priority":100}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"priority":100}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"priority":100`)
assert.NotContains(t, rec.Body.String(), `"priority":0`)
})
t.Run("Priority to 0", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "3"}, `{"priority":0}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "3"}, `{"priority":0}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"priority":0`)
assert.NotContains(t, rec.Body.String(), `"priority":100`)
})
t.Run("Start date", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"start_date":"2020-02-10T10:00:00Z"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"start_date":"2020-02-10T10:00:00Z"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"start_date":"2020-02-10T10:00:00Z"`)
assert.NotContains(t, rec.Body.String(), `"start_date":0`)
})
t.Run("Start date unset", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "7"}, `{"start_date":"0001-01-01T00:00:00Z"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "7"}, `{"start_date":"0001-01-01T00:00:00Z"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"start_date":"0001-01-01T00:00:00Z"`)
assert.NotContains(t, rec.Body.String(), `"start_date":"2020-02-10T10:00:00Z"`)
})
t.Run("End date", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"end_date":"2020-02-10T12:00:00Z"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"end_date":"2020-02-10T12:00:00Z"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"end_date":"2020-02-10T12:00:00Z"`)
assert.NotContains(t, rec.Body.String(), `"end_date":""`)
})
t.Run("End date unset", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "8"}, `{"end_date":"0001-01-01T00:00:00Z"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "8"}, `{"end_date":"0001-01-01T00:00:00Z"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"end_date":"0001-01-01T00:00:00Z"`)
assert.NotContains(t, rec.Body.String(), `"end_date":"2020-02-10T10:00:00Z"`)
})
t.Run("Color", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"hex_color":"f0f0f0"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"hex_color":"f0f0f0"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"hex_color":"f0f0f0"`)
assert.NotContains(t, rec.Body.String(), `"hex_color":""`)
})
t.Run("Color unset", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "31"}, `{"hex_color":""}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "31"}, `{"hex_color":""}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"hex_color":""`)
assert.NotContains(t, rec.Body.String(), `"hex_color":"f0f0f0"`)
})
t.Run("Percent Done", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"percent_done":0.1}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"percent_done":0.1}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"percent_done":0.1`)
assert.NotContains(t, rec.Body.String(), `"percent_done":0,`)
})
t.Run("Percent Done unset", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "33"}, `{"percent_done":0}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "33"}, `{"percent_done":0}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"percent_done":0,`)
assert.NotContains(t, rec.Body.String(), `"percent_done":0.1`)
@ -212,112 +212,112 @@ func TestTask(t *testing.T) {
})
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "99999"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "99999"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "14"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "14"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "15"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "15"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "16"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "16"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "17"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "17"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "18"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "18"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "19"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "19"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "20"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "20"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "21"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "21"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "22"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "22"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "23"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "23"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "24"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "24"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "25"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "25"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "26"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "26"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
})
t.Run("Move to other list", func(t *testing.T) {
t.Run("Move to other project", func(t *testing.T) {
t.Run("normal", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"list_id":7}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"project_id":7}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"list_id":7`)
assert.NotContains(t, rec.Body.String(), `"list_id":1`)
assert.Contains(t, rec.Body.String(), `"project_id":7`)
assert.NotContains(t, rec.Body.String(), `"project_id":1`)
})
t.Run("Forbidden", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"list_id":20}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"project_id":20}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
})
t.Run("Read Only", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"list_id":6}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"project_id":6}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
})
})
t.Run("Bucket", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"bucket_id":3}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"bucket_id":3}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"bucket_id":3`)
assert.NotContains(t, rec.Body.String(), `"bucket_id":1`)
})
t.Run("Different List", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"bucket_id":4}`)
t.Run("Different Project", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"bucket_id":4}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToList)
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToProject)
})
t.Run("Nonexisting Bucket", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"bucket_id":9999}`)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"bucket_id":9999}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
})
@ -325,81 +325,81 @@ func TestTask(t *testing.T) {
})
t.Run("Delete", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "1"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "99999"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "99999"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "14"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "14"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "15"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "15"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "16"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "16"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "17"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "17"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "18"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "18"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "19"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "19"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "20"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "20"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "21"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "21"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "22"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "22"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "23"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "23"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "24"})
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "24"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "25"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "25"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "26"})
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "26"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
@ -407,110 +407,110 @@ func TestTask(t *testing.T) {
})
t.Run("Create", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9999"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9999"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
// Owned by user13
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "20"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "6"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "6"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "7"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "7"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "8"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "8"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "10"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "11"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "12"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "12"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "13"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "13"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "14"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "14"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "15"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "15"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "16"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "16"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "17"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "17"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
})
t.Run("Bucket", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum","bucket_id":3}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum","bucket_id":3}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"bucket_id":3`)
assert.NotContains(t, rec.Body.String(), `"bucket_id":1`)
})
t.Run("Different List", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum","bucket_id":4}`)
t.Run("Different Project", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum","bucket_id":4}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToList)
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToProject)
})
t.Run("Nonexisting Bucket", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum","bucket_id":9999}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum","bucket_id":9999}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
})
})
t.Run("Link Share", func(t *testing.T) {
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
db.AssertExists(t, "tasks", map[string]interface{}{
"list_id": 2,
"project_id": 2,
"title": "Lorem Ipsum",
"created_by_id": -2,
}, false)

View File

@ -24,7 +24,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestUserList(t *testing.T) {
func TestUserProject(t *testing.T) {
t.Run("Normal test", func(t *testing.T) {
rec, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserList, &testuser1, "", nil, nil)
assert.NoError(t, err)

View File

@ -58,6 +58,10 @@ func getClient() (*mail.Client, error) {
mail.WithTimeout((config.MailerQueueTimeout.GetDuration() + 3) * time.Second), // 3s more for us to close before mail server timeout
}
if config.MailerForceSSL.GetBool() {
opts = append(opts, mail.WithSSL())
}
if config.MailerUsername.GetString() != "" && config.MailerPassword.GetString() != "" {
opts = append(opts, mail.WithSMTPAuth(authType))
}

View File

@ -85,7 +85,7 @@ func getMessage(opts *Opts) *mail.Msg {
m.Subject(opts.Subject)
for _, h := range opts.Headers {
m.SetHeader(h.Field, h.Content)
m.SetGenHeader(h.Field, h.Content)
}
for name, content := range opts.Embeds {

View File

@ -28,7 +28,7 @@ import (
)
// SecondsUntilInactive defines the seconds until a user is considered inactive
const SecondsUntilInactive = 60
const SecondsUntilInactive = 30
// ActiveUsersKey is the key used to store active users in redis
const ActiveUsersKey = `activeusers`
@ -55,12 +55,13 @@ func init() {
users: make(map[int64]*ActiveUser),
mutex: &sync.Mutex{},
}
}
promauto.NewGaugeFunc(prometheus.GaugeOpts{
func setupActiveUsersMetric() {
err := registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_active_users",
Help: "The currently active users on this node",
Help: "The number of users active within the last 30 seconds on this node",
}, func() float64 {
allActiveUsers, err := getActiveUsers()
if err != nil {
log.Error(err.Error())
@ -75,7 +76,10 @@ func init() {
}
}
return float64(activeUsersCount)
})
}))
if err != nil {
log.Criticalf("Could not register metrics for currently active users: %s", err)
}
}
// SetUserActive sets a user as active and pushes it to redis

View File

@ -28,8 +28,8 @@ import (
)
const (
// ListCountKey is the name of the key in which we save the list count
ListCountKey = `listcount`
// ProjectCountKey is the name of the key in which we save the project count
ProjectCountKey = `projectcount`
// UserCountKey is the name of the key we use to store total users in redis
UserCountKey = `usercount`
@ -65,16 +65,16 @@ func InitMetrics() {
GetRegistry()
// Register total list count metric
// Register total project count metric
err := registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_list_count",
Help: "The number of lists on this instance",
Name: "vikunja_project_count",
Help: "The number of projects on this instance",
}, func() float64 {
count, _ := GetCount(ListCountKey)
count, _ := GetCount(ProjectCountKey)
return float64(count)
}))
if err != nil {
log.Criticalf("Could not register metrics for %s: %s", ListCountKey, err)
log.Criticalf("Could not register metrics for %s: %s", ProjectCountKey, err)
}
// Register total user count metric
@ -113,7 +113,7 @@ func InitMetrics() {
log.Criticalf("Could not register metrics for %s: %s", TaskCountKey, err)
}
// Register total user count metric
// Register total teams count metric
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_team_count",
Help: "The total number of teams on this instance",
@ -124,9 +124,11 @@ func InitMetrics() {
if err != nil {
log.Criticalf("Could not register metrics for %s: %s", TeamCountKey, err)
}
setupActiveUsersMetric()
}
// GetCount returns the current count from redis
// GetCount returns the current count from keyvalue
func GetCount(key string) (count int64, err error) {
cnt, exists, err := keyvalue.Get(key)
if err != nil {
@ -145,7 +147,7 @@ func GetCount(key string) (count int64, err error) {
return
}
// SetCount sets the list count to a given value
// SetCount sets the project count to a given value
func SetCount(count int64, key string) error {
return keyvalue.Put(key, count)
}

View File

@ -0,0 +1,432 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
const sqliteRename20221113170740 = `
-- buckets
create table buckets_dg_tmp
(
id INTEGER not null
primary key autoincrement,
title TEXT not null,
project_id INTEGER not null,
"limit" INTEGER default 0,
is_done_bucket INTEGER,
position REAL,
created DATETIME not null,
updated DATETIME not null,
created_by_id INTEGER not null
);
insert into buckets_dg_tmp(id, title, project_id, "limit", is_done_bucket, position, created, updated, created_by_id)
select id,
title,
list_id,
"limit",
is_done_bucket,
position,
created,
updated,
created_by_id
from buckets;
drop table buckets;
alter table buckets_dg_tmp
rename to buckets;
create unique index UQE_buckets_id
on buckets (id);
-- link shares
create table link_shares_dg_tmp
(
id INTEGER not null
primary key autoincrement,
hash TEXT not null,
name TEXT,
project_id INTEGER not null,
"right" INTEGER default 0 not null,
sharing_type INTEGER default 0 not null,
password TEXT,
shared_by_id INTEGER not null,
created DATETIME not null,
updated DATETIME not null
);
insert into link_shares_dg_tmp(id, hash, name, project_id, "right", sharing_type, password, shared_by_id, created,
updated)
select id,
hash,
name,
list_id,
"right",
sharing_type,
password,
shared_by_id,
created,
updated
from link_shares;
drop table link_shares;
alter table link_shares_dg_tmp
rename to link_shares;
create index IDX_link_shares_right
on link_shares ("right");
create index IDX_link_shares_shared_by_id
on link_shares (shared_by_id);
create index IDX_link_shares_sharing_type
on link_shares (sharing_type);
create unique index UQE_link_shares_hash
on link_shares (hash);
create unique index UQE_link_shares_id
on link_shares (id);
-- tasks
create table tasks_dg_tmp
(
id INTEGER not null
primary key autoincrement,
title TEXT not null,
description TEXT,
done INTEGER,
done_at DATETIME,
due_date DATETIME,
project_id INTEGER not null,
repeat_after INTEGER,
repeat_mode INTEGER default 0 not null,
priority INTEGER,
start_date DATETIME,
end_date DATETIME,
hex_color TEXT,
percent_done REAL,
"index" INTEGER default 0 not null,
uid TEXT,
cover_image_attachment_id INTEGER default 0,
created DATETIME not null,
updated DATETIME not null,
bucket_id INTEGER,
position REAL,
kanban_position REAL,
created_by_id INTEGER not null
);
insert into tasks_dg_tmp(id, title, description, done, done_at, due_date, project_id, repeat_after, repeat_mode,
priority, start_date, end_date, hex_color, percent_done, "index", uid,
cover_image_attachment_id, created, updated, bucket_id, position, kanban_position,
created_by_id)
select id,
title,
description,
done,
done_at,
due_date,
list_id,
repeat_after,
repeat_mode,
priority,
start_date,
end_date,
hex_color,
percent_done,
"index",
uid,
cover_image_attachment_id,
created,
updated,
bucket_id,
position,
kanban_position,
created_by_id
from tasks;
drop table tasks;
alter table tasks_dg_tmp
rename to tasks;
create index IDX_tasks_done
on tasks (done);
create index IDX_tasks_done_at
on tasks (done_at);
create index IDX_tasks_due_date
on tasks (due_date);
create index IDX_tasks_end_date
on tasks (end_date);
create index IDX_tasks_list_id
on tasks (project_id);
create index IDX_tasks_repeat_after
on tasks (repeat_after);
create index IDX_tasks_start_date
on tasks (start_date);
create unique index UQE_tasks_id
on tasks (id);
--- team_lists
create table team_lists_dg_tmp
(
id INTEGER not null
primary key autoincrement,
team_id INTEGER not null,
project_id INTEGER not null,
"right" INTEGER default 0 not null,
created DATETIME not null,
updated DATETIME not null
);
insert into team_lists_dg_tmp(id, team_id, project_id, "right", created, updated)
select id, team_id, list_id, "right", created, updated
from team_lists;
drop table team_lists;
alter table team_lists_dg_tmp
rename to team_lists;
create index IDX_team_lists_list_id
on team_lists (project_id);
create index IDX_team_lists_right
on team_lists ("right");
create index IDX_team_lists_team_id
on team_lists (team_id);
create unique index UQE_team_lists_id
on team_lists (id);
--- users
create table users_dg_tmp
(
id INTEGER not null
primary key autoincrement,
name TEXT,
username TEXT not null,
password TEXT,
email TEXT,
status INTEGER default 0,
avatar_provider TEXT,
avatar_file_id INTEGER,
issuer TEXT,
subject TEXT,
email_reminders_enabled INTEGER default 1,
discoverable_by_name INTEGER default 0,
discoverable_by_email INTEGER default 0,
overdue_tasks_reminders_enabled INTEGER default 1,
overdue_tasks_reminders_time TEXT default '09:00' not null,
default_project_id INTEGER,
week_start INTEGER,
language TEXT,
timezone TEXT,
deletion_scheduled_at DATETIME,
deletion_last_reminder_sent DATETIME,
export_file_id INTEGER,
created DATETIME not null,
updated DATETIME not null
);
insert into users_dg_tmp(id, name, username, password, email, status, avatar_provider, avatar_file_id, issuer, subject,
email_reminders_enabled, discoverable_by_name, discoverable_by_email,
overdue_tasks_reminders_enabled, overdue_tasks_reminders_time, default_project_id, week_start,
language, timezone, deletion_scheduled_at, deletion_last_reminder_sent, export_file_id,
created, updated)
select id,
name,
username,
password,
email,
status,
avatar_provider,
avatar_file_id,
issuer,
subject,
email_reminders_enabled,
discoverable_by_name,
discoverable_by_email,
overdue_tasks_reminders_enabled,
overdue_tasks_reminders_time,
default_list_id,
week_start,
language,
timezone,
deletion_scheduled_at,
deletion_last_reminder_sent,
export_file_id,
created,
updated
from users;
drop table users;
alter table users_dg_tmp
rename to users;
create index IDX_users_default_list_id
on users (default_project_id);
create index IDX_users_discoverable_by_email
on users (discoverable_by_email);
create index IDX_users_discoverable_by_name
on users (discoverable_by_name);
create index IDX_users_overdue_tasks_reminders_enabled
on users (overdue_tasks_reminders_enabled);
create unique index UQE_users_id
on users (id);
create unique index UQE_users_username
on users (username);
--- users_list
create table users_lists_dg_tmp
(
id INTEGER not null
primary key autoincrement,
user_id INTEGER not null,
project_id INTEGER not null,
"right" INTEGER default 0 not null,
created DATETIME not null,
updated DATETIME not null
);
insert into users_lists_dg_tmp(id, user_id, project_id, "right", created, updated)
select id, user_id, list_id, "right", created, updated
from users_lists;
drop table users_lists;
alter table users_lists_dg_tmp
rename to users_lists;
create index IDX_users_lists_list_id
on users_lists (project_id);
create index IDX_users_lists_right
on users_lists ("right");
create index IDX_users_lists_user_id
on users_lists (user_id);
create unique index UQE_users_lists_id
on users_lists (id);
`
type colToRename struct {
table string
oldName string
newName string
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20221113170740",
Description: "Rename lists to projects",
Migrate: func(tx *xorm.Engine) error {
// SQLite does not support renaming columns. Instead, we'll need to run manual sql.
if tx.Dialect().URI().DBType == schemas.SQLITE {
_, err := tx.Exec(sqliteRename20221113170740)
if err != nil {
return err
}
} else {
colsToRename := []*colToRename{
{
table: "buckets",
oldName: "list_id",
newName: "project_id",
},
{
table: "link_shares",
oldName: "list_id",
newName: "project_id",
},
{
table: "tasks",
oldName: "list_id",
newName: "project_id",
},
{
table: "team_lists",
oldName: "list_id",
newName: "project_id",
},
{
table: "users",
oldName: "default_list_id",
newName: "default_project_id",
},
{
table: "users_lists",
oldName: "list_id",
newName: "project_id",
},
}
for _, col := range colsToRename {
if tx.Dialect().URI().DBType == schemas.POSTGRES || tx.Dialect().URI().DBType == schemas.MYSQL {
_, err := tx.Exec("ALTER TABLE `" + col.table + "` RENAME COLUMN `" + col.oldName + "` TO `" + col.newName + "`")
if err != nil {
return err
}
}
}
}
err := renameTable(tx, "lists", "projects")
if err != nil {
return err
}
err = renameTable(tx, "team_lists", "team_projects")
if err != nil {
return err
}
err = renameTable(tx, "users_lists", "users_projects")
if err != nil {
return err
}
return nil
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -24,13 +24,13 @@ import (
// BulkTask is the definition of a bulk update task
type BulkTask struct {
// A list of task ids to update
// A project of task ids to update
IDs []int64 `json:"task_ids"`
Tasks []*Task `json:"-"`
Task
}
func (bt *BulkTask) checkIfTasksAreOnTheSameList(s *xorm.Session) (err error) {
func (bt *BulkTask) checkIfTasksAreOnTheSameProject(s *xorm.Session) (err error) {
// Get the tasks
err = bt.GetTasksByIDs(s)
if err != nil {
@ -41,11 +41,11 @@ func (bt *BulkTask) checkIfTasksAreOnTheSameList(s *xorm.Session) (err error) {
return ErrBulkTasksNeedAtLeastOne{}
}
// Check if all tasks are in the same list
var firstListID = bt.Tasks[0].ListID
// Check if all tasks are in the same project
var firstProjectID = bt.Tasks[0].ProjectID
for _, t := range bt.Tasks {
if t.ListID != firstListID {
return ErrBulkTasksMustBeInSameList{firstListID, t.ListID}
if t.ProjectID != firstProjectID {
return ErrBulkTasksMustBeInSameProject{firstProjectID, t.ProjectID}
}
}
@ -55,13 +55,13 @@ func (bt *BulkTask) checkIfTasksAreOnTheSameList(s *xorm.Session) (err error) {
// CanUpdate checks if a user is allowed to update a task
func (bt *BulkTask) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
err := bt.checkIfTasksAreOnTheSameList(s)
err := bt.checkIfTasksAreOnTheSameProject(s)
if err != nil {
return false, err
}
// A user can update an task if he has write acces to its list
l := &List{ID: bt.Tasks[0].ListID}
// A user can update an task if he has write acces to its project
l := &Project{ID: bt.Tasks[0].ProjectID}
return l.CanWrite(s, a)
}
@ -72,10 +72,10 @@ func (bt *BulkTask) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param task body models.BulkTask true "The task object. Looks like a normal task, the only difference is it uses an array of list_ids to update."
// @Param task body models.BulkTask true "The task object. Looks like a normal task, the only difference is it uses an array of project_ids to update."
// @Success 200 {object} models.Task "The updated task object."
// @Failure 400 {object} web.HTTPError "Invalid task object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the task (aka its list)"
// @Failure 403 {object} web.HTTPError "The user does not have access to the task (aka its project)"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/bulk [post]
func (bt *BulkTask) Update(s *xorm.Session, a web.Auth) (err error) {

View File

@ -47,7 +47,7 @@ func TestBulkTask_Update(t *testing.T) {
},
},
{
name: "Test with one task on different list",
name: "Test with one task on different project",
fields: fields{
IDs: []int64{10, 11, 12, 13},
Task: Task{

View File

@ -110,222 +110,222 @@ func (err ValidationHTTPError) Error() string {
}
// ===========
// List errors
// Project errors
// ===========
// ErrListDoesNotExist represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist.
type ErrListDoesNotExist struct {
// ErrProjectDoesNotExist represents a "ErrProjectDoesNotExist" kind of error. Used if the project does not exist.
type ErrProjectDoesNotExist struct {
ID int64
}
// IsErrListDoesNotExist checks if an error is a ErrListDoesNotExist.
func IsErrListDoesNotExist(err error) bool {
_, ok := err.(ErrListDoesNotExist)
// IsErrProjectDoesNotExist checks if an error is a ErrProjectDoesNotExist.
func IsErrProjectDoesNotExist(err error) bool {
_, ok := err.(ErrProjectDoesNotExist)
return ok
}
func (err ErrListDoesNotExist) Error() string {
return fmt.Sprintf("List does not exist [ID: %d]", err.ID)
func (err ErrProjectDoesNotExist) Error() string {
return fmt.Sprintf("Project does not exist [ID: %d]", err.ID)
}
// ErrCodeListDoesNotExist holds the unique world-error code of this error
const ErrCodeListDoesNotExist = 3001
// ErrCodeProjectDoesNotExist holds the unique world-error code of this error
const ErrCodeProjectDoesNotExist = 3001
// HTTPError holds the http error description
func (err ErrListDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeListDoesNotExist, Message: "This list does not exist."}
func (err ErrProjectDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeProjectDoesNotExist, Message: "This project does not exist."}
}
// ErrNeedToHaveListReadAccess represents an error, where the user dont has read access to that List
type ErrNeedToHaveListReadAccess struct {
ListID int64
UserID int64
// ErrNeedToHaveProjectReadAccess represents an error, where the user dont has read access to that Project
type ErrNeedToHaveProjectReadAccess struct {
ProjectID int64
UserID int64
}
// IsErrNeedToHaveListReadAccess checks if an error is a ErrListDoesNotExist.
func IsErrNeedToHaveListReadAccess(err error) bool {
_, ok := err.(ErrNeedToHaveListReadAccess)
// IsErrNeedToHaveProjectReadAccess checks if an error is a ErrProjectDoesNotExist.
func IsErrNeedToHaveProjectReadAccess(err error) bool {
_, ok := err.(ErrNeedToHaveProjectReadAccess)
return ok
}
func (err ErrNeedToHaveListReadAccess) Error() string {
return fmt.Sprintf("User needs to have read access to that list [ListID: %d, UserID: %d]", err.ListID, err.UserID)
func (err ErrNeedToHaveProjectReadAccess) Error() string {
return fmt.Sprintf("User needs to have read access to that project [ProjectID: %d, UserID: %d]", err.ProjectID, err.UserID)
}
// ErrCodeNeedToHaveListReadAccess holds the unique world-error code of this error
const ErrCodeNeedToHaveListReadAccess = 3004
// ErrCodeNeedToHaveProjectReadAccess holds the unique world-error code of this error
const ErrCodeNeedToHaveProjectReadAccess = 3004
// HTTPError holds the http error description
func (err ErrNeedToHaveListReadAccess) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeNeedToHaveListReadAccess, Message: "You need to have read access to this list."}
func (err ErrNeedToHaveProjectReadAccess) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeNeedToHaveProjectReadAccess, Message: "You need to have read access to this project."}
}
// ErrListTitleCannotBeEmpty represents a "ErrListTitleCannotBeEmpty" kind of error. Used if the list does not exist.
type ErrListTitleCannotBeEmpty struct{}
// ErrProjectTitleCannotBeEmpty represents a "ErrProjectTitleCannotBeEmpty" kind of error. Used if the project does not exist.
type ErrProjectTitleCannotBeEmpty struct{}
// IsErrListTitleCannotBeEmpty checks if an error is a ErrListTitleCannotBeEmpty.
func IsErrListTitleCannotBeEmpty(err error) bool {
_, ok := err.(ErrListTitleCannotBeEmpty)
// IsErrProjectTitleCannotBeEmpty checks if an error is a ErrProjectTitleCannotBeEmpty.
func IsErrProjectTitleCannotBeEmpty(err error) bool {
_, ok := err.(ErrProjectTitleCannotBeEmpty)
return ok
}
func (err ErrListTitleCannotBeEmpty) Error() string {
return "List title cannot be empty."
func (err ErrProjectTitleCannotBeEmpty) Error() string {
return "Project title cannot be empty."
}
// ErrCodeListTitleCannotBeEmpty holds the unique world-error code of this error
const ErrCodeListTitleCannotBeEmpty = 3005
// ErrCodeProjectTitleCannotBeEmpty holds the unique world-error code of this error
const ErrCodeProjectTitleCannotBeEmpty = 3005
// HTTPError holds the http error description
func (err ErrListTitleCannotBeEmpty) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeListTitleCannotBeEmpty, Message: "You must provide at least a list title."}
func (err ErrProjectTitleCannotBeEmpty) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeProjectTitleCannotBeEmpty, Message: "You must provide at least a project title."}
}
// ErrListShareDoesNotExist represents a "ErrListShareDoesNotExist" kind of error. Used if the list share does not exist.
type ErrListShareDoesNotExist struct {
// ErrProjectShareDoesNotExist represents a "ErrProjectShareDoesNotExist" kind of error. Used if the project share does not exist.
type ErrProjectShareDoesNotExist struct {
ID int64
Hash string
}
// IsErrListShareDoesNotExist checks if an error is a ErrListShareDoesNotExist.
func IsErrListShareDoesNotExist(err error) bool {
_, ok := err.(ErrListShareDoesNotExist)
// IsErrProjectShareDoesNotExist checks if an error is a ErrProjectShareDoesNotExist.
func IsErrProjectShareDoesNotExist(err error) bool {
_, ok := err.(ErrProjectShareDoesNotExist)
return ok
}
func (err ErrListShareDoesNotExist) Error() string {
return "List share does not exist."
func (err ErrProjectShareDoesNotExist) Error() string {
return "Project share does not exist."
}
// ErrCodeListShareDoesNotExist holds the unique world-error code of this error
const ErrCodeListShareDoesNotExist = 3006
// ErrCodeProjectShareDoesNotExist holds the unique world-error code of this error
const ErrCodeProjectShareDoesNotExist = 3006
// HTTPError holds the http error description
func (err ErrListShareDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeListShareDoesNotExist, Message: "The list share does not exist."}
func (err ErrProjectShareDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeProjectShareDoesNotExist, Message: "The project share does not exist."}
}
// ErrListIdentifierIsNotUnique represents a "ErrListIdentifierIsNotUnique" kind of error. Used if the provided list identifier is not unique.
type ErrListIdentifierIsNotUnique struct {
// ErrProjectIdentifierIsNotUnique represents a "ErrProjectIdentifierIsNotUnique" kind of error. Used if the provided project identifier is not unique.
type ErrProjectIdentifierIsNotUnique struct {
Identifier string
}
// IsErrListIdentifierIsNotUnique checks if an error is a ErrListIdentifierIsNotUnique.
func IsErrListIdentifierIsNotUnique(err error) bool {
_, ok := err.(ErrListIdentifierIsNotUnique)
// IsErrProjectIdentifierIsNotUnique checks if an error is a ErrProjectIdentifierIsNotUnique.
func IsErrProjectIdentifierIsNotUnique(err error) bool {
_, ok := err.(ErrProjectIdentifierIsNotUnique)
return ok
}
func (err ErrListIdentifierIsNotUnique) Error() string {
return "List identifier is not unique."
func (err ErrProjectIdentifierIsNotUnique) Error() string {
return "Project identifier is not unique."
}
// ErrCodeListIdentifierIsNotUnique holds the unique world-error code of this error
const ErrCodeListIdentifierIsNotUnique = 3007
// ErrCodeProjectIdentifierIsNotUnique holds the unique world-error code of this error
const ErrCodeProjectIdentifierIsNotUnique = 3007
// HTTPError holds the http error description
func (err ErrListIdentifierIsNotUnique) HTTPError() web.HTTPError {
func (err ErrProjectIdentifierIsNotUnique) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusBadRequest,
Code: ErrCodeListIdentifierIsNotUnique,
Message: "A list with this identifier already exists.",
Code: ErrCodeProjectIdentifierIsNotUnique,
Message: "A project with this identifier already exists.",
}
}
// ErrListIsArchived represents an error, where a list is archived
type ErrListIsArchived struct {
ListID int64
// ErrProjectIsArchived represents an error, where a project is archived
type ErrProjectIsArchived struct {
ProjectID int64
}
// IsErrListIsArchived checks if an error is a list is archived error.
func IsErrListIsArchived(err error) bool {
_, ok := err.(ErrListIsArchived)
// IsErrProjectIsArchived checks if an error is a project is archived error.
func IsErrProjectIsArchived(err error) bool {
_, ok := err.(ErrProjectIsArchived)
return ok
}
func (err ErrListIsArchived) Error() string {
return fmt.Sprintf("List is archived [ListID: %d]", err.ListID)
func (err ErrProjectIsArchived) Error() string {
return fmt.Sprintf("Project is archived [ProjectID: %d]", err.ProjectID)
}
// ErrCodeListIsArchived holds the unique world-error code of this error
const ErrCodeListIsArchived = 3008
// ErrCodeProjectIsArchived holds the unique world-error code of this error
const ErrCodeProjectIsArchived = 3008
// HTTPError holds the http error description
func (err ErrListIsArchived) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeListIsArchived, Message: "This list is archived. Editing or creating new tasks is not possible."}
func (err ErrProjectIsArchived) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeProjectIsArchived, Message: "This project is archived. Editing or creating new tasks is not possible."}
}
// ErrListCannotBelongToAPseudoNamespace represents an error where a list cannot belong to a pseudo namespace
type ErrListCannotBelongToAPseudoNamespace struct {
ListID int64
// ErrProjectCannotBelongToAPseudoNamespace represents an error where a project cannot belong to a pseudo namespace
type ErrProjectCannotBelongToAPseudoNamespace struct {
ProjectID int64
NamespaceID int64
}
// IsErrListCannotBelongToAPseudoNamespace checks if an error is a list is archived error.
func IsErrListCannotBelongToAPseudoNamespace(err error) bool {
_, ok := err.(*ErrListCannotBelongToAPseudoNamespace)
// IsErrProjectCannotBelongToAPseudoNamespace checks if an error is a project is archived error.
func IsErrProjectCannotBelongToAPseudoNamespace(err error) bool {
_, ok := err.(*ErrProjectCannotBelongToAPseudoNamespace)
return ok
}
func (err *ErrListCannotBelongToAPseudoNamespace) Error() string {
return fmt.Sprintf("List cannot belong to a pseudo namespace [ListID: %d, NamespaceID: %d]", err.ListID, err.NamespaceID)
func (err *ErrProjectCannotBelongToAPseudoNamespace) Error() string {
return fmt.Sprintf("Project cannot belong to a pseudo namespace [ProjectID: %d, NamespaceID: %d]", err.ProjectID, err.NamespaceID)
}
// ErrCodeListCannotBelongToAPseudoNamespace holds the unique world-error code of this error
const ErrCodeListCannotBelongToAPseudoNamespace = 3009
// ErrCodeProjectCannotBelongToAPseudoNamespace holds the unique world-error code of this error
const ErrCodeProjectCannotBelongToAPseudoNamespace = 3009
// HTTPError holds the http error description
func (err *ErrListCannotBelongToAPseudoNamespace) HTTPError() web.HTTPError {
func (err *ErrProjectCannotBelongToAPseudoNamespace) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeListCannotBelongToAPseudoNamespace,
Message: "This list cannot belong a dynamically generated namespace.",
Code: ErrCodeProjectCannotBelongToAPseudoNamespace,
Message: "This project cannot belong a dynamically generated namespace.",
}
}
// ErrListMustBelongToANamespace represents an error where a list must belong to a namespace
type ErrListMustBelongToANamespace struct {
ListID int64
// ErrProjectMustBelongToANamespace represents an error where a project must belong to a namespace
type ErrProjectMustBelongToANamespace struct {
ProjectID int64
NamespaceID int64
}
// IsErrListMustBelongToANamespace checks if an error is a list must belong to a namespace error.
func IsErrListMustBelongToANamespace(err error) bool {
_, ok := err.(*ErrListMustBelongToANamespace)
// IsErrProjectMustBelongToANamespace checks if an error is a project must belong to a namespace error.
func IsErrProjectMustBelongToANamespace(err error) bool {
_, ok := err.(*ErrProjectMustBelongToANamespace)
return ok
}
func (err *ErrListMustBelongToANamespace) Error() string {
return fmt.Sprintf("List must belong to a namespace [ListID: %d, NamespaceID: %d]", err.ListID, err.NamespaceID)
func (err *ErrProjectMustBelongToANamespace) Error() string {
return fmt.Sprintf("Project must belong to a namespace [ProjectID: %d, NamespaceID: %d]", err.ProjectID, err.NamespaceID)
}
// ErrCodeListMustBelongToANamespace holds the unique world-error code of this error
const ErrCodeListMustBelongToANamespace = 3010
// ErrCodeProjectMustBelongToANamespace holds the unique world-error code of this error
const ErrCodeProjectMustBelongToANamespace = 3010
// HTTPError holds the http error description
func (err *ErrListMustBelongToANamespace) HTTPError() web.HTTPError {
func (err *ErrProjectMustBelongToANamespace) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeListMustBelongToANamespace,
Message: "This list must belong to a namespace.",
Code: ErrCodeProjectMustBelongToANamespace,
Message: "This project must belong to a namespace.",
}
}
// ================
// List task errors
// Project task errors
// ================
// ErrTaskCannotBeEmpty represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist.
// ErrTaskCannotBeEmpty represents a "ErrProjectDoesNotExist" kind of error. Used if the project does not exist.
type ErrTaskCannotBeEmpty struct{}
// IsErrTaskCannotBeEmpty checks if an error is a ErrListDoesNotExist.
// IsErrTaskCannotBeEmpty checks if an error is a ErrProjectDoesNotExist.
func IsErrTaskCannotBeEmpty(err error) bool {
_, ok := err.(ErrTaskCannotBeEmpty)
return ok
}
func (err ErrTaskCannotBeEmpty) Error() string {
return "List task title cannot be empty."
return "Project task title cannot be empty."
}
// ErrCodeTaskCannotBeEmpty holds the unique world-error code of this error
@ -333,22 +333,22 @@ const ErrCodeTaskCannotBeEmpty = 4001
// HTTPError holds the http error description
func (err ErrTaskCannotBeEmpty) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeTaskCannotBeEmpty, Message: "You must provide at least a list task title."}
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeTaskCannotBeEmpty, Message: "You must provide at least a project task title."}
}
// ErrTaskDoesNotExist represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist.
// ErrTaskDoesNotExist represents a "ErrProjectDoesNotExist" kind of error. Used if the project does not exist.
type ErrTaskDoesNotExist struct {
ID int64
}
// IsErrTaskDoesNotExist checks if an error is a ErrListDoesNotExist.
// IsErrTaskDoesNotExist checks if an error is a ErrProjectDoesNotExist.
func IsErrTaskDoesNotExist(err error) bool {
_, ok := err.(ErrTaskDoesNotExist)
return ok
}
func (err ErrTaskDoesNotExist) Error() string {
return fmt.Sprintf("List task does not exist. [ID: %d]", err.ID)
return fmt.Sprintf("Project task does not exist. [ID: %d]", err.ID)
}
// ErrCodeTaskDoesNotExist holds the unique world-error code of this error
@ -359,28 +359,28 @@ func (err ErrTaskDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTaskDoesNotExist, Message: "This task does not exist"}
}
// ErrBulkTasksMustBeInSameList represents a "ErrBulkTasksMustBeInSameList" kind of error.
type ErrBulkTasksMustBeInSameList struct {
// ErrBulkTasksMustBeInSameProject represents a "ErrBulkTasksMustBeInSameProject" kind of error.
type ErrBulkTasksMustBeInSameProject struct {
ShouldBeID int64
IsID int64
}
// IsErrBulkTasksMustBeInSameList checks if an error is a ErrBulkTasksMustBeInSameList.
func IsErrBulkTasksMustBeInSameList(err error) bool {
_, ok := err.(ErrBulkTasksMustBeInSameList)
// IsErrBulkTasksMustBeInSameProject checks if an error is a ErrBulkTasksMustBeInSameProject.
func IsErrBulkTasksMustBeInSameProject(err error) bool {
_, ok := err.(ErrBulkTasksMustBeInSameProject)
return ok
}
func (err ErrBulkTasksMustBeInSameList) Error() string {
return fmt.Sprintf("All bulk editing tasks must be in the same list. [Should be: %d, is: %d]", err.ShouldBeID, err.IsID)
func (err ErrBulkTasksMustBeInSameProject) Error() string {
return fmt.Sprintf("All bulk editing tasks must be in the same project. [Should be: %d, is: %d]", err.ShouldBeID, err.IsID)
}
// ErrCodeBulkTasksMustBeInSameList holds the unique world-error code of this error
const ErrCodeBulkTasksMustBeInSameList = 4003
// ErrCodeBulkTasksMustBeInSameProject holds the unique world-error code of this error
const ErrCodeBulkTasksMustBeInSameProject = 4003
// HTTPError holds the http error description
func (err ErrBulkTasksMustBeInSameList) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeBulkTasksMustBeInSameList, Message: "All tasks must be in the same list."}
func (err ErrBulkTasksMustBeInSameProject) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeBulkTasksMustBeInSameProject, Message: "All tasks must be in the same project."}
}
// ErrBulkTasksNeedAtLeastOne represents a "ErrBulkTasksNeedAtLeastOne" kind of error.
@ -1042,7 +1042,7 @@ const ErrCodeNamespaceIsArchived = 5012
// HTTPError holds the http error description
func (err ErrNamespaceIsArchived) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeNamespaceIsArchived, Message: "This namespaces is archived. Editing or creating new lists is not possible."}
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeNamespaceIsArchived, Message: "This namespaces is archived. Editing or creating new projects is not possible."}
}
// ============
@ -1095,7 +1095,7 @@ func (err ErrTeamDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "This team does not exist."}
}
// ErrTeamAlreadyHasAccess represents an error where a team already has access to a list/namespace
// ErrTeamAlreadyHasAccess represents an error where a team already has access to a project/namespace
type ErrTeamAlreadyHasAccess struct {
TeamID int64
ID int64
@ -1167,38 +1167,38 @@ func (err ErrCannotDeleteLastTeamMember) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeCannotDeleteLastTeamMember, Message: "You cannot delete the last member of a team."}
}
// ErrTeamDoesNotHaveAccessToList represents an error, where the Team is not the owner of that List (used i.e. when deleting a List)
type ErrTeamDoesNotHaveAccessToList struct {
ListID int64
TeamID int64
// ErrTeamDoesNotHaveAccessToProject represents an error, where the Team is not the owner of that Project (used i.e. when deleting a Project)
type ErrTeamDoesNotHaveAccessToProject struct {
ProjectID int64
TeamID int64
}
// IsErrTeamDoesNotHaveAccessToList checks if an error is a ErrListDoesNotExist.
func IsErrTeamDoesNotHaveAccessToList(err error) bool {
_, ok := err.(ErrTeamDoesNotHaveAccessToList)
// IsErrTeamDoesNotHaveAccessToProject checks if an error is a ErrProjectDoesNotExist.
func IsErrTeamDoesNotHaveAccessToProject(err error) bool {
_, ok := err.(ErrTeamDoesNotHaveAccessToProject)
return ok
}
func (err ErrTeamDoesNotHaveAccessToList) Error() string {
return fmt.Sprintf("Team does not have access to the list [ListID: %d, TeamID: %d]", err.ListID, err.TeamID)
func (err ErrTeamDoesNotHaveAccessToProject) Error() string {
return fmt.Sprintf("Team does not have access to the project [ProjectID: %d, TeamID: %d]", err.ProjectID, err.TeamID)
}
// ErrCodeTeamDoesNotHaveAccessToList holds the unique world-error code of this error
const ErrCodeTeamDoesNotHaveAccessToList = 6007
// ErrCodeTeamDoesNotHaveAccessToProject holds the unique world-error code of this error
const ErrCodeTeamDoesNotHaveAccessToProject = 6007
// HTTPError holds the http error description
func (err ErrTeamDoesNotHaveAccessToList) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToList, Message: "This team does not have access to the list."}
func (err ErrTeamDoesNotHaveAccessToProject) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToProject, Message: "This team does not have access to the project."}
}
// ====================
// User <-> List errors
// User <-> Project errors
// ====================
// ErrUserAlreadyHasAccess represents an error where a user already has access to a list/namespace
// ErrUserAlreadyHasAccess represents an error where a user already has access to a project/namespace
type ErrUserAlreadyHasAccess struct {
UserID int64
ListID int64
UserID int64
ProjectID int64
}
// IsErrUserAlreadyHasAccess checks if an error is ErrUserAlreadyHasAccess.
@ -1208,7 +1208,7 @@ func IsErrUserAlreadyHasAccess(err error) bool {
}
func (err ErrUserAlreadyHasAccess) Error() string {
return fmt.Sprintf("User already has access to that list. [User ID: %d, List ID: %d]", err.UserID, err.ListID)
return fmt.Sprintf("User already has access to that project. [User ID: %d, Project ID: %d]", err.UserID, err.ProjectID)
}
// ErrCodeUserAlreadyHasAccess holds the unique world-error code of this error
@ -1216,31 +1216,31 @@ const ErrCodeUserAlreadyHasAccess = 7002
// HTTPError holds the http error description
func (err ErrUserAlreadyHasAccess) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserAlreadyHasAccess, Message: "This user already has access to this list."}
return web.HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserAlreadyHasAccess, Message: "This user already has access to this project."}
}
// ErrUserDoesNotHaveAccessToList represents an error, where the user is not the owner of that List (used i.e. when deleting a List)
type ErrUserDoesNotHaveAccessToList struct {
ListID int64
UserID int64
// ErrUserDoesNotHaveAccessToProject represents an error, where the user is not the owner of that Project (used i.e. when deleting a Project)
type ErrUserDoesNotHaveAccessToProject struct {
ProjectID int64
UserID int64
}
// IsErrUserDoesNotHaveAccessToList checks if an error is a ErrListDoesNotExist.
func IsErrUserDoesNotHaveAccessToList(err error) bool {
_, ok := err.(ErrUserDoesNotHaveAccessToList)
// IsErrUserDoesNotHaveAccessToProject checks if an error is a ErrProjectDoesNotExist.
func IsErrUserDoesNotHaveAccessToProject(err error) bool {
_, ok := err.(ErrUserDoesNotHaveAccessToProject)
return ok
}
func (err ErrUserDoesNotHaveAccessToList) Error() string {
return fmt.Sprintf("User does not have access to the list [ListID: %d, UserID: %d]", err.ListID, err.UserID)
func (err ErrUserDoesNotHaveAccessToProject) Error() string {
return fmt.Sprintf("User does not have access to the project [ProjectID: %d, UserID: %d]", err.ProjectID, err.UserID)
}
// ErrCodeUserDoesNotHaveAccessToList holds the unique world-error code of this error
const ErrCodeUserDoesNotHaveAccessToList = 7003
// ErrCodeUserDoesNotHaveAccessToProject holds the unique world-error code of this error
const ErrCodeUserDoesNotHaveAccessToProject = 7003
// HTTPError holds the http error description
func (err ErrUserDoesNotHaveAccessToList) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToList, Message: "This user does not have access to the list."}
func (err ErrUserDoesNotHaveAccessToProject) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToProject, Message: "This user does not have access to the project."}
}
// =============
@ -1392,38 +1392,38 @@ func (err ErrBucketDoesNotExist) HTTPError() web.HTTPError {
}
}
// ErrBucketDoesNotBelongToList represents an error where a kanban bucket does not belong to a list
type ErrBucketDoesNotBelongToList struct {
BucketID int64
ListID int64
// ErrBucketDoesNotBelongToProject represents an error where a kanban bucket does not belong to a project
type ErrBucketDoesNotBelongToProject struct {
BucketID int64
ProjectID int64
}
// IsErrBucketDoesNotBelongToList checks if an error is ErrBucketDoesNotBelongToList.
func IsErrBucketDoesNotBelongToList(err error) bool {
_, ok := err.(ErrBucketDoesNotBelongToList)
// IsErrBucketDoesNotBelongToProject checks if an error is ErrBucketDoesNotBelongToProject.
func IsErrBucketDoesNotBelongToProject(err error) bool {
_, ok := err.(ErrBucketDoesNotBelongToProject)
return ok
}
func (err ErrBucketDoesNotBelongToList) Error() string {
return fmt.Sprintf("Bucket does not not belong to list [BucketID: %d, ListID: %d]", err.BucketID, err.ListID)
func (err ErrBucketDoesNotBelongToProject) Error() string {
return fmt.Sprintf("Bucket does not not belong to project [BucketID: %d, ProjectID: %d]", err.BucketID, err.ProjectID)
}
// ErrCodeBucketDoesNotBelongToList holds the unique world-error code of this error
const ErrCodeBucketDoesNotBelongToList = 10002
// ErrCodeBucketDoesNotBelongToProject holds the unique world-error code of this error
const ErrCodeBucketDoesNotBelongToProject = 10002
// HTTPError holds the http error description
func (err ErrBucketDoesNotBelongToList) HTTPError() web.HTTPError {
func (err ErrBucketDoesNotBelongToProject) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusBadRequest,
Code: ErrCodeBucketDoesNotBelongToList,
Message: "This bucket does not belong to that list.",
Code: ErrCodeBucketDoesNotBelongToProject,
Message: "This bucket does not belong to that project.",
}
}
// ErrCannotRemoveLastBucket represents an error where a kanban bucket is the last on a list and thus cannot be removed.
// ErrCannotRemoveLastBucket represents an error where a kanban bucket is the last on a project and thus cannot be removed.
type ErrCannotRemoveLastBucket struct {
BucketID int64
ListID int64
BucketID int64
ProjectID int64
}
// IsErrCannotRemoveLastBucket checks if an error is ErrCannotRemoveLastBucket.
@ -1433,7 +1433,7 @@ func IsErrCannotRemoveLastBucket(err error) bool {
}
func (err ErrCannotRemoveLastBucket) Error() string {
return fmt.Sprintf("Cannot remove last bucket of list [BucketID: %d, ListID: %d]", err.BucketID, err.ListID)
return fmt.Sprintf("Cannot remove last bucket of project [BucketID: %d, ProjectID: %d]", err.BucketID, err.ProjectID)
}
// ErrCodeCannotRemoveLastBucket holds the unique world-error code of this error
@ -1444,7 +1444,7 @@ func (err ErrCannotRemoveLastBucket) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeCannotRemoveLastBucket,
Message: "You cannot remove the last bucket on this list.",
Message: "You cannot remove the last bucket on this project.",
}
}
@ -1477,32 +1477,32 @@ func (err ErrBucketLimitExceeded) HTTPError() web.HTTPError {
}
}
// ErrOnlyOneDoneBucketPerList represents an error where a bucket is set to the done bucket but one already exists for its list.
type ErrOnlyOneDoneBucketPerList struct {
// ErrOnlyOneDoneBucketPerProject represents an error where a bucket is set to the done bucket but one already exists for its project.
type ErrOnlyOneDoneBucketPerProject struct {
BucketID int64
ListID int64
ProjectID int64
DoneBucketID int64
}
// IsErrOnlyOneDoneBucketPerList checks if an error is ErrBucketLimitExceeded.
func IsErrOnlyOneDoneBucketPerList(err error) bool {
_, ok := err.(*ErrOnlyOneDoneBucketPerList)
// IsErrOnlyOneDoneBucketPerProject checks if an error is ErrBucketLimitExceeded.
func IsErrOnlyOneDoneBucketPerProject(err error) bool {
_, ok := err.(*ErrOnlyOneDoneBucketPerProject)
return ok
}
func (err *ErrOnlyOneDoneBucketPerList) Error() string {
return fmt.Sprintf("There can be only one done bucket per list [BucketID: %d, ListID: %d]", err.BucketID, err.ListID)
func (err *ErrOnlyOneDoneBucketPerProject) Error() string {
return fmt.Sprintf("There can be only one done bucket per project [BucketID: %d, ProjectID: %d]", err.BucketID, err.ProjectID)
}
// ErrCodeOnlyOneDoneBucketPerList holds the unique world-error code of this error
const ErrCodeOnlyOneDoneBucketPerList = 10005
// ErrCodeOnlyOneDoneBucketPerProject holds the unique world-error code of this error
const ErrCodeOnlyOneDoneBucketPerProject = 10005
// HTTPError holds the http error description
func (err *ErrOnlyOneDoneBucketPerList) HTTPError() web.HTTPError {
func (err *ErrOnlyOneDoneBucketPerProject) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeOnlyOneDoneBucketPerList,
Message: "There can be only one done bucket per list.",
Code: ErrCodeOnlyOneDoneBucketPerProject,
Message: "There can be only one done bucket per project.",
}
}

View File

@ -80,6 +80,18 @@ func (t *TaskAssigneeCreatedEvent) Name() string {
return "task.assignee.created"
}
// TaskAssigneeDeletedEvent represents a TaskAssigneeDeletedEvent event
type TaskAssigneeDeletedEvent struct {
Task *Task
Assignee *user.User
Doer *user.User
}
// Name defines the name for TaskAssigneeDeletedEvent
func (t *TaskAssigneeDeletedEvent) Name() string {
return "task.assignee.deleted"
}
// TaskCommentCreatedEvent represents an event where a task comment has been created
type TaskCommentCreatedEvent struct {
Task *Task
@ -104,6 +116,66 @@ func (t *TaskCommentUpdatedEvent) Name() string {
return "task.comment.edited"
}
// TaskCommentDeletedEvent represents a TaskCommentDeletedEvent event
type TaskCommentDeletedEvent struct {
Task *Task
Comment *TaskComment
Doer *user.User
}
// Name defines the name for TaskCommentDeletedEvent
func (t *TaskCommentDeletedEvent) Name() string {
return "task.comment.deleted"
}
// TaskAttachmentCreatedEvent represents a TaskAttachmentCreatedEvent event
type TaskAttachmentCreatedEvent struct {
Task *Task
Attachment *TaskAttachment
Doer *user.User
}
// Name defines the name for TaskAttachmentCreatedEvent
func (t *TaskAttachmentCreatedEvent) Name() string {
return "task.attachment.created"
}
// TaskAttachmentDeletedEvent represents a TaskAttachmentDeletedEvent event
type TaskAttachmentDeletedEvent struct {
Task *Task
Attachment *TaskAttachment
Doer *user.User
}
// Name defines the name for TaskAttachmentDeletedEvent
func (t *TaskAttachmentDeletedEvent) Name() string {
return "task.attachment.deleted"
}
// TaskRelationCreatedEvent represents a TaskRelationCreatedEvent event
type TaskRelationCreatedEvent struct {
Task *Task
Relation *TaskRelation
Doer *user.User
}
// Name defines the name for TaskRelationCreatedEvent
func (t *TaskRelationCreatedEvent) Name() string {
return "task.relation.created"
}
// TaskRelationDeletedEvent represents a TaskRelationDeletedEvent event
type TaskRelationDeletedEvent struct {
Task *Task
Relation *TaskRelation
Doer *user.User
}
// Name defines the name for TaskRelationDeletedEvent
func (t *TaskRelationDeletedEvent) Name() string {
return "task.relation.deleted"
}
//////////////////////
// Namespace Events //
//////////////////////
@ -142,68 +214,68 @@ func (t *NamespaceDeletedEvent) Name() string {
}
/////////////////
// List Events //
// Project Events //
/////////////////
// ListCreatedEvent represents an event where a list has been created
type ListCreatedEvent struct {
List *List
Doer *user.User
// ProjectCreatedEvent represents an event where a project has been created
type ProjectCreatedEvent struct {
Project *Project
Doer *user.User
}
// Name defines the name for ListCreatedEvent
func (l *ListCreatedEvent) Name() string {
return "list.created"
// Name defines the name for ProjectCreatedEvent
func (l *ProjectCreatedEvent) Name() string {
return "project.created"
}
// ListUpdatedEvent represents an event where a list has been updated
type ListUpdatedEvent struct {
List *List
Doer web.Auth
// ProjectUpdatedEvent represents an event where a project has been updated
type ProjectUpdatedEvent struct {
Project *Project
Doer web.Auth
}
// Name defines the name for ListUpdatedEvent
func (l *ListUpdatedEvent) Name() string {
return "list.updated"
// Name defines the name for ProjectUpdatedEvent
func (l *ProjectUpdatedEvent) Name() string {
return "project.updated"
}
// ListDeletedEvent represents an event where a list has been deleted
type ListDeletedEvent struct {
List *List
Doer web.Auth
// ProjectDeletedEvent represents an event where a project has been deleted
type ProjectDeletedEvent struct {
Project *Project
Doer web.Auth
}
// Name defines the name for ListDeletedEvent
func (t *ListDeletedEvent) Name() string {
return "list.deleted"
// Name defines the name for ProjectDeletedEvent
func (t *ProjectDeletedEvent) Name() string {
return "project.deleted"
}
////////////////////
// Sharing Events //
////////////////////
// ListSharedWithUserEvent represents an event where a list has been shared with a user
type ListSharedWithUserEvent struct {
List *List
User *user.User
Doer web.Auth
// ProjectSharedWithUserEvent represents an event where a project has been shared with a user
type ProjectSharedWithUserEvent struct {
Project *Project
User *user.User
Doer web.Auth
}
// Name defines the name for ListSharedWithUserEvent
func (l *ListSharedWithUserEvent) Name() string {
return "list.shared.user"
// Name defines the name for ProjectSharedWithUserEvent
func (l *ProjectSharedWithUserEvent) Name() string {
return "project.shared.user"
}
// ListSharedWithTeamEvent represents an event where a list has been shared with a team
type ListSharedWithTeamEvent struct {
List *List
Team *Team
Doer web.Auth
// ProjectSharedWithTeamEvent represents an event where a project has been shared with a team
type ProjectSharedWithTeamEvent struct {
Project *Project
Team *Team
Doer web.Auth
}
// Name defines the name for ListSharedWithTeamEvent
func (l *ListSharedWithTeamEvent) Name() string {
return "list.shared.team"
// Name defines the name for ProjectSharedWithTeamEvent
func (l *ProjectSharedWithTeamEvent) Name() string {
return "project.shared.team"
}
// NamespaceSharedWithUserEvent represents an event where a namespace has been shared with a user

View File

@ -57,7 +57,7 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
defer dumpWriter.Close()
// Get the data
err = exportListsAndTasks(s, u, dumpWriter)
err = exportProjectsAndTasks(s, u, dumpWriter)
if err != nil {
return err
}
@ -72,7 +72,7 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
return err
}
// Background files
err = exportListBackgrounds(s, u, dumpWriter)
err = exportProjectBackgrounds(s, u, dumpWriter)
if err != nil {
return err
}
@ -97,7 +97,7 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
return err
}
exportFile, err := files.CreateWithMimeAndSession(s, exported, tmpFilename, uint64(stat.Size()), u, "application/zip")
exportFile, err := files.CreateWithMimeAndSession(s, exported, tmpFilename, uint64(stat.Size()), u, "application/zip", false)
if err != nil {
return err
}
@ -121,7 +121,7 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
})
}
func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
namspaces, _, _, err := (&Namespace{IsArchived: true}).ReadAll(s, u, "", -1, 0)
if err != nil {
@ -129,29 +129,29 @@ func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err err
}
namespaceIDs := []int64{}
namespaces := []*NamespaceWithListsAndTasks{}
listMap := make(map[int64]*ListWithTasksAndBuckets)
listIDs := []int64{}
for _, n := range namspaces.([]*NamespaceWithLists) {
namespaces := []*NamespaceWithProjectsAndTasks{}
projectMap := make(map[int64]*ProjectWithTasksAndBuckets)
projectIDs := []int64{}
for _, n := range namspaces.([]*NamespaceWithProjects) {
if n.ID < 1 {
// Don't include filters
continue
}
nn := &NamespaceWithListsAndTasks{
nn := &NamespaceWithProjectsAndTasks{
Namespace: n.Namespace,
Lists: []*ListWithTasksAndBuckets{},
Projects: []*ProjectWithTasksAndBuckets{},
}
for _, l := range n.Lists {
ll := &ListWithTasksAndBuckets{
List: *l,
for _, l := range n.Projects {
ll := &ProjectWithTasksAndBuckets{
Project: *l,
BackgroundFileID: l.BackgroundFileID,
Tasks: []*TaskWithComments{},
}
nn.Lists = append(nn.Lists, ll)
listMap[l.ID] = ll
listIDs = append(listIDs, l.ID)
nn.Projects = append(nn.Projects, ll)
projectMap[l.ID] = ll
projectIDs = append(projectIDs, l.ID)
}
namespaceIDs = append(namespaceIDs, n.ID)
@ -162,13 +162,13 @@ func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err err
return nil
}
// Get all lists
lists, err := getListsForNamespaces(s, namespaceIDs, true)
// Get all projects
projects, err := getProjectsForNamespaces(s, namespaceIDs, true)
if err != nil {
return err
}
tasks, _, _, err := getTasksForLists(s, lists, u, &taskOptions{
tasks, _, _, err := getTasksForProjects(s, projects, u, &taskOptions{
page: 0,
perPage: -1,
})
@ -181,17 +181,17 @@ func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err err
taskMap[t.ID] = &TaskWithComments{
Task: *t,
}
if _, exists := listMap[t.ListID]; !exists {
log.Debugf("[User Data Export] List %d does not exist for task %d, omitting", t.ListID, t.ID)
if _, exists := projectMap[t.ProjectID]; !exists {
log.Debugf("[User Data Export] Project %d does not exist for task %d, omitting", t.ProjectID, t.ID)
continue
}
listMap[t.ListID].Tasks = append(listMap[t.ListID].Tasks, taskMap[t.ID])
projectMap[t.ProjectID].Tasks = append(projectMap[t.ProjectID].Tasks, taskMap[t.ID])
}
comments := []*TaskComment{}
err = s.
Join("LEFT", "tasks", "tasks.id = task_comments.task_id").
In("tasks.list_id", listIDs).
In("tasks.project_id", projectIDs).
Find(&comments)
if err != nil {
return
@ -206,17 +206,17 @@ func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err err
}
buckets := []*Bucket{}
err = s.In("list_id", listIDs).Find(&buckets)
err = s.In("project_id", projectIDs).Find(&buckets)
if err != nil {
return
}
for _, b := range buckets {
if _, exists := listMap[b.ListID]; !exists {
log.Debugf("[User Data Export] List %d does not exist for bucket %d, omitting", b.ListID, b.ID)
if _, exists := projectMap[b.ProjectID]; !exists {
log.Debugf("[User Data Export] Project %d does not exist for bucket %d, omitting", b.ProjectID, b.ID)
continue
}
listMap[b.ListID].Buckets = append(listMap[b.ListID].Buckets, b)
projectMap[b.ProjectID].Buckets = append(projectMap[b.ProjectID].Buckets, b)
}
data, err := json.Marshal(namespaces)
@ -228,9 +228,9 @@ func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err err
}
func exportTaskAttachments(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
lists, _, _, err := getRawListsForUser(
projects, _, _, err := getRawProjectsForUser(
s,
&listOptions{
&projectOptions{
user: u,
page: -1,
},
@ -239,7 +239,7 @@ func exportTaskAttachments(s *xorm.Session, u *user.User, wr *zip.Writer) (err e
return err
}
tasks, _, _, err := getRawTasksForLists(s, lists, u, &taskOptions{page: -1})
tasks, _, _, err := getRawTasksForProjects(s, projects, u, &taskOptions{page: -1})
if err != nil {
return err
}
@ -279,10 +279,10 @@ func exportSavedFilters(s *xorm.Session, u *user.User, wr *zip.Writer) (err erro
return utils.WriteBytesToZip("filters.json", data, wr)
}
func exportListBackgrounds(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
lists, _, _, err := getRawListsForUser(
func exportProjectBackgrounds(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
projects, _, _, err := getRawProjectsForUser(
s,
&listOptions{
&projectOptions{
user: u,
page: -1,
},
@ -292,7 +292,7 @@ func exportListBackgrounds(s *xorm.Session, u *user.User, wr *zip.Writer) (err e
}
fs := make(map[int64]io.ReadCloser)
for _, l := range lists {
for _, l := range projects {
if l.BackgroundFileID == 0 {
continue
}

View File

@ -29,7 +29,7 @@ type FavoriteKind int
const (
FavoriteKindUnknown FavoriteKind = iota
FavoriteKindTask
FavoriteKindList
FavoriteKindProject
)
// Favorite represents an entity which is a favorite to someone

View File

@ -31,8 +31,8 @@ type Bucket struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"bucket"`
// The title of this bucket.
Title string `xorm:"text not null" valid:"required" minLength:"1" json:"title"`
// The list this bucket belongs to.
ListID int64 `xorm:"bigint not null" json:"list_id" param:"list"`
// The project this bucket belongs to.
ProjectID int64 `xorm:"bigint not null" json:"project_id" param:"project"`
// All tasks which belong to this bucket.
Tasks []*Task `xorm:"-" json:"tasks"`
@ -77,19 +77,19 @@ func getBucketByID(s *xorm.Session, id int64) (b *Bucket, err error) {
return
}
func getDefaultBucket(s *xorm.Session, listID int64) (bucket *Bucket, err error) {
func getDefaultBucket(s *xorm.Session, projectID int64) (bucket *Bucket, err error) {
bucket = &Bucket{}
_, err = s.
Where("list_id = ?", listID).
OrderBy("id asc").
Where("project_id = ?", projectID).
OrderBy("position asc").
Get(bucket)
return
}
func getDoneBucketForList(s *xorm.Session, listID int64) (bucket *Bucket, err error) {
func getDoneBucketForProject(s *xorm.Session, projectID int64) (bucket *Bucket, err error) {
bucket = &Bucket{}
exists, err := s.
Where("list_id = ? and is_done_bucket = ?", listID, true).
Where("project_id = ? and is_done_bucket = ?", projectID, true).
Get(bucket)
if err != nil {
return nil, err
@ -101,14 +101,14 @@ func getDoneBucketForList(s *xorm.Session, listID int64) (bucket *Bucket, err er
return
}
// ReadAll returns all buckets with their tasks for a certain list
// @Summary Get all kanban buckets of a list
// @Description Returns all kanban buckets with belong to a list including their tasks.
// ReadAll returns all buckets with their tasks for a certain project
// @Summary Get all kanban buckets of a project
// @Description Returns all kanban buckets with belong to a project including their tasks.
// @tags task
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "List Id"
// @Param id path int true "Project Id"
// @Param page query int false "The page number for tasks. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search tasks by task text."
@ -119,15 +119,15 @@ func getDoneBucketForList(s *xorm.Session, listID int64) (bucket *Bucket, err er
// @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`."
// @Success 200 {array} models.Bucket "The buckets with their tasks"
// @Failure 500 {object} models.Message "Internal server error"
// @Router /lists/{id}/buckets [get]
// @Router /projects/{id}/buckets [get]
func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
list, err := GetListSimpleByID(s, b.ListID)
project, err := GetProjectSimpleByID(s, b.ProjectID)
if err != nil {
return nil, 0, 0, err
}
can, _, err := list.CanRead(s, auth)
can, _, err := project.CanRead(s, auth)
if err != nil {
return nil, 0, 0, err
}
@ -135,10 +135,10 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
return nil, 0, 0, ErrGenericForbidden{}
}
// Get all buckets for this list
// Get all buckets for this project
buckets := []*Bucket{}
err = s.
Where("list_id = ?", b.ListID).
Where("project_id = ?", b.ProjectID).
OrderBy("position").
Find(&buckets)
if err != nil {
@ -202,7 +202,7 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
opts.filters[bucketFilterIndex].value = id
ts, _, _, err := getRawTasksForLists(s, []*List{{ID: bucket.ListID}}, auth, opts)
ts, _, _, err := getRawTasksForProjects(s, []*Project{{ID: bucket.ProjectID}}, auth, opts)
if err != nil {
return nil, 0, 0, err
}
@ -226,7 +226,7 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
for _, task := range tasks {
// Check if the bucket exists in the map to prevent nil pointer panics
if _, exists := bucketMap[task.BucketID]; !exists {
log.Debugf("Tried to put task %d into bucket %d which does not exist in list %d", task.ID, task.BucketID, b.ListID)
log.Debugf("Tried to put task %d into bucket %d which does not exist in project %d", task.ID, task.BucketID, b.ProjectID)
continue
}
bucketMap[task.BucketID].Tasks = append(bucketMap[task.BucketID].Tasks, task)
@ -237,18 +237,18 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
// Create creates a new bucket
// @Summary Create a new bucket
// @Description Creates a new kanban bucket on a list.
// @Description Creates a new kanban bucket on a project.
// @tags task
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "List Id"
// @Param id path int true "Project Id"
// @Param bucket body models.Bucket true "The bucket object"
// @Success 200 {object} models.Bucket "The created bucket object."
// @Failure 400 {object} web.HTTPError "Invalid bucket object provided."
// @Failure 404 {object} web.HTTPError "The list does not exist."
// @Failure 404 {object} web.HTTPError "The project does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id}/buckets [put]
// @Router /projects/{id}/buckets [put]
func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
b.CreatedBy, err = GetUserOrLinkShareUser(s, a)
if err != nil {
@ -273,24 +273,24 @@ func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param listID path int true "List Id"
// @Param projectID path int true "Project Id"
// @Param bucketID path int true "Bucket Id"
// @Param bucket body models.Bucket true "The bucket object"
// @Success 200 {object} models.Bucket "The created bucket object."
// @Failure 400 {object} web.HTTPError "Invalid bucket object provided."
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{listID}/buckets/{bucketID} [post]
// @Router /projects/{projectID}/buckets/{bucketID} [post]
func (b *Bucket) Update(s *xorm.Session, a web.Auth) (err error) {
doneBucket, err := getDoneBucketForList(s, b.ListID)
doneBucket, err := getDoneBucketForProject(s, b.ProjectID)
if err != nil {
return err
}
if doneBucket != nil && doneBucket.IsDoneBucket && b.IsDoneBucket && doneBucket.ID != b.ID {
return &ErrOnlyOneDoneBucketPerList{
return &ErrOnlyOneDoneBucketPerProject{
BucketID: b.ID,
ListID: b.ListID,
ProjectID: b.ProjectID,
DoneBucketID: doneBucket.ID,
}
}
@ -309,28 +309,28 @@ func (b *Bucket) Update(s *xorm.Session, a web.Auth) (err error) {
// Delete removes a bucket, but no tasks
// @Summary Deletes an existing bucket
// @Description Deletes an existing kanban bucket and dissociates all of its task. It does not delete any tasks. You cannot delete the last bucket on a list.
// @Description Deletes an existing kanban bucket and dissociates all of its task. It does not delete any tasks. You cannot delete the last bucket on a project.
// @tags task
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param listID path int true "List Id"
// @Param projectID path int true "Project Id"
// @Param bucketID path int true "Bucket Id"
// @Success 200 {object} models.Message "Successfully deleted."
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{listID}/buckets/{bucketID} [delete]
// @Router /projects/{projectID}/buckets/{bucketID} [delete]
func (b *Bucket) Delete(s *xorm.Session, a web.Auth) (err error) {
// Prevent removing the last bucket
total, err := s.Where("list_id = ?", b.ListID).Count(&Bucket{})
total, err := s.Where("project_id = ?", b.ProjectID).Count(&Bucket{})
if err != nil {
return
}
if total <= 1 {
return ErrCannotRemoveLastBucket{
BucketID: b.ID,
ListID: b.ListID,
BucketID: b.ID,
ProjectID: b.ProjectID,
}
}
@ -341,7 +341,7 @@ func (b *Bucket) Delete(s *xorm.Session, a web.Auth) (err error) {
}
// Get the default bucket
defaultBucket, err := getDefaultBucket(s, b.ListID)
defaultBucket, err := getDefaultBucket(s, b.ProjectID)
if err != nil {
return
}

View File

@ -23,7 +23,7 @@ import (
// CanCreate checks if a user can create a new bucket
func (b *Bucket) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
l := &List{ID: b.ListID}
l := &Project{ID: b.ProjectID}
return l.CanWrite(s, a)
}
@ -43,6 +43,6 @@ func (b *Bucket) canDoBucket(s *xorm.Session, a web.Auth) (bool, error) {
if err != nil {
return false, err
}
l := &List{ID: bb.ListID}
l := &Project{ID: bb.ProjectID}
return l.CanWrite(s, a)
}

View File

@ -33,7 +33,7 @@ func TestBucket_ReadAll(t *testing.T) {
defer s.Close()
testuser := &user.User{ID: 1}
b := &Bucket{ListID: 1}
b := &Bucket{ProjectID: 1}
bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", 0, 0)
assert.NoError(t, err)
@ -49,22 +49,22 @@ func TestBucket_ReadAll(t *testing.T) {
assert.Len(t, buckets, 3)
// Assert all tasks are in the right bucket
assert.Len(t, buckets[0].Tasks, 3)
assert.Len(t, buckets[1].Tasks, 12)
assert.Len(t, buckets[0].Tasks, 12)
assert.Len(t, buckets[1].Tasks, 3)
assert.Len(t, buckets[2].Tasks, 3)
// Assert we have bucket 1, 2, 3 but not 4 (that belongs to a different list) and their position
assert.Equal(t, int64(2), buckets[0].ID)
assert.Equal(t, int64(1), buckets[1].ID)
// Assert we have bucket 1, 2, 3 but not 4 (that belongs to a different project) and their position
assert.Equal(t, int64(1), buckets[0].ID)
assert.Equal(t, int64(2), buckets[1].ID)
assert.Equal(t, int64(3), buckets[2].ID)
// Kinda assert all tasks are in the right buckets
assert.Equal(t, int64(1), buckets[1].Tasks[0].BucketID)
assert.Equal(t, int64(1), buckets[1].Tasks[1].BucketID)
assert.Equal(t, int64(1), buckets[0].Tasks[0].BucketID)
assert.Equal(t, int64(1), buckets[0].Tasks[1].BucketID)
assert.Equal(t, int64(2), buckets[0].Tasks[0].BucketID)
assert.Equal(t, int64(2), buckets[0].Tasks[1].BucketID)
assert.Equal(t, int64(2), buckets[0].Tasks[2].BucketID)
assert.Equal(t, int64(2), buckets[1].Tasks[0].BucketID)
assert.Equal(t, int64(2), buckets[1].Tasks[1].BucketID)
assert.Equal(t, int64(2), buckets[1].Tasks[2].BucketID)
assert.Equal(t, int64(3), buckets[2].Tasks[0].BucketID)
assert.Equal(t, int64(3), buckets[2].Tasks[1].BucketID)
@ -77,7 +77,7 @@ func TestBucket_ReadAll(t *testing.T) {
testuser := &user.User{ID: 1}
b := &Bucket{
ListID: 1,
ProjectID: 1,
TaskCollection: TaskCollection{
FilterBy: []string{"title"},
FilterComparator: []string{"like"},
@ -89,8 +89,8 @@ func TestBucket_ReadAll(t *testing.T) {
buckets := bucketsInterface.([]*Bucket)
assert.Len(t, buckets, 3)
assert.Equal(t, int64(2), buckets[1].Tasks[0].ID)
assert.Equal(t, int64(33), buckets[1].Tasks[1].ID)
assert.Equal(t, int64(2), buckets[0].Tasks[0].ID)
assert.Equal(t, int64(33), buckets[0].Tasks[1].ID)
})
t.Run("accessed by link share", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
@ -98,11 +98,11 @@ func TestBucket_ReadAll(t *testing.T) {
defer s.Close()
linkShare := &LinkSharing{
ID: 1,
ListID: 1,
Right: RightRead,
ID: 1,
ProjectID: 1,
Right: RightRead,
}
b := &Bucket{ListID: 1}
b := &Bucket{ProjectID: 1}
result, _, _, err := b.ReadAll(s, linkShare, "", 0, 0)
assert.NoError(t, err)
buckets, _ := result.([]*Bucket)
@ -116,7 +116,7 @@ func TestBucket_ReadAll(t *testing.T) {
defer s.Close()
testuser := &user.User{ID: 12}
b := &Bucket{ListID: 23}
b := &Bucket{ProjectID: 23}
result, _, _, err := b.ReadAll(s, testuser, "", 0, 0)
assert.NoError(t, err)
buckets, _ := result.([]*Bucket)
@ -135,8 +135,8 @@ func TestBucket_Delete(t *testing.T) {
defer s.Close()
b := &Bucket{
ID: 2, // The second bucket only has 3 tasks
ListID: 1,
ID: 2, // The second bucket only has 3 tasks
ProjectID: 1,
}
err := b.Delete(s, user)
assert.NoError(t, err)
@ -147,20 +147,20 @@ func TestBucket_Delete(t *testing.T) {
tasks := []*Task{}
err = s.Where("bucket_id = ?", 1).Find(&tasks)
assert.NoError(t, err)
assert.Len(t, tasks, 15)
assert.Len(t, tasks, 16)
db.AssertMissing(t, "buckets", map[string]interface{}{
"id": 2,
"list_id": 1,
"id": 2,
"project_id": 1,
})
})
t.Run("last bucket in list", func(t *testing.T) {
t.Run("last bucket in project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
b := &Bucket{
ID: 34,
ListID: 18,
ID: 34,
ProjectID: 18,
}
err := b.Delete(s, user)
assert.Error(t, err)
@ -169,8 +169,8 @@ func TestBucket_Delete(t *testing.T) {
assert.NoError(t, err)
db.AssertExists(t, "buckets", map[string]interface{}{
"id": 34,
"list_id": 18,
"id": 34,
"project_id": 18,
}, false)
})
}
@ -217,19 +217,19 @@ func TestBucket_Update(t *testing.T) {
testAndAssertBucketUpdate(t, b, s)
})
t.Run("only one done bucket per list", func(t *testing.T) {
t.Run("only one done bucket per project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
b := &Bucket{
ID: 1,
ListID: 1,
ProjectID: 1,
IsDoneBucket: true,
}
err := b.Update(s, &user.User{ID: 1})
assert.Error(t, err)
assert.True(t, IsErrOnlyOneDoneBucketPerList(err))
assert.True(t, IsErrOnlyOneDoneBucketPerProject(err))
})
}

View File

@ -151,8 +151,8 @@ func (l *Label) ReadAll(s *xorm.Session, a web.Auth, search string, page int, pe
return nil, 0, 0, err
}
return getLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
Search: search,
return GetLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
Search: []string{search},
User: u,
GetForUser: u.ID,
Page: page,
@ -178,13 +178,13 @@ func (l *Label) ReadAll(s *xorm.Session, a web.Auth, search string, page int, pe
func (l *Label) ReadOne(s *xorm.Session, a web.Auth) (err error) {
label, err := getLabelByIDSimple(s, l.ID)
if err != nil {
return err
return
}
*l = *label
u, err := user.GetUserByID(s, l.CreatedByID)
if err != nil {
return err
return
}
l.CreatedBy = u
@ -192,14 +192,16 @@ func (l *Label) ReadOne(s *xorm.Session, a web.Auth) (err error) {
}
func getLabelByIDSimple(s *xorm.Session, labelID int64) (*Label, error) {
label := Label{}
exists, err := s.ID(labelID).Get(&label)
if err != nil {
return &label, err
}
if !exists {
return &Label{}, ErrLabelDoesNotExist{labelID}
}
return &label, err
return GetLabelSimple(s, &Label{ID: labelID})
}
func GetLabelSimple(s *xorm.Session, l *Label) (*Label, error) {
exists, err := s.Get(l)
if err != nil {
return l, err
}
if !exists {
return &Label{}, ErrLabelDoesNotExist{l.ID}
}
return l, err
}

View File

@ -77,7 +77,7 @@ func (l *Label) hasAccessToLabel(s *xorm.Session, a web.Auth) (has bool, maxRigh
builder.
Select("id").
From("tasks").
Where(builder.In("list_id", getUserListsStatement(u.ID).Select("l.id"))),
Where(builder.In("project_id", getUserProjectsStatement(u.ID).Select("l.id"))),
)
ll := &LabelTask{}

View File

@ -35,7 +35,7 @@ import (
type LabelTask struct {
// The unique, numeric id of this label.
ID int64 `xorm:"bigint autoincr not null unique pk" json:"-"`
TaskID int64 `xorm:"bigint INDEX not null" json:"-" param:"listtask"`
TaskID int64 `xorm:"bigint INDEX not null" json:"-" param:"projecttask"`
// The label id you want to associate with a task.
LabelID int64 `xorm:"bigint INDEX not null" json:"label_id" param:"label"`
// A timestamp when this task was created. You cannot change this value.
@ -52,7 +52,7 @@ func (LabelTask) TableName() string {
// Delete deletes a label on a task
// @Summary Remove a label from a task
// @Description Remove a label from a task. The user needs to have write-access to the list to be able do this.
// @Description Remove a label from a task. The user needs to have write-access to the project to be able do this.
// @tags labels
// @Accept json
// @Produce json
@ -71,7 +71,7 @@ func (lt *LabelTask) Delete(s *xorm.Session, a web.Auth) (err error) {
// Create adds a label to a task
// @Summary Add a label to a task
// @Description Add a label to a task. The user needs to have write-access to the list to be able do this.
// @Description Add a label to a task. The user needs to have write-access to the project to be able do this.
// @tags labels
// @Accept json
// @Produce json
@ -100,7 +100,7 @@ func (lt *LabelTask) Create(s *xorm.Session, a web.Auth) (err error) {
return err
}
err = updateListByTaskID(s, lt.TaskID)
err = updateProjectByTaskID(s, lt.TaskID)
return
}
@ -129,16 +129,16 @@ func (lt *LabelTask) ReadAll(s *xorm.Session, a web.Auth, search string, page in
return nil, 0, 0, ErrNoRightToSeeTask{lt.TaskID, a.GetID()}
}
return getLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
return GetLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
User: &user.User{ID: a.GetID()},
Search: search,
Search: []string{search},
Page: page,
TaskIDs: []int64{lt.TaskID},
})
}
// Helper struct, contains the label + its task ID
type labelWithTaskID struct {
// LabelWithTaskID is a helper struct, contains the label + its task ID
type LabelWithTaskID struct {
TaskID int64 `json:"-"`
Label `xorm:"extends"`
}
@ -146,7 +146,7 @@ type labelWithTaskID struct {
// LabelByTaskIDsOptions is a struct to not clutter the function with too many optional parameters.
type LabelByTaskIDsOptions struct {
User *user.User
Search string
Search []string
Page int
PerPage int
TaskIDs []int64
@ -155,9 +155,9 @@ type LabelByTaskIDsOptions struct {
GetForUser int64
}
// Helper function to get all labels for a set of tasks
// GetLabelsByTaskIDs is a helper function to get all labels for a set of tasks
// Used when getting all labels for one task as well when getting all lables
func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, resultCount int, totalEntries int64, err error) {
func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*LabelWithTaskID, resultCount int, totalEntries int64, err error) {
// We still need the task ID when we want to get all labels for a task, but because of this, we get the same label
// multiple times when it is associated to more than one task.
// Because of this whole thing, we need this extra switch here to only group by Task IDs if needed.
@ -170,7 +170,7 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
}
// Get all labels associated with these tasks
var labels []*labelWithTaskID
var labels []*LabelWithTaskID
cond := builder.And(builder.NotNull{"label_tasks.label_id"})
if len(opts.TaskIDs) > 0 && opts.GetForUser == 0 {
cond = builder.And(builder.In("label_tasks.task_id", opts.TaskIDs), cond)
@ -180,7 +180,7 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
builder.
Select("id").
From("tasks").
Where(builder.In("list_id", getUserListsStatement(opts.GetForUser).Select("l.id"))),
Where(builder.In("project_id", getUserProjectsStatement(opts.GetForUser).Select("l.id"))),
), cond)
}
if opts.GetUnusedLabels {
@ -188,8 +188,14 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
}
ids := []int64{}
if opts.Search != "" {
vals := strings.Split(opts.Search, ",")
for _, search := range opts.Search {
search = strings.Trim(search, " ")
if search == "" {
continue
}
vals := strings.Split(search, ",")
for _, val := range vals {
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
@ -202,8 +208,19 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
if len(ids) > 0 {
cond = builder.And(cond, builder.In("labels.id", ids))
} else {
cond = builder.And(cond, db.ILIKE("labels.title", opts.Search))
} else if len(opts.Search) > 0 {
var searchcond builder.Cond
for _, search := range opts.Search {
search = strings.Trim(search, " ")
if search == "" {
continue
}
searchcond = builder.Or(searchcond, db.ILIKE("labels.title", search))
}
cond = builder.And(cond, searchcond)
}
limit, start := getLimitFromPageIndex(opts.Page, opts.PerPage)
@ -254,7 +271,6 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
Select("count(DISTINCT labels.id)").
Join("LEFT", "label_tasks", "label_tasks.label_id = labels.id").
Where(cond).
And("labels.title LIKE ?", "%"+opts.Search+"%").
Count(&Label{})
if err != nil {
return nil, 0, 0, err
@ -264,7 +280,7 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
}
// Create or update a bunch of task labels
func (t *Task) updateTaskLabels(s *xorm.Session, creator web.Auth, labels []*Label) (err error) {
func (t *Task) UpdateTaskLabels(s *xorm.Session, creator web.Auth, labels []*Label) (err error) {
// If we don't have any new labels, delete everything right away. Saves us some hassle.
if len(labels) == 0 && len(t.Labels) > 0 {
@ -293,17 +309,17 @@ func (t *Task) updateTaskLabels(s *xorm.Session, creator web.Auth, labels []*Lab
for _, oldLabel := range allLabels {
found = false
if newLabels[oldLabel.ID] != nil {
found = true // If a new label is already in the list with old labels
found = true // If a new label is already in the project with old labels
}
// Put all labels which are only on the old list to the trash
// Put all labels which are only on the old project to the trash
if !found {
labelsToDelete = append(labelsToDelete, oldLabel.ID)
} else {
t.Labels = append(t.Labels, oldLabel)
}
// Put it in a list with all old labels, just using the loop here
// Put it in a project with all old labels, just using the loop here
oldLabels[oldLabel.ID] = oldLabel
}
@ -349,7 +365,7 @@ func (t *Task) updateTaskLabels(s *xorm.Session, creator web.Auth, labels []*Lab
t.Labels = append(t.Labels, label)
}
err = updateListLastUpdated(s, &List{ID: t.ListID})
err = updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
return
}
@ -357,7 +373,7 @@ func (t *Task) updateTaskLabels(s *xorm.Session, creator web.Auth, labels []*Lab
type LabelTaskBulk struct {
// All labels you want to update at once.
Labels []*Label `json:"labels"`
TaskID int64 `json:"-" param:"listtask"`
TaskID int64 `json:"-" param:"projecttask"`
web.CRUDable `json:"-"`
web.Rights `json:"-"`
@ -381,7 +397,7 @@ func (ltb *LabelTaskBulk) Create(s *xorm.Session, a web.Auth) (err error) {
if err != nil {
return
}
labels, _, _, err := getLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
labels, _, _, err := GetLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
TaskIDs: []int64{ltb.TaskID},
})
if err != nil {
@ -390,5 +406,5 @@ func (ltb *LabelTaskBulk) Create(s *xorm.Session, a web.Auth) (err error) {
for _, l := range labels {
task.Labels = append(task.Labels, &l.Label)
}
return task.updateTaskLabels(s, a, ltb.Labels)
return task.UpdateTaskLabels(s, a, ltb.Labels)
}

View File

@ -78,7 +78,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
args: args{
a: &user.User{ID: 1},
},
wantLabels: []*labelWithTaskID{
wantLabels: []*LabelWithTaskID{
{
TaskID: 1,
Label: label,
@ -116,7 +116,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
a: &user.User{ID: 1},
search: "VISIBLE",
},
wantLabels: []*labelWithTaskID{
wantLabels: []*LabelWithTaskID{
{
TaskID: 1,
Label: label,

View File

@ -70,7 +70,7 @@ func TestLabel_ReadAll(t *testing.T) {
args: args{
a: &user.User{ID: 1},
},
wantLs: []*labelWithTaskID{
wantLs: []*LabelWithTaskID{
{
Label: Label{
ID: 1,

View File

@ -42,17 +42,17 @@ const (
SharingTypeWithPassword
)
// LinkSharing represents a shared list
// LinkSharing represents a shared project
type LinkSharing struct {
// The ID of the shared thing
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"share"`
// The public id to get this shared list
// The public id to get this shared project
Hash string `xorm:"varchar(40) not null unique" json:"hash" param:"hash"`
// The name of this link share. All actions someone takes while being authenticated with that link will appear with that name.
Name string `xorm:"text null" json:"name"`
// The ID of the shared list
ListID int64 `xorm:"bigint not null" json:"-" param:"list"`
// The right this list is shared with. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
// The ID of the shared project
ProjectID int64 `xorm:"bigint not null" json:"-" param:"project"`
// The right this project is shared with. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
// The kind of this link. 0 = undefined, 1 = without password, 2 = with password.
@ -61,11 +61,11 @@ type LinkSharing struct {
// The password of this link share. You can only set it, not retrieve it after the link share has been created.
Password string `xorm:"text null" json:"password"`
// The user who shared this list
// The user who shared this project
SharedBy *user.User `xorm:"-" json:"shared_by"`
SharedByID int64 `xorm:"bigint INDEX not null" json:"-"`
// A timestamp when this list was shared. You cannot change this value.
// A timestamp when this project was shared. You cannot change this value.
Created time.Time `xorm:"created not null" json:"created"`
// A timestamp when this share was last updated. You cannot change this value.
Updated time.Time `xorm:"updated not null" json:"updated"`
@ -89,7 +89,7 @@ func GetLinkShareFromClaims(claims jwt.MapClaims) (share *LinkSharing, err error
share = &LinkSharing{}
share.ID = int64(claims["id"].(float64))
share.Hash = claims["hash"].(string)
share.ListID = int64(claims["list_id"].(float64))
share.ProjectID = int64(claims["project_id"].(float64))
share.Right = Right(claims["right"].(float64))
share.SharedByID = int64(claims["sharedByID"].(float64))
return
@ -114,21 +114,21 @@ func (share *LinkSharing) toUser() *user.User {
}
}
// Create creates a new link share for a given list
// @Summary Share a list via link
// @Description Share a list via link. The user needs to have write-access to the list to be able do this.
// Create creates a new link share for a given project
// @Summary Share a project via link
// @Description Share a project via link. The user needs to have write-access to the project to be able do this.
// @tags sharing
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param list path int true "List ID"
// @Param project path int true "Project ID"
// @Param label body models.LinkSharing true "The new link share object"
// @Success 201 {object} models.LinkSharing "The created link share object."
// @Failure 400 {object} web.HTTPError "Invalid link share object provided."
// @Failure 403 {object} web.HTTPError "Not allowed to add the list share."
// @Failure 404 {object} web.HTTPError "The list does not exist."
// @Failure 403 {object} web.HTTPError "Not allowed to add the project share."
// @Failure 404 {object} web.HTTPError "The project does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{list}/shares [put]
// @Router /projects/{project}/shares [put]
func (share *LinkSharing) Create(s *xorm.Session, a web.Auth) (err error) {
err = share.Right.isValid()
@ -156,48 +156,48 @@ func (share *LinkSharing) Create(s *xorm.Session, a web.Auth) (err error) {
}
// ReadOne returns one share
// @Summary Get one link shares for a list
// @Summary Get one link shares for a project
// @Description Returns one link share by its ID.
// @tags sharing
// @Accept json
// @Produce json
// @Param list path int true "List ID"
// @Param project path int true "Project ID"
// @Param share path int true "Share ID"
// @Security JWTKeyAuth
// @Success 200 {object} models.LinkSharing "The share links"
// @Failure 403 {object} web.HTTPError "No access to the list"
// @Failure 403 {object} web.HTTPError "No access to the project"
// @Failure 404 {object} web.HTTPError "Share Link not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{list}/shares/{share} [get]
// @Router /projects/{project}/shares/{share} [get]
func (share *LinkSharing) ReadOne(s *xorm.Session, a web.Auth) (err error) {
exists, err := s.Where("id = ?", share.ID).Get(share)
if err != nil {
return err
}
if !exists {
return ErrListShareDoesNotExist{ID: share.ID, Hash: share.Hash}
return ErrProjectShareDoesNotExist{ID: share.ID, Hash: share.Hash}
}
share.Password = ""
return
}
// ReadAll returns all shares for a given list
// @Summary Get all link shares for a list
// @Description Returns all link shares which exist for a given list
// ReadAll returns all shares for a given project
// @Summary Get all link shares for a project
// @Description Returns all link shares which exist for a given project
// @tags sharing
// @Accept json
// @Produce json
// @Param list path int true "List ID"
// @Param project path int true "Project ID"
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search shares by hash."
// @Security JWTKeyAuth
// @Success 200 {array} models.LinkSharing "The share links"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{list}/shares [get]
// @Router /projects/{project}/shares [get]
func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
list := &List{ID: share.ListID}
can, _, err := list.CanRead(s, a)
project := &Project{ID: share.ProjectID}
can, _, err := project.CanRead(s, a)
if err != nil {
return nil, 0, 0, err
}
@ -210,7 +210,7 @@ func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, pa
var shares []*LinkSharing
query := s.
Where(builder.And(
builder.Eq{"list_id": share.ListID},
builder.Eq{"project_id": share.ProjectID},
builder.Or(
db.ILIKE("hash", search),
db.ILIKE("name", search),
@ -246,7 +246,7 @@ func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, pa
// Total count
totalItems, err = s.
Where("list_id = ? AND hash LIKE ?", share.ListID, "%"+search+"%").
Where("project_id = ? AND hash LIKE ?", share.ProjectID, "%"+search+"%").
Count(&LinkSharing{})
if err != nil {
return nil, 0, 0, err
@ -257,18 +257,18 @@ func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, pa
// Delete removes a link share
// @Summary Remove a link share
// @Description Remove a link share. The user needs to have write-access to the list to be able do this.
// @Description Remove a link share. The user needs to have write-access to the project to be able do this.
// @tags sharing
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param list path int true "List ID"
// @Param project path int true "Project ID"
// @Param share path int true "Share Link ID"
// @Success 200 {object} models.Message "The link was successfully removed."
// @Failure 403 {object} web.HTTPError "Not allowed to remove the link."
// @Failure 404 {object} web.HTTPError "Share Link not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{list}/shares/{share} [delete]
// @Router /projects/{project}/shares/{share} [delete]
func (share *LinkSharing) Delete(s *xorm.Session, a web.Auth) (err error) {
_, err = s.Where("id = ?", share.ID).Delete(share)
return
@ -282,19 +282,19 @@ func GetLinkShareByHash(s *xorm.Session, hash string) (share *LinkSharing, err e
return
}
if !has {
return share, ErrListShareDoesNotExist{Hash: hash}
return share, ErrProjectShareDoesNotExist{Hash: hash}
}
return
}
// GetListByShareHash returns a link share by its hash
func GetListByShareHash(s *xorm.Session, hash string) (list *List, err error) {
// GetProjectByShareHash returns a link share by its hash
func GetProjectByShareHash(s *xorm.Session, hash string) (project *Project, err error) {
share, err := GetLinkShareByHash(s, hash)
if err != nil {
return
}
list, err = GetListSimpleByID(s, share.ListID)
project, err = GetProjectSimpleByID(s, share.ProjectID)
return
}
@ -306,7 +306,7 @@ func GetLinkShareByID(s *xorm.Session, id int64) (share *LinkSharing, err error)
return
}
if !has {
return share, ErrListShareDoesNotExist{ID: id}
return share, ErrProjectShareDoesNotExist{ID: id}
}
return
}

View File

@ -28,7 +28,7 @@ func (share *LinkSharing) CanRead(s *xorm.Session, a web.Auth) (bool, int, error
return false, 0, nil
}
l, err := GetListByShareHash(s, share.Hash)
l, err := GetProjectByShareHash(s, share.Hash)
if err != nil {
return false, 0, err
}
@ -56,7 +56,7 @@ func (share *LinkSharing) canDoLinkShare(s *xorm.Session, a web.Auth) (bool, err
return false, nil
}
l, err := GetListSimpleByID(s, share.ListID)
l, err := GetProjectSimpleByID(s, share.ProjectID)
if err != nil {
return false, err
}

View File

@ -33,8 +33,8 @@ func TestLinkSharing_Create(t *testing.T) {
defer s.Close()
share := &LinkSharing{
ListID: 1,
Right: RightRead,
ProjectID: 1,
Right: RightRead,
}
err := share.Create(s, doer)
@ -52,8 +52,8 @@ func TestLinkSharing_Create(t *testing.T) {
defer s.Close()
share := &LinkSharing{
ListID: 1,
Right: Right(123),
ProjectID: 1,
Right: Right(123),
}
err := share.Create(s, doer)
@ -66,9 +66,9 @@ func TestLinkSharing_Create(t *testing.T) {
defer s.Close()
share := &LinkSharing{
ListID: 1,
Right: RightRead,
Password: "somePassword",
ProjectID: 1,
Right: RightRead,
Password: "somePassword",
}
err := share.Create(s, doer)
@ -92,7 +92,7 @@ func TestLinkSharing_ReadAll(t *testing.T) {
defer s.Close()
share := &LinkSharing{
ListID: 1,
ProjectID: 1,
}
all, _, _, err := share.ReadAll(s, doer, "", 1, -1)
shares := all.([]*LinkSharing)
@ -109,7 +109,7 @@ func TestLinkSharing_ReadAll(t *testing.T) {
defer s.Close()
share := &LinkSharing{
ListID: 1,
ProjectID: 1,
}
all, _, _, err := share.ReadAll(s, doer, "wITHPASS", 1, -1)
shares := all.([]*LinkSharing)

View File

@ -1,836 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"strconv"
"strings"
"time"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/builder"
"xorm.io/xorm"
)
// List represents a list of tasks
type List struct {
// The unique, numeric id of this list.
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"list"`
// The title of the list. You'll see this in the namespace overview.
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
// The description of the list.
Description string `xorm:"longtext null" json:"description"`
// The unique list short identifier. Used to build task identifiers.
Identifier string `xorm:"varchar(10) null" json:"identifier" valid:"runelength(0|10)" minLength:"0" maxLength:"10"`
// The hex color of this list
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
NamespaceID int64 `xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace"`
// The user who created this list.
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
// Whether or not a list is archived.
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
// The id of the file this list has set as background
BackgroundFileID int64 `xorm:"null" json:"-"`
// Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /lists/{listID}/background
BackgroundInformation interface{} `xorm:"-" json:"background_information"`
// Contains a very small version of the list background to use as a blurry preview until the actual background is loaded. Check out https://blurha.sh/ to learn how it works.
BackgroundBlurHash string `xorm:"varchar(50) null" json:"background_blur_hash"`
// True if a list is a favorite. Favorite lists show up in a separate namespace. This value depends on the user making the call to the api.
IsFavorite bool `xorm:"-" json:"is_favorite"`
// The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.
// Will only returned when retreiving one list.
Subscription *Subscription `xorm:"-" json:"subscription,omitempty"`
// The position this list has when querying all lists. See the tasks.position property on how to use this.
Position float64 `xorm:"double null" json:"position"`
// A timestamp when this list was created. You cannot change this value.
Created time.Time `xorm:"created not null" json:"created"`
// A timestamp when this list was last updated. You cannot change this value.
Updated time.Time `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
}
type ListWithTasksAndBuckets struct {
List
// An array of tasks which belong to the list.
Tasks []*TaskWithComments `xorm:"-" json:"tasks"`
// Only used for migration.
Buckets []*Bucket `xorm:"-" json:"buckets"`
BackgroundFileID int64 `xorm:"null" json:"background_file_id"`
}
// TableName returns a better name for the lists table
func (l *List) TableName() string {
return "lists"
}
// ListBackgroundType holds a list background type
type ListBackgroundType struct {
Type string
}
// ListBackgroundUpload represents the list upload background type
const ListBackgroundUpload string = "upload"
// FavoritesPseudoList holds all tasks marked as favorites
var FavoritesPseudoList = List{
ID: -1,
Title: "Favorites",
Description: "This list has all tasks marked as favorites.",
NamespaceID: FavoritesPseudoNamespace.ID,
IsFavorite: true,
Created: time.Now(),
Updated: time.Now(),
}
// GetListsByNamespaceID gets all lists in a namespace
func GetListsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (lists []*List, err error) {
switch nID {
case SharedListsPseudoNamespace.ID:
nnn, err := getSharedListsInNamespace(s, false, doer)
if err != nil {
return nil, err
}
if nnn != nil && nnn.Lists != nil {
lists = nnn.Lists
}
case FavoritesPseudoNamespace.ID:
namespaces := make(map[int64]*NamespaceWithLists)
_, err := getNamespacesWithLists(s, &namespaces, "", false, 0, -1, doer.ID)
if err != nil {
return nil, err
}
namespaceIDs, _ := getNamespaceOwnerIDs(namespaces)
ls, err := getListsForNamespaces(s, namespaceIDs, false)
if err != nil {
return nil, err
}
nnn, err := getFavoriteLists(s, ls, namespaceIDs, doer)
if err != nil {
return nil, err
}
if nnn != nil && nnn.Lists != nil {
lists = nnn.Lists
}
case SavedFiltersPseudoNamespace.ID:
nnn, err := getSavedFilters(s, doer)
if err != nil {
return nil, err
}
if nnn != nil && nnn.Lists != nil {
lists = nnn.Lists
}
default:
err = s.Select("l.*").
Alias("l").
Join("LEFT", []string{"namespaces", "n"}, "l.namespace_id = n.id").
Where("l.is_archived = false").
Where("n.is_archived = false").
Where("namespace_id = ?", nID).
Find(&lists)
}
if err != nil {
return nil, err
}
// get more list details
err = addListDetails(s, lists, doer)
return lists, err
}
// ReadAll gets all lists a user has access to
// @Summary Get all lists a user has access to
// @Description Returns all lists a user has access to.
// @tags list
// @Accept json
// @Produce json
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search lists by title."
// @Param is_archived query bool false "If true, also returns all archived lists."
// @Security JWTKeyAuth
// @Success 200 {array} models.List "The lists"
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists [get]
func (l *List) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
// Check if we're dealing with a share auth
shareAuth, ok := a.(*LinkSharing)
if ok {
list, err := GetListSimpleByID(s, shareAuth.ListID)
if err != nil {
return nil, 0, 0, err
}
lists := []*List{list}
err = addListDetails(s, lists, a)
return lists, 0, 0, err
}
lists, resultCount, totalItems, err := getRawListsForUser(
s,
&listOptions{
search: search,
user: &user.User{ID: a.GetID()},
page: page,
perPage: perPage,
isArchived: l.IsArchived,
})
if err != nil {
return nil, 0, 0, err
}
// Add more list details
err = addListDetails(s, lists, a)
return lists, resultCount, totalItems, err
}
// ReadOne gets one list by its ID
// @Summary Gets one list
// @Description Returns a list by its ID.
// @tags list
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "List ID"
// @Success 200 {object} models.List "The list"
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [get]
func (l *List) ReadOne(s *xorm.Session, a web.Auth) (err error) {
if l.ID == FavoritesPseudoList.ID {
// Already "built" the list in CanRead
return nil
}
// Check for saved filters
if getSavedFilterIDFromListID(l.ID) > 0 {
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromListID(l.ID))
if err != nil {
return err
}
l.Title = sf.Title
l.Description = sf.Description
l.Created = sf.Created
l.Updated = sf.Updated
l.OwnerID = sf.OwnerID
}
// Get list owner
l.Owner, err = user.GetUserByID(s, l.OwnerID)
if err != nil {
return err
}
// Check if the namespace is archived and set the namespace to archived if it is not already archived individually.
if !l.IsArchived {
err = l.CheckIsArchived(s)
if err != nil {
if !IsErrNamespaceIsArchived(err) && !IsErrListIsArchived(err) {
return
}
l.IsArchived = true
}
}
// Get any background information if there is one set
if l.BackgroundFileID != 0 {
// Unsplash image
l.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, l.BackgroundFileID)
if err != nil && !files.IsErrFileIsNotUnsplashFile(err) {
return
}
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
l.BackgroundInformation = &ListBackgroundType{Type: ListBackgroundUpload}
}
}
l.IsFavorite, err = isFavorite(s, l.ID, a, FavoriteKindList)
if err != nil {
return
}
l.Subscription, err = GetSubscription(s, SubscriptionEntityList, l.ID, a)
return
}
// GetListSimpleByID gets a list with only the basic items, aka no tasks or user objects. Returns an error if the list does not exist.
func GetListSimpleByID(s *xorm.Session, listID int64) (list *List, err error) {
list = &List{}
if listID < 1 {
return nil, ErrListDoesNotExist{ID: listID}
}
exists, err := s.
Where("id = ?", listID).
OrderBy("position").
Get(list)
if err != nil {
return
}
if !exists {
return nil, ErrListDoesNotExist{ID: listID}
}
return
}
// GetListSimplByTaskID gets a list by a task id
func GetListSimplByTaskID(s *xorm.Session, taskID int64) (l *List, err error) {
// We need to re-init our list object, because otherwise xorm creates a "where for every item in that list object,
// leading to not finding anything if the id is good, but for example the title is different.
var list List
exists, err := s.
Select("lists.*").
Table(List{}).
Join("INNER", "tasks", "lists.id = tasks.list_id").
Where("tasks.id = ?", taskID).
Get(&list)
if err != nil {
return
}
if !exists {
return &List{}, ErrListDoesNotExist{ID: l.ID}
}
return &list, nil
}
// GetListsByIDs returns a map of lists from a slice with list ids
func GetListsByIDs(s *xorm.Session, listIDs []int64) (lists map[int64]*List, err error) {
lists = make(map[int64]*List, len(listIDs))
if len(listIDs) == 0 {
return
}
err = s.In("id", listIDs).Find(&lists)
return
}
type listOptions struct {
search string
user *user.User
page int
perPage int
isArchived bool
}
func getUserListsStatement(userID int64) *builder.Builder {
dialect := config.DatabaseType.GetString()
if dialect == "sqlite" {
dialect = builder.SQLITE
}
return builder.Dialect(dialect).
Select("l.*").
From("lists", "l").
Join("INNER", "namespaces n", "l.namespace_id = n.id").
Join("LEFT", "team_namespaces tn", "tn.namespace_id = n.id").
Join("LEFT", "team_members tm", "tm.team_id = tn.team_id").
Join("LEFT", "team_lists tl", "l.id = tl.list_id").
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
Join("LEFT", "users_lists ul", "ul.list_id = l.id").
Join("LEFT", "users_namespaces un", "un.namespace_id = l.namespace_id").
Where(builder.Or(
builder.Eq{"tm.user_id": userID},
builder.Eq{"tm2.user_id": userID},
builder.Eq{"ul.user_id": userID},
builder.Eq{"un.user_id": userID},
builder.Eq{"l.owner_id": userID},
)).
OrderBy("position").
GroupBy("l.id")
}
// Gets the lists only, without any tasks or so
func getRawListsForUser(s *xorm.Session, opts *listOptions) (lists []*List, resultCount int, totalItems int64, err error) {
fullUser, err := user.GetUserByID(s, opts.user.ID)
if err != nil {
return nil, 0, 0, err
}
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
var isArchivedCond builder.Cond = builder.Eq{"1": 1}
if !opts.isArchived {
isArchivedCond = builder.And(
builder.Eq{"l.is_archived": false},
builder.Eq{"n.is_archived": false},
)
}
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
var filterCond builder.Cond
ids := []int64{}
if opts.search != "" {
vals := strings.Split(opts.search, ",")
for _, val := range vals {
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
log.Debugf("List search string part '%s' is not a number: %s", val, err)
continue
}
ids = append(ids, v)
}
}
filterCond = db.ILIKE("l.title", opts.search)
if len(ids) > 0 {
filterCond = builder.In("l.id", ids)
}
// Gets all Lists where the user is either owner or in a team which has access to the list
// Or in a team which has namespace read access
query := getUserListsStatement(fullUser.ID).
Where(filterCond).
Where(isArchivedCond)
if limit > 0 {
query = query.Limit(limit, start)
}
err = s.SQL(query).Find(&lists)
if err != nil {
return nil, 0, 0, err
}
query = getUserListsStatement(fullUser.ID).
Where(filterCond).
Where(isArchivedCond)
totalItems, err = s.
SQL(query.Select("count(*)")).
Count(&List{})
return lists, len(lists), totalItems, err
}
// addListDetails adds owner user objects and list tasks to all lists in the slice
func addListDetails(s *xorm.Session, lists []*List, a web.Auth) (err error) {
if len(lists) == 0 {
return
}
var ownerIDs []int64
for _, l := range lists {
ownerIDs = append(ownerIDs, l.OwnerID)
}
// Get all list owners
owners := map[int64]*user.User{}
if len(ownerIDs) > 0 {
err = s.In("id", ownerIDs).Find(&owners)
if err != nil {
return
}
}
var fileIDs []int64
var listIDs []int64
for _, l := range lists {
listIDs = append(listIDs, l.ID)
if o, exists := owners[l.OwnerID]; exists {
l.Owner = o
}
if l.BackgroundFileID != 0 {
l.BackgroundInformation = &ListBackgroundType{Type: ListBackgroundUpload}
}
fileIDs = append(fileIDs, l.BackgroundFileID)
}
favs, err := getFavorites(s, listIDs, a, FavoriteKindList)
if err != nil {
return err
}
subscriptions, err := GetSubscriptions(s, SubscriptionEntityList, listIDs, a)
if err != nil {
log.Errorf("An error occurred while getting list subscriptions for a namespace item: %s", err.Error())
subscriptions = make(map[int64]*Subscription)
}
for _, list := range lists {
// Don't override the favorite state if it was already set from before (favorite saved filters do this)
if list.IsFavorite {
continue
}
list.IsFavorite = favs[list.ID]
if subscription, exists := subscriptions[list.ID]; exists {
list.Subscription = subscription
}
}
if len(fileIDs) == 0 {
return
}
// Unsplash background file info
us := []*UnsplashPhoto{}
err = s.In("file_id", fileIDs).Find(&us)
if err != nil {
return
}
unsplashPhotos := make(map[int64]*UnsplashPhoto, len(us))
for _, u := range us {
unsplashPhotos[u.FileID] = u
}
// Build it all into the lists slice
for _, l := range lists {
// Only override the file info if we have info for unsplash backgrounds
if _, exists := unsplashPhotos[l.BackgroundFileID]; exists {
l.BackgroundInformation = unsplashPhotos[l.BackgroundFileID]
}
}
return
}
// NamespaceList is a meta type to be able to join a list with its namespace
type NamespaceList struct {
List List `xorm:"extends"`
Namespace Namespace `xorm:"extends"`
}
// CheckIsArchived returns an ErrListIsArchived or ErrNamespaceIsArchived if the list or its namespace is archived.
func (l *List) CheckIsArchived(s *xorm.Session) (err error) {
// When creating a new list, we check if the namespace is archived
if l.ID == 0 {
n := &Namespace{ID: l.NamespaceID}
return n.CheckIsArchived(s)
}
nl := &NamespaceList{}
exists, err := s.
Table("lists").
Join("LEFT", "namespaces", "lists.namespace_id = namespaces.id").
Where("lists.id = ? AND (lists.is_archived = true OR namespaces.is_archived = true)", l.ID).
Get(nl)
if err != nil {
return
}
if exists && nl.List.ID != 0 && nl.List.IsArchived {
return ErrListIsArchived{ListID: l.ID}
}
if exists && nl.Namespace.ID != 0 && nl.Namespace.IsArchived {
return ErrNamespaceIsArchived{NamespaceID: nl.Namespace.ID}
}
return nil
}
func checkListBeforeUpdateOrDelete(s *xorm.Session, list *List) error {
if list.NamespaceID < 0 {
return &ErrListCannotBelongToAPseudoNamespace{ListID: list.ID, NamespaceID: list.NamespaceID}
}
// Check if the namespace exists
if list.NamespaceID > 0 {
_, err := GetNamespaceByID(s, list.NamespaceID)
if err != nil {
return err
}
}
// Check if the identifier is unique and not empty
if list.Identifier != "" {
exists, err := s.
Where("identifier = ?", list.Identifier).
And("id != ?", list.ID).
Exist(&List{})
if err != nil {
return err
}
if exists {
return ErrListIdentifierIsNotUnique{Identifier: list.Identifier}
}
}
return nil
}
func CreateList(s *xorm.Session, list *List, auth web.Auth) (err error) {
err = list.CheckIsArchived(s)
if err != nil {
return err
}
doer, err := user.GetFromAuth(auth)
if err != nil {
return err
}
list.OwnerID = doer.ID
list.Owner = doer
list.ID = 0 // Otherwise only the first time a new list would be created
err = checkListBeforeUpdateOrDelete(s, list)
if err != nil {
return
}
_, err = s.Insert(list)
if err != nil {
return
}
list.Position = calculateDefaultPosition(list.ID, list.Position)
_, err = s.Where("id = ?", list.ID).Update(list)
if err != nil {
return
}
if list.IsFavorite {
if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil {
return err
}
}
// Create a new first bucket for this list
b := &Bucket{
ListID: list.ID,
Title: "Backlog",
}
err = b.Create(s, auth)
if err != nil {
return
}
return events.Dispatch(&ListCreatedEvent{
List: list,
Doer: doer,
})
}
func UpdateList(s *xorm.Session, list *List, auth web.Auth, updateListBackground bool) (err error) {
err = checkListBeforeUpdateOrDelete(s, list)
if err != nil {
return
}
if list.NamespaceID == 0 {
return &ErrListMustBelongToANamespace{
ListID: list.ID,
NamespaceID: list.NamespaceID,
}
}
// We need to specify the cols we want to update here to be able to un-archive lists
colsToUpdate := []string{
"title",
"is_archived",
"identifier",
"hex_color",
"namespace_id",
"position",
}
if list.Description != "" {
colsToUpdate = append(colsToUpdate, "description")
}
if updateListBackground {
colsToUpdate = append(colsToUpdate, "background_file_id", "background_blur_hash")
}
wasFavorite, err := isFavorite(s, list.ID, auth, FavoriteKindList)
if err != nil {
return err
}
if list.IsFavorite && !wasFavorite {
if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil {
return err
}
}
if !list.IsFavorite && wasFavorite {
if err := removeFromFavorite(s, list.ID, auth, FavoriteKindList); err != nil {
return err
}
}
_, err = s.
ID(list.ID).
Cols(colsToUpdate...).
Update(list)
if err != nil {
return err
}
err = events.Dispatch(&ListUpdatedEvent{
List: list,
Doer: auth,
})
if err != nil {
return err
}
l, err := GetListSimpleByID(s, list.ID)
if err != nil {
return err
}
*list = *l
err = list.ReadOne(s, auth)
return
}
// Update implements the update method of CRUDable
// @Summary Updates a list
// @Description Updates a list. This does not include adding a task (see below).
// @tags list
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "List ID"
// @Param list body models.List true "The list with updated values you want to update."
// @Success 200 {object} models.List "The updated list."
// @Failure 400 {object} web.HTTPError "Invalid list object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [post]
func (l *List) Update(s *xorm.Session, a web.Auth) (err error) {
fid := getSavedFilterIDFromListID(l.ID)
if fid > 0 {
f, err := getSavedFilterSimpleByID(s, fid)
if err != nil {
return err
}
f.Title = l.Title
f.Description = l.Description
f.IsFavorite = l.IsFavorite
err = f.Update(s, a)
if err != nil {
return err
}
*l = *f.toList()
return nil
}
return UpdateList(s, l, a, false)
}
func updateListLastUpdated(s *xorm.Session, list *List) error {
_, err := s.ID(list.ID).Cols("updated").Update(list)
return err
}
func updateListByTaskID(s *xorm.Session, taskID int64) (err error) {
// need to get the task to update the list last updated timestamp
task, err := GetTaskByIDSimple(s, taskID)
if err != nil {
return err
}
return updateListLastUpdated(s, &List{ID: task.ListID})
}
// Create implements the create method of CRUDable
// @Summary Creates a new list
// @Description Creates a new list in a given namespace. The user needs write-access to the namespace.
// @tags list
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param namespaceID path int true "Namespace ID"
// @Param list body models.List true "The list you want to create."
// @Success 201 {object} models.List "The created list."
// @Failure 400 {object} web.HTTPError "Invalid list object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/lists [put]
func (l *List) Create(s *xorm.Session, a web.Auth) (err error) {
err = CreateList(s, l, a)
if err != nil {
return
}
return l.ReadOne(s, a)
}
// Delete implements the delete method of CRUDable
// @Summary Deletes a list
// @Description Delets a list
// @tags list
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "List ID"
// @Success 200 {object} models.Message "The list was successfully deleted."
// @Failure 400 {object} web.HTTPError "Invalid list object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [delete]
func (l *List) Delete(s *xorm.Session, a web.Auth) (err error) {
// Delete the list
_, err = s.ID(l.ID).Delete(&List{})
if err != nil {
return
}
// Delete all tasks on that list
// Using the loop to make sure all related entities to all tasks are properly deleted as well.
tasks, _, _, err := getRawTasksForLists(s, []*List{l}, a, &taskOptions{})
if err != nil {
return
}
for _, task := range tasks {
err = task.Delete(s, a)
if err != nil {
return err
}
}
return events.Dispatch(&ListDeletedEvent{
List: l,
Doer: a,
})
}
// SetListBackground sets a background file as list background in the db
func SetListBackground(s *xorm.Session, listID int64, background *files.File, blurHash string) (err error) {
l := &List{
ID: listID,
BackgroundFileID: background.ID,
BackgroundBlurHash: blurHash,
}
_, err = s.
Where("id = ?", l.ID).
Cols("background_file_id", "background_blur_hash").
Update(l)
return
}

View File

@ -33,8 +33,8 @@ import (
// RegisterListeners registers all event listeners
func RegisterListeners() {
events.RegisterListener((&ListCreatedEvent{}).Name(), &IncreaseListCounter{})
events.RegisterListener((&ListDeletedEvent{}).Name(), &DecreaseListCounter{})
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{})
@ -44,13 +44,22 @@ func RegisterListeners() {
events.RegisterListener((&TaskCommentCreatedEvent{}).Name(), &SendTaskCommentNotification{})
events.RegisterListener((&TaskAssigneeCreatedEvent{}).Name(), &SendTaskAssignedNotification{})
events.RegisterListener((&TaskDeletedEvent{}).Name(), &SendTaskDeletedNotification{})
events.RegisterListener((&ListCreatedEvent{}).Name(), &SendListCreatedNotification{})
events.RegisterListener((&ProjectCreatedEvent{}).Name(), &SendProjectCreatedNotification{})
events.RegisterListener((&TaskAssigneeCreatedEvent{}).Name(), &SubscribeAssigneeToTask{})
events.RegisterListener((&TeamMemberAddedEvent{}).Name(), &SendTeamMemberAddedNotification{})
events.RegisterListener((&TaskCommentUpdatedEvent{}).Name(), &HandleTaskCommentEditMentions{})
events.RegisterListener((&TaskCreatedEvent{}).Name(), &HandleTaskCreateMentions{})
events.RegisterListener((&TaskUpdatedEvent{}).Name(), &HandleTaskUpdatedMentions{})
events.RegisterListener((&UserDataExportRequestedEvent{}).Name(), &HandleUserDataExport{})
events.RegisterListener((&TaskCommentCreatedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
events.RegisterListener((&TaskCommentUpdatedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
events.RegisterListener((&TaskCommentDeletedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
events.RegisterListener((&TaskAssigneeCreatedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
events.RegisterListener((&TaskAssigneeDeletedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
events.RegisterListener((&TaskAttachmentCreatedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
events.RegisterListener((&TaskAttachmentDeletedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
events.RegisterListener((&TaskRelationCreatedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
events.RegisterListener((&TaskRelationDeletedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
}
//////
@ -403,48 +412,101 @@ func (s *HandleTaskUpdatedMentions) Handle(msg *message.Message) (err error) {
Doer: event.Doer,
IsNew: false,
}
_, err = notifyMentionedUsers(sess, event.Task, event.Task.Description, n)
return err
}
// HandleTaskUpdateLastUpdated represents a listener
type HandleTaskUpdateLastUpdated struct {
}
// Name defines the name for the HandleTaskUpdateLastUpdated listener
func (s *HandleTaskUpdateLastUpdated) Name() string {
return "handle.task.update.last.updated"
}
// Handle is executed when the event HandleTaskUpdateLastUpdated listens on is fired
func (s *HandleTaskUpdateLastUpdated) Handle(msg *message.Message) (err error) {
// Using a map here allows us to plug this listener to all kinds of task events
event := map[string]interface{}{}
err = json.Unmarshal(msg.Payload, &event)
if err != nil {
return err
}
task, is := event["Task"].(map[string]interface{})
if !is {
log.Errorf("Event payload does not contain task ID")
return
}
taskID, is := task["id"]
if !is {
log.Errorf("Event payload does not contain a valid task ID")
return
}
var taskIDInt int64
switch v := taskID.(type) {
case int64:
taskIDInt = v
case int:
taskIDInt = int64(v)
case int32:
taskIDInt = int64(v)
case float64:
taskIDInt = int64(v)
case float32:
taskIDInt = int64(v)
default:
log.Errorf("Event payload does not contain a valid task ID")
return
}
sess := db.NewSession()
defer sess.Close()
return updateTaskLastUpdated(sess, &Task{ID: taskIDInt})
}
///////
// List Event Listeners
// Project Event Listeners
type IncreaseListCounter struct {
type IncreaseProjectCounter struct {
}
func (s *IncreaseListCounter) Name() string {
return "list.counter.increase"
func (s *IncreaseProjectCounter) Name() string {
return "project.counter.increase"
}
func (s *IncreaseListCounter) Handle(msg *message.Message) (err error) {
return keyvalue.IncrBy(metrics.ListCountKey, 1)
func (s *IncreaseProjectCounter) Handle(msg *message.Message) (err error) {
return keyvalue.IncrBy(metrics.ProjectCountKey, 1)
}
type DecreaseListCounter struct {
type DecreaseProjectCounter struct {
}
func (s *DecreaseListCounter) Name() string {
return "list.counter.decrease"
func (s *DecreaseProjectCounter) Name() string {
return "project.counter.decrease"
}
func (s *DecreaseListCounter) Handle(msg *message.Message) (err error) {
return keyvalue.DecrBy(metrics.ListCountKey, 1)
func (s *DecreaseProjectCounter) Handle(msg *message.Message) (err error) {
return keyvalue.DecrBy(metrics.ProjectCountKey, 1)
}
// SendListCreatedNotification represents a listener
type SendListCreatedNotification struct {
// SendProjectCreatedNotification represents a listener
type SendProjectCreatedNotification struct {
}
// Name defines the name for the SendListCreatedNotification listener
func (s *SendListCreatedNotification) Name() string {
return "send.list.created.notification"
// Name defines the name for the SendProjectCreatedNotification listener
func (s *SendProjectCreatedNotification) Name() string {
return "send.project.created.notification"
}
// Handle is executed when the event SendListCreatedNotification listens on is fired
func (s *SendListCreatedNotification) Handle(msg *message.Message) (err error) {
event := &ListCreatedEvent{}
// Handle is executed when the event SendProjectCreatedNotification listens on is fired
func (s *SendProjectCreatedNotification) Handle(msg *message.Message) (err error) {
event := &ProjectCreatedEvent{}
err = json.Unmarshal(msg.Payload, event)
if err != nil {
return err
@ -453,21 +515,21 @@ func (s *SendListCreatedNotification) Handle(msg *message.Message) (err error) {
sess := db.NewSession()
defer sess.Close()
subscribers, err := getSubscribersForEntity(sess, SubscriptionEntityList, event.List.ID)
subscribers, err := getSubscribersForEntity(sess, SubscriptionEntityProject, event.Project.ID)
if err != nil {
return err
}
log.Debugf("Sending list created notifications to %d subscribers for list %d", len(subscribers), event.List.ID)
log.Debugf("Sending project created notifications to %d subscribers for project %d", len(subscribers), event.Project.ID)
for _, subscriber := range subscribers {
if subscriber.UserID == event.Doer.ID {
continue
}
n := &ListCreatedNotification{
Doer: event.Doer,
List: event.List,
n := &ProjectCreatedNotification{
Doer: event.Doer,
Project: event.Project,
}
err = notifications.Notify(subscriber.User, n)
if err != nil {

View File

@ -103,7 +103,7 @@ func TestSendingMentionNotification(t *testing.T) {
assert.NoError(t, err)
tc := &TaskComment{
Comment: "Lorem Ipsum @user1 @user2 @user3 @user4 @user5 @user6",
TaskID: 32, // user2 has access to the list that task belongs to
TaskID: 32, // user2 has access to the project that task belongs to
}
err = tc.Create(s, u)
assert.NoError(t, err)
@ -156,7 +156,7 @@ func TestSendingMentionNotification(t *testing.T) {
assert.NoError(t, err)
tc := &TaskComment{
Comment: "Lorem Ipsum @user2",
TaskID: 32, // user2 has access to the list that task belongs to
TaskID: 32, // user2 has access to the project that task belongs to
}
err = tc.Create(s, u)
assert.NoError(t, err)

View File

@ -39,14 +39,14 @@ var (
// GetTables returns all structs which are also a table.
func GetTables() []interface{} {
return []interface{}{
&List{},
&Project{},
&Task{},
&Team{},
&TeamMember{},
&TeamList{},
&TeamProject{},
&TeamNamespace{},
&Namespace{},
&ListUser{},
&ProjectUser{},
&NamespaceUser{},
&TaskAssginee{},
&Label{},

View File

@ -61,27 +61,27 @@ type Namespace struct {
// A timestamp when this namespace was last updated. You cannot change this value.
Updated time.Time `xorm:"updated not null" json:"updated"`
// If set to true, will only return the namespaces, not their lists.
// 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:"-"`
}
// SharedListsPseudoNamespace is a pseudo namespace used to hold shared lists
var SharedListsPseudoNamespace = Namespace{
// SharedProjectsPseudoNamespace is a pseudo namespace used to hold shared projects
var SharedProjectsPseudoNamespace = Namespace{
ID: -1,
Title: "Shared Lists",
Description: "Lists of other users shared with you via teams or directly.",
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 lists and tasks
// FavoritesPseudoNamespace is a pseudo namespace used to hold favorited projects and tasks
var FavoritesPseudoNamespace = Namespace{
ID: -2,
Title: "Favorites",
Description: "Favorite lists and tasks.",
Description: "Favorite projects and tasks.",
Created: time.Now(),
Updated: time.Now(),
}
@ -106,9 +106,9 @@ func getNamespaceSimpleByID(s *xorm.Session, id int64) (namespace *Namespace, er
return nil, ErrNamespaceDoesNotExist{ID: id}
}
// Get the namesapce with shared lists
// Get the namesapce with shared projects
if id == -1 {
return &SharedListsPseudoNamespace, nil
return &SharedProjectsPseudoNamespace, nil
}
if id == FavoritesPseudoNamespace.ID {
@ -181,24 +181,24 @@ func (n *Namespace) ReadOne(s *xorm.Session, a web.Auth) (err error) {
return
}
// NamespaceWithLists represents a namespace with list meta informations
type NamespaceWithLists struct {
// NamespaceWithProjects represents a namespace with project meta informations
type NamespaceWithProjects struct {
Namespace `xorm:"extends"`
Lists []*List `xorm:"-" json:"lists"`
Projects []*Project `xorm:"-" json:"projects"`
}
type NamespaceWithListsAndTasks struct {
type NamespaceWithProjectsAndTasks struct {
Namespace
Lists []*ListWithTasksAndBuckets `xorm:"-" json:"lists"`
Projects []*ProjectWithTasksAndBuckets `xorm:"-" json:"projects"`
}
func makeNamespaceSlice(namespaces map[int64]*NamespaceWithLists, userMap map[int64]*user.User, subscriptions map[int64]*Subscription) []*NamespaceWithLists {
all := make([]*NamespaceWithLists, 0, len(namespaces))
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.Lists {
for _, l := range n.Projects {
if n.Subscription != nil && l.Subscription == nil {
l.Subscription = n.Subscription
}
@ -253,7 +253,7 @@ func getNamespaceArchivedCond(archived bool) builder.Cond {
return isArchivedCond
}
func getNamespacesWithLists(s *xorm.Session, namespaces *map[int64]*NamespaceWithLists, search string, isArchived bool, page, perPage int, userID int64) (numberOfTotalItems int64, err error) {
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)
@ -289,11 +289,11 @@ func getNamespacesWithLists(s *xorm.Session, namespaces *map[int64]*NamespaceWit
GroupBy("namespaces.id").
Where(filterCond).
Where(isArchivedCond).
Count(&NamespaceWithLists{})
Count(&NamespaceWithProjects{})
return numberOfTotalItems, err
}
func getNamespaceOwnerIDs(namespaces map[int64]*NamespaceWithLists) (namespaceIDs, ownerIDs []int64) {
func getNamespaceOwnerIDs(namespaces map[int64]*NamespaceWithProjects) (namespaceIDs, ownerIDs []int64) {
for _, nsp := range namespaces {
namespaceIDs = append(namespaceIDs, nsp.ID)
ownerIDs = append(ownerIDs, nsp.OwnerID)
@ -324,36 +324,36 @@ func getNamespaceSubscriptions(s *xorm.Session, namespaceIDs []int64, userID int
return subscriptionsMap, err
}
func getListsForNamespaces(s *xorm.Session, namespaceIDs []int64, archived bool) ([]*List, error) {
lists := []*List{}
listQuery := s.
func getProjectsForNamespaces(s *xorm.Session, namespaceIDs []int64, archived bool) ([]*Project, error) {
projects := []*Project{}
projectQuery := s.
OrderBy("position").
In("namespace_id", namespaceIDs)
if !archived {
listQuery.And("is_archived = false")
projectQuery.And("is_archived = false")
}
err := listQuery.Find(&lists)
return lists, err
err := projectQuery.Find(&projects)
return projects, err
}
func getSharedListsInNamespace(s *xorm.Session, archived bool, doer *user.User) (sharedListsNamespace *NamespaceWithLists, err error) {
// Create our pseudo namespace to hold the shared lists
sharedListsPseudonamespace := SharedListsPseudoNamespace
sharedListsPseudonamespace.OwnerID = doer.ID
sharedListsNamespace = &NamespaceWithLists{
sharedListsPseudonamespace,
[]*List{},
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 lists individually shared with our user (not via a namespace)
individualLists := []*List{}
iListQuery := s.Select("l.*").
Table("lists").
// 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_lists", "tl"}, "l.id = tl.list_id").
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_lists", "ul"}, "ul.list_id = l.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},
@ -364,53 +364,53 @@ func getSharedListsInNamespace(s *xorm.Session, archived bool, doer *user.User)
)).
GroupBy("l.id")
if !archived {
iListQuery.And("l.is_archived = false")
iProjectQuery.And("l.is_archived = false")
}
err = iListQuery.Find(&individualLists)
err = iProjectQuery.Find(&individualProjects)
if err != nil {
return
}
// Make the namespace -1 so we now later which one it was
// + Append it to all lists we already have
for _, l := range individualLists {
l.NamespaceID = sharedListsNamespace.ID
// + Append it to all projects we already have
for _, l := range individualProjects {
l.NamespaceID = sharedProjectsNamespace.ID
}
sharedListsNamespace.Lists = individualLists
sharedProjectsNamespace.Projects = individualProjects
// Remove the sharedListsPseudonamespace if we don't have any shared lists
if len(individualLists) == 0 {
sharedListsNamespace = nil
// Remove the sharedProjectsPseudonamespace if we don't have any shared projects
if len(individualProjects) == 0 {
sharedProjectsNamespace = nil
}
return
}
func getFavoriteLists(s *xorm.Session, lists []*List, namespaceIDs []int64, doer *user.User) (favoriteNamespace *NamespaceWithLists, err error) {
// Create our pseudo namespace with favorite lists
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 = &NamespaceWithLists{
favoriteNamespace = &NamespaceWithProjects{
Namespace: pseudoFavoriteNamespace,
Lists: []*List{{}},
Projects: []*Project{{}},
}
*favoriteNamespace.Lists[0] = FavoritesPseudoList // Copying the list to be able to modify it later
favoriteNamespace.Lists[0].Owner = doer
*favoriteNamespace.Projects[0] = FavoritesPseudoProject // Copying the project to be able to modify it later
favoriteNamespace.Projects[0].Owner = doer
for _, list := range lists {
if !list.IsFavorite {
for _, project := range projects {
if !project.IsFavorite {
continue
}
favoriteNamespace.Lists = append(favoriteNamespace.Lists, list)
favoriteNamespace.Projects = append(favoriteNamespace.Projects, project)
}
// Check if we have any favorites or favorited lists and remove the favorites namespace from the list if not
// 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", "lists", "tasks.list_id = lists.id").
Join("INNER", "namespaces", "lists.namespace_id = namespaces.id").
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
@ -425,25 +425,25 @@ func getFavoriteLists(s *xorm.Session, lists []*List, namespaceIDs []int64, doer
return
}
// If we don't have any favorites in the favorites pseudo list, remove that pseudo list from the namespace
// 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.Lists {
if l.ID == FavoritesPseudoList.ID {
favoriteNamespace.Lists = append(favoriteNamespace.Lists[:in], favoriteNamespace.Lists[in+1:]...)
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.Lists) == 0 {
if len(favoriteNamespace.Projects) == 0 {
return nil, nil
}
return
}
func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *NamespaceWithLists, err error) {
func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *NamespaceWithProjects, err error) {
savedFilters, err := getSavedFiltersForUser(s, doer)
if err != nil {
return
@ -455,16 +455,16 @@ func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *N
savedFiltersPseudoNamespace := SavedFiltersPseudoNamespace
savedFiltersPseudoNamespace.OwnerID = doer.ID
savedFiltersNamespace = &NamespaceWithLists{
savedFiltersNamespace = &NamespaceWithProjects{
Namespace: savedFiltersPseudoNamespace,
Lists: make([]*List, 0, len(savedFilters)),
Projects: make([]*Project, 0, len(savedFilters)),
}
for _, filter := range savedFilters {
filterList := filter.toList()
filterList.NamespaceID = savedFiltersNamespace.ID
filterList.Owner = doer
savedFiltersNamespace.Lists = append(savedFiltersNamespace.Lists, filterList)
filterProject := filter.toProject()
filterProject.NamespaceID = savedFiltersNamespace.ID
filterProject.Owner = doer
savedFiltersNamespace.Projects = append(savedFiltersNamespace.Projects, filterProject)
}
return
@ -480,9 +480,9 @@ func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *N
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search namespaces by name."
// @Param is_archived query bool false "If true, also returns all archived namespaces."
// @Param namespaces_only query bool false "If true, also returns only namespaces without their lists."
// @Param namespaces_only query bool false "If true, also returns only namespaces without their projects."
// @Security JWTKeyAuth
// @Success 200 {array} models.NamespaceWithLists "The Namespaces."
// @Success 200 {array} models.NamespaceWithProjects "The Namespaces."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces [get]
//
@ -492,19 +492,19 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
return nil, 0, 0, ErrGenericForbidden{}
}
// This map will hold all namespaces and their lists. The key is usually the id of the namespace.
// We're using a map here because it makes a few things like adding lists or removing pseudo namespaces easier.
namespaces := make(map[int64]*NamespaceWithLists)
// 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)
//////////////////////////////
// Lists with their namespaces
// Projects with their namespaces
doer, err := user.GetFromAuth(a)
if err != nil {
return nil, 0, 0, err
}
numberOfTotalItems, err = getNamespacesWithLists(s, &namespaces, search, n.IsArchived, page, perPage, doer.ID)
numberOfTotalItems, err = getNamespacesWithProjects(s, &namespaces, search, n.IsArchived, page, perPage, doer.ID)
if err != nil {
return nil, 0, 0, err
}
@ -531,23 +531,23 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
return all, len(all), numberOfTotalItems, nil
}
// Get all lists
lists, err := getListsForNamespaces(s, namespaceIDs, n.IsArchived)
// Get all projects
projects, err := getProjectsForNamespaces(s, namespaceIDs, n.IsArchived)
if err != nil {
return nil, 0, 0, err
}
///////////////
// Shared Lists
// Shared Projects
sharedListsNamespace, err := getSharedListsInNamespace(s, n.IsArchived, doer)
sharedProjectsNamespace, err := getSharedProjectsInNamespace(s, n.IsArchived, doer)
if err != nil {
return nil, 0, 0, err
}
if sharedListsNamespace != nil {
namespaces[sharedListsNamespace.ID] = sharedListsNamespace
lists = append(lists, sharedListsNamespace.Lists...)
if sharedProjectsNamespace != nil {
namespaces[sharedProjectsNamespace.ID] = sharedProjectsNamespace
projects = append(projects, sharedProjectsNamespace.Projects...)
}
/////////////////
@ -560,20 +560,20 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
if savedFiltersNamespace != nil {
namespaces[savedFiltersNamespace.ID] = savedFiltersNamespace
lists = append(lists, savedFiltersNamespace.Lists...)
projects = append(projects, savedFiltersNamespace.Projects...)
}
/////////////////
// Add list details (favorite state, among other things)
err = addListDetails(s, lists, a)
// Add project details (favorite state, among other things)
err = addProjectDetails(s, projects, a)
if err != nil {
return
}
/////////////////
// Favorite lists
// Favorite projects
favoritesNamespace, err := getFavoriteLists(s, lists, namespaceIDs, doer)
favoritesNamespace, err := getFavoriteProjects(s, projects, namespaceIDs, doer)
if err != nil {
return nil, 0, 0, err
}
@ -585,12 +585,12 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
//////////////////////
// Put it all together
for _, list := range lists {
if list.NamespaceID == SharedListsPseudoNamespace.ID || list.NamespaceID == SavedFiltersPseudoNamespace.ID {
// Shared lists and filtered lists are already in the namespace
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[list.NamespaceID].Lists = append(namespaces[list.NamespaceID].Lists, list)
namespaces[project.NamespaceID].Projects = append(namespaces[project.NamespaceID].Projects, project)
}
all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap)
@ -663,7 +663,7 @@ func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) {
return deleteNamespace(s, n, a, true)
}
func deleteNamespace(s *xorm.Session, n *Namespace, a web.Auth, withLists bool) (err error) {
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 {
@ -681,23 +681,23 @@ func deleteNamespace(s *xorm.Session, n *Namespace, a web.Auth, withLists bool)
Doer: a,
}
if !withLists {
if !withProjects {
return events.Dispatch(namespaceDeleted)
}
// Delete all lists with their tasks
lists, err := GetListsByNamespaceID(s, n.ID, &user.User{})
// Delete all projects with their tasks
projects, err := GetProjectsByNamespaceID(s, n.ID, &user.User{})
if err != nil {
return
}
if len(lists) == 0 {
if len(projects) == 0 {
return events.Dispatch(namespaceDeleted)
}
// Looping over all lists to let the list handle properly cleaning up the tasks and everything else associated with it.
for _, list := range lists {
err = list.Delete(s, a)
// 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
}
@ -748,7 +748,7 @@ func (n *Namespace) Update(s *xorm.Session, a web.Auth) (err error) {
}
}
// We need to specify the cols we want to update here to be able to un-archive lists
// We need to specify the cols we want to update here to be able to un-archive projects
colsToUpdate := []string{
"title",
"is_archived",

View File

@ -73,7 +73,7 @@ func (n *Namespace) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bo
}
if a.GetID() == nn.OwnerID ||
nn.ID == SharedListsPseudoNamespace.ID ||
nn.ID == SharedProjectsPseudoNamespace.ID ||
nn.ID == FavoritesPseudoNamespace.ID ||
nn.ID == SavedFiltersPseudoNamespace.ID {
return true, int(RightAdmin), nil
@ -88,7 +88,7 @@ func (n *Namespace) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bo
for each passed right. That way, we can check with a single sql query (instead if 8)
if the user has the right to see the list or not.
if the user has the right to see the project or not.
*/
var conds []builder.Cond

View File

@ -72,7 +72,7 @@ func TestTeamNamespace_ReadAll(t *testing.T) {
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
teams, _, _, err := tn.ReadAll(s, u, "READ_only_on_list6", 1, 50)
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)

View File

@ -207,21 +207,21 @@ func TestNamespace_ReadAll(t *testing.T) {
n := &Namespace{}
nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
assert.NoError(t, err)
namespaces := nn.([]*NamespaceWithLists)
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 list and namespace are not archived
// Ensure every project and namespace are not archived
for _, namespace := range namespaces {
assert.False(t, namespace.IsArchived)
for _, list := range namespace.Lists {
assert.False(t, list.IsArchived)
for _, project := range namespace.Projects {
assert.False(t, project.IsArchived)
}
}
})
t.Run("no own shared lists", func(t *testing.T) {
t.Run("no own shared projects", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
@ -229,18 +229,18 @@ func TestNamespace_ReadAll(t *testing.T) {
n := &Namespace{}
nn, _, _, err := n.ReadAll(s, user6, "", 1, -1)
assert.NoError(t, err)
namespaces := nn.([]*NamespaceWithLists)
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
sharedListOccurences := make(map[int64]int64)
for _, list := range namespaces[1].Lists {
assert.NotEqual(t, user1.ID, list.OwnerID)
sharedListOccurences[list.ID]++
sharedProjectOccurences := make(map[int64]int64)
for _, project := range namespaces[1].Projects {
assert.NotEqual(t, user1.ID, project.OwnerID)
sharedProjectOccurences[project.ID]++
}
for listID, occ := range sharedListOccurences {
assert.Equal(t, int64(1), occ, "shared list %d is present %d times, should be 1", listID, occ)
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) {
@ -253,12 +253,12 @@ func TestNamespace_ReadAll(t *testing.T) {
}
nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
assert.NoError(t, err)
namespaces := nn.([]*NamespaceWithLists)
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 lists
// Ensure every namespace does not contain projects
for _, namespace := range namespaces {
assert.Nil(t, namespace.Lists)
assert.Nil(t, namespace.Projects)
}
})
t.Run("ids only", func(t *testing.T) {
@ -271,7 +271,7 @@ func TestNamespace_ReadAll(t *testing.T) {
}
nn, _, _, err := n.ReadAll(s, user7, "13,14", 1, -1)
assert.NoError(t, err)
namespaces := nn.([]*NamespaceWithLists)
namespaces := nn.([]*NamespaceWithProjects)
assert.NotNil(t, namespaces)
assert.Len(t, namespaces, 2)
assert.Equal(t, int64(13), namespaces[0].ID)
@ -287,7 +287,7 @@ func TestNamespace_ReadAll(t *testing.T) {
}
nn, _, _, err := n.ReadAll(s, user1, "1,w", 1, -1)
assert.NoError(t, err)
namespaces := nn.([]*NamespaceWithLists)
namespaces := nn.([]*NamespaceWithProjects)
assert.NotNil(t, namespaces)
assert.Len(t, namespaces, 1)
assert.Equal(t, int64(1), namespaces[0].ID)
@ -301,7 +301,7 @@ func TestNamespace_ReadAll(t *testing.T) {
IsArchived: true,
}
nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
namespaces := nn.([]*NamespaceWithLists)
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
@ -316,7 +316,7 @@ func TestNamespace_ReadAll(t *testing.T) {
n := &Namespace{}
nn, _, _, err := n.ReadAll(s, user11, "", 1, -1)
namespaces := nn.([]*NamespaceWithLists)
namespaces := nn.([]*NamespaceWithProjects)
assert.NoError(t, err)
// Assert the first namespace is not the favorites namespace
assert.NotEqual(t, FavoritesPseudoNamespace.ID, namespaces[0].ID)
@ -328,11 +328,11 @@ func TestNamespace_ReadAll(t *testing.T) {
n := &Namespace{}
nn, _, _, err := n.ReadAll(s, user12, "", 1, -1)
namespaces := nn.([]*NamespaceWithLists)
namespaces := nn.([]*NamespaceWithProjects)
assert.NoError(t, err)
// Assert the first namespace is the favorites namespace and contains lists
// 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].Lists)
assert.NotEqual(t, 0, namespaces[0].Projects)
})
t.Run("no saved filters", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
@ -341,7 +341,7 @@ func TestNamespace_ReadAll(t *testing.T) {
n := &Namespace{}
nn, _, _, err := n.ReadAll(s, user11, "", 1, -1)
namespaces := nn.([]*NamespaceWithLists)
namespaces := nn.([]*NamespaceWithProjects)
assert.NoError(t, err)
// Assert the first namespace is not the favorites namespace
assert.NotEqual(t, SavedFiltersPseudoNamespace.ID, namespaces[0].ID)
@ -364,7 +364,7 @@ func TestNamespace_ReadAll(t *testing.T) {
n := &Namespace{}
nn, _, _, err := n.ReadAll(s, user6, "NamespACE7", 1, -1)
assert.NoError(t, err)
namespaces := nn.([]*NamespaceWithLists)
namespaces := nn.([]*NamespaceWithProjects)
assert.NotNil(t, namespaces)
assert.Len(t, namespaces, 2)
assert.Equal(t, int64(7), namespaces[1].ID)

Some files were not shown because too many files have changed in this diff Show More