Compare commits

...

176 Commits

Author SHA1 Message Date
kolaente 4f05061cc3
fix: lint 2023-01-24 14:50:53 +01:00
kolaente 14d8e2d586
fix: lint 2023-01-24 14:21:49 +01:00
kolaente 37240caad4
fix: lint 2023-01-23 19:14:56 +01:00
kolaente b96b10dc70
fix(migration): bullet-proof csv parsing for TickTick import 2023-01-23 19:13:07 +01:00
kooshi af9faad25a fix ticktick migration, still need to update test to match 2023-01-05 20:31:15 -06: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
kolaente b10dbce1a1
chore: release preparations 2022-10-28 12:24:01 +02:00
renovate 7b77974b03 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.16 (#1301)
Reviewed-on: vikunja/api#1301
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 06:32:09 +00:00
renovate 05358350af fix(deps): update module github.com/wneessen/go-mail to v0.3.4 (#1302)
Reviewed-on: vikunja/api#1302
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 06:31:24 +00:00
kolaente 9fc08a0790
fix(lists): return correct max right for lists where the user has created the namespace 2022-10-25 18:54:11 +02:00
renovate f6b897e8e7 fix(deps): update module github.com/wneessen/go-mail to v0.3.3 (#1300)
Reviewed-on: vikunja/api#1300
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-25 16:50:06 +00:00
renovate 8de78c48f8 fix(deps): update module github.com/spf13/cobra to v1.6.1 (#1299)
Reviewed-on: vikunja/api#1299
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-25 05:23:24 +00:00
renovate a13126d1dd fix(deps): update module github.com/stretchr/testify to v1.8.1 (#1298)
Reviewed-on: vikunja/api#1298
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-24 07:51:01 +00:00
renovate b96e681270 fix(deps): update module github.com/wneessen/go-mail to v0.3.2 (#1297)
Reviewed-on: vikunja/api#1297
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-21 12:59:46 +00:00
kolaente f5fd849a0b
chore: remove unused dependencies 2022-10-21 12:30:54 +02:00
renovate 144e115394 fix(deps): update module golang.org/x/image to v0.1.0 (#1293)
Reviewed-on: vikunja/api#1293
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-19 20:13:14 +00:00
renovate 815fc10135 fix(deps): update module golang.org/x/crypto to v0.1.0 (#1295)
Reviewed-on: vikunja/api#1295
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-19 19:19:47 +00:00
renovate 955a1771ae fix(deps): update module golang.org/x/oauth2 to v0.1.0 (#1296)
Reviewed-on: vikunja/api#1296
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-19 19:06:22 +00:00
renovate aca930655b chore(deps): update module github.com/coreos/go-systemd/v22 to v22.4.0 (#1287)
Reviewed-on: vikunja/api#1287
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-18 11:41:04 +00:00
renovate 2ba78d240f fix(deps): update golang.org/x/term digest to 8365914 (#1289)
Reviewed-on: vikunja/api#1289
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-18 09:15:44 +00:00
renovate cad18945bb fix(deps): update module github.com/swaggo/swag to v1.8.7 (#1290)
Reviewed-on: vikunja/api#1290
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-18 08:32:38 +00:00
renovate 9fc0fc184d fix(deps): update module golang.org/x/sync to v0.1.0 (#1291)
Reviewed-on: vikunja/api#1291
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-18 08:00:56 +00:00
renovate 1577c8d3f3 fix(deps): update golang.org/x/image digest to ffcb3fe (#1288)
Reviewed-on: vikunja/api#1288
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-18 07:54:52 +00:00
renovate 2eb4d07aa9 fix(deps): update golang.org/x/oauth2 digest to 6fdb5e3 (#1284)
Reviewed-on: vikunja/api#1284
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-14 16:56:38 +00:00
renovate 4789a69455 fix(deps): update golang.org/x/sys digest to 95e765b (#1283)
Reviewed-on: vikunja/api#1283
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-14 13:28:36 +00:00
renovate 35f01a4549 fix(deps): update module github.com/labstack/echo/v4 to v4.9.1 (#1282)
Reviewed-on: vikunja/api#1282
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-12 22:17:30 +00:00
renovate dca51c762b fix(deps): update module github.com/wneessen/go-mail to v0.3.1 (#1281)
Reviewed-on: vikunja/api#1281
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-12 16:14:26 +00:00
kolaente 6515dd6908
chore(deps): update golang.org/x/text to v0.3.8 2022-10-12 16:47:07 +02:00
renovate 0ea4de3f56 fix(deps): update golang.org/x/crypto digest to 56aed06 (#1280)
Reviewed-on: vikunja/api#1280
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-12 14:14:04 +00:00
renovate c2104a3374 fix(deps): update module github.com/wneessen/go-mail to v0.3.0 (#1278)
Reviewed-on: vikunja/api#1278
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-11 19:18:03 +00:00
renovate aaceb4e968 fix(deps): update module github.com/spf13/cobra to v1.6.0 (#1277)
Reviewed-on: vikunja/api#1277
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-11 16:19:16 +00:00
renovate 4ec4c0a65d fix(deps): update golang.org/x/sys digest to 090e330 (#1276)
Reviewed-on: vikunja/api#1276
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-10 20:06:26 +00:00
renovate c68bd235e8 fix(deps): update golang.org/x/crypto digest to d6f0a8c (#1275)
Reviewed-on: vikunja/api#1275
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-10 20:05:33 +00:00
konrad df2e36c2a3 feat: TickTick migrator (#1273)
Reviewed-on: vikunja/api#1273
2022-10-09 21:12:30 +00:00
kolaente f5a33478f2
fix(migration): make sure importing works when the csv file has errors and don't try to parse empty values as dates 2022-10-09 22:58:08 +02:00
kolaente 0d044997df
fix(migration): expose ticktick migrator to /info 2022-10-09 22:45:01 +02:00
kolaente 5e40f4ec89
fix(migration): properly parse duration 2022-10-09 22:44:57 +02:00
kolaente 5871d32c2d
feat(migration): generate swagger docs 2022-10-09 22:44:54 +02:00
kolaente 3af9855148
feat(migration): add routes for TickTick migrator 2022-10-09 22:44:49 +02:00
kolaente e5394d6d4b
feat(migration): add TickTick migrator 2022-10-09 22:44:32 +02:00
kolaente b8769c746c
feat: allow a user to remove themselves from a team 2022-10-09 16:39:40 +02:00
renovate b331fdd29a chore(deps): update dependency klakegg/hugo to v0.104.2 (#1267)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1267
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-07 20:56:01 +00:00
kolaente 2fc690a783
fix: make sure list subscriptions are set correctly when their namespace has a subscription already 2022-10-07 14:18:36 +02:00
renovate bcb286a7f0 fix(deps): update golang.org/x/sys digest to 84dc82d (#1271)
Reviewed-on: vikunja/api#1271
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-07 11:16:41 +00:00
kolaente 008908eb49
fix: make sure a user can only be assigned once to a task
See https://community.vikunja.io/t/task-can-be-assigned-twice-or-more-to-the-same-user/883
2022-10-06 18:22:19 +02:00
renovate 12e0e12bae fix(deps): update golang.org/x/oauth2 digest to b44042a (#1270)
Reviewed-on: vikunja/api#1270
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-06 16:15:22 +00:00
konrad d43762e9d9 feat(task): add cover image attachment id property (#1263)
Reviewed-on: vikunja/api#1263
2022-10-05 13:27:55 +00:00
kolaente 631a265d2d
feat: add proper checks and errors to see if an attachment belongs to the task it's being used as cover image in 2022-10-05 15:12:29 +02:00
kolaente e113fe34d0
chore: generate swagger docs 2022-10-05 15:12:29 +02:00
kolaente 0eb47096db
fix: make cover image id actually updatable 2022-10-05 15:12:29 +02:00
kolaente 0e1904d50b
fix: make cover image id actually updatable 2022-10-05 15:12:29 +02:00
kolaente b4b25499f2
feat(task): add cover image attachment id property 2022-10-05 15:12:29 +02:00
renovate b735ffc4b3 fix(deps): update golang.org/x/crypto digest to 4161e89 (#1268)
Reviewed-on: vikunja/api#1268
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-05 07:47:53 +00:00
renovate 95105aaa35 fix(deps): update module github.com/labstack/gommon to v0.4.0 (#1269)
Reviewed-on: vikunja/api#1269
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-05 07:47:28 +00:00
renovate 66331b1002 fix(deps): update module github.com/getsentry/sentry-go to v0.14.0 (#1266)
Reviewed-on: vikunja/api#1266
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-04 19:39:56 +00:00
renovate ed6a27da6a chore(deps): update dependency klakegg/hugo to v0.102.3 (#1265)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1265
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-04 12:04:55 +00:00
kolaente b5ee39b887
fix(ci): make sure release os packages are properly named 2022-10-02 16:30:06 +02:00
kolaente 0d8451ab6e
fix(ci): make sure release zip files have a .zip ending 2022-10-02 16:30:06 +02:00
renovate 81f09f7dc0 fix(deps): update module github.com/wneessen/go-mail to v0.2.9 (#1264)
Reviewed-on: vikunja/api#1264
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-02 12:19:23 +00:00
kolaente 5a40100ac5
feat: provide default user settings for new users via config 2022-10-02 11:00:58 +02:00
renovate 0694314e52 fix(deps): update module github.com/swaggo/swag to v1.8.6 (#1243)
Reviewed-on: vikunja/api#1243
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-02 08:40:47 +00:00
renovate a547a9eb25 fix(deps): update module github.com/magefile/mage to v1.14.0 (#1259)
Reviewed-on: vikunja/api#1259
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-02 07:29:13 +00:00
renovate 0fcd03f561 fix(deps): update module src.techknowlogick.com/xormigrate to v1.5.0 (#1262)
Reviewed-on: vikunja/api#1262
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-01 20:59:56 +00:00
renovate ffedd02b08 fix(deps): update module github.com/yuin/goldmark to v1.5.2 (#1261)
Reviewed-on: vikunja/api#1261
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-01 20:59:42 +00:00
renovate c84684a425 fix(deps): update module github.com/wneessen/go-mail to v0.2.8 (#1258)
Reviewed-on: vikunja/api#1258
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-01 17:31:10 +00:00
kolaente aed560339b
fix(todoist): properly import all done tasks 2022-10-01 19:29:05 +02:00
kolaente 0612f4d0e0
feat: add gitea issue template 2022-10-01 18:14:49 +02:00
kolaente ce621ee5d6
feat: remove gitea issue template so that only the form is used 2022-10-01 18:09:08 +02:00
kolaente 9c4bb5a244
feat: add github issue templates 2022-10-01 17:53:30 +02:00
kolaente c076f73a87
fix: make sure user searches are always case-insensitive
See vikunja/frontend#2196 (comment)
Resolves https://github.com/go-vikunja/frontend/issues/29
2022-10-01 17:39:08 +02:00
Dominik Pschenitschni 36265fcedf feat(docs): document pnpm (#1251)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/api#1251
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-01 15:30:20 +00:00
konrad 53419180be feat: upgrade xorm (#1197)
Reviewed-on: vikunja/api#1197
2022-10-01 15:10:00 +00:00
renovate c5bd09702a chore(deps): update dependency golang to v1.19 (#1228)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1228
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-01 15:05:12 +00:00
kolaente fcb205a842
fix: use connection string for postgres 2022-10-01 16:57:06 +02:00
kolaente 4323803fd6
feat: upgrade xorm 2022-10-01 16:32:22 +02:00
kolaente 903b8ff438
chore: go mod tidy 2022-10-01 16:26:22 +02:00
kolaente b1fd13bbcb
feat: upgrade xorm 2022-10-01 16:25:29 +02:00
kolaente 878d19beb8
fix: make sure pseudo namespaces and lists always have the current user as owner 2022-10-01 15:19:46 +02:00
kolaente 96ed1e33e3
fix: don't allow setting a list namespace to 0
See https://github.com/go-vikunja/app/issues/13
2022-10-01 15:02:17 +02:00
renovate 374a0f9ce3 fix(deps): update module github.com/spf13/viper to v1.13.0 (#1260)
Reviewed-on: vikunja/api#1260
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-30 20:07:21 +00:00
kolaente 580bd5aeaa
fix(deps): update module github.com/lib/pq to v1.10.7 2022-09-30 19:30:18 +02:00
kolaente c359d6a97d
fix(deps): update golang.org/x/term digest to 7a66f97 2022-09-30 19:29:32 +02:00
kolaente 038702a2a0
fix(deps): update golang.org/x/image digest to e7cb969 2022-09-30 19:29:09 +02:00
kolaente 6426d40825
fix(deps): update module github.com/coreos/go-oidc/v3 to v3.4.0 2022-09-30 19:28:42 +02:00
kolaente bbe102dd57
fix(deps): update module src.techknowlogick.com/xgo to v1.5.0+1.19 2022-09-30 19:28:06 +02:00
kolaente 65484bc432
fix(deps): update golang.org/x/oauth2 digest to f213421 2022-09-30 19:27:19 +02:00
kolaente 54f6cc7a64
fix(deps): update golang.org/x/sync digest to 8fcdb60 2022-09-30 19:26:17 +02:00
kolaente f1b2338227
chore(deps): update klakegg/hugo docker tag to v0.101.0 2022-09-30 19:24:49 +02:00
kolaente 45defebcf4
fix: tasks with the same assignee as doer should not appear twice in overdue task mails 2022-09-30 18:35:40 +02:00
kolaente 86ee8273bc
chore: upgrade echo 2022-09-30 13:48:18 +02:00
Luca Bernstein 3adfeb3b34 fix(namespaces): add list subscriptions (#1254)
Add list subscriptions to namespaces call to enable frontend to show subscription state correctly.

Resolves https://github.com/go-vikunja/frontend/issues/75

Reviewed-on: vikunja/api#1254
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Luca Bernstein <luca@lucabernstein.com>
Co-committed-by: Luca Bernstein <luca@lucabernstein.com>
2022-09-29 09:49:24 +00:00
Felix Breidenstein 9bb8a26706
fix(docs): Fix redirect_url example (#50)
The name of the openid provider gets appended to the redirect_url
2022-09-28 15:34:53 +02:00
Luca Bernstein 54b7f7127c fix(caldav): no failed login emails for tokens (#1252)
Prevent Vikunja from sending mail notifications for failed login attempts if CalDav token is used.

Before, as the provided password value was tested against the user password regardless of whether it was a CalDav token, it triggered a failed login attempt email every three times.

Reviewed-on: vikunja/api#1252
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Luca Bernstein <luca@lucabernstein.com>
Co-committed-by: Luca Bernstein <luca@lucabernstein.com>
2022-09-27 15:12:37 +00:00
Luca Bernstein 25609db567 fix(mail): pass mail server timeout (#1253)
Fix error log for mailserver closing logic, as default timeout of 15 seconds of mail client package used triggers before our logic leading to error on close.

Resolves https://github.com/go-vikunja/api/issues/48

Reviewed-on: vikunja/api#1253
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Luca Bernstein <luca@lucabernstein.com>
Co-committed-by: Luca Bernstein <luca@lucabernstein.com>
2022-09-26 16:09:39 +00:00
kolaente 2e3603507c
fix(docs): document pnpm instead of yarn 2022-09-23 12:26:42 +02:00
kolaente 2efc1b5a87
feat(docs): add docs about how to deploy Vikunja in a subdirectory 2022-09-23 12:23:59 +02:00
Arie 090c67138a
fix: preserve dates for repeating tasks (#47)
Reviewed-At: https://github.com/go-vikunja/api/pull/47
2022-09-16 17:20:08 +02:00
kolaente d8f387f796
fix: don't try to compress riscv64 binaries in releases 2022-09-07 16:38:43 +02:00
kolaente aaeffe925e
fix(caldav): make sure duration and due date follow rfc5545
Related discussion: https://community.vikunja.io/t/error-with-davx-synchronization/810
2022-09-07 15:39:40 +02:00
kolaente f814dd03eb
feat: add sponsor to readme (relm) 2022-09-06 12:02:35 +02:00
kolaente 2369ce5554
fix(docs): clarify using port 25 as mail port when mail does not work 2022-09-05 17:32:48 +02:00
kolaente c19479757a
fix: properly log extra message 2022-09-01 14:19:00 +02:00
kolaente 8fddbf43ba
chore: release preparations 2022-08-17 17:04:47 +02:00
kolaente beb4d07cf9
fix: don't override saved filters 2022-08-17 17:03:01 +02:00
kolaente 10ded56f66
fix: don't fail a migration if there is no filter saved 2022-08-17 12:27:03 +02:00
95 changed files with 3473 additions and 3120 deletions

View File

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

View File

@ -132,14 +132,15 @@ steps:
event: [ push, tag, pull_request ]
- name: lint
image: golang:1.17-alpine
image: golang:1.19-alpine
pull: true
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.47.3
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.49.0
- ./mage-static check:all
when:
event: [ push, tag, pull_request ]
@ -348,8 +349,8 @@ depends_on:
- testing
workspace:
base: /go
path: src/code.vikunja.io/api
base: /source
path: /
trigger:
ref:
@ -502,8 +503,8 @@ steps:
depends_on: [ sign-release ]
# Build os packages and push it to our bucket
- name: build-os-packages
image: goreleaser/nfpm
- name: build-os-packages-unstable
image: goreleaser/nfpm:v2.22.2
pull: true
commands:
- apk add git go
@ -511,7 +512,26 @@ steps:
- mv dist/os-packages/vikunja*.x86_64.rpm dist/os-packages/vikunja-unstable-x86_64.rpm
- mv dist/os-packages/vikunja*_amd64.deb dist/os-packages/vikunja-unstable-amd64.deb
- mv dist/os-packages/vikunja*_x86_64.apk dist/os-packages/vikunja-unstable-x86_64.apk
depends_on: [ static-build-linux ]
when:
branch:
- main
event:
- push
depends_on: [ after-build-compress ]
- name: build-os-packages-version
image: goreleaser/nfpm:v2.22.2
pull: true
commands:
- apk add git go
- ./mage-static release:packages
- mv dist/os-packages/vikunja*.x86_64.rpm dist/os-packages/vikunja-${DRONE_TAG##v}-x86_64.rpm
- mv dist/os-packages/vikunja*_amd64.deb dist/os-packages/vikunja-${DRONE_TAG##v}-amd64.deb
- mv dist/os-packages/vikunja*_x86_64.apk dist/os-packages/vikunja-${DRONE_TAG##v}-x86_64.apk
when:
event:
- tag
depends_on: [ after-build-compress ]
# Push the os releases to our pseudo-s3-bucket
- name: release-os-latest
@ -534,7 +554,7 @@ steps:
- main
event:
- push
depends_on: [ build-os-packages ]
depends_on: [ build-os-packages-unstable ]
- name: release-os-version
image: plugins/s3
@ -554,44 +574,7 @@ steps:
when:
event:
- tag
depends_on: [ build-os-packages ]
### Broken, disabled until we figure out how to fix it
# - name: deb-structure
# image: kolaente/reprepro
# pull: true
# environment:
# GPG_PRIVATE_KEY:
# from_secret: gpg_privatekey
# commands:
# - export GPG_TTY=$(tty)
# - gpg -qk
# - echo "use-agent" >> ~/.gnupg/gpg.conf
# - gpgconf --kill gpg-agent
# - echo $GPG_PRIVATE_KEY > ~/frederik.gpg
# - gpg --import ~/frederik.gpg
# - mkdir debian/conf -p
# - cp build/reprepro-dist-conf debian/conf/distributions
# - ./mage-static release:reprepro
# depends_on: [ build-os-packages ]
# Push the releases to our pseudo-s3-bucket
- name: release-deb
image: plugins/s3
pull: true
settings:
bucket: vikunja-releases
access_key:
from_secret: aws_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
path_style: true
strip_prefix: debian
source: debian/*/*/*/*/*
target: /deb/
# depends_on: [ deb-structure ]
depends_on: [ build-os-packages-version ]
---
kind: pipeline
@ -622,7 +605,7 @@ steps:
- tar -xzf vikunja-theme.tar.gz
- name: build
image: klakegg/hugo:0.93.3
image: klakegg/hugo:0.104.2
pull: true
commands:
- cd docs
@ -644,102 +627,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
@ -752,7 +644,8 @@ steps:
- git fetch --tags
- name: docker-unstable
image: plugins/docker:linux-amd64
image: thegeeklab/drone-docker-buildx
privileged: true
pull: true
settings:
username:
@ -760,86 +653,36 @@ steps:
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
pull: always
image: plugins/manifest
settings:
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
- name: docker-release
image: thegeeklab/drone-docker-buildx
privileged: true
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/api
auto_tag: true
ignore_missing: true
spec: docker-manifest.tmpl
password:
from_secret: docker_password
username:
from_secret: docker_username
when:
ref:
- "refs/tags/**"
- name: manifest-release-latest
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
platforms:
- linux/386
- linux/amd64
- linux/arm/v6
- linux/arm/v7
- linux/arm64/v8
depends_on: [ fetch-tags ]
when:
ref:
- "refs/tags/**"
@ -858,9 +701,7 @@ depends_on:
- testing
- release
- deploy-docs
- docker-arm-release
- docker-amd64-release
- docker-manifest
- docker-release
steps:
- name: notify
@ -878,6 +719,6 @@ steps:
- failure
---
kind: signature
hmac: e72b631f902689777e3263ae9527e5aa47738b9021538f7cb5034f95ac265f07
hmac: f8ce17f7158088a124039f579ba10364788d306feac3feeb51689dce440d6213
...

View File

@ -1,44 +0,0 @@
<!--
Please fill out this issue template to report a bug.
If you want to propose a new feature, please open a discussion thread in the forum: https://community.vikunja.io
-->
**Version information:**
Frontend Version:
API Version:
Browser and OS Version:
**Steps to reproduce:**
<!--
Add clear steps to reproduce the bug. Provide screenshots where applicable.
-->
1.
2.
...
**Expected behavior:**
<!--
Describe what happened.
-->
**Actual behavior:**
<!--
Describe what happened instead.
-->
**Checklist:**
* [ ] I have provided all required information
* [ ] I am using the latest release or the latest unstable build
* [ ] I was able to reproduce the bug on [try](https://try.vikunja.io)

View File

@ -1,11 +0,0 @@
# Description
# Checklist
* [ ] I added or improved tests
* [ ] I added or improved docs for my feature
* [ ] Swagger (including `mage do-the-swag`)
* [ ] Error codes
* [ ] New config options (including adding them to `config.yml.saml` and running `mage generate-docs`)

58
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@ -0,0 +1,58 @@
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

17
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,17 @@
blank_issues_enabled: false
contact_links:
- name: Frontend issues
url: https://code.vikunja.io/frontend/issues
about: This is the API repo. Please open frontend-related bug reports and discussions in the frontend repo. Not sure if you issue is frontend or api? Ask in Matrix or the forum first.
- name: Forum
url: https://community.vikunja.io/
about: Feature Requests, Questions, configuration or deployment problems should be discussed in the forum.
- name: Security-related issues
url: https://vikunja.io/contact/#security
about: For security concerns, please send a mail to security@vikunja.io instead of opening a public issue.
- name: Chat on Matrix
url: https://matrix.to/#/#vikunja:matrix.org
about: Please ask any quick questions here.
- name: Translations
url: https://crowdin.com/project/vikunja
about: Any problems or requests for new languages about translations should be handled in crowdin.

2
.gitignore vendored
View File

@ -4,6 +4,8 @@
config.yml
config.yaml
!docs/config.yml
!.github/ISSUE_TEMPLATE/config.yml
!.gitea/ISSUE_TEMPLATE/config.yml
docs/themes/
*.db
Run

View File

@ -7,6 +7,155 @@ 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.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
* *(caldav)* Make sure duration and due date follow rfc5545
* *(caldav)* No failed login emails for tokens (#1252)
* *(ci)* Make sure release zip files have a .zip ending
* *(ci)* Make sure release os packages are properly named
* *(docs)* Clarify using port 25 as mail port when mail does not work
* *(docs)* Document pnpm instead of yarn
* *(docs)* Fix redirect_url example (#50)
* *(lists)* Return correct max right for lists where the user has created the namespace
* *(mail)* Pass mail server timeout (#1253)
* *(migration)* Properly parse duration
* *(migration)* Expose ticktick migrator to /info
* *(migration)* Make sure importing works when the csv file has errors and don't try to parse empty values as dates
* *(namespaces)* Add list subscriptions (#1254)
* *(todoist)* Properly import all done tasks* Properly log extra message ([c194797](c19479757a20d72484b4e071b45055746ff2b67e))
* Don't try to compress riscv64 binaries in releases ([d8f387f](d8f387f7967ffb94035de2fcfc4578247ae1023e))
* Preserve dates for repeating tasks (#47) ([090c671](090c67138a16258480b866b05c6fdc2e02d12c89))
* Tasks with the same assignee as doer should not appear twice in overdue task mails ([45defeb](45defebcf435cade4b72763236e1e2dfdac770cc))
* Don't allow setting a list namespace to 0 ([96ed1e3](96ed1e33e38beec1bb1ab0813074b035dd02fade))
* Make sure pseudo namespaces and lists always have the current user as owner ([878d19b](878d19beb81869392e33a8ffc1ec247d1cf1e4d6))
* Use connection string for postgres ([fcb205a](fcb205a842a4e828e6e933339b23f5aa8b297125))
* Make sure user searches are always case-insensitive ([c076f73](c076f73a87bc9b39b17389e25d0186ab71aa24bf))
* Make cover image id actually updatable ([0e1904d](0e1904d50b8576a2e9ea5812314aa3c8f304edb5))
* Make cover image id actually updatable ([0eb4709](0eb47096db02ceb5032c7439b3b901fbadd0d1bb))
* Make sure a user can only be assigned once to a task ([008908e](008908eb49eeb50a554c416422feb3b465efa165))
* Make sure list subscriptions are set correctly when their namespace has a subscription already ([2fc690a](2fc690a783f5b702fad71da627aa616017727f56))
### Dependencies
* *(deps)* Update klakegg/hugo docker tag to v0.101.0
* *(deps)* Update golang.org/x/sync digest to 8fcdb60
* *(deps)* Update golang.org/x/oauth2 digest to f213421
* *(deps)* Update module src.techknowlogick.com/xgo to v1.5.0+1.19
* *(deps)* Update module github.com/coreos/go-oidc/v3 to v3.4.0
* *(deps)* Update golang.org/x/image digest to e7cb969
* *(deps)* Update golang.org/x/term digest to 7a66f97
* *(deps)* Update module github.com/lib/pq to v1.10.7
* *(deps)* Update module github.com/spf13/viper to v1.13.0 (#1260)
* *(deps)* Update dependency golang to v1.19 (#1228)
* *(deps)* Update module github.com/wneessen/go-mail to v0.2.8 (#1258)
* *(deps)* Update module github.com/yuin/goldmark to v1.5.2 (#1261)
* *(deps)* Update module src.techknowlogick.com/xormigrate to v1.5.0 (#1262)
* *(deps)* Update module github.com/magefile/mage to v1.14.0 (#1259)
* *(deps)* Update module github.com/swaggo/swag to v1.8.6 (#1243)
* *(deps)* Update module github.com/wneessen/go-mail to v0.2.9 (#1264)
* *(deps)* Update dependency klakegg/hugo to v0.102.3 (#1265)
* *(deps)* Update module github.com/getsentry/sentry-go to v0.14.0 (#1266)
* *(deps)* Update module github.com/labstack/gommon to v0.4.0 (#1269)
* *(deps)* Update golang.org/x/crypto digest to 4161e89 (#1268)
* *(deps)* Update golang.org/x/oauth2 digest to b44042a (#1270)
* *(deps)* Update golang.org/x/sys digest to 84dc82d (#1271)
* *(deps)* Update dependency klakegg/hugo to v0.104.2 (#1267)
* *(deps)* Update golang.org/x/crypto digest to d6f0a8c (#1275)
* *(deps)* Update golang.org/x/sys digest to 090e330 (#1276)
* *(deps)* Update module github.com/spf13/cobra to v1.6.0 (#1277)
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.0 (#1278)
* *(deps)* Update golang.org/x/crypto digest to 56aed06 (#1280)
* *(deps)* Update golang.org/x/text to v0.3.8
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.1 (#1281)
* *(deps)* Update module github.com/labstack/echo/v4 to v4.9.1 (#1282)
* *(deps)* Update golang.org/x/sys digest to 95e765b (#1283)
* *(deps)* Update golang.org/x/oauth2 digest to 6fdb5e3 (#1284)
* *(deps)* Update golang.org/x/image digest to ffcb3fe (#1288)
* *(deps)* Update module golang.org/x/sync to v0.1.0 (#1291)
* *(deps)* Update module github.com/swaggo/swag to v1.8.7 (#1290)
* *(deps)* Update golang.org/x/term digest to 8365914 (#1289)
* *(deps)* Update module github.com/coreos/go-systemd/v22 to v22.4.0 (#1287)
* *(deps)* Update module golang.org/x/oauth2 to v0.1.0 (#1296)
* *(deps)* Update module golang.org/x/crypto to v0.1.0 (#1295)
* *(deps)* Update module golang.org/x/image to v0.1.0 (#1293)
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.2 (#1297)
* *(deps)* Update module github.com/stretchr/testify to v1.8.1 (#1298)
* *(deps)* Update module github.com/spf13/cobra to v1.6.1 (#1299)
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.3 (#1300)
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.4 (#1302)
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.16 (#1301)
### Features
* *(docs)* Add docs about how to deploy Vikunja in a subdirectory
* *(docs)* Document pnpm (#1251)
* *(migration)* Add TickTick migrator
* *(migration)* Add routes for TickTick migrator
* *(migration)* Generate swagger docs
* *(task)* Add cover image attachment id property
* *(task)* Add cover image attachment id property (#1263)* Add sponsor to readme (relm) ([f814dd0](f814dd03eb7f1ae08ea67ae0e3e89b8b4e684ce3))
* Upgrade xorm ([b1fd13b](b1fd13bbcbc551d1bbfe78d91fe6209369709df5))
* Upgrade xorm ([4323803](4323803fd6801e21121eac0d9f9cd62879f090f7))
* Upgrade xorm (#1197) ([5341918](53419180be386d675b4513e7ec70aca85b5ac99b))
* Add github issue templates ([9c4bb5a](9c4bb5a24429dec686e3ccdcd2b920ce5528031c))
* Remove gitea issue template so that only the form is used ([ce621ee](ce621ee5d6b47a0776628073bbd53312a97d116b))
* Add gitea issue template ([0612f4d](0612f4d0e03fbe85018f51056c4833557e78cd3f))
* Provide default user settings for new users via config ([5a40100](5a40100ac5be33d2cbce3c25e355d4036b9b4d3f))
* Add proper checks and errors to see if an attachment belongs to the task it's being used as cover image in ([631a265](631a265d2de9a6196faf28574023fc3cdcc0bfc7))
* Allow a user to remove themselves from a team ([b8769c7](b8769c746ceddc9818f91d6a8a404293ea2e837e))
* TickTick migrator (#1273) ([df2e36c](df2e36c2a378d4bd1b81d959da180b6e9b9a37b9))
### Miscellaneous Tasks
* Upgrade echo ([86ee827](86ee8273bce36c7b4767a34e0d878d63b37ea1b4))
* Go mod tidy ([903b8ff](903b8ff43871234f41f706d571ee2caaba5f4232))
* Generate swagger docs ([e113fe3](e113fe34d074f698f4b0cb237821f359976daa5c))
* Remove unused dependencies ([f5fd849](f5fd849a0b93ff3bba53ac4907bb3fb04fa8692b))
## [0.19.2] - 2022-08-17
### Bug Fixes
* Don't fail a migration if there is no filter saved ([10ded56](10ded56f6697ef47910ec68d37f26ed47cbe9180))
* Don't override saved filters ([beb4d07](beb4d07cf95fc25f7cc5f7471b46bdab49f95fe0))
## [0.19.1] - 2022-08-17
### Bug Fixes

View File

@ -1,9 +1,9 @@
##############
# Build stage
FROM golang:1.18-alpine AS build-env
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:go-1.19.2 AS build-env
RUN apk --no-cache add build-base git && \
RUN \
go install github.com/magefile/mage@latest && \
mv /go/bin/mage /usr/local/go/bin
@ -13,9 +13,11 @@ ARG VIKUNJA_VERSION
COPY . /go/src/code.vikunja.io/api
WORKDIR /go/src/code.vikunja.io/api
ARG TARGETOS TARGETARCH TARGETVARIANT
# Checkout version if set
RUN if [ -n "${VIKUNJA_VERSION}" ]; then git checkout "${VIKUNJA_VERSION}"; fi \
&& mage build:clean build
RUN if [ -n "${VIKUNJA_VERSION}" ]; then git checkout "${VIKUNJA_VERSION}"; fi && \
mage build:clean && \
mage release:xgo $TARGETOS/$TARGETARCH/$TARGETVARIANT
###################
# The actual image
@ -25,7 +27,7 @@ FROM alpine:3.16
LABEL maintainer="maintainers@vikunja.io"
WORKDIR /app/vikunja/
COPY --from=build-env /go/src/code.vikunja.io/api/vikunja .
COPY --from=build-env /build/vikunja-* vikunja
ENV VIKUNJA_SERVICE_ROOTPATH=/app/vikunja/
# Dynamic permission changing stuff

View File

@ -1,48 +0,0 @@
##############
# Build stage
FROM golang:1.18-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.19.1-brightgreen.svg)](https://dl.vikunja.io)
[![Download](https://img.shields.io/badge/download-v0.20.1-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,6 +56,10 @@ 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

@ -127,7 +127,8 @@ mailer:
enabled: false
# SMTP Host
host: ""
# SMTP Host port
# SMTP Host port.
# **NOTE:** If you're unable to send mail and the only error you see in the logs is an `EOF`, try setting the port to `25`.
port: 587
# SMTP Auth Type. Can be either `plain`, `login` or `cram-md5`.
authtype: "plain"
@ -161,7 +162,7 @@ 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"
@ -190,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
@ -310,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.
@ -323,3 +312,28 @@ metrics:
username:
# If set to a non-empty value the /metrics endpoint will require this as a password via basic auth in combination with the username below.
password:
# Provide default settings for new users. When a new user is created, these settings will automatically be set for the user. If you change them in the config file afterwards they will not be changed back for existing users.
defaultsettings:
# The avatar source for the user. Can be `gravatar`, `initials`, `upload` or `marble`. If you set this to `upload` you'll also need to specify `defaultsettings.avatar_file_id`.
avatar_provider: initials
# The id of the file used as avatar.
avatar_file_id: 0
# If set to true users will get task reminders via email.
email_reminders_enabled: false
# If set to true will allow other users to find this user when searching for parts of their name.
discoverable_by_name: false
# If set to true will allow other users to find this user when searching for their exact email.
discoverable_by_email: false
# If set to true will send an email every day with all overdue tasks at a configured time.
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
# 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.
language: <unset>
# The time zone of each individual user. This will affect when users get reminders and overdue task emails.
timezone: <time zone set at service.timezone>

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

0
docs/.hugo_build.lock Normal file
View File

View File

@ -1,5 +1,5 @@
---
date: "2019-02-12:00:00+02:00"
date: "2022-09-21:00:00+02:00"
title: "Development"
toc: true
draft: false
@ -36,13 +36,13 @@ Make sure to check the other doc articles for specific development tasks like [t
## Frontend requirements
The code for the frontend is located at [code.vikunja.io/frontend](https://code.vikunja.io/frontend).
More instructions can be found in the repo's README.
You need to have yarn v1 and nodejs in version 16 installed.
You need to have [pnpm](https://pnpm.io/) and nodejs in version 16 or 18 installed.
## Git flow
The `main` branch is the latest and bleeding edge branch with all changes. Unstable releases are automatically
created from this branch.
The `main` branch is the latest and bleeding edge branch with all changes. Unstable releases are automatically created from this branch.
A release gets tagged from the main branch with the version name as tag name.
@ -50,7 +50,6 @@ Backports and point-releases should go to a `release/version` branch, based on t
## Conventional commits
We're using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) because they greatly simplify
generating release notes.
We're using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) because they greatly simplify generating release notes.
It is not required to use them when creating a PR, but appreciated.

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

@ -98,12 +98,12 @@ Check out the docs [in the frontend repo](https://kolaente.dev/vikunja/frontend/
To run the frontend unit tests, run
{{< highlight bash >}}
yarn test:unit
pnpm run test:unit
{{< /highlight >}}
The frontend also has a watcher available that re-runs all unit tests every time you change something.
To use it, simply run
{{< highlight bash >}}
yarn test:unit-watch
pnpm run test:unit-watch
{{< /highlight >}}

View File

@ -1,5 +1,5 @@
---
date: "2019-02-12:00:00+02:00"
date: "2022-09-21:00:00+02:00"
title: "Build from sources"
draft: false
type: "doc"
@ -16,13 +16,13 @@ To completely build Vikunja from source, you need to build the api and frontend.
## API
The Vikunja API has no other dependencies than go itself.
The Vikunja API has no other dependencies than go itself.
That means compiling it boils down to these steps:
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.17`.
2. Make sure [Mage](https://magefile.org) is properly installed on your system.
3. Clone the repo with `git clone https://code.vikunja.io/api` and switch into the directory.
3. Run `mage build:build` in the source of this repo. This will build a binary in the root of the repo which will be able to run on your system.
4. Run `mage build:build` in the source of this repo. This will build a binary in the root of the repo which will be able to run on your system.
*Note:* Static ressources such as email templates are built into the binary.
For these to work, you may need to run `mage build:generate` before building the vikunja binary.
@ -38,9 +38,7 @@ More options are available, please refer to the [magefile docs]({{< ref "../deve
The code for the frontend is located at [code.vikunja.io/frontend](https://code.vikunja.io/frontend).
You need to have yarn v1 and nodejs in version 16 installed.
1. Make sure [yarn v1](https://yarnpkg.com/getting-started/install) is properly installed on your system.
3. Clone the repo with `git clone https://code.vikunja.io/frontend` and switch into the directory.
3. Install all dependencies with `yarn install`
4. Build the frontend with `yarn build`. This will result in a js bundle in the `dist/` folder which you can deploy.
1. Make sure you have [pnpm](https://pnpm.io/installation) properly installed on your system.
2. Clone the repo with `git clone https://code.vikunja.io/frontend` and switch into the directory.
3. Install all dependencies with `pnpm install`
4. Build the frontend with `pnpm run build`. This will result in a static js bundle in the `dist/` folder which you can deploy.

View File

@ -657,7 +657,8 @@ Environment path: `VIKUNJA_MAILER_HOST`
### port
SMTP Host port
SMTP Host port.
**NOTE:** If you're unable to send mail and the only error you see in the logs is an `EOF`, try setting the port to `25`.
Default: `587`
@ -839,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`
@ -968,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>`
@ -1173,3 +1163,132 @@ Full path: `metrics.password`
Environment path: `VIKUNJA_METRICS_PASSWORD`
---
## defaultsettings
Provide default settings for new users. When a new user is created, these settings will automatically be set for the user. If you change them in the config file afterwards they will not be changed back for existing users.
### avatar_provider
The avatar source for the user. Can be `gravatar`, `initials`, `upload` or `marble`. If you set this to `upload` you'll also need to specify `defaultsettings.avatar_file_id`.
Default: `initials`
Full path: `defaultsettings.avatar_provider`
Environment path: `VIKUNJA_DEFAULTSETTINGS_AVATAR_PROVIDER`
### avatar_file_id
The id of the file used as avatar.
Default: `0`
Full path: `defaultsettings.avatar_file_id`
Environment path: `VIKUNJA_DEFAULTSETTINGS_AVATAR_FILE_ID`
### email_reminders_enabled
If set to true users will get task reminders via email.
Default: `false`
Full path: `defaultsettings.email_reminders_enabled`
Environment path: `VIKUNJA_DEFAULTSETTINGS_EMAIL_REMINDERS_ENABLED`
### discoverable_by_name
If set to true will allow other users to find this user when searching for parts of their name.
Default: `false`
Full path: `defaultsettings.discoverable_by_name`
Environment path: `VIKUNJA_DEFAULTSETTINGS_DISCOVERABLE_BY_NAME`
### discoverable_by_email
If set to true will allow other users to find this user when searching for their exact email.
Default: `false`
Full path: `defaultsettings.discoverable_by_email`
Environment path: `VIKUNJA_DEFAULTSETTINGS_DISCOVERABLE_BY_EMAIL`
### overdue_tasks_reminders_enabled
If set to true will send an email every day with all overdue tasks at a configured time.
Default: `true`
Full path: `defaultsettings.overdue_tasks_reminders_enabled`
Environment path: `VIKUNJA_DEFAULTSETTINGS_OVERDUE_TASKS_REMINDERS_ENABLED`
### overdue_tasks_reminders_time
When to send the overdue task reminder email.
Default: `9:00`
Full path: `defaultsettings.overdue_tasks_reminders_time`
Environment path: `VIKUNJA_DEFAULTSETTINGS_OVERDUE_TASKS_REMINDERS_TIME`
### default_list_id
The id of the default list. Make sure users actually have access to this list when setting this value.
Default: `0`
Full path: `defaultsettings.default_list_id`
Environment path: `VIKUNJA_DEFAULTSETTINGS_DEFAULT_LIST_ID`
### week_start
Start of the week for the user. `0` is sunday, `1` is monday and so on.
Default: `0`
Full path: `defaultsettings.week_start`
Environment path: `VIKUNJA_DEFAULTSETTINGS_WEEK_START`
### language
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.
Default: `<unset>`
Full path: `defaultsettings.language`
Environment path: `VIKUNJA_DEFAULTSETTINGS_LANGUAGE`
### timezone
The time zone of each individual user. This will affect when users get reminders and overdue task emails.
Default: `<time zone set at service.timezone>`
Full path: `defaultsettings.timezone`
Environment path: `VIKUNJA_DEFAULTSETTINGS_TIMEZONE`

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

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

@ -37,9 +37,32 @@ Authelia config:
description: Vikunja
secret: <vikunja secret>
redirect_uris:
- https://vikunja.mydomain.com/auth/openid/ <----- Matching slash at the end
- https://vikunja.mydomain.com/auth/openid/authelia
scopes:
- openid
- 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

@ -0,0 +1,39 @@
---
title: "Running Vikunja in a subdirectory"
date: 2022-09-23T12:15:04+02:00
draft: false
menu:
sidebar:
parent: "setup"
---
# Running Vikunja in a subdirectory
Running Vikunja in a subdirectory is not supported out of the box.
However, you can still run it in a subdirectory but need to build the frontend yourself.
## Frontend
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
```
pnpm vite build --base=/SUBPATH
pnpm workbox copyLibraries dist/
```
Where `SUBPATH` is the subdirectory you want to run Vikunja on.
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.
## API
If you're not using a reverse proxy you're good to go.
Simply configure the api url in the frontend as you normally would.
If you're using a reverse proxy you'll need to adjust the paths so that the api is available at `/SUBPATH/api/v1`.
You can check if everything is working correctly by opening `/SUBPATH/api/v1/info` in a browser.

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

@ -54,13 +54,14 @@ This document describes the different errors Vikunja can return.
| 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". |
| 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. |
## Task
@ -81,10 +82,12 @@ This document describes the different errors Vikunja can return.
| 4013 | 400 | The task sort param is invalid. |
| 4014 | 400 | The task sort order is invalid. |
| 4015 | 404 | The task comment does not exist. |
| 4016 | 403 | Invalid task field. |
| 4017 | 403 | Invalid task filter comparator. |
| 4018 | 403 | Invalid task filter concatinator. |
| 4019 | 403 | Invalid task filter value. |
| 4016 | 400 | Invalid task field. |
| 4017 | 400 | Invalid task filter comparator. |
| 4018 | 400 | Invalid task filter concatinator. |
| 4019 | 400 | Invalid task filter value. |
| 4020 | 400 | The provided attachment does not belong to that task. |
| 4021 | 400 | This user is already assigned to that task. |
## Namespace

100
go.mod
View File

@ -21,60 +21,60 @@ require (
gitea.com/xorm/xorm-redis-cache v0.2.0
github.com/ThreeDotsLabs/watermill v1.1.1
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-20221122102835-109346913e54
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.2.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.13.0
github.com/getsentry/sentry-go v0.16.0
github.com/go-redis/redis/v8 v8.11.5
github.com/go-sql-driver/mysql v1.6.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/golang-jwt/jwt/v4 v4.4.3
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/jinzhu/copier v0.3.5
github.com/labstack/echo/v4 v4.8.0
github.com/labstack/gommon v0.3.1
github.com/lib/pq v1.10.6
github.com/magefile/mage v1.13.0
github.com/mattn/go-sqlite3 v1.14.15
github.com/labstack/echo-jwt/v4 v4.0.0
github.com/labstack/echo/v4 v4.10.0
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/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/cobra v1.5.0
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.8.0
github.com/swaggo/swag v1.8.4
github.com/spf13/afero v1.9.3
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.14.0
github.com/stretchr/testify v1.8.1
github.com/swaggo/swag v1.8.9
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.2.6
github.com/yuin/goldmark v1.4.13
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539
golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035
github.com/wneessen/go-mail v0.3.7
github.com/yuin/goldmark v1.5.3
golang.org/x/crypto v0.5.0
golang.org/x/image v0.3.0
golang.org/x/oauth2 v0.3.0
golang.org/x/sync v0.1.0
golang.org/x/sys v0.4.0
golang.org/x/term v0.4.0
gopkg.in/d4l3k/messagediff.v1 v1.2.1
gopkg.in/yaml.v3 v3.0.1
src.techknowlogick.com/xgo v1.4.1-0.20210311222705-d25c33fcd864
src.techknowlogick.com/xormigrate v1.4.0
xorm.io/builder v0.3.9
xorm.io/core v0.7.3
xorm.io/xorm v1.1.2
src.techknowlogick.com/xgo v1.5.1-0.20220906164532-735bfdfb90d9
src.techknowlogick.com/xormigrate v1.5.0
xorm.io/builder v0.3.12
xorm.io/xorm v1.3.2
)
require (
@ -89,30 +89,32 @@ require (
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-errors/errors v1.1.1 // 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
github.com/go-openapi/swag v0.19.15 // indirect
github.com/gocarina/gocsv v0.0.0-20221216233619-1fea7ae8d380 // indirect
github.com/goccy/go-json v0.9.11 // indirect
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/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap 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/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // 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-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -120,38 +122,34 @@ require (
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.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // 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/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.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.3.0 // indirect
github.com/subosito/gotenv v1.4.1 // 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.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/text v0.6.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/square/go-jose.v2 v2.5.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/coreos/bbolt => go.etcd.io/bbolt v1.3.4
github.com/coreos/go-systemd => github.com/coreos/go-systemd/v22 v22.0.0
github.com/hpcloud/tail => github.com/jeffbean/tail v1.0.1 // See https://github.com/hpcloud/tail/pull/159
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
)

804
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,6 @@ import (
"fmt"
"github.com/iancoleman/strcase"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
@ -547,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);)
@ -559,7 +572,9 @@ func (Release) Compress(ctx context.Context) error {
return nil
}
// No mips or s390x for you today
if strings.Contains(info.Name(), "mips") || strings.Contains(info.Name(), "s390x") {
if strings.Contains(info.Name(), "mips") ||
strings.Contains(info.Name(), "s390x") ||
strings.Contains(info.Name(), "riscv64") { // not supported by upx
return nil
}
@ -663,7 +678,7 @@ func (Release) Zip() error {
fmt.Printf("Zipping %s...\n", info.Name())
c := exec.Command("zip", "-r", RootPath+"/"+DIST+"/zip/"+info.Name(), ".", "-i", "*")
c := exec.Command("zip", "-r", RootPath+"/"+DIST+"/zip/"+info.Name()+".zip", ".", "-i", "*")
c.Dir = path
out, err := c.Output()
fmt.Print(string(out))
@ -688,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") {
@ -697,16 +712,16 @@ 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 := ioutil.ReadFile(nfpmConfigPath)
nfpmconfig, err := os.ReadFile(nfpmConfigPath)
if err != nil {
return err
}
fixedConfig := strings.ReplaceAll(string(nfpmconfig), "<version>", VersionNumber)
fixedConfig = strings.ReplaceAll(fixedConfig, "<binlocation>", BinLocation)
if err := ioutil.WriteFile(nfpmConfigPath, []byte(fixedConfig), 0); err != nil {
if err := os.WriteFile(nfpmConfigPath, []byte(fixedConfig), 0); err != nil {
return err
}
@ -719,7 +734,7 @@ func (Release) Packages() error {
runAndStreamOutput(binpath, "pkg", "--packager", "rpm", "--target", releasePath)
runAndStreamOutput(binpath, "pkg", "--packager", "apk", "--target", releasePath)
return ioutil.WriteFile(nfpmConfigPath, nfpmconfig, 0)
return os.WriteFile(nfpmConfigPath, nfpmconfig, 0)
}
type Dev mg.Namespace
@ -883,7 +898,7 @@ func (s *` + name + `) Handle(msg *message.Message) (err error) {
if _, err := f.Seek(idx, 0); err != nil {
return err
}
remainder, err := ioutil.ReadAll(f)
remainder, err := io.ReadAll(f)
if err != nil {
return err
}
@ -1072,7 +1087,7 @@ const (
// Generates the config docs from a commented config.yml.sample file in the repo root.
func GenerateDocs() error {
config, err := ioutil.ReadFile("config.yml.sample")
config, err := os.ReadFile("config.yml.sample")
if err != nil {
return err
}
@ -1122,7 +1137,7 @@ func GenerateDocs() error {
// We write the full file to prevent old content leftovers at the end
// I know, there are probably better ways to do this.
if err := ioutil.WriteFile(configDocPath, []byte(fullConfig), 0); err != nil {
if err := os.WriteFile(configDocPath, []byte(fullConfig), 0); err != nil {
return err
}

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"
)
@ -58,10 +59,12 @@ type Todo struct {
RelatedToUID string
Color string
Start time.Time
End time.Time
DueDate time.Time
Duration time.Duration
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
@ -181,6 +184,10 @@ SUMMARY:` + t.Summary + getCaldavColor(t.Color)
if t.Start.Unix() > 0 {
caldavtodos += `
DTSTART:` + makeCalDavTimeFromTimeStamp(t.Start)
if t.Duration != 0 && t.DueDate.Unix() == 0 {
caldavtodos += `
DURATION:PT` + formatDuration(t.Duration)
}
}
if t.End.Unix() > 0 {
caldavtodos += `
@ -217,16 +224,21 @@ DUE:` + makeCalDavTimeFromTimeStamp(t.DueDate)
CREATED:` + makeCalDavTimeFromTimeStamp(t.Created)
}
if t.Duration != 0 {
caldavtodos += `
DURATION:PT` + formatDuration(t.Duration)
}
if t.Priority != 0 {
caldavtodos += `
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)
}
}
caldavtodos += `
LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated)
@ -241,7 +253,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,82 @@ 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`,
},

View File

@ -42,13 +42,15 @@ 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,
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,
})
}

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`
@ -161,6 +157,18 @@ const (
MetricsEnabled Key = `metrics.enabled`
MetricsUsername Key = `metrics.username`
MetricsPassword Key = `metrics.password`
DefaultSettingsAvatarProvider Key = `defaultsettings.avatar_provider`
DefaultSettingsAvatarFileID Key = `defaultsettings.avatar_file_id`
DefaultSettingsEmailRemindersEnabled Key = `defaultsettings.email_reminders_enabled`
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`
DefaultSettingsWeekStart Key = `defaultsettings.week_start`
DefaultSettingsLanguage Key = `defaultsettings.language`
DefaultSettingsTimezone Key = `defaultsettings.timezone`
DefaultSettingsOverdueTaskRemindersTime Key = `defaultsettings.overdue_tasks_reminders_time`
)
// GetString returns a string config value
@ -357,7 +365,6 @@ func InitDefaultConfig() {
CorsOrigins.setDefault([]string{"*"})
CorsMaxAge.setDefault(0)
// Migration
MigrationWunderlistEnable.setDefault(false)
MigrationTodoistEnable.setDefault(false)
MigrationTrelloEnable.setDefault(false)
MigrationMicrosoftTodoEnable.setDefault(false)
@ -371,6 +378,10 @@ func InitDefaultConfig() {
KeyvalueType.setDefault("memory")
// Metrics
MetricsEnabled.setDefault(false)
// Settings
DefaultSettingsAvatarProvider.setDefault("initials")
DefaultSettingsOverdueTaskRemindersEnabled.setDefault(true)
DefaultSettingsOverdueTaskRemindersTime.setDefault("9:00")
}
// InitConfig initializes the config, sets defaults etc.
@ -438,6 +449,10 @@ func InitConfig() {
MigrationMicrosoftTodoRedirectURL.Set(ServiceFrontendurl.GetString() + "migrate/microsoft-todo")
}
if DefaultSettingsTimezone.GetString() == "" {
DefaultSettingsTimezone.Set(ServiceTimeZone.GetString())
}
if ServiceEnableMetrics.GetBool() {
log.Println("WARNING: service.enablemetrics is deprecated and will be removed in a future release. Please use metrics.enable.")
MetricsEnabled.Set(true)

View File

@ -19,6 +19,7 @@ package db
import (
"encoding/gob"
"fmt"
"net/url"
"os"
"strconv"
"strings"
@ -27,9 +28,9 @@ import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
xrc "gitea.com/xorm/xorm-redis-cache"
"xorm.io/core"
"xorm.io/xorm"
"xorm.io/xorm/caches"
"xorm.io/xorm/names"
"xorm.io/xorm/schemas"
_ "github.com/go-sql-driver/mysql" // Because.
@ -80,7 +81,7 @@ func CreateDBEngine() (engine *xorm.Engine, err error) {
log.Fatalf("Error parsing time zone: %s", err)
}
engine.SetTZDatabase(loc)
engine.SetMapper(core.GonicMapper{})
engine.SetMapper(names.GonicMapper{})
logger := log.NewXormLogger("")
engine.SetLogger(logger)
@ -147,11 +148,26 @@ 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())
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s",
host,
port,
connStr := getPostgreSQLConnectionString(
config.DatabaseHost.GetString(),
config.DatabaseUser.GetString(),
config.DatabasePassword.GetString(),
config.DatabaseDatabase.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

@ -3,7 +3,7 @@
list_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
@ -11,7 +11,7 @@
list_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

View File

@ -337,6 +337,7 @@
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

View File

@ -23,9 +23,10 @@ import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
"github.com/stretchr/testify/assert"
"xorm.io/core"
"xorm.io/xorm"
"xorm.io/xorm/names"
)
// CreateTestEngine creates an instance of the db engine which lives in memory
@ -48,7 +49,7 @@ func CreateTestEngine() (engine *xorm.Engine, err error) {
}
}
engine.SetMapper(core.GonicMapper{})
engine.SetMapper(names.GonicMapper{})
logger := log.NewXormLogger("DEBUG")
logger.ShowSQL(os.Getenv("UNIT_TESTS_VERBOSE") == "1")
engine.SetLogger(logger)

View File

@ -227,7 +227,7 @@ func TestArchived(t *testing.T) {
assertHandlerErrorCode(t, err, models.ErrCodeListIsArchived)
})
t.Run("unarchivable", func(t *testing.T) {
rec, err := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "22"}, `{"title":"LoremIpsum","is_archived":false}`)
rec, err := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "22"}, `{"title":"LoremIpsum","is_archived":false,"namespace_id":1}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"is_archived":false`)
})

View File

@ -232,12 +232,12 @@ func TestLinkSharing(t *testing.T) {
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"}`)
rec, err := testHandlerListWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "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"}`)
rec, err := testHandlerListAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"title":"TestLoremIpsum","namespace_id":2}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})

View File

@ -171,7 +171,7 @@ 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"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "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
@ -183,7 +183,7 @@ func TestList(t *testing.T) {
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
})
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"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "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`)
@ -211,12 +211,12 @@ func TestList(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "7"}, `{"title":"TestLoremIpsum"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "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"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "8"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
@ -227,12 +227,12 @@ func TestList(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "10"}, `{"title":"TestLoremIpsum"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "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"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "11"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
@ -243,12 +243,12 @@ func TestList(t *testing.T) {
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"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "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"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "14"}, `{"title":"TestLoremIpsum","namespace_id":9}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
@ -259,12 +259,12 @@ func TestList(t *testing.T) {
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"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "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"}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "17"}, `{"title":"TestLoremIpsum","namespace_id":12}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})

View File

@ -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,"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,"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"}}]`)
})
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,"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,"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`)
})
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,"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,"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"}}]`)
})
// 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,"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,"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"}}`)
})
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,"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,"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`)
})
// 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,"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,"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"}}`)
})
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,"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,"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"}}`)
})
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,"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,"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`)
})
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,"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,"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"}}`)
})
t.Run("invalid sort parameter", func(t *testing.T) {
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
@ -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,"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,"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"}}]`)
})
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,"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,"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`)
})
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,"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,"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"}}]`)
})
// 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,"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,"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,"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"}}`)
})
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,"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,"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`)
})
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,"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,"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,"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"}}`)
})
t.Run("invalid parameter", func(t *testing.T) {
// Invalid parameter should not sort at all

View File

@ -55,6 +55,7 @@ func getClient() (*mail.Client, error) {
InsecureSkipVerify: config.MailerSkipTLSVerify.GetBool(),
ServerName: config.MailerHost.GetString(),
}),
mail.WithTimeout((config.MailerQueueTimeout.GetDuration() + 3) * time.Second), // 3s more for us to close before mail server timeout
}
if config.MailerUsername.GetString() != "" && config.MailerPassword.GetString() != "" {
@ -105,14 +106,14 @@ func StartMailDaemon() {
if !open {
err = c.DialWithContext(context.Background())
if err != nil {
log.Error("Error during connect to smtp server: %s", err)
log.Errorf("Error during connect to smtp server: %s", err)
break
}
open = true
}
err = c.Send(m)
if err != nil {
log.Error("Error when sending mail: %s", err)
log.Errorf("Error when sending mail: %s", err)
break
}
// Close the connection to the SMTP server if no email was sent in
@ -122,10 +123,10 @@ func StartMailDaemon() {
open = false
err = c.Close()
if err != nil {
log.Error("Error closing the mail server connection: %s\n", err)
log.Errorf("Error closing the mail server connection: %s\n", err)
break
}
log.Infof("Closed connection to mail server")
log.Info("Closed connection to mail server")
}
}
}

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

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

View File

@ -40,7 +40,11 @@ func init() {
for _, f := range filters {
filter := map[string]interface{}{}
err = json.Unmarshal([]byte(f["filters"].(string)), &filter)
filterJSON, is := f["filters"].(string)
if !is {
continue
}
err = json.Unmarshal([]byte(filterJSON), &filter)
if err != nil {
return err
}
@ -85,7 +89,9 @@ func init() {
f["filters"] = string(filtersJSON)
_, err = tx.Where("id", f["id"]).
_, err = tx.Where("id = ?", f["id"]).
Cols("filters").
NoAutoCondition().
Table("saved_filters").
Update(f)
if err != nil {

View File

@ -0,0 +1,43 @@
// 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"
)
type tasks20221002120521 struct {
CoverImageAttachmentID int64 `xorm:"bigint default 0" json:"cover_image_attachment_id"`
}
func (tasks20221002120521) TableName() string {
return "tasks"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20221002120521",
Description: "Add cover image attachment id",
Migrate: func(tx *xorm.Engine) error {
return tx.Sync2(tasks20221002120521{})
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -283,6 +283,34 @@ func (err *ErrListCannotBelongToAPseudoNamespace) HTTPError() web.HTTPError {
}
}
// ErrListMustBelongToANamespace represents an error where a list must belong to a namespace
type ErrListMustBelongToANamespace struct {
ListID 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)
return ok
}
func (err *ErrListMustBelongToANamespace) Error() string {
return fmt.Sprintf("List must belong to a namespace [ListID: %d, NamespaceID: %d]", err.ListID, err.NamespaceID)
}
// ErrCodeListMustBelongToANamespace holds the unique world-error code of this error
const ErrCodeListMustBelongToANamespace = 3010
// HTTPError holds the http error description
func (err *ErrListMustBelongToANamespace) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeListMustBelongToANamespace,
Message: "This list must belong to a namespace.",
}
}
// ================
// List task errors
// ================
@ -791,6 +819,62 @@ func (err ErrInvalidTaskFilterValue) HTTPError() web.HTTPError {
}
}
// ErrAttachmentDoesNotBelongToTask represents an error where the provided task cover attachment does not belong to the same task
type ErrAttachmentDoesNotBelongToTask struct {
TaskID int64
AttachmentID int64
}
// IsErrAttachmentAndCoverMustBelongToTheSameTask checks if an error is ErrAttachmentDoesNotBelongToTask.
func IsErrAttachmentAndCoverMustBelongToTheSameTask(err error) bool {
_, ok := err.(ErrAttachmentDoesNotBelongToTask)
return ok
}
func (err ErrAttachmentDoesNotBelongToTask) Error() string {
return fmt.Sprintf("Task attachment and cover image do not belong to the same task [TaskID: %d, AttachmentID: %d]", err.TaskID, err.AttachmentID)
}
// ErrCodeAttachmentDoesNotBelongToTask holds the unique world-error code of this error
const ErrCodeAttachmentDoesNotBelongToTask = 4020
// HTTPError holds the http error description
func (err ErrAttachmentDoesNotBelongToTask) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusBadRequest,
Code: ErrCodeAttachmentDoesNotBelongToTask,
Message: "This attachment does not belong to that task.",
}
}
// ErrUserAlreadyAssigned represents an error where the user is already assigned to this task
type ErrUserAlreadyAssigned struct {
TaskID int64
UserID int64
}
// IsErrUserAlreadyAssigned checks if an error is ErrUserAlreadyAssigned.
func IsErrUserAlreadyAssigned(err error) bool {
_, ok := err.(ErrUserAlreadyAssigned)
return ok
}
func (err ErrUserAlreadyAssigned) Error() string {
return fmt.Sprintf("User is already assigned to task [TaskID: %d, UserID: %d]", err.TaskID, err.UserID)
}
// ErrCodeUserAlreadyAssigned holds the unique world-error code of this error
const ErrCodeUserAlreadyAssigned = 4021
// HTTPError holds the http error description
func (err ErrUserAlreadyAssigned) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusBadRequest,
Code: ErrCodeUserAlreadyAssigned,
Message: "This user is already assigned to that task.",
}
}
// =================
// Namespace errors
// =================

View File

@ -81,7 +81,7 @@ func getDefaultBucket(s *xorm.Session, listID int64) (bucket *Bucket, err error)
bucket = &Bucket{}
_, err = s.
Where("list_id = ?", listID).
OrderBy("id asc").
OrderBy("position asc").
Get(bucket)
return
}

View File

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

View File

@ -156,7 +156,7 @@ func GetListsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (lists [
Alias("l").
Join("LEFT", []string{"namespaces", "n"}, "l.namespace_id = n.id").
Where("l.is_archived = false").
Where("n.is_archived = false").
Where("n.is_archived = false OR n.is_archived IS NULL").
Where("namespace_id = ?", nID).
Find(&lists)
}
@ -476,12 +476,22 @@ func addListDetails(s *xorm.Session, lists []*List, a web.Auth) (err error) {
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 {
@ -630,6 +640,13 @@ func UpdateList(s *xorm.Session, list *List, auth web.Auth, updateListBackground
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",

View File

@ -66,6 +66,7 @@ func (ld *ListDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bool,
// @Failure 403 {object} web.HTTPError "The user does not have access to the list or namespace"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{listID}/duplicate [put]
//
//nolint:gocyclo
func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {

View File

@ -202,7 +202,7 @@ func (l *List) isOwner(u *user.User) bool {
func (l *List) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
/*
The following loop creates an sql condition like this one:
The following loop creates a sql condition like this one:
(ul.user_id = 1 AND ul.right = 1) OR (un.user_id = 1 AND un.right = 1) OR
(tm.user_id = 1 AND tn.right = 1) OR (tm2.user_id = 1 AND tl.right = 1) OR
@ -242,16 +242,19 @@ func (l *List) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, i
conds = append(conds, builder.Eq{"n.owner_id": a.GetID()})
type allListRights struct {
UserNamespace NamespaceUser `xorm:"extends"`
UserList ListUser `xorm:"extends"`
UserNamespace *NamespaceUser `xorm:"extends"`
UserList *ListUser `xorm:"extends"`
TeamNamespace TeamNamespace `xorm:"extends"`
TeamList TeamList `xorm:"extends"`
TeamNamespace *TeamNamespace `xorm:"extends"`
TeamList *TeamList `xorm:"extends"`
NamespaceOwnerID int64 `xorm:"namespaces_owner_id"`
}
r := &allListRights{}
var maxRight = 0
exists, err := s.
Select("l.*, un.right, ul.right, tn.right, tl.right, n.owner_id as namespaces_owner_id").
Table("lists").
Alias("l").
// User stuff
@ -285,6 +288,9 @@ func (l *List) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, i
if int(r.TeamList.Right) > maxRight {
maxRight = int(r.TeamList.Right)
}
if r.NamespaceOwnerID == a.GetID() {
maxRight = int(RightAdmin)
}
return exists, maxRight, err
}

View File

@ -140,8 +140,9 @@ func TestList_CreateOrUpdate(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
list := List{
ID: 99999999,
Title: "test",
ID: 99999999,
Title: "test",
NamespaceID: 1,
}
err := list.Update(s, usr)
assert.Error(t, err)

View File

@ -198,6 +198,11 @@ func makeNamespaceSlice(namespaces map[int64]*NamespaceWithLists, userMap map[in
n.Owner = userMap[n.OwnerID]
n.Subscription = subscriptions[n.ID]
all = append(all, n)
for _, l := range n.Lists {
if n.Subscription != nil && l.Subscription == nil {
l.Subscription = n.Subscription
}
}
}
sort.Slice(all, func(i, j int) bool {
return all[i].ID < all[j].ID
@ -335,7 +340,7 @@ func getListsForNamespaces(s *xorm.Session, namespaceIDs []int64, archived bool)
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.Owner = doer
sharedListsPseudonamespace.OwnerID = doer.ID
sharedListsNamespace = &NamespaceWithLists{
sharedListsPseudonamespace,
[]*List{},
@ -385,12 +390,13 @@ func getSharedListsInNamespace(s *xorm.Session, archived bool, doer *user.User)
func getFavoriteLists(s *xorm.Session, lists []*List, namespaceIDs []int64, doer *user.User) (favoriteNamespace *NamespaceWithLists, err error) {
// Create our pseudo namespace with favorite lists
pseudoFavoriteNamespace := FavoritesPseudoNamespace
pseudoFavoriteNamespace.Owner = doer
pseudoFavoriteNamespace.OwnerID = doer.ID
favoriteNamespace = &NamespaceWithLists{
Namespace: pseudoFavoriteNamespace,
Lists: []*List{{}},
}
*favoriteNamespace.Lists[0] = FavoritesPseudoList // Copying the list to be able to modify it later
favoriteNamespace.Lists[0].Owner = doer
for _, list := range lists {
if !list.IsFavorite {
@ -448,7 +454,7 @@ func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *N
}
savedFiltersPseudoNamespace := SavedFiltersPseudoNamespace
savedFiltersPseudoNamespace.Owner = doer
savedFiltersPseudoNamespace.OwnerID = doer.ID
savedFiltersNamespace = &NamespaceWithLists{
Namespace: savedFiltersPseudoNamespace,
Lists: make([]*List, 0, len(savedFilters)),
@ -479,6 +485,7 @@ func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *N
// @Success 200 {array} models.NamespaceWithLists "The Namespaces."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces [get]
//
//nolint:gocyclo
func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
if _, is := a.(*LinkSharing); is {
@ -517,6 +524,7 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
if err != nil {
return nil, 0, 0, err
}
ownerMap[doer.ID] = doer
if n.NamespacesOnly {
all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap)

View File

@ -18,6 +18,7 @@ package models
import (
"bufio"
"sort"
"strconv"
"strings"
"time"
@ -230,14 +231,23 @@ func (n *UndoneTaskOverdueNotification) Name() string {
// UndoneTasksOverdueNotification represents a UndoneTasksOverdueNotification notification
type UndoneTasksOverdueNotification struct {
User *user.User
Tasks []*Task
Tasks map[int64]*Task
}
// ToMail returns the mail notification for UndoneTasksOverdueNotification
func (n *UndoneTasksOverdueNotification) ToMail() *notifications.Mail {
overdueLine := ""
sortedTasks := make([]*Task, 0, len(n.Tasks))
for _, task := range n.Tasks {
sortedTasks = append(sortedTasks, task)
}
sort.Slice(sortedTasks, func(i, j int) bool {
return sortedTasks[i].DueDate.Before(sortedTasks[j].DueDate)
})
overdueLine := ""
for _, task := range sortedTasks {
until := time.Until(task.DueDate).Round(1*time.Hour) * -1
overdueLine += `* [` + task.Title + `](` + config.ServiceFrontendurl.GetString() + "tasks/" + strconv.FormatInt(task.ID, 10) + `), overdue since ` + utils.HumanizeDuration(until) + "\n"
}

View File

@ -228,28 +228,53 @@ func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int6
// that task, if there is none it will look for a subscription on the list the task belongs to and if that also
// doesn't exist it will check for a subscription for the namespace the list is belonging to.
func GetSubscription(s *xorm.Session, entityType SubscriptionEntityType, entityID int64, a web.Auth) (subscription *Subscription, err error) {
subs, err := GetSubscriptions(s, entityType, []int64{entityID}, a)
if err != nil || len(subs) == 0 {
return nil, err
}
if sub, exists := subs[entityID]; exists {
return sub, nil // Take exact match first, if available
}
for _, sub := range subs {
return sub, nil // For parents, take next available
}
return nil, nil
}
// GetSubscriptions returns a map of subscriptions to a set of given entity IDs
func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entityIDs []int64, a web.Auth) (listsToSubscriptions map[int64]*Subscription, err error) {
u, is := a.(*user.User)
if !is {
return
}
if err := entityType.validate(); err != nil {
return nil, err
}
subscription = &Subscription{}
cond := getSubscriberCondForEntity(entityType, entityID)
exists, err := s.
var entitiesFilter builder.Cond
for _, eID := range entityIDs {
if entitiesFilter == nil {
entitiesFilter = getSubscriberCondForEntity(entityType, eID)
continue
}
entitiesFilter = entitiesFilter.Or(getSubscriberCondForEntity(entityType, eID))
}
var subscriptions []*Subscription
err = s.
Where("user_id = ?", u.ID).
And(cond).
Get(subscription)
if !exists {
And(entitiesFilter).
Find(&subscriptions)
if err != nil {
return nil, err
}
subscription.Entity = subscription.EntityType.String()
return subscription, err
listsToSubscriptions = make(map[int64]*Subscription)
for _, sub := range subscriptions {
sub.Entity = sub.EntityType.String()
listsToSubscriptions[sub.EntityID] = sub
}
return listsToSubscriptions, nil
}
func getSubscribersForEntity(s *xorm.Session, entityType SubscriptionEntityType, entityID int64) (subscriptions []*Subscription, err error) {

View File

@ -221,6 +221,19 @@ func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, list *Li
return ErrUserDoesNotHaveAccessToList{list.ID, newAssigneeID}
}
exist, err := s.
Where("task_id = ? AND user_id = ?", t.ID, newAssigneeID).
Exist(&TaskAssginee{})
if err != nil {
return err
}
if exist {
return &ErrUserAlreadyAssigned{
UserID: newAssigneeID,
TaskID: t.ID,
}
}
_, err = s.Insert(TaskAssginee{
TaskID: t.ID,
UserID: newAssigneeID,

View File

@ -74,7 +74,8 @@ func validateTaskField(fieldName string) error {
taskPropertyUpdated,
taskPropertyPosition,
taskPropertyKanbanPosition,
taskPropertyBucketID:
taskPropertyBucketID,
taskPropertyIndex:
return nil
}
return ErrInvalidTaskField{TaskField: fieldName}

View File

@ -24,6 +24,7 @@ import (
"time"
"code.vikunja.io/api/pkg/config"
"github.com/iancoleman/strcase"
"github.com/vectordotdev/go-datemath"
"xorm.io/xorm/schemas"
@ -44,10 +45,47 @@ const (
taskFilterComparatorIn taskFilterComparator = "in"
)
// Guess what you get back if you ask Safari for a rfc 3339 formatted date?
const safariDateAndTime = "2006-01-02 15:04"
const safariDate = "2006-01-02"
type taskFilter struct {
field string
value interface{} // Needs to be an interface to be able to hold the field's native value
comparator taskFilterComparator
isNumeric bool
}
func parseTimeFromUserInput(timeString string) (value time.Time, err error) {
value, err = time.Parse(time.RFC3339, timeString)
if err != nil {
value, err = time.Parse(safariDateAndTime, timeString)
}
if err != nil {
value, err = time.Parse(safariDate, timeString)
}
if err != nil {
// Here we assume a date like 2022-11-1 and try to parse it manually
parts := strings.Split(timeString, "-")
if len(parts) < 3 {
return
}
year, err := strconv.Atoi(parts[0])
if err != nil {
return value, err
}
month, err := strconv.Atoi(parts[1])
if err != nil {
return value, err
}
day, err := strconv.Atoi(parts[2])
if err != nil {
return value, err
}
value = time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
return value.In(config.GetTimeZone()), nil
}
return value.In(config.GetTimeZone()), err
}
func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err error) {
@ -90,8 +128,9 @@ func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err
}
// Cast the field value to its native type
var reflectValue *reflect.StructField
if len(c.FilterValue) > i {
filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, c.FilterValue[i])
reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, c.FilterValue[i])
if err != nil {
return nil, ErrInvalidTaskFilterValue{
Value: filter.field,
@ -99,6 +138,9 @@ func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err
}
}
}
if reflectValue != nil {
filter.isNumeric = reflectValue.Type.Kind() == reflect.Int64
}
filters = append(filters, filter)
}
@ -165,8 +207,7 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa
if err == nil {
value = t.Time(datemath.WithLocation(config.GetTimeZone()))
} else {
value, err = time.Parse(time.RFC3339, rawValue)
value = value.(time.Time).In(config.GetTimeZone())
value, err = parseTimeFromUserInput(rawValue)
}
}
case reflect.Slice:
@ -192,7 +233,7 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa
return
}
func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string) (nativeValue interface{}, err error) {
func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string) (reflectField *reflect.StructField, nativeValue interface{}, err error) {
realFieldName := strings.ReplaceAll(strcase.ToCamel(fieldName), "Id", "ID")
@ -203,11 +244,11 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
for _, val := range vals {
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return nil, err
return nil, nil, err
}
valueSlice = append(valueSlice, v)
}
return valueSlice, nil
return nil, valueSlice, nil
}
nativeValue, err = strconv.ParseInt(value, 10, 64)
@ -217,12 +258,12 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
if realFieldName == "Assignees" {
vals := strings.Split(value, ",")
valueSlice := append([]string{}, vals...)
return valueSlice, nil
return nil, valueSlice, nil
}
field, ok := reflect.TypeOf(&Task{}).Elem().FieldByName(realFieldName)
if !ok {
return nil, ErrInvalidTaskField{TaskField: fieldName}
return nil, nil, ErrInvalidTaskField{TaskField: fieldName}
}
if comparator == taskFilterComparatorIn {
@ -231,12 +272,13 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
for _, val := range vals {
v, err := getValueForField(field, val)
if err != nil {
return nil, err
return nil, nil, err
}
valueSlice = append(valueSlice, v)
}
return valueSlice, nil
return nil, valueSlice, nil
}
return getValueForField(field, value)
val, err := getValueForField(field, value)
return &field, val, err
}

View File

@ -46,6 +46,7 @@ const (
taskPropertyPosition string = "position"
taskPropertyKanbanPosition string = "kanban_position"
taskPropertyBucketID string = "bucket_id"
taskPropertyIndex string = "index"
)
const (

View File

@ -37,7 +37,9 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
var tasks []*Task
err = s.
Where("due_date is not null and due_date < ?", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
Where("due_date is not null AND due_date < ? AND lists.is_archived = false AND namespaces.is_archived = false", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
Join("LEFT", "lists", "lists.id = tasks.list_id").
Join("LEFT", "namespaces", "lists.namespace_id = namespaces.id").
And("done = false").
Find(&tasks)
if err != nil {
@ -92,10 +94,10 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
if !exists {
uts[t.User.ID] = &userWithTasks{
user: t.User,
tasks: []*Task{},
tasks: make(map[int64]*Task),
}
}
uts[t.User.ID].tasks = append(uts[t.User.ID].tasks, t.Task)
uts[t.User.ID].tasks[t.Task.ID] = t.Task
}
}
@ -104,7 +106,7 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
type userWithTasks struct {
user *user.User
tasks []*Task
tasks map[int64]*Task
}
// RegisterOverdueReminderCron registers a function which checks once a day for tasks that are overdue and not done.
@ -138,9 +140,13 @@ func RegisterOverdueReminderCron() {
}
if len(ut.tasks) == 1 {
n = &UndoneTaskOverdueNotification{
User: ut.user,
Task: ut.tasks[0],
// We know there's only one entry in the map so this is actually O(1) and we can use it to get the
// first entry without knowing the key of it.
for _, t := range ut.tasks {
n = &UndoneTaskOverdueNotification{
User: ut.user,
Task: t,
}
}
}

View File

@ -202,7 +202,7 @@ func (rel *TaskRelation) Create(s *xorm.Session, a web.Auth) error {
// @Param relation body models.TaskRelation true "The relation object"
// @Param taskID path int true "Task ID"
// @Param relationKind path string true "The kind of the relation. See the TaskRelation type for more info."
// @Param otherTaskID path int true "The id of the other task."
// @Param otherTaskId path int true "The id of the other task."
// @Success 200 {object} models.Message "The task relation was successfully deleted."
// @Failure 400 {object} web.HTTPError "Invalid task relation object provided."
// @Failure 404 {object} web.HTTPError "The task relation was not found."

View File

@ -98,6 +98,9 @@ type Task struct {
// All attachments this task has
Attachments []*TaskAttachment `xorm:"-" json:"attachments"`
// If this task has a cover image, the field will return the id of the attachment that is the cover image.
CoverImageAttachmentID int64 `xorm:"bigint default 0" json:"cover_image_attachment_id"`
// True if a task is a favorite task. Favorite tasks show up in a separate "Important" list. This value depends on the user making the call to the api.
IsFavorite bool `xorm:"-" json:"is_favorite"`
@ -224,6 +227,9 @@ func getFilterCond(f *taskFilter, includeNulls bool) (cond builder.Cond, err err
if includeNulls {
cond = builder.Or(cond, &builder.IsNull{field})
if f.isNumeric {
cond = builder.Or(cond, &builder.IsNull{field}, &builder.Eq{field: 0})
}
}
return
@ -303,10 +309,10 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
// Because it does not have support for NULLS FIRST or NULLS LAST we work around this by
// first sorting for null (or not null) values and then the order we actually want to.
if db.Type() == schemas.MYSQL {
orderby += param.sortBy + " IS NULL, "
orderby += "`" + param.sortBy + "` IS NULL, "
}
orderby += param.sortBy + " " + param.orderBy.String()
orderby += "`" + param.sortBy + "` " + param.orderBy.String()
// Postgres and sqlite allow us to control how columns with null values are sorted.
// To make that consistent with the sort order we have and other dbms, we're adding a separate clause here.
@ -577,7 +583,9 @@ func GetTasksByUIDs(s *xorm.Session, uids []string, a web.Auth) (tasks []*Task,
func getRemindersForTasks(s *xorm.Session, taskIDs []int64) (reminders []*TaskReminder, err error) {
reminders = []*TaskReminder{}
err = s.In("task_id", taskIDs).Find(&reminders)
err = s.In("task_id", taskIDs).
OrderBy("reminder asc").
Find(&reminders)
return
}
@ -822,6 +830,11 @@ func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucke
}
}
if task.BucketID == 0 && originalTask != nil && originalTask.BucketID != 0 {
task.BucketID = originalTask.BucketID
}
// Either no bucket was provided or the task was moved between lists
if task.BucketID == 0 || (originalTask != nil && task.ListID != 0 && originalTask.ListID != task.ListID) {
bucket, err = getDefaultBucket(s, task.ListID)
if err != nil {
@ -866,6 +879,19 @@ func calculateDefaultPosition(entityID int64, position float64) float64 {
return position
}
func getNextTaskIndex(s *xorm.Session, listID int64) (nextIndex int64, err error) {
latestTask := &Task{}
_, err = s.
Where("list_id = ?", listID).
OrderBy("`index` desc").
Get(latestTask)
if err != nil {
return 0, err
}
return latestTask.Index + 1, nil
}
// Create is the implementation to create a list task
// @Summary Create a task
// @Description Inserts a task into a list.
@ -917,16 +943,14 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
}
// Get the index for this task
latestTask := &Task{}
_, err = s.Where("list_id = ?", t.ListID).OrderBy("id desc").Get(latestTask)
t.Index, err = getNextTaskIndex(s, t.ListID)
if err != nil {
return err
}
t.Index = latestTask.Index + 1
// If no position was supplied, set a default one
t.Position = calculateDefaultPosition(latestTask.ID+1, t.Position)
t.KanbanPosition = calculateDefaultPosition(latestTask.ID+1, t.KanbanPosition)
t.Position = calculateDefaultPosition(t.Index, t.Position)
t.KanbanPosition = calculateDefaultPosition(t.Index, t.KanbanPosition)
if _, err = s.Insert(t); err != nil {
return err
}
@ -979,6 +1003,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
// @Failure 403 {object} web.HTTPError "The user does not have access to the task (aka its list)"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{id} [post]
//
//nolint:gocyclo
func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
@ -1006,7 +1031,7 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
updateDone(&ot, t)
if err := setTaskBucket(s, t, &ot, t.BucketID != ot.BucketID); err != nil {
if err := setTaskBucket(s, t, &ot, t.BucketID != 0 && t.BucketID != ot.BucketID); err != nil {
return err
}
@ -1038,20 +1063,35 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
"position",
"repeat_mode",
"kanban_position",
"cover_image_attachment_id",
}
// If the task is being moved between lists, make sure to move the bucket + index as well
if t.ListID != 0 && ot.ListID != t.ListID {
latestTask := &Task{}
_, err = s.Where("list_id = ?", t.ListID).OrderBy("id desc").Get(latestTask)
t.Index, err = getNextTaskIndex(s, t.ListID)
if err != nil {
return err
}
t.Index = latestTask.Index + 1
colsToUpdate = append(colsToUpdate, "index")
}
// If a task attachment is being set as cover image, check if the attachment actually belongs to the task
if t.CoverImageAttachmentID != 0 {
is, err := s.Exist(&TaskAttachment{
TaskID: t.ID,
ID: t.CoverImageAttachmentID,
})
if err != nil {
return err
}
if !is {
return &ErrAttachmentDoesNotBelongToTask{
AttachmentID: t.CoverImageAttachmentID,
TaskID: t.ID,
}
}
}
wasFavorite, err := isFavorite(s, t.ID, a, FavoriteKindTask)
if err != nil {
return
@ -1147,6 +1187,10 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
if !t.IsFavorite {
ot.IsFavorite = false
}
// Attachment cover image
if t.CoverImageAttachmentID == 0 {
ot.CoverImageAttachmentID = 0
}
_, err = s.ID(t.ID).
Cols(colsToUpdate...).
@ -1289,18 +1333,39 @@ func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) {
}
}
// If a task has a start and end date, the end date should keep the difference to the start date when setting them as new
if !oldTask.StartDate.IsZero() && !oldTask.EndDate.IsZero() {
diff := oldTask.EndDate.Sub(oldTask.StartDate)
newTask.StartDate = now.Add(repeatDuration)
newTask.EndDate = now.Add(repeatDuration + diff)
} else {
if !oldTask.StartDate.IsZero() {
// We want to preserve intervals among the due, start and end dates.
// The due date is used as a reference point for all new dates, so the
// behaviour depends on whether the due date is set at all.
if oldTask.DueDate.IsZero() {
// If a task has no due date, but does have a start and end date, the
// end date should keep the difference to the start date when setting
// them as new
if !oldTask.StartDate.IsZero() && !oldTask.EndDate.IsZero() {
diff := oldTask.EndDate.Sub(oldTask.StartDate)
newTask.StartDate = now.Add(repeatDuration)
newTask.EndDate = now.Add(repeatDuration + diff)
} else {
if !oldTask.StartDate.IsZero() {
newTask.StartDate = now.Add(repeatDuration)
}
if !oldTask.EndDate.IsZero() {
newTask.EndDate = now.Add(repeatDuration)
}
}
} else {
// If the old task has a start and due date, we set the new start date
// to preserve the interval between them.
if !oldTask.StartDate.IsZero() {
diff := oldTask.DueDate.Sub(oldTask.StartDate)
newTask.StartDate = newTask.DueDate.Add(-diff)
}
// If the old task has an end and due date, we set the new end date
// to preserve the interval between them.
if !oldTask.EndDate.IsZero() {
newTask.EndDate = now.Add(repeatDuration)
diff := oldTask.DueDate.Sub(oldTask.EndDate)
newTask.EndDate = newTask.DueDate.Add(-diff)
}
}
@ -1309,9 +1374,9 @@ func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) {
// This helper function updates the reminders, doneAt, start and end dates of the *old* task
// and saves the new values in the newTask object.
// We make a few assumtions here:
// 1. Everything in oldTask is the truth - we figure out if we update anything at all if oldTask.RepeatAfter has a value > 0
// 2. Because of 1., this functions should not be used to update values other than Done in the same go
// We make a few assumptions here:
// 1. Everything in oldTask is the truth - we figure out if we update anything at all if oldTask.RepeatAfter has a value > 0
// 2. Because of 1., this functions should not be used to update values other than Done in the same go
func updateDone(oldTask *Task, newTask *Task) {
if !oldTask.Done && newTask.Done {
switch oldTask.RepeatMode {
@ -1345,8 +1410,14 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []time.Time) (err erro
return
}
// Resolve duplicates and sort them
reminderMap := make(map[string]time.Time, len(reminders))
for _, reminder := range reminders {
reminderMap[reminder.UTC().String()] = reminder
}
// Loop through all reminders and add them
for _, r := range reminders {
for _, r := range reminderMap {
_, err = s.Insert(&TaskReminder{TaskID: t.ID, Reminder: r})
if err != nil {
return err

View File

@ -20,10 +20,10 @@ import (
"testing"
"time"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/user"
"github.com/stretchr/testify/assert"
)
@ -324,6 +324,21 @@ func TestTask_Update(t *testing.T) {
"bucket_id": 1,
}, false)
})
t.Run("moving a task between lists should give it a correct index", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
task := &Task{
ID: 12,
ListID: 2, // From list 1
}
err := task.Update(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
assert.Equal(t, int64(3), task.Index)
})
}
func TestTask_Delete(t *testing.T) {

View File

@ -79,7 +79,7 @@ func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) {
// Delete deletes a user from a team
// @Summary Remove a user from a team
// @Description Remove a user from a team. This will also revoke any access this user might have via that team.
// @Description Remove a user from a team. This will also revoke any access this user might have via that team. A user can remove themselves from the team if they are not the last user in the team.
// @tags team
// @Produce json
// @Security JWTKeyAuth

View File

@ -17,6 +17,7 @@
package models
import (
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/xorm"
)
@ -28,6 +29,13 @@ func (tm *TeamMember) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
// CanDelete checks if the user can delete a new team member
func (tm *TeamMember) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
u, err := user.GetUserByUsername(s, tm.Username)
if err != nil {
return false, err
}
if u.ID == a.GetID() {
return true, nil
}
return tm.IsAdmin(s, a)
}

View File

@ -79,7 +79,7 @@ type TeamMember struct {
}
// TableName makes beautiful table names
func (TeamMember) TableName() string {
func (*TeamMember) TableName() string {
return "team_members"
}

View File

@ -51,6 +51,7 @@ type Provider struct {
Key string `json:"key"`
OriginalAuthURL string `json:"-"`
AuthURL string `json:"auth_url"`
LogoutURL string `json:"logout_url"`
ClientID string `json:"client_id"`
ClientSecret string `json:"-"`
openIDProvider *oidc.Provider

View File

@ -60,6 +60,7 @@ func GetAllProviders() (providers []*Provider, err error) {
}
provider, err := getProviderFromMap(pi)
if err != nil {
if provider != nil {
log.Errorf("Error while getting openid provider %s: %s", provider.Name, err)
@ -119,12 +120,18 @@ func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err erro
k := getKeyFromName(name)
logoutURL, ok := pi["logouturl"].(string)
if !ok {
logoutURL = ""
}
provider = &Provider{
Name: pi["name"].(string),
Key: k,
AuthURL: pi["authurl"].(string),
OriginalAuthURL: pi["authurl"].(string),
ClientSecret: pi["clientsecret"].(string),
LogoutURL: logoutURL,
}
cl, is := pi["clientid"].(int)
@ -143,7 +150,6 @@ func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err erro
ClientID: provider.ClientID,
ClientSecret: provider.ClientSecret,
RedirectURL: config.AuthOpenIDRedirectURL.GetString() + k,
// Discovery returns the OAuth2 endpoints.
Endpoint: provider.openIDProvider.Endpoint(),

View File

@ -18,7 +18,7 @@ package gravatar
import (
"context"
"io/ioutil"
"io"
"net/http"
"strconv"
"strings"
@ -74,7 +74,7 @@ func (g *Provider) GetAvatar(user *user.User, size int64) ([]byte, string, error
return nil, "", err
}
defer resp.Body.Close()
avatarContent, err := ioutil.ReadAll(resp.Body)
avatarContent, err := io.ReadAll(resp.Body)
if err != nil {
return nil, "", err
}

View File

@ -20,13 +20,14 @@ import (
"bytes"
"image"
"image/png"
"io/ioutil"
"io"
"strconv"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/modules/keyvalue"
"code.vikunja.io/api/pkg/user"
"github.com/disintegration/imaging"
)
@ -82,7 +83,7 @@ func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType
return nil, "", err
}
avatar, err = ioutil.ReadAll(buf)
avatar, err = io.ReadAll(buf)
if err != nil {
return nil, "", err
}

View File

@ -47,7 +47,7 @@ func unsplashImage(url string, c echo.Context) error {
// @Description Get an unsplash image. **Returns json on error.**
// @tags list
// @Produce octet-stream
// @Param thumb path int true "Unsplash Image ID"
// @Param image path int true "Unsplash Image ID"
// @Security JWTKeyAuth
// @Success 200 {} string "The image"
// @Failure 404 {object} models.Message "The image does not exist."
@ -67,7 +67,7 @@ func ProxyUnsplashImage(c echo.Context) error {
// @Description Get an unsplash thumbnail image. The thumbnail is cropped to a max width of 200px. **Returns json on error.**
// @tags list
// @Produce octet-stream
// @Param thumb path int true "Unsplash Image ID"
// @Param image path int true "Unsplash Image ID"
// @Security JWTKeyAuth
// @Success 200 {} string "The thumbnail"
// @Failure 404 {object} models.Message "The image does not exist."

View File

@ -22,6 +22,7 @@ import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"
@ -144,11 +145,16 @@ func Restore(filename string) error {
// FIXME: There has to be a general way to do this but this works for now.
if table == "notifications" {
for i := range content {
decoded, err := base64.StdEncoding.DecodeString(content[i]["notification"].(string))
if err != nil {
var decoded []byte
decoded, err = base64.StdEncoding.DecodeString(content[i]["notification"].(string))
if err != nil && !errors.Is(err, base64.CorruptInputError(0)) {
return fmt.Errorf("could not decode notification %s: %w", content[i]["notification"], err)
}
if err != nil && errors.Is(err, base64.CorruptInputError(0)) {
decoded = []byte(content[i]["notification"].(string))
}
content[i]["notification"] = string(decoded)
}
}

View File

@ -18,15 +18,14 @@ package migration
import (
"bytes"
"io/ioutil"
"code.vikunja.io/api/pkg/modules/background/handler"
"io"
"xorm.io/xorm"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/background/handler"
"code.vikunja.io/api/pkg/user"
)
@ -212,7 +211,7 @@ func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithListsAndTas
// Check if we have a file to create
if len(a.File.FileContent) > 0 {
a.TaskID = t.ID
fr := ioutil.NopCloser(bytes.NewReader(a.File.FileContent))
fr := io.NopCloser(bytes.NewReader(a.File.FileContent))
err = a.NewAttachment(s, fr, a.File.Name, a.File.Size, user)
if err != nil {
return

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,242 @@
// 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 ticktick
import (
"encoding/csv"
"errors"
"io"
"regexp"
"sort"
"strconv"
"strings"
"time"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/migration"
"code.vikunja.io/api/pkg/user"
"github.com/gocarina/gocsv"
)
const timeISO = "2006-01-02T15:04:05-0700"
type Migrator struct {
}
type tickTickTask struct {
FolderName string `csv:"Folder Name"`
ListName string `csv:"List Name"`
Title string `csv:"Title"`
TagsList string `csv:"Tags"`
Tags []string `csv:"-"`
Content string `csv:"Content"`
IsChecklistString string `csv:"Is Check list"`
IsChecklist bool `csv:"-"`
StartDate tickTickTime `csv:"Start Date"`
DueDate tickTickTime `csv:"Due Date"`
ReminderDuration string `csv:"Reminder"`
Reminder time.Duration `csv:"-"`
Repeat string `csv:"Repeat"`
Priority int `csv:"Priority"`
Status string `csv:"Status"`
CreatedTime tickTickTime `csv:"Created Time"`
CompletedTime tickTickTime `csv:"Completed Time"`
Order float64 `csv:"Order"`
TaskID int64 `csv:"taskId"`
ParentID int64 `csv:"parentId"`
}
type tickTickTime struct {
time.Time
}
func (date *tickTickTime) UnmarshalCSV(csv string) (err error) {
date.Time = time.Time{}
if csv == "" {
return nil
}
date.Time, err = time.Parse(timeISO, csv)
return err
}
// Copied from https://stackoverflow.com/a/57617885
var durationRegex = regexp.MustCompile(`P([\d\.]+Y)?([\d\.]+M)?([\d\.]+D)?T?([\d\.]+H)?([\d\.]+M)?([\d\.]+?S)?`)
// ParseDuration converts a ISO8601 duration into a time.Duration
func parseDuration(str string) time.Duration {
matches := durationRegex.FindStringSubmatch(str)
if len(matches) == 0 {
return 0
}
years := parseDurationPart(matches[1], time.Hour*24*365)
months := parseDurationPart(matches[2], time.Hour*24*30)
days := parseDurationPart(matches[3], time.Hour*24)
hours := parseDurationPart(matches[4], time.Hour)
minutes := parseDurationPart(matches[5], time.Second*60)
seconds := parseDurationPart(matches[6], time.Second)
return years + months + days + hours + minutes + seconds
}
func parseDurationPart(value string, unit time.Duration) time.Duration {
if len(value) != 0 {
if parsed, err := strconv.ParseFloat(value[:len(value)-1], 64); err == nil {
return time.Duration(float64(unit) * parsed)
}
}
return 0
}
func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.NamespaceWithListsAndTasks) {
namespace := &models.NamespaceWithListsAndTasks{
Namespace: models.Namespace{
Title: "Migrated from TickTick",
},
Lists: []*models.ListWithTasksAndBuckets{},
}
lists := make(map[string]*models.ListWithTasksAndBuckets)
for _, t := range tasks {
_, has := lists[t.ListName]
if !has {
lists[t.ListName] = &models.ListWithTasksAndBuckets{
List: models.List{
Title: t.ListName,
},
}
}
labels := make([]*models.Label, 0, len(t.Tags))
for _, tag := range t.Tags {
labels = append(labels, &models.Label{
Title: tag,
})
}
task := &models.TaskWithComments{
Task: models.Task{
ID: t.TaskID,
Title: t.Title,
Description: t.Content,
StartDate: t.StartDate.Time,
EndDate: t.DueDate.Time,
DueDate: t.DueDate.Time,
Done: t.Status == "1",
DoneAt: t.CompletedTime.Time,
Position: t.Order,
Labels: labels,
},
}
if !t.DueDate.IsZero() && t.Reminder > 0 {
task.Task.Reminders = []time.Time{
t.DueDate.Add(t.Reminder * -1),
}
}
if t.ParentID != 0 {
task.RelatedTasks = map[models.RelationKind][]*models.Task{
models.RelationKindParenttask: {{ID: t.ParentID}},
}
}
lists[t.ListName].Tasks = append(lists[t.ListName].Tasks, task)
}
for _, l := range lists {
namespace.Lists = append(namespace.Lists, l)
}
sort.Slice(namespace.Lists, func(i, j int) bool {
return namespace.Lists[i].Title < namespace.Lists[j].Title
})
return []*models.NamespaceWithListsAndTasks{namespace}
}
// Name is used to get the name of the ticktick migration - we're using the docs here to annotate the status route.
// @Summary Get migration status
// @Description Returns if the current user already did the migation or not. This is useful to show a confirmation message in the frontend if the user is trying to do the same migration again.
// @tags migration
// @Produce json
// @Security JWTKeyAuth
// @Success 200 {object} migration.Status "The migration status"
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/ticktick/status [get]
func (m *Migrator) Name() string {
return "ticktick"
}
func newLineSkipDecoder(r io.Reader, linesToSkip int) gocsv.SimpleDecoder {
reader := csv.NewReader(r)
// reader.FieldsPerRecord = -1
for i := 0; i < linesToSkip; i++ {
_, err := reader.Read()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
log.Debugf("[TickTick Migration] CSV parse error: %s", err)
}
}
reader.FieldsPerRecord = 0
return gocsv.NewSimpleDecoderFromCSVReader(reader)
}
// Migrate takes a ticktick export, parses it and imports everything in it into Vikunja.
// @Summary Import all lists, tasks etc. from a TickTick backup export
// @Description Imports all projects, tasks, notes, reminders, subtasks and files from a TickTick backup export into Vikunja.
// @tags migration
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param import formData string true "The TickTick backup csv file."
// @Success 200 {object} models.Message "A message telling you everything was migrated successfully."
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/ticktick/migrate [post]
func (m *Migrator) Migrate(user *user.User, file io.ReaderAt, size int64) error {
fr := io.NewSectionReader(file, 0, size)
//r := csv.NewReader(fr)
allTasks := []*tickTickTask{}
decode := newLineSkipDecoder(fr, 3)
err := gocsv.UnmarshalDecoder(decode, &allTasks)
if err != nil {
return err
}
for _, task := range allTasks {
if task.IsChecklistString == "Y" {
task.IsChecklist = true
}
reminder := parseDuration(task.ReminderDuration)
if reminder > 0 {
task.Reminder = reminder
}
task.Tags = strings.Split(task.TagsList, ", ")
}
vikunjaTasks := convertTickTickToVikunja(allTasks)
return migration.InsertFromStructure(vikunjaTasks, user)
}

View File

@ -0,0 +1,139 @@
// 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 ticktick
import (
"testing"
"time"
"code.vikunja.io/api/pkg/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConvertTicktickTasksToVikunja(t *testing.T) {
t1, err := time.Parse(time.RFC3339Nano, "2022-11-18T03:00:00.4770000Z")
require.NoError(t, err)
time1 := tickTickTime{Time: t1}
t2, err := time.Parse(time.RFC3339Nano, "2022-12-18T03:00:00.4770000Z")
require.NoError(t, err)
time2 := tickTickTime{Time: t2}
t3, err := time.Parse(time.RFC3339Nano, "2022-12-10T03:00:00.4770000Z")
require.NoError(t, err)
time3 := tickTickTime{Time: t3}
duration, err := time.ParseDuration("24h")
require.NoError(t, err)
tickTickTasks := []*tickTickTask{
{
TaskID: 1,
ParentID: 0,
ListName: "List 1",
Title: "Test task 1",
Tags: []string{"label1", "label2"},
Content: "Lorem Ipsum Dolor sit amet",
StartDate: time1,
DueDate: time2,
Reminder: duration,
Repeat: "FREQ=WEEKLY;INTERVAL=1;UNTIL=20190117T210000Z",
Status: "0",
Order: -1099511627776,
},
{
TaskID: 2,
ParentID: 1,
ListName: "List 1",
Title: "Test task 2",
Status: "1",
CompletedTime: time3,
Order: -1099511626,
},
{
TaskID: 3,
ParentID: 0,
ListName: "List 1",
Title: "Test task 3",
Tags: []string{"label1", "label2", "other label"},
StartDate: time1,
DueDate: time2,
Reminder: duration,
Status: "0",
Order: -109951627776,
},
{
TaskID: 4,
ParentID: 0,
ListName: "List 2",
Title: "Test task 4",
Status: "0",
Order: -109951627777,
},
}
vikunjaTasks := convertTickTickToVikunja(tickTickTasks)
assert.Len(t, vikunjaTasks, 1)
assert.Len(t, vikunjaTasks[0].Lists, 2)
assert.Len(t, vikunjaTasks[0].Lists[0].Tasks, 3)
assert.Equal(t, vikunjaTasks[0].Lists[0].Title, tickTickTasks[0].ListName)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Title, tickTickTasks[0].Title)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Description, tickTickTasks[0].Content)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].StartDate, tickTickTasks[0].StartDate.Time)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].EndDate, tickTickTasks[0].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].DueDate, tickTickTasks[0].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Labels, []*models.Label{
{Title: "label1"},
{Title: "label2"},
})
//assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Reminders, tickTickTasks[0].) // TODO
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Position, tickTickTasks[0].Order)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Done, false)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[1].Title, tickTickTasks[1].Title)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[1].Position, tickTickTasks[1].Order)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[1].Done, true)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[1].DoneAt, tickTickTasks[1].CompletedTime.Time)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[1].RelatedTasks, models.RelatedTaskMap{
models.RelationKindParenttask: []*models.Task{
{
ID: tickTickTasks[1].ParentID,
},
},
})
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Title, tickTickTasks[2].Title)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Description, tickTickTasks[2].Content)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].StartDate, tickTickTasks[2].StartDate.Time)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].EndDate, tickTickTasks[2].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].DueDate, tickTickTasks[2].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Labels, []*models.Label{
{Title: "label1"},
{Title: "label2"},
{Title: "other label"},
})
//assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Reminders, tickTickTasks[0].) // TODO
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Position, tickTickTasks[2].Order)
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Done, false)
assert.Len(t, vikunjaTasks[0].Lists[1].Tasks, 1)
assert.Equal(t, vikunjaTasks[0].Lists[1].Title, tickTickTasks[3].ListName)
assert.Equal(t, vikunjaTasks[0].Lists[1].Tasks[0].Title, tickTickTasks[3].Title)
assert.Equal(t, vikunjaTasks[0].Lists[1].Tasks[0].Position, tickTickTasks[3].Order)
}

View File

@ -45,28 +45,25 @@ type apiTokenResponse struct {
}
type label struct {
ID int64 `json:"id"`
ID string `json:"id"`
Name string `json:"name"`
Color int64 `json:"color"`
Color string `json:"color"`
ItemOrder int64 `json:"item_order"`
IsDeleted int64 `json:"is_deleted"`
IsFavorite int64 `json:"is_favorite"`
IsDeleted bool `json:"is_deleted"`
IsFavorite bool `json:"is_favorite"`
}
type project struct {
ID int64 `json:"id"`
LegacyID int64 `json:"legacy_id"`
Name string `json:"name"`
Color int64 `json:"color"`
ParentID int64 `json:"parent_id"`
ChildOrder int64 `json:"child_order"`
Collapsed int64 `json:"collapsed"`
Shared bool `json:"shared"`
LegacyParentID int64 `json:"legacy_parent_id"`
SyncID int64 `json:"sync_id"`
IsDeleted int64 `json:"is_deleted"`
IsArchived int64 `json:"is_archived"`
IsFavorite int64 `json:"is_favorite"`
ID string `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
ParentID string `json:"parent_id"`
ChildOrder int64 `json:"child_order"`
Collapsed bool `json:"collapsed"`
Shared bool `json:"shared"`
IsDeleted bool `json:"is_deleted"`
IsArchived bool `json:"is_archived"`
IsFavorite bool `json:"is_favorite"`
}
type dueDate struct {
@ -78,44 +75,43 @@ type dueDate struct {
}
type item struct {
ID int64 `json:"id"`
LegacyID int64 `json:"legacy_id"`
UserID int64 `json:"user_id"`
ProjectID int64 `json:"project_id"`
LegacyProjectID int64 `json:"legacy_project_id"`
Content string `json:"content"`
Priority int64 `json:"priority"`
Due *dueDate `json:"due"`
ParentID int64 `json:"parent_id"`
LegacyParentID int64 `json:"legacy_parent_id"`
ChildOrder int64 `json:"child_order"`
SectionID int64 `json:"section_id"`
DayOrder int64 `json:"day_order"`
Collapsed int64 `json:"collapsed"`
Children interface{} `json:"children"`
Labels []int64 `json:"labels"`
AddedByUID int64 `json:"added_by_uid"`
AssignedByUID int64 `json:"assigned_by_uid"`
ResponsibleUID int64 `json:"responsible_uid"`
Checked int64 `json:"checked"`
InHistory int64 `json:"in_history"`
IsDeleted int64 `json:"is_deleted"`
DateAdded time.Time `json:"date_added"`
HasMoreNotes bool `json:"has_more_notes"`
DateCompleted time.Time `json:"date_completed"`
ID string `json:"id"`
LegacyID string `json:"legacy_id"`
UserID string `json:"user_id"`
ProjectID string `json:"project_id"`
Content string `json:"content"`
Priority int64 `json:"priority"`
Due *dueDate `json:"due"`
ParentID string `json:"parent_id"`
ChildOrder int64 `json:"child_order"`
SectionID string `json:"section_id"`
Children interface{} `json:"children"`
Labels []string `json:"labels"`
AddedByUID string `json:"added_by_uid"`
AssignedByUID string `json:"assigned_by_uid"`
ResponsibleUID string `json:"responsible_uid"`
Checked bool `json:"checked"`
IsDeleted bool `json:"is_deleted"`
DateAdded time.Time `json:"added_at"`
HasMoreNotes bool `json:"has_more_notes"`
DateCompleted time.Time `json:"completed_at"`
}
type itemWrapper struct {
Item *item `json:"item"`
}
type doneItem struct {
CompletedDate time.Time `json:"completed_date"`
CompletedDate time.Time `json:"completed_at"`
Content string `json:"content"`
ID int64 `json:"id"`
ProjectID int64 `json:"project_id"`
TaskID int64 `json:"task_id"`
UserID int `json:"user_id"`
ID string `json:"id"`
ProjectID string `json:"project_id"`
TaskID string `json:"task_id"`
}
type doneItemSync struct {
Items []*doneItem `json:"items"`
Items []*doneItem `json:"items"`
Projects map[string]*project `json:"projects"`
}
type fileAttachment struct {
@ -127,18 +123,14 @@ type fileAttachment struct {
}
type note struct {
ID int64 `json:"id"`
LegacyID int64 `json:"legacy_id"`
PostedUID int64 `json:"posted_uid"`
ProjectID int64 `json:"project_id"`
LegacyProjectID int64 `json:"legacy_project_id"`
ItemID int64 `json:"item_id"`
LegacyItemID int64 `json:"legacy_item_id"`
Content string `json:"content"`
FileAttachment *fileAttachment `json:"file_attachment"`
UidsToNotify []int64 `json:"uids_to_notify"`
IsDeleted int64 `json:"is_deleted"`
Posted time.Time `json:"posted"`
ID string `json:"id"`
PostedUID int64 `json:"posted_uid"`
ProjectID string `json:"project_id"`
ItemID string `json:"item_id"`
Content string `json:"content"`
FileAttachment *fileAttachment `json:"file_attachment"`
IsDeleted bool `json:"is_deleted"`
Posted time.Time `json:"posted_at"`
}
type projectNote struct {
@ -148,15 +140,13 @@ type projectNote struct {
IsDeleted int64 `json:"is_deleted"`
Posted time.Time `json:"posted"`
PostedUID int64 `json:"posted_uid"`
ProjectID int64 `json:"project_id"`
ProjectID string `json:"project_id"`
UidsToNotify []int64 `json:"uids_to_notify"`
}
type reminder struct {
ID int64 `json:"id"`
NotifyUID int64 `json:"notify_uid"`
ItemID int64 `json:"item_id"`
Service string `json:"service"`
ID string `json:"id"`
ItemID string `json:"item_id"`
Type string `json:"type"`
Due *dueDate `json:"due"`
MmOffset int64 `json:"mm_offset"`
@ -164,11 +154,11 @@ type reminder struct {
}
type section struct {
ID int64 `json:"id"`
DateAdded time.Time `json:"date_added"`
ID string `json:"id"`
DateAdded time.Time `json:"added_at"`
IsDeleted bool `json:"is_deleted"`
Name string `json:"name"`
ProjectID int64 `json:"project_id"`
ProjectID string `json:"project_id"`
SectionOrder int64 `json:"section_order"`
}
@ -182,32 +172,32 @@ type sync struct {
Sections []*section `json:"sections"`
}
var todoistColors = map[int64]string{}
var todoistColors = map[string]string{}
func init() {
todoistColors = make(map[int64]string, 19)
// The todoists colors are static, taken from https://developer.todoist.com/sync/v8/#colors
todoistColors = map[int64]string{
30: "b8256f",
31: "db4035",
32: "ff9933",
33: "fad000",
34: "afb83b",
35: "7ecc49",
36: "299438",
37: "6accbc",
38: "158fad",
39: "14aaf5",
40: "96c3eb",
41: "4073ff",
42: "884dff",
43: "af38eb",
44: "eb96eb",
45: "e05194",
46: "ff8d85",
47: "808080",
48: "b8b8b8",
49: "ccac93",
todoistColors = make(map[string]string, 19)
// The todoists colors are static, taken from https://developer.todoist.com/guides/#colors
todoistColors = map[string]string{
"berry_red": "b8256f",
"red": "db4035",
"orange": "ff9933",
"yellow": "fad000",
"olive_green": "afb83b",
"lime_green": "7ecc49",
"green": "299438",
"mint_green": "6accbc",
"teal": "158fad",
"sky_blue": "14aaf5",
"light_blue": "96c3eb",
"blue": "4073ff",
"grape": "884dff",
"violet": "af38eb",
"lavender": "eb96eb",
"magenta": "e05194",
"salmon": "ff8d85",
"charcoal": "808080",
"grey": "b8b8b8",
"taupe": "ccac93",
}
}
@ -261,7 +251,7 @@ func parseDate(dateString string) (date time.Time, err error) {
return date, err
}
func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVikunjaHierachie []*models.NamespaceWithListsAndTasks, err error) {
func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVikunjaHierachie []*models.NamespaceWithListsAndTasks, err error) {
newNamespace := &models.NamespaceWithListsAndTasks{
Namespace: models.Namespace{
@ -270,20 +260,22 @@ func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVik
}
// A map for all vikunja lists with the project id they're coming from as key
lists := make(map[int64]*models.ListWithTasksAndBuckets, len(sync.Projects))
lists := make(map[string]*models.ListWithTasksAndBuckets, len(sync.Projects))
// A map for all vikunja tasks with the todoist task id as key to find them easily and add more data
tasks := make(map[int64]*models.TaskWithComments, len(sync.Items))
tasks := make(map[string]*models.TaskWithComments, len(sync.Items))
// A map for all vikunja labels with the todoist id as key to find them easier
labels := make(map[int64]*models.Label, len(sync.Labels))
labels := make(map[string]*models.Label, len(sync.Labels))
sections := make(map[string]int64)
for _, p := range sync.Projects {
list := &models.ListWithTasksAndBuckets{
List: models.List{
Title: p.Name,
HexColor: todoistColors[p.Color],
IsArchived: p.IsArchived == 1,
IsArchived: p.IsArchived,
},
}
@ -296,20 +288,22 @@ func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVik
return sync.Sections[i].SectionOrder < sync.Sections[j].SectionOrder
})
var fabricatedSectionID int64 = 1
for _, section := range sync.Sections {
if section.IsDeleted || section.ProjectID == 0 {
if section.IsDeleted || section.ProjectID == "" {
continue
}
lists[section.ProjectID].Buckets = append(lists[section.ProjectID].Buckets, &models.Bucket{
ID: section.ID,
ID: fabricatedSectionID,
Title: section.Name,
Created: section.DateAdded,
})
sections[section.ID] = fabricatedSectionID
}
for _, label := range sync.Labels {
labels[label.ID] = &models.Label{
labels[label.Name] = &models.Label{
Title: label.Name,
HexColor: todoistColors[label.Color],
}
@ -320,8 +314,8 @@ func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVik
Task: models.Task{
Title: i.Content,
Created: i.DateAdded.In(config.GetTimeZone()),
Done: i.Checked == 1,
BucketID: i.SectionID,
Done: i.Checked,
BucketID: sections[i.SectionID],
},
}
@ -352,24 +346,31 @@ func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVik
}
// Put all labels together from earlier
for _, lID := range i.Labels {
task.Labels = append(task.Labels, labels[lID])
for _, lName := range i.Labels {
task.Labels = append(task.Labels, labels[lName])
}
tasks[i.ID] = task
if _, exists := lists[i.ProjectID]; !exists {
log.Debugf("[Todoist Migration] Tried to put item %s in project %s but the project does not exist", i.ID, i.ProjectID)
continue
}
lists[i.ProjectID].Tasks = append(lists[i.ProjectID].Tasks, task)
fabricatedSectionID++
}
// If the parenId of a task is not 0, create a task relation
// We're looping again here to make sure we have seem all tasks before and have them in our map
for _, i := range sync.Items {
if i.ParentID == 0 {
if i.ParentID == "" {
continue
}
if _, exists := tasks[i.ParentID]; !exists {
log.Debugf("[Todoist Migration] Could not find task %d in tasks map while trying to get resolve subtasks for task %d", i.ParentID, i.ID)
log.Debugf("[Todoist Migration] Could not find task %s in tasks map while trying to get resolve subtasks for task %s", i.ParentID, i.ID)
continue
}
@ -397,7 +398,7 @@ func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVik
// FIXME: Should be comments
for _, n := range sync.Notes {
if _, exists := tasks[n.ItemID]; !exists {
log.Debugf("[Todoist Migration] Could not find task %d for note %d", n.ItemID, n.ID)
log.Debugf("[Todoist Migration] Could not find task %s for note %s", n.ItemID, n.ID)
continue
}
@ -450,7 +451,7 @@ func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVik
}
if _, exists := tasks[r.ItemID]; !exists {
log.Debugf("Could not find task %d for reminder %d while trying to resolve reminders", r.ItemID, r.ID)
log.Debugf("Could not find task %s for reminder %s while trying to resolve reminders", r.ItemID, r.ID)
continue
}
@ -527,7 +528,7 @@ func (m *Migration) Migrate(u *user.User) (err error) {
"sync_token": []string{"*"},
"resource_types": []string{"[\"all\"]"},
}
resp, err := migration.DoPost("https://api.todoist.com/sync/v8/sync", form)
resp, err := migration.DoPost("https://api.todoist.com/sync/v9/sync", form)
if err != nil {
return
}
@ -540,37 +541,66 @@ func (m *Migration) Migrate(u *user.User) (err error) {
}
log.Debugf("[Todoist Migration] Getting done items for user %d", u.ID)
// Get all done tasks
resp, err = migration.DoPost("https://api.todoist.com/sync/v8/completed/get_all", form)
if err != nil {
return
}
defer resp.Body.Close()
completedSyncResponse := &doneItemSync{}
err = json.NewDecoder(resp.Body).Decode(completedSyncResponse)
if err != nil {
return
}
// Get all done tasks and projects
offset := 0
doneItems := make(map[string]*doneItem)
sort.Slice(completedSyncResponse.Items, func(i, j int) bool {
return completedSyncResponse.Items[i].CompletedDate.After(completedSyncResponse.Items[j].CompletedDate)
})
doneItems := make(map[int64]*doneItem, len(completedSyncResponse.Items))
for _, i := range completedSyncResponse.Items {
if _, has := doneItems[i.TaskID]; has {
// Only set the newest completion date
continue
for {
resp, err = migration.DoPost("https://api.todoist.com/sync/v9/completed/get_all?limit=200&offset="+strconv.Itoa(offset), form)
if err != nil {
return
}
doneItems[i.TaskID] = i
defer resp.Body.Close()
completedSyncResponse := &doneItemSync{}
err = json.NewDecoder(resp.Body).Decode(completedSyncResponse)
if err != nil {
return
}
sort.Slice(completedSyncResponse.Items, func(i, j int) bool {
return completedSyncResponse.Items[i].CompletedDate.After(completedSyncResponse.Items[j].CompletedDate)
})
for _, i := range completedSyncResponse.Items {
if _, has := doneItems[i.TaskID]; has {
// Only set the newest completion date
continue
}
doneItems[i.TaskID] = i
// need to get done item data
resp, err = migration.DoPost("https://api.todoist.com/sync/v9/items/get", url.Values{
"token": []string{token},
"item_id": []string{i.TaskID},
})
if err != nil {
return
}
defer resp.Body.Close()
doneI := &itemWrapper{}
err = json.NewDecoder(resp.Body).Decode(doneI)
if err != nil {
return
}
log.Debugf("[Todoist Migration] Retrieved full task data for done task %s", i.TaskID)
syncResponse.Items = append(syncResponse.Items, doneI.Item)
}
if len(completedSyncResponse.Items) < 200 {
break
}
offset++
log.Debugf("[Todoist Migration] User %d has more than 200 done tasks or projects, looping to get more; iteration %d", u.ID, offset)
}
log.Debugf("[Todoist Migration] Got %d done items for user %d", len(completedSyncResponse.Items), u.ID)
log.Debugf("[Todoist Migration] Got %d done items for user %d", len(doneItems), u.ID)
log.Debugf("[Todoist Migration] Getting archived projects for user %d", u.ID)
// Get all archived projects
resp, err = migration.DoPost("https://api.todoist.com/sync/v8/projects/get_archived", form)
resp, err = migration.DoPost("https://api.todoist.com/sync/v9/projects/get_archived", form)
if err != nil {
return
}
@ -587,9 +617,8 @@ func (m *Migration) Migrate(u *user.User) (err error) {
log.Debugf("[Todoist Migration] Getting data for archived projects for user %d", u.ID)
// Project data is not included in the regular sync for archived projects so we need to get all of those by hand
//https://api.todoist.com/sync/v8/projects/get_data\?project_id\=2269005399
for _, p := range archivedProjects {
resp, err = migration.DoPost("https://api.todoist.com/sync/v8/projects/get_data?project_id="+strconv.FormatInt(p.ID, 10), form)
resp, err = migration.DoPost("https://api.todoist.com/sync/v9/projects/get_data?project_id="+p.ID, form)
if err != nil {
return
}

View File

@ -17,8 +17,7 @@
package todoist
import (
"io/ioutil"
"strconv"
"os"
"testing"
"time"
@ -47,33 +46,32 @@ func TestConvertTodoistToVikunja(t *testing.T) {
dueTimeWithTime = dueTimeWithTime.In(config.GetTimeZone())
nilTime, err := time.Parse(time.RFC3339Nano, "0001-01-01T00:00:00Z")
assert.NoError(t, err)
exampleFile, err := ioutil.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/wunderlist/testimage.jpg")
exampleFile, err := os.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/testimage.jpg")
assert.NoError(t, err)
makeTestItem := func(id, projectId int64, hasDueDate, hasLabels, done bool) *item {
makeTestItem := func(id, projectId string, hasDueDate, hasLabels, done bool) *item {
item := &item{
ID: id,
UserID: 1855589,
UserID: "1855589",
ProjectID: projectId,
Content: "Task" + strconv.FormatInt(id, 10),
Content: "Task" + id,
Priority: 1,
ParentID: 0,
ChildOrder: 1,
DateAdded: time1,
DateCompleted: nilTime,
}
if done {
item.Checked = 1
item.Checked = true
item.DateCompleted = time3
}
if hasLabels {
item.Labels = []int64{
80000,
80001,
80002,
80003,
item.Labels = []string{
"Label1",
"Label2",
"Label3",
"Label4",
}
}
@ -91,163 +89,163 @@ func TestConvertTodoistToVikunja(t *testing.T) {
testSync := &sync{
Projects: []*project{
{
ID: 396936926,
ID: "396936926",
Name: "Project1",
Color: 30,
Color: "berry_red",
ChildOrder: 1,
Collapsed: 0,
Collapsed: false,
Shared: false,
IsDeleted: 0,
IsArchived: 0,
IsFavorite: 0,
IsDeleted: false,
IsArchived: false,
IsFavorite: false,
},
{
ID: 396936927,
ID: "396936927",
Name: "Project2",
Color: 37,
Color: "mint_green",
ChildOrder: 1,
Collapsed: 0,
Collapsed: false,
Shared: false,
IsDeleted: 0,
IsArchived: 0,
IsFavorite: 0,
IsDeleted: false,
IsArchived: false,
IsFavorite: false,
},
{
ID: 396936928,
ID: "396936928",
Name: "Project3 - Archived",
Color: 37,
Color: "mint_green",
ChildOrder: 1,
Collapsed: 0,
Collapsed: false,
Shared: false,
IsDeleted: 0,
IsArchived: 1,
IsFavorite: 0,
IsDeleted: false,
IsArchived: true,
IsFavorite: false,
},
},
Items: []*item{
makeTestItem(400000000, 396936926, false, false, false),
makeTestItem(400000001, 396936926, false, false, false),
makeTestItem(400000002, 396936926, false, false, false),
makeTestItem(400000003, 396936926, true, true, true),
makeTestItem(400000004, 396936926, false, true, false),
makeTestItem(400000005, 396936926, true, false, true),
makeTestItem(400000006, 396936926, true, false, true),
makeTestItem("400000000", "396936926", false, false, false),
makeTestItem("400000001", "396936926", false, false, false),
makeTestItem("400000002", "396936926", false, false, false),
makeTestItem("400000003", "396936926", true, true, true),
makeTestItem("400000004", "396936926", false, true, false),
makeTestItem("400000005", "396936926", true, false, true),
makeTestItem("400000006", "396936926", true, false, true),
{
ID: 400000110,
UserID: 1855589,
ProjectID: 396936926,
ID: "400000110",
UserID: "1855589",
ProjectID: "396936926",
Content: "Task with parent",
Priority: 2,
ParentID: 400000006,
ParentID: "400000006",
ChildOrder: 1,
Checked: 0,
Checked: false,
DateAdded: time1,
},
{
ID: 400000106,
UserID: 1855589,
ProjectID: 396936926,
ID: "400000106",
UserID: "1855589",
ProjectID: "396936926",
Content: "Task400000106",
Priority: 1,
ParentID: 0,
ParentID: "",
ChildOrder: 1,
DateAdded: time1,
Checked: 1,
Checked: true,
DateCompleted: time3,
Due: &dueDate{
Date: "2021-01-31T19:00:00Z",
Timezone: nil,
IsRecurring: false,
},
Labels: []int64{
80000,
80001,
80002,
80003,
Labels: []string{
"Label1",
"Label2",
"Label3",
"Label4",
},
},
makeTestItem(400000107, 396936926, false, false, true),
makeTestItem(400000108, 396936926, false, false, true),
makeTestItem("400000107", "396936926", false, false, true),
makeTestItem("400000108", "396936926", false, false, true),
{
ID: 400000109,
UserID: 1855589,
ProjectID: 396936926,
ID: "400000109",
UserID: "1855589",
ProjectID: "396936926",
Content: "Task400000109",
Priority: 1,
ChildOrder: 1,
Checked: 1,
Checked: true,
DateAdded: time1,
DateCompleted: time3,
SectionID: 1234,
SectionID: "1234",
},
makeTestItem(400000007, 396936927, true, false, false),
makeTestItem(400000008, 396936927, true, false, false),
makeTestItem(400000009, 396936927, false, false, false),
makeTestItem(400000010, 396936927, false, false, true),
makeTestItem(400000101, 396936927, false, false, false),
makeTestItem(400000102, 396936927, true, true, false),
makeTestItem(400000103, 396936927, false, true, false),
makeTestItem(400000104, 396936927, false, true, false),
makeTestItem(400000105, 396936927, true, true, false),
makeTestItem("400000007", "396936927", true, false, false),
makeTestItem("400000008", "396936927", true, false, false),
makeTestItem("400000009", "396936927", false, false, false),
makeTestItem("400000010", "396936927", false, false, true),
makeTestItem("400000101", "396936927", false, false, false),
makeTestItem("400000102", "396936927", true, true, false),
makeTestItem("400000103", "396936927", false, true, false),
makeTestItem("400000104", "396936927", false, true, false),
makeTestItem("400000105", "396936927", true, true, false),
makeTestItem(400000111, 396936928, false, false, true),
makeTestItem("400000111", "396936928", false, false, true),
},
Labels: []*label{
{
ID: 80000,
ID: "80000",
Name: "Label1",
Color: 30,
Color: "berry_red",
},
{
ID: 80001,
ID: "80001",
Name: "Label2",
Color: 31,
Color: "red",
},
{
ID: 80002,
ID: "80002",
Name: "Label3",
Color: 32,
Color: "orange",
},
{
ID: 80003,
ID: "80003",
Name: "Label4",
Color: 33,
Color: "yellow",
},
},
Notes: []*note{
{
ID: 101476,
ID: "101476",
PostedUID: 1855589,
ItemID: 400000000,
ItemID: "400000000",
Content: "Lorem Ipsum dolor sit amet",
Posted: time1,
},
{
ID: 101477,
ID: "101477",
PostedUID: 1855589,
ItemID: 400000001,
ItemID: "400000001",
Content: "Lorem Ipsum dolor sit amet",
Posted: time1,
},
{
ID: 101478,
ID: "101478",
PostedUID: 1855589,
ItemID: 400000003,
ItemID: "400000003",
Content: "Lorem Ipsum dolor sit amet",
Posted: time1,
},
{
ID: 101479,
ID: "101479",
PostedUID: 1855589,
ItemID: 400000010,
ItemID: "400000010",
Content: "Lorem Ipsum dolor sit amet",
Posted: time1,
},
{
ID: 101480,
ID: "101480",
PostedUID: 1855589,
ItemID: 400000101,
ItemID: "400000101",
Content: "Lorem Ipsum dolor sit amet",
FileAttachment: &fileAttachment{
FileName: "file.md",
@ -263,43 +261,43 @@ func TestConvertTodoistToVikunja(t *testing.T) {
{
ID: 102000,
Content: "Lorem Ipsum dolor sit amet",
ProjectID: 396936926,
ProjectID: "396936926",
Posted: time3,
PostedUID: 1855589,
},
{
ID: 102001,
Content: "Lorem Ipsum dolor sit amet 2",
ProjectID: 396936926,
ProjectID: "396936926",
Posted: time3,
PostedUID: 1855589,
},
{
ID: 102002,
Content: "Lorem Ipsum dolor sit amet 3",
ProjectID: 396936926,
ProjectID: "396936926",
Posted: time3,
PostedUID: 1855589,
},
{
ID: 102003,
Content: "Lorem Ipsum dolor sit amet 4",
ProjectID: 396936927,
ProjectID: "396936927",
Posted: time3,
PostedUID: 1855589,
},
{
ID: 102004,
Content: "Lorem Ipsum dolor sit amet 5",
ProjectID: 396936927,
ProjectID: "396936927",
Posted: time3,
PostedUID: 1855589,
},
},
Reminders: []*reminder{
{
ID: 103000,
ItemID: 400000000,
ID: "103000",
ItemID: "400000000",
Due: &dueDate{
Date: "2020-06-15",
IsRecurring: false,
@ -307,40 +305,40 @@ func TestConvertTodoistToVikunja(t *testing.T) {
MmOffset: 180,
},
{
ID: 103001,
ItemID: 400000000,
ID: "103001",
ItemID: "400000000",
Due: &dueDate{
Date: "2020-06-16T07:00:00",
IsRecurring: false,
},
},
{
ID: 103002,
ItemID: 400000002,
ID: "103002",
ItemID: "400000002",
Due: &dueDate{
Date: "2020-07-15T07:00:00Z",
IsRecurring: true,
},
},
{
ID: 103003,
ItemID: 400000003,
ID: "103003",
ItemID: "400000003",
Due: &dueDate{
Date: "2020-06-15T07:00:00",
IsRecurring: false,
},
},
{
ID: 103004,
ItemID: 400000005,
ID: "103004",
ItemID: "400000005",
Due: &dueDate{
Date: "2020-06-15T07:00:00",
IsRecurring: false,
},
},
{
ID: 103006,
ItemID: 400000009,
ID: "103006",
ItemID: "400000009",
Due: &dueDate{
Date: "2020-06-15T07:00:00",
IsRecurring: false,
@ -349,9 +347,9 @@ func TestConvertTodoistToVikunja(t *testing.T) {
},
Sections: []*section{
{
ID: 1234,
ID: "1234",
Name: "Some Bucket",
ProjectID: 396936926,
ProjectID: "396936926",
},
},
}
@ -359,19 +357,19 @@ func TestConvertTodoistToVikunja(t *testing.T) {
vikunjaLabels := []*models.Label{
{
Title: "Label1",
HexColor: todoistColors[30],
HexColor: todoistColors["berry_red"],
},
{
Title: "Label2",
HexColor: todoistColors[31],
HexColor: todoistColors["red"],
},
{
Title: "Label3",
HexColor: todoistColors[32],
HexColor: todoistColors["orange"],
},
{
Title: "Label4",
HexColor: todoistColors[33],
HexColor: todoistColors["yellow"],
},
}
@ -385,11 +383,11 @@ func TestConvertTodoistToVikunja(t *testing.T) {
List: models.List{
Title: "Project1",
Description: "Lorem Ipsum dolor sit amet\nLorem Ipsum dolor sit amet 2\nLorem Ipsum dolor sit amet 3",
HexColor: todoistColors[30],
HexColor: todoistColors["berry_red"],
},
Buckets: []*models.Bucket{
{
ID: 1234,
ID: 1,
Title: "Some Bucket",
},
},
@ -510,7 +508,7 @@ func TestConvertTodoistToVikunja(t *testing.T) {
Done: true,
Created: time1,
DoneAt: time3,
BucketID: 1234,
BucketID: 1,
},
},
},
@ -519,7 +517,7 @@ func TestConvertTodoistToVikunja(t *testing.T) {
List: models.List{
Title: "Project2",
Description: "Lorem Ipsum dolor sit amet 4\nLorem Ipsum dolor sit amet 5",
HexColor: todoistColors[37],
HexColor: todoistColors["mint_green"],
},
Tasks: []*models.TaskWithComments{
{
@ -616,7 +614,7 @@ func TestConvertTodoistToVikunja(t *testing.T) {
{
List: models.List{
Title: "Project3 - Archived",
HexColor: todoistColors[37],
HexColor: todoistColors["mint_green"],
IsArchived: true,
},
Tasks: []*models.TaskWithComments{
@ -634,7 +632,7 @@ func TestConvertTodoistToVikunja(t *testing.T) {
},
}
doneItems := make(map[int64]*doneItem)
doneItems := make(map[string]*doneItem)
hierachie, err := convertTodoistToVikunja(testSync, doneItems)
assert.NoError(t, err)
assert.NotNil(t, hierachie)

View File

@ -18,7 +18,7 @@ package trello
import (
"bytes"
"io/ioutil"
"os"
"testing"
"time"
@ -36,7 +36,7 @@ func TestConvertTrelloToVikunja(t *testing.T) {
time1, err := time.Parse(time.RFC3339Nano, "2014-09-26T08:25:05Z")
assert.NoError(t, err)
exampleFile, err := ioutil.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/wunderlist/testimage.jpg")
exampleFile, err := os.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/testimage.jpg")
assert.NoError(t, err)
trelloData := []*trello.Board{

View File

@ -1,512 +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 wunderlist
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/migration"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils"
)
// Migration represents the implementation of the migration for wunderlist
type Migration struct {
// Code is the code used to get a user api token
Code string `query:"code" json:"code"`
}
// This represents all necessary fields for getting an api token for the wunderlist api from a code
type wunderlistAuthRequest struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Code string `json:"code"`
}
type wunderlistAuthToken struct {
AccessToken string `json:"access_token"`
}
type task struct {
AssigneeID int `json:"assignee_id"`
CreatedAt time.Time `json:"created_at"`
CreatedByID int `json:"created_by_id"`
Completed bool `json:"completed"`
CompletedAt time.Time `json:"completed_at"`
DueDate string `json:"due_date"`
ID int `json:"id"`
ListID int `json:"list_id"`
Revision int `json:"revision"`
Starred bool `json:"starred"`
Title string `json:"title"`
}
type list struct {
ID int `json:"id"`
CreatedAt time.Time `json:"created_at"`
Title string `json:"title"`
ListType string `json:"list_type"`
Type string `json:"type"`
Revision int `json:"revision"`
Migrated bool `json:"-"`
}
type folder struct {
ID int `json:"id"`
Title string `json:"title"`
ListIds []int `json:"list_ids"`
CreatedAt time.Time `json:"created_at"`
CreatedByRequestID string `json:"created_by_request_id"`
UpdatedAt time.Time `json:"updated_at"`
Type string `json:"type"`
Revision int `json:"revision"`
}
type note struct {
ID int `json:"id"`
TaskID int `json:"task_id"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Revision int `json:"revision"`
}
type file struct {
ID int `json:"id"`
URL string `json:"url"`
TaskID int `json:"task_id"`
ListID int `json:"list_id"`
UserID int `json:"user_id"`
FileName string `json:"file_name"`
ContentType string `json:"content_type"`
FileSize int `json:"file_size"`
LocalCreatedAt time.Time `json:"local_created_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Type string `json:"type"`
Revision int `json:"revision"`
}
type reminder struct {
ID int `json:"id"`
Date time.Time `json:"date"`
TaskID int `json:"task_id"`
Revision int `json:"revision"`
Type string `json:"type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type subtask struct {
ID int `json:"id"`
TaskID int `json:"task_id"`
CreatedAt time.Time `json:"created_at"`
CreatedByID int `json:"created_by_id"`
Revision int `json:"revision"`
Title string `json:"title"`
}
type wunderlistContents struct {
tasks []*task
lists []*list
folders []*folder
notes []*note
files []*file
reminders []*reminder
subtasks []*subtask
}
func convertListForFolder(listID int, list *list, content *wunderlistContents) (*models.ListWithTasksAndBuckets, error) {
l := &models.ListWithTasksAndBuckets{
List: models.List{
Title: list.Title,
Created: list.CreatedAt,
},
}
// Find all tasks belonging to this list and put them in
for _, t := range content.tasks {
if t.ListID == listID {
newTask := &models.Task{
Title: t.Title,
Created: t.CreatedAt,
Done: t.Completed,
}
// Set Done At
if newTask.Done {
newTask.DoneAt = t.CompletedAt.In(config.GetTimeZone())
}
// Parse the due date
if t.DueDate != "" {
dueDate, err := time.Parse("2006-01-02", t.DueDate)
if err != nil {
return nil, err
}
newTask.DueDate = dueDate.In(config.GetTimeZone())
}
// Find related notes
for _, n := range content.notes {
if n.TaskID == t.ID {
newTask.Description = n.Content
}
}
// Attachments
for _, f := range content.files {
if f.TaskID == t.ID {
// Download the attachment and put it in the file
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, f.URL, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
buf := &bytes.Buffer{}
_, err = buf.ReadFrom(resp.Body)
if err != nil {
return nil, err
}
newTask.Attachments = append(newTask.Attachments, &models.TaskAttachment{
File: &files.File{
Name: f.FileName,
Mime: f.ContentType,
Size: uint64(f.FileSize),
Created: f.CreatedAt,
// We directly pass the file contents here to have a way to link the attachment to the file later.
// Because we don't have an ID for our task at this point of the migration, we cannot just throw all
// attachments in a slice and do the work of downloading and properly storing them later.
FileContent: buf.Bytes(),
},
Created: f.CreatedAt,
})
}
}
// Subtasks
for _, s := range content.subtasks {
if s.TaskID == t.ID {
if newTask.RelatedTasks[models.RelationKindSubtask] == nil {
newTask.RelatedTasks = make(models.RelatedTaskMap)
}
newTask.RelatedTasks[models.RelationKindSubtask] = append(newTask.RelatedTasks[models.RelationKindSubtask], &models.Task{
Title: s.Title,
})
}
}
// Reminders
for _, r := range content.reminders {
if r.TaskID == t.ID {
newTask.Reminders = append(newTask.Reminders, r.Date.In(config.GetTimeZone()))
}
}
l.Tasks = append(l.Tasks, &models.TaskWithComments{Task: *newTask})
}
}
return l, nil
}
func convertWunderlistToVikunja(content *wunderlistContents) (fullVikunjaHierachie []*models.NamespaceWithListsAndTasks, err error) {
// Make a map from the list with the key being list id for easier handling
listMap := make(map[int]*list, len(content.lists))
for _, l := range content.lists {
listMap[l.ID] = l
}
// First, we look through all folders and create namespaces for them.
for _, folder := range content.folders {
namespace := &models.NamespaceWithListsAndTasks{
Namespace: models.Namespace{
Title: folder.Title,
Created: folder.CreatedAt,
Updated: folder.UpdatedAt,
},
}
// Then find all lists for that folder
for _, listID := range folder.ListIds {
if list, exists := listMap[listID]; exists {
l, err := convertListForFolder(listID, list, content)
if err != nil {
return nil, err
}
namespace.Lists = append(namespace.Lists, l)
// And mark the list as migrated so we don't iterate over it again
list.Migrated = true
}
}
// And then finally put the namespace (which now has all the details) back in the full array.
fullVikunjaHierachie = append(fullVikunjaHierachie, namespace)
}
// At the end, loop over all lists which don't belong to a namespace and put them in a default namespace
if len(listMap) > 0 {
newNamespace := &models.NamespaceWithListsAndTasks{
Namespace: models.Namespace{
Title: "Migrated from wunderlist",
},
}
for _, list := range listMap {
if list.Migrated {
continue
}
l, err := convertListForFolder(list.ID, list, content)
if err != nil {
return nil, err
}
newNamespace.Lists = append(newNamespace.Lists, l)
}
fullVikunjaHierachie = append(fullVikunjaHierachie, newNamespace)
}
return
}
func makeAuthGetRequest(token *wunderlistAuthToken, urlPart string, v interface{}, urlParams url.Values) error {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://a.wunderlist.com/api/v1/"+urlPart, nil)
if err != nil {
return err
}
req.Header.Set("X-Access-Token", token.AccessToken)
req.Header.Set("X-Client-ID", config.MigrationWunderlistClientID.GetString())
req.URL.RawQuery = urlParams.Encode()
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
buf := &bytes.Buffer{}
_, err = buf.ReadFrom(resp.Body)
if err != nil {
return err
}
if resp.StatusCode > 399 {
return fmt.Errorf("wunderlist API Error: Status Code: %d, Response was: %s", resp.StatusCode, buf.String())
}
// If the response is an empty json array, we need to exit here, otherwise this breaks the json parser since it
// expects a null for an empty slice
str := buf.String()
if str == "[]" {
return nil
}
return json.Unmarshal(buf.Bytes(), v)
}
// Migrate migrates a user's wunderlist lists, tasks, etc.
// @Summary Migrate all lists, tasks etc. from wunderlist
// @Description Migrates all folders, lists, tasks, notes, reminders, subtasks and files from wunderlist to vikunja.
// @tags migration
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param migrationCode body wunderlist.Migration true "The auth code previously obtained from the auth url. See the docs for /migration/wunderlist/auth."
// @Success 200 {object} models.Message "A message telling you everything was migrated successfully."
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/wunderlist/migrate [post]
func (w *Migration) Migrate(user *user.User) (err error) {
log.Debugf("[Wunderlist migration] Starting wunderlist migration for user %d", user.ID)
// Struct init
wContent := &wunderlistContents{
tasks: []*task{},
lists: []*list{},
folders: []*folder{},
notes: []*note{},
files: []*file{},
reminders: []*reminder{},
subtasks: []*subtask{},
}
// 0. Get api token from oauth user token
authRequest := wunderlistAuthRequest{
ClientID: config.MigrationWunderlistClientID.GetString(),
ClientSecret: config.MigrationWunderlistClientSecret.GetString(),
Code: w.Code,
}
jsonAuth, err := json.Marshal(authRequest)
if err != nil {
return
}
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "https://www.wunderlist.com/oauth/access_token", bytes.NewBuffer(jsonAuth))
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
authToken := &wunderlistAuthToken{}
err = json.NewDecoder(resp.Body).Decode(authToken)
if err != nil {
return
}
log.Debugf("[Wunderlist migration] Start getting all data from wunderlist for user %d", user.ID)
// 1. Get all folders
err = makeAuthGetRequest(authToken, "folders", &wContent.folders, nil)
if err != nil {
return
}
// 2. Get all lists
err = makeAuthGetRequest(authToken, "lists", &wContent.lists, nil)
if err != nil {
return
}
for _, l := range wContent.lists {
listQueryParam := url.Values{"list_id": []string{strconv.Itoa(l.ID)}}
// 3. Get all tasks for each list
tasks := []*task{}
err = makeAuthGetRequest(authToken, "tasks", &tasks, listQueryParam)
if err != nil {
return
}
wContent.tasks = append(wContent.tasks, tasks...)
// 3. Get all done tasks for each list
doneTasks := []*task{}
err = makeAuthGetRequest(authToken, "tasks", &doneTasks, url.Values{"list_id": []string{strconv.Itoa(l.ID)}, "completed": []string{"true"}})
if err != nil {
return
}
wContent.tasks = append(wContent.tasks, doneTasks...)
// 4. Get all notes for all lists
notes := []*note{}
err = makeAuthGetRequest(authToken, "notes", &notes, listQueryParam)
if err != nil {
return
}
wContent.notes = append(wContent.notes, notes...)
// 5. Get all files for all lists
fils := []*file{}
err = makeAuthGetRequest(authToken, "files", &fils, listQueryParam)
if err != nil {
return
}
wContent.files = append(wContent.files, fils...)
// 6. Get all reminders for all lists
reminders := []*reminder{}
err = makeAuthGetRequest(authToken, "reminders", &reminders, listQueryParam)
if err != nil {
return
}
wContent.reminders = append(wContent.reminders, reminders...)
// 7. Get all subtasks for all lists
subtasks := []*subtask{}
err = makeAuthGetRequest(authToken, "subtasks", &subtasks, listQueryParam)
if err != nil {
return
}
wContent.subtasks = append(wContent.subtasks, subtasks...)
}
log.Debugf("[Wunderlist migration] Got all data from wunderlist for user %d", user.ID)
log.Debugf("[Wunderlist migration] Migrating data to vikunja format for user %d", user.ID)
// Convert + Insert everything
fullVikunjaHierachie, err := convertWunderlistToVikunja(wContent)
if err != nil {
return
}
log.Debugf("[Wunderlist migration] Done migrating data to vikunja format for user %d", user.ID)
log.Debugf("[Wunderlist migration] Insert data into db for user %d", user.ID)
err = migration.InsertFromStructure(fullVikunjaHierachie, user)
if err != nil {
return err
}
log.Debugf("[Wunderlist migration] Done inserting data into db for user %d", user.ID)
log.Debugf("[Wunderlist migration] Wunderlist migration for user %d done", user.ID)
return nil
}
// AuthURL returns the url users need to authenticate against
// @Summary Get the auth url from wunderlist
// @Description Returns the auth url where the user needs to get its auth code. This code can then be used to migrate everything from wunderlist to Vikunja.
// @tags migration
// @Produce json
// @Security JWTKeyAuth
// @Success 200 {object} handler.AuthURL "The auth url."
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/wunderlist/auth [get]
func (w *Migration) AuthURL() string {
return "https://www.wunderlist.com/oauth/authorize?client_id=" +
config.MigrationWunderlistClientID.GetString() +
"&redirect_uri=" +
config.MigrationWunderlistRedirectURL.GetString() +
"&state=" + utils.MakeRandomString(32)
}
// Name is used to get the name of the wunderlist migration
// @Summary Get migration status
// @Description Returns if the current user already did the migation or not. This is useful to show a confirmation message in the frontend if the user is trying to do the same migration again.
// @tags migration
// @Produce json
// @Security JWTKeyAuth
// @Success 200 {object} migration.Status "The migration status"
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/wunderlist/status [get]
func (w *Migration) Name() string {
return "wunderlist"
}

View File

@ -1,386 +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 wunderlist
import (
"io/ioutil"
"strconv"
"testing"
"time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/models"
"github.com/stretchr/testify/assert"
"gopkg.in/d4l3k/messagediff.v1"
)
func TestWunderlistParsing(t *testing.T) {
config.InitConfig()
time1, err := time.Parse(time.RFC3339Nano, "2013-08-30T08:29:46.203Z")
assert.NoError(t, err)
time1 = time1.In(config.GetTimeZone())
time2, err := time.Parse(time.RFC3339Nano, "2013-08-30T08:36:13.273Z")
assert.NoError(t, err)
time2 = time2.In(config.GetTimeZone())
time3, err := time.Parse(time.RFC3339Nano, "2013-09-05T08:36:13.273Z")
assert.NoError(t, err)
time3 = time3.In(config.GetTimeZone())
time4, err := time.Parse(time.RFC3339Nano, "2013-08-02T11:58:55Z")
assert.NoError(t, err)
time4 = time4.In(config.GetTimeZone())
exampleFile, err := ioutil.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/wunderlist/testimage.jpg")
assert.NoError(t, err)
createTestTask := func(id, listID int, done bool) *task {
completedAt, err := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:00Z")
assert.NoError(t, err)
if done {
completedAt = time1
}
completedAt = completedAt.In(config.GetTimeZone())
return &task{
ID: id,
AssigneeID: 123,
CreatedAt: time1,
DueDate: "2013-09-05",
ListID: listID,
Title: "Ipsum" + strconv.Itoa(id),
Completed: done,
CompletedAt: completedAt,
}
}
createTestNote := func(id, taskID int) *note {
return &note{
ID: id,
TaskID: taskID,
Content: "Lorem Ipsum dolor sit amet",
CreatedAt: time3,
UpdatedAt: time2,
}
}
fixtures := &wunderlistContents{
folders: []*folder{
{
ID: 123,
Title: "Lorem Ipsum",
ListIds: []int{1, 2, 3, 4},
CreatedAt: time1,
UpdatedAt: time2,
},
},
lists: []*list{
{
ID: 1,
CreatedAt: time1,
Title: "Lorem1",
},
{
ID: 2,
CreatedAt: time1,
Title: "Lorem2",
},
{
ID: 3,
CreatedAt: time1,
Title: "Lorem3",
},
{
ID: 4,
CreatedAt: time1,
Title: "Lorem4",
},
{
ID: 5,
CreatedAt: time4,
Title: "List without a namespace",
},
},
tasks: []*task{
createTestTask(1, 1, false),
createTestTask(2, 1, false),
createTestTask(3, 2, true),
createTestTask(4, 2, false),
createTestTask(5, 3, false),
createTestTask(6, 3, true),
createTestTask(7, 3, true),
createTestTask(8, 3, false),
createTestTask(9, 4, true),
createTestTask(10, 4, true),
},
notes: []*note{
createTestNote(1, 1),
createTestNote(2, 2),
createTestNote(3, 3),
},
files: []*file{
{
ID: 1,
URL: "https://vikunja.io/testimage.jpg", // Using an image which we are hosting, so it'll still be up
TaskID: 1,
ListID: 1,
FileName: "file.md",
ContentType: "text/plain",
FileSize: 12345,
CreatedAt: time2,
UpdatedAt: time4,
},
{
ID: 2,
URL: "https://vikunja.io/testimage.jpg",
TaskID: 3,
ListID: 2,
FileName: "file2.md",
ContentType: "text/plain",
FileSize: 12345,
CreatedAt: time3,
UpdatedAt: time4,
},
},
reminders: []*reminder{
{
ID: 1,
Date: time4,
TaskID: 1,
CreatedAt: time4,
UpdatedAt: time4,
},
{
ID: 2,
Date: time3,
TaskID: 4,
CreatedAt: time3,
UpdatedAt: time3,
},
},
subtasks: []*subtask{
{
ID: 1,
TaskID: 2,
CreatedAt: time4,
Title: "LoremSub1",
},
{
ID: 2,
TaskID: 2,
CreatedAt: time4,
Title: "LoremSub2",
},
{
ID: 3,
TaskID: 4,
CreatedAt: time4,
Title: "LoremSub3",
},
},
}
expectedHierachie := []*models.NamespaceWithListsAndTasks{
{
Namespace: models.Namespace{
Title: "Lorem Ipsum",
Created: time1,
Updated: time2,
},
Lists: []*models.ListWithTasksAndBuckets{
{
List: models.List{
Created: time1,
Title: "Lorem1",
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Ipsum1",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Description: "Lorem Ipsum dolor sit amet",
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "file.md",
Mime: "text/plain",
Size: 12345,
Created: time2,
FileContent: exampleFile,
},
Created: time2,
},
},
Reminders: []time.Time{time4},
},
},
{
Task: models.Task{
Title: "Ipsum2",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Description: "Lorem Ipsum dolor sit amet",
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
Title: "LoremSub1",
},
{
Title: "LoremSub2",
},
},
},
},
},
},
},
{
List: models.List{
Created: time1,
Title: "Lorem2",
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Ipsum3",
Done: true,
DoneAt: time1,
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Description: "Lorem Ipsum dolor sit amet",
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "file2.md",
Mime: "text/plain",
Size: 12345,
Created: time3,
FileContent: exampleFile,
},
Created: time3,
},
},
},
},
{
Task: models.Task{
Title: "Ipsum4",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Reminders: []time.Time{time3},
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
Title: "LoremSub3",
},
},
},
},
},
},
},
{
List: models.List{
Created: time1,
Title: "Lorem3",
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Ipsum5",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
},
},
{
Task: models.Task{
Title: "Ipsum6",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Done: true,
DoneAt: time1,
},
},
{
Task: models.Task{
Title: "Ipsum7",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Done: true,
DoneAt: time1,
},
},
{
Task: models.Task{
Title: "Ipsum8",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
},
},
},
},
{
List: models.List{
Created: time1,
Title: "Lorem4",
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Ipsum9",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Done: true,
DoneAt: time1,
},
},
{
Task: models.Task{
Title: "Ipsum10",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Done: true,
DoneAt: time1,
},
},
},
},
},
},
{
Namespace: models.Namespace{
Title: "Migrated from wunderlist",
},
Lists: []*models.ListWithTasksAndBuckets{
{
List: models.List{
Created: time4,
Title: "List without a namespace",
},
},
},
},
}
hierachie, err := convertWunderlistToVikunja(fixtures)
assert.NoError(t, err)
assert.NotNil(t, hierachie)
if diff, equal := messagediff.PrettyDiff(hierachie, expectedHierachie); !equal {
t.Errorf("converted wunderlist data = %v, want %v, diff: %v", hierachie, expectedHierachie, diff)
}
}

View File

@ -19,19 +19,16 @@ package v1
import (
"net/http"
vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file"
microsofttodo "code.vikunja.io/api/pkg/modules/migration/microsoft-todo"
"code.vikunja.io/api/pkg/modules/migration/trello"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/modules/auth/openid"
microsofttodo "code.vikunja.io/api/pkg/modules/migration/microsoft-todo"
"code.vikunja.io/api/pkg/modules/migration/ticktick"
"code.vikunja.io/api/pkg/modules/migration/todoist"
"code.vikunja.io/api/pkg/modules/migration/wunderlist"
"code.vikunja.io/api/pkg/modules/migration/trello"
vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file"
"code.vikunja.io/api/pkg/version"
"github.com/labstack/echo/v4"
)
@ -97,6 +94,7 @@ func Info(c echo.Context) error {
TaskCommentsEnabled: config.ServiceEnableTaskComments.GetBool(),
AvailableMigrators: []string{
(&vikunja_file.FileMigrator{}).Name(),
(&ticktick.Migrator{}).Name(),
},
Legal: legalInfo{
ImprintURL: config.LegalImprintURL.GetString(),
@ -122,10 +120,6 @@ func Info(c echo.Context) error {
info.AuthInfo.OpenIDConnect.Providers = providers
// Migrators
if config.MigrationWunderlistEnable.GetBool() {
m := &wunderlist.Migration{}
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())
}
if config.MigrationTodoistEnable.GetBool() {
m := &todoist.Migration{}
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())

View File

@ -22,6 +22,7 @@ import (
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/user"
"xorm.io/xorm"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
@ -35,37 +36,47 @@ func BasicAuth(username, password string, c echo.Context) (bool, error) {
Username: username,
Password: password,
}
u, err := user.CheckUserCredentials(s, credentials)
if err != nil && !user.IsErrWrongUsernameOrPassword(err) && !user.IsErrAccountIsNotLocal(err) {
log.Errorf("Error during basic auth for caldav: %v", err)
var err error
u, err := checkUserCaldavTokens(s, credentials)
if user.IsErrUserDoesNotExist(err) {
return false, nil
}
if err == nil {
if u == nil {
u, err = user.CheckUserCredentials(s, credentials)
if err != nil {
log.Errorf("Error during basic auth for caldav: %v", err)
return false, nil
}
}
if u != nil && err == nil {
c.Set("userBasicAuth", u)
return true, nil
}
return false, nil
}
tokens, err := user.GetCaldavTokens(u)
func checkUserCaldavTokens(s *xorm.Session, login *user.Login) (*user.User, error) {
usr, err := user.GetUserByUsername(s, login.Username)
if err != nil || usr == nil {
log.Warningf("Error while retrieving users from database: %v", err)
return nil, err
}
tokens, err := user.GetCaldavTokens(usr)
if err != nil {
log.Errorf("Error while getting tokens for caldav auth: %v", err)
return false, nil
return nil, err
}
// Looping over all tokens until we find one that matches
for _, token := range tokens {
err = bcrypt.CompareHashAndPassword([]byte(token.Token), []byte(password))
err = bcrypt.CompareHashAndPassword([]byte(token.Token), []byte(login.Password))
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
continue
}
log.Errorf("Error while verifying tokens for caldav auth: %v", err)
return false, nil
return nil, nil
}
c.Set("userBasicAuth", u)
return true, nil
return usr, nil
}
return false, nil
return nil, nil
}

View File

@ -19,7 +19,7 @@ package caldav
import (
"bytes"
"fmt"
"io/ioutil"
"io"
"reflect"
"strconv"
"strings"
@ -61,9 +61,9 @@ func ListHandler(c echo.Context) error {
}
// Try to parse a task from the request payload
body, _ := ioutil.ReadAll(c.Request().Body)
body, _ := io.ReadAll(c.Request().Body)
// Restore the io.ReadCloser to its original state
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(body))
c.Request().Body = io.NopCloser(bytes.NewBuffer(body))
// Parse it
vtodo := string(body)
if vtodo != "" && strings.HasPrefix(vtodo, `BEGIN:VCALENDAR`) {
@ -127,9 +127,9 @@ func PrincipalHandler(c echo.Context) error {
}
// Try to parse a task from the request payload
body, _ := ioutil.ReadAll(c.Request().Body)
body, _ := io.ReadAll(c.Request().Body)
// Restore the io.ReadCloser to its original state
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(body))
c.Request().Body = io.NopCloser(bytes.NewBuffer(body))
log.Debugf("[CALDAV] Request Body: %v\n", string(body))
log.Debugf("[CALDAV] Request Headers: %v\n", c.Request().Header)
@ -157,9 +157,9 @@ func EntryHandler(c echo.Context) error {
}
// Try to parse a task from the request payload
body, _ := ioutil.ReadAll(c.Request().Body)
body, _ := io.ReadAll(c.Request().Body)
// Restore the io.ReadCloser to its original state
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(body))
c.Request().Body = io.NopCloser(bytes.NewBuffer(body))
log.Debugf("[CALDAV] Request Body: %v\n", string(body))
log.Debugf("[CALDAV] Request Headers: %v\n", c.Request().Header)

View File

@ -48,15 +48,10 @@ package routes
import (
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/ulule/limiter/v3"
vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
@ -70,9 +65,10 @@ import (
"code.vikunja.io/api/pkg/modules/migration"
migrationHandler "code.vikunja.io/api/pkg/modules/migration/handler"
microsofttodo "code.vikunja.io/api/pkg/modules/migration/microsoft-todo"
"code.vikunja.io/api/pkg/modules/migration/ticktick"
"code.vikunja.io/api/pkg/modules/migration/todoist"
"code.vikunja.io/api/pkg/modules/migration/trello"
"code.vikunja.io/api/pkg/modules/migration/wunderlist"
vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file"
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
"code.vikunja.io/api/pkg/routes/caldav"
_ "code.vikunja.io/api/pkg/swagger" // To generate swagger docs
@ -82,10 +78,11 @@ import (
"github.com/getsentry/sentry-go"
sentryecho "github.com/getsentry/sentry-go/echo"
"github.com/golang-jwt/jwt/v4"
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
elog "github.com/labstack/gommon/log"
"github.com/ulule/limiter/v3"
)
// NewEcho registers a new Echo instance
@ -274,29 +271,8 @@ func registerAPIRoutes(a *echo.Group) {
ur.POST("/shares/:share/auth", apiv1.AuthenticateLinkShare)
}
// ===== Routes with Authetication =====
// Authetification
a.Use(middleware.JWTWithConfig(middleware.JWTConfig{
// Custom parse function to make the middleware work with the github.com/golang-jwt/jwt/v4 package.
// See https://github.com/labstack/echo/pull/1916#issuecomment-878046299
ParseTokenFunc: func(auth string, c echo.Context) (interface{}, error) {
keyFunc := func(t *jwt.Token) (interface{}, error) {
if t.Method.Alg() != "HS256" {
return nil, fmt.Errorf("unexpected jwt signing method=%v", t.Header["alg"])
}
return []byte(config.ServiceJWTSecret.GetString()), nil
}
token, err := jwt.Parse(auth, keyFunc)
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return token, nil
},
}))
// ===== Routes with Authentication =====
a.Use(echojwt.JWT([]byte(config.ServiceJWTSecret.GetString())))
// Rate limit
setupRateLimit(a, config.RateLimitKind.GetString())
@ -613,16 +589,6 @@ func registerAPIRoutes(a *echo.Group) {
}
func registerMigrations(m *echo.Group) {
// Wunderlist
if config.MigrationWunderlistEnable.GetBool() {
wunderlistMigrationHandler := &migrationHandler.MigrationWeb{
MigrationStruct: func() migration.Migrator {
return &wunderlist.Migration{}
},
}
wunderlistMigrationHandler.RegisterRoutes(m)
}
// Todoist
if config.MigrationTodoistEnable.GetBool() {
todoistMigrationHandler := &migrationHandler.MigrationWeb{
@ -653,12 +619,21 @@ func registerMigrations(m *echo.Group) {
microsoftTodoMigrationHandler.RegisterRoutes(m)
}
// Vikunja File Migrator
vikunjaFileMigrationHandler := &migrationHandler.FileMigratorWeb{
MigrationStruct: func() migration.FileMigrator {
return &vikunja_file.FileMigrator{}
},
}
vikunjaFileMigrationHandler.RegisterRoutes(m)
// TickTick File Migrator
tickTickFileMigrator := migrationHandler.FileMigratorWeb{
MigrationStruct: func() migration.FileMigrator {
return &ticktick.Migrator{}
},
}
tickTickFileMigrator.RegisterRoutes(m)
}
func registerCalDavRoutes(c *echo.Group) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -58,30 +58,9 @@ definitions:
value.
type: string
created_by:
$ref: '#/definitions/user.User'
allOf:
- $ref: '#/definitions/user.User'
description: The user who initially created the bucket.
filter_by:
description: The field name of the field to filter by
items:
type: string
type: array
filter_comparator:
description: The comparator for field and value
items:
type: string
type: array
filter_concat:
description: The way all filter conditions are concatenated together, can
be either "and" or "or".,
type: string
filter_include_nulls:
description: If set to true, the result will also include null values
type: boolean
filter_value:
description: The value of the field name to filter by
items:
type: string
type: array
id:
description: The unique, numeric id of this bucket.
type: integer
@ -97,22 +76,10 @@ definitions:
list_id:
description: The list this bucket belongs to.
type: integer
order_by:
description: The query parameter to order the items by. This can be either
asc or desc, with asc being the default.
items:
type: string
type: array
position:
description: The position this bucket has when querying all buckets. See the
tasks.position property on how to use this.
type: number
sort_by:
description: The query parameter to sort by. This is for ex. done, priority,
etc.
items:
type: string
type: array
tasks:
description: All tasks which belong to this bucket.
items:
@ -126,8 +93,6 @@ definitions:
description: A timestamp when this bucket was last updated. You cannot change
this value.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.BulkAssignees:
properties:
@ -136,8 +101,6 @@ definitions:
items:
$ref: '#/definitions/user.User'
type: array
web.CRUDable: {}
web.Rights: {}
type: object
models.BulkTask:
properties:
@ -154,12 +117,17 @@ definitions:
bucket_id:
description: BucketID is the ID of the kanban bucket this task belongs to.
type: integer
cover_image_attachment_id:
description: If this task has a cover image, the field will return the id
of the attachment that is the cover image.
type: integer
created:
description: A timestamp when this task was created. You cannot change this
value.
type: string
created_by:
$ref: '#/definitions/user.User'
allOf:
- $ref: '#/definitions/user.User'
description: The user who initially created the task.
description:
description: The task description.
@ -224,7 +192,8 @@ definitions:
sort by this later.
type: integer
related_tasks:
$ref: '#/definitions/models.RelatedTaskMap'
allOf:
- $ref: '#/definitions/models.RelatedTaskMap'
description: All related tasks, grouped by their relation kind
reminder_dates:
description: An array of datetimes when the user wants to be reminded of the
@ -238,16 +207,18 @@ definitions:
increase all remindes and the due date by its amount.
type: integer
repeat_mode:
allOf:
- $ref: '#/definitions/models.TaskRepeatMode'
description: 'Can have three possible values which will trigger when the task
is marked as done: 0 = repeats after the amount specified in repeat_after,
1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from
the current date rather than the last set date.'
type: integer
start_date:
description: When this task starts.
type: string
subscription:
$ref: '#/definitions/models.Subscription'
allOf:
- $ref: '#/definitions/models.Subscription'
description: |-
The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.
Will only returned when retreiving one task.
@ -264,8 +235,6 @@ definitions:
description: A timestamp when this task was last updated. You cannot change
this value.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.DatabaseNotifications:
properties:
@ -290,8 +259,6 @@ definitions:
description: When this notification is marked as read, this will be updated
with the current timestamp.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.Label:
properties:
@ -300,7 +267,8 @@ definitions:
value.
type: string
created_by:
$ref: '#/definitions/user.User'
allOf:
- $ref: '#/definitions/user.User'
description: The user who created this label
description:
description: The label description.
@ -322,8 +290,6 @@ definitions:
description: A timestamp when this label was last updated. You cannot change
this value.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.LabelTask:
properties:
@ -334,8 +300,6 @@ definitions:
label_id:
description: The label id you want to associate with a task.
type: integer
web.CRUDable: {}
web.Rights: {}
type: object
models.LabelTaskBulk:
properties:
@ -344,8 +308,6 @@ definitions:
items:
$ref: '#/definitions/models.Label'
type: array
web.CRUDable: {}
web.Rights: {}
type: object
models.LinkSharing:
properties:
@ -368,26 +330,27 @@ definitions:
it after the link share has been created.
type: string
right:
allOf:
- $ref: '#/definitions/models.Right'
default: 0
description: The right this list is shared with. 0 = Read only, 1 = Read &
Write, 2 = Admin. See the docs for more details.
maximum: 2
type: integer
shared_by:
$ref: '#/definitions/user.User'
allOf:
- $ref: '#/definitions/user.User'
description: The user who shared this list
sharing_type:
allOf:
- $ref: '#/definitions/models.SharingType'
default: 0
description: The kind of this link. 0 = undefined, 1 = without password, 2
= with password.
maximum: 2
type: integer
updated:
description: A timestamp when this share was last updated. You cannot change
this value.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.List:
properties:
@ -429,14 +392,16 @@ definitions:
namespace_id:
type: integer
owner:
$ref: '#/definitions/user.User'
allOf:
- $ref: '#/definitions/user.User'
description: The user who created this list.
position:
description: The position this list has when querying all lists. See the tasks.position
property on how to use this.
type: number
subscription:
$ref: '#/definitions/models.Subscription'
allOf:
- $ref: '#/definitions/models.Subscription'
description: |-
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.
@ -449,19 +414,16 @@ definitions:
description: A timestamp when this list was last updated. You cannot change
this value.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.ListDuplicate:
properties:
list:
$ref: '#/definitions/models.List'
allOf:
- $ref: '#/definitions/models.List'
description: The copied list
namespace_id:
description: The target namespace ID
type: integer
web.CRUDable: {}
web.Rights: {}
type: object
models.ListUser:
properties:
@ -473,11 +435,12 @@ definitions:
description: The unique, numeric id of this list <-> user relation.
type: integer
right:
allOf:
- $ref: '#/definitions/models.Right'
default: 0
description: The right this user has. 0 = Read only, 1 = Read & Write, 2 =
Admin. See the docs for more details.
maximum: 2
type: integer
updated:
description: A timestamp when this relation was last updated. You cannot change
this value.
@ -485,8 +448,6 @@ definitions:
user_id:
description: The username.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.Message:
properties:
@ -514,10 +475,12 @@ definitions:
description: Whether or not a namespace is archived.
type: boolean
owner:
$ref: '#/definitions/user.User'
allOf:
- $ref: '#/definitions/user.User'
description: The user who owns this namespace
subscription:
$ref: '#/definitions/models.Subscription'
allOf:
- $ref: '#/definitions/models.Subscription'
description: |-
The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.
Will only returned when retreiving one namespace.
@ -530,8 +493,6 @@ definitions:
description: A timestamp when this namespace was last updated. You cannot
change this value.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.NamespaceUser:
properties:
@ -543,11 +504,12 @@ definitions:
description: The unique, numeric id of this namespace <-> user relation.
type: integer
right:
allOf:
- $ref: '#/definitions/models.Right'
default: 0
description: The right this user has. 0 = Read only, 1 = Read & Write, 2 =
Admin. See the docs for more details.
maximum: 2
type: integer
updated:
description: A timestamp when this relation was last updated. You cannot change
this value.
@ -555,8 +517,6 @@ definitions:
user_id:
description: The username.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.NamespaceWithLists:
properties:
@ -582,10 +542,12 @@ definitions:
$ref: '#/definitions/models.List'
type: array
owner:
$ref: '#/definitions/user.User'
allOf:
- $ref: '#/definitions/user.User'
description: The user who owns this namespace
subscription:
$ref: '#/definitions/models.Subscription'
allOf:
- $ref: '#/definitions/models.Subscription'
description: |-
The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.
Will only returned when retreiving one namespace.
@ -598,8 +560,6 @@ definitions:
description: A timestamp when this namespace was last updated. You cannot
change this value.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.RelatedTaskMap:
additionalProperties:
@ -607,6 +567,44 @@ definitions:
$ref: '#/definitions/models.Task'
type: array
type: object
models.RelationKind:
enum:
- unknown
- subtask
- parenttask
- related
- duplicateof
- duplicates
- blocking
- blocked
- precedes
- follows
- copiedfrom
- copiedto
type: string
x-enum-varnames:
- RelationKindUnknown
- RelationKindSubtask
- RelationKindParenttask
- RelationKindRelated
- RelationKindDuplicateOf
- RelationKindDuplicates
- RelationKindBlocking
- RelationKindBlocked
- RelationKindPreceeds
- RelationKindFollows
- RelationKindCopiedFrom
- RelationKindCopiedTo
models.Right:
enum:
- 0
- 1
- 2
type: integer
x-enum-varnames:
- RightRead
- RightWrite
- RightAdmin
models.SavedFilter:
properties:
created:
@ -617,7 +615,8 @@ definitions:
description: The description of the filter
type: string
filters:
$ref: '#/definitions/models.TaskCollection'
allOf:
- $ref: '#/definitions/models.TaskCollection'
description: The actual filters this filter contains
id:
description: The unique numeric id of this saved filter
@ -627,7 +626,8 @@ definitions:
a separate namespace together with favorite lists.
type: boolean
owner:
$ref: '#/definitions/user.User'
allOf:
- $ref: '#/definitions/user.User'
description: The user who owns this filter
title:
description: The title of the filter.
@ -638,9 +638,17 @@ definitions:
description: A timestamp when this filter was last updated. You cannot change
this value.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.SharingType:
enum:
- 0
- 1
- 2
type: integer
x-enum-varnames:
- SharingTypeUnknown
- SharingTypeWithoutPassword
- SharingTypeWithPassword
models.Subscription:
properties:
created:
@ -656,10 +664,9 @@ definitions:
description: The numeric ID of the subscription
type: integer
user:
$ref: '#/definitions/user.User'
allOf:
- $ref: '#/definitions/user.User'
description: The user who made this subscription
web.CRUDable: {}
web.Rights: {}
type: object
models.Task:
properties:
@ -676,12 +683,17 @@ definitions:
bucket_id:
description: BucketID is the ID of the kanban bucket this task belongs to.
type: integer
cover_image_attachment_id:
description: If this task has a cover image, the field will return the id
of the attachment that is the cover image.
type: integer
created:
description: A timestamp when this task was created. You cannot change this
value.
type: string
created_by:
$ref: '#/definitions/user.User'
allOf:
- $ref: '#/definitions/user.User'
description: The user who initially created the task.
description:
description: The task description.
@ -746,7 +758,8 @@ definitions:
sort by this later.
type: integer
related_tasks:
$ref: '#/definitions/models.RelatedTaskMap'
allOf:
- $ref: '#/definitions/models.RelatedTaskMap'
description: All related tasks, grouped by their relation kind
reminder_dates:
description: An array of datetimes when the user wants to be reminded of the
@ -760,16 +773,18 @@ definitions:
increase all remindes and the due date by its amount.
type: integer
repeat_mode:
allOf:
- $ref: '#/definitions/models.TaskRepeatMode'
description: 'Can have three possible values which will trigger when the task
is marked as done: 0 = repeats after the amount specified in repeat_after,
1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from
the current date rather than the last set date.'
type: integer
start_date:
description: When this task starts.
type: string
subscription:
$ref: '#/definitions/models.Subscription'
allOf:
- $ref: '#/definitions/models.Subscription'
description: |-
The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.
Will only returned when retreiving one task.
@ -781,8 +796,6 @@ definitions:
description: A timestamp when this task was last updated. You cannot change
this value.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.TaskAssginee:
properties:
@ -790,8 +803,6 @@ definitions:
type: string
user_id:
type: integer
web.CRUDable: {}
web.Rights: {}
type: object
models.TaskAttachment:
properties:
@ -805,8 +816,6 @@ definitions:
type: integer
task_id:
type: integer
web.CRUDable: {}
web.Rights: {}
type: object
models.TaskCollection:
properties:
@ -844,8 +853,6 @@ definitions:
items:
type: string
type: array
web.CRUDable: {}
web.Rights: {}
type: object
models.TaskComment:
properties:
@ -859,8 +866,6 @@ definitions:
type: integer
updated:
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.TaskRelation:
properties:
@ -869,20 +874,30 @@ definitions:
value.
type: string
created_by:
$ref: '#/definitions/user.User'
allOf:
- $ref: '#/definitions/user.User'
description: The user who created this relation
other_task_id:
description: The ID of the other task, the task which is being related.
type: integer
relation_kind:
allOf:
- $ref: '#/definitions/models.RelationKind'
description: The kind of the relation.
type: string
task_id:
description: The ID of the "base" task, the task which has a relation to another.
type: integer
web.CRUDable: {}
web.Rights: {}
type: object
models.TaskRepeatMode:
enum:
- 0
- 1
- 2
type: integer
x-enum-varnames:
- TaskRepeatModeDefault
- TaskRepeatModeMonth
- TaskRepeatModeFromCurrentDate
models.Team:
properties:
created:
@ -890,7 +905,8 @@ definitions:
this value.
type: string
created_by:
$ref: '#/definitions/user.User'
allOf:
- $ref: '#/definitions/user.User'
description: The user who created this team.
description:
description: The team's description.
@ -912,8 +928,6 @@ definitions:
description: A timestamp when this relation was last updated. You cannot change
this value.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.TeamList:
properties:
@ -925,11 +939,12 @@ definitions:
description: The unique, numeric id of this list <-> team relation.
type: integer
right:
allOf:
- $ref: '#/definitions/models.Right'
default: 0
description: The right this team has. 0 = Read only, 1 = Read & Write, 2 =
Admin. See the docs for more details.
maximum: 2
type: integer
team_id:
description: The team id.
type: integer
@ -937,8 +952,6 @@ definitions:
description: A timestamp when this relation was last updated. You cannot change
this value.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.TeamMember:
properties:
@ -957,8 +970,6 @@ definitions:
description: The username of the member. We use this to prevent automated
user id entering.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.TeamNamespace:
properties:
@ -970,11 +981,12 @@ definitions:
description: The unique, numeric id of this namespace <-> team relation.
type: integer
right:
allOf:
- $ref: '#/definitions/models.Right'
default: 0
description: The right this team has. 0 = Read only, 1 = Read & Write, 2 =
Admin. See the docs for more details.
maximum: 2
type: integer
team_id:
description: The team id.
type: integer
@ -982,8 +994,6 @@ definitions:
description: A timestamp when this relation was last updated. You cannot change
this value.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.TeamUser:
properties:
@ -1014,7 +1024,6 @@ definitions:
maxLength: 250
minLength: 1
type: string
web.Auth: {}
type: object
models.TeamWithRight:
properties:
@ -1023,7 +1032,8 @@ definitions:
this value.
type: string
created_by:
$ref: '#/definitions/user.User'
allOf:
- $ref: '#/definitions/user.User'
description: The user who created this team.
description:
description: The team's description.
@ -1042,16 +1052,11 @@ definitions:
minLength: 1
type: string
right:
default: 0
description: The right this team has. 0 = Read only, 1 = Read & Write, 2 =
Admin. See the docs for more details.
type: integer
$ref: '#/definitions/models.Right'
updated:
description: A timestamp when this relation was last updated. You cannot change
this value.
type: string
web.CRUDable: {}
web.Rights: {}
type: object
models.UserWithRight:
properties:
@ -1070,10 +1075,7 @@ definitions:
description: The full name of the user.
type: string
right:
default: 0
description: The right this user has. 0 = Read only, 1 = Read & Write, 2 =
Admin. See the docs for more details.
type: integer
$ref: '#/definitions/models.Right'
updated:
description: A timestamp when this task was last updated. You cannot change
this value.
@ -1083,7 +1085,6 @@ definitions:
maxLength: 250
minLength: 1
type: string
web.Auth: {}
type: object
notifications.DatabaseNotification:
properties:
@ -1119,6 +1120,8 @@ definitions:
type: string
key:
type: string
logout_url:
type: string
name:
type: string
type: object
@ -1250,7 +1253,6 @@ definitions:
maxLength: 250
minLength: 1
type: string
web.Auth: {}
type: object
v1.LinkShareAuth:
properties:
@ -1399,12 +1401,6 @@ definitions:
message:
type: string
type: object
wunderlist.Migration:
properties:
code:
description: Code is the code used to get a user api token
type: string
type: object
info:
contact:
email: hello@vikunja.io
@ -1501,7 +1497,7 @@ paths:
parameters:
- description: Unsplash Image ID
in: path
name: thumb
name: image
required: true
type: integer
produces:
@ -1531,7 +1527,7 @@ paths:
parameters:
- description: Unsplash Image ID
in: path
name: thumb
name: image
required: true
type: integer
produces:
@ -3242,6 +3238,55 @@ paths:
summary: Get migration status
tags:
- migration
/migration/ticktick/migrate:
post:
consumes:
- application/json
description: Imports all projects, tasks, notes, reminders, subtasks and files
from a TickTick backup export into Vikunja.
parameters:
- description: The TickTick backup csv file.
in: formData
name: import
required: true
type: string
produces:
- application/json
responses:
"200":
description: A message telling you everything was migrated successfully.
schema:
$ref: '#/definitions/models.Message'
"500":
description: Internal server error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Import all lists, tasks etc. from a TickTick backup export
tags:
- migration
/migration/ticktick/status:
get:
description: Returns if the current user already did the migation or not. This
is useful to show a confirmation message in the frontend if the user is trying
to do the same migration again.
produces:
- application/json
responses:
"200":
description: The migration status
schema:
$ref: '#/definitions/migration.Status'
"500":
description: Internal server error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get migration status
tags:
- migration
/migration/todoist/auth:
get:
description: Returns the auth url where the user needs to get its auth code.
@ -3433,77 +3478,6 @@ paths:
summary: Get migration status
tags:
- migration
/migration/wunderlist/auth:
get:
description: Returns the auth url where the user needs to get its auth code.
This code can then be used to migrate everything from wunderlist to Vikunja.
produces:
- application/json
responses:
"200":
description: The auth url.
schema:
$ref: '#/definitions/handler.AuthURL'
"500":
description: Internal server error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get the auth url from wunderlist
tags:
- migration
/migration/wunderlist/migrate:
post:
consumes:
- application/json
description: Migrates all folders, lists, tasks, notes, reminders, subtasks
and files from wunderlist to vikunja.
parameters:
- description: The auth code previously obtained from the auth url. See the
docs for /migration/wunderlist/auth.
in: body
name: migrationCode
required: true
schema:
$ref: '#/definitions/wunderlist.Migration'
produces:
- application/json
responses:
"200":
description: A message telling you everything was migrated successfully.
schema:
$ref: '#/definitions/models.Message'
"500":
description: Internal server error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Migrate all lists, tasks etc. from wunderlist
tags:
- migration
/migration/wunderlist/status:
get:
description: Returns if the current user already did the migation or not. This
is useful to show a confirmation message in the frontend if the user is trying
to do the same migration again.
produces:
- application/json
responses:
"200":
description: The migration status
schema:
$ref: '#/definitions/migration.Status'
"500":
description: Internal server error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get migration status
tags:
- migration
/namespace/{id}:
post:
consumes:
@ -5190,7 +5164,7 @@ paths:
type: string
- description: The id of the other task.
in: path
name: otherTaskID
name: otherTaskId
required: true
type: integer
produces:
@ -5546,7 +5520,8 @@ paths:
/teams/{id}/members/{userID}:
delete:
description: Remove a user from a team. This will also revoke any access this
user might have via that team.
user might have via that team. A user can remove themselves from the team
if they are not the last user in the team.
parameters:
- description: Team ID
in: path

View File

@ -54,7 +54,17 @@ func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) {
}
user.Status = StatusActive
user.AvatarProvider = "initials"
user.AvatarProvider = config.DefaultSettingsAvatarProvider.GetString()
user.AvatarFileID = config.DefaultSettingsAvatarFileID.GetInt64()
user.EmailRemindersEnabled = config.DefaultSettingsEmailRemindersEnabled.GetBool()
user.DiscoverableByName = config.DefaultSettingsDiscoverableByName.GetBool()
user.DiscoverableByEmail = config.DefaultSettingsDiscoverableByEmail.GetBool()
user.OverdueTasksRemindersEnabled = config.DefaultSettingsOverdueTaskRemindersEnabled.GetBool()
user.OverdueTasksRemindersTime = config.DefaultSettingsOverdueTaskRemindersTime.GetString()
user.DefaultListID = config.DefaultSettingsDefaultListID.GetInt64()
user.WeekStart = config.DefaultSettingsWeekStart.GetInt()
user.Language = config.DefaultSettingsLanguage.GetString()
user.Timezone = config.DefaultSettingsTimezone.GetString()
// Insert it
_, err = s.Insert(user)

View File

@ -369,6 +369,16 @@ func TestListUsers(t *testing.T) {
assert.True(t, len(all) > 0)
assert.Equal(t, all[0].Username, "user1")
})
t.Run("case insensitive", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "uSEr1", nil)
assert.NoError(t, err)
assert.True(t, len(all) > 0)
assert.Equal(t, all[0].Username, "user1")
})
t.Run("all users", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()

View File

@ -19,8 +19,11 @@ package user
import (
"strings"
"code.vikunja.io/api/pkg/db"
"xorm.io/builder"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
type ListUserOpts struct {
@ -45,14 +48,22 @@ func ListUsers(s *xorm.Session, search string, opts *ListUserOpts) (users []*Use
if search != "" {
for _, queryPart := range strings.Split(search, ",") {
var usernameCond builder.Cond = builder.Eq{"username": queryPart}
if db.Type() == schemas.POSTGRES {
usernameCond = builder.Expr("username ILIKE ?", queryPart)
}
if db.Type() == schemas.SQLITE {
usernameCond = builder.Expr("username = ? COLLATE NOCASE", queryPart)
}
conds = append(conds,
builder.Eq{"username": queryPart},
usernameCond,
builder.And(
builder.Eq{"email": queryPart},
builder.Eq{"discoverable_by_email": true},
),
builder.And(
builder.Like{"name", "%" + queryPart + "%"},
db.ILIKE("name", queryPart),
builder.Eq{"discoverable_by_name": true},
),
)